Skip to content

feat(claude): expose agent.output_format for structured output - #11

Merged
col merged 9 commits into
mainfrom
feat/claude-output-format
Aug 18, 2026
Merged

feat(claude): expose agent.output_format for structured output#11
col merged 9 commits into
mainfrom
feat/claude-output-format

Conversation

@col

@col col commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Adds an optional agent.output_format key to the POST /api/initialise manifest, letting a caller constrain a Claude turn's output to a JSON Schema. Maps onto claude.outputFormat, which a2a-wrapper#33 added and @col/a2a-claude@0.2.1-beta.6 ships.

Spec: docs/superpowers/specs/2026-08-18-claude-output-format-design.md
Plan: docs/superpowers/plans/2026-08-18-claude-output-format.md

What it looks like

"agent": {
  "platform": "claude",
  "output_format": {
    "type": "json_schema",
    "schema": { "type": "object", "properties": { "status": { "type": "string" } } }
  }
}

Omitting the key leaves the wrapper's freeform-text default in place, so no existing manifest changes behaviour.

Validation contract

Throng validates the wrapper shape only:

Input Result
not an object, or type !== "json_schema" 400 — agent.output_format
schema missing / not an object / an array 400 — agent.output_format.schema
absent accepted; outputFormat left unset

The schema body itself is not inspected. Throng doesn't own JSON Schema validity — an invalid schema surfaces from the SDK at turn time, not at initialise.

This duplicates a shape check a2a-claude also performs, deliberately. The wrapper validates inside initialize(), so a bad value there fails the boot after initialise has already returned 202. Validating here turns that into a field-level 400 at the API boundary, consistent with how permission_mode, model, effort and thinking are already handled. The accepted cost is that a future wrapper release adding a second output-format type would be blocked by our whitelist until updated here too.

Verbatim schema pass-through

Only the outer key is renamed (output_formatoutputFormat). Unlike thinking.budget_tokensbudgetTokens, nothing rewrites keys inside schema — they're JSON Schema's own vocabulary (additionalProperties, patternProperties, …) and renaming them would corrupt the schema. Tests at both the validation and mapping layers pin this with a nested schema that survives deep-equal.

Operational notes

  • The structured result is an additional application/json data part on the response artifact; the text part still publishes first, so text-only clients are unaffected. The data part is only appended when the result is a JSON object — an array or scalar top-level schema produces no data part.
  • An unsatisfiable schema fails the turn with "Structured output retries exhausted." rather than degrading to freeform output. That usually means a schema bug, though the SDK reports the same exhaustion when structured output is retracted by a model fallback.

One claim was deliberately not made: whether the text part carries the JSON payload instead of prose under a structured format. result and structured_output are independent fields on SDKResultSuccess and nothing in the SDK or wrapper documents one replacing the other, so the docs assert only what's established. Worth confirming with a live turn before anyone relies on the text part's contents.

Verification

npm run build, npm run typecheck, npm test -- --force all pass from the repo root — 7/7 turbo tasks, 364 tests (249 core, 87 claude, 22 codex, 6 agent).

Out of scope

No default schema — Throng ships none, the manifest producer decides. No changes to packages/core, throng-agent, or throng-agent-codex. No other unexposed ClaudeConfig field wired up.

🤖 Generated with Claude Code

https://claude.ai/code/session_01My4JH7AAyLhe9Hb5626Gdv

col and others added 9 commits August 18, 2026 18:13
Design for a new optional agent.output_format manifest key that maps onto
a2a-claude's claude.outputFormat (added in 0.2.1-beta.6), letting callers
constrain a Claude turn's output to a JSON Schema.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01My4JH7AAyLhe9Hb5626Gdv
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01My4JH7AAyLhe9Hb5626Gdv
Final review caught operator-facing claims that nothing in the SDK or
wrapper substantiates:

- Dropped the assertion that the text part carries the JSON payload under
  a structured format. `result` and `structured_output` are independent
  fields on SDKResultSuccess and nothing documents one replacing the
  other; the wrapper's own README says text-only clients are unaffected.
  Docs now state only what is established.
- Noted that the application/json data part is conditional: the wrapper
  appends it only for a non-null, non-array object, so an array or scalar
  top-level schema produces no data part.
- Softened the retry-exhaustion diagnosis — the same message is reported
  when structured output is retracted by a model fallback.
- initialise returns 202, not 200.

Also restores coverage for an omitted `schema` key, the likeliest real
manifest error, which an earlier review round removed on the incorrect
grounds that it was unreachable through JSON.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01My4JH7AAyLhe9Hb5626Gdv
@col
col merged commit ca02fed into main Aug 18, 2026
1 check passed
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