import { t } from "structural"; import type { Transport } from "../transports/transport-common.ts"; import { Result, ok } from "./result.ts"; import type { BuiltinIRRole } from "./ir-roles.ts"; import type { Content } from "./llm-ir.ts"; /* * Tool definitions * ------------------------------------------------------------------------------------------------- * * Tools have to define a few things: * * 1. A tool name. * 2. A caller-facing description of what the tool does. * 3. A schema for the input arguments from the LLM. * 4. A schema that we parse the input into. If we don't need parse the LLM input, this can be * autogenerated. * 5. A parse function that parses the input into the parsed form. If we don't need to parse the LLM * input, this can be autogenerated. * 6. A validate function that validates the parsed data at runtime, e.g. "does this file still have * this search string even though we performed two edits previously on it" * 7. A run function, which takes the tool data and runs it, returning some result */ export type ToolDef< Data, Name extends string, Arguments, Parsed, SubagentNames extends string, Extra, > = { name: Name; description: string; ArgumentsSchema: t.Type; ParsedSchema: t.Type; parse: ( args: ToolParseArgs, ) => Promise, string>>; validate: ( abortSignal: AbortSignal, transport: Transport, t: { original: Schema; parsed: Schema; }, data: Data, ) => Promise>; run: ( args: ToolRunArgs, ) => Promise, string>>; }; export type ToolParseArgs = { signal: AbortSignal; transport: Transport; original: Arguments; }; export type ToolRunArgs = { signal: AbortSignal; transport: Transport; toolCall: { toolCallId: string; original: Schema; parsed: Schema; }; data: Data; }; type ToolImplementationRunArgs< Data, Name extends string, Arguments, Parsed, CustomIR, > = ToolRunArgs & { customIR: CustomIR; }; export type ParseResult = { original: Arguments; parsed: Parsed; }; /* * Basic schema for tool calling */ type Schema = { name: Name; arguments: Arguments; }; /* * Tools can return: * * - output content * - subagent invocations (for their subagent dependencies) * - any custom LLM IR defined */ export type ToolReturn = | (Content & { type: "output"; // The line count to show in the UI, if it's not just the number of lines in the content lines?: number; }) | { type: "invoke-subagent"; name: SubagentName; } | { type: "custom-ir"; data: Extra; }; type RawToolFactory< Data, S extends AnyToolSchema, Parsed, SubagentNames extends string, Extra, > = (args: { signal: AbortSignal; transport: Transport; data: Data; }) => Promise | null>; type SimpleRawToolFactory< Data, S extends AnyToolSchema, Parsed, SubagentNames extends string, Extra, CustomIR, > = (args: { signal: AbortSignal; transport: Transport; data: Data }) => Promise< | (Omit< ToolDef, "ArgumentsSchema" | "ParsedSchema" | "description" | "name" | "validate" | "run" > & { run: ( args: ToolImplementationRunArgs, ) => Promise, string>>; } & Partial< Pick, "validate"> >) | null >; type SimpleAutoParsedRawToolFactory< Data, S extends AnyToolSchema, SubagentNames extends string, Extra, CustomIR, > = (args: { signal: AbortSignal; transport: Transport; data: Data }) => Promise< | (Omit< ToolDef, "ArgumentsSchema" | "ParsedSchema" | "description" | "name" | "parse" | "validate" | "run" > & { run: ( args: ToolImplementationRunArgs, ) => Promise, string>>; } & Partial< Pick< ToolDef, "validate" > >) | null >; type ToolFactoryArgs = { signal: AbortSignal; transport: Transport; data: Data; }; type DynamicToolFactoryReturn any> = ReturnType< Extract>, null>, (...args: any) => any> >; type DynamicToolFactorySubagents any> = ToolSubagentNames< Exclude>, null> >; type DynamicToolFactoryExtra any> = ToolFactoryExtra< Exclude>, null> >; type RuntimeToolDefinition = Omit< ToolDef, "ArgumentsSchema" | "ParsedSchema" | "description" | "name" | "parse" | "validate" | "run" > & { run: ( args: ToolImplementationRunArgs, ) => Promise, string>>; } & Partial, "parse" | "validate">>; type AnyToolSchema = Schema; type AnyParsedArguments = any; type AnySubagentNames = any; type AnyCustomIR = any; type AnyToolData = any; /* * Tool extension IRs are, by definition, tool-output-shaped IRs: they exist only as the result * of running a tool call, so they must carry the tool call they answer. Constraining extension * IRs to this shape lets libocto treat every extension IR as a tool output without clients * re-deriving that decision. */ export type ToolExtensionIR = Role extends BuiltinIRRole ? never : { role: Role; toolCall: { toolCallId: string; }; }; // Public tool factories are the precise raw factory type plus the capability brand. Keeping the raw // factory in the intersection preserves literal tool names and parsed argument types. export type ToolFactory< Data, S extends AnyToolSchema, Parsed, SubagentNames extends string, Extra extends ToolExtensionIR, > = RawToolFactory & ToolFactoryRequirements; export class ToolBuilder { withData(): ToolBuilder { return new ToolBuilder(); } /* * Caller-facing tool declaration, explicit parse form. * * Use this overload when the LLM's raw arguments are not the shape the tool wants to run with. * define(...) must then supply parse({ original, ... }) to produce ParsedSchema. */ declare< Name extends string, Arguments, Parsed, const SubagentNames extends string = never, >(partial: { name: Name; description: string; ArgumentsSchema: t.Type; ParsedSchema: t.Type; subagents?: readonly SubagentNames[]; }): DeclaredTool; /* * Caller-facing tool declaration, auto-parse form. * * Use this for the common case where the LLM's raw arguments are already the shape the tool runs * with. The caller omits ParsedSchema and parse(...). The resulting tool behaves as if * ParsedSchema === ArgumentsSchema and parse(...) returned { original: x, parsed: x }. */ declare(partial: { name: Name; description: string; ArgumentsSchema: t.Type; ParsedSchema?: undefined; subagents?: readonly SubagentNames[]; }): DeclaredTool; declare(partial: { name: string; description: string; ArgumentsSchema: t.Type; ParsedSchema?: t.Type; subagents?: readonly string[]; }): any { return new DeclaredTool(partial); } /* * Caller-facing dynamic tool builder. * * Use this when the exact tool shape is not known until load time, for example when the schema * depends on data, transport capabilities, or discovered project state. The caller returns a * normal declare(...).define(...) result from the selector rather than repeating a second dynamic * API for name/schema/subagents/parse behavior. * * The selected tool carries the same type information as a static tool. This wrapper preserves * that information and forwards the runtime args into the selected factory. */ dynamicDefineTool< T extends ( args: ToolFactoryArgs, ) => Promise | null>, >( factory: T, ): ((args: ToolFactoryArgs) => DynamicToolFactoryReturn) & ToolFactoryRequirements, DynamicToolFactoryExtra> { const selectTool = factory as unknown as (args: ToolFactoryArgs) => ReturnType; const wrapped = async (args: ToolFactoryArgs) => { const selectedTool = await selectTool(args); const runSelectedTool = selectedTool as | null | ((args: ToolFactoryArgs) => DynamicToolFactoryReturn); // One-level flatMap: the selector returns a tool factory; dynamicDefineTool must itself // behave like a tool factory that returns the selected tool definition. return runSelectedTool?.(args) ?? null; }; // The brand has no runtime representation; it only makes defineAgent reject tools whose custom // IR output is not included in the target IR universe. return wrapped as unknown as ((args: ToolFactoryArgs) => DynamicToolFactoryReturn) & ToolFactoryRequirements, DynamicToolFactoryExtra>; } } export class DeclaredTool< Data, Name extends string, Arguments, Parsed, SubagentNames extends string, Extra extends ToolExtensionIR, RequiresParse extends boolean, CustomIR, > { constructor( private readonly partial: { name: Name; description: string; ArgumentsSchema: t.Type; ParsedSchema?: t.Type; subagents?: readonly SubagentNames[]; }, private readonly customIRBuilders: CustomIRBuilderMap | null = null, ) {} withCustomIR( builders: Builders, ): DeclaredTool< Data, Name, Arguments, Parsed, SubagentNames, CustomIRDataForTool, RequiresParse, BoundCustomIRForTool > { return new DeclaredTool(this.partial, builders); } define< T extends SimpleRawToolFactory< Data, Schema, Parsed, SubagentNames, Extra, CustomIR >, >( this: DeclaredTool, factory: (args: Parameters[0]) => Promise>>, ): ToolFactory, Parsed, SubagentNames, Extra>; define< T extends SimpleAutoParsedRawToolFactory< Data, Schema, SubagentNames, Extra, CustomIR >, >( this: DeclaredTool, factory: (args: Parameters[0]) => Promise>>, ): ToolFactory, Arguments, SubagentNames, Extra>; define( factory: (args: ToolFactoryArgs) => Promise | null>, ): any { const partial = this.partial; const customIRBuilders = this.customIRBuilders; const wrapped = async (args: ToolFactoryArgs) => { const def = await factory(args); if (def === null) return null; return { ...def, name: partial.name, description: partial.description, ArgumentsSchema: partial.ArgumentsSchema, ParsedSchema: partial.ParsedSchema ?? partial.ArgumentsSchema, parse: def.parse ?? (async ({ original }) => ({ success: true as const, data: { original, parsed: original, }, })), validate: def.validate ?? (async () => ({ success: true, data: null })), run: async (runArgs: ToolRunArgs) => def.run({ ...runArgs, customIR: bindCustomIR(customIRBuilders, flattenToolCall(runArgs.toolCall)), }), }; }; // The brand has no runtime representation; it only makes defineAgent reject tools whose custom // IR output is not included in the target IR universe. return wrapped as unknown as ToolFactory< Data, Schema, Parsed, SubagentNames, Extra >; } } // A shared tool builder for callers that do not need a dedicated builder instance. export const TOOL_BUILDER = new ToolBuilder(); export type ToolMap> = { [key: string]: ToolFactory; }; type CustomIRBuilderMap = Record (args: any) => ToolExtensionIR>; type DeclaredToolMap< Data, Name extends string, Arguments, Parsed, SubagentNames extends string, Extra extends ToolExtensionIR, > = { [K in Name]: ToolFactory; }; type DeclaredToolCall< Data, Name extends string, Arguments, Parsed, SubagentNames extends string, > = ToolCall>; type CustomIRData = { [K in keyof Builders]: Builders[K] extends (toolCall: Call) => (args: any) => infer IR ? IR extends ToolExtensionIR ? IR : never : never; }[keyof Builders]; type CustomIRDataForTool< Data, Name extends string, Arguments, Parsed, SubagentNames extends string, Builders extends CustomIRBuilderMap, > = CustomIRData>; type BoundCustomIR = { [K in keyof Builders]: Builders[K] extends (toolCall: Call) => (args: infer Args) => infer IR ? (args: Args) => Result<{ type: "custom-ir"; data: IR }, string> : never; }; type BoundCustomIRForTool< Data, Name extends string, Arguments, Parsed, SubagentNames extends string, Builders extends CustomIRBuilderMap, > = BoundCustomIR>; export type LoadedTools> = { [K in keyof T]: Exclude>, null>; }; export type ToolCall> = { [K in keyof LoadedTools]: { type: "tool-call"; name: LoadedTools[K]["name"]; toolCallId: string; parsed: t.GetType[K]["ParsedSchema"]>; original: t.GetType[K]["ArgumentsSchema"]>; }; }[keyof LoadedTools]; function flattenToolCall(toolCall: { toolCallId: string; original: Schema; parsed: Schema; }) { return { type: "tool-call" as const, name: toolCall.parsed.name, toolCallId: toolCall.toolCallId, original: toolCall.original.arguments, parsed: toolCall.parsed.arguments, }; } function bindCustomIR(builders: CustomIRBuilderMap | null, toolCall: unknown) { const bound: Record< string, (args: unknown) => Result<{ type: "custom-ir"; data: unknown }, string> > = {}; for (const [name, builder] of Object.entries(builders ?? {})) { const build = builder(toolCall); bound[name] = args => ok({ type: "custom-ir", data: build(args) }); } return bound; } /* * ALERT ALERT * * Type system bullshit used for branding and validating IRs with respect to tools and subagent * dependencies. */ // Tool factories are structurally just functions, so TypeScript can otherwise forget which IR // extension set they were created for if the implementation does not currently return custom IR. // These unique-symbol fields are a compile-time-only brand attached by declare(...).define(...). declare const toolFactoryExtra: unique symbol; declare const toolFactorySubagents: unique symbol; // The brand should be covariant: a base tool that emits no custom IR (never) is valid in a richer // IR context, but a richer tool is not valid in a base IR context. type Covariant = () => T; // Required phantom fields keep Extra and SubagentNames visible when checking assignability to a // ToolMap. They are never read or written at runtime. export type ToolFactoryRequirements< SubagentNames extends string, Extra extends ToolExtensionIR, > = { readonly [toolFactoryExtra]: Covariant; readonly [toolFactorySubagents]: Covariant; }; type ToolFactoryExtra = T extends ToolFactoryRequirements ? Extra : never; export type ToolSubagentNames = T extends ToolFactoryRequirements ? SubagentNames : never;