agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
72 lines (71 loc) • 3.99 kB
TypeScript
/**
* window/notice — the authored message a DROP leaves behind, and why it has
* to exist at all.
*
* Pattern: Authored envelope with no untrusted payload whatsoever.
* Role: core/ layer. Shared by both drop strategies.
* Emits: N/A.
*
* The first reason for this message is the WIRE, not the prose. An agent
* window looks like `user, assistant+tool, assistant+tool, …`, so dropping
* the oldest turns leaves an ASSISTANT message at the head — and the
* providers that care (Anthropic) require the window to open on a user turn.
* A silent drop of the window's head therefore produces a request the vendor
* rejects. Something must occupy that position.
*
* Given that we have to author a message there anyway, it should say what
* happened rather than be filler. So it does: how many messages left, at
* which iteration, by which strategy, where they still are — and, since
* 9.57.0, WHICH TOOLS' RESULTS were among them. Unlike the compaction frame
* there is no model output involved at ALL — every character below is written
* by this library, and the only caller data that reaches it is a tool NAME,
* shape-filtered to a plain identifier and dropped outright when it is not
* one. So a drop still has no prompt-injection surface to speak of.
*
* That last sentence is the one this file exists for now. A drop the model is
* not told about is how a model came to invent an id: its `whats_here` result
* left the window, nothing said so, and it assembled a plausible-looking id
* out of an entity name it remembered. "Tool results are among them
* (whats_here) — call the tool again" states the absence instead of leaving
* it silent. Whether it changes what the model does next is NOT measured: the
* archived runs behind 9.57.0 were not re-run with this sentence on. It ships
* as honesty (a drop the model is not told about is unfindable from inside the
* conversation), not as a behaviour claim.
*
* It appears ONLY when the removal reaches the front of what may leave. That
* is the window's head in the ordinary case; since 9.55.0 it is the position
* just after the CURRENT REQUEST when the request was sitting at the head and
* refused to go (see currentRequest.ts) — the notice is still the first thing
* after the last un-droppable message, and it says that the request was kept.
* A removal further in leaves the original opening turn in place, so there is
* no wire problem to solve — and splicing a lone `user` message between two
* assistant turns is its own risk. The ledger names that removal either way.
*
* It does not accumulate: the notice is an ordinary oldest turn next time
* round, so the next drop absorbs it and files a fresh one.
*/
import type { LLMMessage } from '../../../provider-contract/types.js';
export { DROP_NOTICE_PREFIX, isDropNotice } from '../../../lib/saidByPerson.js';
/**
* Build the message that takes the head position after a drop.
*
* `role: 'user'` for the same reason the compaction frame is: it takes the
* head of the window, and the head of the window must be a user turn. (When
* the CURRENT REQUEST is holding that position it is already a user turn, and
* the notice follows it — see below.)
*
* `currentRequestKept` (9.55.0) is set when the drop stopped short of the
* window's head because the message the run is executing was sitting there
* and refused to leave — so the notice takes the position AFTER it rather
* than the head itself. It says so, because a model reading "3 earlier
* messages were dropped" and then finding a request above it should be told
* which of the two facts to trust. Omitted, the notice is byte-identical to
* the one this library has always written.
*/
export declare function buildDropNotice(facts: {
readonly droppedMessageCount: number;
readonly iteration: number;
readonly strategy: string;
readonly currentRequestKept?: boolean;
readonly toolNames?: readonly string[];
}): LLMMessage;