fix(ai): keep tools with a throwing execute runnable (#60431)

This commit is contained in:
Aiden Cline
2026-10-29 23:13:02 -05:00
committed by GitHub
Unverified
parent 57c7b70d2e
commit fbd8aecaa1
3 changed files with 26 additions and 9 deletions
+3 -2
View File
@@ -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<any> = typeof Schema.Unknown,
Success extends ToolSchema<any> = typeof Schema.Unknown,
Result extends Schema.Schema.Type<Success> | PromiseLike<Schema.Schema.Type<Success>> = never,
Result extends Schema.Schema.Type<Success> | PromiseLike<Schema.Schema.Type<Success>> | NoExecute = NoExecute,
>(
config: ToolOptions<
Parameters,
+15 -7
View File
@@ -130,7 +130,7 @@ export type ToolOptions<
export const tool = <
Parameters extends ToolSchema<any> = typeof Schema.Unknown,
Success extends ToolSchema<any> = typeof Schema.Unknown,
Result extends Effect.Effect<Schema.Schema.Type<Success>, ToolFailure> = never,
Result extends Effect.Effect<Schema.Schema.Type<Success>, 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<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>, Execute>(
config: ToolOptions<Parameters, Success, Execute>,
execute: ToolExecute<Parameters, ToolSchema<unknown>> | undefined,
): Execute extends (...args: never) => never ? Tool<Parameters, Success> : ExecutableTool<Parameters, Success>
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<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>, Result>(
config: ToolOptions<Parameters, Success, (...args: never) => Result>,
execute: ((params: Schema.Schema.Type<Parameters>, context?: ToolExecuteContext) => unknown) | undefined,
): 0 extends 1 & Result // `any`
? ExecutableTool<Parameters, Success>
: [Result, NoExecute] extends [NoExecute, Result]
? Tool<Parameters, Success>
: ExecutableTool<Parameters, Success>
export function build(
config: ToolOptions<ToolSchema<any>, ToolSchema<any>, unknown>,
execute: ToolExecute<ToolSchema<any>, ToolSchema<any>> | undefined,
execute: ((params: any, context?: ToolExecuteContext) => any) | undefined,
): Tool {
const shared = {
description: config.description,
+8
View File
@@ -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<unknown>(1) })
ai.llm.loop({ model, prompt: "Fail.", tools: { fail, parse, inspect } })