fix(core): recover malformed tool input

This commit is contained in:
Dustin Deus 2026-07-18 19:13:52 +00:00
commit 7840a0d378
8 changed files with 109 additions and 16 deletions

View file

@ -153,7 +153,21 @@ export const wrappedSystemUpdate = Effect.fn("ProviderShared.wrappedSystemUpdate
* routes: `Invalid JSON input for <route> tool call <name>`.
*/
export const parseToolInput = (route: string, name: string, raw: string) =>
parseJson(route, raw || "{}", `Invalid JSON input for ${route} tool call ${name}`)
Effect.try({
try: () => decodeJson(raw || "{}"),
catch: () =>
new LLMError({
module: "ProviderShared",
method: "stream",
reason: new InvalidProviderOutputReason({
route,
message: `Invalid JSON input for ${route} tool call ${name}`,
raw,
source: "tool-input",
toolName: name,
}),
}),
})
export const IMAGE_MIMES = ["image/png", "image/jpeg", "image/gif", "image/webp"] as const
export const VIDEO_MIMES = ["video/mp4", "video/webm", "video/quicktime"] as const

View file

@ -106,6 +106,8 @@ export class InvalidProviderOutputReason extends Schema.Class<InvalidProviderOut
message: Schema.String,
route: Schema.optional(Schema.String),
raw: Schema.optional(Schema.String),
source: Schema.optional(Schema.Literal("tool-input")),
toolName: Schema.optional(Schema.String),
providerMetadata: Schema.optional(ProviderMetadata),
}) {}

View file

@ -64,6 +64,25 @@ describe("ToolStream", () => {
}),
)
it.effect("classifies malformed tool input with its raw arguments", () =>
Effect.gen(function* () {
const tools = ToolStream.start(ToolStream.empty<number>(), 0, {
id: "call_1",
name: "lookup",
input: '{"query":"partial',
})
const error = yield* ToolStream.finish(ADAPTER, tools, 0).pipe(Effect.flip)
expect(error).toBeInstanceOf(LLMError)
expect(error.reason).toMatchObject({
_tag: "InvalidProviderOutput",
source: "tool-input",
toolName: "lookup",
raw: '{"query":"partial',
})
}),
)
it.effect("preserves providerExecuted and clears all tools", () =>
Effect.gen(function* () {
const first: ToolStream.State<number> = ToolStream.start(ToolStream.empty<number>(), 0, {