UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

259 lines • 13.6 kB
/** * selfExplain — the IN-CONVERSATION door over the agent's own trace. * * `.selfExplain()` on the builder mounts ONE skill plus ONE scoped tool * provider. Day to day the tool catalog carries only the skill's * activation row — the trace tools are NOT in the skill (skill `tools` * land in the static registry, exposed every iteration); they ride a * `skillScopedTools` provider gated on the skill's activation, composed * with whatever provider the consumer already set. When the user asks a * why-question the LLM activates the skill, and the NEXT iteration's * catalog gains the trace tools, bound to the agent's own PREVIOUS * COMPLETED run. * * The two pieces here: * * 1. `SelfExplainBinding` — the late-binding seam, a plain CombinedRecorder * attached like any consumer recorder (zero engine changes): * * - capture at `onRunEnd`/`onRunFailed`: the just-finished run's * snapshot becomes the explainable evidence (a FAILED run is * still a completed trace — "why did you fail?" works); * - rotate at `onRunStart`: a FRESH ControlDepRecorder per run. The * retired instance never sees the new run's events, so its live * `asLookup()` survives Convention-4's runId reset — the captured * control edges stay valid for the whole next turn. * * B13 safety lives here: `Agent.run()` reassigns its executor at run * START, so resolving artifacts mid-run through `getLastSnapshot()` * would expose the IN-FLIGHT run. Capturing only at terminal flush * means the binding can never serve anything but a completed run. * * Keyed PER CONVERSATION (R2-11): one agent instance can serve * many people (`standingAgent({ agent })`), so "the previous completed * run" is asked of the ASKING run's session — the evidence of the last * run that session completed, never whichever run the instance finished * last. A run with no session reads the last run that had none. A tool's * retained inner runs are served through the same key * (`core/innerRunRecords.ts · innerRunsOfConversation`). * * 2. `buildSelfExplainSkill` — the skill in two modes: * * - INLINE (default): the skill unlocks the trace tools — every name * in `TRACE_TOOL_NAMES` (11 today) — in the main agent's own loop * (same model). The count is deliberately not restated anywhere * that could fall behind the pack. * - DELEGATE: the skill unlocks ONE tool — `explain_run(question)` — * whose execute runs a nested `traceDebugAgent` on the consumer's * chosen (cheaper) provider/model and returns its evidence-cited * answer. The main conversation pays for one tool call; the * trace-walking loop happens at the delegate's price. Loaded via * dynamic import so the builder never statically pulls Agent * through this module (no core ↔ lib cycle). */ import { type CombinedRecorder, type RuntimeSnapshot } from 'footprintjs'; import type { Injection } from '../injection-engine/types.js'; import type { AgentOptions } from '../core/agent/types.js'; import type { Unsubscribe } from '../events/dispatcher.js'; import type { AgentfootprintEvent } from '../events/registry.js'; import type { ToolProvider } from '../tools/toolProvider.js'; import { type InnerRunLookup } from '../core/innerRunRecords.js'; import type { TraceToolpackArtifacts, TraceToolpackOptions } from './types.js'; /** * How much of a turn's evidence the binding keeps. * * Both default to TRUE, which is the point: the tools that read them * (`read_narrative`, `inspect_tool_call`) are on the catalog either way, * and a tool that answers "⚠ no evidence" by default is a tool that * teaches the model not to call it. Turn one off when the cost matters * more than the answer — a very long-running turn, or a run whose * narrative would repeat what the structured tools already say. */ export interface SelfExplainInclude { /** The run's plain-English story → the `read_narrative` tool. Default true. */ readonly narrative?: boolean; /** * A bounded tail of the run's typed events → tool-call timings and * outcomes in `inspect_tool_call`, and the Context Integrity findings * `find_context_errors` reads. Default true. Off means no wildcard event * subscription is made at all, not a subscription that is ignored — and * `find_context_errors` then reports the evidence channel as ABSENT * rather than reporting a run with no context errors. */ readonly events?: boolean; } /** Consumer surface for `.selfExplain()` on the Agent builder. */ export interface SelfExplainOptions { /** Appended to the recommended skill body (ours stays; yours adds). */ readonly instruction?: string; /** * Answer why-questions on a SEPARATE (typically cheaper) model: the * skill unlocks one `explain_run` tool that runs a nested * `traceDebugAgent` and returns its evidence-cited answer. */ readonly delegate?: { readonly provider: AgentOptions['provider']; readonly model: string; readonly maxIterations?: number; }; /** Skill id (activation key for `read_skill`). Default 'self-explain'. */ readonly id?: string; /** Bounding dials forwarded to the toolpack. */ readonly toolpack?: TraceToolpackOptions; /** Which optional parts of a turn's evidence to capture. Both default true. */ readonly include?: SelfExplainInclude; /** * Cap on retained events per turn (only with `include.events`). Default * 2,000 — enough for a long tool-using turn, small enough that a server * holding one binding per agent does not grow without limit. A tail that * dropped events says so in `inspect_tool_call`. */ readonly maxEvents?: number; } /** Default per-turn event cap for the self-explain binding. */ export declare const SELF_EXPLAIN_MAX_EVENTS = 2000; /** Which conversation the agent is serving, and which run — see {@link SelfExplainSource.getServing}. */ export interface SelfExplainServing { /** The evidence key. `undefined` = a direct run with no session. */ readonly conversation: string | undefined; /** The run in flight, or the one served last. */ readonly runId: string; /** A conversation no later request can name (a hosted request with no * session) — kept on its own, smaller shelf. */ readonly oneShot?: boolean; } /** * What the binding reads a completed turn's evidence FROM. * * One object rather than three wiring calls, on purpose: the * `BoundaryRecorder` lesson in this codebase is that a seam needing three * separate connections gets two of them in some integrations, and the * missing third fails silently. Here the only caller is `AgentBuilder`, * and it hands over all of it at once. */ export interface SelfExplainSource { /** The just-finished run's snapshot — `agent.getLastSnapshot()`. */ getSnapshot(): RuntimeSnapshot | undefined; /** The run's narrative entries — `agent.getLastNarrativeEntries()`. */ getNarrative?(): readonly { readonly text: string; }[]; /** The typed event stream — `agent.on('*', …)`. */ on?(type: '*', listener: (event: AgentfootprintEvent) => void): Unsubscribe; /** * The records TOOLS kept of their own runs — present when the agent * mounts a `flowchartAsTool({ keepRecord: true })`. Unlike the other * three, this is NOT captured at the terminal flush: the store is a live, * bounded object owned by the tool, and the binding holds the lookup so * `inspect_tool_run` reads whatever the tool has filed. Rotating it per * turn would throw away the previous turn's inner runs at exactly the * moment the follow-up question arrives. */ getInnerRuns?(): InnerRunLookup | undefined; /** * The conversation the agent is serving — or, between runs, served last — * and that run's id. The conversation is the KEY the evidence is kept and * served under: read at the terminal flush it names the conversation the * finished turn belongs to, read inside a turn it names the conversation * asking. It is the run's session; a HOSTED run with no session gets the * host's per-request key, which nobody can present again; only a direct run * with no session shares `undefined`. Absent (a bare `getSnapshot` binding) * every run shares one key — the single-conversation behaviour — and inner * runs are served unfiltered. */ getServing?(): SelfExplainServing; } /** * The late-binding seam. Create one per built Agent, attach * `binding.recorder()` via `agent.attach()`, and point `bindTo()` at the * agent's `getLastSnapshot`. `artifacts` then always answers with the * previous COMPLETED run of the asking conversation — never the in-flight * one, and never another conversation's. */ export declare class SelfExplainBinding { private readonly include; private readonly maxEvents; private source; private ctrl; private tail; /** Evidence and run ids per conversation (see * {@link SelfExplainSource.getServing}), on two shelves: one for * conversations a caller can come back to ({@link MAX_CONVERSATIONS}), one * for one-shot ones ({@link MAX_ONE_SHOT_CONVERSATIONS}). Insertion order is * recency order. Run ids are recorded at run START, so a run that PAUSED or * FAILED (neither reaches the terminal capture) still owns the records its * tools filed (recheck RS4). */ private readonly lasting; private readonly oneShot; constructor(include?: SelfExplainInclude, maxEvents?: number); private get wantsNarrative(); private get wantsEvents(); /** * Point the binding at the agent it explains. * * Accepts the bare `getSnapshot` function it has always accepted (the * evidence a completed run leaves behind on its own), or the full * source, which adds the two parts a snapshot does NOT carry: the * narrative, and the typed event stream. */ bindTo(source: SelfExplainSource | (() => RuntimeSnapshot | undefined)): void; /** * Evidence of the previous completed run OF THE ASKING CONVERSATION, or * undefined when that conversation has completed none (or its evidence was * dropped past the bound). Never another conversation's. */ get artifacts(): TraceToolpackArtifacts | undefined; /** * Does the binding serve the run a tool call is in? Only the agent's OWN run * — the one its instance is serving — reads its evidence. A call from the * agent mounted as a composition member carries that composition's run id * (`ctx.runId`), which is never the instance's: the instance's evidence is * another caller's, so it is not served there. */ servesRun(runId: string | undefined): boolean; /** The conversation (and run) in flight, or served last. */ private serving; /** Which shelf a conversation lives on. */ private shelfOf; /** Remember that the serving run belongs to its conversation (bounded both ways). */ private recordRun; /** File the serving conversation's evidence as its most recent, within the bound. */ private file; /** The recorder to attach — forwards flow events to the per-run ctrl. */ recorder(): CombinedRecorder; } /** The default skill id — the activation key the LLM passes to read_skill. */ export declare const SELF_EXPLAIN_SKILL_ID = "self-explain"; /** * The skill `.selfExplain()` mounts — methodology body ONLY. The trace * tools deliberately do NOT ride the skill: skill `tools` land in the * static registry (exposed every iteration); catalog gating is the * ToolProvider's job — see {@link buildSelfExplainToolProvider}. */ export declare function buildSelfExplainSkill(options: SelfExplainOptions): Injection; /** * The gated tool delivery — `skillScopedTools` (the shipped primitive) * scoped to the skill's id, composed with the consumer's own provider * when they set one. The iteration after activation, `ctx.activeSkillId` * matches and the catalog gains the trace tools (inline) or the single * `explain_run` tool (delegate). * * INLINE MODE LOADS THE PACK LAZILY (9.94.0). The toolpack module is the * largest single module on the library's default graph, and until this * release every consumer who imported `Agent` carried it — `.selfExplain()` * or not — because this function reached it statically. Now the pack is * `import()`ed the first time the self-explain skill is ACTIVE on an * iteration, and never before: not at `build()`, not on a turn where the * model never opened the skill, not at all for an agent that did not enable * `.selfExplain()`. A bundler splits it into its own chunk; Node loads it * through the same `require` it always did, one microtask later. * * What the consumer sees: `list(ctx)` still answers `[]` synchronously when * the skill is not active (the sync fast path the tools slot skips the await * for), and a Promise of the same eleven tools when it is — the shape * `ToolProvider.list` has always allowed, and the one the composed provider * below already used. The provider id is unchanged, so `.watch()` output and * the `skill-scoped:` refusal in the builder read exactly as before. */ export declare function buildSelfExplainToolProvider(binding: SelfExplainBinding, options: SelfExplainOptions, existing?: ToolProvider): ToolProvider; //# sourceMappingURL=selfExplain.d.ts.map