UNPKG

agentfootprint

Version:

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

173 lines • 7.56 kB
"use strict"; /** * composeRequest — the two rules that turn COMMITTED pieces into the pieces a * provider is handed. * * Role: Lens. Two pure functions, no scope, no events, no I/O. They are the * model-facing half of request assembly that a Fold must be able to * repeat: `callLLM.ts` · `buildCallLLMStage` applies them on the way * out, and `servedView.ts` · `servedAt` applies the same two on the way * back, over the same committed values. * Reads: its arguments. * Emits: N/A. * * WHY THIS FILE EXISTS. Both rules were written inline, and the system-prompt * join was written inline FIVE times (callLLM, LLMCall, two message-API * charts, and the tool-calls stage's self-call frame). Five copies of a rule * is five chances to disagree about what the model read — and the moment a * reader tries to rebuild a served request from the commit log, a sixth copy * appears in the reader. The joined string itself is never committed (only its * pieces are), so the rebuild rests entirely on this join being one function. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.messagesFromInjections = exports.stripFrameworkFields = exports.contributingPieces = exports.joinSystemPrompt = exports.SYSTEM_PROMPT_SEPARATOR = exports.toolsWithheld = void 0; /** * THE TOOLS RULE — whether a call is served its tools, read from the two * committed keys that decide it (`wrapUpAsked`, `toolsOffAfterRevision`). * `callLLM` applies it on the way out and `servedView` on the way back, so the * two cannot disagree about which call was served none. * * @example * ```ts * toolsWithheld(undefined, true); // 'evidence-revision' * toolsWithheld(undefined, undefined); // null — the call is served its tools * ``` */ function toolsWithheld(wrapUpAsked, toolsOffAfterRevision) { if (wrapUpAsked === true) return 'wrap-up'; return toolsOffAfterRevision === true ? 'evidence-revision' : null; } exports.toolsWithheld = toolsWithheld; /** * The separator between two system-prompt pieces. A blank line, because the * pieces are independent sections (a base prompt, a skill body, a retrieved * passage) and a model reads them as sections only when they are separated * like sections. */ exports.SYSTEM_PROMPT_SEPARATOR = '\n\n'; /** * THE SYSTEM-PROMPT JOIN — the one rule turning committed injection records * into the single string a provider is handed. * * Empty pieces are dropped BEFORE joining, not after: a record with no * `rawContent` (a summary-only record, which is what a slot writes when the * content lives elsewhere) must not spend a blank line between its neighbours. * * @example * ```ts * joinSystemPrompt([{ rawContent: 'You are a bot.' }, {}, { rawContent: 'Be brief.' }]); * // 'You are a bot.\n\nBe brief.' * ``` */ function joinSystemPrompt(pieces) { return contributingPieces(pieces) .map((r) => r.rawContent) .join(exports.SYSTEM_PROMPT_SEPARATOR); } exports.joinSystemPrompt = joinSystemPrompt; /** * The pieces that actually reach the joined string, in order — the same * survivors {@link joinSystemPrompt} concatenates. * * Exported because two things need the SURVIVORS and not just the string: the * receipt lists one fingerprint per piece, and a rebuilt view lists the pieces * beside the text. Deriving that list a second time is how the list and the * string come to disagree about which piece was position 2. * * @example * ```ts * contributingPieces([{ rawContent: 'a' }, { rawContent: '' }, { rawContent: 'b' }]).length; // 2 * ``` */ function contributingPieces(pieces) { return pieces.filter((r) => typeof r.rawContent === 'string' && r.rawContent.length > 0); } exports.contributingPieces = contributingPieces; /** * Drop the fields that exist for the library and never for the model. * * These include: `injectedBy`, the delivery marker (7.21), * `notDispatched`, the batch settlement's marker (9.113.0 — the model reads the * settled call's sentence; the marker is that fact for the library's readers), * `toolResultOrigin`, the findings-only producer notice origin, * and `toolChars`, the tool-bytes boundary on a result the inputs layer * annotated (honesty layer 2 — the model reads the whole content; the boundary * tells the library's readers where the tool's own words end), and * `foldedExempt`, a compaction summary's exemption lineage (the exempt corpus * reads it; the model reads the summary's text). * Messages without any pass through BY REFERENCE, so an agent that delivers, * settles and annotates nothing allocates nothing — and the array's length and * order are untouched either way, which is what keeps * `CacheMarker{field:'messages'}` honest. * * They are removed before the request exists rather than trusted to be * ignored: a consumer-authored adapter that serializes a message wholesale * would otherwise put library internals on someone's wire. Stripping removes a * FIELD, never a message, so `messages[i]` is still the message a cache * marker's index names. * * @example * ```ts * stripFrameworkFields([{ role: 'user', content: 'hi', injectedBy: 'memory' }]); * // [{ role: 'user', content: 'hi' }] * ``` */ function stripFrameworkFields(messages) { if (!messages.some(carriesFrameworkFields)) return messages; return messages.map((m) => { if (!carriesFrameworkFields(m)) return m; const { injectedBy: _delivered, notDispatched: _settled, toolResultOrigin: _resultOrigin, toolChars: _toolBytes, foldedExempt: _lineage, ...composed } = m; void _delivered; void _settled; void _resultOrigin; void _toolBytes; void _lineage; return composed; }); } exports.stripFrameworkFields = stripFrameworkFields; /** Does this message carry a field {@link stripFrameworkFields} removes? */ function carriesFrameworkFields(message) { return (message.injectedBy !== undefined || message.notDispatched !== undefined || message.toolResultOrigin !== undefined || message.toolChars !== undefined || message.foldedExempt !== undefined); } /** * THE MESSAGES-SLOT JOIN — the one rule turning committed `messagesInjections` * records into the conversation a provider is handed. * * It exists for the same reason {@link joinSystemPrompt} does, and for one * more. `LLMCall` and the message-API charts have no `history`: the messages * slot IS their conversation, so this projection is not an observability * side-record for them — it is the committed source. A rebuild that read * `history` on those charts found nothing and reported an empty conversation * the provider never sent (`servedView.ts` · `viewOf`). One function, called on * the way out and on the way back, is what makes the rebuild true there. * * Empty lines are dropped, exactly as the calling stage drops them, so index * `i` means the same message on both sides. * * @example * ```ts * messagesFromInjections([ * { contentSummary: 'hi', rawContent: 'hi there', asRole: 'user' }, * { contentSummary: '', rawContent: '' }, * ]); * // [{ role: 'user', content: 'hi there' }] * ``` */ function messagesFromInjections(records) { return records .map((r) => ({ role: r.asRole ?? 'user', content: r.rawContent ?? r.contentSummary, })) .filter((m) => m.content.length > 0); } exports.messagesFromInjections = messagesFromInjections; //# sourceMappingURL=composeRequest.js.map