Skip to content

docs(sdk): add structured output guide - #695

Open
luciobaiocchi wants to merge 3 commits into
OpenHands:mainfrom
luciobaiocchi:docs/structured-output
Open

docs(sdk): add structured output guide#695
luciobaiocchi wants to merge 3 commits into
OpenHands:mainfrom
luciobaiocchi:docs/structured-output

Conversation

@luciobaiocchi

Copy link
Copy Markdown
Contributor

Why

Documents the response_schema structured-output mechanism landed in
OpenHands/software-agent-sdk#4207 (merged as da03ee40). The feature shipped
without a docs page — the example that would have carried one was deferred out
of that PR, so there is currently nothing in the guides describing it.

Summary

Adds sdk/guides/structured-output.mdx and registers it in docs.json next to
Custom Tools (both are tool-system guides).

The guide covers:

  • attaching a Pydantic model via Tool(name=..., params={"response_schema": ...})
  • reading typed results with parse_last_response() / parse_response() off
    agent.tools_map, plus action.structured_output
  • annotating any tool (not just FinishTool) — e.g. forcing a rationale on
    every terminal command
  • the raw JSON Schema form, which returns a validated dict instead of a model
    instance
  • constraints: reserved field names (kind, security_risk,
    structured_output, summary), one tool per spec (tool sets are rejected),
    strict validation
  • a <Note> on the persistence/remote round-trip, where a Pydantic schema is
    serialized to JSON Schema and parse_response() then returns a dict

Notes

Every code claim in the guide was executed against the merged SDK before
writing, rather than inferred from the diff: schema fields merge while the tool
keeps its own arguments, action.kind stays FinishAction (no dynamic subclass
leaks into the event log), structured_output carries the values,
parse_response() returns the typed model for a Pydantic schema and a validated
dict for a JSON Schema, and all four reserved field names are rejected at
resolve time.

No expandable examples/... code block is used, since there is no
structured-output example file in the SDK for sync_code_blocks.py to sync
from. If an example lands later, the blocks can be converted to the synced form.

Branched off current main (9a171cf), so the diff is just the new guide plus
the one navigation line.

Documents the response_schema mechanism landed in
OpenHands/software-agent-sdk#4207: attaching a Pydantic model or JSON
Schema to any tool spec, reading typed results via parse_response /
parse_last_response, the raw JSON Schema form, and the constraints
(reserved field names, one tool per spec, round-trip to dict).
@luciobaiocchi
luciobaiocchi requested a review from enyst as a code owner August 6, 2026 16:57
Comment thread sdk/guides/structured-output.mdx Outdated
result = cast(ProjectFacts, finish_tool.parse_response(event.action))
```

The values also live on the action itself as `action.structured_output` (a plain dict), which is what gets persisted with the event.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The claim that action.structured_output "is what gets persisted with the event" is incorrect. It's a PrivateAttr/property excluded from event serialization, so it comes back None after any persist/reload round-trip

Worth correcting so readers don't rely on this field surviving persistence.

Comment thread sdk/guides/structured-output.mdx Outdated
## Constraints

<Warning>
**Reserved field names.** A response schema may not declare `kind`, `security_risk`, `structured_output`, or `summary`. The SDK injects those onto every action, so a schema using them is rejected with a `ValueError` when the tool is resolved — at configuration time, not mid-run.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a response_schema field that shares a name with the underlying tool's own action field also raises a ValueError at resolution time.

@@ -0,0 +1,124 @@
---

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look in the other examples. You can add a runnable example (and you had one n your PR). Follow what the other pages did :-)

- fix the persistence claim: structured_output is a PrivateAttr excluded
  from event serialization, so it is None after a reload; parse_last_response
  re-reads the tool call and does survive
- note that a schema field clashing with the tool's own field also raises
  at resolution time, not just the reserved meta names
- follow the other guides: add a Ready-to-run Example block backed by
  examples/01_standalone_sdk/56_structured_output.py
- trim the prose throughout (124 -> 50 lines)
Observed while running the example: the model may attempt to send the
schema fields when calling other tools, which are rejected as unexpected
arguments before the agent retries.
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.

2 participants