UNPKG

agentfootprint

Version:

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

349 lines • 15.6 kB
"use strict"; /** * conversationRedaction — the names an agent's record carries the * conversation under, as one footprintjs `RedactionPolicy`. * * Pattern: a vocabulary the library owns, handed out as data (a policy), with * the caller's own selectors joined on (`unionRedactionPolicies`). * Role: the Map half of `src/redaction/` for the question "what do I name * to keep the conversation out?" — `Agent.create({ redact })` is the * one door; this is the value most callers hand it. * * WHY THE LIBRARY OWNS THE LIST. footprintjs selects by NAME: a policy keeps a * value out of every record when it names the state key, or the event field * (at any depth), the value travels under. An agent's record carries the * conversation under many names, nearly all of them the library's own — the * person's message, the model's words and thinking, tool arguments and * results, what memory and retrieval recalled, what was injected into the * model's context, a paused call's arguments, the draft a schema check * rejected. A caller cannot be expected to know them; the library can, and * `test/redaction/agent-redaction.vocabulary.test.ts` runs each feature listed * in `CONVERSATION_FEATURES` with a canary in every place the conversation * goes, and fails when a canary reaches any record. * * WHAT IS NOT ON IT, by design: * - values an app's own tools or rules put in the record under names of * their own (a tool's result is covered by `result`, but a field inside an * event your middleware emits is yours to name) — join them: * `conversationRedaction({ patterns: [/ssn|email/i] })`; * - error text written by code (`error`, `errorMessage`, `lastError`): a * message a tool or provider throws can quote what it failed on, and * `error` also names a flag the record's readers count on * (`stream.tool_end`'s `error: true`). On an EVENT it is kept out anyway, * under ANY policy: the value-kind rule serves every string that is not a * library word as the placeholder (`knownStrings.ts`) — a parser message * that quotes the model's draft included. In STATE the name is yours to * add; * - structure: counts, kinds, flags, timings and verdict words stay * readable — the record still shows WHAT happened, without the words; * - content the library quotes under names of its own (a validation issue's * quoted argument, a check-in's evidence pack, a matcher's witness, a * route guard's judged result, the words of a tool's coverage declaration * and a described result's data on their events, …) is not named here: * generic names (`value`, `text`, `note`, `checked`) would hide structure * across every event. On events it is kept out under ANY policy by the * value-kind rule (`knownStrings.ts`): no string survives that is not a * library word. * * Names it shares with structure, kept out with it: `permission.check`'s * `result` (its verdict word — a refused call still reads as refused, from * `stream.tool_end`'s `notExecuted`). */ Object.defineProperty(exports, "__esModule", { value: true }); exports.keepsConversationOut = exports.conversationRedaction = exports.withCurrentVocabulary = exports.carriedRunPolicy = exports.VOCABULARY_VERSION = exports.CONVERSATION_FEATURES = void 0; const advanced_1 = require("footprintjs/advanced"); const policy_js_1 = require("./policy.js"); /** The features whose records the vocabulary is proven against, by the test above. */ exports.CONVERSATION_FEATURES = Object.freeze([ 'the turn (message, answer, history, streamed tokens)', 'tools (arguments, results, tool-result rules, a paused call)', 'model thinking', 'asking a person (askHuman) and resuming', 'the inputs layer (an argument the person is asked for)', 'structured output (schema retries, the output fallback)', 'memory (episodic, semantic, summarize, top-k) and retrieval (RAG)', 'the evidence gate (names and numbers)', 'code in an answer (a script submitted, shown and attached)', 'a tool’s declared coverage and described results (absent, coverage, describedResult)', 'compaction (the window folding the conversation)', 'a person approving a call (check-in)', 'skill graphs (routing on the person’s words)', 'compositions (Sequence, Parallel, Conditional, Loop, Graph)', ]); /** * The state keys and event fields the conversation travels under, grouped by * where it comes from. A key names a state key AND an event field of that * name at any depth (footprintjs's by-name law). */ const NAMES = { /** The turn itself: what the person wrote, what the model wrote, the window. */ turn: [ 'message', 'messages', 'userMessage', 'userPrompt', 'history', 'newMessages', 'content', 'llmLatestContent', 'llmLatestToolCalls', 'finalContent', 'answer', 'response', 'answerNegatives', 'assembledSystem', 'assembledMessages', ], /** The window: what a compaction folded away, kept word for word beside its summary. */ window: ['foldedSpans'], /** What the model was given: injected context, the assembled system prompt. */ context: [ 'rawContent', 'contentSummary', 'systemPromptText', 'systemPromptInjections', 'messagesInjections', 'activeInjections', 'resolvedInstructions', 'slotCompositions', 'droppedSummaries', ], /** The model's thinking. */ thinking: ['thinkingBlocks', 'rawThinking', 'blocks'], /** Tool calls: what they took, what they returned, what a rule saw. */ tools: [ 'args', 'result', 'modelResult', 'lastToolResult', 'toolResults', 'toolCalls', 'middlewareDecisions', 'pausedToolArgs', 'pausedAskArgs', 'pausedCheckInArgs', 'pausedCredentialArgs', // A code-running call that asked from inside `execute`: its program as // proposed and as handed to the tool, carried to the row that settles it. 'pausedScriptPrograms', // A consent request's record: its nonce lets whoever holds it confirm a // guess of the paused call's arguments against the request id // (`core/consentRequest.ts`), so it is kept out with them. 'pausedRequest', 'policyHaltArgs', 'claimFacts', // The findings ledger — and with it every `script` row's `text`, the program // a code-running call submitted (`core/agent/scripts/`). The row's field is // kept out by THIS name: `text` itself is too generic to name (it would hide // structure on every event), and an attached script's `text` on // `turn_end.scripts` is a model-written string the value-kind rule already // serves as the placeholder. 'findingsLedger', 'findingsRequest', // The opt-in verifier's private evidence/selected executed scope. Resume // checkpoints remain private; exported records must not disclose this bag. 'claimVerification', 'negativeClaimState', // The inputs layer's raw answers, held in working state until the calls run // (`core/agent/arguments/README.md`). 'argumentAsk', 'argumentAnswersKept', 'argumentResolutions', // The name of the tool the model called last, copied into the cache // decision's state — a name the model may have made up. 'lastToolName', ], /** * What a tool declared it checked and did not, as state: words the tool * composes from its call and its result (`absent()`'s own example quotes its * arguments) — every declaration of the run, and a typed answer's limits. */ coverage: ['coverageDeclared', 'answerCoverage'], /** A person asked mid-run, and their reply. */ pause: ['questionPayload', 'resumeInput'], /** Checks over the answer: the draft a schema rejected, the values a gate flagged. */ checks: [ 'rawOutput', 'rawOutputPreview', 'primaryErrorMessage', 'fallbackErrorMessage', 'outputAttempts', 'outputSchemaFailure', 'outputContractUnmet', 'reliabilityFailCauseMessage', 'unsupported', 'unsupportedValues', 'evidenceRecovery', 'evidence', 'explanation', // The values a guarding evidence gate found unsupported in the draft. 'evidenceUnsupported', ], /** Memory and retrieval: what was loaded, chosen, formatted and written. */ memory: [ 'loaded', 'selected', 'formatted', 'retrieved', 'newFacts', 'loadedFacts', 'newBeats', 'loadedBeats', 'newMessageEmbeddings', 'runEvidence', 'scoreEvidence', ], /** Mounted maps: the engagement standing quotes the words that founded it. */ maps: ['mapEngagement', 'nextMapEngagement'], /** Compositions: what one member hands the next. */ compositions: [ 'current', 'results', 'branchResults', 'graphInput', 'routerInput', 'handoffMessage', 'echoedMessage', 'routingReason', 'packed', 'resultSummary', ], }; /** * Keys named per memory or retrieval definition — `memoryInjection_<id>`, * `retrievalEvidence_<id>` (`memory/define.types.ts` · `memoryInjectionKey`, * `retrievalEvidenceKey`) — matched by their prefix. */ const PATTERN_PREFIXES = ['memoryInjection_', 'retrievalEvidence_']; const PATTERNS = PATTERN_PREFIXES.map((prefix) => new RegExp(`^${prefix}`)); /** * State keys whose STRUCTURE stays readable while one field quotes the * conversation: the turn's routing verdict keeps its decision and drops the * words a matcher found (`fields` — footprintjs's per-key field selector). */ const FIELDS = { turnRoute: ['witness'], // The injection engine's observability summary keeps its counts; the name of // the tool the model called last may be one the model made up. injectionContextSummary: ['lastToolName'], // The answer check's tool-call ledger keeps `complete`; the calls carry tool // names, which the model may have made up (`lastToolName`'s reasoning). toolCallLedger: ['calls', 'inFlight'], }; /** The vocabulary as one frozen policy. */ const CONVERSATION = Object.freeze({ keys: Object.freeze(Object.values(NAMES).flat()), patterns: Object.freeze([...PATTERNS]), fields: Object.freeze(Object.fromEntries(Object.entries(FIELDS).map(([key, paths]) => [key, [...paths]]))), }); /** * The vocabulary's VERSION — derived from its own contents (every key, pattern * and field selector, sorted), so it changes whenever the list does and never * waits on a hand bump. A carried policy that keeps the conversation out * records the version it was built under (`carriedRunPolicy`); a resumed leg * whose record names another version is covered by the CURRENT vocabulary as * well (`withCurrentVocabulary`) — fail closed, never the older list alone. */ exports.VOCABULARY_VERSION = versionOf(CONVERSATION); /** A policy's selectors as one identity — FNV-1a over the sorted list; an identity, not a secret. */ function versionOf(policy) { const parts = [ ...[...(policy.keys ?? [])].sort().map((key) => `k:${key}`), ...(policy.patterns ?? []).map((p) => `p:${p.flags}/${p.source}`).sort(), ...Object.entries(policy.fields ?? {}) .map(([key, paths]) => `f:${key}=${[...paths].sort().join(',')}`) .sort(), ].join('\n'); let hash = 0x811c9dc5; for (let i = 0; i < parts.length; i += 1) { hash ^= parts.charCodeAt(i); hash = Math.imul(hash, 0x01000193) >>> 0; } return `v1-${hash.toString(16).padStart(8, '0')}`; } /** * `policy` as a run's state carries it (`AgentState.runRedaction`) — with the * vocabulary version it was built under when it keeps the conversation out. * * @internal */ function carriedRunPolicy(policy) { const carried = (0, policy_js_1.carriedRedactionPolicy)(policy); return keepsConversationOut(policy) ? { ...carried, vocabulary: exports.VOCABULARY_VERSION } : carried; } exports.carriedRunPolicy = carriedRunPolicy; /** * A carried policy as a resumed leg is covered by it: joined with the CURRENT * vocabulary when its record names another version (`version`, read off the * checkpoint). An older list lacks the names an upgrade added — names that * keep the conversation out of the leg's STATE, its snapshot and its commit * log. Joining the current list keeps the leg at least as covered as the run * that paused. Same version, or none: the policy as carried. * * @internal */ function withCurrentVocabulary(policy, version) { if (version === undefined || version === exports.VOCABULARY_VERSION) return policy; return (0, policy_js_1.unionRedactionPolicies)(policy, CONVERSATION); } exports.withCurrentVocabulary = withCurrentVocabulary; /** * The policy that keeps an agent's conversation out of every record it * retains or serves — joined with `extra`, the names your own app adds. * * Hand it to the one door, `Agent.create({ redact })`. What it covers, what it * leaves readable and what it does not name are in this file's header and in * `src/redaction/README.md`. * * @param extra your own selectors (`keys`, `patterns`, `fields`, * `emitPatterns`, `diagnostics`), validated like `redact` * itself; joined, never replacing the library's names. * @returns a frozen `RedactionPolicy`. * * @example * ```ts * import { Agent } from 'agentfootprint'; * import { conversationRedaction } from 'agentfootprint/security'; * * const agent = Agent.create({ * provider, * model: 'claude-sonnet-4-5', * redact: conversationRedaction({ patterns: [/ssn|email|phone/i] }), * }).build(); * ``` */ function conversationRedaction(extra) { if (extra === undefined) return CONVERSATION; (0, policy_js_1.assertRedactionPolicy)(extra, 'conversationRedaction'); return (0, policy_js_1.unionRedactionPolicies)(CONVERSATION, extra); } exports.conversationRedaction = conversationRedaction; /** Every name the vocabulary keeps out — each pattern by a key its prefix names (`<prefix><id>`). */ const CONVERSATION_PROBES = Object.freeze([ ...(CONVERSATION.keys ?? []), ...PATTERN_PREFIXES.map((p) => `${p}x`), ]); /** * Whether `policy` keeps an agent's WHOLE conversation out of its records — * every name the vocabulary lists selected by its rule (a pattern of the * policy's own that covers a name counts), `conversationRedaction()` or more. * * The question a chart-backed tool asks before it hands the MODEL a value the * calling run's policy selects (`core/servableSnapshot.ts` · * `modelFacingState`): the value travels on in the calling run's conversation * — the tool's result in its history and events, whatever the model then * says — so only a calling run whose records keep the conversation out keeps * that value out of them too. * * @internal */ function keepsConversationOut(policy) { if (policy === undefined) return false; const rule = new advanced_1.RedactionRule(policy); return CONVERSATION_PROBES.every((name) => rule.isKeyRedacted(name)); } exports.keepsConversationOut = keepsConversationOut; //# sourceMappingURL=conversation.js.map