agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
97 lines (96 loc) • 4.96 kB
TypeScript
/**
* buildMessageApiChart — PROOF of the locked "messageAPI merge-tree" shape
* (MENTAL_MODEL.md ★ LOCKED DESIGN), LLM-only (no tools subflow yet).
*
* This is Step 1 of the agreed build order: prove the Context-selector →
* slot subflows → messageAPI stage → Call-LLM tree works + renders, BEFORE
* bringing it to the Agent (Step 2 adds the tools subflow + the loop).
*
* Chart shape (LLM-only):
*
* Seed
* → Context (SELECTOR stage — picks which slots to engineer)
* ├─ sf-system-prompt ┐ (selected branches run in parallel)
* └─ sf-messages ──────┴─→ messageAPI stage (the join point)
* → Call-LLM
*
* WHY a selector (not a plain fork): "Context = Selector stage" — it RETURNS
* the list of slot branch ids to engineer this iteration, and `select()`
* captures evidence (which slots + why). That is what will unify Static and
* Dynamic agent in ONE chart later: Static picks only `messages` per loop;
* Dynamic also picks `system-prompt` (and `tools`) when they re-engineer.
* The picked-set IS the lit/unlit-pill signal. For this LLM-only proof the
* selector picks BOTH slots (a one-shot call engineers everything once).
*
* WHY messageAPI is a REAL stage: it assembles the LLM request bulk that the
* agent's `callLLM` builds invisibly today (`buildCallLLMStage`,
* callLLM.ts · buildCallLLMStage) — `systemPrompt`
* (separate field) + `messages` (the conversation, incl. tool-results) → the
* message-API payload. Making it a stage makes that assembly visible +
* inspectable in Lens/Trace. (Tools is a separate field added at Call-LLM —
* it joins in Step 2.)
*
* Slots are REAL subflows (reused verbatim: buildSystemPromptSlot /
* buildMessagesSlot) writing the convention INJECTION_KEYS, so ContextRecorder
* emits context.injected and Lens renders them — no bespoke collapser.
*/
import type { FlowChart } from 'footprintjs';
import type { LLMProvider } from '../../provider-contract/types.js';
export interface MessageApiChartDeps {
readonly provider: LLMProvider;
readonly model: string;
readonly systemPrompt: string;
readonly structureRecorders?: readonly import('footprintjs').StructureRecorder[];
/**
* The id of the run this chart is about to make (9.91.0) — supply it and
* Call-LLM mints a receipt, the fingerprint of what the model was handed,
* committed at the call for `receiptAt` and `servedAt` to read.
*
* IT IS A DEP BECAUSE THIS IS A CHART BUILDER, NOT A RUNNER. `Agent` and
* `LLMCall` own their executor and mint a run id per run; this chart is
* handed to a `FlowChartExecutor` the caller owns, and nothing in a stage's
* scope carries that executor's run id. So the one value the receipt cannot
* do without has to come from whoever starts the run.
*
* OMIT IT AND NO RECEIPT IS MINTED — deliberately, rather than minting an
* unsalted one. Every hash on a receipt is salted with the run id precisely
* so a short system prompt or a two-word turn cannot be fingerprinted across
* runs (`receipt.ts`, the third law), and a receipt is committed state that
* travels in recordings. A chart that minted with an empty salt would ship
* dictionary-attackable fingerprints by default. `servedAt` then declares
* the absence — `no-receipt-on-chart`, cause `'no-receipt-committed'` — and
* rebuilds the view as it always did.
*
* @example (internal — `buildMessageApiChart` is not on the package's public
* surface; the type is, so a caller composing this chart inside the library
* reads the contract here)
* ```ts
* const runId = `run-${Date.now()}`;
* const chart = buildMessageApiChart({ provider, model, systemPrompt, getRunId: () => runId });
* const executor = new FlowChartExecutor(chart);
* await executor.run({ input: { message: 'hi' } });
* receiptAt(executor.getSnapshot(), 1)?.basis.runId; // runId
* ```
*/
readonly getRunId?: () => string | undefined;
}
/**
* Build the LLM-only messageAPI merge-tree chart.
*
* **Cancellation.** Every model call reads its signal from the run's `env`.
* For an abort or a deadline to cancel the provider's request, pass the signal
* there too: `env: { signal }`, and for a deadline `AbortSignal.timeout(ms)`. A
* bare `FlowChartExecutor` hands its own `signal` and `timeoutMs` to the
* traversal only. That stops the chart, but a call already in flight runs to
* completion, and you are billed for it.
*
* ```ts
* const controller = new AbortController();
* await new FlowChartExecutor(chart).run({
* input: { message: 'hi' },
* signal: controller.signal, // stops the traversal
* env: { signal: controller.signal }, // cancels the model call in flight
* });
* ```
*/
export declare function buildMessageApiChart(deps: MessageApiChartDeps): FlowChart;