import type { z } from 'zod' import type { Logger } from '../../utils/logger.js' import type { CodeNavigationProvider } from '../code-navigation/index.js' // Type-only, and circular by design: a tool-result guardrail is described in // terms of the tool that produced the result, and the registry that holds // the guardrails is described here. Erased at compile time, so neither // module exists at runtime to depend on the other. import type { ToolResultGuardrailSpec } from '../guardrail/index.js' import type { SessionId, TurnId } from '../ids/index.js' import type { InvocationState } from '../invocation/index.js' import type { PermissionMode } from '../permission/index.js' import type { Sandbox } from '../sandbox/index.js' import type { ToolPresentation } from './presentation.js' export interface ToolRegistryRef { searchDeferred(query: string): ToolDefinition[] /** Ranked active matches, when this registry supports active-tool discovery. */ searchActive?(query: string): ToolDefinition[] activate(names: string[]): void getAvailability(name: string): ToolAvailability } /** * The slice of the skills registry a tool is given. * * Structural for the reason `ToolRegistryRef` is: this file is imported by * everything, and naming `SkillRegistry` here would drag the skill loader's * filesystem imports into every consumer's type graph. */ export interface SkillRegistryRef { /** * Enumerate the metadata the model may use to discover a skill. * * Optional so an older structural registry that only supports named loads * remains a valid host. `SkillTool` refuses its list mode when this member is * absent: `names()` cannot distinguish a model skill from an operator-only * one, so treating it as a safe catalog would disclose an authority boundary. */ catalog?(): | readonly { /** The name the registry accepts, which may be namespaced by its host. */ registeredName: string description: string location: string allowedTools?: string invocation?: 'model' | 'operator' | 'both' }[] | Promise< readonly { registeredName: string description: string location: string allowedTools?: string invocation?: 'model' | 'operator' | 'both' }[] > /** * Load a skill's full body, or `undefined` for a name nobody registered. * * Full disclosure by design: a tool call asking for a skill is asking * for its instructions, and returning metadata the model already has in * its manifest would answer a question it did not ask. */ load(name: string): Promise< | { skill: { metadata: { name: string description: string allowedTools?: string /** * The literal union, not `string`. Widening it here would * let a ref satisfy this interface while carrying a value * `isInvocableBy` cannot read, and the failure would be a * silent `both` — the fail-open answer. */ invocation?: 'model' | 'operator' | 'both' } body?: string } } | undefined > /** Every registered name, for a "did you mean" that names real options. */ names(): readonly string[] } /** * The slice of the background job registry a tool is given. * * A structural reference rather than the class, for the reason * `ToolRegistryRef` exists: this type file is imported by everything, and * naming the implementation here would drag a `node:child_process` module * into every consumer's type graph. `owner` is not on this surface at all — * the executor binds it to the turn, so a tool cannot start a job that * outlives, or is billed to, somebody else's run. */ export interface BackgroundJobRegistryRef { start(params: { command: string; workingDirectory: string }): { id: string status: string } get(id: string): { id: string; status: string; exitCode?: number } read( id: string, opts?: { fromOffset?: number }, ): { chunk: string nextOffset: number droppedBytes: number status: string exitCode?: number } kill(id: string): Promise<{ id: string; status: string }> list(): readonly { id: string; command: string; status: string }[] /** * Await this job's exit instead of reading it in a loop; resolves at * once for a job that has already stopped. Optional, and added after * the rest of this surface: a host implementing this interface directly * rather than through `bindOwner` may not have it yet, so `wait_for_job` * checks for it and says so rather than assuming every host can wait. */ waitForExit?( id: string, opts?: { signal?: AbortSignal }, ): Promise<{ id: string status: string exitCode?: number }> /** * Say that the model is waiting on this job, so the turn stays open for it * when the model stops calling tools. * * Called by `wait_for_job` and nothing else. The kernel holds a finishing * run open — bounded, and for no model tokens — only for a job marked * here; a job nobody marked never delays a turn, which is what a dev server * or a watcher needs. Optional for the reason `waitForExit` is: a host * implementing this interface directly may have nowhere to record the * intent, and then there is simply no hold. */ markAwaited?(id: string): void } /** * Tracks which files the agent has read in the current turn. * Write tool consults this to enforce the "read before overwrite" invariant * an existing file must be read first or the write fails. * Keys are the resolved path used by the tool — sandbox-relative when a sandbox * is active, absolute (`workingDirectory`-resolved) otherwise. */ export interface FileReadTracker { /** * `content` lets the tracker fingerprint what was read, which is what * makes drift detectable later. Optional so a host that only needs the * read-before-overwrite guard can keep its existing implementation. * `fullWriteCallId` links a successfully written complete body to its * execution; pass ToolContext.toolUseId only after the write succeeds. */ recordRead(key: string, content?: string, fullWriteCallId?: string): void hasRead(key: string): boolean /** * Optional witness of a successful full-body write, supplied by the executing * tool through recordRead's third argument. An unchanged later observation * preserves it; different or unknown content clears it. Never infer from a name. */ writeCallId?(key: string): string | undefined /** * Optional chain of calls whose bodies compose the current content: the * full-body write that started it, then the successful edits applied on * top of it, in order. * * Defined only while at least one edit sits on a witnessed write — exactly * when `writeCallId` is not. An edited file's body is no longer the write * call's body, and a consumer that knows only `writeCallId` has to keep * seeing nothing there rather than a claim that has quietly stopped being * true. The chain names INPUTS: reconstructing the body means replaying * those calls' visible arguments and checking the result against * `fingerprint(key)`, never asserting it. */ editChain?(key: string): { rootWriteCallId: string; editCallIds: readonly string[] } | undefined /** * Optional record of a successful edit's resulting content, with the call * that produced it. * * Does everything `recordRead(key, content)` does, and additionally extends * the chain when this edit ran against content the ledger already had a * fingerprint for and a witnessed write underneath it — the two facts that * make the replay reproducible. Missing either, it is an ordinary * observation of a body nobody can replay: the fingerprint advances and the * chain is cleared. Pass ToolContext.toolUseId only after the edit succeeded. */ recordEdit?(key: string, content: string, callId: string): void /** * Optional witness of a read that returned the file WHOLE, with the * fingerprint of the rendering it returned. * * Does everything `recordRead(key, content)` does, and additionally records * that the body is visible in `callId`'s receipt — not as text this ledger * holds, but as the exact rendering `callId` emitted, so a consumer can * check the receipt it can see against `renderedFingerprint` before * referencing it. A partial read never calls this: a window proves nothing * about the rest of the file. Pass `ToolContext.toolUseId` and the * fingerprint of the tool's own output string. * * The witness is recorded only where no write witness or chain survives the * observation, because a write-rooted body is one the model composed and * can be replayed through. It is cleared by exactly what clears a write * witness — different or unknown content — and additionally by any * `recordEdit`, because a read roots no chain: the body it witnesses exists * only as a rendering, and nothing may be replayed on top of it. */ recordFullRead?(key: string, content: string, callId: string, renderedFingerprint: string): void /** * Optional witness of the full read above: the call whose receipt holds the * body, and the fingerprint of what that call emitted. * * `renderedFingerprint` is deliberately not the body's fingerprint — * `fingerprint(key)` is that. It describes the RECEIPT, so a consumer * admits the reference only while the receipt it can see is byte-for-byte * what the tool returned; an elided, spilled or cleared one is not. * * The derived work context asks a tracker for `writeCallId` and * `fingerprint` before it reads any witness at all — a name alone does not * say the ledger is the one execution wrote — so a tracker implementing * this pair and neither of those still establishes nothing. */ readWitness?(key: string): { callId: string; renderedFingerprint: string } | undefined /** * Optional note that a built-in mutation has just refused this path because * the body on disk differs from the fingerprint above. * * Not an observation. The refusing tool read the disk, but all it reports * here is the disagreement — recording the body it found would re-baseline * the drift check and admit the very mutation that was refused. An * implementation must therefore leave `fingerprint`, `hasRead`, * `writeCallId`, `editChain` and `readWitness` exactly as they were, and any * later content observation clears the flag. It exists so a consumer that * cannot read the filesystem — the derived work context — can stop * referencing a body it has been told is stale, without a check of its own. */ recordDriftObserved?(key: string): void /** Whether a refused mutation has reported this path stale since the last observation. */ driftObserved?(key: string): boolean /** * Fingerprint of the body captured at the last read, when one was. * * A file mutation is computed against what the agent READ, and between * that read and the write the file may have moved under it — a person * editing in an editor, another process, a second agent. The in-process * lock cannot see any of those. Comparing this against the body actually * on disk at mutation time is what turns a silent lost update into a * refusal the agent can act on by re-reading. */ fingerprint?(key: string): string | undefined } export interface ToolPauseOption { readonly id: string readonly label: string readonly description?: string } export interface ToolPauseRequest { /** * Names this pause within the call. * * A tool call may pause more than once — "which environment", then * "are you sure" — and the answers have to be told apart. The name is * what a resume payload is routed by, so it is the author's to choose * and should describe the decision, not the tool. */ readonly name: string /** The question, in the words the human will read. */ readonly prompt: string /** Short topic label, when the surface showing this has room for one. */ readonly header?: string readonly options?: readonly ToolPauseOption[] readonly multiSelect?: boolean /** Defaults to true: a human who disagrees with every option can say so. */ readonly allowFreeText?: boolean } /** * How a pause ended. * * `unanswered` is deliberately not a variant of `answered` with an empty * selection. A tool that pauses to ask "may I charge this card" and reads * silence as yes is worse than one that never asked, so the absence of an * answer has its own shape and cannot be destructured into consent by * accident. */ export type ToolPauseOutcome = | { readonly status: 'answered' readonly selectedOptionIds: readonly string[] readonly text?: string } | { readonly status: 'unanswered'; readonly reason: string } | { readonly status: 'aborted' } export type RequestToolPause = (request: ToolPauseRequest) => Promise /** * Where one tool execution entered the runtime from. * * The executor owns this value. A nested caller supplies only its private * operation context to {@link ToolContext.dispatchTool}; the executor derives * the parent id from the context it already issued, so a tool cannot relabel * itself as somebody else's child. */ export type ToolCallSource = | { readonly kind: 'direct' } | { readonly kind: 'nested'; readonly parentToolUseId: string } | { readonly kind: 'code' readonly parentToolUseId: string /** The code runtime's id, unique within the parent program. */ readonly runtimeToolCallId: string } /** * Operation authority a trusted tool may narrow when it dispatches another * tool. * * `signal` is fused with the parent call's signal; it can revoke authority * earlier but can never extend the parent's lifetime. `runtimeToolCallId` * identifies one request inside a model-authored program. The executor still * chooses the child's event identity and the parent lineage; durable pause * ownership stays with the model-issued ancestor present in the checkpoint. */ export interface ToolDispatchOptions { readonly signal?: AbortSignal readonly runtimeToolCallId?: string } export interface ToolContext { /** * Host-owned read snapshot. Bound to this tool call's lifetime, including its * deadline; an optional signal can cancel capture earlier. Never replays * effects. Unsupported stores return undefined. */ captureSessionEvidence?: ( maxReadBytes?: number, signal?: AbortSignal, ) => Promise /** The session this call belongs to. */ sessionId: SessionId /** The turn this call belongs to. */ turnId: TurnId workingDirectory: string /** * Directories besides the working directory the file tools may reach, * absolute. A host adds one for a session (`/add-dir`); a sandboxed turn * binds each. Relative paths still resolve against the working * directory; an absolute path inside any of these is accepted. */ additionalDirectories?: readonly string[] /** * Absolute paths outside the working directory and the added directories * that THIS call was approved to reach. * * Set by the executor only for a call a review approved after the * kernel named these paths in it (`ToolCallSummary.escalation`), and only * when the turn asked for that (`QueryParams.outsideRootAccess: * 'review'`). The file tools treat each as one more root for this call * alone; nothing here widens the next call. Absent is the ordinary case: * a path outside the roots is refused. */ approvedPaths?: readonly string[] /** * This call was approved to run outside the turn's sandbox. * * Set by the executor only when a reviewer CONFIRMED the escape for this * call by id (`HITLResumeDecision.confirmedEscalations`) and the turn * allows escapes at all (`QueryParams.sandboxEscape: 'review'`). A tool * that offers an escape honours it only when this is `true`, and refuses * the request otherwise. */ sandboxEscapeApproved?: boolean abortSignal: AbortSignal env: Record log: (level: 'info' | 'warn' | 'error', message: string) => void permissionContext?: { mode: PermissionMode sessionId: string turnId: string workingDirectory: string } invocationState?: InvocationState toolRegistry?: ToolRegistryRef /** * The names this turn may call, if the turn was narrowed. * * Enforced at dispatch, not only used to decide which schemas the model is * shown. It was the latter alone for a while, which made the narrowing * presentational: a step could withhold a tool from the request and the * executor would still run it when the model named it anyway — from * repeated context, from a gateway with its own tool memory, or from a * replayed prefix. * * Absent means no narrowing, which is not the same as an empty list: an * empty list is a turn that may call nothing. */ allowedTools?: readonly string[] sandbox?: Sandbox /** * Symbol resolution for this turn, when a host wired one up. * * Absent means the `lsp` tool is not registered at all — see * `tools/builtins/lsp.ts` for why a tool that is always present and * always says "unavailable" is worse than one that is not there. */ codeNavigation?: CodeNavigationProvider fileReadTracker?: FileReadTracker /** * Where work that outlives this call is held. * * Absent means the host has provided nowhere to put a background job, and * a tool asked for one must REFUSE rather than fall back to `cmd &`. The * fallback is not a lesser version of this — under the local sandbox's * `linux-namespace` tier the wrapping `sh` is PID 1 of a fresh PID * namespace, so a backgrounded grandchild dies the moment that shell * exits, on the successful path. It returns in milliseconds looking like * it worked, with the work already dead. It is also absent from sandboxed * query-built contexts: the shipped registry owns host processes and * cannot preserve a sandbox boundary. */ backgroundJobs?: BackgroundJobRegistryRef /** * Where the `skill` tool reads from. * * Absent means the turn has no skills, and the tool says so rather than * reporting an empty list — "no skills here" and "no registry" are * different answers. */ skills?: SkillRegistryRef /** * How this turn reaches the web. * * Two independent halves, and either may be absent. This kernel ships a * guarded fetch provider and NO search backend, so `search` missing is * the ordinary case rather than a failure — the tools say which piece is * missing so an operator can tell a wiring decision from a fault. */ web?: { readonly fetch?: { fetch(request: { url: string; signal?: AbortSignal }): Promise<{ url: string status: number contentType?: string body: string truncated: boolean redirects: readonly string[] }> } readonly search?: { search(request: { query: string limit?: number signal?: AbortSignal }): Promise<{ query: string hits: readonly { title: string; url: string; snippet?: string }[] }> } } /** * Adopt the tool scope a skill declared. * * Called by the `skill` tool when a loaded skill names `allowed-tools`. * The scope INTERSECTS what the turn already allows and takes effect from * the next batch — a skill loaded alongside other calls must not * retroactively refuse them. */ adoptSkillScope?: (scope: { skill: string allowedTools: readonly string[] }) => void /** * Effective model-visible character cap for this tool result. * * Present on executor-owned calls so a tool that can paginate does so * before the generic head+tail fallback loses its middle. `0` means the * host disabled the cap; absent means a direct host invocation did not * declare one. */ maxToolOutputChars?: number /** * Screens the TURN asked for, applied to results this call produces. * * Worth having because a turn usually does not build its registry: a host * assembles one and hands it to `runAgent`, so a registry-construction * option alone is the host's to write and the kernel's default reaches * nobody. * * The registry's own {@link ToolRegistryConfig.resultGuardrails} WIN when * the registry was built with them — including an empty array, which means * none — because a registry that stated its policy has stated it. These * apply to a registry that declared none, which is the ordinary case: a * host assembles a registry and hands it to a turn it does not own. * * `undefined` means the turn declared none; an empty array means the turn * declared none ON PURPOSE, which is how a caller turns off a screen the * executor would otherwise install by default. */ toolResultGuardrails?: readonly ToolResultGuardrailSpec[] /** * Run another tool through the same dispatch this call came through. * * For `run_code`, whose whole purpose is calling tools in a loop. NOT * added to {@link ToolRegistryRef}: that ref is about discovering and * activating tools, and putting `execute` on it would make "can dispatch" * a property of holding a registry reference rather than a capability a * host wired deliberately. * * Available only for this invocation. The executor closes it when the * visible call settles or is abandoned, aborts calls already started by * it, and waits for their executor-owned terminal records before reporting * the parent complete. Retaining the function does not retain authority. * * Tools are host-installed code — the model cannot add one — so the trust * boundary this protects is the MODEL's reach. `allowedTools` is enforced * again at dispatch. When the turn has an operator authorization gate, a * nested call must be explicitly allowed by that gate; a deny or an * undecided call fails closed because another durable human review cannot * be opened from inside the already-executing parent. */ dispatchTool?: ( name: string, input: unknown, options?: ToolDispatchOptions, ) => Promise /** * The `tool_use_id` of the assistant block that triggered this * execution. Tools that spawn background work (e.g. coordinator * `create_task`) thread this id into their tracking metadata so * a later, asynchronous completion can be replied back as a * canonical `tool_result` content block bound to the same id. * Optional because not every executor path provides it yet. */ toolUseId?: string /** * Stable, turn-scoped identity shared by every direct tool call the model * issued in the same response batch. * * `toolUseId` answers "which call is this?"; this answers "which sibling * calls were launched together?". Hosts use it to keep a concurrent group * visible while one sibling is still running without accidentally reviving * terminal work from an older batch or another turn whose provider reused a * call id. Optional because a host may invoke a tool directly, outside the * query executor. */ toolBatchId?: string /** * How this execution entered the tool registry. * * Present on executor-owned calls. Optional because a host may invoke a * tool directly outside a turn and construct its own minimal context. */ source?: ToolCallSource /** * Raise a durable pause and wait for a human to resolve it. * * The pause machinery is excellent and was reachable from exactly four * kernel-owned points — the plan gate, the tool-review gate, the * iteration cadence, and one built-in question tool. A host-authored * tool had no seam to it, so the operations that most want their own * confirmation with their own wording (a spend, an outbound post, a * destructive migration) had to settle for the generic tool-review * gate or hand-thread a recorder and a resume callback into a private * builder, which nothing in this type suggested was possible. * * The park is a real checkpoint, so a host can see the pause on every * surface a tool-review park appears on, and the answer routes back by * name on resume — several tools pausing in one batch each get their * own, and one tool may pause more than once. * * Absent when whatever is driving the tool provides no route to a * human — a host calling a tool directly, outside a turn. A tool must * treat it as optional and decide what to do without one, and must * never read an unanswered pause as consent; the outcome says which it * was, in its own shape, so silence cannot be destructured into a yes. */ requestPause?: RequestToolPause /** * Span to parent this tool's `execute_tool` span to. * * OTel's GenAI conventions define a strict hierarchy — * `invoke_agent` → `chat {model}` → `execute_tool` — and vendor * dashboards rely on it. `startActiveSpan` cannot supply the parent * here: the span-owning bodies upstream are async GENERATORS, and a * generator resumes on its consumer's async context, so the ambient * context is already gone by the time a tool runs. Passing the parent * explicitly is the only approach that actually works, and * `ToolContext` is already threaded to exactly the right place. */ parentSpan?: import('@opentelemetry/api').Span /** * Say how far along you are, for a host rendering a live view. * * A tool may run for the full per-tool deadline — two minutes by * default — and before this it was silent for all of it: a host could * show that a build had started and then nothing until it finished or * timed out. * * Fire-and-forget and never throws, so a tool can call it freely * without wrapping it. This is latest state, not a log: while a host is * consuming one update, later calls replace the single pending update. * Each published message is capped at 8 KiB of UTF-8 with a visible * omission marker, and every accepted update settles before that call's * terminal event. Put complete output in `ToolResult`, not here. * * The model never sees these: progress answers "is it still working?", * which is a question only a human asks, and putting it in the * conversation would spend tokens telling the model something it cannot * act on. * * Absent when the executing surface has no event stream to write to. */ report?: (message: string, fraction?: number) => void } export interface ToolResult { success: boolean output: string data?: unknown error?: string /** * Rich content for the MODEL, when a string cannot carry it — a * screenshot, a chart, a PDF. `output` stays the text the host and the * transcript see; when this is set it is what reaches the provider. * * Keeping the two separate is deliberate: a host UI wants "screenshot * (1280x800)", the model wants the pixels, and forcing one channel to * serve both is what made `computer-use` send megabytes of base64 as * text. */ content?: import('../message/index.js').ToolResultContent /** * This failure might succeed if tried again — a network blip, a lock * contention, a rate limit — as opposed to one that never will, like a * missing file or a rejected argument. * * Nothing distinguished the two before, so a transient failure cost a * full model round trip to retry: the error went back as a * `tool_result`, the model read it, and decided (or didn't) to call * again. Only meaningful alongside {@link ToolDefinition.maxRetries}; * a tool that has not opted into retries is never retried no matter * what it sets here. */ retryable?: boolean /** * Facts this result pins into the turn's working memory, by key. The * kernel keeps them in the working-memory slot — in front of the model * every iteration, across compaction — and a later pin under the same * key replaces the earlier one. For what a tool knows and the model * must not lose: what its controls do, where things are, what failed. */ workingState?: readonly import('../../compaction/types.js').WorkingStatePin[] } export interface ToolDefinition extends ToolPresentation { name: string description: string inputSchema: z.ZodType /** * Optional canonical JSON Schema shown to models instead of the runtime * Zod schema. Use this when runtime compatibility accepts aliases or * constraints that should not be advertised to a model. * * This is intentionally independent of TInput: the model-facing contract * may be narrower than the execution decoder. */ modelInputSchema?: Record /** * Ask capable providers to constrain generated input to modelInputSchema. * ToolRegistry rejects this flag unless modelInputSchema is also present. */ enforceModelInput?: boolean /** * Concise, model-readable recovery guidance appended when inputSchema * rejects a call. Use for conditional schemas whose required shapes * cannot be reconstructed from JSON Schema's top-level `required` list. */ validationErrorHint?: string /** * The shape this tool returns, as JSON Schema, appended to the * description the model sees. * * JSON Schema rather than Zod because it is **shown, never validated**: * namzu does not check a tool's return value against it, so converting * through Zod would only cost fidelity. Native tools that want one can * render their Zod type with `renderToolSchema`. * * Optional and omitted by default — a tool whose return shape is * obvious from its description gains nothing from spending prompt on * it, and every tool schema rides in the cached prefix of every * request. */ outputSchema?: Record /** * The argument that holds a shell command line, when one does. * * Declared on the tool because only the tool knows. An operator writing * `bash = { "git status*" = "allow" }` means the `command` argument, and * every other way of learning that is a list somewhere else that drifts: * a host compiling permissions has the tool's NAME and no reason to know * which of its arguments is the interesting one. * * What it buys is the difference between a permission about text and a * permission about commands. A rule routed through this argument reads * `git status && rm -rf ~` as the two commands it is, so an `allow` * written for `git status*` declines it instead of approving it on the * strength of the first few words. See `decomposeCommandLine`. * * Omit it on every tool that takes no command line. A tool that runs * something but takes an argument list rather than a line — no shell, no * chaining — has nothing to decompose and must not claim otherwise. */ commandArgument?: string /** * The argument that holds a filesystem path the tool resolves against the * turn's roots (the working directory and the added directories). * * Declared so the kernel can see, BEFORE the call runs, that it names a * path outside those roots and turn it into an approval request instead * of a refusal (`QueryParams.outsideRootAccess: 'review'`). A tool that * does not declare it keeps refusing such a path at execution, which is * the safe direction to be wrong in. */ pathArgument?: string /** * The boolean argument by which a call asks to run outside the turn's * sandbox, when the tool offers that at all (the shipped `bash` does). * * A call that sets it under a sandbox is always reviewed, is never * approved by a mode, a remembered grant or an `allow` rule, and runs * unconfined only when a reviewer confirmed it by id. See * `ToolContext.sandboxEscapeApproved`. */ sandboxEscapeArgument?: string execute(input: TInput, context: ToolContext): Promise tier?: string permissions?: ToolPermission[] category?: 'filesystem' | 'shell' | 'network' | 'analysis' | 'custom' /** * Deadline for a single execution, overriding the turn-level default. * * On expiry the executor stops waiting and returns a model-visible * error result, so a slow dependency becomes something the agent can * route around instead of a turn that never comes back. The tool's * `context.abortSignal` fires at the same moment; a tool that honours * it also stops doing work, and one that ignores it merely becomes * detached. * * Omit to inherit the executor's default. */ timeoutMs?: number /** * How many times a FAILED execution may be retried in-loop before the * error is handed to the model. * * **Defaults to 0, and that default is load-bearing.** Retrying is only * safe if the tool is idempotent, and the SDK cannot know that: silently * re-running a `write_file`, a `git push` or a payment call is worse * than never retrying at all. The tool author opts in, per tool. * * Even then, only failures the tool marked * {@link ToolResult.retryable} are retried — a missing file is not * going to appear on the second attempt, and burning the budget on it * just delays the error the model needs to see. */ maxRetries?: number /** * This tool's output IS the turn's answer: settle with it instead of * asking the model to restate it. * * Every delegation path is blocking and returns the worker's final * text as the dispatching call's result, after which the loop went * round again — so a router agent, whose entire job is to pick a * specialist, paid one extra model call per request at the parent's * full context size, the most expensive call in the turn. The relay is * also LOSSY: the parent paraphrases the worker's answer through its * own (compacted) context, so what the caller receives is not what the * worker produced. * * Honoured only when the terminal call is the ONLY call in the turn * and it did not fail. A model that asked for other work in the same * turn meant to see those results, and ending the turn would discard * answers it requested; that turn takes the ordinary path and the * reason is logged. A failed terminal call is not an answer either — * the error goes back to the model, which is the point of returning * errors to it at all. * * Off by default. The generic case is `structured_output`, which the * runtime has always settled on; this is that rule made available to * any tool. */ terminal?: boolean isReadOnly?(input: TInput): boolean isDestructive?(input: TInput): boolean isConcurrencySafe?(input: TInput): boolean /** * Opt-in ordering boundary in a direct model tool-call batch. Earlier * calls settle before this call starts; later calls wait for this call * to settle (including failed results). Defaults to false. Independent * calls between barriers retain their existing concurrency-safe behavior. * Nested dispatch is owned by its enclosing call, not separately queued; * mark the enclosing tool as a barrier to isolate its nested operations. * Deadlines still abandon uncooperative tools: settlement does not prove * their external effects have stopped, and does not imply success. */ executionBarrier?: boolean /** * Where this tool came from, when it did not come from here. * * Absent means host-defined: this process, code the operator installed, * no untrusted party in the chain. Present means a connected server * supplied both the tool and its own description of what the tool does * — including whether it is read-only, which three separate gates were * treating as a fact rather than as the hint the wire calls it. * * See {@link isTrustedReadOnly}. This field exists so a gate can tell * the two apart; `isReadOnly` keeps reporting faithfully what the * server said, because the outbound re-export and the destructive * label shown to a human both need the server's own answer. */ provenance?: ToolProvenance } export interface ToolProvenance { /** The connected server this tool came from, named as configured. */ readonly server: string /** * The operator marked this server's read-only claims as trustworthy. * * Per server, never global: one switch meaning "trust annotations" * hands every connected server the same reach, which is the hole it * would be closing. Default false — an unmarked server's claim raises * the requirement and never lowers it. */ readonly readOnlyHintTrusted: boolean } export type ToolPermission = | 'file_read' | 'file_write' | 'shell_execute' | 'network_access' | 'env_access' export interface LLMToolSchema { type: 'function' function: { name: string description: string parameters: Record } } export type ToolAvailability = 'deferred' | 'active' | 'suspended' export type ZodToJsonSchema = (schema: z.ZodType) => Record export interface ToolTierDefinition { id: string label: string priority: number description?: string } export interface ToolTierConfig { tiers: ToolTierDefinition[] guidanceTemplate?: (tiers: ToolTierDefinition[]) => string labelInDescription?: boolean } export interface ToolRegistryConfig { logger?: Logger tierConfig?: ToolTierConfig /** * Screens run against every tool result before anything downstream * reads it — the output budget, compaction, and the model itself are * all past this point. * * Absent means no screening, which is what shipped before this existed: * a connected server's text reached the model unexamined. See * {@link ToolResultGuardrailSpec}. */ resultGuardrails?: readonly ToolResultGuardrailSpec[] } export interface ToolExecutionResult extends ToolResult { permissionDenied?: boolean permissionMessage?: string } /** * An input decoded exactly once by its owning tool registry. * * `input` is the detached, deeply frozen JSON review projection: * authorization, approval UI, probes and audit all inspect this value. The * registry privately retains a separate detached copy that * {@link ToolRegistryContract.executePrepared} gives the tool. Caller-owned * aliases therefore cannot change either side after preparation. A * preparation is registry-owned and cannot be executed by a different * registry or after that tool registration is replaced. */ export interface PreparedToolExecution { readonly toolName: string readonly input: unknown } /** Result of decoding a tool input at the execution boundary. */ export type ToolPreparationResult = | { readonly success: true; readonly prepared: PreparedToolExecution } | { readonly success: false; readonly result: ToolExecutionResult } /** * Full tool registry contract — registration, lookup, execution, prompt generation. * Concrete implementation: `ToolRegistry` in `registry/tool/execute.ts`. */ export interface ToolRegistryContract { register(id: string, tool: ToolDefinition): void register(tool: ToolDefinition, initialState?: ToolAvailability): void register(tools: ToolDefinition[], initialState?: ToolAvailability): void unregister(id: string): boolean clear(): void get(name: string): ToolDefinition | undefined getOrThrow(name: string): ToolDefinition has(name: string): boolean getAll(): ToolDefinition[] listIds(): string[] listNames(): string[] getAvailability(name: string): ToolAvailability activate(names: string[]): void defer(names: string[]): void suspendAll(): void hasSuspended(): boolean searchDeferred(query: string): ToolDefinition[] /** Ranked active matches, when this registry supports active-tool discovery. */ searchActive?(query: string): ToolDefinition[] getCallableTools(toolNames?: string[]): ToolDefinition[] /** * Decode/transform an input once, before authorization or human review. * * The returned preparation is opaque registry authority. Implementations * must not run tool code here. They must detach both the executable value * and `prepared.input` from caller/schema aliases, and make the latter a * deeply immutable JSON projection of the exact value retained for * execution. Unsupported mutable/exotic graphs fail closed. */ prepareExecution(toolName: string, rawInput: unknown): ToolPreparationResult /** Execute the exact value retained by `prepareExecution`, without parsing again. */ executePrepared( prepared: PreparedToolExecution, context: ToolContext, ): Promise execute(toolName: string, rawInput: unknown, context: ToolContext): Promise size(): number toLLMTools(toolNames?: string[]): LLMToolSchema[] toPromptSection(toolNames?: string[]): string toTierGuidance(): string | null assignTiers(mapping: Record): void } export * from './repair.js' export type { ToolCallView, ToolPresentation, ToolResultView, } from './presentation.js'