agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
974 lines • 53.4 kB
TypeScript
/**
* Tool types — Agent's tool-call contract.
*
* Pattern: Strategy (GoF) — each Tool is a strategy for "how to execute
* this named operation given these args".
* Role: Consumer-facing shape. Agent.tool(...) accepts these.
* Emits: N/A (types only).
*/
import type { RedactionPolicy } from 'footprintjs';
import type { LLMToolSchema, ToolCapability } from '../provider-contract/types.js';
import type { ToolArtifacts } from '../artifacts/capability.js';
import type { ArtifactMeta } from '../artifacts/types.js';
import { type ToolWants } from '../artifacts/wants.js';
import type { Credential, CredentialNeed, CredentialProvider } from '../identity/types.js';
import type { MemoryIdentity } from '../memory/identity/types.js';
import { type ToolResultClass } from '../semantics/types.js';
import { assertResultColumns, COLUMN_TYPES, type ColumnDeclaration, type ColumnType, type ToolResultColumns } from '../integrity/column-types/types.js';
import { type AskComponent } from './askComponent.js';
import { type AskOrAssume, type ToolPeriod } from './declare.js';
import { type ToolScript } from './script.js';
import type { ToolSearch } from '../semantics/coverage/search/types.js';
import type { TimeContext } from '../time/wire.js';
import type { CheckInDemand } from '../core/checkin.js';
import type { TeardownOptions, TeardownScope } from './toolSessions.js';
/**
* One executable tool the Agent can call.
*
* - `schema` is what the LLM sees (name, description, JSON schema).
* - `execute` runs when the LLM requests this tool with the given args.
* Returns anything JSON-serializable; the framework forwards it back
* to the LLM as the tool result.
*/
export interface Tool<TArgs = Record<string, unknown>, TResult = unknown> {
readonly schema: LLMToolSchema;
/** Declare-and-push: a credential this tool needs. The framework resolves it
* BEFORE invoking and injects `ctx.credential`; it is NOT in `schema`, so the
* LLM never sees or fills it. */
readonly needs?: CredentialNeed;
/**
* Declared artifact ARGUMENTS (9.22.0) — argument name → the artifact
* `kind` it must resolve to (e.g. `wants: { dataset: 'dataset/rows' }`).
*
* The `needs` precedent applied to data: the MODEL passes the ~26-char
* `art_…` ref as the argument (declare it `type: 'string'` in
* `inputSchema`), and at dispatch — BEFORE `execute` — the framework
* redeems it under the run's own scope and kind-checks the meta. The
* handler receives the RESOLVED DATA in `args` (and the claim tickets on
* `ctx.wanted`); a stale, unknown, or wrong-kind ref never reaches the
* tool — the model reads a teaching refusal listing the live refs of the
* wanted kind. Resolution rides `agentfootprint.artifacts.resolved`;
* refusals ride `artifacts.refused` with `op: 'dispatch'`.
*
* **Whether the model MAY omit it is your `inputSchema`'s to say.** Name
* the argument in `required` and dispatch refuses the call by name when no
* ref arrives — the handler is never entered believing the framework
* resolved something it did not. Leave it out and an omitted argument is
* the model choosing not to use one: the tool runs, `args` carries no such
* key, and `ctx.wanted` has no entry for it.
*
* Requires an attached store: an Agent refuses at BUILD when a statically
* registered tool declares `wants` with no `artifacts` configured (config
* that lies otherwise); other dispatch doors refuse at dispatch, by name.
* Omitted → byte-identical behavior (nothing resolved, nothing measured).
*/
readonly wants?: ToolWants;
/**
* Declarative demand for a human check-in BEFORE this tool runs — consent
* for a consequential action, with an evidence pack riding the ask.
* `'always'` trips on every call; a `(args, ctx) => boolean` predicate trips
* selectively (e.g. only refunds over $1000). When it trips the tool-dispatch
* loop pauses BEFORE execute and surfaces a `CheckInRequest`; the human
* answers with `checkInApproved` / `checkInDeclined`. Omitted → byte-identical
* behavior (no gate, no events, no pause). See `.checkIn()` on the Agent
* builder to configure the evidence pack. Ordered AFTER the permission gate
* and arg-validation, BEFORE credential resolution.
*
* Non-generic here (a `Tool` widens into `Tool[]` registries); `defineTool`
* exposes a predicate typed to the tool's args at the CALL site.
*/
readonly checkIn?: CheckInDemand;
/**
* Which REGISTERED screen component collects this tool's check-in decision
* (9.24.0) — ids and props only, never markup. Rides the `CheckInRequest`
* when the gate trips, so the answering screen renders its own registered
* component instead of prose. Meaningless without `checkIn` and refused
* beside its absence at `defineTool` — a component for a gate that never
* fires is configuration that lies. A `propsRef` here must resolve in the
* RUN's artifact scope when the gate trips (validated at raise time); a
* check-in fires BEFORE `execute`, so the tool cannot mint it mid-call —
* static declarations usually want inline `props`.
*/
readonly checkInComponent?: AskComponent;
/**
* Where this tool came from — the name of the MCP server that served it.
*
* **Absent means "this agent's own".** A tool you wrote with `defineTool`
* carries nothing here, and that absence is the fact: nobody else supplied
* it. A tool that arrived over MCP carries the client's `name`
* (`mcpClient({ name: 'aws-mcp' })`), because the same tool NAME can come
* from two servers and a policy that cannot tell them apart governs both.
*
* It travels to the decision point as `ToolMiddlewareContext.toolSource` —
* the tool-dispatch chain and `mcpServe`'s serving-side chain read the same
* field.
*
* Set by `mcpClient` / `mockMcpClient`. `defineTool` never sets it, so it
* cannot be spoofed by accident; a hand-built `Tool` may set it deliberately
* when it is genuinely relaying another source's tool.
*/
readonly source?: string;
/**
* What this tool touches, DECLARED by whoever wrote it (9.11.0).
*
* The framework never infers this. A tool's capabilities are not knowable
* from its name, its schema or its description, and classifying them by guess
* would rest a policy decision on a heuristic — so a tool that says nothing
* gets nothing asked about it, exactly as before.
*
* **Enforced when both sides speak.** When a tool declares a capability AND
* the configured `PermissionChecker` declares it `governs` that capability,
* the dispatch loop asks once per declared capability, right after the
* `'tool_call'` check allows — `check({ capability: 'external_net', target:
* '<tool name>' })`. Either side silent → not asked, not refused. A denial
* lands like every other refusal in the loop: the tool does not run and the
* model reads a result it can adapt to.
*
* @example a tool the operator wants governed as a network egress
* defineTool({
* name: 'fetch_invoice',
* description: 'Fetch an invoice PDF from the billing service',
* capabilities: ['external_net', 'user_data'],
* inputSchema: { … },
* execute: async ({ id }) => …,
* });
*/
readonly capabilities?: readonly ToolCapability[];
/**
* The refusing ceiling on THIS tool's result (9.20.0): when the handler's
* stringified return exceeds `maxChars`, the model reads a teaching refusal
* naming the true size, the ceiling and how to narrow — and the oversized
* payload never enters context, history or any event. See
* {@link ToolResultCeiling} for why refusal, not truncation. Omitted →
* byte-identical behavior (nothing measured, nothing emitted).
*/
readonly resultCeiling?: ToolResultCeiling;
/**
* The declared CLASS of this tool's results (9.53.0) — what kind of answer
* it gives (`'triage'` — a health/fault verdict; `'inventory'` — a
* population listing). Declared, never inferred (the `capabilities` law),
* and validated at definition against the closed set. The
* `check:semantics` gate keys its per-class rules on it — a `'triage'`
* tool whose sample result declares no coverage fails the build by name.
* Omitted → no class rules; the semantic-envelope rules still apply to any
* result that carries the `af_semantics` marker.
*/
readonly resultClass?: ToolResultClass;
/**
* THE ARTIFACT KIND A PLACED RESULT IS MINTED UNDER (9.70.0) — this tool's
* result in the CONSUMER's vocabulary (`'dataset/rows'`), not the
* framework's.
*
* Artifact PLACEMENT (`artifacts: { store, placement }`) checks an oversized
* result into the store and hands the model a claim ticket. Absent this
* field it mints under `tool-result/<toolName>` — honest, and unspendable:
* `wants` is exact-match on kind BY LAW (no wildcards, no hierarchy), so a
* downstream `wants: { dataset: 'dataset/rows' }` refuses the very ticket
* the framework just minted, as a kind mismatch. Field-verified: the
* consumer had to re-mint by hand at the seam, which is the framework
* declining to carry its own ref.
*
* The fix is DECLARED, never inferred (the `capabilities` / `resultClass`
* law) and never a loosening of the matcher: only the author knows what
* their tool actually produces, and `wants` staying exact is what makes a
* ticket a promise. Declaring `resultKind` makes the MINT speak the
* consumer's vocabulary instead.
*
* A non-empty string; an empty or blank one is refused at `defineTool`,
* because a kind is what a ticket is redeemed against and a blank kind
* redeems against nothing. Omitted → exactly today's bytes
* (`tool-result/<toolName>`, and no measurement at all without placement).
*
* @example a tool whose placed result a `wants` consumer can spend
* defineTool({
* name: 'get_rows',
* description: 'Fetch the rows of a dataset',
* resultKind: 'dataset/rows',
* inputSchema: { … },
* execute: async () => …,
* });
* // elsewhere: defineTool({ name: 'chart', wants: { dataset: 'dataset/rows' }, … })
*/
readonly resultKind?: string;
/**
* WHAT THIS TOOL'S ROWS CONTAIN (9.78.0) — column name to type, the
* sibling of {@link Tool.resultKind}. `resultKind` says what the result IS;
* this says what it CONTAINS.
*
* THE MEASURED FAILURES, all three the same shape — a number became
* something else, and nothing noticed at the seam:
*
* 1. `str(m.get("logical_unit_number") or "")` — LUN 0 is falsy, so LUN 0
* was stored as an EMPTY STRING on 2,094 mappings, and a host group
* missing the LUN an initiator probes first became indistinguishable
* from one that had it.
* 2. `round(mib / 1024, 1)` rendered an 8 MiB disk as `0.0 GB`, which
* reads as NO DISK during a live incident.
* 3. A family of tools returned their numbers as quoted strings
* (`"1240"`), which silently blanked every chart, because nothing
* downstream could tell a measure from a label.
*
* Declaring the columns gives the library something to check the rows
* against. It catches 1 and 3. It cannot catch 2, and says so: the check
* judges TYPE, never MEANING (see `COLUMN_TYPE_CEILING`, quoted verbatim
* into every finding).
*
* A promise about what it NAMES, never a closed schema — an unlisted column
* is allowed and never judged. Two spellings: a bare type, or the object
* form when a column may legitimately hold nothing.
*
* ARMED BY TWO HALVES, like every write-seam check: this declaration AND
* the operator's `checkColumnTypes` dial (default `'off'`). Omitted, or
* with the dial off → exactly today's bytes; nothing is measured, no
* finding is filed, and the model reads the rows the tool returned.
*
* @example the LUN report that lost its zeroes
* defineTool({
* name: 'host_group_mappings',
* description: 'The LUN mappings of a host group',
* resultKind: 'dataset/rows',
* resultColumns: {
* logical_unit_number: 'number',
* host_group: 'string',
* comment: { type: 'string', nullable: true },
* },
* inputSchema: { … },
* execute: async () => [{ logical_unit_number: 0, host_group: 'vdi-a' }],
* });
*/
readonly resultColumns?: ToolResultColumns;
/**
* WHO OWNS THIS TOOL (9.60.0) — the identity edge, stamped at the one
* moment the code demonstrably knows both ends: registration. Before
* this field, ownership was only DERIVABLE (from the per-pass
* InjectionRecord, or the maps kernel's MountedMap) — a checker asking
* "who owns get_zones" between registration and the first tools-slot
* pass had no answer, and a static `.tool()` registration's sourceId
* was just the tool's own name. The Context Integrity checks read this
* stamp and never infer identity; a tool without one is `unreachable`
* to subject-joined checks, which the disposition ledger counts.
* Omitted → exactly today's bytes (`source: 'registry'`).
*/
readonly owner?: ToolOwner;
/**
* WHERE THIS TOOL'S ARGUMENTS COME FROM (9.60.0) — the names of tools
* whose RESULTS ground what a caller passes here (`screen_fire` fires at
* ids that `whats_here` listed). Declared by the author, never inferred:
* only the author knows the dependency. The dangling-reference check
* reads it at composition — when a declared ground's results have left
* the window (`WindowRecord.droppedObservations`) and were not
* re-established, offering this tool files a finding. Omitted → this
* tool is never that check's subject, byte-identical.
*/
readonly argumentsFrom?: readonly string[];
/**
* THE NAMED INGREDIENT TOOLS THIS TOOL IS COMPOSED OF (9.76.0) — the
* registered tools its body calls through the run's own dispatch
* (`ctx.tools`), declared by the author, never inferred.
*
* Consumer-side readers, which is what earns it a place here (the
* `resultKind` / `argumentsFrom` law — a declaration rails read, nothing
* that governs execution): the agent-build drift gate asserts every named
* ingredient is a registered tool, so a runbook whose inventory tool was
* renamed fails the BUILD by name instead of failing its first run; and it
* joins the MCP `_meta` declaration list so a composed tool served over the
* wire says what it is made of.
*
* Checked at AGENT BUILD, not at definition — the ingredients need not
* exist before this tool is defined, and the catalog is only complete once
* every `.tool()` registration has landed. Tools delivered by a
* `ToolProvider` are invisible to the check (there is no build-time list);
* with a provider configured the gate warns instead of refusing.
* Omitted → nothing is checked, byte-identical.
*/
readonly composedOf?: readonly string[];
/**
* WHETHER THIS TOOL'S PROCEDURE CAN RAISE AN APPROVAL GATE (9.76.0) — a
* mid-run pause that asks a human before continuing. Declared, never
* inferred (the `capabilities` law): the framework cannot see through a
* tool boundary into an inner chart that gates.
*
* Consumer-side readers: composition-time checks that must refuse a gating
* tool where a pause cannot be resumed (a fan-out branch — the runbook
* grammar's compiler is the named reader). It does not govern execution;
* the runtime pause refusal remains the backstop for a tool that omits it.
* `false` is a declaration too ("this procedure never gates"), distinct
* from saying nothing. Omitted → byte-identical.
*/
readonly gates?: boolean;
/**
* FINGERPRINT THE REPEATED-CALL LEDGER ON ARGUMENTS ALONE (9.62.0) —
* `'arguments'` tells `core/agent/repeatedCall.ts` that this tool's own
* RESULT is not evidence of repetition and must not be folded into the
* match key.
*
* The repeated-call nudge exists to catch a model calling the same tool
* with the same arguments and getting nowhere. By default it proves "got
* nowhere" by also requiring the RESULT to match — a `check status` call
* returning a different status is progress, not a loop, and the default
* rule (correctly) says nothing about it. That default quietly breaks for
* a tool whose result is not a function of its arguments on purpose: a
* screen/UI tool that stamps a fresh version number, timestamp, or cursor
* into every answer so a human or a downstream cache can tell which
* render is current. Call a tool like that twice with byte-identical
* arguments and the default fingerprint never matches — the detector is
* silently inert for it, forever. This was found in a real recorded
* failure: an agent re-fired a completed navigation sequence and nothing
* noticed, because each fire's fresh stamp made it look like new
* information.
*
* Declared, never inferred — the `capabilities` / `resultClass` law. Only
* the tool's author knows whether its result is signal or a stamp;
* guessing from a name or a response shape would rest a detector on a
* heuristic the framework cannot verify. The note's wording changes to
* match when this fires (it stops claiming the result matched, because it
* did not) — see `repeatedCall.ts` for both sentences.
*
* **This never suppresses execution.** The ledger only ever appends a
* teaching sentence to a result the tool already returned, strictly AFTER
* `execute` ran — the same anti-guarantee `runCheckpoint.ts` and
* `Agent.ts` document for tool calls generally (durability replay,
* resumed runs, and every retry of this kind still execute the tool; there
* is no dedup here or anywhere else in this library).
*
* Omitted → byte-identical behavior: the ledger keeps folding the result
* into the key exactly as it always has, for every tool that does not
* declare this.
*
* @example a screen tool whose result always carries a fresh version stamp
* defineTool({
* name: 'render_screen',
* description: 'Render the named screen',
* repeatedWhen: 'arguments',
* inputSchema: { … },
* execute: async ({ view }) => `rendered ${view} @v${Date.now()}`,
* });
*/
readonly repeatedWhen?: 'arguments';
/**
* PER-ARGUMENT RULES (the inputs layer, honesty layer 2) — what the library
* does when a call leaves an argument out. `{ assume: value }`: the library
* FILLS the value (the tool never does) and files it on the findings ledger
* as `default`, so the answer's standing says the value was assumed.
* `{ ask: question, choices? }`: the PERSON gives the value — the library
* asks ONCE for everything a batch of calls left out, before anything in the
* batch runs, through the typed input pause (`isInputPause`,
* `agent.resume(checkpoint, { requestId, values })`), and files the answer
* as `answered`. A present value runs as sent, filed as the model's own.
*
* The served copy of the schema drops a ruled argument from `required` and
* says the rule in the property's description; the registry schema — which
* `validateToolArgs` judges and `mcpServe` serves — is never edited.
* Carried over MCP in `_meta.agentfootprint`. Declaring it arms the inputs
* layer's mount (`sf-inputs`) on an agent that registers the tool; a ruled
* tool that only a ToolProvider serves needs `AgentBuilder.inputsLayer()`,
* and without it the call is refused rather than run unruled.
* Omitted → byte-identical.
*
* @example
* ```ts
* askOrAssume: { window: { assume: '2h' } }
* askOrAssume: { window: { ask: 'Which period should the search cover?', choices: ['1h', '24h'] } }
* ```
*/
readonly askOrAssume?: AskOrAssume;
/**
* WHICH ARGUMENTS SET THE PERIOD the answer covers, how their values are
* spelled, and the facts about the source. Today's single argument
* (`argument` + `spelling`: `lookback` `24h`, `signed-lookback` `-24h`,
* `iso-range`, `wall-range` with a `zoneArgument`; or `accepts` for
* several) is sugar over `forms` — every shape the tool takes: `bounds`
* (two arguments, each an `iso` instant, `epoch-ms`, `epoch-s`, a `date` or
* a `wall` time), `joined`, `object`, `day`, `lookback` with its `units`.
* Facts: `direction`, `retention`, `maxRange`, `granularity`,
* `filtersToAsked`. Each argument must carry an `askOrAssume` rule (an
* `object` form's argument excepted — a ruled argument is flat). Rows for
* them carry `period: true`. Under `.time()` the library fills a period
* the model left out from the turn's one window, binds a sent one to the
* person's window, and hands the tool `ctx.time`. Carried over MCP in
* `_meta.agentfootprint.period`. Omitted → byte-identical.
*/
readonly period?: ToolPeriod;
/**
* WHAT ONE CALL OF THIS TOOL SEARCHES — declared once, as typed data, so the
* library composes each call's coverage from the arguments the call actually
* ran with (`semantics/coverage/search/`). Every `inputSchema` property is
* classified by role (`population`, `filter`, `period`, `source`, `cap`,
* `page`, `inert`), with a default where `execute` applies one
* (`whenOmitted`: a value, or `{ any: true }`); `fixed` names the qualifiers
* the tool always applies; `gaps` the ground it never reaches, each with the
* facet it `bears` on; `complete: 'within-arguments'` lets a call stand for
* the whole population, filters and period in effect. The result says
* `absent({ what, checked: 'declared' })` (or `coverage(rows, { checked:
* 'declared' })`) and the funnel composes the rest: composition can only
* under-claim, relative to a true declaration. Refused at `defineTool`, and
* again at the agent's registration, when malformed. Carried over MCP in
* `_meta.agentfootprint.search`, where it composes rows marked
* `composed: 'remote'` that establish nothing. Omitted → byte-identical.
*/
readonly search?: ToolSearch;
/**
* `'read'` — this tool changes nothing (design R6). A declaration a later
* check reads, never one that governs execution: only an `effects: 'read'`
* written in `defineTool`, on a LOCAL tool, counts
* (`readOnlyByDeclaration`); a hand-set field, an MCP annotation or a remote
* `readOnlyHint` never does, and a remote tool carrying it is refused at
* registration (`effects-on-remote`). It never crosses MCP. Omitted →
* byte-identical.
*/
readonly effects?: 'read';
/**
* THIS TOOL RUNS A PROGRAM, and `argument` is the ONE string argument that
* carries it (`language` is the program's language, the info string of the
* fence it is shown in). Declared, never inferred: the library never reads
* it off a tool's name, a property named `code` or the description, and a
* tool that runs code without declaring it has no submissions on the record.
*
* While `.namesAndNumbersFromEvidence({ code: 'match' })` or
* `.scriptTravelsWithTheAnswer()` is on, every call of the tool files one
* `script` row on the findings ledger: the program SUBMITTED to the tool
* (after any before-tool middleware) and what the runner reported. A fenced
* block the model shows is then matched, byte for byte, against this turn's
* submissions; the program itself is never evidence for a value.
* `codeRunnerTool` declares `{ argument: 'code', language }`. Refused at
* `defineTool` (and at the dispatch and MCP doors, `tools/script.ts`) when
* `argument` is not a declared string property of the input schema.
* Carried over MCP in `_meta.agentfootprint.script`. Omitted →
* byte-identical.
*
* @example
* ```ts
* script: { argument: 'query', language: 'sql' }
* ```
*/
readonly script?: ToolScript;
/**
* THE APP'S OWN WORDS for what a call of this tool does — "loading the
* attached data" — read by a PERSON. The registered name (`schema.name`) is
* the model's vocabulary, and an app may forbid showing it, so the standing
* line (`.answerLayer({ standingLine: true })`) names a reason's calls ONLY
* by this — "Not sure — loading the attached data reported no matching
* results" — and uses it in its assumed clause; the limits block marks the
* items this call declared with it and names its `Period:` / "In progress"
* lines by it. A call whose tool declares none is counted ("and 1 other
* call"), never named there. Not every surface uses it yet: the time
* layer's limits lines and the answer account's rows still print the
* registered name (a known follow-up).
*
* One plain line of at most 60 characters (`lib/displayName.ts` — no control,
* line separator or invisible format character), stored trimmed; refused at
* `defineTool` and again at registration. Write it to read as the subject of
* a sentence ("loading the attached data", "the order lookup"). It names the
* call and changes nothing else: not the standing, not what the model sees.
* Omitted → byte-identical.
*/
readonly displayName?: string;
execute(args: TArgs, ctx: ToolExecutionContext): Promise<TResult> | TResult;
}
/**
* Refuse a `displayName` a person should not read, at definition time — the
* rule (`lib/displayName.ts` · `displayNameProblem`) asked for a tool built by
* `defineTool` and, at registration, for one assembled by hand.
*/
export declare function assertDisplayName(toolName: string, displayName: unknown): void;
/**
* Whether the APP declared this tool read-only: `effects: 'read'` written in
* `defineTool`, on a local tool (no `source`). Tool metadata, an MCP
* annotation or a remote `readOnlyHint` never answer yes.
*/
export declare function readOnlyByDeclaration(tool: Tool): boolean;
/** Carry an app's read-only declaration through a library-owned, behavior-preserving wrapper. */
export declare function inheritReadOnly(from: Tool, to: Tool): void;
/**
* Refuse an `effects` this library cannot honour — any value but `'read'` at
* `defineTool`, and any `effects` on a remote tool at registration
* (`effects-on-remote`: a remote tool's word never relaxes a check).
*/
export declare function assertToolEffects(toolName: string, effects: unknown, remote?: boolean): void;
/**
* The stamped identity edge a tool carries (9.60.0): which subsystem it
* belongs to, in the vocabulary the record already uses for sources.
*/
export interface ToolOwner {
readonly kind: import('../events/types.js').ContextSource;
readonly id: string;
}
/**
* A declared cap on ONE tool's result that REFUSES instead of truncating
* (9.20.0).
*
* WHY refusal: a truncated result reads as a complete one — the model cannot
* tell the data ends where the cut happened, so it fabricates from the part it
* saw. A refusal that names the size, the ceiling and the parameters to narrow
* produces a clean retry instead (field-verified on a ~191k-char return). The
* agent-level `maxToolResultChars` remains the OTHER answer — truncate with a
* verbatim head and a marker — for operators capping tools they did not write;
* this one is the TOOL AUTHOR's contract, and only the author knows which
* parameters (`narrowBy`) make the retry smaller.
*
* The record keeps the truth: a typed `agentfootprint.tools.result_refused`
* event carries the true size, and the delivered result carries status
* `'invalid'` so `onToolStatus` edges can route on it.
*/
export interface ToolResultCeiling {
/** The ceiling, in characters of the stringified result. Positive whole
* number; anything else is refused at `defineTool`. */
readonly maxChars: number;
/** Parameter names the refusal suggests narrowing by (e.g. `['limit',
* 'fields']`). Optional; when present it must name at least one — an empty
* list is refused, because omitting the field is how "no suggestions" is
* said. */
readonly narrowBy?: readonly string[];
}
/**
* Refuse a `resultCeiling` this library cannot honor, at definition time —
* naming the tool and the fix, never failing at the first oversized result of
* the first run. Exported beside `lib/toolName.ts` · `assertValidToolName` for consumers
* assembling `Tool` objects by hand.
*/
export declare function assertResultCeiling(toolName: string, ceiling: ToolResultCeiling | undefined): void;
/**
* Refuse a `resultClass` outside the closed set, at definition time — naming
* the tool, the value and the whole vocabulary (the `assertResultCeiling`
* law: a declaration this library cannot honor fails HERE, never at the
* first gate run of the first CI pipeline). Exported beside it for consumers
* assembling `Tool` objects by hand.
*/
export declare function assertResultClass(toolName: string, resultClass: ToolResultClass | undefined): void;
/**
* The COLUMN-TYPE CONTRACT's rule and vocabulary, re-exported HERE.
*
* They are DEFINED in `src/integrity/column-types/types.ts`, because
* `src/integrity/` is a strict leaf that imports nothing outside itself and
* the check has to be able to read the words. They are re-exported here
* because `defineTool` and the MCP ingest both reach for their rules through
* `tools/tools.ts` — one door for every declaration's rule, and no second copy
* of any of them anywhere.
*/
export { assertResultColumns, COLUMN_TYPES, type ColumnDeclaration, type ColumnType, type ToolResultColumns, };
/**
* Refuse a `resultKind` that could never be redeemed, at definition time —
* naming the tool and the fix (the `assertResultCeiling` / `assertResultClass`
* law: a declaration this library cannot honor fails HERE, never at the first
* oversized result of the first run). Exported beside them for consumers
* assembling `Tool` objects by hand.
*
* The one rule is non-blankness, and it is not a formality: the kind is what a
* `wants` argument is matched against by exact string equality, so a blank or
* whitespace-only kind mints a ticket no declaration can ever name — the same
* refusal `assertToolWants` raises on the consuming end, raised on the
* producing end too. There is deliberately NO charset or shape rule: the kind
* is the consumer's vocabulary, and the library does not own it.
*/
export declare function assertResultKind(toolName: string, resultKind: string | undefined): void;
/**
* Refuse an `owner` stamp with a blank half, at definition time — naming the
* tool and what a blank half would do.
*
* An identity edge with a blank half is worse than none: the Context Integrity
* checks JOIN on it, so a blank `kind` or `id` joins the wrong subjects rather
* than no subjects. Extracted from `defineTool` (9.71.0) so the MCP ingest
* boundary can hold the SAME line on a declaration that arrived over the wire —
* one rule, one message, two doors.
*
* Deliberately NOT a closed-set check on `kind`: the union is enforced by the
* compiler for anyone calling `defineTool`, and the value is only ever read
* into a prose reason (`owned by <kind> '<id>'`), never switched on.
*/
export declare function assertToolOwner(toolName: string, owner: ToolOwner | undefined): void;
/**
* Refuse an `argumentsFrom` edge that could never join, at definition time.
*
* The dangling-reference and unsupported-argument checks join on these names,
* so a blank one — or a tool grounded by itself — would join the wrong subjects
* or none at all. Extracted from `defineTool` (9.71.0) for the same reason as
* {@link assertToolOwner}: the MCP ingest boundary enforces the identical rule
* on a declaration a remote server sent.
*/
export declare function assertArgumentsFrom(toolName: string, argumentsFrom: readonly string[] | undefined): void;
/**
* Refuse a `composedOf` list that could never be drift-checked, at definition
* time — the {@link assertArgumentsFrom} law applied to composition: the
* agent-build gate joins on these names, and a blank one — or a tool composed
* of itself — would join the wrong subjects or none. The REGISTRATION check
* (is every named ingredient actually registered?) deliberately does NOT
* happen here: the ingredients need not exist before this tool is defined,
* and only the agent build sees the complete catalog.
*/
export declare function assertComposedOf(toolName: string, composedOf: readonly string[] | undefined): void;
/**
* Refuse a `gates` declaration that is not a boolean, at definition time.
* Trivial for anyone the compiler vets; load-bearing at the MCP ingest
* boundary, where a foreign server can put anything under the key and a
* truthy string would silently declare a gate nobody wrote.
*/
export declare function assertGates(toolName: string, gates: boolean | undefined): void;
/** Options for one {@link ToolDispatch.call}. */
export interface ToolDispatchCallOptions {
/** Abort signal for the inner call. Defaults to the outer call's own. */
readonly signal?: AbortSignal;
/**
* Declare an inner ABSENCE survivable (9.76.0). By default a dispatch
* consumer that composes answers (runbookAsTool) propagates an inner
* `absent()` as its own answer — "the inventory found nothing" IS the
* runbook's result, and pretending to a verdict over it would be the
* confident-partial-answer failure. Pass `true` when the caller can carry
* on without this source and will state the gap itself (usually as a
* coverage entry). The raw dispatch delivered on `ctx.tools` returns every
* result untouched either way — the propagation policy belongs to the
* consumer that wraps it.
*/
readonly allowAbsent?: boolean;
}
/**
* The run's own tool dispatch, delivered at execute time as `ctx.tools`
* (9.76.0) — how one tool's body calls ANOTHER registered tool through the
* same map the model dispatches by, instead of importing its module and
* building a second query stack.
*
* What it sees: the agent's static catalog (`.tool()` registrations plus
* skill-carried tools) — the same dispatch map the tool-calls handler uses.
* Tools delivered by a `ToolProvider` are NOT visible (there is no build-time
* list), a stated caveat, not an accident.
*
* What an inner call gets: the outer call's own facts (credentials, signal,
* progress) with `hasArtifacts: false` — an inner tool must not mint claim
* tickets competing with the composed answer's own — and a derived
* `toolCallId` naming the outer call it belongs to. A declared `needs` is
* resolved before the inner execute (fail-closed: a service that requires
* interactive consent refuses by name — an inner call cannot pause).
*/
export interface ToolDispatch {
/** Is this name in the dispatch map? Provider-delivered tools answer false. */
has(name: string): boolean;
/**
* Execute a registered tool and return its result exactly as returned —
* a coverage envelope arrives as the envelope, an absence as the absence.
*/
call(name: string, args: unknown, opts?: ToolDispatchCallOptions): Promise<unknown>;
}
/** Runtime context passed to tool.execute(). */
export interface ToolExecutionContext {
/** Unique id of THIS tool invocation (matches stream.tool_start.toolCallId). */
readonly toolCallId: string;
/** Current iteration number of the ReAct loop. */
readonly iteration: number;
/**
* The call's time (the time layer, step T5a — `time/wire.ts`): the
* half-open range the call asks for (`asked`, edge `'exclusive'` — the
* person's window when the library filled or bound it, the sent value read
* back otherwise), the person's zone, the turn's frozen `now` and the
* moment the library dispatched the call. Present only under `.time()` on a
* call to a tool whose `period` declares forms, so the tool can declare the
* `period.queried` its read covered without parsing its own argument. Over
* MCP it travels in the call's `_meta.agentfootprint.time`.
*/
readonly time?: TimeContext;
/** Abort signal propagated from run({ env: { signal } }). */
readonly signal?: AbortSignal;
/**
* The bound credential provider — the PULL escape hatch for dynamic needs.
* Always present: when none is attached it's a fail-closed provider that
* THROWS, so it never silently no-ops via optional chaining. Prefer the
* declarative `needs` + `ctx.credential` for the common case.
*/
readonly credentials: CredentialProvider;
/** True when a real provider is attached. Branch on this for intentional
* degraded (no-credential) mode instead of relying on `undefined`. */
readonly hasCredentials: boolean;
/**
* The claim-check store, bound to THIS run's scope (9.21.0) — shaped
* exactly like `credentials`. Always present: with no store attached every
* method throws a teaching refusal naming how to attach one
* (`Agent.create({ ..., artifacts })`), so a missing store can never read
* as an empty one. The scope (tenant/principal/conversation) is composed by
* the framework from the run's identity/session and closed over — a tool
* cannot name, widen, or replace it. `put` stamps `origin`
* (`{ runId, toolCallId }`) from the run's own facts.
*/
readonly artifacts: ToolArtifacts;
/** True when a real artifact store is attached. Branch on this for an
* intentional no-store (degraded) mode instead of catching the refusal. */
readonly hasArtifacts: boolean;
/**
* The claim tickets behind this call's resolved `wants` arguments (9.22.0)
* — argument name → the `ArtifactMeta` whose data replaced the ref in
* `args`. Present ONLY when the tool declared `wants` and at least one
* declared argument resolved; absent otherwise (absent and empty are
* different facts). The data itself is already in `args`.
*/
readonly wanted?: Readonly<Record<string, ArtifactMeta>>;
/** The credential resolved for this tool's declared `needs` (declare-and-push).
* Present only when the tool declared a need and it resolved successfully. */
readonly credential?: Credential;
/**
* The run's own tool dispatch (9.76.0) — see {@link ToolDispatch}. Present
* on the agent's dispatch paths; ABSENT at doors with no dispatch map
* (`mcpServe`, the offline trace context, a hand-built context in a test).
* Absent and empty are different facts: branch on the absence rather than
* optional-chaining past it, and prefer a fail-closed refusal (the
* `credentials` law) when your tool cannot work without it.
*/
readonly tools?: ToolDispatch;
/**
* Report progress from INSIDE a long-running tool — "hop 3 of 12 done", said
* mid-`execute`, while the call is still running.
*
* A tool call is otherwise ATOMIC on the record: `stream.tool_start` fires,
* the handler runs for as long as it runs, and `stream.tool_end` carries the
* result. For a forty-second twelve-hop walk that is one long silence — the
* person watching cannot tell working from hung, and neither can an operator
* reading the archive afterwards.
*
* Each call files one `agentfootprint.stream.tool_progress` event, in call
* order, BEFORE this call's `tool_end`. The framework stamps `toolCallId`,
* `toolName` and `iteration`: identity facts are never the tool's to state,
* so a report cannot claim to be from another call. `payload` is the tool
* author's own data, forwarded untouched.
*
* **Always present, never fatal.** With nothing listening it is a no-op that
* drops the report; it never throws, never blocks (nothing is awaited), and
* never changes what `execute` returns or what the model reads. A tool that
* calls it zero times behaves exactly as it did before this existed.
*
* **`payload` must survive `structuredClone`** — it rides the ordinary emit
* channel into every event sink and every recording, so plain data only (no
* class instances, no live handles, no functions). Progress is TELEMETRY: it
* never enters the tool result, the history, or the model's view.
*
* **What a PERSON sees (9.54.0).** One call, two faces. The structured
* payload rides to the record untouched, exactly as above — and the live
* status surfaces (`agent.enable.liveStatus(...)`, `attachStatus`) now show
* the report too, so a long call is no longer silent in the browser. Because
* `payload` is yours and typed `unknown`, the display contract is narrow and
* literal:
*
* - A top-level **string field named `message`** is shown to the person
* VERBATIM, trimmed, cut at **120 characters** with the cut stated
* (`… (+N more)`). `message` is the field MCP's own progress
* notification uses; nothing else in the payload is read.
* - **Anything else** — no `message`, a non-string one, an empty one, a
* bare string payload — shows the generic line instead:
* `` `<toolName>` reported progress (N so far)… ``. Your payload is
* never pretty-printed into that sentence. A status line is prose, and
* a tool's JSON is not a sentence anyone wrote.
*
* So `ctx.progress({ done: 3, total: 12 })` keeps its numbers on the record
* and says "reported progress (3 so far)" on screen; adding
* `message: 'Hop 3 of 12'` puts your own words there instead — and keeps the
* numbers. Consumers override the wording by template key
* (`tool.progress`, `tool.progress.generic`, or per tool).
*
* Doors with no event stream to file on — `mcpServe`, the offline
* `callTraceTool` context — supply the no-op. A tool must not have to know
* which door it is behind to be safe to call this from.
*
* @example a twelve-hop walk that says where it is
* execute: async (args, ctx) => {
* for (const [i, hop] of hops.entries()) {
* await walk(hop);
* // `message` is what the person reads; the rest is what the record
* // keeps. Drop `message` and the screen still says a report landed.
* ctx.progress({
* message: `Hop ${i + 1} of ${hops.length} — ${hop.id}`,
* done: i + 1,
* total: hops.length,
* hop: hop.id,
* });
* }
* return summary;
* }
*/
progress(payload: unknown): void;
/**
* The run this call belongs to.
*
* **Absent when there is no run.** A call served over `mcpServe` is one call,
* not a turn in a conversation, and minting a synthetic run id there would
* fabricate a run that never existed. Branch on the absence.
*/
readonly runId?: string;
/**
* The hosting conversation this run is bound to, when it is bound to one —
* `HostRequest.sessionId`, threaded through `agent.run({ sessionId })`.
*
* Never derived, never defaulted to `runId`, never the anonymous latch.
*
* **It is caller data, not identity.** Anyone who can reach the host can put
* any string here, including someone else's. Never key a live session on it
* alone — compose it with tenant and principal via {@link toolSessionKey}.
*/
readonly sessionId?: string;
/**
* The identity the CALLER supplied — `run({ identity })`, the same tuple
* memory and the permission gate scope on.
*
* **Absent when the caller passed none.** Deliberately NOT the run's internal
* `runIdentity`, which is always populated (it defaults to
* `{ conversationId: '<runId>' }`, or to `{ conversationId: sessionId }` on a
* session-bound run since 9.10.0): handing either of those to a tool would
* publish a SYNTHESIZED conversation as if somebody had named one. A tool
* that wants the session has `ctx.sessionId` for it, which is the fact the
* transport actually delivered.
*/
readonly identity?: MemoryIdentity;
/**
* The redaction policy the calling run is covered by — the agent's
* `redact`, joined with any a composition or an outer caller added.
* **Absent when the run has none.**
*
* A run this tool starts is a NESTED run, and footprintjs's law is that a
* run's policy covers the runs nested in it. The library's own nested runs
* take it for you — `flowchartAsTool`, `runbookAsTool` and the
* `.selfExplain({ delegate })` debugger join it with their own. A tool that
* starts another agent hands it on, exactly as it hands `ctx.signal`:
*
* ```ts
* execute: (args, ctx) => specialist.run({ message: args.text }, {
* ...(ctx.signal && { env: { signal: ctx.signal } }),
* ...(ctx.redact && { redact: ctx.redact }),
* })
* ```
*
* It names what to keep out of RECORDS. It never touches the arguments this
* tool receives: the tool runs on real values.
*/
readonly redact?: RedactionPolicy;
/**
* Register cleanup for work THIS call started — a code-interpreter session, a
* browser context, a lease.
*
* The tool learns its isolation key at execute time and registers cleanup for
* exactly that key in the same breath; there is no other seam where both are
* in hand. Registering twice under one `(tool, scope, key)` is a no-op that
* keeps the FIRST cleanup (it holds the live handle) and refreshes liveness,
* so calling this on every execute is the intended shape for a reused
* session.
*
* Throws, naming the door, when `scope` is not in {@link teardownScopes} — a
* capability nobody implements is a promise the library cannot keep.
*
* @example a session that lives as long as the run
* const key = toolSessionKey(ctx, 'run');
* const session = await runner.start({ key });
* ctx.onTeardown?.(() => session.stop(), { scope: 'run', key });
*/
onTeardown?(cleanup: () => void | Promise<void>, options?: TeardownOptions): void;
/**
* Which teardown scopes this door can actually honour — `[]` means none ever
* fires here.
*
* A FACT to branch on, exactly like `hasCredentials`, rather than an
* `undefined` to optional-chain past: a tool that wants a run-scoped session
* needs to know it is talking to a door that has no runs BEFORE it opens one.
*/
readonly teardownScopes?: readonly TeardownScope[];
}
/**
* Internal: registry entry keyed by tool name.
* Consumer never sees this shape.
*/
export interface ToolRegistryEntry {
readonly name: string;
readonly tool: Tool;
}
/**
* Convenience input for `defineTool` — flatter than `Tool` itself.
* Consumers describe the tool inline; the helper assembles `schema`.
*
* `inputSchema` is a JSON Schema object (the same one the LLM will
* see). For tools that take no arguments, pass `{ type: 'object',
* properties: {} }` or omit and we'll default to that.
*/
export interface DefineToolOptions<TArgs, TResult> {
readonly name: string;
readonly description: string;
readonly inputSchema?: Readonly<Record<string, unknown>>;
/** Declare a credential this tool needs (declare-and-push). Resolved by the
* framework before `execute` and injected as `ctx.credential`. */
readonly needs?: CredentialNeed;
/** Declare artifact arguments: arg name → required artifact kind (see
* {@link Tool.wants}). The model passes the `art_…` ref; the framework
* resolves it before `execute` and the handler reads the data. */
readonly wants?: ToolWants;
/** Demand a human check-in before this tool runs (see {@link Tool.checkIn}).
* `'always'` or a `(args, ctx) => boolean` predicate. */
readonly checkIn?: CheckInDemand<TArgs>;
/** The registered screen component that collects the check-in decision
* (see {@link Tool.checkInComponent}). Requires `checkIn`. */
readonly checkInComponent?: AskComponent;
/** Declare what this tool touches (see {@link Tool.capabilities}). Consulted
* only when the configured checker declares it governs them. */
readonly capabilities?: readonly ToolCapability[];
/** Refuse (never truncate) a result over this many chars, teaching the model
* to narrow (see {@link ToolResultCeiling}). Omitted → byte-identical. */
readonly resultCeiling?: ToolResultCeiling;
/** The declared class of this tool's results — `'triage'` or `'inventory'`
* (see {@link Tool.resultClass}). Keys the `check:semantics` per-class
* rules. Omitted → no class rules. */
readonly resultClass?: ToolResultClass;
/** The artifact kind a PLACED result is minted under, in the consumer's
* vocabulary (see {@link Tool.resultKind}). Omitted →
* `tool-result/<name>`, byte-identical. */
readonly resultKind?: string;
/** What this tool's ROWS contain — column name to type (see
* {@link Tool.resultColumns}). Needs the `checkColumnTypes` dial too.
* Omitted → nothing measured, byte-identical. */
readonly resultColumns?: ToolResultColumns;
/** The stamped identity edge — see {@link Tool.owner}. */
readonly owner?: ToolOwner;
/** The declared argument grounds — see {@link Tool.argumentsFrom}. */
readonly argumentsFrom?: readonly string[];
/** The named ingredient tools this tool calls through `ctx.tools` — see
* {@link Tool.composedOf}. Drift-checked at agent build, when the catalog
* is complete. Omitted → nothing checked, byte-identical. */
readonly composedOf?: readonly string[];
/** Whether this tool's procedure can raise an approval gate — see
* {@link Tool.gates}. Omitted → nothing declared, byte-identical. */
readonly gates?: boolean;
/** Fingerprint the repeated-call ledger on arguments alone, ignoring this
* tool's own result — see {@link Tool.repeatedWhen}. Omitted →
* byte-identical (the ledger keeps comparing results, as always). */
readonly repeatedWhen?: 'arguments';
/** Per-argument rules — what the library does when a call leaves an argument
* out (see {@link Tool.askOrAssume}). Omitted → byte-identical. */
readonly askOrAssume?: AskOrAssume;
/** Which argument sets the period the answer covers (see {@link Tool.period}).
* Requires a rule on that argument. Omitted → byte-identical. */
readonly period?: ToolPeriod;
/** What one call of this tool searches, classified (see {@link Tool.search}).
* Refused here when malformed. Omitted → byte-identical. */
readonly search?: ToolSearch;
/** `'read'` — this tool changes nothing (see {@link Tool.effects}). Omitted →
* byte-identical. */
readonly effects?: 'read';
/** This tool runs a program, carried by the one string argument `argument`
* (see {@link Tool.script}). Refused here when that argument is not a
* declared string property. Omitted → byte-identical. */
readonly script?: ToolScript;
/** The app's own words for what a call of this tool does, read by a person
* (see {@link Tool.displayName}). Omitted → byte-identical. */
readonly displayName?: string;
execute(args: TArgs, ctx: ToolExecutionContext): Promise<TResult> | TResult;
}
/**
* Ergonomic builder for `Tool`. Equivalent to constructing an object
* literal with `schema` + `execute`, but flatter and safer — the name
* + description live alongside the executor so they can't drift.
*
* @example
* const weather = defineTool<{ city: string }, string>({
* name: 'weather',
* description: 'Get current weather for a city',
* inputSchema: {
* type: 'object',
* properties: { city: { type: 'string' } },
* required: ['city'],
* },
* execute: async ({ city }) => `${city}: 72°F sunny`,
* });
*
* const agent = Agent.create({ provider }).tool(weather).build();
*/
export declare function defineTool<TArgs = Record<string, unknown>, TResult = unknown>(options: DefineToolOptions<TArgs, TResult>): Tool<TArgs, TResult>;