agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
80 lines (79 loc) • 4.32 kB
TypeScript
/**
* delivery/rules — where a delivered injection may go, and whether it is
* already there.
*
* Pattern: Pure functions over the window (no scope, no I/O, no provider).
* Role: core/ layer. The counterpart to `window/turns.ts`: that module
* decides what may LEAVE the window, this one decides what may
* ENTER it. Both answer with a refusal that names itself, and both
* exist so the rule lives in exactly one place instead of being
* re-derived at each call site.
* Emits: N/A.
*
* ── The one rule worth arguing about ─────────────────────────────────
* Collision is judged on the EFFECTIVE wire role, where `'tool'` counts as
* `'user'`. That is deliberately more conservative than any single provider
* requires: Anthropic coalesces `tool_result`s into a user turn (so a user
* message after tool results breaks its strict alternation), while OpenAI
* keeps `role: 'tool'` and would accept one. Judging per-provider would make
* the SAME declaration deliver on one wire and defer on another, with
* nothing in the recording to tell them apart — which is the exact
* provider-dependent truth this whole feature exists to avoid. So the floor
* is the rule, uniformly.
*
* The consequence is real and stated rather than hidden: inside a tool-using
* agent loop the window ends on the user's turn (iteration 1) or on tool
* results (every iteration after), so a `'user'`-role injection typically
* never gets a slot. `'assistant'` does, and `'system'` does on the providers
* that carry it.
*
* Testable on its own — see `test/core/delivery/delivery-rules.test.ts`.
*/
import type { LLMMessage, WireRole } from '../../../provider-contract/types.js';
import type { DeferralReason } from '../../../injection-engine/messagesSlotRefusal.js';
/**
* The role a message OCCUPIES on the wire, for sequencing purposes.
*
* A `tool` message is folded into `user` because that is what the strictest
* provider does with it. Everything else is itself.
*/
export declare function effectiveWireRole(role: LLMMessage['role']): WireRole;
/**
* The stable identity of one deliverable message: who declared it, and what
* it says.
*
* Position within the injection's own list is deliberately NOT part of the
* key — it cannot be recovered from a message already sitting in the window,
* and the key has to mean the same thing whether it is read from the run's
* ledger or recovered from a marker on a restored history. Content identity
* is what matters: a memory recall whose text changed between iterations is a
* NEW delivery (it is new content), an always-on fact repeating verbatim is
* the same one and is delivered once, and one injection declaring the same
* sentence twice says it once.
*/
export declare function deliveryKey(injectionId: string, role: string, content: string): string;
/**
* Every delivery key already present as a marker in the window.
*
* Unioned with the run's ledger before deciding what to deliver, and that
* union is what makes replay safe: `agent.resumeOnError` rebuilds the
* conversation but starts a fresh scope, so the ledger is empty while the
* delivered messages are right there in the restored history. Reading the
* window itself is the only way to know that.
*/
export declare function keysInWindow(history: readonly LLMMessage[]): ReadonlySet<string>;
/**
* Why a message may NOT be appended to the end of `history` right now, or
* `undefined` when it may.
*
* Two refusals, in the order a reader cares about them:
* • `unanswered-tool-call` — the tail is an assistant turn whose tool calls
* have no results yet. Appending here would sit BETWEEN a `tool_use` and
* its `tool_result`, which is the one split the window family refuses in
* the other direction too.
* • `role-collision` — the turn at the end already speaks in this effective
* role, and providers reject two in a row.
*/
export declare function refusalForPlacement(history: readonly LLMMessage[], role: WireRole): DeferralReason | undefined;
/** The effective role of the turn a delivery would land behind, for the note. */
export declare function tailWireRole(history: readonly LLMMessage[]): WireRole | undefined;