Skip to content

Fix #282: lenient type coercion for workflow StructuredOutput#315

Open
ericleepi314 wants to merge 1 commit into
feature/issue-284-concurrent-sessionsfrom
fix/issue-282-structured-output-coercion
Open

Fix #282: lenient type coercion for workflow StructuredOutput#315
ericleepi314 wants to merge 1 commit into
feature/issue-284-concurrent-sessionsfrom
fix/issue-282-structured-output-coercion

Conversation

@ericleepi314

Copy link
Copy Markdown
Collaborator

Closes #282

Stacked on #314 (deep stack down to #304). Merge in order; GitHub retargets automatically.

Summary

Weak models (glm) return JSON types as strings — "42", "true", arrays as JSON-encoded strings — and the workflow engine's strict StructuredOutput validation burned all schema-repair retries on trivially coercible mismatches (PR #266's known follow-up).

coerce_to_schema(obj, schema) — a lenient, never-raising pre-pass run before strict validation in StructuredOutputCollector.offer:

  • Keyed strictly on what the schema expects (never guesses from the value): "42"42 for integer, "3.14"3.14 for number, "true"/"false"→bool for boolean, JSON-encoded strings→json.loads for array/object (type-checked, then recursively coerced), integral floats→int for integer.
  • Recursive through properties/items; type may be a string or list; anyOf/oneOf deliberately untouched (documented).
  • ajv coerceTypes parity (from critic review against the reference semantics): "3.0"3 for integer-only schemas; a string already satisfying a union with "string" is never rewritten; "NaN"/"Infinity" strings are rejected (parse as floats but aren't valid JSON numbers — the critic caught that they'd silently enter the output contract); bools never become ints.
  • Anything uncoercible passes through unchanged so strict validation reports the real error and the repair-retry loop still works.

The injected tool now records collector.value (the coerced shape) in the outbox and tool result, so every consumer sees the same schema-conformant object.

Test plan

  • 26 tests: per-rule units, an end-to-end glm-shaped fixture (every scalar stringly-typed, array JSON-encoded → accepted on attempt 1), NaN/Infinity rejection, "3.0"/"3.5" integer-only behavior, string-union no-rewrite, bool-never-int, uncoercible pass-through, genuinely-wrong-output still errors/exhausts
  • Full workflow suite: 172 passed
  • Full suite on the stack: 7865 passed, 0 failed, 5 skipped
  • Critic review loop: APPROVE after 1 revision round (NaN/Infinity acceptance + ajv-conformance gaps came out of it, verified by execution)

🤖 Generated with Claude Code

Weak models (glm) emit JSON types as strings ("42", "true", arrays as
JSON-encoded strings) and burned every schema-repair retry on trivially
coercible mismatches, making structured workflow stages effectively
Anthropic/OpenAI-only.

coerce_to_schema runs before strict validation in the collector: keyed
strictly on what the schema expects (never guessing from the value),
recursive through properties/items, never raises, and anything
uncoercible passes through so strict validation reports the real error.
ajv coerceTypes parity choices: "3.0" -> 3 for integer schemas; a
string satisfying a union with "string" is never rewritten; NaN /
Infinity strings are rejected (not valid JSON numbers); bools never
become ints. The tool records the coerced value everywhere so all
consumers see one schema-conformant shape.

Closes #282

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant