/** * 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 '../../adapters/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. */ export declare function buildMessageApiChart(deps: MessageApiChartDeps): FlowChart;