agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
167 lines • 7.5 kB
TypeScript
/**
* tools/runChain — INTERNAL. The one place a chain is walked.
*
* Pattern: Chain of Responsibility driver.
* Role: tools/ 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 '../provider-contract/types.js';
import type { MemoryIdentity } from '../memory/identity/types.js';
import type { AskPayload, MessageMiddleware, MiddlewareDecision, ToolMiddleware } from '../core/agent/middleware/types.js';
/** Tool args, as they travel the chain. */
export type ToolArgs = Readonly<Record<string, unknown>>;
/**
* 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[];
/**
* Set when the link did not deny but ASKED, and `askPolicy: 'refuse'`
* turned the ask into this denial — the call needed a person and there
* was no pause to carry the question. Absent on every link's own deny.
*/
readonly refusedAsk?: true;
} | {
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<ToolChainResult>;
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<ToolAfterChainResult>;
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<MessageChainResult>;
//# sourceMappingURL=runChain.d.ts.map