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.
This commit is contained in:
can1357
2026-08-20 06:11:48 +02:00
parent d8cad583e5
commit baf894425c
2 changed files with 12 additions and 19 deletions
+3 -6
View File
@@ -1593,12 +1593,9 @@ function coerceArgsFromIssues(args: unknown, issues: FlatIssue[]): { value: unkn
// a type coercion actually needs to write into a leaf. // a type coercion actually needs to write into a leaf.
let owned = false; let owned = false;
let nextArgs: unknown = args; let nextArgs: unknown = args;
for (const issue of issues) { for (const issue of issues) {
// Issues surfaced from a failed union branch are guesses from that // Failed union branches still contribute schema-directed type repairs.
// branch's diagnosis, not authoritative: another variant may accept the // Container-to-string conversion remains enabled for string branches.
// value as-is. Lossy repairs (key deletion, container stringification,
// singleton wrapping) stay off for them; lossless repairs still apply.
if (issue.keyword === "unrecognized") { if (issue.keyword === "unrecognized") {
if (issue.unionBranch) continue; if (issue.unionBranch) continue;
const previous = nextArgs; const previous = nextArgs;
@@ -1610,7 +1607,7 @@ function coerceArgsFromIssues(args: unknown, issues: FlatIssue[]): { value: unkn
if (issue.expectedTypes.length === 0) continue; if (issue.expectedTypes.length === 0) continue;
const currentValue = getValueAtPointer(nextArgs, issue.instancePath); 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; let coercedValue = result.changed ? result.value : undefined;
if ( if (
coercedValue === undefined && coercedValue === undefined &&
@@ -56,11 +56,7 @@ describe("Tool argument coercion", () => {
expect(result.payload).toBe('{"a":1,"nested":["x"]}'); expect(result.payload).toBe('{"a":1,"nested":["x"]}');
}); });
it("does not stringify container values diagnosed inside a failed union branch", () => { it("stringifies container values when a string union branch matches", () => {
// 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.
const tool: Tool = { const tool: Tool = {
name: "union-string", name: "union-string",
description: "", description: "",
@@ -74,14 +70,14 @@ describe("Tool argument coercion", () => {
} as never, } as never,
}; };
expect(() => const result = validateToolArguments(tool, {
validateToolArguments(tool, { type: "toolCall",
type: "toolCall", id: "call-union-object",
id: "call-union-object", name: "union-string",
name: "union-string", arguments: { payload: { a: 1 } },
arguments: { payload: { a: 1 } }, }) as { payload: string };
}),
).toThrow(/payload/); expect(result.payload).toBe('{"a":1}');
}); });
it("does not delete unrecognized keys diagnosed inside a failed union branch", () => { it("does not delete unrecognized keys diagnosed inside a failed union branch", () => {