yoagent 0.26.0

The agent loop for Rust: stream from 7 LLM protocols, run tools, loop until done. Native or wasm32 (Cloudflare Workers).
Documentation
# 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.

```rust
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::claude_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"],
            "additionalProperties": false
        }),
    )
    .await?;
```

Derive the schema however you like — by hand as above, or with the
[`schemars`](https://crates.io/crates/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`; Anthropic's native JSON outputs also require
`additionalProperties: false` on objects and reject numeric/length constraints
(`minimum`, `maxLength`, …); Gemini rejects `$defs`/`$ref`. Schemas are
passed through as given.

## How each provider enforces it

| Protocol | Mechanism |
|----------|-----------|
| Anthropic, `native_structured_output` on (every `ModelConfig::claude_*` preset, and `ModelConfig::anthropic("claude-…")` / `opencode_zen("claude-…")` ids from Claude 4.5 on) | `output_config.format: {type: "json_schema", schema}` — the reply's text block is constrained to the schema; no tool is forced, thinking stays on, and the key shares `output_config` with the thinking `effort` |
| Anthropic, flag off (Claude ids before 4.5, non-Claude ids behind a Messages-protocol gateway, `AnthropicCompat::default()`) | 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_structured` runs the loop to completion internally and returns the
  parsed `T` — there is no event receiver for this call.
- Three error shapes: `Provider { message }` when the API call itself failed
  (auth, network once retries are exhausted, a schema-induced 400 — retrying
  the parse is pointless), was cancelled (`"Cancelled"`), or ended as a
  refusal or content-filter stop (carrying its explanation);
  `Parse { source, raw }` when the model's text didn't deserialize (the raw
  text is preserved so you can retry or salvage); `NoOutput` when the run
  produced no text. Only messages produced by **this call** are considered —
  stale output from earlier turns is never parsed.
- On Anthropic's tool-forcing path 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 such structured prompts as
  **extraction/finalization calls**, not agentic tool-using turns. The native
  path has neither restriction: tool choice stays `auto`, so the model may
  call your tools first, and the last text block is parsed. The loop's
  unwrap of the synthetic call runs only on the tool-forcing path, so on the
  native path a tool of yours that happens to share the schema's name is
  executed normally.
- **Claude Fable 5.1 and Opus 5.5 reject forced `tool_choice`** (`any`/`tool`)
  with a 400. Their presets (`claude_fable_5_1()`, `claude_opus_5_5()`) set
  `AnthropicCompat::native_structured_output`, so `prompt_structured` works on
  them, and so does a bare `ModelConfig::anthropic("claude-opus-5-5", ..)`,
  which infers the flag from the id. If you set `config.anthropic` yourself,
  start from `AnthropicCompat::for_claude_id(id)` rather than
  `AnthropicCompat::default()`, or the flag is off again and the call gets
  the 400 as `Provider { .. }`.

  Anthropic lists native JSON outputs for Fable 5/5.1, Opus 4.5 through 5.5,
  Sonnet 4.5/4.6/5 and Haiku 4.5 (on Amazon Bedrock, fewer: see the
  Anthropic structured-outputs page). `AnthropicCompat::default()` leaves the
  flag off, and so does `ModelConfig::anthropic` for a non-Claude id, because
  gateways that speak the Messages protocol may not accept
  `output_config.format`.
- Markdown code fences around the JSON are stripped defensively before
  parsing.