From baf894425c0e7106026f9f9b089ed58b3da120cd Mon Sep 17 00:00:00 2001 From: can1357 Date: Thu, 20 Aug 2026 06:11:48 +0200 Subject: [PATCH] feat(ai): enabled container-to-string coercion for union branches - Enable stringification repairs for expected types in union branches. - Update test to expect successful string coercion for objects in string union branches. --- packages/ai/src/utils/validation.ts | 9 +++----- .../ai/test/tool-argument-coercion.test.ts | 22 ++++++++----------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/packages/ai/src/utils/validation.ts b/packages/ai/src/utils/validation.ts index 537892596..24eb85d2d 100644 --- a/packages/ai/src/utils/validation.ts +++ b/packages/ai/src/utils/validation.ts @@ -1593,12 +1593,9 @@ function coerceArgsFromIssues(args: unknown, issues: FlatIssue[]): { value: unkn // a type coercion actually needs to write into a leaf. let owned = false; let nextArgs: unknown = args; - for (const issue of issues) { - // Issues surfaced from a failed union branch are guesses from that - // branch's diagnosis, not authoritative: another variant may accept the - // value as-is. Lossy repairs (key deletion, container stringification, - // singleton wrapping) stay off for them; lossless repairs still apply. + // Failed union branches still contribute schema-directed type repairs. + // Container-to-string conversion remains enabled for string branches. if (issue.keyword === "unrecognized") { if (issue.unionBranch) continue; const previous = nextArgs; @@ -1610,7 +1607,7 @@ function coerceArgsFromIssues(args: unknown, issues: FlatIssue[]): { value: unkn if (issue.expectedTypes.length === 0) continue; const currentValue = getValueAtPointer(nextArgs, issue.instancePath); - const result = tryCoerceForExpectedTypes(currentValue, issue.expectedTypes, !issue.unionBranch); + const result = tryCoerceForExpectedTypes(currentValue, issue.expectedTypes, true); let coercedValue = result.changed ? result.value : undefined; if ( coercedValue === undefined && diff --git a/packages/ai/test/tool-argument-coercion.test.ts b/packages/ai/test/tool-argument-coercion.test.ts index 53d42447d..3de65a49a 100644 --- a/packages/ai/test/tool-argument-coercion.test.ts +++ b/packages/ai/test/tool-argument-coercion.test.ts @@ -56,11 +56,7 @@ describe("Tool argument coercion", () => { expect(result.payload).toBe('{"a":1,"nested":["x"]}'); }); - it("does not stringify container values diagnosed inside a failed union branch", () => { - // Regression: a subagent yield payload with an object in a string-typed - // schema field sat under the tool's `anyOf` wrapper; the stringify repair - // fired on that branch's guess, so validation "passed" and downstream - // consumers received encoded text instead of a retryable error. + it("stringifies container values when a string union branch matches", () => { const tool: Tool = { name: "union-string", description: "", @@ -74,14 +70,14 @@ describe("Tool argument coercion", () => { } as never, }; - expect(() => - validateToolArguments(tool, { - type: "toolCall", - id: "call-union-object", - name: "union-string", - arguments: { payload: { a: 1 } }, - }), - ).toThrow(/payload/); + const result = validateToolArguments(tool, { + type: "toolCall", + id: "call-union-object", + name: "union-string", + arguments: { payload: { a: 1 } }, + }) as { payload: string }; + + expect(result.payload).toBe('{"a":1}'); }); it("does not delete unrecognized keys diagnosed inside a failed union branch", () => {