UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

102 lines (99 loc) • 4.08 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.llmExtractor = void 0; const abort_js_1 = require("../../lib/abort.js"); const types_js_1 = require("./types.js"); const DEFAULT_SYSTEM_PROMPT = `You are an extractor that distills a single turn of a conversation into narrative beats for long-term memory. A "beat" is a one-sentence, self-contained summary of something salient that happened this turn — a fact the user revealed, a decision the agent made, an important question, a result returned. Return JSON in this exact shape: { "beats": [ { "summary": "one sentence", "importance": 0.0_to_1.0, "refs": ["msg-<turn>-<index>", ...], "category": "identity|preference|fact|task|question|tool-result|other" } ] } Guidelines: - Importance 0.9+ for identity, strong preferences, commitments. - Importance 0.5-0.7 for questions, task progress. - Importance 0.3 or lower for low-salience tool chatter. - Return [] if nothing salient happened. - Return ONLY the JSON object — no prose, no code fences.`; /** Build a stable ref id matching heuristicExtractor's convention. */ function refId(turnNumber, index) { return `msg-${turnNumber}-${index}`; } /** Serialize messages for the extractor LLM's user prompt. */ function formatMessagesForExtractor(messages, turnNumber) { const lines = [`Turn ${turnNumber}:`]; for (let i = 0; i < messages.length; i++) { const m = messages[i]; if (m.role === 'system') continue; const ref = refId(turnNumber, i); lines.push(`[${ref}] ${m.role}: ${m.content}`); } return lines.join('\n'); } /** * Parse the extractor's raw JSON response into validated beats. * Returns an empty array on any parse / shape failure — the `onParseError` * callback fires so consumers can observe failures without crashing turns. */ function parseBeatsResponse(raw, onParseError) { try { const parsed = JSON.parse(raw); const rawBeats = (parsed?.beats ?? []); if (!Array.isArray(rawBeats)) return []; const beats = []; for (const rb of rawBeats) { if (!rb || typeof rb !== 'object') continue; const b = rb; if (typeof b.summary !== 'string' || b.summary.length === 0) continue; const refs = Array.isArray(b.refs) ? b.refs.filter((r) => typeof r === 'string') : []; const category = typeof b.category === 'string' ? b.category : undefined; beats.push({ summary: b.summary, importance: (0, types_js_1.asImportance)(b.importance), refs, ...(category ? { category } : {}), }); } return beats; } catch (err) { onParseError(err, raw); return []; } } function llmExtractor(config) { const { provider } = config; const systemPrompt = config.systemPrompt ?? DEFAULT_SYSTEM_PROMPT; const onParseError = config.onParseError ?? ((err, raw) => { // eslint-disable-next-line no-console console.warn('[agentfootprint] llmExtractor: failed to parse LLM response — returning no beats', { error: err, rawPreview: raw.slice(0, 200) }); }); return { async extract(args) { const userContent = formatMessagesForExtractor(args.messages, args.turnNumber); // The run's signal rides the request and bounds the wait; an answer // that lands after it is refused (lib/abort.ts · callUnder). const request = { systemPrompt, messages: [{ role: 'user', content: userContent }], model: 'memory-extractor', }; const response = await (0, abort_js_1.callUnder)(request, args.signal, (wired) => provider.complete(wired)); return parseBeatsResponse(response.content ?? '', onParseError); }, }; } exports.llmExtractor = llmExtractor; //# sourceMappingURL=llmExtractor.js.map