From fbd8aecaa1c7a3337db562adb549d5aa35848d23 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 29 Oct 2026 23:13:02 -0500 Subject: [PATCH] fix(ai): keep tools with a throwing execute runnable (#60431) --- packages/ai/src/index.ts | 5 +++-- packages/ai/src/tool.ts | 22 +++++++++++++++------- packages/ai/test/loop-tools.types.ts | 8 ++++++++ 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index d1adf6f1f8c..abf396eb328 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -35,7 +35,8 @@ import { type TranscriptionRequest, type TranscriptionRequestInput, } from "./transcription.js" -import { ToolFailure, build, type ToolExecuteContext, type ToolOptions, type ToolSchema } from "./tool.js" +import { ToolFailure, build } from "./tool.js" +import type { NoExecute, ToolExecuteContext, ToolOptions, ToolSchema } from "./tool.js" import { fileMediaType } from "./utils/media-type.js" import { Video, type VideoModel, type VideoRequest, type VideoRequestInput } from "./video.js" @@ -282,7 +283,7 @@ const fileError = (message: string, cause: unknown) => export const tool = < Parameters extends ToolSchema = typeof Schema.Unknown, Success extends ToolSchema = typeof Schema.Unknown, - Result extends Schema.Schema.Type | PromiseLike> = never, + Result extends Schema.Schema.Type | PromiseLike> | NoExecute = NoExecute, >( config: ToolOptions< Parameters, diff --git a/packages/ai/src/tool.ts b/packages/ai/src/tool.ts index 11f8f3dafa4..22e71ac65b3 100644 --- a/packages/ai/src/tool.ts +++ b/packages/ai/src/tool.ts @@ -130,7 +130,7 @@ export type ToolOptions< export const tool = < Parameters extends ToolSchema = typeof Schema.Unknown, Success extends ToolSchema = typeof Schema.Unknown, - Result extends Effect.Effect, ToolFailure> = never, + Result extends Effect.Effect, ToolFailure> | NoExecute = NoExecute, >( config: ToolOptions< Parameters, @@ -139,14 +139,22 @@ export const tool = < >, ) => build(config, config.execute) -/** @internal Builds a tool for both clients from its options and an Effect `execute`; no `execute` infers `never`. */ -export function build, Success extends ToolSchema, Execute>( - config: ToolOptions, - execute: ToolExecute> | undefined, -): Execute extends (...args: never) => never ? Tool : ExecutableTool +declare const noExecute: unique symbol +/** @internal The inferred `execute` result when `execute` is omitted; no function can return it. */ +export type NoExecute = { readonly [noExecute]: never } + +/** @internal Builds a tool for both clients from its options and an Effect `execute`. */ +export function build, Success extends ToolSchema, Result>( + config: ToolOptions Result>, + execute: ((params: Schema.Schema.Type, context?: ToolExecuteContext) => unknown) | undefined, +): 0 extends 1 & Result // `any` + ? ExecutableTool + : [Result, NoExecute] extends [NoExecute, Result] + ? Tool + : ExecutableTool export function build( config: ToolOptions, ToolSchema, unknown>, - execute: ToolExecute, ToolSchema> | undefined, + execute: ((params: any, context?: ToolExecuteContext) => any) | undefined, ): Tool { const shared = { description: config.description, diff --git a/packages/ai/test/loop-tools.types.ts b/packages/ai/test/loop-tools.types.ts index 7c6b059e549..dce970821b6 100644 --- a/packages/ai/test/loop-tools.types.ts +++ b/packages/ai/test/loop-tools.types.ts @@ -47,3 +47,11 @@ LLM.loop({ model, prompt: "Use any tool.", tools: { add, echo, weather, shout } // @ts-expect-error The loop runs tools, so each one needs an execute handler. LLM.loop({ model, prompt: "Get weather.", tools: { schemaOnly } }) + +const unavailable = () => { + throw new Error("Unavailable.") +} +const fail = AI.tool({ description: "Fail.", parameters: Schema.String, execute: unavailable }) +const parse = AI.tool({ description: "Parse.", parameters: Schema.String, execute: (text) => JSON.parse(text) }) +const inspect = tool({ description: "Inspect.", parameters: Schema.String, execute: () => Effect.succeed(1) }) +ai.llm.loop({ model, prompt: "Fail.", tools: { fail, parse, inspect } })