Structured Outputs
Get a typed, schema-validated reply instead of free text. The JSON Schema is enforced natively by the provider — not by prompt begging.
#![allow(unused)] fn main() { use yoagent::{Agent, provider::ModelConfig}; #[derive(serde::Deserialize)] struct Invoice { vendor: String, total_cents: u64, line_items: Vec<String>, } let mut agent = Agent::from_config(ModelConfig::anthropic("claude-sonnet-5", "Sonnet 5")); let invoice: Invoice = agent .prompt_structured( "Extract the invoice from the attached text: ...", serde_json::json!({ "type": "object", "properties": { "vendor": {"type": "string"}, "total_cents": {"type": "integer"}, "line_items": {"type": "array", "items": {"type": "string"}} }, "required": ["vendor", "total_cents", "line_items"] }), ) .await?; }
Derive the schema however you like — by hand as above, or with the
schemars crate (convert with
serde_json::to_value(schemars::schema_for!(Invoice))). Mind the provider
dialects: OpenAI strict mode requires additionalProperties: false and every
property listed in required; Gemini rejects $defs/$ref. Schemas are
passed through as given.
How each provider enforces it
| Protocol | Mechanism |
|---|---|
| Anthropic | Forced tool call — a synthetic tool is built from your schema and tool_choice forces it; the loop unwraps the call back into text |
| OpenAI-compatible | response_format: {type: "json_schema", strict: true} |
| Google Gemini | generationConfig.responseSchema + JSON mime type (note: Gemini uses an OpenAPI-style schema dialect — your schema is passed through as given) |
| OpenAI Responses / Azure / Vertex / Bedrock | Not yet wired — a warning is logged and the model replies as free text, which still must parse into T |
Semantics & caveats
prompt_structuredruns the loop to completion internally and returns the parsedT— there is no event receiver for this call.- Three error shapes:
Provider { message }when the API call itself failed (auth, network, a schema-induced 400 — retrying the parse is pointless);Parse { source, raw }when the model's text didn't deserialize (the raw text is preserved so you can retry or salvage);NoOutputwhen the run produced no text. Only messages produced by this call are considered — stale output from earlier turns is never parsed. - On Anthropic the forced tool call preempts regular tools for that request, and disables extended thinking for that request (forced tool choice and thinking are mutually exclusive at the API level — a warning is logged). Treat structured prompts as extraction/finalization calls, not agentic tool-using turns.
- Markdown code fences around the JSON are stripped defensively before parsing.