/** * buildAgentChart — assemble the agent's full footprintjs FlowChart * from stage functions + slot subflows + memory wiring. * * This is the "chart composition" that used to live inline in * `Agent.buildChart()`. Extracted for v2.11.2 so: * * 1. Agent.ts focuses on Agent class lifecycle (constructor, run, * attach, getSpec) instead of chart wiring details. * 2. The reliability gate chart (v2.11.x) wires into ONE focused * file rather than surgically into Agent.ts's 250-line composition * block. * 3. The composition is independently readable + reviewable — * consumers building custom agent shapes have a reference. * * Chart shape: * * Initialize * → [memory READ subflows for each .memory()] * → InjectionEngine (subflow) ← loop target (tool-calls loops here) * → Context (selector, PARALLEL fan-out, failFast) * ⇉ {System Prompt ‖ Messages ‖ Tools} (slot subflows) * → converge * → UpdateSkillHistory * → Cache (sf-cache subflow: decideCacheMarkers → CacheGate * → ApplyMarkers / SkipCaching) * → CallLLM (also emits the per-iteration iteration_start marker) * → [NormalizeThinking] (subflow, only when a ThinkingHandler resolved) * → Route (decider) * ├─ tool-calls (pausable) → loopTo(InjectionEngine) ← branch-sourced loop * └─ final (subflow) → terminal leaf * ┌────── PrepareFinal * ├──── [memory WRITE subflows] * └──── BreakFinal ($break) * * (This chart has no reliability subflow, and never grew one. The plan * described here — "the reliability gate chart mounts as a subflow before * CallLLM with a TranslateFailFast stage after it. Lands in the next * commit." — did not land: `.reliability()` is implemented INLINE in the * CallLLM stage by `executeWithReliability`, and * `buildReliabilityGateChart` is reachable from no shipped path. Verified * 2026-07-28.) */ import type { FlowChart, StructureRecorder } from 'footprintjs'; import type { CachePolicy } from '../../cache/types.js'; import type { MemoryDefinition } from '../../memory/define.types.js'; import type { RouteBranch } from './stages/route.js'; /** * Stage handlers + slot subflows the chart composer needs. Mostly * passed through verbatim from Agent.buildChart() — the chart shape * is identical to what was inline before. */ export interface AgentChartDeps { /** Memory READ/WRITE pipeline definitions (one per `.memory()`). */ readonly memories: readonly MemoryDefinition[]; /** Evidence bridge (#5): `causalEvidenceRecorder().collect`, threaded into * CAUSAL memories' write mounts so snapshots persist real evidence * (decisions/toolCalls/iterations/duration/tokens) instead of zeros. * Set by the Agent when any mounted memory is CAUSAL. */ readonly causalEvidenceSource?: () => import('../../memory/causal/evidenceRecorder.js').RunEvidence; /** Cache policy for the system-prompt slot, threaded into * CacheDecision's inputMapper so its decision rules can match. */ readonly systemPromptCachePolicy: CachePolicy; /** Hard ReAct iteration cap, threaded into CacheDecision's * inputMapper for max-iteration policies. */ readonly maxIterations: number; readonly seed: (scope: never) => void | Promise; readonly callLLM: (scope: never) => Promise; readonly routeDecider: (scope: never) => RouteBranch | Promise; readonly toolCallsHandler: import('footprintjs').PausableHandler; /** * The schema re-ask branch (7.26). Present ONLY when the agent was built * with `.outputSchema(parser, { retries })` — and when present it is a * THIRD branch of the Route decider carrying the same `{ loopTo }` the * tool branch does, because a re-ask is one more ordinary turn. * * Conditional mount, for the reason every conditional mount here exists: * an agent that did not opt in gets no branch, no stage, no scope key and * no event — its chart and its commit log are the ones it always had. */ readonly outputRetryStage?: (scope: never) => void; readonly injectionEngineSubflow: FlowChart; /** Relevance entry router (`entryByRelevance`) — a once-per-turn function stage * mounted before the InjectionEngine (off the ReAct loop). Present only when the * skill graph was built with a relevance scorer. */ readonly pickEntryStage?: (scope: never) => Promise; /** * The turn-start routing CASCADE stage (SG-C) — subsumes PickEntry on * graphs that run it (`classify` configured, or `continuity: * 'conversation'`). Mounted in the SAME slot under the SAME id * (`STAGE_IDS.PICK_ENTRY`) so recorded structures stay stable; never * present together with `pickEntryStage` (Agent.buildChart picks exactly * one). Absent → the chart is byte-identical to 9.16. */ readonly routeTurnStage?: (scope: never) => Promise; /** * The window-strategy stage (`.window()` / `.compaction()`). Present ONLY * when the consumer configured a strategy — and when present it BECOMES the * ReAct loop target, mounted immediately before the current one. * * It has to be the loop target rather than merely sit in the loop body: * the loop is branch-sourced (`tool-calls → { loopTo }`), so anything ahead * of the target runs once and is never seen again. Being the target also * puts the window change BEFORE the injection engine and the slots, which * is the point — the triggers, the three slots and the wire then all see * the same window, and no component gets a different past than the model * does. * * `strategyName` rides along so the chart itself says which policy is * mounted; a reader of the graph should not have to guess. */ readonly windowStage?: { readonly strategyName: string; readonly run: (scope: never) => Promise; }; /** * The messages-slot DELIVERY stage (7.21). Present ONLY when the agent has * something that could target the messages slot — a registered injection * declaring `inject.messages`, or any `.memory()` whose recall might format * as a non-system role. When absent the chart is the one it always was, so * an agent with nothing to deliver is byte-identical to 7.20. * * Mounted between the InjectionEngine and the Context fan-out: after the * engine has decided what is active, before anything reads the window. The * placement is the design — a delivered message has to be part of the past * that the slots project, the cache decision indexes, and the wire sends, * or the recording and the request would disagree about the conversation. */ readonly deliverStage?: (scope: never) => void; readonly systemPromptSubflow: FlowChart; readonly messagesSubflow: FlowChart; readonly toolsSubflow: FlowChart; /** * Optional thinking-normalization sub-subflow (v2.14+). Mounted as a * stage AFTER CallLLM, BEFORE Route, only when a `ThinkingHandler` * resolved (either auto-wired by `provider.name` or explicitly set * via `.thinkingHandler()`). When undefined, the stage is NOT added — * zero overhead for non-thinking agents (build-time conditional mount). */ readonly thinkingSubflow?: FlowChart; readonly updateSkillHistoryStage: (scope: never) => void; /** * Whether ≥1 Skill is registered. The `UpdateSkillHistory` stage (and * therefore the cache's skill-churn rule) is mounted ONLY when true: * with no skills the window would record "no skill" every iteration and * `detectSkillChurn` could never fire, so the stage would be pure dead * weight + a misleading box. Mirrors the `skills.length > 0` gate that * auto-attaches `read_skill`, and the `thinkingSubflow` conditional mount. */ readonly hasSkills: boolean; /** * Whether ≥1 registered skill declares `steps` (9.18.0). Gates the step * pointer's mapper threading — engine in/out, the tools-slot arg, and (in * the grouped chart) the `sf-llm-call` boundary — so an agent without * stepped skills seeds and commits exactly the keys it always did. The * threading lives HERE in the builders, not in the subflow (the SG-C * blast-radius lesson): the alias discipline is * `stepPointer` in (readonly input) → Evaluate writes `nextStepPointer` * (the currentSkillId/nextSkillCursor precedent — the pointer changes * every iteration, so the turn-constant `turnRoute` pattern would serve * the tools slot a stale value) → mappers carry the alias back onto the * parent's `stepPointer`. */ readonly hasSteps?: boolean; /** * The mount kernel's plan (9.58.0) — present ONLY on an agent built with * `.maps()`. Gates the `mapEngagement`/`nextMapEngagement` alias round * trip through the injection-engine boundary (the stepPointer discipline * verbatim). Absent → the keys are never threaded, so every other agent's * mapper bytes are exactly what they were. */ readonly engagementPlan?: import('../../maps/engagement/types.js').EngagementPlan; /** * The unfinished-steps nudge branch (9.18.0). Present ONLY on an agent * with ≥1 stepped skill — and when present it is one more branch of the * Route decider carrying the same `{ loopTo }` the tool branch does, * because a nudge is one more ordinary turn (the SchemaRetry mechanism * verbatim). Absent → no branch, no stage, no scope key, no event. */ readonly stepNudgeStage?: (scope: never) => void; /** * The evidence recheck branch (9.35.0). Present ONLY on an agent built with * `.namesAndNumbersFromEvidence({ posture: 'guard' | 'rails' })` — the * `'assist'` posture records and never loops, so it mounts no branch. One * more branch of the Route decider carrying the same `{ loopTo }` the tool * branch does, because a correction is one more ordinary turn (the * SchemaRetry mechanism verbatim). Absent → no branch, no stage, no event. */ readonly evidenceRecheckStage?: (scope: never) => void; /** * The out-of-budget wrap-up branch (9.56.0). Present on any agent that can * CALL a tool — a registered tool, or a `ToolProvider` that might list one — * unless `wrapUpAtMaxIterations: false` turned it off. An agent with no tool * surface can never run out of budget mid-action (a limit only cuts a turn * short when tool calls were pending), so it mounts no branch and its chart * is drawn exactly as it always was. * * One more branch of the Route decider carrying the same `{ loopTo }` the * tool branch does, because a wrap-up is one more ordinary turn (the * SchemaRetry mechanism verbatim). Absent → no branch, no stage, no scope * key, no event. */ readonly wrapUpStage?: (scope: never) => void; /** * The evidence gate is mounted (9.35.0), at ANY posture. In the GROUPED * chart this gates bubbling `systemPromptInjections` out of `sf-llm-call`: * the slot writes it INSIDE the subflow and the outer Route decider needs * it to exempt values the app's own prompt (base prompt, skill body, a * retrieved passage) supplied — without the mapper key the gate would flag * the prompt's own identifiers in the default grouped shape (the * `hasEscalation` blast-radius lesson, one field down). The flat chart * shares one scope and needs no key; the flag is still threaded there so * both builders read the same deps object. */ readonly hasEvidenceGate?: boolean; /** * `.limitsTravelWithTheAnswer()` is configured (this release). Swaps the * final branch's first stage for the variant that folds the run's declared * coverage into the answer before the turn payload is captured. Absent → * the same stage function the chart has always mounted, byte for byte; the * RECORDING half (events + `coverageDeclared`) is unconditional and does * not depend on this flag. */ readonly attachCoverageLimits?: boolean; /** * An escalation brain is declared (9.19.0). In the GROUPED chart this * gates threading `skillEscalated` across the `sf-llm-call` boundary — * the flip is written by tool-calls on the OUTER scope and read by * callLLM INSIDE the subflow, so without the mapper key the escalation * would silently never serve a call in the default grouped shape (the * SG-C blast-radius lesson: the threading lives in the builders). The * flat chart shares one scope and needs no key; the flag is still * accepted here so both builders take identical deps. */ readonly hasEscalation?: boolean; /** * ReAct loop semantics. `'dynamic'` (default) re-runs the InjectionEngine + * all 3 slots every iteration (loop → InjectionEngine). `'classic'` * engineers context ONCE (InjectionEngine + system-prompt + tools up front) * and loops only the Messages slot (loop → Messages). See AgentOptions.reactMode. */ readonly reactMode?: 'classic' | 'dynamic'; /** Structure recorders threaded into both `flowChart()` calls (the * main chart and the PrepareFinal sub-chart). Each recorder * observes per-node build events (`onStageAdded` / * `onSubflowMounted` / etc.) for the Agent's chart. Undefined when * the consumer didn't attach any. */ readonly structureRecorders?: readonly StructureRecorder[]; } /** * Build the agent's complete FlowChart from the supplied deps. */ export declare function buildAgentChart(deps: AgentChartDeps): FlowChart;