/** * Tools slot subflow builder * * Pattern: Builder (returns a FlowChart mountable via addSubFlowChartNext). * Role: Layer-3 context engineering. Resolves the tools list the LLM * sees on this iteration — one InjectionRecord per exposed tool. * Emits: None directly; ContextRecorder sees the writes. * * Minimal scope for Phase 3e: static tool registry, all exposed every * iteration. Full permission gating / skill activation / context-aware * tool filtering arrives in Phase 5. */ import type { FlowChart } from 'footprintjs'; import type { LLMToolSchema } from '../../adapters/types.js'; import { type StepPlanFor } from '../../lib/injection-engine/skillSteps.js'; import type { Tool } from '../tools.js'; import type { ToolProvider } from '../../tool-providers/types.js'; /** * Mutable cache shared between `buildToolsSlot` (writer) and * `buildToolCallsHandler` (reader) within ONE run. The Tools slot * resolves the provider's tools each iteration and stashes the * Tool[] here; the toolCalls handler reads on dispatch — so async * providers pay the discovery cost once, not twice. Scoped to the * chart build so concurrent `agent.run()` calls each get their own * cache. */ export interface ProviderToolCache { current: readonly Tool[]; } export interface ToolsSlotConfig { /** Tool registry exposed to the LLM. Empty → empty slot (LLMCall case). */ readonly tools: readonly LLMToolSchema[]; /** * Registration-time owner stamps by tool name (9.60.0) — the identity * edges `Tool.owner` declared. The record then attributes a registry * tool to its OWNING subsystem instead of deriving `source:'registry'`. * Absent or unmatched → exactly today's bytes. */ readonly toolOwners?: ReadonlyMap; /** * The mount kernel's map cards (9.60.0) — id + owned tool names, for the * compose-seam integrity backstop below. Present only when `.maps()` is * mounted; absent → the backstop never runs, byte-identical. */ /** * The per-run disposition ledger, by REFERENCE (9.60.0) — the * ProviderToolCache pattern: build-closure plumbing, never scope state. * The compose backstop notes one disposition per pass here. */ readonly integrityLedger?: { current: import('../../integrity/disposition/ledger.js').DispositionLedger | undefined; }; readonly mountedMaps?: ReadonlyArray<{ readonly id: string; readonly toolNames: readonly string[]; }>; /** * Optional `ToolProvider` consulted PER-ITERATION (Block A5 follow-up). * When set, the slot calls `provider.list(ctx)` each iteration with * the current `{ iteration, activeSkillId, identity, signal }`. * Provider-supplied tool schemas are MERGED with the static `tools` * registry — both flow to the LLM. This is what makes Dynamic ReAct's * tool list reshape per iteration. */ readonly toolProvider?: ToolProvider; /** * Mutable cache the slot writes to after resolving `toolProvider.list(ctx)`. * The same cache reference is passed to `buildToolCallsHandler` so * dispatch reads from this iteration's resolved Tool[] instead of * calling `list()` a second time. Required when `toolProvider` is set. */ readonly providerToolCache?: ProviderToolCache; /** * Rebuild `read_skill`'s SCHEMA for this iteration's cursor (8.5.0). * * Set only for a `.skillGraph()` agent in a per-iteration ReAct mode. The tool's * enum is the full catalog and never changes; what varies is the DESCRIPTION — * which ids the gate will actually grant from where the cursor stands. Without it * the menu advertised every registered skill on every iteration while the gate * admitted a subset, so the model was routinely offered ids it would be refused. * * Substituted by NAME in Compose, so dispatch is untouched: the tool-calls handler * resolves executables from `registryByName`, never from the schema list. */ readonly readSkillFor?: (args: { readonly currentSkillId?: string; readonly hiddenSkillIds?: readonly string[]; /** The turn-start MENU (SG-C), passed only while the turn's menu verdict * is outstanding — see the Compose stage. */ readonly menu?: { readonly candidates: ReadonlyArray<{ readonly id: string; readonly relevance?: number; }>; readonly cursorId?: string; readonly stay?: boolean; }; }) => LLMToolSchema | undefined; /** * Which skills the caller's role may NOT see this run (9.11.0). * * Resolved in the async Discover stage and read by the sync Compose stage — * the same shape `providerToolCache` uses, and for the same reason: a * `PermissionChecker` may be async (a Redis lookup, a hub call) and Compose * is pure. Set only when a checker declares it governs `'skill_read'`; then * every hidden skill's row disappears from the `read_skill` menu, and the * dispatch loop refuses the activation with the policy's own message. * * Its errors are NOT swallowed: a visibility resolver that throws leaves the * iteration to the same reliability rules a failed `ToolProvider.list` does, * because composing a menu from a policy that did not answer is exactly the * fail-open this feature exists to prevent. */ readonly hiddenSkillIds?: () => Promise | readonly string[]; /** Budget cap (chars). Default: 2000. Set from the public door as * `contextBudget.tools` on `AgentOptions`. */ readonly budgetCap?: number; /** * The frozen step plans, keyed by skill id (9.18.0) — set only when ≥1 * registered skill declares `steps`. While a stepped tenure is active and * unfinished (`scope.stepPointer`, threaded by the mount mappers), Compose * narrows the OFFER to the current step: the stepped skill's own tools * that are not the current step's tool are held out of the request, the * current step's tool description leads with the `[Step k of n — ]` * banner (a rebuilt schema copy substituted BY NAME — the `read_skill` * precedent, so dispatch is untouched), and `skip_step` is appended. * * The escape hatches STAY OFFERED (house law): `read_skill`, * `list_skills`, every OTHER active skill's tools, the baseline `.tool()` * registry, provider tools. The hold-out applies only to names whose SOLE * active owner is the stepped skill — a name another active source also * carries (the same Tool reference shared across skills, a provider) is * somebody's escape hatch and is never pulled; a baseline `.tool()` can * never even collide (that overlap is refused at Agent build). Absent * plan / absent pointer / complete procedure → this path is * byte-identical to today. */ readonly stepPlanFor?: StepPlanFor; } /** * Build the Tools slot subflow. * * Mount with: * builder.addSubFlowChartNext(SUBFLOW_IDS.TOOLS, buildToolsSlot(cfg), 'Tools', { * inputMapper: (parent) => ({ iteration: parent.iteration }), * outputMapper: (sf) => ({ toolsInjections: sf.toolsInjections, toolSchemas: sf.toolSchemas }), * }) */ export declare function buildToolsSlot(config: ToolsSlotConfig): FlowChart;