Interface: IToolDef<TInput, TOutput>
Defined in: contracts/types.ts:389
One tool as the application defines it: the name and description the model reads, the Zod schema of the input, the
policy, when it ends the run, when it is offered, its limits, and the execute that does the work.
Written by the application with defineTool, so TInput and TOutput are inferred from inputSchema and execute;
passed in IToolRegistryConfig.tools or, for one call, in IGatewayRunInput.additionalTools. The ToolRegistry
parses the input and executes it, the ToolPolicy decides and narrows it, the LLMClient hands it to the model.
Type Parameters
TInput
TInput = unknown
TOutput
TOutput = unknown
Properties
description
description:
string
Defined in: contracts/types.ts:393
What the model reads to decide when to call it. Precise text matters.
endsRun?
optionalendsRun?:boolean| ((args) =>boolean|Promise<boolean>)
Defined in: contracts/types.ts:437
Whether a successful call of this tool ends the run (D86). Omitted = never.
For tools that are terminal by nature: a hand-over to a human agent, an escalation, a placed order.
true: every call. A predicate: per call, on the parsed input and the tool's output (ended only when the hand-over
was accepted). Evaluated after the tool returned, so a call that failed, timed out, was refused or was rejected by the
human never ends the run. The other calls of the same step still run; then the model gets one last step without tools to
phrase the answer, and the response names the tool in endedByTool.
With a predicate, write execute before endsRun in the tool literal: output is typed from the return type of
execute, and TypeScript infers the two functions in the order they are written, so a predicate written first sees unknown.
Examples
true - `handover_to_agent`: once a human has the case the assistant has nothing more to do
({output}) => output.status === "accepted" - ends only when the hand-over went through
Default
false
inputSchema
inputSchema:
ZodType<TInput>
Defined in: contracts/types.ts:395
Zod schema of the input; ToolRegistry parses the model's input with it before execute.
maxOutputChars?
optionalmaxOutputChars?:number
Defined in: contracts/types.ts:456
Overrides IToolRegistryConfig.maxOutputChars for this tool (D60).
name
name:
string
Defined in: contracts/types.ts:391
Unique, snake_case; the model refers to it by this name.
policy?
optionalpolicy?:EToolCallVerdict| ((args) => TToolCallPolicyResult | Promise<TToolCallPolicyResult>)
Defined in: contracts/types.ts:418
What happens to a call of this tool before it runs (D87): it runs, it is refused, or a human must approve it.
One rule per tool, decided per call on the parsed input, outside the tool's own code.
A verdict applies to every call: NEEDS_APPROVAL (a refund, an email), DENY (a kill switch for a tool that stays
registered), ALLOW (the same as omitting the field). A predicate decides per call on the parsed input and the user
context, sync or async (it may look something up), and returns a TToolCallPolicyResult. The rules apply in this order:
an input the schema rejects never reaches the policy (execute reports it to the model); then this field; then the
profile's approvals as the floor, which turns an ALLOW into NEEDS_APPROVAL. DENY wins over everything: a human is
never asked to approve a call that would be refused. In a parallel step a predicate decides each call after the earlier
calls of the step ran (D112), so it may read state they change. An input a human edited on approval is decided again (D109): a
DENY refuses it, a NEEDS_APPROVAL runs it (the human approved that input).
Examples
EToolCallVerdict.NEEDS_APPROVAL - every refund waits for a human
({input}) => input.amount > 500 ? {verdict: EToolCallVerdict.NEEDS_APPROVAL, rejectionText: `The refund of ${input.amount} was declined.`} : {verdict: EToolCallVerdict.ALLOW} - small refunds run, large ones wait
async ({input}) => (await charges.get(input.chargeId)) ? {verdict: EToolCallVerdict.ALLOW} : {verdict: EToolCallVerdict.DENY, reason: "There is no charge with this id."} - refuses what cannot work
Default
EToolCallVerdict.ALLOW
tags
tags:
string[]
Defined in: contracts/types.ts:397
Free tags for policy filtering, e.g. "billing".
timeoutMs?
optionaltimeoutMs?:number
Defined in: contracts/types.ts:454
Overrides IToolRegistryConfig.toolTimeoutMs for this tool (D60).
visibleWhen?
optionalvisibleWhen?:TToolVisibleWhenHandler
Defined in: contracts/types.ts:452
In which steps of a run the model is offered this tool (D89). Omitted = every step.
For a tool that belongs to one moment of the run: a classifier of the request on the first step, a report only after the data was fetched.
Made for agents with many tools: a classifier offered on the first step turns the request into a stated intent, so the
model picks the next tools from that result instead of guessing from the prompt. Decided before every model call, on the step index and the tools executed so far in this run. A tool hidden for a step
is not in the model's tool list and cannot be called by name in it (D72). requires of IToolPolicyConfig is the same
rule as config; a tool must pass both. Visibility is not a forced call: the model may still answer without calling it.
Examples
({stepIndex}) => stepIndex === 0 - `classify_intent` is offered only on the first step
({executed}) => executed.includes("lookup_order") - offered once the order was looked up, like `requires: {refund: ["lookup_order"]}`
Methods
execute()
execute(
args):Promise<TOutput>
Defined in: contracts/types.ts:480
Executes the tool. Called only by ToolRegistry.execute, under the tool's timeout: a tool that has not answered by then
fails with a 504 the model reads as a tool error, and abortSignal is aborted so the tool can stop its own work (D60).
The same signal is aborted when the call is stopped by its caller (IGatewayAskInput.abortSignal / timeoutMs); the gateway
stops waiting then, so a tool that ignores the signal keeps running for nothing, and an abort never undoes what it did.
To tell the model what went wrong, throw a dynaError with a userMessage. A thrown error never ends the run: it
becomes the tool's result, which the model reads to recover (ask the user for the missing data, try another way, answer
without the tool). The model reads one line, code: <code>, status: <status>, userMessage: <userMessage>, so:
userMessagemust be safe to show to the end user — no hosts, ids of internal systems, stack or SQL — and say how to proceed: "No order with id 4711; ask the customer for the number on the invoice", not "Query failed".statustells the model the class of the failure (a 404 is not a 503);codeis the locator an end user can quote to support, and is stamped on the stored result asIToolResultPart.errorCode.canRetry: falseadds "Do not retry this call." for a failure that will repeat with the same input.messageanddataare for the developer only: they go to the server log and toIToolRegistryConfig.onToolError, never to the model.
Any other error — a plain Error, a dynaError without userMessage — is treated as a bug of the tool or an outage of
what it calls: the model reads a generic text that says the input was not the cause (status 500, the gateway's code), the
error itself is logged and reported through onToolError. So a tool that lets its HTTP client's error through leaks
nothing; a tool that wants the model to recover must catch and rethrow with a userMessage.
Parameters
args
abortSignal
AbortSignal
Aborted at the tool's timeout or when the call is aborted. Pass it to fetch / the SDK of the service the tool calls.
input
TInput
Validated input.
threadId
string
Thread of the call.
userContext
Who is calling.
Returns
Promise<TOutput>