agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
141 lines (140 loc) • 6.29 kB
TypeScript
/**
* AgentThinkingTraceRecorder — produce an AgentThinkingUI `Trace` from a run.
*
* AgentThinkingUI (the "watch it think" player) consumes a framework-agnostic
* `Trace` — a list of beats: `prompt → ask → return → answer`, where a tool
* reply is `data` (reason) or an `instruction` (a skill that says how to act).
* This recorder builds that `Trace` from agentfootprint's emit stream AS THE RUN
* TRAVERSES (no post-processing) — so any agentfootprint agent gets the
* domain-expert view for free, and AgentThinkingUI stays vendor-agnostic (it
* just renders the `Trace` JSON, exactly as it renders the OTLP adapter's output).
*
* Mapping (from the events already on the stream):
* stream.llm_end (toolCalls>0) → the brain reasoned; content + usage become
* the upcoming ask's `brain` + `cost`.
* stream.llm_end (toolCalls==0) → the final `answer`, unless contentWithheld.
* agent.turn_end → a withheld answer becomes a beat only on
* accepted delivery, using finalContent.
* stream.tool_start → `ask` (read_skill → reaching for a skill).
* stream.tool_end → `return` (read_skill → replyType:'instruction'
* + skill; any other tool → replyType:'data').
* a bracket carrying → no beat (9.113.0): a call a paused batch
* `notDispatched` never dispatched neither asked nor returned.
*
* Commentary (each beat's `brain`): filled from agentfootprint's OWN commentary
* engine — the SAME `selectCommentaryKey`/`extractCommentaryVars`/`renderCommentary`
* the Lens uses — so AgentThinkingUI's Notepad / bottom caption read identically
* to the Lens commentary panel (one engine, consumer-overridable via
* `commentaryTemplates`). The LLM's own reasoning still wins on the first ask of
* an iteration; the engine fills every other beat so no line is ever blank.
*
* Convention 1 (one purpose) + Convention 4 (run-scoped — resets per run).
*/
import type { EmitRecorder } from 'footprintjs';
import { type CommentaryTemplates } from './commentary/commentaryTemplates.js';
export interface AttCost {
ms: number;
tokens: number;
tokensIn?: number;
tokensOut?: number;
tokensCached?: number;
}
export interface AttAnswer {
headline: string;
[key: string]: unknown;
}
/** One tool the model had at its disposal for an LLM call — what it "saw" when it
* decided. Lets a domain expert expand and read the menu next to the model's
* reasoning, to debug WHY it chose (or skipped) a tool. */
export interface AttToolSeen {
readonly name: string;
readonly description?: string;
}
export type AttStep = {
kind: 'prompt';
brain: string;
cost: AttCost;
} | {
kind: 'ask';
tool: string;
toolName?: string;
/** The call's id on the record (9.111.0) — the key every ledger row about
* this call carries, so a reader can join the beat to its declarations
* without guessing by name or order. Absent only on a trace recorded
* before the field existed. */
toolCallId?: string;
input: Record<string, unknown>;
brain: string;
cost: AttCost;
/** Model's extended-thinking chain-of-thought for this iteration (when the
* provider emitted reasoning blocks). Rendered as the "thinking" callout. */
thinking?: string;
/** The tools the model saw (name + description) for this iteration's call —
* the menu it chose from. On the iteration's first ask only. */
toolsSeen?: readonly AttToolSeen[];
} | {
kind: 'return';
tool: string;
toolName?: string;
/** The call this result answers (9.111.0) — the same id as its ask beat. */
toolCallId?: string;
replyType: 'data' | 'instruction' | 'both';
output: Record<string, unknown>;
brain: string;
/** Who authored `brain`: the model's own words, or a framework delivery
* sentence. Consumers (the notepad) prefix "LLM reasons —" only for
* model-authored text; absent = model (the pre-field-report default). */
brainSource?: 'model' | 'framework';
cost: AttCost;
brainMode?: 'reason' | 'act';
skill?: string;
error?: string;
} | {
kind: 'answer';
to: string;
brain: string;
answer: AttAnswer;
cost: AttCost;
error?: string;
/** Model's extended-thinking chain-of-thought before the final answer. */
thinking?: string;
/** The tools the model saw (name + description) for the final call. */
toolsSeen?: readonly AttToolSeen[];
};
export interface AttTrace {
task: string;
title?: string;
agent: string;
model: string;
asker: string;
steps: AttStep[];
/**
* The stage each step was recorded under — `at[i]` is the `runtimeStageId`
* of the emit that produced `steps[i]` (the prompt step, made by
* `getTrace`, carries `''`: it names no stage). Index-aligned with `steps`
* so a story beat can be placed on the run's own cursor axes; a trace
* recorded before 9.99.0 has no `at`.
*/
at?: readonly string[];
}
export interface AgentThinkingTraceOptions {
readonly id?: string;
readonly agent?: string;
readonly model?: string;
readonly asker?: string;
/**
* Override agentfootprint's bundled commentary templates — the SAME shape as
* the Lens's `commentaryTemplates` prop (partial; spread over the defaults).
* Drives each beat's `brain` narration, so AgentThinkingUI's Notepad / bottom
* caption read like the Lens commentary panel — one engine, one voice,
* consumer-overridable. Omit to use the bundled English defaults.
*/
readonly commentaryTemplates?: Partial<CommentaryTemplates>;
}
export interface AgentThinkingTraceHandle extends EmitRecorder {
/** The AgentThinkingUI `Trace` for the run so far. `task` (the headline of the
* replay pill) defaults to the captured user message; override any field. */
getTrace(overrides?: Partial<Pick<AttTrace, 'task' | 'title' | 'agent' | 'model' | 'asker'>>): AttTrace;
clear(): void;
}
export declare function agentThinkingTrace(options?: AgentThinkingTraceOptions): AgentThinkingTraceHandle;