Skip to main content

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?​

optional endsRun?: 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?​

optional maxOutputChars?: 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?​

optional policy?: 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?​

optional timeoutMs?: number

Defined in: contracts/types.ts:454

Overrides IToolRegistryConfig.toolTimeoutMs for this tool (D60).


visibleWhen?​

optional visibleWhen?: 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:

  • userMessage must 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".
  • status tells the model the class of the failure (a 404 is not a 503); code is the locator an end user can quote to support, and is stamped on the stored result as IToolResultPart.errorCode.
  • canRetry: false adds "Do not retry this call." for a failure that will repeat with the same input.
  • message and data are for the developer only: they go to the server log and to IToolRegistryConfig.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​

IUserContext

Who is calling.

Returns​

Promise<TOutput>