UNPKG

agentfootprint

Version:

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

137 lines • 6.26 kB
"use strict"; /** * loadSnapshot — read-side stage for Causal memory. * * Embeds the user's current question, searches the store for the most * similar past run, projects the snapshot per `SnapshotProjection`, * and writes the formatted result to `scope.formatted` so the * downstream slot subflow injects it as a system message. * * Reads from scope: `identity`, `messages` (or `newMessages` fallback) * Writes to scope: `formatted` — array of `LLMMessage` to inject * * Strict-threshold semantics: * When `minScore` is set and no past snapshot meets it, returns an * empty `formatted`. NO fallback — garbage past context is worse than * no context. * * Empty-query handling: * No user message → no embedding → no search → empty result. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.loadSnapshot = void 0; const define_types_js_1 = require("../define.types.js"); function defaultQueryFrom(scope) { const scopeAny = scope; const incoming = scopeAny.messages ?? []; const source = incoming.length > 0 ? incoming : scope.newMessages ?? []; for (let i = source.length - 1; i >= 0; i--) { const m = source[i]; if (m.role === 'user' && m.content) return String(m.content); } return ''; } // LENS · system-text · request-ephemeral // reads: the nearest past run ← store.search() under the run identity, projected per SnapshotProjection // below minScore it returns EMPTY rather than a weaker match — an honest omission, not a degraded claim. // law: may omit, never deny; every clause anchored to the call it was composed on. function loadSnapshot(config) { const { store, embedder } = config; if (!store.search) { throw new Error('loadSnapshot: the configured store does not implement search(). ' + 'Causal memory requires a vector-capable adapter (InMemoryStore, pgvector, ...).'); } const queryFrom = config.queryFrom ?? defaultQueryFrom; const topK = config.topK ?? 1; const projection = config.projection ?? define_types_js_1.SNAPSHOT_PROJECTIONS.DECISIONS; const minScore = config.minScore ?? 0.7; return async (scope) => { const identity = scope.identity; const text = queryFrom(scope).trim(); if (text.length === 0) { scope.formatted = []; return; } const signal = scope.$getEnv?.()?.signal; const queryVec = (await embedder.embed({ text, ...(signal ? { signal } : {}), })); // store.search optional on MemoryStore but required when an embedder // is configured (validated upstream by defineMemory). // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const results = await store.search(identity, queryVec, { k: topK, minScore, ...(config.embedderId !== undefined && { embedderId: config.embedderId }), }); if (results.length === 0) { // Strict threshold: no match → no injection. Garbage > none is wrong. scope.formatted = []; return; } const messages = results.map((r) => formatProjection(r.entry, projection, r.score)); scope.formatted = messages; }; } exports.loadSnapshot = loadSnapshot; /** * Render one snapshot into a `system` message per the chosen * projection. The shape is intentionally compact so multiple * snapshots fit comfortably in context. */ function formatProjection(entry, projection, score) { const snap = entry.value; const header = `[Past run · query: "${truncate(snap.query, 80)}" · score: ${score.toFixed(2)}]`; let body; switch (projection) { case define_types_js_1.SNAPSHOT_PROJECTIONS.DECISIONS: { // Tool evidence is part of the "why": in LLM-decided flows the // operator-level facts (creditScore=580, dti=0.45) arrive as tool // results, so the decisions projection includes them. // SECURITY: this replays STORED TOOL OUTPUT into a future prompt — a // persisted prompt-injection surface if tools ingest untrusted content. // Treat snapshot stores as prompt-trusted; sanitize tool output at the // tool boundary when it carries third-party text. const toolLines = snap.toolCalls.length === 0 ? '' : `\nTool evidence:\n${snap.toolCalls .map((t) => `- ${t.name}(${JSON.stringify(t.args)}) → ${t.resultPreview}` + (t.errored ? ' [ERROR]' : '')) .join('\n')}`; body = snap.decisions.length === 0 && snap.toolCalls.length === 0 ? `(no decision evidence captured)\nFinal answer: ${snap.finalContent}` : `${snap.decisions .map((d) => `- ${d.stageId} → "${d.chosen}"${d.rule ? ` (rule: ${d.rule})` : ''}` + (d.evidence ? `; evidence: ${JSON.stringify(d.evidence)}` : '')) .join('\n')}${toolLines}\nFinal answer: ${snap.finalContent}`; break; } case define_types_js_1.SNAPSHOT_PROJECTIONS.NARRATIVE: body = snap.narrative ?? `(no narrative captured)\nFinal answer: ${snap.finalContent}`; break; case define_types_js_1.SNAPSHOT_PROJECTIONS.COMMITS: // commitLog isn't yet captured in SnapshotEntry; project the // decisions list as a stand-in for now. body = snap.decisions.length === 0 ? `(no commit log captured)\nFinal answer: ${snap.finalContent}` : snap.decisions.map((d) => `${d.stageId}: chose "${d.chosen}"`).join('\n'); break; case define_types_js_1.SNAPSHOT_PROJECTIONS.FULL: body = JSON.stringify(snap, null, 2); break; default: body = `Final answer: ${snap.finalContent}`; } return { role: 'system', content: `${header}\n${body}`, }; } function truncate(s, n) { return s.length > n ? `${s.slice(0, n - 1)}…` : s; } //# sourceMappingURL=loadSnapshot.js.map