/** * Authoring and dispatching a tool: the mechanical half of writing one, which * every host had been reimplementing. * * A `ToolPlugin` is a bundle whose `execute` dispatches by tool name, because * that is the shape a plugin with shared setup wants. It is not the shape a * *single* tool wants, and a host with a flat list of tools ends up writing the * same four steps for each: switch on the name, parse the arguments, map a * parse failure onto a result the model can read, and encode the result as a * `role:"tool"` message. All four are mechanical, all four are easy to get * subtly wrong (the usual bug is a parse failure thrown rather than returned, * which turns a recoverable "you passed the wrong argument" into a dead run), * and none of them are where a host's judgement belongs. * * {@link defineTool} and {@link pluginFromTools} do those steps. They are a * convenience over the vocabulary in `./tool`, not a replacement for it: a * plugin that needs shared setup across its tools, or whose dispatch is genuinely * one decision, still writes `ToolPlugin` by hand and loses nothing. */ import { z } from "zod"; import type OpenAI from "openai"; import type { ToolAnnotations, ToolDef, ToolPlugin, ToolResult } from "./tool.js"; /** * A tool that carries its own implementation. * * `execute` takes `unknown` and does the parsing itself, which is what lets a * heterogeneous array of tools — each with a different argument type — sit in * one `tools: DefinedTool[]` without a cast anywhere. It also makes a defined * tool independently useful: dispatch it directly and you still get argument * validation, without going through {@link pluginFromTools}. */ export interface DefinedTool extends ToolDef { /** * Run this tool against unvalidated arguments, parsing with `parameters` * first. A parse failure comes back as a `validation` result rather than a * throw, because the model can fix it on the next turn and a throw would end * the run instead. * * Does **not** apply `normalizeArgs` — that is the dispatcher's step, run * before the idempotency hash. Applying it here too would apply it twice. */ execute(args: unknown, ctx: TCtx): Promise> | ToolResult; } /** * The declaration side of {@link defineTool}. `schema` is the single source of * truth: it is what the model is shown (converted to JSON Schema) and what the * model's arguments are validated against, so the two can never drift. */ export interface ToolSpec { readonly name: string; readonly description: string; readonly schema: TSchema; execute(args: z.output, ctx: TCtx): Promise> | ToolResult; readonly annotations?: ToolAnnotations; readonly hidden?: boolean; readonly supportsProgress?: boolean; /** * Pre-computed JSON Schema, for a tool authored as raw JSON Schema rather * than zod. `schema` still validates the arguments. * * It is also the one way to author a tool whose `schema` is not * parse-idempotent (a `.transform()`, which `z.toJSONSchema` refuses to * convert). Doing so makes the schema's idempotence your responsibility: * `execute` parses defensively, so a dispatcher that already parsed hands * this a value the schema must still accept. */ readonly rawJsonSchema?: Record; /** * Pure canonicalization of already-validated arguments — sorting a set-like * array, lower-casing a key. Runs **after** the schema parse, per * `ToolDef.normalizeArgs`, so it receives defaults already applied and a * shape it can rely on; a normalizer handed raw model output would have to * re-check everything the schema just checked. * * Applied by the **dispatcher**, before the idempotency hash — not by * `execute`. It must be idempotent anyway (`f(f(x)) === f(x)`), because * nothing can stop a host applying it more than once. */ readonly normalizeArgs?: (args: z.output) => z.output; readonly summarizeActivity?: (args: unknown) => string | null; } /** * Define one tool from its schema and implementation. * * The returned value is an ordinary {@link ToolDef} with an `execute` attached, * so it drops into anything that already consumes `ToolDef` — a catalog * renderer, a schema regression test — without an adapter. */ export declare function defineTool(spec: ToolSpec): DefinedTool; export interface PluginSpec { readonly name: string; readonly description: string; readonly tools: readonly DefinedTool[]; readonly systemMessage?: string; readonly icon?: string; readonly isAvailable?: () => boolean; } /** * Bundle self-contained tools into a {@link ToolPlugin}. * * The generated `execute` is only a name resolver — each tool already validates * its own arguments (see {@link DefinedTool}). An unknown name is a *returned* * `not_found` failure rather than a throw: it happens whenever a resumed * session's history references a tool that has since been retired, and a run * should survive that. */ export declare function pluginFromTools(spec: PluginSpec): ToolPlugin; /** * Convert a tool to the wire definition a provider is shown. * * `rawJsonSchema` wins when present (an MCP tool forwards its server's schema * verbatim); otherwise the zod schema is converted. Either way the result goes * through {@link sanitizeToolSchema}, because a strict validator rejects the * *entire* request on the first unsupported construct — one bad tool takes * every other tool down with it. * * `wireName` exists because tool naming is host policy: Monad encodes * `plugin__tool` so it can route a call back to its plugin, and a host with a * flat namespace does not need to. Defaults to the tool's own name. * * Returns the narrow `ChatCompletionFunctionTool` rather than the * `ChatCompletionTool` union — a tool built from a parameter schema is always * the function variant, and returning the union would make every caller narrow * past a `custom` case that cannot occur. It still assigns to the union. * * Converts whatever it is handed. **Skip `hidden` tools in the caller's catalog * loop** — a hidden tool stays runnable so a resumed session's history still * resolves, but advertising it puts a retired tool back in front of the model. */ export declare function toolWireDefinition(tool: ToolDef, wireName?: string): OpenAI.ChatCompletionFunctionTool;