/** * middleware/runChain — INTERNAL. The one place a chain is walked. * * Pattern: Chain of Responsibility driver. * Role: core/ layer. Both stages (tool dispatch, message boundary) and * the MCP server call in here, so the ordering law, the * throw-is-a-denial law and the ledger row shape are written once * and cannot drift between the three callers. * Emits: N/A — it RETURNS rows; the caller commits and emits them, so a * middleware still never touches scope. * * ## The rules this file implements * * **Order is declaration order.** The chain is walked front to back, and * each link is handed the previous link's output — so a scrubber placed * first is a scrubber every later rule sees the effect of. * * **The first non-allow answer wins.** A deny or an ask stops the walk; * the links behind it do not run. There is no "second opinion" pass, * because a refusal that a later middleware could overturn is not a * refusal. * * **A middleware that throws is a denial, never a pass.** The thrown * message becomes the reason. This is the same deny-by-default the * permission gate has always applied to a checker that throws: a * governance layer whose failure mode is "allow" is not a governance * layer. * * **A middleware that malfunctions is a denial too** (8.18.0). Two ways to * malfunction at the message boundary: answer `ask` where no pause exists to * carry it, or `allow` a value that is not text. Both used to fall through to * the allow branch — the ask filed an allow row for a rule that thought it had * paused, and the non-string became `content: undefined` on the wire. Same law, * same row: a link whose answer cannot be honoured has not permitted anything. * * **The result comes back through the onion.** `runToolAfterChain` walks the * SAME list in reverse, so the first-declared link gets the first word about * the call and the last word about the result. An outer rule is the one that * sees what every inner rule already did — which is the only order in which * "wrap" means anything. * * **A link that has no hook for a moment files no row.** It did not decide; * saying it allowed would be inventing a decision, and the ledger's whole * value is that a row means somebody looked. */ import type { LLMMessage } from '../../../adapters/types.js'; import type { MemoryIdentity } from '../../../memory/identity/types.js'; import type { AskPayload, MessageMiddleware, MiddlewareDecision, ToolMiddleware } from './types.js'; /** Tool args, as they travel the chain. */ export type ToolArgs = Readonly>; /** * What to do when a middleware answers `ask` somewhere that cannot pause. * * `'pause'` — the caller will checkpoint (the normal dispatch path). * `'refuse'` — the ask is converted into a deny naming the middleware. Used * on the resume path (footprintjs's `PausableHandler.resume` returns void, * so a resumed dispatch cannot suspend a second time) and by `mcpServe` * (MCP is request/response — there is no pause to carry the ask). */ export type AskPolicy = 'pause' | 'refuse'; export type ToolChainResult = { readonly kind: 'allow'; readonly args: ToolArgs; readonly decisions: MiddlewareDecision[]; } | { readonly kind: 'deny'; readonly reason: string; readonly middleware: string; readonly args: ToolArgs; readonly decisions: MiddlewareDecision[]; } | { readonly kind: 'ask'; readonly payload: AskPayload; readonly middleware: string; /** Index of the link that asked — resume continues from `index + 1`. */ readonly index: number; /** Args as transformed up to the ask. These ride the checkpoint. */ readonly args: ToolArgs; readonly decisions: MiddlewareDecision[]; }; export interface ToolChainInput { readonly toolName: string; /** `Tool.source` for the tool being dispatched, when it has one. */ readonly toolSource?: string; readonly toolCallId: string; readonly iteration: number; readonly args: ToolArgs; readonly history: readonly LLMMessage[]; readonly identity?: MemoryIdentity; readonly signal?: AbortSignal; /** * Where to start walking. `0` for a fresh dispatch; `n` on resume, to * continue the chain AFTER the link that asked without re-running the * links whose decisions the checkpoint already carries. */ readonly startIndex?: number; readonly askPolicy?: AskPolicy; } /** Walk the tool chain. Never throws — a throwing link becomes a denial. */ export declare function runToolChain(chain: readonly ToolMiddleware[], input: ToolChainInput): Promise; export type ToolAfterChainResult = { readonly kind: 'allow'; readonly result: unknown; readonly decisions: MiddlewareDecision[]; } | { readonly kind: 'deny'; readonly reason: string; readonly middleware: string; /** The tool's REAL result. The side effect happened; the record says so. */ readonly result: unknown; readonly decisions: MiddlewareDecision[]; }; export interface ToolAfterChainInput { readonly toolName: string; readonly toolSource?: string; readonly toolCallId: string; readonly iteration: number; /** The args the tool ACTUALLY ran with — every before-transform applied. */ readonly args: ToolArgs; readonly result: unknown; /** `true` when the tool threw and `result` is the error's message. */ readonly error?: true; readonly history: readonly LLMMessage[]; readonly identity?: MemoryIdentity; readonly signal?: AbortSignal; } /** * Walk the chain BACKWARDS over a finished call. Never throws — a throwing * link becomes a denial, which at this moment means the model reads the * reason instead of the result. * * Only called for a call that actually executed. A call the chain denied, or * one still waiting on a person, has no result to decide about, and asking a * rule about a result that does not exist would be the same fabrication the * outcome union exists to prevent. */ export declare function runToolAfterChain(chain: readonly ToolMiddleware[], input: ToolAfterChainInput): Promise; export type MessageChainResult = { readonly kind: 'allow'; readonly content: string; readonly decisions: MiddlewareDecision[]; } | { readonly kind: 'deny'; readonly reason: string; readonly middleware: string; /** Content as transformed by the links BEFORE the refusal. */ readonly content: string; readonly decisions: MiddlewareDecision[]; }; export interface MessageChainInput { readonly phase: 'input' | 'output'; readonly content: string; readonly history: readonly LLMMessage[]; /** ReAct iteration for the ledger row. `0` at `'input'` (before iter 1). */ readonly iteration: number; readonly identity?: MemoryIdentity; readonly signal?: AbortSignal; } /** Walk the message chain. Never throws — a throwing link becomes a denial. */ export declare function runMessageChain(chain: readonly MessageMiddleware[], input: MessageChainInput): Promise;