agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
150 lines (149 loc) • 6.96 kB
TypeScript
/**
* 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.
*/
import type { LLMMessage } from '../../provider-contract/types.js';
import type { ContextRole } from '../../events/types.js';
/**
* Why a call's tool list is withheld: the out-of-budget wrap-up, or the run's
* tools went off after the evidence revision under an enforcing answer
* validation.
*/
export type ToolsWithheld = 'wrap-up' | 'evidence-revision';
/**
* 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
* ```
*/
export declare function toolsWithheld(wrapUpAsked: unknown, toolsOffAfterRevision: unknown): ToolsWithheld | null;
/** What the system-prompt join needs from an injection record. */
export interface SystemPromptPiece {
/** The piece's full text. Absent or empty ⇒ the piece contributes nothing. */
readonly rawContent?: string;
}
/**
* 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.
*/
export declare const 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.'
* ```
*/
export declare function joinSystemPrompt(pieces: readonly SystemPromptPiece[]): string;
/**
* 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
* ```
*/
export declare function contributingPieces<T extends SystemPromptPiece>(pieces: readonly T[]): readonly (T & {
readonly rawContent: string;
})[];
/**
* 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' }]
* ```
*/
export declare function stripFrameworkFields(messages: readonly LLMMessage[]): readonly LLMMessage[];
/** What the messages-slot join needs from an injection record. */
export interface MessagesSlotPiece {
/** The line's full text. Falls back to the summary when a slot recorded
* only that. */
readonly rawContent?: string;
readonly contentSummary: string;
/** The role the line goes out as. Absent ⇒ `'user'`. */
readonly asRole?: ContextRole;
}
/**
* 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' }]
* ```
*/
export declare function messagesFromInjections(records: readonly MessagesSlotPiece[]): readonly LLMMessage[];