Earlier in this series we got a typed object out of a single model call by passing a schema as the response format. That worked because it was one call in, one shaped object out.
An agent is a loop, and the loop breaks that trick. This post gets the shaped object back, using the tool machinery already built.
Why the single-call trick does not carry over
A single call has one output, so you constrain it with a schema and the reply comes back shaped.
An agent's answer arrives at the end of a loop that has been calling tools, and the final turn is just the model deciding it has enough and writing free text. No single response exists to constrain, and the answer you care about is whatever the model happened to type last.
For a conversation that is fine. For a pipeline that needs {"answer": ..., "confidence": ...} it is
not, because free text is exactly what the next stage cannot consume.
The insight: make the answer a tool
The agent already has one reliable way to produce structured data: a tool call. Tool calls arrive schema-shaped and validated, which is the whole reason we build on them.
So make the final answer a tool.
Create a tool whose input schema is the output type you want. When the agent is ready to answer, it does not write free text. It calls this tool, filling the schema with its answer, and the loop reads the structured arguments straight off the call.
class TicketVerdict(BaseModel):
answer: str
staff_hours_freed: float
pays_for_itself: bool
confidence: Literal["low", "medium", "high"]
You do not write a tool for that by hand. The agent builds one from the schema.
A single call constrains its one reply. An agent calls a final-answer tool whose schema is the output type.
What changes in the agent
The change is small, because it rides the tool machinery already in place. Three touch points.
Take an output type. The agent accepts an optional output_type. When it is set, the agent builds
a tool named final_answer whose parameters are that schema, and adds it to the tool list like any
other tool.
def _setup_tools(self):
tools = list(self.raw_tools)
if self.output_type:
tools.append(final_answer_tool(self.output_type)) # schema becomes a tool
return tools
Ask for it when it is time to answer. When the agent would otherwise give a final answer, it is
steered to call final_answer instead of writing prose. The output arrives as validated tool
arguments.
Read the answer off the call. The loop already knows a run ends when the model stops asking for
tools. With structured output, the run ends when the model calls final_answer, and the result is the
call's arguments, parsed into your model.
result = await agent.run(question)
verdict = result.output # a TicketVerdict, not a string
verdict.pays_for_itself # True
verdict.confidence # "high"
Why this beats parsing prose
You could let the agent write free text and parse a structure out of it afterwards. That is the fragile path, and it fails the same way text-parsing ReAct failed: prose varies, and your parser meets a phrasing it did not expect.
Routing the answer through a tool call reuses the one part of the model that is trained to produce structured data on demand. It is the same reason the whole series builds on tool calling rather than text parsing.
| Parse the prose | Answer via a tool | |
|---|---|---|
| How the answer arrives | Free text you post-process | Validated tool arguments |
| What guarantees the shape | Your parser, and hope | The schema on the tool |
| When it breaks | A phrasing you did not expect | Rarely |
| New machinery needed | A parser per output type | None; reuses tool calling |
Parsing prose is the fragile path. A final-answer tool reuses the machinery already trained for structure.
What to take from this
- The single-call schema trick does not survive a loop, because the answer arrives at the end of tool calls, not from one constrained response.
- Make the final answer a tool whose input schema is your output type. The agent fills it, and you read a validated object off the call.
- The change is small because it reuses tool calling, the one path already trained to produce structure on demand.
The agent is complete: it reasons, calls tools, loops, and can return a typed answer. The question left is the one the whole series has been building toward. Does handing it tools actually make it better?