agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
106 lines (105 loc) • 5.32 kB
TypeScript
/**
* window/folded — join a summary sitting in a conversation back to what it
* stands for.
*
* Pattern: Pure lookup over recorded facts. No I/O, no inference.
* Role: core/ layer, and the ONE public door onto retained originals.
* Emits: N/A.
*
* ── Why a fingerprint and not an index ──────────────────────────────────────
* The obvious join key is "the summary is at `history[3]`". It is also wrong.
* A later fold swallows the span that CONTAINS an earlier summary, and every
* index after it moves; a conversation that folded four times would need its
* stored indices rewritten on every fold, and any reader holding an old one
* would silently be pointing at somebody else's message.
*
* So the join key is a fingerprint of the summary message's full content —
* authored frame included. Three things follow, and all three are the point:
*
* • it costs NO extra bytes on the wire (no id token planted in the frame);
* • it survives JSON, storage, and being read by a runtime that has never
* heard of this function;
* • it cannot be FORGED. A model that writes a message opening with the
* frame's own words produces different content, therefore a different
* fingerprint, therefore no match — `isCompactedSummary` alone says "this
* LOOKS like a frame", and this says "this IS the frame of a fold that was
* recorded". The two answers are different questions and the gap between
* them is exactly where a forgery would live.
*
* ── Absent is an answer ─────────────────────────────────────────────────────
* `undefined` means "no fold was recorded for this message" and never "there
* were no originals". A conversation stored by 8.1 has summaries and no spans
* at all; one folded under `retain: 'discard'` has a span whose `messages` is
* absent. Those are three different facts and the caller can tell them apart:
* no span, a span with no messages, a span with messages.
*/
import type { LLMMessage } from '../../../provider-contract/types.js';
import type { FoldedSpan } from './types.js';
/**
* The join key for one summary message: a fingerprint of its whole content.
*
* @internal Consumers join through {@link foldedSpanFor}; this is exported for
* the strategy that stamps the span and for tests that pin the two agree.
*/
export declare function summaryFingerprint(content: string): string;
/**
* The join key for one SPAN — the messages a fold would replace.
*
* Same hash family as {@link summaryFingerprint}, different subject: that one
* identifies the claim, this one identifies what the claim would be about.
* `summarizeOldest` uses it to remember which spans it has already been
* refused on, so a deterministic "no" is paid for once instead of once per
* iteration for the rest of the run.
*
* Role and content, both — a span whose messages are identical but whose roles
* differ is a different transcript and deserves to be asked about again.
*
* @internal
*/
export declare function spanFingerprint(messages: readonly LLMMessage[]): string;
/**
* The smallest thing that can answer "what did this conversation fold?".
*
* Structural rather than `AgentRunCheckpoint` on purpose: it also fits a
* paused run's `conversation`, a hand-built fixture, and anything a consumer
* pulled out of their own store — and it keeps this file from importing the
* checkpoint module, which imports this one's sibling types.
*/
export interface FoldedConversation {
readonly folded?: readonly FoldedSpan[];
}
/**
* What one summary message in a conversation stands for.
*
* Hand it the conversation you stored and a message out of its `history`; get
* back the fold that produced that message, with the original messages when
* the policy retained them.
*
* @returns the span, or `undefined` when this conversation recorded no fold
* for this message — see "Absent is an answer" above.
*
* @example Show a user what the agent actually saw last week
* ```ts
* import { foldedSpanFor, isCompactedSummary } from 'agentfootprint';
*
* const conversation = readEnvelope(await sessions.hydrate(sessionId));
* for (const message of conversation.history) {
* if (!isCompactedSummary(message)) continue;
* const span = foldedSpanFor(conversation, message);
* console.log(`summary of ${span?.messageCount ?? '?'} messages`);
* for (const original of span?.messages ?? []) {
* console.log(` ${original.role}: ${original.content}`);
* }
* }
* ```
*/
export declare function foldedSpanFor(conversation: FoldedConversation, message: LLMMessage): FoldedSpan | undefined;
/**
* Every message this conversation ever folded, oldest fold first, flattened.
*
* The transcript-shaped answer to `foldedSpanFor`'s message-shaped one: what
* a support view prints when somebody asks what the agent was told before the
* summaries. Spans that were discarded contribute nothing — they have nothing
* to contribute, and `foldedSpanFor` is where you find out that they existed.
*/
export declare function foldedMessages(conversation: FoldedConversation): readonly LLMMessage[];