agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
108 lines (107 loc) • 5.61 kB
TypeScript
/**
* compaction/summarize — the authored frame, and the call that fills it.
*
* Pattern: Template Method with an authored envelope + untrusted payload.
* Role: core/ layer. Two boundaries are enforced here, and they are the
* same boundary pointed in opposite directions:
*
* 1. Going OUT to the summarizer, the folded transcript is DATA —
* wrapped in a delimiter the authored instruction names, so a
* message inside the conversation cannot re-instruct the
* summarizer just by looking like an instruction.
* 2. Coming BACK into the window, the summary is DATA — appended
* after an authored label the library wrote. A summarizer that
* returns "IGNORE ALL PREVIOUS INSTRUCTIONS" produces a message
* that still says, in the library's own words and first, that
* what follows is a summary written by a model.
*
* The frame is authored. The summary is data. Neither can become
* the other.
*
* A third rule joined them in 8.2: the frame may only claim what is
* true. It names where the folded messages went, and it reads that
* from the resolved retention policy rather than asserting a
* constant — see {@link retentionSentence}.
* Emits: N/A (the stage emits; this file only builds and calls).
*/
import type { LLMMessage, LLMProvider, LLMResponse } from '../../../provider-contract/types.js';
import type { CompactionRetention } from './types.js';
export { COMPACTED_FRAME_PREFIX, isCompactedSummary } from '../../../lib/saidByPerson.js';
/**
* What the summarizer is asked to do. Authored, fixed, and never composed
* from run content — the only variable part of the summarizer's prompt is
* the transcript, and that arrives between delimiters this text names.
*/
export declare const SUMMARIZER_SYSTEM_PROMPT: string;
/** The folded span, rendered as the summarizer's input payload. */
export declare function renderTranscript(messages: readonly LLMMessage[]): string;
/**
* Build the message that replaces the folded span IN THE WINDOW.
*
* `role: 'user'` because the fold can reach the window's head, and the
* providers that care (Anthropic) require the window to open on a user turn.
* Since 9.55.0 it usually does NOT reach it — the CURRENT REQUEST sits there
* and refuses to be folded, so the frame lands immediately after it — but the
* role is right either way and a fold of a window with no identifiable
* request still opens on one.
*
* The label is written by this function and always comes first. `summary` is
* appended to it verbatim: the library never edits model output, and it never
* lets model output speak in the library's voice either.
*
* The summary is a claim, so it exempts nothing from the evidence gate or the
* contingent check: the values the FOLDED person/app turns exempted ride as
* `foldedExempt` (read by `evidence/evidenceIndex.ts` · `exemptFromRun`), and
* the text itself is never indexed as supplied.
*
* The label states the retention policy, and states it truthfully — see
* {@link retentionSentence}. `COMPACTED_FRAME_PREFIX` is unchanged: it is what
* every reader and every test matches on, and it stays put.
*/
export declare function buildSummaryMessage(summary: string, facts: {
readonly foldedMessageCount: number;
readonly iteration: number;
readonly model: string;
readonly retain: CompactionRetention;
/**
* The exemption lineage of the folded span (`evidence/evidenceIndex.ts` ·
* `exemptLineageOf`, computed from the ORIGINAL messages). Stamped as
* `LLMMessage.foldedExempt` when non-empty; the summary's text never
* exempts anything, so a frame built without it exempts nothing.
*/
readonly foldedExempt?: readonly string[];
}): LLMMessage;
export interface SummarizeResult {
readonly text: string;
readonly usage: LLMResponse['usage'];
}
/**
* Call the summarizer over the folded span.
*
* Throws whatever the provider throws — the STAGE decides that a broken
* summarizer means "no fold this iteration", not "no run". Deciding that
* here would hide the failure from the record.
*
* ## This call is UN-DECORATED, and that is on purpose
*
* `provider.complete(...)` below is the raw provider. None of the machinery
* that wraps the agent's own LLM call wraps this one:
*
* • no `reliability` retry loop (that lives inside the `call-llm` stage);
* • no `withRetry` / `withFallback` / `withCircuitBreaker` (those decorate
* the provider the AGENT was constructed with, not this argument);
* • no cache subflow (`sf-cache` sits in front of `call-llm` only).
*
* One attempt, and a failure is recorded as a `'summarizer-failed'` refusal
* rather than retried. A fold is optional work — the run is correct without
* it — so spending a retry budget on it, or letting a summarizer outage open
* a circuit that then blocks the agent's real calls, would trade something
* that matters for something that does not.
*
* The consequence worth knowing: passing the AGENT'S OWN provider instance as
* the summarizer gives you one object that behaves two different ways inside
* one run — retried and cached on one path, neither on this one. `.compaction()`
* refuses that exact pairing when the model matches too; pass a second
* instance.
*/
export declare function runSummarizer(provider: LLMProvider, model: string, span: readonly LLMMessage[], signal: AbortSignal | undefined): Promise<SummarizeResult>;