Summary
AiError.ToolParameterValidationError declares toolParams: Schema.Json and validates it at construction. Because Schema.Json rejects NaN, Infinity, and -Infinity, constructing the error throws whenever the offending params contain one of those values — so the diagnostic path for a bad tool call becomes a defect (Die) instead of the typed, retryable failure it is meant to be.
The value that triggers this is the very value the error exists to report, which makes the failure mode inverted: the worse the input, the less usable the error.
Line refs at main HEAD 4d89bb8ffb4cf567a1d11072246b6161ce638712:
packages/effect/src/unstable/ai/AiError.ts:1017 — toolParams: Schema.Json
packages/effect/src/unstable/ai/Toolkit.ts:302-304 — the only construction site, inside Toolkit.handle's Effect.mapError, passing the raw params straight through:
const decodedParams = yield* schemas.decodeParameters(params).pipe(
Effect.mapError((cause) =>
AiError.make({
module: "Toolkit",
method: `${name}.handle`,
reason: new AiError.ToolParameterValidationError({
toolName: name,
toolParams: params, // <- unvalidated, may contain NaN/Infinity
description: cause.message
})
})
)
)
Since the constructor throws synchronously inside mapError, the throw escapes as a defect rather than the typed error.
Scope / severity
This cannot arise from a model's JSON tool call — JSON cannot encode NaN or infinities. It arises from programmatic callers of toolkit.handle (tests, in-process tool invocation, non-JSON transports, provider adapters that hand through already-decoded JS values). We hit it in a test that used NaN as a deliberately-bad tool parameter and got an opaque Error: Schema validation failed defect instead of ToolParameterValidationError.
So: low frequency, but it converts a diagnostic path into a crash path, and the resulting defect carries none of the tool name or decode description that the typed error would have carried.
Repro
effect@4.0.0-rc.109; 4.0.0-rc.111 and main are byte-identical in AiError.ts for this declaration.
import * as Effect from "effect/Effect"
import * as Exit from "effect/Exit"
import * as Schema from "effect/Schema"
import * as Stream from "effect/Stream"
import * as AiError from "effect/unstable/ai/AiError"
import * as Tool from "effect/unstable/ai/Tool"
import * as Toolkit from "effect/unstable/ai/Toolkit"
// 1. Direct construction of the diagnostic error throws on a non-JSON value.
try {
new AiError.ToolParameterValidationError({
toolName: "GetWeather",
toolParams: { city: NaN },
description: "Expected string, got number"
})
console.log("1. constructed OK")
} catch (e) {
console.log("1. construction THREW:", (e as Error).message)
}
// 2. End to end through Toolkit.handle.
const GetWeather = Tool.make("GetWeather", {
parameters: Schema.Struct({ city: Schema.String }),
success: Schema.String
})
const kit = Toolkit.make(GetWeather)
const layer = kit.toLayer({ GetWeather: () => Effect.succeed("sunny") })
const run = (params: unknown) =>
Effect.gen(function*() {
const built = yield* kit
const exit = yield* Effect.exit(
Effect.flatMap(built.handle("GetWeather", params as any), Stream.runCollect)
)
if (Exit.isSuccess(exit)) return "Success"
return (exit.cause as any).reasons
.map((r: any) => `${r._tag}(${r.error?.reason?._tag ?? String(r.defect).split("\n")[0]})`)
.join(", ")
}).pipe(Effect.provide(layer))
Effect.runPromise(
Effect.gen(function*() {
console.log("2. { city: 123 } ->", yield* run({ city: 123 }))
console.log("3. { city: NaN } ->", yield* run({ city: NaN }))
}) as Effect.Effect<void>
)
Output:
1. construction THREW: Schema validation failed
2. { city: 123 } -> Fail(ToolParameterValidationError)
3. { city: NaN } -> Die(Error: Schema validation failed)
Line 2 is the expected shape. Line 3 is the bug: the same class of input, one JSON-representable and one not, produces a typed retryable failure in one case and an untyped defect in the other. Infinity and -Infinity behave the same as NaN.
Expected
toolkit.handle should fail with AiError / ToolParameterValidationError for any parameter value that fails the tool's parameter schema, including values Schema.Json cannot represent.
No caller-side workaround
ToolParameterValidationError has no unsafe constructor: .make validates identically, there is no makeUnsafe, and a second { disableValidation: true } argument is not honoured. The only construction site is internal to Toolkit.handle, so a consumer cannot sanitize params between the decode failure and the error construction — only before calling handle, which defeats the purpose of reporting what was actually passed.
Suggested direction
Any one of:
- Type the field as
Schema.Unknown — it is diagnostic payload, not a wire contract.
- Keep
Schema.Json but sanitize at the construction site in Toolkit.ts (e.g. encode non-finite numbers to their string sentinels, or fall back to a string rendering of params).
- Validate the field lazily rather than at construction.
(1) seems closest to the field's purpose. Happy to open a PR for whichever direction you prefer.
Versions
- Observed on
effect@4.0.0-rc.109
- Verified unchanged on
effect@4.0.0-rc.111 (current rc dist-tag) — packages/effect/src/unstable/ai/AiError.ts is identical to rc.109
- Verified unchanged on
main @ 4d89bb8ffb4cf567a1d11072246b6161ce638712
- Bun 1.x, macOS
Searched open and closed issues for ToolParameterValidationError, toolParams, AiError NaN, Schema.Json NaN — the nearest matches are #6335 (parameter-decode failure bypasses failureMode: "return", same code path, different symptom) and #6350 (Schema.Number non-finite audit, opposite direction: schemas that are too permissive). Neither covers this.
Summary
AiError.ToolParameterValidationErrordeclarestoolParams: Schema.Jsonand validates it at construction. BecauseSchema.JsonrejectsNaN,Infinity, and-Infinity, constructing the error throws whenever the offending params contain one of those values — so the diagnostic path for a bad tool call becomes a defect (Die) instead of the typed, retryable failure it is meant to be.The value that triggers this is the very value the error exists to report, which makes the failure mode inverted: the worse the input, the less usable the error.
Line refs at
mainHEAD4d89bb8ffb4cf567a1d11072246b6161ce638712:packages/effect/src/unstable/ai/AiError.ts:1017—toolParams: Schema.Jsonpackages/effect/src/unstable/ai/Toolkit.ts:302-304— the only construction site, insideToolkit.handle'sEffect.mapError, passing the rawparamsstraight through:Since the constructor throws synchronously inside
mapError, the throw escapes as a defect rather than the typed error.Scope / severity
This cannot arise from a model's JSON tool call — JSON cannot encode
NaNor infinities. It arises from programmatic callers oftoolkit.handle(tests, in-process tool invocation, non-JSON transports, provider adapters that hand through already-decoded JS values). We hit it in a test that usedNaNas a deliberately-bad tool parameter and got an opaqueError: Schema validation faileddefect instead ofToolParameterValidationError.So: low frequency, but it converts a diagnostic path into a crash path, and the resulting defect carries none of the tool name or decode description that the typed error would have carried.
Repro
effect@4.0.0-rc.109;4.0.0-rc.111andmainare byte-identical inAiError.tsfor this declaration.Output:
Line 2 is the expected shape. Line 3 is the bug: the same class of input, one JSON-representable and one not, produces a typed retryable failure in one case and an untyped defect in the other.
Infinityand-Infinitybehave the same asNaN.Expected
toolkit.handleshould fail withAiError/ToolParameterValidationErrorfor any parameter value that fails the tool's parameter schema, including valuesSchema.Jsoncannot represent.No caller-side workaround
ToolParameterValidationErrorhas no unsafe constructor:.makevalidates identically, there is nomakeUnsafe, and a second{ disableValidation: true }argument is not honoured. The only construction site is internal toToolkit.handle, so a consumer cannot sanitizeparamsbetween the decode failure and the error construction — only before callinghandle, which defeats the purpose of reporting what was actually passed.Suggested direction
Any one of:
Schema.Unknown— it is diagnostic payload, not a wire contract.Schema.Jsonbut sanitize at the construction site inToolkit.ts(e.g. encode non-finite numbers to their string sentinels, or fall back to a string rendering ofparams).(1) seems closest to the field's purpose. Happy to open a PR for whichever direction you prefer.
Versions
effect@4.0.0-rc.109effect@4.0.0-rc.111(currentrcdist-tag) —packages/effect/src/unstable/ai/AiError.tsis identical to rc.109main@4d89bb8ffb4cf567a1d11072246b6161ce638712Searched open and closed issues for
ToolParameterValidationError,toolParams,AiError NaN,Schema.Json NaN— the nearest matches are #6335 (parameter-decode failure bypassesfailureMode: "return", same code path, different symptom) and #6350 (Schema.Numbernon-finite audit, opposite direction: schemas that are too permissive). Neither covers this.