Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@
"pages": [
"sdk/guides/hello-world",
"sdk/guides/custom-tools",
"sdk/guides/structured-output",
"sdk/guides/mcp",
"sdk/guides/skill",
"sdk/guides/plugins",
Expand Down
51 changes: 51 additions & 0 deletions sdk/guides/structured-output.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---

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 :-)

title: Structured Output
description: Attach a schema to any tool so the LLM returns typed, validated fields alongside the tool's own arguments.
---

import RunExampleCode from "/sdk/shared-snippets/how-to-run-example.mdx";

Pass a Pydantic model (or a JSON Schema dict) as a tool's `response_schema`. Its fields are merged into the schema the LLM sees, so the model must populate them when it calls that tool, and the reply is validated on receipt — no prompting for a format, no output parsing.

```python
class ProjectFacts(BaseModel):
description: str = Field(description="One-paragraph description of the project.")
facts: list[str] = Field(description="Three concise, distinct facts.")


agent = Agent(
llm=llm,
tools=[Tool(name="FinishTool", params={"response_schema": ProjectFacts})],
)
```

The tool keeps its own arguments — `FinishTool` still takes `message`, now alongside `description` and `facts`. This works on any tool, including [custom](/sdk/guides/custom-tools) and [MCP](/sdk/guides/mcp) tools.

## Reading results

Resolved tools live on `agent.tools_map`. Use `parse_last_response()` for the most recent call, or `parse_response(action)` for a specific one:

```python
finish_tool = agent.tools_map["finish"]
facts = cast(ProjectFacts | None, finish_tool.parse_last_response(conversation.state.events))
```

`parse_last_response()` returns `None` if the tool has not been called. With a JSON Schema dict instead of a model, both methods return a validated `dict`.

<Note>
`parse_last_response()` re-reads the tool call, so it works after a conversation is persisted and reloaded. `action.structured_output` is in-memory only — it is not serialized with the event and comes back `None` after a round-trip, so prefer the parse methods.
</Note>

## Constraints

- **Reserved names.** A schema may not declare `kind`, `security_risk`, `structured_output`, or `summary`, nor reuse one of the tool's own field names (e.g. `message` on `FinishTool`). Both raise a `ValueError` when the tool is resolved.
- **One tool per spec.** A spec that resolves to a tool set is rejected; attach the schema to the individual tool instead.
- **Scoped to its tool.** A model may try to send the schema fields when calling *other* tools; those calls are rejected as unexpected arguments and the agent retries.

## Ready-to-run Example

```python icon="python" expandable examples/01_standalone_sdk/56_structured_output.py
# content is auto-synced
```

<RunExampleCode path_to_script="examples/01_standalone_sdk/56_structured_output.py"/>
Loading