# Technical Documentation: 0xPlaygrounds/rig

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/0xPlaygrounds/rig)

### Overview (`README.md`)

<p align="center">
<picture>
    <source media="(prefers-color-scheme: dark)" srcset="img/rig-rebranded-logo-white.svg">
    <source media="(prefers-color-scheme: light)" srcset="img/rig-rebranded-logo-black.svg">
    <img src="img/rig-rebranded-logo-white.svg" style="width: 40%; height: 40%;" alt="Rig logo">
</picture>
<br>
<br>
<a href="https://rig.rs/docs"><img src="https://img.shields.io/badge/📖 docs-rig.rs-dca282.svg" /></a> &nbsp;
<a href="https://docs.rs/rig/latest/rig/"><img src="https://img.shields.io/badge/docs-API Reference-dca282.svg" /></a> &nbsp;
<a href="https://crates.io/crates/rig"><img src="https://img.shields.io/crates/v/rig.svg?color=dca282" /></a>
&nbsp;
<a href="https://crates.io/crates/rig"><img src="https://img.shields.io/crates/d/rig-core.svg?color=dca282" /></a>
&nbsp;
<a href="LICENSE"><img src="https://img.shields.io/crates/l/rig.svg?color=dca282" /></a>
</br>
<a href="https://discord.gg/playgrounds"><img src="https://img.shields.io/discord/511303648119226382?color=%236d82cc&label=Discord&logo=discord&logoColor=white" /></a>
&nbsp;
<a href=""><img src="https://img.shields.io/badge/built_with-Rust-dca282.svg?logo=rust" /></a>
&nbsp;
<a href="https://github.com/0xPlaygrounds/rig"><img src="https://img.shields.io/github/stars/0xPlaygrounds/rig?style=social" alt="stars - rig" /></a>
<br>

<br>
</p>
&nbsp;


<div align="center">

[📑 Docs](https://rig.rs/docs)
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
[🌐 Website](https://rig.rs)
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
[🤝 Contribute](https://github.com/0xPlaygrounds/rig/issues/new)
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
[✍🏽 Blogs](https://rig.rs/docs/guides)
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://ryzome.ai"><img src="img/ryzome-bg.png" height="32" align="absmiddle" alt="Ryzome" /></a>

</div>

Rig is a Rust library for building LLM applications, and `rig`, a terminal
coding agent built on it that you extend with Bevy plugins.

## The coding agent

```bash
cargo install rig
rig
```

In the agent, `/login` signs in with a ChatGPT plan or saves a provider's API
key, or export the key (such as `OPENAI_API_KEY`) before starting. `/model` picks
the model and `/help` lists the commands. `rig -p "fix the failing test"`
answers one prompt without the terminal view.

- **Everything is a plugin.** The agent is a [Bevy](https://bevy.org) app; its
  tools, commands, sessions, sign-in and terminal view are each a plugin listed
  in `plugins.toml`, so any of them can be removed, replaced or added to.
- **It rebuilds itself.** `/reload` rebuilds the agent with your plugin
  changes and restarts it in the same session, and keeps the old build running
  if the new one does not compile.
- **Scriptable.** `rig -p --json` and `rig --json` drive it over pipes;
  `rig help-agents` is the protocol.
- **More when you want it.** Subagents and checkpoints of what `shell`
  changed are optional plugins, and the tools work on remote hosts over ssh
  or any command.

The [agent guide](docs/agent.md) covers the keys, sessions, compaction,
plugins and files, and the [plugin guide](crates/rig-harness/PLUGINS.md) shows
how to write a tool, command or panel. The agent needs Rust 1.97.1 or newer
and runs on Linux and macOS.

## The library

```bash
cargo add rig
```

```rust
use rig::prelude::*;
use rig::providers::openai::{self, OpenAI};

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
    // The client reads `OPENAI_API_KEY` and builds the models it serves.
    let model = OpenAI::from_env()?.completion(openai::GPT_6_1_SOL);
    let comedian = AgentBuilder::new(model)
        .preamble("You are a comedian here to entertain the user using humour and jokes.")
        .build();

    let response = comedian.prompt("Entertain me!").await?;
    println!("{}", response.output());
    Ok(())
}
```

`#[tokio::main]` needs tokio's `macros` and `rt-multi-thread` features
(`cargo add tokio --features macros,rt-multi-thread`).

- 20+ model providers and 10+ vector stores, each under one interface
- Agents with tools, multi-turn streaming and typed hooks
- Completion, embedding, transcription, audio and image generation
- [GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) for OpenTelemetry
- Recording and replay of an agent's effects ([`rig-cassette`](crates/rig-cassette/README.md))
- Browser WASM (`wasm32-unknown-unknown`) for the core and the agent runtime ([target support](crates/rig-agent/README.md#target-support))

More in [`examples/`](examples), the [docs](https://rig.rs/docs) and the
[API reference](https://docs.rs/rig/latest/rig/).

## Crates

The `rig` crate re-exports the core and the agent runtime, and each companion
crate behind a feature.

| Crate | What it is |
| --- | --- |
| [`rig-core`](crates/rig-core) | Provider-neutral messages, completion and embedding models, tool contracts, the conversation type, vector-store contracts, and the providers |
| [`rig-agent`](crates/rig-agent) | The agent builder, prompting and streaming, hooks, the tool registry and the serializable `AgentRun` |
| [`rig-memory`](crates/rig-memory) | Windows, compaction, and saving, resuming and forking conversations |
| [`rig-cassette`](crates/rig-cassette) | Effect logs: recording, replay and checkpoints |
| [`rig-tools`](crates/rig-tools) | Coding tools any rig agent can use: read, write, edit, search, shell |
| [`rig-ecs`](crates/rig-ecs), [`rig-harness`](crates/rig-harness) | The agent kernel on Bevy's ECS, and the harness the coding agent's plugins build on |
| [`rig-tui`](plugins/rig-tui), [`rig-basics`](plugins/rig-basics) | The terminal view, and the coding agent's default plugins |

### Integrations

```toml
rig = { version = "0.44.0", features = ["lancedb", "fastembed"] }
```

| Integration | Crate | Feature | Module path |
| --- | --- | --- | --- |
| AWS Bedrock | [`rig-bedrock`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-bedrock) | `bedrock` | `rig::bedrock` |
| AWS S3Vectors | [`rig-s3vectors`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-s3vectors) | `s3vectors` | `rig::s3vectors` |
| Candle (local Llama/SmolLM2/Qwen3 tools, YOLOv8 pose) | [`rig-candle`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-candle) | `candle` | `rig::candle` |
| Cloudflare Vectorize | [`rig-vectorize`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-vectorize) | `vectorize` | `rig::vectorize` |
| FastEmbed | [`rig-fastembed`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-fastembed) | `fastembed` | `rig::fastembed` |
| Google Gemini gRPC | [`rig-gemini-grpc`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-gemini-grpc) | `gemini-grpc` | `rig::gemini_grpc` |
| Google Vertex AI | [`rig-vertexai`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-vertexai) | `vertexai` | `rig::vertexai` |
| HelixDB | [`rig-helixdb`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-helixdb) | `helixdb` | `rig::helixdb` |
| LanceDB | [`rig-lancedb`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-lancedb) | `lancedb` | `rig::lancedb` |
| Conversations over time | [`rig-memory`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-memory) | `memory` | `rig::memory` |
| Milvus | [`rig-milvus`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-milvus) | `milvus` | `rig::milvus` |
| MongoDB | [`rig-mongodb`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-mongodb) | `mongodb` | `rig::mongodb` |
| Neo4j | [`rig-neo4j`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-neo4j) | `neo4j` | `rig::neo4j` |
| PostgreSQL | [`rig-postgres`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-postgres) | `postgres` | `rig::postgres` |
| Qdrant | [`rig-qdrant`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-qdrant) | `qdrant` | `rig::qdrant` |
| ScyllaDB | [`rig-scylladb`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-scylladb) | `scylladb` | `rig::scylladb` |
| SQLite | [`rig-sqlite`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-sqlite) | `sqlite` | `rig::sqlite` |
| SurrealDB | [`rig-surrealdb`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-surrealdb) | `surrealdb` | `rig::surrealdb` |
| TypeSafe Jev (experimental judgments) | [`rig-typesafeai`](crates/rig-typesafeai) | `typesafeai` | `rig::typesafeai` |

## Community

Projects built on Rig are listed in
[awesome-rig](https://github.com/0xPlaygrounds/awesome-rig); open a pull
request there to add yours. Questions and ideas are welcome on
[Discord](https://discord.gg/playgrounds), and [CONTRIBUTING.md](CONTRIBUTING.md)
says how to contribute. Rig is MIT licensed.

> [!WARNING]
> Rig is pre-1.0. Releases contain breaking changes; each release's
> [changelog](CHANGELOG.md) and [migration notes](MIGRATING.md) say what
> changed and how to move.

<p align="center">
<br>
<br>
<img src="img/built-by-playgrounds.svg" alt="Build by Playgrounds" width="30%">
</p>

---

### Overview (`examples/README.md`)

# Rig examples

Each example is its own package. Run one with:

```sh
cargo run -p <example-name>
```

Most examples expect provider API keys in the environment (e.g. `OPENAI_API_KEY`,
`ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, `COHERE_API_KEY`). See each example's source for specifics.

| Example | Description |
| --- | --- |
| `agent_autonomous` | Demonstrates an autonomous extractor loop that keeps feeding its own output back in. |
| `agent_evaluator_optimizer` | See source. |
| `agent_orchestrator` | See source. |
| `agent_parallelization` | See source. |
| `agent_prompt_chaining` | Demonstrates prompt chaining with two agents in sequence. |
| `agent_routing` | Demonstrates routing one prompt into different follow-up prompts. |
| `agent_run_stepping` | Drives the agent loop by hand with the sans-IO [`AgentRun`] state machine. |
| `agent_stream_chat` | Demonstrates a streamed run over prior conversation history (`prompt(..).history(..).stream()`). |
| `agent_tool_call_streaming` | Streams tool-call arguments as they arrive on a raw completion stream and an agent run: each call's start names its tool, fragments render through `parse_partial_arguments`, a hook can stop mid-call, and a summary checks the joined fragments against each call's end. Takes a provider argument. |
| `agent_with_agent_tool` | See source. |
| `agent_with_approval_policy` | Demonstrates a non-interactive, policy-based HITL gate: an `AgentHook` auto-approves an allow-list, denies the rest (fail-closed), and applies an arg-based rule (mirrors `needs_approval`/`interrupt_on` predicates). |
| `agent_with_context` | Demonstrates adding small context documents directly to an agent. |
| `agent_with_default_max_turns` | Demonstrates extending the default agent loop budget for tool-heavy prompts. |
| `agent_with_durable_approval` | Demonstrates **durable** HITL: the hand-driven `AgentRun` is serialized while tool calls are pending and resumed from JSON (as another process would), so approval can happen out-of-process / later. |
| `agent_with_echochambers` | See source. |
| `agent_with_human_in_the_loop` | Demonstrates human-in-the-loop tool-call approval: an `AgentHook` pauses on each tool call so a human can approve/deny/edit/abort, mapped onto the dispatch-boundary `DispatchAction` (`Proceed`/`skip`/`rewrite_tool_args`/`stop`). |
| `agent_with_loaders` | Demonstrates loading real example files into agent context. |
| `agent_with_memory` | Demonstrates Rig-managed conversation memory with an in-memory backend; `-- --stream` streams each prompt. |
| `agent_with_tools_otel` | Agent multi-turn with tools, but with a tracing subscriber that sends all logs/traces to an OTel collector; `-- --stream` streams the answer. |
| `agent_with_tools` | Demonstrates registering runtime-defined tools on an agent. |
| `agent` | Demonstrates the smallest useful agent setup with OpenAI. |
| `calculator_chatbot` | See source. |
| `chain` | Demonstrates a retrieval-augmented pipeline with `parallel!` and `lookup`. |
| `complex_agentic_loop_claude` | See source. |
| `custom_vector_store` | Example: Implementing a custom vector store backend |
| `debate` | See source. |
| `enum_dispatch` | See source. |
| `extractor` | Demonstrates typed extraction and extraction with usage metadata. |
| `force_tool_first_turn` | Demonstrates a per-turn `RequestPatch` footgun and its fix: forcing `tool_choice = Required` on *every* turn loops until `max_turns`, so an `AgentHook` gates the patch on `ctx.turn() == 1` to force the tool only up front. |
| `gemini_deep_research` | Runs Deep Research or Deep Research Max in the background, then streams it with resume or polls it, and prints the report's sources. |
| `gemini_default_api_recovery` | Demonstrates recovering from Gemini emitting a legacy `default_api` tool name. |
| `gemini_extractor_with_rag` | See source. |
| `gemini_nanobanana_image_generation` | Generates a sized image with Nano Banana 2, optionally from a reference image, and writes every image and text part. |
| `gemini_stream_kill_token_count` | Live Gemini example: obtaining a token-count estimate when a streaming |
| `gemini_text_to_speech` | Speaks a line in one voice, then a two-host script as PCM joined into one WAV file. |
| `gemini_video_understanding` | Demonstrates Gemini video understanding with provider-specific request parameters. |
| [`litellm`](litellm/README.md) | Runs an agent through a LiteLLM proxy using its OpenAI-compatible Chat Completions API. Includes local proxy setup. |
| `manual_tool_calls` | Demonstrates manual tool-call handling with an explicit raw `Model` request. |
| `multi_agent` | See source. |
| `multi_extract` | Demonstrates fan-out structured extraction with `try_parallel!`. |
| `multi_turn_agent` | See source. |
| `openai_agent_completions_api_otel` | This example shows how you can use OpenAI's Completions API. |
| `openai_streaming_per_call_usage` | Shows how to inspect per-completion-call usage in an agent stream. |
| `pdf_agent` | See source. |
| `rag_dynamic_tools` | Retrieves the tools each prompt needs from a vector index, then answers a one-tool and a multi-turn prompt. |
| `rag` | Answers from documents found by vector search; `-- ollama` uses a local Ollama server. |
| `reasoning_loop` | See source. |
| `request_hook` | Demonstrates observing prompt/response/tool lifecycle events by stacking two `AgentHook`s with `add_hook`. |
| `reqwest_middleware` | Demonstrates supplying a custom reqwest client with retry middleware. |
| `rmcp_example` | Connects agents to MCP servers: a local server's tools and resources, and with `--bin remote -- <url>` any hosted streamable HTTP server. |
| `sentiment_classifier` | Demonstrates the smallest typed extractor for classification. |
| `transcription` | See source. |
| `tool_result_outcomes` | Demonstrates structured disk (`Other`/`EIO`) and network (`Network`/`ENETUNREACH`) tool failures, a run-scoped scratchpad ledger, and ordered recorder/policy hooks that terminate fatal failures while returning recoverable feedback to the model. Run `cargo run -p tool_result_outcomes -- --help` for credential-free usage. |
| `vector_search_cohere` | Demonstrates vector search with separate Cohere document and query embeddings. |
| `vector_search` | Demonstrates embedding documents and querying an in-memory vector index with OpenAI, or with `-- ollama` a local Ollama server. |

---

### Overview (`scripts/README.md`)

# scripts

- `check-dependency-floors.py` — downgrades every direct dependency to its declared floor and checks the workspace still builds.
- `release-notes.sh [PREVIOUS_TAG] [OUT_DIR]` — collects the commit log, merged PR notes and public API diffs since the previous tag into one directory for the release editorial pass.
- `release-notes-prompt.md` — the editorial pass that writes `CHANGELOG.md` and `MIGRATING.md` on the release PR, from the output of `release-notes.sh`.

---

### Overview (`tests/README.md`)

# Test Suites

Run the smallest useful local check for changed behavior and leave comprehensive
execution to CI; see [DEVELOPING.md](../DEVELOPING.md) for check selection,
`cargo xtask verify` modes, review, and publication.

Provider test targets have two owners:

- Cassette-backed suites are targets of `rig-cassette`, beside the corpus they
  replay: `crates/rig-cassette/tests/<provider>.rs` with its modules under
  `crates/rig-cassette/tests/providers/<provider>/cassette/` (and `/live/`
  for the ignored live cells those suites still carry). The shared drivers they
  include live in `crates/rig-cassette/tests/common/`.
- Providers with no recorded corpus keep their live-only suites in the facade:
  `tests/<provider>.rs` with `tests/providers/<provider>/`. These are ignored
  tests that require a real service.
- `tests/core.rs` contains provider-agnostic core behavior tests and the guards
  that scan the source tree, which need the repository root.
- `test-support/service-tests/integrations.rs` runs the vector-store suites from `tests/integrations/` as the unpublished `rig-service-tests` package. `tests/integrations.rs` retains the root Bedrock integrations.

Cassette suites require a checkout of this repository: `rig-cassette` excludes
its fixtures and integration tests from the published package. The unpublished
`rig-cassette-minimal` runner at
`crates/rig-cassette/tests/minimal/Cargo.toml` executes the same `verify`
source, plus the shared effect-log/classic-replay regressions. It selects only
cassette's `agent` feature, without the native HTTP engine.

```sh
RIG_PROVIDER_TEST_MODE=replay cargo nextest run --locked -p rig-cassette-minimal --all-features --retries 0
```

Keep `RIG_REGENERATE_GOLDEN` unset. This execution complements, rather than
replaces, the unified verification runs through `rig-cassette`.
The default/all-features cassette library still owns the unified library
regressions; `core-all` also retains the migrated classic replay cases.
Minimal verification runs with zero retries; unified verification and both
standalone/default-member effect-corpus configurations retain their existing
two. The standalone effect-corpus lane explicitly includes the
`rig-test-support` corpus regressions. Source ownership includes the library
tests shared by the minimal runner, not only its verification entrypoint.

## Testing Doctrine

**Recorded provider traffic is the default evidence; provider APIs are the
ultimate judge of whether the code is correct.** Every genuine pre-existing
streaming bug found during the #2258 refactor was found by live recording,
not by the synthetic corpus — a corpus written alongside an abstraction
encodes the team's model of the wire and structurally cannot falsify it.

- **Cassette-first.** New provider behavior gets a cassette-backed test.
  A unit test still earns its place — for internal behavior that is
  definitory rather than observed — but each unit test of provider-facing
  behavior should say (in its doc comment) why it isn't, or can't be, a
  cassette test. Recording needs API keys and isn't trivial; writing the
  cassette test a contributor couldn't is core maintainer work.
- **Record first, derive the assertion, review the derivation.** Prefer
  assertions generated from a replay of real traffic and then reviewed over
  assertions hand-authored from documentation — hand-authored expectations
  encode the same model of the wire the code under test does.
- **Assert on the request boundary too.** A frozen cassette replays the
  provider's *responses*; it cannot by itself catch outbound drift in the
  requests the live code builds. The cassette harness matches each request
  body against the recorded one, so a request-shape regression fails as a
  404 mock miss — treat that as a first-class assertion, and when a change
  intentionally alters a request, update the recorded body deliberately and
  say so. (This is exactly what caught fabricated ids reaching request
  serializers in #2258.) Body matching compares key-sorted canonical JSON,
  so map reordering is invisible to it (Layer 0's determinism check below
  exists for that), and a cassette cannot notice the provider changing its
  behavior after record time (Layer 3 exists for that).
- **Never weaken a cassette to make it pass.** Update assertions and
  recordings to the new intended behavior, or re-record; a scrubbed value
  must redact real data, never invent data that was not recorded.

## Core Tests

Start with the owning package and a test-name filter. Confirm that the filter
actually ran the intended tests; a successful command running zero tests is not
verification.

```bash
cargo nextest run --locked --profile local -p rig-core --lib <test-name-filter>
cargo test --locked -p rig-core --lib <test-name-filter>
cargo test --locked -p rig --test <provider> <test-name-filter>
cargo clippy --locked -p rig-core --all-features --tests -- -D warnings
```

The Clippy command is useful for core test changes involving lint-sensitive
patterns, not a mandatory check for every edit. Use the owning package and
relevant features for other changes. Broader facade test commands:

```bash
cargo test -p rig --test core          # provider-agnostic core tests
cargo test -p rig                      # all default non-ignored root-crate tests
cargo test -p rig --all-features       # same, with all root crate features
```

### Fallible test assertions

Workspace Clippy denies `panic_in_result_fn`: `assert!` and `assert_eq!` inside
a test returning `Result` fail that check even when the test itself passes.
Use `anyhow::ensure!(actual == expected, "value changed")` in such tests,
alongside `?` for fallible setup. Preserve each assertion's condition; do not
add lint suppressions or weaken the test. Unit-returning tests can keep their
existing assertion style.

### Core feature isolation

The core crate's dev-dependency on itself enables `test-utils`, `websocket`,
and its default features. Consequently, a core unit-test run with
`--no-default-features` does not prove those features are absent. For an actual
feature-disabled library check, select core alone without test targets:

```bash
cargo check --locked -p rig-core --no-default-features --lib
```

When the change needs WASM coverage and the target is installed, add
`--target wasm32-unknown-unknown`. Keep runtime test coverage and compilation
coverage distinct in the report.

## Cassette Provider Tests

Cassette tests replay committed HTTP interactions by default and do not require
provider API keys. Fixtures live under `crates/rig-cassette/fixtures/cassettes/<provider>/...`.

Replay one migrated provider suite (`anthropic`, `bedrock`, `chatgpt`, `cohere`,
`copilot`, `deepseek`, `doubleword`, `gemini`, `groq`, `llamacpp`, `mistral`,
`mistralrs`, `ollama`, `openai`, `openrouter`, `perplexity`, `venice`, `xai`)
with:

```bash
cargo test -p rig-cassette --all-features --test <provider> <provider>::cassette -- --nocapture --test-threads=1
```

Record mode requires the relevant provider credentials in the environment and
overwrites existing cassette files:

```bash
RIG_PROVIDER_TEST_MODE=record \
cargo test -p rig-cassette --all-features --test <provider> <provider>::cassette -- --nocapture --test-threads=1
```

Prefer re-recording by fixture, from the repository root:

```bash
cargo xtask cassette record [--cap N] [--pause SECS] [--dry-run] [--no-cleanup] <provider/scenario.yaml>...
```

It runs each fixture's owning test once in record mode and logs every run in
an attempt ledger, before it starts and when it ends. A test stops at six
attempts, or at `--cap` when that is lower. After a failed run it copies every
fixture change aside and restores the provider's fixtures as they were just
before the run. It ends with the cleanup pass below, whatever happened, and
exits non-zero when any test did not record, including one already at its cap.
The other commands:

- `cassette owner <fixture>...` prints every test that records a fixture.
- `cassette spend [--cap-per-wire USD] [--cap-total USD]` prices the attempt
  ledger conservatively. A failed or interrupted attempt costs at least $0.05.
- `cassette scan [--base REF] [<fixture>...]` checks changed fixtures for
  credentials, account ids, emails and home paths. It matches case-sensitively
  on whole tokens, and searches for every exported `*_API_KEY`, `*_TOKEN` and
  `*_SECRET` value literally.
- `cassette goldens [--test TARGET]...` regenerates effect goldens from
  replay: each selected target runs with `RIG_REGENERATE_GOLDEN=1`, so each
  golden holds what its producer logs over its replayed cassette.
- `cassette cleanup [ledger.jsonl]` runs the cleanup pass on its own.

The recorder refuses to write a fixture, and panics, in two cases:

- A reply is an account failure the cell did not declare as its subject: a
  refused credential, a spent quota, a rate limit or an empty balance. This
  includes one delivered inside a successful stream. Declare an expected
  failure with `CassetteSpec::expects_account_failure` or, in a wrapper that
  presents a rejected key, `ProviderCassette::expect_account_failure`
  (`bogus_api_key()` declares it too).
- An OpenAI or xAI Responses request stored a response the same session never
  deleted. Send `store: false` unless the cell is a chain that deletes its
  own state.

Fixtures older than the stored-state rule are listed in
`crates/rig-cassette/tests/common/stored_state_grandfathered.txt`. When you
re-record one with `store: false`, delete its line; the safety tests fail
until you do.

A failed or refused recording is written under the attempt root, never over
the fixture. The attempt root is `RIG_CASSETTE_ATTEMPT_DIR`, or by default
`cassette-attempts` in the target directory. As replies arrive, before the
test can panic, the recorder appends every stored response, file, cache,
interaction, conversation or vector store it sees created to `ledger.jsonl`
there. `cargo xtask cassette cleanup` deletes whatever the ledger still holds,
using each provider's API key. A 404, or Gemini's 403 "not found", counts as
already gone.

The recorder normalizes volatile timestamps (`created`, `created_at`, …) at
any depth, so a recording pass sees live values its fixture never holds.
Compare a reply document with its fixture through
`assert_matches_recorded_document`, or one field through
`assert_wire_value_matches`. Both compare such keys by type when recording and
exactly in replay. The cassette-safety tests reject an exact comparison of a
volatile key outside replay.

A handful of committed cassettes hold bytes no provider will return: they are
hand-derived from a live capture, or deliberately corrupted to pin a parser
regression. The test that owns such a fixture marks itself, so the sweep above
abandons it instead of healing it:

```rust
if crate::cassettes::skip_when_recording(
    "cell 3 is hand-derived from cell 2: the tool input carries a control byte",
) {
    return;
}
```

Hand-editing a committed cassette means adding that line, with a reason naming
why the bytes are not obtainable live. Scripted fault families need no marker:
they borrow frames from another scenario's fixture and never open a recording
session of their own.

#### Stale objects slow macOS test processes

On macOS, incremental builds leave each relinked test binary's object files
beside it in `target/debug/deps`, and nothing deletes the old ones. Every
process that asks CoreFoundation for its main bundle lists that directory:
reqwest does when it reads the system proxy settings, and rustls-native-certs
does when it loads the platform trust store. With hundreds of thousands of
stale objects the listing takes seconds per test. A fully parallel
`-p rig-cassette` run then fails the tests that start an httpmock server,
which loads the trust store (`no native root CA certificates found`). Delete
the stale objects. A binary built before then loses only its backtrace line
numbers until it is relinked:

```bash
find target/debug/deps -maxdepth 1 -name '*.rcgu.o' -delete
```

Bedrock replay gives the SDK a plain HTTP client for the loopback replay
server, so it never loads the trust store.

#### Time in cassette tests

Code whose requests depend on time (a cache that expires, a TTL chosen from
the gaps between calls) sends different bodies when time differs, and the
request snapshots pin bodies byte for byte. Give such code the session's clock,
`ProviderCassette::clock()`, instead of the system clock:

- **Recording** reads wall time and saves every reading, in order, beside the
  fixture as `<fixture>.clock.json`.
- **Replay** returns the same readings in the same order. It panics when the
  code asks for more readings than were recorded, and `finish` panics when it
  asks for fewer: either way the code reads time differently from its
  recording, so re-record.

A test that needs a real pause calls `ProviderCassette::pause(duration)`. It
sleeps only while recording, returns at once on replay, and refuses anything
over `MAX_PAUSE` (60 s): no cassette test waits longer, and no replayed test
sleeps at all.

#### Request snapshots

Every replay in this workspace also checks each request body against the
request snapshot beside its fixture, `<fixture>.requests.json`, and fails
with the differing JSON pointers when they disagree. `.cargo/config.toml`
sets `RIG_CASSETTE_SNAPSHOTS=check` and `cargo xtask verify` forces it; the
published engine leaves snapshots off.

A snapshot holds only how the body Rig sends differs from the recorded one,
one entry per differing interaction:

```json
{
  "interactions": [
    {
      "index": 0,
      "changes": [
        { "path": "/messages/0/content", "recorded": [{ "text": "hi", "type": "text" }], "sent": "hi" },
        { "path": "/messages", "splice": 2, "recorded": [], "sent": [{ "role": "user" }] }
      ]
    }
  ]
}
```

`path` is a JSON pointer into the scrubbed, key-sorted recorded body.
`recorded` alone removes a key, `sent` alone adds one, and both replace a
value. With `splice`, `recorded` and `sent` are the array items removed and
inserted at that index. A multipart body is compared as its parts: headers,
and the body as text up to 4 KiB, else its length and FNV-1a hash. A fixture
whose requests all equal their recordings has no snapshot. Full copies of
every request body would add about 156 MB beside 222 MB of cassettes; the
recording already pins those bytes, so only the difference is stored.

#### Shape-matched replay

Replay in this workspace matches a request to a recording by its coarse
shape, not its bytes: `.cargo/config.toml` sets `RIG_CASSETTE_MATCHING=shape`
and `cargo xtask verify` forces it; the published engine matches exactly.
The method, path, query and recorded headers still match exactly. The body
key keeps:

- the field structure, with every value and its type erased;
- every array of objects in order, so the roles of `messages` and the items
  of a Responses `input` stay in sequence;
- the values of `model`, `role`, `name` (tool names, also inside content)
  and `stream` (the reply mode, with the path and query);

and reduces a content value (`content`, `parts`, `system`, `instructions`,
`output`, `prompt`, `response`, `result`) to the tool names in it, so a
string and a one-part text array agree. Tool schemas and tool arguments
(`parameters`, `input_schema`, `arguments`, `args`, ...) are erased, an array
of scalars collapses, and a multipart body is keyed by its field names.
Unordered replay serves, among the unplayed interactions with the key, the
one whose expected body (the recording with its snapshot applied) the
request is closest to, counted in snapshot changes.

So a change to how Rig writes a request that keeps its coarse shape needs no
re-record: the request still replays, the snapshot check fails until
`cargo xtask cassette snapshots` records the difference, and the reviewer
reads that diff. A change of shape (a new field, another role sequence)
misses its recording and needs one. A cell whose subject is the exact matcher
opts out with `CassetteSpec::exact_matched()`.

```bash
cargo xtask cassette snapshots                  # rewrite every snapshot from replay
cargo xtask cassette snapshots --test openai    # rewrite only what one target replays
cargo xtask cassette snapshots --check          # replay and fail on any difference
```

A rewrite deletes every snapshot first, then replays with
`RIG_CASSETTE_SNAPSHOTS=write`; with `--test` it deletes nothing. Run it after
a change to what Rig sends or after re-recording a fixture, and review the
snapshot diff with the change.

#### Acceptance index

`crates/rig-cassette/fixtures/acceptance.toml` maps every request fact Rig
sends, per provider and encoder, to one interaction recorded live whose
request holds it. A fact is one object of the body: its path with array
indices collapsed, its sorted keys, and each key's scalar type or container
kind, keeping the values of discriminators such as `role` and `type`. A
JSON Schema's property names (`properties`, `$defs`, ...) share one path, and
a tool call's arguments or a tool's result is not read. A body that is not
JSON is one fact: empty, binary, text, or its multipart field names. Facts
do not capture combinations of fields: a request that only combines objects
already recorded needs no recording of its own. The coarse key and replay
still exercise every whole request. What Rig sends is read offline from each
recording with its snapshot applied, so the check reads files only. A
recording whose request body the proxy recorder dropped is pinned to the
facts its snapshot showed when the fixture was first indexed, until the
fixture changes.

```bash
cargo xtask cassette acceptance           # rewrite the index
cargo xtask cassette acceptance --check   # CI's `verify --check acceptance`
```

`--check` fails when a fact Rig sends has no live recording, naming the
provider, the encoder, the path, the fact and the smallest cassette that
sends it, and when the index is stale. The rules for a change to what Rig
sends:

- it keeps every request's coarse shape: refresh the snapshots, rewrite the
  index, and record nothing;
- it adds a fact: re-record one cassette per provider and fact that
  `--check` lists, and only those;
- never re-record a cassette whose shape did not change.

ChatGPT record mode additionally needs `CHATGPT_ACCESS_TOKEN=... CHATGPT_ACCOUNT_ID=...`.

Bedrock cassette replay does not require AWS credentials. Bedrock record mode uses the AWS
SDK credential provider chain and a direct SigV4-aware recorder, so it requires AWS credentials
with Bedrock model access in `us-east-1`. The Bedrock recorder buffers streaming/event-stream
responses and stores non-UTF-8 cassette bodies as base64; those opaque bodies are intended for
replay fidelity, and safety checks also scan their decoded bytes for credential-shaped material.

Venice's text-to-speech scenario records through the direct recorder rather than
the httpmock proxy: the proxy exports bodies as strings, so a binary response
(raw audio) is exported with no body at all and replays as zero bytes. Its
transcription scenario stays on the proxy path, where the same limitation drops
the *request's* multipart body. A cassette that recorded no body still matches
a multipart request; its request snapshot pins the parts Rig sends.

Run one cassette test by passing a test-name substring after the test target;
the filter is a substring match, so use the full module path only when the
shorter name is ambiguous:

```bash
cargo test -p rig-cassette --all-features --test gemini \
  streaming_tools_smoke \
  -- --nocapture --test-threads=1
```

### Cassette Safety

Record mode scrubs and safety-checks cassette contents before writing fixtures,
and the committed cassette safety tests enforce the same scrubbed form during
normal test runs. Scrubbing removes credentials (API keys, bearer tokens,
cookies, OAuth tokens, SigV4 material), AWS account numbers in ARNs and
home-directory paths, drops response headers outside a small allowlist, and
normalizes volatile timestamps. Everything else the provider sent is recorded
verbatim:
thinking signatures, encrypted and redacted reasoning, tool-call and response
ids, cache keys, cursors, generated images. Those values are provider-issued
and carry no secret of ours, and keeping them lets a recording seed a live
call and lets a replay decode the provider's own bytes. Request matching
compares the recorded bytes as they are, so a fixture must hold exactly what
the test sends: a fixture recorded before this policy replays only while its
placeholders are values a response delivered and the test echoes back, and a
fixture whose placeholder stands for a value the test itself mints must be
re-recorded. Still review every cassette diff for:

- no API keys, bearer tokens, cookies, or provider account identifiers;
- expected request paths, methods, and bodies;
- expected provider responses for the scenario;
- no unrelated cassette churn.

### History survival and portability

A provider hands Rig opaque fields only it can interpret (thinking signatures,
encrypted reasoning, redacted reasoning, reasoning item ids, tool-call ids)
and expects them back on the next turn, in the slot that carries them: the
signature on its own reasoning block, the ciphertext on its reasoning item,
the call id on both the call and its result. A Responses message's `phase`
is held to the same rule: it must return on the assistant item with the
message's id. The rule lives in
`test-support/rig-test-support/src/history_survival.rs`, modeled per dialect,
and is applied twice:

- `crates/rig-cassette/tests/cassette_history_survival.rs` sweeps every
  committed cassette and effect golden at zero provider cost: delivered fields
  must reach every continuation request in their slot, every recorded request
  must pair its tool calls with results (a stored Responses chain answers
  calls held by `previous_response_id`), every native `chat_history` must
  pair too (this includes the requests after cancellations, invalid arguments
  and provider faults), and a census proves each provider and content kind
  was examined. Legacy `REDACTED_<n>` placeholders cannot prove a slot and
  are counted, not judged. Exemptions need a cited provider behavior and are
  reported when stale.
- `history_survival_matrix` cells per provider run three prompts over
  deterministic lookup/verify tools with one transient tool failure, assert
  the same delivered content on unary and streaming transports, then apply
  the rule to the recording they just made. `portability_matrix` cells decode
  another wire's committed reply through Rig's real decoder, continue it on
  the target wire, and assert no foreign reasoning state reached it.

An assistant turn records its origin: the wire format, provider and model
that produced it. A request to that same model replays the turn's provider
items verbatim, and any other request replays only its canonical fields.
Further cell families exercise the same round trip: `stateful_chain_matrix`
(OpenAI `previous_response_id` and file ids, Gemini `cachedContents` and
Interactions), `adversarial_matrix` (reused call ids, reordered parallel results, long
ciphertext, signed empty reasoning, a history ported across three providers
and back), `image_input_matrix` and `request_identity_matrix`. Chains create
and delete their server-side resources in the recorded session, so a
committed chain replays but its handles cannot seed a live call.

Record a cell with the ordinary record mode and an exact test name. Set
`RIG_LONG_TASK_ATTEMPT_DIR` to keep the exchanges of a failed attempt outside
the tree, and `RIG_HISTORY_SURVIVAL_CENSUS` or `RIG_PORTABILITY_REPORT` to
write the census and the forwarded-field report to a file.

### Goldens checked in the test

A producer listed in `crates/rig-cassette/fixtures/in-test-goldens.txt` checks
its log in the test instead of against a committed golden: the log replays
record by record through its own replayers by effect id. Such a golden is
never written, `RIG_REGENERATE_GOLDEN` included.

The parity snapshots (`fixtures/parity/<provider>.json`) are kept the same
way: each pins one reply per reply shape and mode, the smallest, then the
first in path order, and every other reply is checked in the test by its
`call` and `stream().finish()` agreeing. `RIG_REGENERATE_PARITY=1` rewrites
the pinned entries.

## Prompt Cache Testing

Provider prompt caching is a **prefix match**: the cache key is derived from the exact request
bytes up to each breakpoint, so any change to an earlier block invalidates everything after it.
The failure that costs real money is therefore not "caching is off" — it is "caching silently
degraded", where a reordered map, a rewritten earlier turn, or a re-advertised tool set moves the
prefix and every request quietly misses while the counters stay non-zero.

Four layers guard this. Each catches something the others structurally cannot.

### Layer 0 — free, key-free, whole-corpus (`tests/cassette_cache_prefix.rs`)

Checks over cassettes that already exist, costing no provider traffic:

- `recorded_conversations_do_not_move_their_cache_prefix` — for consecutive same-endpoint requests
  in one cassette, turn N-1's canonical blocks must be a prefix of turn N's. The rule lives in
  `test-support/rig-test-support/src/cache_prefix.rs` and is shared with the per-scenario harness so the two cannot
  drift. It compares cached *content*: `cache_control` markers are stripped first, because
  Anthropic's documented incremental-caching pattern moves the breakpoint forward every turn and
  that is correct behavior, not a prefix move.
- `every_provider_is_covered_by_the_prefix_check` — a per-provider census that makes the check
  **fail closed**. Every recorded request is classified modeled / non-conversational / unmodeled,
  and an unmodeled *conversational* endpoint is a finding rather than a silent skip.
- `*_request_serialization_is_deterministic` — serializes the same `CompletionRequest` eight times
  through each provider's real client and requires byte-identical output. This is the only check
  that can see unstable map or tool ordering: cassette replay compares key-sorted canonical JSON,
  and a `serde_json::Value` round-trip normalizes key order too, so a `HashMap` in a request body
  busts every real cache while all recorded evidence looks identical.
- `every_cassette_provider_has_a_cache_suite` — a provider with cassettes and no cache scenario
  fails unless it is in `NO_CACHE_SUITE` with a reason.

### Layer 1 — the shared harness (`test-support/rig-test-support/src/cache_conformance.rs`)

One deterministic three-turn probe — warm, byte-identical repeat, then append and repeat —
asserted against a per-provider `CacheSupport` descriptor, so adding a provider is a descriptor
rather than another copy of the assertion logic.

The assertion that carries the weight is `assert_hit_ratio`. `cached_input_tokens > 0` passes
just as happily when 200 of 40,000 prefix tokens are cached as when 39,800 are; the ratio is
taken against turn 1's billed prompt. **Getting the denominator wrong makes the assertion
vacuous**, and it is one rule on every provider: `CacheSupport::input_tokens`, turn 1's
`input_tokens`, which counts cache reads and writes as `Usage`'s rustdoc
(`rig_core::completion::Usage`) states. `tests/cassette_usage_census.rs` enforces that
contract on every recorded call of every provider.

Growth is asserted as a ratio, not a monotonic token count: providers cache in coarse blocks, so
the absolute figure drifts a few tokens as block boundaries re-align (Gemini was measured going
3,765 -> 3,760 across a 21-token append) while a genuine prefix move collapses it to zero.

### Layer 2 — per-provider cassettes (`crates/rig-cassette/fixtures/cassettes/<provider>/prompt_caching/`)

Recorded live, replayed key-free. Replay is not a tautology: the harness matches request bodies,
so a rig change that perturbs the outbound prefix fails as a replay miss in CI with no API key.
Record one scenario at a time:

```bash
RIG_PROVIDER_TEST_MODE=record \
cargo test -p rig-cassette --all-features --test openai openai::cassette::prompt_caching \
  -- --exact --nocapture --test-threads=1
```

### Layer 2b: long runs (`test-support/rig-test-support/src/cache_longrun.rs`)

A probe proves a hit on turn 2 or 3. Caching can still degrade over a long run: a marker that
stops moving forward, a prefix that drifts after turn 40, a cache key that changes, or cache
writes that cost more than the reads save. Each provider with caching records the same 100-turn
support chat (the handbook preamble, one `lookup_order` call per turn, about 200 calls) under
`fixtures/cassettes/<provider>/long_run_caching/` (Gemini: `auto_caching/`), plus a 30-turn run
with caching off as the baseline.

`cache_longrun` holds everything the runs share:

- The workload (`SUPPORT_PREAMBLE`, `LookupOrder`, `question`) and the turn helpers `chat` and
  `chat_streamed`, which retry a 5xx or 429 up to three times with record-only pauses of 2, 5
  and 10 s and count the retries.
- One reader per wire, `CacheWire::{Gemini, Anthropic, OpenAiChat, OpenAiResponses}`. For each
  recorded call it gives rig's `Usage` as the wire reported it (input, cached reads, cache
  writes, output and total), the conversation the call continues, the OpenAI
  `prompt_cache_key`, and for Gemini the cache the call reads and every `cachedContents`
  resource created. Prompt tokens are `input_tokens` on every wire, as `Usage`'s rustdoc
  states.
- The figures, printed as one `CACHE_LONGRUN <provider>/<scenario> {json}` line per run so a PR
  table is generated rather than transcribed: calls, fixture bytes, retries, prompt tokens,
  cached reads, cache writes, cached share, hit rate (calls after the first that read anything),
  the smallest per-call share from the first read on, cache accuracy (reads over cacheable
  tokens, where a call's cacheable tokens are its conversation's previous prompt), where the
  reads fell short of that, writes over prompt tokens, input dollars with and without caching
  through `rig_core::completion::CacheCost` at the provider's rates, and the same for the first
  30 turns alone, to set beside the baseline.
- The checks `check` runs on every run: rig's `Usage` matches the wire on every call (input,
  cached reads, cache writes, output and total); the input saving, counting every write, read
  and stored token-hour, is at least the run's floor; from each conversation's first read on,
  every call is cached above the run's floor and none reads nothing (the prefix-move
  signature); cache writes stay under the run's cap.

"Not faked" means something different per provider, and each run asserts its own form:

- **Anthropic** bills a write above the input price, so a marker that stops moving forward
  shows as the same prefix written again and again: writes stay under a share of prompt tokens.
- **OpenAI** caches automatically and bills writes on GPT-5.6 and later: every request carries
  the same `prompt_cache_key`, and writes stay under a share of prompt tokens.
- **Gemini** caches are resources: a request that reads one carries none of what it holds,
  cache plus request tail is the conversation byte for byte, every cache is deleted, every
  replaced cache was read at least three times, and creation stays at or below 15% of prompt
  tokens.

To add a provider, give `CacheWire` a variant with its reader (the call path, the usage fields,
the first user message), add a `with_<provider>_long_run_cassette` wrapper and register it
with the cassette-safety scan, and record one 100-turn fixture and its baseline.
Rates come from the provider's price page, quoted with the date. Thresholds are placeholders
until a first recording confirms them; never loosen one to make a recording pass. Record with:

```bash
cargo xtask cassette record --cap 3 <provider>/long_run_caching/<fixture>.yaml ...
```

A 100-turn run takes six to eleven minutes to record. Replay prints the figures:

```bash
cargo test -p rig-cassette --all-features --test anthropic long_run_caching -- --nocapture --test-threads=1
```

#### Workloads beyond the support chat (`cache_longrun::workloads`)

The support chat measures one shape of traffic. `cache_longrun::workloads` holds the others,
each a deterministic function that drives one conversation and returns its
`RunLog`. Each names its conversation with a marker in the first user message, so one fixture
can hold several conversations and `LongRun::conversation` can pick one out.

- **`document_session`**: a store policy of about eight thousand tokens attached on the first
  turn, then a question about it every turn. On Anthropic the document enables citations from
  the first request, because enabling them later changes the rendered system prompt.
- **`mid_system_chat`**: a mid-conversation system message every few turns, alternately where
  Anthropic takes one and where rig moves it after the next user turn. The run asserts every
  instruction stays in `messages` and none is hoisted into `system`.
- **`dynamic_tools_chat`** with the `ToolSchedule` hook: the advertised tools change every few
  turns through a request patch. A turn that fails ends the run and is returned, so a run can
  assert a refusal.

The runs live in `<provider>/cassette/long_run_workloads.rs` (workloads on their own) and
`long_run_features.rs` (features that can move a prefix), with fixtures under
`<provider>/long_run_caching/`:

| run | model | measures |
|---|---|---|
| `document_60` | claude-opus-5-5 | a large fixed input with citations, read back every turn |
| `mid_system_every_10_60` | claude-opus-5-5 | mid-conversation system messages keep the prefix |
| `dynamic_tools_30` | claude-opus-5-5 | a changed tool list: the thinking-block binding 400 on turn 11 |
| `document_60_responses` | gpt-6-sol | as `document_60`, without citations |

The OpenAI run is stateless (`store: false`), reasons at low effort and sends one
`prompt_cache_key`. It runs on Responses only: the GPT-6 models take function tools on Chat
Completions only at `reasoning_effort: "none"`, and gpt-6-astra and gpt-6.1-sol not at all.

To add a workload, write it in `cache_longrun::workloads` as a deterministic function taking
the agent, the clock, its size and a conversation marker; keep generated inputs fixed text
(no nonce or timestamp), so a re-recording sends the bytes its cassette holds. Add a run for
it in `long_run_workloads.rs` with its own `Limits`, rehearse it once under a scratch scenario
name, and record it with `cargo xtask cassette record`.

### Layer 3 — live economics (`live_cache_economics`, `#[ignore]`d)

A cassette pins what a provider did at record time; only a live run catches the provider changing
its cache semantics under us. Each cell prints a `LIVE-CACHE-ECONOMICS` row, so the economics
table can be regenerated rather than trusted as a transcription:

```bash
cargo test -p rig-cassette --all-features --test openai live_cache_economics \
  -- --exact --ignored --nocapture --test-threads=1
```

### Rules for recording cache fixtures

- **Never commit a cache cassette whose recorded turn 2 shows zero reads.** The Layer-1 assertions
  run identically in record mode, so a bad session fails instead of committing a fixture that pins
  a miss. That is the intended outcome — a recorded miss is worse than a failed recording.
- **Some providers need a warm-up pass.** Gemini's implicit cache only serves a prefix once an
  entry for *that exact prefix* exists, so on a cold run the grown turn-3 prefix reads zero. Run
  every implicit-cache scenario twice and keep the second recording. Mistral's cache is
  intermittent for a different reason (routing without cache affinity) and may need several attempts.
- **Padding must be deterministic and committed** — no nonce, no timestamp. A nonce guarantees a
  turn-1 miss, churns the cassette on every re-record, and breaks body matching. The org-pre-warm
  risk it would avoid is already tolerated by `assert_warms`.
- **Pad above the provider's documented minimum** (`min_cacheable_tokens` in the descriptor) or
  the API silently declines to cache. Where a provider's rate limit is tighter than the default
  probe, `CacheProbe::with_padding` shrinks it — Groq's 8,000 TPM tier needs this.
- **All three turns must record back-to-back in one test body**, because cache TTLs are minutes.
  The shared probe does this by construction.
- **Never mark a cache scenario `.unordered()`.** Ordered replay is what lets two byte-identical
  requests replay two different recorded responses, which is the only reason a miss-then-hit pair
  is replayable at all.

### Gemini: two caching features, and they behave differently

Gemini is the one provider in the matrix with **two** caching features, and the
suite covers both because they are not interchangeable.

**Implicit caching** is automatic and best-effort. Measured on
`gemini-2.5-flash` over an 18,497-token corpus: five consecutive turns reusing
that corpus read **zero** cached tokens (~92k tokens billed at full price), and
only a sixth request read 99.6%. It keys on a prefix the provider has already
seen, so a fresh conversation starts cold and there is no way to pre-warm it.

**Explicit caching** (`cachedContents`) uploads once and hands back a handle.
Same corpus, same day: **100.0% on turn one**, and 100.0% again from an
unrelated conversation. It bills storage per token-hour, so it pays when one
large fixed payload is reused enough to beat that — and it pays immediately
rather than after a warm-up.

Practical consequences for anyone touching these fixtures:

- **Disable thinking** (`generationConfig.thinkingConfig.thinkingBudget: 0`) on
  every cell that is not specifically about thinking. Gemini 2.5 spends its
  output budget on thoughts first, so a small `max_tokens` yields a response
  with no message at all.
- **Explicit-cache cells create billed server-side resources.** Delete what you
  create, including on the failure path.
- **Cache handles are account-scoped and server-generated.** They ride in
  request bodies, request *paths* and responses, and the generated-token
  scrubber cannot reach them (it stops a token at `/`), so
  `scrub_resource_names` handles them. Never assert a literal handle.
- **Pad past the documented 1,024-token minimum.** Every probe's padding
  assumes it; a prompt below it is not cached.

**Automatic caching** (`gemini::caching`, fixtures under
`gemini/auto_caching/`) is recorded as long runs: 100-turn support chats
(about 200 calls each), a 60-call tool loop, four sub-agents and a lifecycle
run. Their cache book reads the session's recorded clock, so each fixture has
a `.clock.json` beside it. They run on the shared long-run checks (Layer 2b),
which for a run with a book include Gemini's rules: no request re-sends what
its cache holds, cache plus request tail is the conversation byte for byte,
every created cache is deleted, every replaced cache was read at least three
times, and cache creation stays under 15% of prompt tokens.

## Live Provider Tests

Live provider tests use real provider APIs, local model servers, or account credentials. They are
ignored by default unless a test file says otherwise.

```bash
# all ignored tests for one provider target
cargo test -p rig-cassette --all-features --test openrouter -- --ignored --nocapture --test-threads=1
# one ignored provider test
cargo test -p rig-cassette --all-features --test openai \
  responses_document_file_id_roundtrip_live \
  -- --ignored --nocapture --test-threads=1
```

Use the provider-specific environment variables named in the ignored test reason or provider
module, such as `OPENROUTER_API_KEY`, `MISTRAL_API_KEY`, `GROQ_API_KEY`, `XAI_API_KEY`,
`HUGGINGFACE_API_KEY`, or local services such as Ollama and llama.cpp (`llama-server`, which also serves a `.llamafile`).

## Local Artifact Model Tests

`rig-candle` has an ignored native model-contract suite. It is not an HTTP
cassette: it loads one pinned Qwen3 GGUF artifact and runs provider-neutral
completion, buffered/raw-streaming parity, parallel and sequential tools,
zero-argument and complex typed arguments, call/result history correlation,
result serialization, invalid-call recovery, hook rewrite chaining, turn-local
request patches, cancellation/max-turn diagnostics, extraction with usage,
tool-choice, protocol hygiene, and synthetic structured-output scenarios
through Rig's agent driver.

```bash
export RIG_CANDLE_TEST_MODEL_DIR="$PWD/crates/rig-candle/test-models/qwen3-4b-q4-k-m"
./crates/rig-candle/tests/download_qwen3.sh
cargo test --release -p rig-candle --test live_conformance \
  -- --ignored --nocapture --test-threads=1
```

The 2.33-GiB model is checksum-verified, cached in an ignored directory, and
loaded once per test binary. The measured ARM64 release run completed in 164.41
seconds; allow at least fifteen minutes for slower CPU hosts and more than twice
the checkpoint size during loading. Use serial execution to bound CPU and memory use.
See `crates/rig-candle/README.md` for revisions, hashes, measured performance,
and the boundary between model-contract and provider-transport tests.

Reusable scenarios and typed validators are exported from
`rig_core::test_utils`. A provider suite should call a complete model-driving
scenario when its cassette records the same prompt and tool definitions. When
wire-specific prompts, schemas, request parameters, or metadata must remain
local, the provider test should retain that transport setup and call the shared
validator on its public Rig result. Do not move authentication, HTTP body, SSE,
hosted-tool, remote-file, or provider-session assertions into the portable
module.

Universal scenarios require only the public completion/agent contract.
Optional capability scenarios—parallel model emission, structured reasoning,
provider-assigned IDs, native constrained decoding, and hosted tools—must be
selected explicitly. A provider that does not expose one optional capability
must not weaken the universal assertions or silently mark the scenario passed.

## Integration Tests

External-service integration tests are collected under the `integrations` target and are gated by
feature flags. Some start Docker containers through `testcontainers`, so Docker must be running;
others are ignored because they need external credentials or pre-provisioned services. Check each
integration module for required environment variables (Vectorize requires `VECTORIZE_INDEX_NAME`;
Bedrock needs AWS credentials plus access to the configured Bedrock models).

```bash
# all enabled non-ignored integration tests
cargo test -p rig-service-tests --all-features --test integrations
# one feature-gated group
cargo test -p rig-service-tests --features qdrant --test integrations qdrant -- --nocapture
cargo test -p rig-service-tests --features mongodb --test integrations mongodb -- --nocapture
cargo test -p rig-service-tests --features sqlite --test integrations sqlite -- --nocapture
# ignored groups
cargo test -p rig --features bedrock --test integrations bedrock -- --ignored --nocapture --test-threads=1
cargo test -p rig-service-tests --features vectorize --test integrations vectorize -- --ignored --nocapture --test-threads=1
```

## Shared test implementation

`test-support/rig-test-support` compiles neutral tools, cassette paths, cache and
stream assertions and golden comparison once. Provider binaries import these
modules; their cassette-safety tests remain registered in each binary. The
generic corpus matrix drivers live under
`crates/rig-cassette/tests/common/corpus_matrix`: a cell edit then rebuilds its
provider binaries without invalidating unrelated providers. Their long-loop
regression tests stay with those modules. Other shared helper regressions run
in the support crate; its `corpus_` regressions also retain the standalone
effect-corpus dependency configuration.

Matrix declarations keep literal scenario names and explicit per-cell parameters.
`golden_matrix!` runs a common agent cell against its named oracle, and
`case_matrix!` selects a family body.
A `case_matrix!` declaration without a wrapper contains only scripted rows;
it registers tests without claiming cassette scenarios.
Their parsers reject malformed rows and exclude ignored rows from the recording
inventory. Keep a compiled listing when changing declarations: source discovery
alone does not establish that a configuration registers or executes a test.

## Regression scenarios

### Runtime scenarios and the reply bank

The agent loop, tool lifecycle, turn endings, resume, memory and the effect
bus behave the same on every provider. Their scenarios run
once, in the `runtime` target of `rig-cassette`
(`crates/rig-cassette/tests/runtime.rs` and `runtime/`), over replies from
the reply bank instead of a provider's cassette. A bank transport serves the
replies in order and ignores what was sent, so each reply still passes
through its provider's real decoder while request encoding stays pinned by
the request snapshots and the acceptance index. Streamed replies arrive one
server-sent event per chunk, so a consumer that stops at a delta drops the
stream before its end, as it does live.

The bank lives in `crates/rig-cassette/fixtures/bank/` and is written by
`cargo xtask cassette bank` from the corpus, offline:

- `<provider>.yaml`: one real recorded reply per provider, completion
  encoder, reply shape and called tools, the smallest the corpus holds, with
  the interaction it came from. A reply shape is the status, the framing, and
  the skeleton of the whole body or of each stream event: JSON keys and types
  kept, values erased except discriminators such as `type` and
  `finish_reason`. A fixture whose test marks it hand-derived gives the bank
  nothing.
- `scripts.tsv`: every fixture's replies as bank keys. A runtime scenario
  names the scenario whose reply shapes it runs against
  (`rig_test_support::bank::script`).
- `pinned.txt` and `pinned.yaml`: a scenario whose assertions read what a
  reply says (an answer marker, tool arguments, reasoning that must be
  non-empty) is listed by hand in `pinned.txt` with its reason, and the bank
  keeps that fixture's replies verbatim in `pinned.yaml`
  (`rig_test_support::bank::recorded`).

An entry or script whose fixture is deleted stays in the bank, so pruning a
cassette does not take its replies from the runtime scenarios. `pinned.txt`
is the only file of the bank written by hand; never edit a reply, re-record
the cassette and rewrite the bank.

```bash
cargo xtask cassette bank           # rewrite the bank after a cassette changed
cargo xtask cassette bank --check   # CI's `verify --check bank`
cargo nextest run --locked --profile local -p rig-cassette --test runtime
```

The runtime target holds the corpus matrix's cells on rig-agent (`cells`),
its focused families (`families`, `faults`), the turn-termination cells
(`termination`, run over every bank reply that decodes to the cell's
ending), the agent tool sessions (`sessions`), the lifecycle matrix
(`lifecycle`), and `decode`, which decodes every bank reply through its
provider's decoder and checks it against the tools and ending the bank read
off its bytes. Which families run here and which stay with their providers
is in `crates/rig-cassette/tests/runtime/runtime-families.md`.

### Stream-fault cells

`tests/providers/{gemini,openai}/cassette/stream_faults.rs` drive the runner
through the real adapter into a stream that ends badly: scripted faults
served by `rig::test_utils`'s sequenced transport, a recording cut before its
terminal (`test-support/rig-test-support/src/stream_faults.rs`), a Gemini
refusal, an in-band error after content. Every scripted frame is a
labelled constant with its provenance beside the cell; no cassette is
hand-edited and no new fixture is committed. A scripted cell pins what the
runtime does with the fault (the failure kind, the record, the committed
history, the tools that never ran); the request shape it would have sent is
pinned by the recording's owning test, not by the cell. Error classification
follows the one funnel in
`rig_core::provider_response` (see `AGENTS.md`, Error Handling).

Consumed cassettes and goldens remain in Git; historical execution logs, proof
snapshots, and archive infrastructure are intentionally not maintained.

---

### Overview (`crates/rig-agent/README.md`)

# rig-agent

`rig-agent` contains Rig's classic agent runtime: builders, the serializable
sans-I/O run state, blocking and streaming drivers, typed hooks, contextual
tools, extraction, and runtime integrations.

Most applications should use the root `rig` facade, where this runtime is
enabled by default. Low-level provider and backend contracts live in
`rig-core`.

Direct users import construction and prompting explicitly:

```rust,ignore
use rig_agent::prelude::*;
use rig_core::{Model, providers::openai::{self, OpenAI}};

let model = OpenAI::from_env()?.completion(openai::GPT_5_2);
let agent = AgentBuilder::new(model).build();
let answer = agent.prompt("Explain ownership briefly.").await?;
```

## Conversation memory

`AgentBuilder::memory` keeps an agent's conversation in a rig-memory
`AsyncStore`: a local `MemoryStore` or `DirStore`, or a store behind a network
or a JavaScript promise. A run loads the conversation (`conversation(log)`
names it) before its first model call and saves its messages with one append
once it finished. A failed load or save fails the run with
`PromptError::Memory`; a run that fails or is cancelled saves nothing. A save
appends only if the log is as the save read it, the store checking and
appending in one step: of two runs that loaded the same log, the second to
save fails with `SessionError::Changed` and adds nothing to the log, even when
the saves overlap, in one process or several sharing the store. A save reads
only its own log, so one store can hold many conversations, a log each. See
`examples/persistent_chat.rs`.

## Recording and replay

The runtime accepts `rig_core::serve::Recorder`, not a concrete log type.
Enable the separate `rig-cassette` crate's `agent` feature for logs and replay
adapters; neither crate configuration adds the native HTTP engine.

Retain a `rig_cassette::effect_log::EffectLogRecorder` and attach a clone with
`AgentBuilder::record_to`. Choose `keeping_stream_events()` when the recording
must preserve stream items. Import `rig_cassette::agent::AgentReplayExt` and
call `agent.stamp(recorder.take())` after the run, or stamp `recorder.log()` for
a snapshot. The extension trait also owns `run_spec_hash` and
`check_replayable`. A host that owns the handler table attaches its recorder
through `Handlers::record_to`.

Register recorded handlers through `rig_cassette::agent::replay::register_all`
or `register_all_checking`, then run the agent as usual. The runtime has no
normal cassette dependency, even with all runtime features enabled.
See [the cassette README](../rig-cassette/README.md) for feature isolation,
checkpoint formats and migration from implicit recording/log getters.

## Handlers and dispatch

An agent dispatches every effect through a `Handlers` table: its own, or a
host's given to `AgentBuilder::over_handlers`. A dispatch looks its handler up
and runs it inside the caller's future or stream, so no executor or driver is
involved, and dropping the future or stream cancels the dispatch and everything
it dispatched. Missing handlers report `HandlerUnavailable`.

Registration refuses a family change under a live key. Typed keys carry a family
proof; binding either a typed or untyped key checks the current descriptor's
family. A handler dispatches nested work through `Handlers::serving(&dispatch)`,
whose dispatches are recorded as children of the one it serves.

Handler layers intercept requests and outcomes, but recording observes the
innermost handler. A denial produces no handler record; an outcome replacement
leaves the original handler answer in the log. Suspended approval retains the
in-flight dispatch. Recorder-provided adapter contexts propagate
to completion adapters unless the request supplies one explicitly; contexts
must distinguish concurrent logical calls, and adapter facts are not effect-log
entries.

## The run protocol

`rig_agent::run::AgentRun` is a steppable, serializable state machine: it owns
every *decision* the agent loop makes — turn budget, tool-call validation and
recovery, history threading, structured-output policy, usage accounting, final
response — and performs no IO. A *driver* calls `next_step()` and acts on the
returned `AgentRunStep` (`CallModel`, `CallTools`, `Done`), feeding results
back with `model_response` / `answer`. This crate's futures loop is the
driver; the machine itself never awaits (a source-level guard keeps it so) and
is `Serialize + Deserialize`, so a run can be suspended between steps and
resumed in another process. Resume with the same Rig version; the envelope format
is checked but cross-version state compatibility is not guaranteed. Serialized
state includes conversation content and every recorded provider response body,
so its sensitivity and size grow with the run. Hosts must apply their persistence
and redaction policy before storing it. Constructing a run directly does not
execute an agent's configured hooks, tools, or retrieval.

Everything an agent loop *is* sits beside it in `rig_agent::run` — the
`RunSpec` it is configured by, `prepare_request` (the pure `(RunSpec,
capabilities, history, tools, patch) → PreparedRequest` step, so every driver
sends the same bytes), the output policy, the per-turn `RequestPatch`, the
run's response and error types, the invalid-call decisions, the streamed-turn
assembler and the loop-side transcript helpers — all sans-IO and serializable.
rig-core keeps only the message-model invariants (`conversation::pairing`, the
`tool::feedback` constructors). A host that drives runs itself (an ECS schedule, a
job system) depends on this crate with default features off: that graph
carries no async runtime, transport or MCP client (a dependency guard pins it),
and `tests/fixtures/agent_run_stepper` is that host in miniature.

## Runtime model routing

High-level agents are concrete values: models are registered in the agent's
handler table under a `ModelRef` label, and each model's `ProviderCapabilities` snapshot
is captured by value at registration. `AgentBuilder::new` registers its model
as `default`; `AgentBuilder::named_model` and `model_route` register models
under labels of your choice. The typed `Model<W, T>` and its erased twin
`DynModel<Completion>` stay available for direct `call` and `stream`, and a
backend is a wire plus a transport rather than a trait implementation.

Replace the default on one agent value with `set_model` (a model value) or
`set_model_label` (a registered label), or change one run's default candidate
with `using_model(label)` or `using_model_value(model)`. Implement
`AgentHook::on_model_select` to route before every model call. Model
selection hooks chain in registration order, with the last selection winning
and a stop terminating the run. Per model-call boundary, completion-call hooks
resolve first and their merged `RequestPatch` is passed to the selection event
(`ModelSelection::request_patch`); only then does request preparation run
against the selected model's captured capabilities and issue the attempt. A
selected model cannot change while its future or stream is in flight. Retries
and calls after tool execution are new boundaries and may select another
model.

Hooks attached through `AgentBuilder::add_hook` apply to every later runner
from that agent; hooks appended to a prompt or runner apply only to that run.
`using_model` changes the run's initial candidate but does not suppress routing
hooks. To force a model, omit routing hooks or append a final hook that always
selects it. Blocking and streaming prompts share this lifecycle: `Stop`
cancels before request preparation and provider execution, and dropping an
in-flight attempt still cancels it by dropping its retained future or stream.

Extractors support the same run-local choice: `extract(...)` returns a
`TypedRun`, so `extractor.extract(text).using_model(label)` or
`.using_model_value(model)` sets that run's default candidate. That model is
the default candidate for each extraction retry, routing hooks may replace it,
and the extractor's default is unchanged for later calls.

```rust,ignore
#[derive(Clone)]
struct RouteModels;

impl AgentHook for RouteModels {
    fn on_model_select(
        &self,
        context: &HookContext,
        _event: ModelSelection<'_>,
    ) -> ModelSelectionAction {
        if context.turn() == 1 {
            ModelSelectionAction::select("fast")
        } else {
            ModelSelectionAction::select("strong")
        }
    }
}

let agent = AgentBuilder::named_model("fast", fast_model)
    .model_route("strong", strong_model)
    .tool(search_tool)
    .build();

let answer = agent
    .prompt("Research, then synthesize")
    .max_turns(3)
    .add_hook(RouteModels)
    .await?;
```

Registered models and routing hooks contain live clients, callbacks, and policy
state, so hook stacks and hook actions are deliberately not serializable;
persist the model's label and register the model again at runtime.
Clones share the retained model safely, while replacing an agent clone has
ordinary value semantics. Concurrent runs keep independent default and
hook-stack snapshots; explicitly synchronized state captured by a hook follows
that hook's own clone semantics. See the credential-free
`runtime_model_routing` example for a complete two-model tool round trip.

Hook stacks preserve registration order, including nested stacks. Request patches
merge; dispatch patches and outcome replacements feed later hooks. Denials,
stops, and model-turn retries short-circuit later hooks, so observers that must
see an event belong before steering hooks. Dispatch interest is a gate: internal
families require opting in through `observes`. Replaced tool output reaches both
the model and result-content telemetry; handler recording retains the original
answer. Streaming deltas are provisional, and `ModelTurnRetried` tells consumers
to discard a rejected turn.

Keep hook-private state in the hook, keyed by run ID when shared across runs.
Use `Scratchpad` for transient cross-hook state and run entries (rig-core
`conversation::Aside`s) for state that must serialize or follow forks. Full snapshots read through `last_entry` avoid
custom replay folds; event-shaped state can instead fold `entries`. Entries
appended during settlement do not enter the finished run. Context-bound handles
use delegation rather than `Deref` so hooks cannot clone an owned dispatch
capability out of their borrowed view.

Retries of tool-free turns consume the ordinary model-call budget. Hooks may
impose narrower limits in run-local state. `ModelTurnFinished` exposes the
attempt's terminal reason and effective output cap for portable truncation
policies; the credential-free `retry_on_truncation` example demonstrates cap
growth on both surfaces.

Tools implement `rig_core::tool::Tool`, work in both runtimes, and receive
each call's `&mut ToolContext`.

Tool output remains typed through dispatch; providers and telemetry render it
at their boundaries. Ordinary serializable results become model output, while
`ToolOutput` and `ToolResultContent` preserve explicit structured or multimodal
content. `Tool::map_error` preserves arbitrary source errors for operators and
exposes safe kind-level feedback to the model by default. Explicit
`ToolExecutionError` constructors expose their detailed messages; use them or
`with_model_output` only for deliberately model-visible feedback. Inbound
context and host-only result metadata belong in `ToolContext`, not model output.

Tool registries publish changes to every attached handler table. Generated tool
keys use `<owner>/tool:<name>#<generation>`, and a request catalog runs the
registration it advertised even after the registry replaced it. Explicit keys
are preserved verbatim: reusing one replaces its binding, even across tool
names. Standalone registry execution runs inline rather than through an agent's
handlers.

## Target support

| Tier | Target | Status |
| --- | --- | --- |
| 1 | native (linux / macOS / windows, `x86_64` and `aarch64`) | Full support, all features; MCP tools via the companion `rig-rmcp` crate |
| 2 | `wasm32-unknown-unknown` (browser) | Supported, with no feature flags to set; `rig-rmcp` (MCP tools) is **not** available |
| — | `wasm32-wasip1` / `wasm32-wasip2` (WASI) | **Not supported** |
| — | `wasm32-unknown-emscripten` | Not supported |

**Building for `wasm32-unknown-unknown` is the entire opt-in** — there are no
wasm feature flags anywhere in the workspace. `rig-core` relaxes its
`WasmCompat*` bounds from the target alone.

Wasm gates name a `target_os` (`all(target_arch = "wasm32", target_os =
"unknown")`) rather than a bare `target_arch = "wasm32"`, because the latter
also matches WASI, which has no JS host. WASI itself does not build: `rig-core`
depends unconditionally on `reqwest`, which pulls `hyper`/`socket2` and a tokio
feature set WASI rejects. Supporting it would mean making `reqwest` optional and
adding a `wasi:http` client behind `rig_core::http_client` — a project, not a
`cfg` fix.

**MCP (`rig-rmcp`) is native-only.** rmcp's `ClientHandler` is declared
`Sized + Send + Sync + 'static` unconditionally — its `local` feature relaxes
the future bounds but not the handler itself — while this crate's handler owns a
tool registry whose erased handlers are deliberately neither `Send` nor
`Sync` on wasm. Enabling `rmcp` on a wasm target fails with a single explanatory
`compile_error!` rather than a wall of trait errors.

---

### Overview (`crates/rig-bedrock/README.md`)

## Rig-Bedrock
This companion crate integrates AWS Bedrock as model provider with Rig.

## Usage

Add the companion crate to your `Cargo.toml`, along with the rig-core crate:

```toml
[dependencies]
rig-bedrock = "0.4.5"
rig-core = "0.36.0"
```

You can also run `cargo add rig-bedrock rig-core` to add the most recent versions of the dependencies to your project.

See the [`/examples`](./examples) folder for usage examples.

Make sure to have AWS credentials env vars loaded before starting client such as:
```shell
export AWS_DEFAULT_REGION=us-east-1
export AWS_SECRET_ACCESS_KEY=.......
export AWS_ACCESS_KEY_ID=......
```

---

### Overview (`crates/rig-candle/README.md`)

# rig-candle

`rig-candle` runs validated local checkpoints through Rig's model and agent
APIs: `CandleModel::completion()` is the model a loaded checkpoint serves, and
`CandlePoseModel::pose()` estimates human poses (see
[Pose estimation](#pose-estimation)). The crate receives byte buffers and
performs no filesystem or network access itself.

```rust,no_run
use rig_agent::agent::AgentBuilder;
use rig_candle::{CandleModel, ModelData};

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let model = CandleModel::from_safetensors_async(ModelData {
        config: std::fs::read("./model/config.json")?,
        tokenizer: std::fs::read("./model/tokenizer.json")?,
        weights: std::fs::read("./model/model.safetensors")?,
    })
    .await?;
    let agent = AgentBuilder::new(model.completion())
        .preamble("You are a concise assistant.")
        .build();
    println!("{}", agent.prompt("Explain ownership briefly.").await?.output());
    Ok(())
}
```

## Validated model profiles

- Llama 3 instruct: one unsharded safetensors checkpoint, explicit Llama 3
  conversation formatting, text conversations.
- SmolLM2-360M-Instruct: Q4_K_M GGUF using Candle's quantized Llama backend,
  explicit ChatML formatting. This remains the small native/WASM demo model.
- Qwen3-4B: the official `Qwen3-4B-Q4_K_M.gguf`, using Candle's
  quantized Qwen3 backend and the explicit Qwen Hermes tool protocol. This is
  the native-only agent-conformance profile; it is rejected on wasm32 because
  its runtime memory cannot fit reliably in wasm32 linear memory.

`ModelArtifacts` selects safetensors versus GGUF explicitly. Native async
constructors and `build_async` perform validation and model construction on
Tokio's blocking pool; synchronous constructors remain available. A load that
has entered the blocking pool runs to completion even if its awaiting future is
dropped. `builder_from_gguf_bytes` gives borrowed GGUF buffers the same
generation/concurrency settings as owned artifacts without copying them,
including static buffers such as `include_bytes!`.
Arbitrary Qwen, Qwen2, Qwen3 MoE/vision, other sizes, shards, and unvalidated
quantizations are rejected rather than treated as Llama.

The loader validates architecture metadata before tensor allocation, exact
profile dimensions and special-token agreement, the complete GGUF vocabulary,
required tensor names/shapes, and the allowed Q4_K_M tensor mix. The effective
GGUF context limit is currently 4096 tokens because that is Candle 0.11's
quantized cache capacity.

## Qwen3 tools and output behavior

Qwen3 supports function name, description, and arbitrary object-shaped JSON
Schema parameters, including optional properties, enums, nested objects, and
arrays. `Auto`, `None`, `Required`, and `Specific` tool choices are represented
explicitly. `Specific` exposes only the named functions; `Required` and
`Specific` add a mandatory-call instruction and reject a model turn with no
call. Malformed/non-object arguments, duplicate IDs, truncated envelopes, and
unmatched results are typed Candle completion errors. Syntactically valid
unknown or disallowed names reach Rig's agent dispatcher, where fail, repair,
retry, or skip handling produces the corresponding typed `PromptError` and
never executes the rejected call.

Argument validation is intentionally split: `rig-candle` validates the JSON
envelope and requires arguments to be an object, while Rig's registered tool
performs typed deserialization/schema validation before execution. Tool
execution always remains in Rig's agent driver. Assistant tool calls and
correlated text or JSON results round-trip through history. Multimodal tool
results and provider-hosted tools are rejected. Caller-controlled text, tool
schemas, arguments, and results containing reserved chat-template delimiters
are rejected before rendering so they cannot create structural prompt content.

Qwen tool syntax can cross token boundaries, so streaming buffers one model
turn, parses it, then emits ordered text/reasoning items and whole tool calls
(a `Start`/`End` pair per call on the stream) followed by the terminal
record. This keeps model XML out of user-visible text
while preserving cancellation, bounded backpressure, and the same parsed result
as buffered completion.

The explicit prompt requests Qwen's no-thinking mode. If a checkpoint emits a
leading `<think>` block anyway, it is represented as Rig reasoning content and
omitted from later rendered history; control syntax is never exposed as normal
text.

Direct `CompletionRequest::output_schema` returns a typed unsupported-feature
error: decoding is not grammar constrained. Rig's `OutputMode::Tool` works and
is the supported path for a real tool followed by a synthetic structured final
result. That mode is best-effort and Rig validates the returned JSON; it is not
native grammar enforcement.

## Pinned live model

Download the official artifacts with the reproducible helper:

```bash
export RIG_CANDLE_TEST_MODEL_DIR="$PWD/crates/rig-candle/test-models/qwen3-4b-q4-k-m"
./crates/rig-candle/tests/download_qwen3.sh
cargo test --release -p rig-candle --test live_conformance \
  -- --ignored --nocapture --test-threads=1
```

The script uses immutable revisions, retries resumable temporary downloads,
checks size and SHA-256, then atomically installs each file. Verified files are
reused and `test-models/` is ignored.

| artifact | revision | bytes | SHA-256 |
|---|---|---:|---|
| `Qwen3-4B-Q4_K_M.gguf` | `bc640142c66e1fdd12af0bd68f40445458f3869b` | 2,497,280,256 (2.33 GiB) | `7485fe6f11af29433bc51cab58009521f205840f5b4ae3a32fa7f92e8534fdf5` |
| `tokenizer.json` | `1cfa9a7208912126459214e8b04321603b3df60c` | 11,422,654 (10.89 MiB) | `aeb13307a71acd8fe81861d94ad54ab689df773318809eed3cbe794b4492dae4` |
| `config.json` | `1cfa9a7208912126459214e8b04321603b3df60c` | 726 | `8ba006f74fecfaaeb392872a60f4a480e7ec9860153d2e1b769ec81f9a147f8a` |

The live test loads the model once, reuses cheap `CandleModel` clones with a
fresh KV cache per request, applies greedy decoding and a fixed seed, enforces
timeouts, and prints tokens, tool counts, history size, timings, throughput, and
safe output. On the ARM64 development host used for the release-mode
verification, it passed a direct completion plus sixteen portable reports:
buffered/raw-streaming text
parity, optional arguments, parallel calls, serial host execution of a parallel
batch, a zero-argument tool, string/JSON result serialization, complex nested
Unicode and escaped arguments, invalid-call fail/repair/retry/skip recovery,
hook rewrite chaining with turn-local request patches, cancellation and
max-turn diagnostics, structured extraction with usage, sequential tools,
streamed tool execution, buffered and streamed synthetic structured output,
and all tool-choice modes. Actual resident memory and speed depend on the target
and concurrent system load; the loaded quantized tensors are based on a
2.33-GiB GGUF, KV cache grows with context, and loading temporarily also holds
the 2.33-GiB input byte buffer alongside constructed tensors. Plan for more than
twice the checkpoint size during loading rather than treating file size as a
hard memory bound.

## Conformance boundary

Portable model-contract helpers live under `rig_core::test_utils`. The
artifact-backed Candle suite calls the model-driving scenarios directly.
Provider cassette suites reuse the same runners where their recorded request
already represents the scenario, and reuse the typed validators when transport
fixtures need provider-specific prompts or schemas. Current adoption covers
Gemini parallel/zero-argument/result-serialization, recovery, cancellation,
hook, extraction, and structured-output cases; OpenAI extraction and structured
output; Anthropic hook rewrites, extraction, and structured output; and Ollama
optional and sequential tools.

The universal model contract covers semantic text, typed arguments, canonical
call/result history, streaming finalization and usage, hook-visible outcomes,
extraction, and structured output. Parallel model emission, structured
reasoning, provider-assigned IDs, native constrained decoding, and hosted tools
remain optional capabilities and are selected independently rather than hidden
behind a passing umbrella test. Provider suites retain HTTP serialization,
authentication, SSE framing, hosted tools, remote files, and provider-specific
reasoning/session assertions; those are transport conformance, not local model
quality.

## Pose estimation

`rig_candle::pose` runs YOLOv8 pose checkpoints in the safetensors layout of
Candle's YOLOv8 example (`lmz/candle-yolo-v8`, sizes n, s, m, l and x). Its
`PoseEstimation` operation takes RGB frames and yields one `FramePoses` event
per frame, in order: every person's box, confidence and the 17 COCO
keypoints, in the frame's pixels. A call folds the events into a `PoseTrack`.
The model is a `Local<PoseEstimation>` wire on the loaded checkpoint, so it
streams, erases to `DynModel<PoseEstimation>` and swaps transports like any
other Rig model.

```rust,no_run
use futures::StreamExt;
use rig_candle::pose::{BodyPart, CandlePoseModel, ImageFrame, PoseRequest};
use rig_core::streaming::Item;

# async fn run(weights: Vec<u8>, video: Vec<(u32, u32, Vec<u8>)>) -> Result<(), Box<dyn std::error::Error>> {
let model = CandlePoseModel::builder(weights)
    .confidence_threshold(0.5)
    .build_async()
    .await?
    .pose();
let frames = video
    .into_iter()
    .map(|(width, height, rgb)| ImageFrame::from_rgb8(width, height, rgb))
    .collect::<Result<PoseRequest, _>>()?;
let mut poses = model.stream(frames)?;
while let Some(item) = poses.next().await {
    if let Item::Event(frame) = item? {
        for person in frame.people {
            println!("{:?}", person.keypoint(BodyPart::LeftWrist));
        }
    }
}
# Ok(())
# }
```

The loader reads the size from the checkpoint and refuses a detection or
non-COCO checkpoint before allocating tensors. Frames are resized so their
longest side is the builder's `input_size` (640 by default, a multiple of 32)
and the other keeps the aspect ratio; overlapping boxes above
`iou_threshold` are suppressed. Frames run one at a time on a blocking worker,
and dropping the stream stops it before the next frame. A reply that ends
before every frame was answered fails the call.

The weights are Ultralytics' YOLOv8 pose weights and are licensed under
AGPL-3.0; rig-candle neither ships nor downloads them. Check that license
before you ship them. `examples/candle_pose` runs the model on JPEG or PNG
files, and the opt-in live test checks the nano checkpoint on a photo:

```bash
./crates/rig-candle/tests/download_yolov8_pose.sh
cargo test --release -p rig-candle --test live_pose -- --ignored --nocapture
```

| artifact | revision | bytes | SHA-256 |
|---|---|---:|---|
| `yolov8n-pose.safetensors` (`lmz/candle-yolo-v8`) | `be388c6fab95ae3035a039070e1b883b9c5a1325` | 6,650,412 | `08c76047b41744c027c8150e81d1dbbd94f996656cf5be266491367ae7fad072` |
| `bike.jpg` (`huggingface/candle`) | `7a62aad24a5d8b1d8cb351e5ef77a7b3457a74b8` | 182,991 | `317e4a9d2d2be7859ba0ab8726a526f5ece9d77daf92857f3e93fb7b367824c1` |

## Runtime behavior and limits

Builder defaults can be overridden by request `max_tokens` and `temperature`,
by the `GenerationOptions` `top_p` and `seed`, and by the `CandleOptions`
provider options `top_k`, `repeat_penalty` and `repeat_last_n`
(`rig_candle::extension`). The same five keys are accepted in
`additional_params`, which take precedence; unknown keys are errors. Omitting
`top_k` or `top_p` inherits its builder default, while an explicit JSON `null`
in `additional_params` disables it.
Native inference runs in `spawn_blocking`; model loading and each complete
inference operation run inside Candle's CPU context so its private Rayon pool
stays active. `max_concurrent_requests` defaults to one. Every request owns its
cache and sampler. Cancellation is cooperative between Candle forwards, and the
bounded stream channel has capacity eight.

WASM inference is synchronous and should run in an application-owned Web Worker.
The maintained browser example embeds SmolLM2 at compile time, rejects modified
artifacts during its build, bounds input/history growth, and includes both a
model-independent worker test and an opt-in real-artifact smoke test. CPU only;
no CUDA/Metal selection, batching, multimodal prompts, or checkpoint shards.

---

### Overview (`crates/rig-cassette/README.md`)

# rig-cassette

Rig's record/replay home: effect logs and runtime replay adapters, the native
HTTP cassette engine, the committed fixture corpora, and their verification suites.

| part | path | published |
|---|---|---|
| effect logs | `src/effect_log/` | yes, always |
| classic-agent replay adapter | `src/agent/` | yes, `agent` feature |
| native HTTP engine | `src/http/` | yes, `http` feature |
| provider cassettes | `fixtures/cassettes/<provider>/...yaml` | no (`exclude`) |
| agent effect-log goldens | `fixtures/effects/<name>.effects.json` | no (`exclude`) |
| cassette provider suites | `tests/<provider>.rs`, `tests/providers/`, `tests/common/` | no (`exclude`) |
| effect-bus verification | `tests/verify/` | no (`exclude`) |
| minimal verification runner | `tests/minimal/Cargo.toml` (shared test sources) | no (`publish = false`, `exclude`) |

## Features and dependency direction

Defaults are empty. An effect-log-only consumer uses:

```toml
rig-cassette = { version = "0.42.0", default-features = false }
```

| features | public modules | additional normal dependencies |
|---|---|---|
| none | `effect_log` | core contracts, futures, serialization and base64 only |
| `jsonl` | `effect_log`, `effect_log::jsonl` (JSON-lines files, native, or a file of a session's store such as `SESSION_EFFECT_LOG`; a recording for replay that no restore reads) | `rig-memory` (its `Files` trait) |
| `agent` | `effect_log`, `agent` | `rig-agent`, without its default features |
| `http` | `effect_log`, `http` | the native HTTP server/client engine, Tokio, ordered/round-trip JSON |
| `bedrock` | `effect_log`, `http` | `http` plus Smithy event-stream decoding |

Effect logs alone acquire neither the agent runtime, HTTP clients/servers,
Tokio nor AWS dependencies. The agent integration is selectable on its own and
does not enable HTTP. These guarantees concern the selected normal dependency
graph; Cargo can still unify features requested by other dependencies in the
same build.

The dependency direction is cassette → runtime → core. The runtime does not
depend on cassette, including through optional features. The `rig` facade re-exports
this crate as `rig::cassette` behind its opt-in `cassette` feature; with the
facade's `agent` feature it also enables the cassette agent adapter. Direct
minimal consumers should depend on `rig-cassette`, rather than the facade's
default transport configuration.

The facade and provider helpers needed by this package's tests remain
version-less path dev-dependencies, omitted from its published manifest.
The facade dev-dependency explicitly enables its capability features so provider
targets retain the same test inventory without relying on workspace defaults.
Independent downstream graph guards live in `src/http/paths.rs` and
`tests/core/dependency_graph.rs`; they distinguish native-engine isolation from
the minimal library and the agent adapter.

## Effect logs and runtime integration

`rig_cassette::effect_log` owns `EffectLog`, `EffectLogRecorder`,
`EffectLogReplayer`, `RequestCheck`, header validation and stable hashing. The
log wire format and fingerprint inputs are unchanged.
The recorder implements `rig_core::serve::Recorder`; the replayer implements
the ordinary core handler interface.

Effect records require `tool_output`: `null` means no publication and an object
means published values, including an empty map. Omitting it cannot establish
whether values were lost, so decoding rejects omission. Replayers publish these
values before resolving outcomes, including errors. Handlers must publish before
resolving and must exclude inbound context, secrets, and live capabilities.
Custom outcomes keep their JSON value in an explicit `payload` field. Nested tool
identities retain origin tags; bare-string identities are rejected. Logs validate
their data rather than a global format number; unknown header fields, including
`format`, are rejected. An interrupted run resumes from its saved state with
`EffectLog::tail` replayed.

Replay preserves recorded semantic families, model identities, and capabilities.
Conflicting declarations are rejected. Callers reapply executable middleware.
Inferred descriptors for logs without declarations cannot establish verified
program compatibility.

The shared replayer supplies exchanges and event sequences. Without retained
stream items, replay cannot prove exact partial state. Stream error positions
preserve ordering even around a final event; a folded outcome cannot
reconstruct that order. Empty metadata is omitted, and logs without error
positions cannot prove the original error-item sequence.

For a classic agent, enable `agent`, retain an `EffectLogRecorder`, and pass a
clone to `AgentBuilder::record_to`. Use `keeping_stream_events()` instead of
`new()` when original stream item boundaries are required. After driving the
agent, import `rig_cassette::agent::AgentReplayExt` and call
`agent.stamp(recorder.take())` (or stamp `recorder.log()` for a snapshot).
The extension trait also supplies `run_spec_hash` and `check_replayable`.
For a host-owned handler table, attach the recorder with
`Handlers::record_to`. Replay registration is
`rig_cassette::agent::replay::register_all` (or `register_all_checking`).

## The engine
Enable `http` and import engine APIs from `rig_cassette::http`. This feature is
native-only and intentionally opts into both JSON features listed above.


Pass a fixture root containing provider directories to `ProviderCassette::start`,
`start_via(RecordVia::Direct, ..)`, `cassette_path` and every `recorded_*` reader:

```text
<fixture-root>/anthropic/completion.yaml
<fixture-root>/openai/nested/scenario.yaml
```

`CassetteSpec::new` preserves strict interaction order; `.unordered()` permits
matching any unused interaction. `finish` checks complete consumption and shuts
down the replay server. A replay session dropped without `finish` panics when
it still holds an unplayed interaction or a refused request, so a test that
returns early cannot pass on a recording it never played to the end. The guard
stays silent while the thread is already panicking, for a fully played session,
and after `finish_after_test_result` returns a test's own error. Invalid
fixtures and failed assertions panic, preserving the original test-support
behavior. Applications that record or replay outside a test use
`ProviderCassette::try_start_at` and `try_finish`, which return a
`CassetteError` for each of those failures instead. `try_finish` shuts the
session down whatever its outcome, so dropping it afterwards never panics.

`RIG_PROVIDER_TEST_MODE` defaults to `replay`. `record` contacts the configured
upstream and overwrites the selected fixture after scrubbing; `start` and
`start_via` read the mode from the environment, and a recording reaches the
provider through the proxy (`start`) or directly (`start_via(RecordVia::Direct, ..)`).
Consumers that stage candidates use `ProviderCassette::start_at` with an explicit
mode and exact path: a live capture records into a candidate path, and a
verification pass replays with `CassetteMode::Replay` even when the environment
asks for recording, so a candidate is never implicitly promoted to a fixture.
While a live run is in progress, `checkpoint_recording` writes the completed,
scrubbed exchanges to a partial path without finalizing the recording.

`RIG_CASSETTE_SNAPSHOTS` is off unless set. With `check`, a replay session
also compares each request body with the fixture's request snapshot,
`<fixture>.requests.json` (`request_snapshot`), and `finish` returns
`CassetteError::SnapshotMismatch` naming each difference by JSON pointer.
With `write`, the session rewrites the snapshot from the requests it received.
A snapshot holds only how a sent body differs from its recording, compared
after scrubbing as canonical JSON or as multipart parts, so a fixture whose
requests equal their recordings has none. This repository sets `check` for
its own cassette tests.

Replay compares request bodies exactly unless `RIG_CASSETTE_MATCHING=shape`
is set or the spec says `.shape_matched()`; `.exact_matched()` keeps exact
matching whatever the variable says. Shape matching still compares the
method, path, query and recorded headers exactly, but compares bodies by a
coarse key: the field structure, the order of arrays of objects, and the
values of `model`, `role`, `name` and `stream`, with every other value, the
types of content, tool schemas and tool arguments erased. A change to how a
request is written then replays without a new recording, and with snapshots
on in `check` mode the session fails until the snapshot holds the change.
Unordered shape matching serves, among the interactions with the request's
key, the one whose body is closest to the request's. This
repository sets `shape` for its own cassette tests.

A recording only becomes the fixture when the test passed and the recording is
clean. A failed test's exchanges, and a recording `finish` refuses, go under
`attempt_root()` (`RIG_CASSETTE_ATTEMPT_DIR`, default `cassette-attempts` in the
target directory). `finish` refuses two kinds of recording:

- one holding an account failure that the cell did not declare: a refused
  credential, a spent quota, a rate limit or an empty balance, as classified
  by `reply_account_failure`, including a failure delivered inside a 2xx
  stream. Cells declare one with `CassetteSpec::expects_account_failure`,
  `ProviderCassette::expect_account_failure` or `bogus_api_key`.
- one in which an OpenAI or xAI Responses request stored a response that the
  session never deleted (`cassette_stored_state`).

A relay in front of the proxy, and the direct recorder for unary and
multipart requests, append every created stored response, file, cache,
interaction, conversation and vector store to the `ledger` module's
`ledger.jsonl` before the reply reaches the test. `ledger::clean_up` deletes
what the ledger still holds. The `cassette_tool` example exposes both checks
and the cleanup pass.

`DirectRecorder`, its request/response types and `DirectRecordingHttpClient`
preserve binary bodies that a text proxy cannot record. When the proxy omits a
non-UTF-8 multipart request body, replay accepts multipart input without comparing
its missing bytes; provider unit tests must cover the multipart shape. Other
requests with an absent recorded body must be empty. SSE and ordinary binary
responses require only `http`. Enable `bedrock` for Smithy event-stream decoding
and scrubbing; its Smithy dependencies are absent otherwise.

The engine retains credential scrubbing, strict request matching, and safety
validation; scrubbing also redacts every key shape `rig_core::redact` knows,
in text, bodies and headers. Provider-issued values (ids, signatures, encrypted reasoning,
cache keys, cursors, generated images) are recorded verbatim, and request
matching compares recorded bytes as they are. Repository-specific source
scans and fixture censuses remain with each caller. Rig's adapter in
`test-support/rig-test-support/src/cassettes.rs` is a thin path binding that
supplies `crates/rig-cassette/fixtures/cassettes`; a downstream can supply
`fixtures/cassettes` of its own instead. `Retry-After` response headers retain
canonical seconds or HTTP dates for replay diagnostics; malformed values are
discarded instead of persisting arbitrary server text. Home-directory paths are
scrubbed separately because credential scans cannot identify operator names or
cache layouts. Only paths at the beginning of a string value are eligible,
preserving embedded public URLs and the model basename. Spaces remain part of
the path to avoid leaking account names.

## The corpora

`fixtures/cassettes/<provider>/` holds the recorded HTTP interactions the
provider suites in this package replay. `fixtures/effects/*.effects.json` holds
agent-produced effect logs. All fixtures are data: they are
regenerated by their producer, never edited by hand, and never regenerated to
make a check pass. `.gitattributes` exempts the cassettes from the
blank-at-eof whitespace check because SSE bodies legitimately end in a blank
line.

No corpus is embedded with `include_*!`; the binaries read them at runtime
from `CARGO_MANIFEST_DIR`, which is why `exclude` can keep them out of the
published tarball (the crates.io 10 MiB upload limit is enforced server-side
and never surfaces in `cargo publish --dry-run`).

## The recording loop

Background, not an instruction to record: recording contacts a real provider
and is a deliberate, separately authorized act.

A golden is produced from the recorded HTTP in three steps:

1. **Record the HTTP.** The producer test runs against the real provider under
   `RIG_PROVIDER_TEST_MODE=record`, on its own exact test filter, and writes a
   cassette under `fixtures/cassettes/`. The golden call is a no-op in that
   mode.
2. **Produce the agent golden.** The same producer runs again in replay mode under
   `RIG_REGENERATE_GOLDEN=1`, so the golden is generated from the *replayed*
   cassette and holds exactly the bytes replay serves.
3. **Replay the goldens.** The suite here replays them with no provider behind any
   key. A change in what the program asks (a kind), what it was answered (an
   outcome) or how a stream was delivered (its events) fails the replay naming
   the record and the JSON pointer of the difference. Fix forward, and
   re-record live when the change is intended, never by hand-editing a golden.

Hooks are program (the header names them; a different stack is refused before
the first dispatch); tools are record (a replayer answers them); nothing the
engine mints is random, so the same program produces the same log twice.

Agent producers live in this package (`tests/providers/*/cassette/corpus_*.rs`)
and the root package (`tests/core/golden_*.rs`); the shared matrix driver is
`tests/common/corpus_matrix.rs` with its `corpus_matrix/` modules.
`tests/core/golden_pairing.rs` enforces one producer per golden.

## The verification suite

`tests/verify/main.rs` is the single entrypoint: every matrix below is a module
of that target, not a target of its own, so the shared corpus implementation
(`tests/corpus/mod.rs`, which holds the program table and the dimension table of
an effect trace as a whole) is compiled once.

The unpublished `rig-cassette-minimal` package in `tests/minimal/Cargo.toml`
points at the same `verify` entrypoint and reuses the effect-log and classic
replay unit-test sources through its `effect_log` target. It depends on
`rig-cassette` with only `agent`, without the native HTTP engine, facade or
provider helpers. Testing the main cassette package does not prove this
isolation: its dev-dependencies enable the native engine.

CI runs the minimal targets separately with zero retries, and the shared
verification target through the default-member graph with two. The all-features
workspace run excludes the nested package to avoid repeating the same sources
with unified features. `core-all` separately retains the migrated classic replay
unit tests; the default/all-features cassette library executes all migrated
library regressions. The planner tracks both the shared test entrypoints and
the shared library regression sources.

| matrix | module | subject |
|---|---|---|
| — | `golden_replay.rs` | the original corpus: ten goldens, both agent interpreters |
| A | `corpus_retrieval.rs` | retrieval effects |
| B | `corpus_hooks.rs` | the hook surface |
| C | `corpus_serving.rs` | serving policy, routing and bus ownership |
| D | `corpus_outcome.rs` | continuation, cancellation and failure outcomes |
| E | `corpus_request_shape.rs` | request-shape axes that change the spec hash |
| F | `corpus_endings.rs` | hook-ended runs |
| G | `corpus_invalid.rs` | invalid tool calls, streamed and ignored |
| H | `corpus_output.rs` | output modes |
| I | `corpus_host.rs` | a host's own families over the host's bus |
| J | `corpus_memory.rs` | memory operations |
| K | `corpus_delta.rs` | the delta wire |
| L | `corpus_resume.rs` | resumption under everything |
| M | `corpus_shaping.rs` | per-turn shaping |
| N | `corpus_breadth.rs` | provider breadth for the pass-2 shapes |
| O | `corpus_oracle.rs` | the oracle and the header |
| P | `corpus_layers.rs` | layers |
| Q | `corpus_causal.rs` | causal dispatch |
| S | `corpus_header.rs` | the header's new types |
| T | `corpus_leftovers.rs` | the leftovers of the #2443 review, and `Denied` |

Each module carries its own dimension table, its cells and what it found; that
header is the census, not this list. Beside the matrices, `golden_refusal.rs`
proves a stale golden refuses rather than passing on a different trace,
`record_replay.rs` and `durable_execution.rs` pin recording and interrupted
runs, `interpreters_agree.rs` states interpreter agreement as a proptest
property, and `log_header.rs` pins the header.

The two agent interpreters (the classic runner and the direct `AgentRun`
driver, both in `tests/corpus/mod.rs`) replay the goldens their matrices
enumerate, including cancelled streams: the replayer answers the record as the
cancel it was, after the events it kept. A producer listed in
`fixtures/in-test-goldens.txt` replays its log record by record through its own
replayers by effect id instead of comparing it with a committed file.

## What lives where

- here: effect logs, concrete replay adapters, the cassette engine,
  `fixtures/cassettes/`, agent goldens in `fixtures/effects/`, every
  cassette-backed provider suite with its producers, and the behaviour of the
  classic agent's handlers (record and replay, durable execution, the two
  interpreters agreeing);
- the root package's `tests/core`: guards that scan the source tree and the
  fixture runners (they need the repository root), plus the one-producer-per-
  golden pairing guard;
- the root package's `tests/providers`: the live-only provider suites of
  providers with no recorded corpus;
- `rig-core`/`rig-agent` unit tests: anything that needs crate-private types.

## Running

```sh
RIG_PROVIDER_TEST_MODE=replay cargo nextest run --locked -p rig-cassette-minimal --test effect_log --retries 0
RIG_PROVIDER_TEST_MODE=replay cargo test --locked -p rig-cassette --lib --all-features
RIG_PROVIDER_TEST_MODE=replay cargo nextest run --locked -p rig-cassette --all-features -E 'binary(verify)'
RIG_PROVIDER_TEST_MODE=replay cargo nextest run --locked -p rig-cassette-minimal --all-features --retries 0
RIG_PROVIDER_TEST_MODE=replay cargo nextest run --locked -p rig-cassette --all-features --test <provider>
```

`RIG_REGENERATE_GOLDEN` must be unset for all of them. The lane owners of these
executions, including their retry policies and the default-member graph twins,
are in `xtask/src/verify/checks.rs` (`default-tests`, `effect-corpus`,
`bus-verification`).

---

### Overview (`crates/rig-core/README.md`)

# Rig
Rig is a Rust library for building LLM-powered applications that focuses on ergonomics and modularity.

More information about this crate can be found in the [crate documentation](https://docs.rs/rig-core/latest/rig_core/).
## Table of contents

- [Rig](#rig)
  - [Table of contents](#table-of-contents)
  - [Features](#features)
  - [Installation](#installation)
  - [WASM target support](#wasm-target-support)
  - [Simple example:](#simple-example)
  - [Integrations](#integrations)
  - [Who is using Rig?](#who-is-using-rig)

## Features
- Portable contracts for agent runtimes, including completions, messages, conversations, and tools
- `conversation`: one conversation type with stable message ids, authors, asides, branches and a change log a store replays; `rig-memory` saves it
- Provider facts a host acts on: calls a provider cut off, prefill support, foreign reasoning
- `Model::check`: a dry run that lists the options a model would drop
- Full [GenAI Semantic Convention](https://opentelemetry.io/docs/specs/semconv/gen-ai/) compatibility
- 20+ model providers, all under one singular unified interface
- Built-in providers selectable as data: `Catalog::connect("anthropic/claude-opus-5-5")` builds a model from a `vendor/model` reference with the key from the environment, in the protocol family the model catalog names, and the model encodes and prices with that catalog's facts. `providers::registry` references name a model or carry a whole typed configuration, and both round-trip through serde without a credential. Model references discard embedded credentials and reject empty identifiers; a configuration's `id()` returns a catalog selection only when its dialect name is registered. Providers outside this catalog can use a `Model` and `ModelAdapter` directly.
- A model catalog an application can correct: lay a fetched copy of models.dev (`Catalog::from_models_dev`, which skips what it cannot read) and the user's override file (`Catalog::from_overrides`, which rejects a misspelt vendor, field or model id with a "did you mean" hint) over `Catalog::builtin()` with `with_models_dev` (which keeps rig's hand-reviewed facts) and `with_overrides`, and connect through the result.
- `catalog::Connector`: reach a catalog's models by `vendor/model`, with keys or a sign-in
- 10+ vector store integrations, all under one singular unified interface
- Full support for LLM completion and embedding workflows
- Support for transcription, audio generation and image generation model capabilities
- Integrate LLMs in your app with minimal boilerplate
- Full WASM compatibility (core library only)

## Installation
```bash
cargo add rig-core --features reqwest
```

The `reqwest` feature gives provider clients a default HTTP transport, so
`OpenAI::from_env()` needs no client of yours. Without it, pass a transport
with `OpenAIConfig::from_env()?.connect(http)`.

## WASM target support

`rig-core` supports the browser-oriented `wasm32-unknown-unknown` target. When
the `pdf` feature is enabled, the host must provide the Web Crypto API's
`Crypto.getRandomValues` implementation, as modern browsers, Web Workers, and
Node.js 19 or later do. WASI targets are not supported.

## Simple example
```rust
use rig_core::{
    completion::CompletionRequest,
    providers::openai::{self, OpenAI},
};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // The client reads `OPENAI_API_KEY` and sends through the bundled reqwest
    // client (rig-core's `reqwest` feature). A config's `connect(http)` uses another.
    let model = OpenAI::from_env()?.completion(openai::GPT_5_2);

    let response = model
        .call(CompletionRequest::new("Who are you?").temperature(0.2))
        .await?;
    println!("{}", response.text());

    Ok(())
}
```
Note using `#[tokio::main]` requires you enable tokio's `macros` and `rt-multi-thread` features
or just `full` to enable all features (`cargo add tokio --features macros,rt-multi-thread`).

You can find more examples in the repository-level `examples/` directory. Many provider-specific examples now also live as ignored live integration tests under the repository-level `tests/providers` directory, organized by provider. When running those provider-backed tests, prefer provider-specific targets such as `cargo test -p rig --test openai -- --ignored --test-threads=1` to avoid rate-limiting. More detailed walkthroughs are regularly published on our Dev.to blog and added to Rig's official documentation at `docs.rig.rs`.

## Integrations
Rig supports the following LLM providers out of the box:

- Anthropic
- Azure OpenAI
- ChatGPT and GitHub Copilot auth-backed clients
- Cohere
- DeepSeek
- Gemini
- Groq
- Hugging Face
- Hyperbolic
- llama.cpp (`llama-server`, and llamafile)
- MiniMax
- Mira
- Mistral
- Moonshot
- Ollama
- OpenAI
- OpenRouter
- Perplexity
- Together
- Venice
- Voyage AI
- xAI
- Xiaomi MiMo
- Z.ai

The built-in `rig_core::vector_store::in_memory_store::InMemoryVectorStore`
stores serializable documents without requiring `Eq` or `Default`. Its custom
ID callbacks accept closures that capture and mutate application state.

Vector stores are available as separate companion-crates and as feature-gated modules on the root `rig` facade:

```toml
rig = { version = "0.36.0", features = ["lancedb", "fastembed"] }
```

- MongoDB: [`rig-mongodb`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-mongodb)
- LanceDB: [`rig-lancedb`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-lancedb)
- Neo4j: [`rig-neo4j`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-neo4j)
- Qdrant: [`rig-qdrant`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-qdrant)
- SQLite: [`rig-sqlite`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-sqlite)
- SurrealDB: [`rig-surrealdb`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-surrealdb)
- Milvus: [`rig-milvus`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-milvus)
- ScyllaDB: [`rig-scylladb`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-scylladb)
- AWS S3Vectors: [`rig-s3vectors`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-s3vectors)
- HelixDB: [`rig-helixdb`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-helixdb)
- Cloudflare Vectorize: [`rig-vectorize`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-vectorize)

The following providers are available as separate companion-crates:

- AWS Bedrock: [`rig-bedrock`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-bedrock)
- Fastembed: [`rig-fastembed`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-fastembed)
- Google Gemini gRPC: [`rig-gemini-grpc`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-gemini-grpc)
- Google Vertex: [`rig-vertexai`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-vertexai)

## Who is using Rig?
Companies, applications, libraries and articles built on Rig are listed in [awesome-rig](https://github.com/0xPlaygrounds/awesome-rig), a community-maintained list. Using Rig? Open a pull request there to add your project.

## Provider selection persistence

Registry references distinguish registered presets from explicit configurations.
Qualified `vendor/format:model` strings remain unambiguous when a vendor gains
another protocol family, but do not freeze preset defaults, endpoint availability,
or model names. Explicit configurations retain host, route, and typed options and
serialize as objects rather than lossy labels. References remove credentials;
standalone configurations may hold them at runtime. Deserialization requires a
self-describing format such as JSON.

Copilot presets accept already-exchanged session tokens and derive their endpoint
from those tokens. Explicit configurations keep their host and instruction
placement. Token exchange and asynchronous SDK setup belong to the host. The
catalog is not exhaustive: other models can use `ModelAdapter` directly.
Registry-qualified names do not replace the provider names used in telemetry.

## Provider implementation

A provider has a serializable configuration (`OpenAIConfig`) and a client
(`OpenAI`), the configuration on a transport. The client builds one `Model`
per capability: the capability's wire, which says what to send and how to
read the reply, on the client's transport. One driver runs every model.
Chat-compatible dialects use the shared `Chat` wire and decoder with
`Dialect` data and `BodyRewrite` hooks rather than duplicating request conversion.
This keeps normalization, retry classification, and telemetry consistent.

An operation (`wire::Operation`) names its request, event and response types,
which event ends a reply, and the fold one reply's events pass through; it is
written once, and every provider of it is interchangeable behind a `DynModel`.
A crate outside Rig adds an operation and serves it with its own wire and
decoder over HTTP, or with `driver::Local` and a transport of its own for an
in-process runtime.

Each wire encodes requests without transport access and creates a fresh decoder
for each reply. Buffered and streaming replies use the same classifier and event
mapping. A buffered response with a distinct shape is another classified event,
not a separate normalization path. The driver owns framing and asynchronous I/O;
decoders can be exercised directly with frames without opening a connection.
Credentials stored in configuration use `Secret`, whose serialized redaction
must be replaced with a credential when the host reloads the configuration.

Construct normalized completion responses through their builders so finish
reasons reconcile with tool output. Preserve unknown terminal reasons in `Other`,
use the selected descriptor's provider name, and retain the decoded provider
payload in `raw` for typed inspection without a second request. Preserve error
bodies through `ProviderError::from_http_response` for failed HTTP responses and
`ProviderError::from_provider_body` for error envelopes on successful HTTP
responses.
Credentials require redacted debug output. Provider changes need coverage for
supported streaming, usage, tool and multimodal content, with examples and facade
exposure matching the configured capabilities.

Request serialization must use stable map ordering. Randomized `HashMap`
iteration can change request bytes and serialized schemas embedded in prompts,
reducing prefix-cache reuse. Compare raw serialized bytes when testing this;
canonicalized cassette JSON can hide ordering differences. The JSON helpers sort
map keys and recursively sort rendered values regardless of serde_json's
`preserve_order` feature.

## Anthropic prompt caching

Manual caching marks the system prompt, final tool definition, and final message
block. Automatic caching delegates the moving conversation breakpoint to the
provider. Combined mode retains static-prefix markers within the four-marker
budget, including any markers already supplied on provider-specific tools.
The final request is validated after raw parameter overrides are merged, so
custom system, message, and tool markers cannot bypass the budget or TTL checks.

A longer TTL can keep shared tools and instructions cached across conversations
without paying the longer storage lifetime for every conversation tail. Set
`with_static_prefix_cache_ttl(CacheTtl::OneHour)` with automatic five-minute
caching for this arrangement. One-hour markers must precede five-minute markers;
request encoding rejects the inverse ordering. Cache-write prices differ by TTL,
so choose a lifetime based on actual reuse.

Native Anthropic Messages response estimates use the reported five-minute and
one-hour creation-token breakdown. Five-minute writes use the catalog write
rate; one-hour writes use twice the catalog input rate, following
[Anthropic's standard API pricing](https://platform.claude.com/docs/en/about-claude/pricing).
A missing or inconsistent breakdown for nonzero writes leaves the write-cost
estimate incomplete. Provider-reported costs take precedence, and other
providers keep their configured pricing behavior. These are API list-price
estimates, not subscription charges or custom-endpoint invoices; service-tier,
long-context and hosted-tool fees may differ. Previously recorded estimates
are not retroactively recomputed.

Caching is skipped when the prefix through a marker is below the model's minimum:

| Model | Minimum tokens |
| --- | ---: |
| `claude-opus-4-7`, `claude-opus-4-6`, `claude-opus-4-5` | 4096 |
| `claude-sonnet-4-6` | 2048 |
| `claude-sonnet-4-5`, `claude-opus-4-1`, `claude-opus-4`, `claude-sonnet-4` | 1024 |
| `claude-haiku-4-5` | 4096 |

## Gemini explicit caching

Explicit caching uploads reusable content and returns a `cachedContents` handle.
Requests can reuse it immediately, including across conversations. Storage is
billed per token-hour until deletion or expiry, in addition to cached-input
charges. Implicit prefix caching is automatic and best-effort; it does not
provide a handle or guarantee a warm first request.

A request using an explicit cache cannot also send `systemInstruction`, `tools`,
or `toolConfig`. Rig rejects these conflicts before sending. Agents therefore
need no preamble, advertised tools, or configured tool choice on those turns.
An empty `RequestPatch::active_tools` allow-list suppresses tool advertisements,
but does not make cached function declarations executable by the agent. Agent
advertisement and dispatch share a registry snapshot. To execute functions
specified in a cache, drive `GenerateContent` directly and append matching
function responses yourself. Provider-hosted tools such as `codeExecution`
need no caller-side dispatch.

Native structured output and context documents can accompany a cache because
neither adds those conflicting fields. `OutputMode::Tool` adds a synthetic tool
and extends the preamble; `OutputMode::Prompted` adds schema instructions to the
preamble. Neither can accompany a cache. Extractors select tool output mode.

Cache resource paths accept a bare id or a `cachedContents/` handle. Validation
rejects path separators, query delimiters, fragments, and traversal segments
rather than escaping them, so lookup, expiry updates, and deletion cannot be
retargeted by an invalid handle. A 403 or 404 on an existing handle maps to
`ProviderError::CacheExpired`, preserving the provider's reply because 403 can
also indicate credential or quota problems. Cache creation does not apply this
mapping, so authorization failures do not become recreation loops.

## Provider observations

`observe::AdapterContext` carries a caller-owned operation identity and a
`Witness` sink. Pass it separately from request data through
`Model::call_observed(request, context)` or `Model::stream_observed(request,
context)`; `Model::call` and `Model::stream` record nothing. Request builders
and request literals contain only provider request data.

Bus-backed `ModelHandle::call_observed` and `ModelHandle::stream_observed`
take the same context. `ModelAdapter` forwards `Dispatch::adapter_context`;
explicit caller context takes precedence over Recorder/Observe context for
that invocation, including through handler layers. It is never serialized into
provider data or effect records and is not inherited by child calls.

Clone the context for attempts of the same operation;
use a distinct non-sensitive identity for another logical call.
`for_host_attempt(subject, ordinal)` rebinds a host retry to its current
dispatch subject while sharing the logical operation and HTTP send counter.
Facts carry the optional host ordinal separately: multiple HTTP sends within
one dispatch share its host ordinal. Already-running attempts keep their
original subjects and ordinals. A changed logical request needs a new context.

The Gemini unary and SSE paths emit `Action::Adapter` facts for send, HTTP
response status, provider metadata, error envelopes and closure through their
shared drivers. Unary closure
reports decoding, error or drop. SSE closure distinguishes terminal, EOF,
partial frame, error and drop; recoverable corrupt frames are separate facts.
Usage facts retain provider-reported optional counts even when decoding or
normalization rejects a response. Each fact is a cumulative snapshot for its
attempt: replace earlier snapshots instead of summing them, keep absent counts
unknown, and do not add overlapping token categories to invent a total.
Provider verdicts retain reported finish/block reasons and model versions;
error-envelope fields remain separate from HTTP status and Rig retryability.
Sparse verdict fields update only when present. Response IDs and allowlisted
headers are bounded, scrubbed analysis data, excluded from semantic comparison.
An ID-only payload attaches its latest ID to the next verdict or attempt
closure without creating an empty semantic event. This also preserves the ID
on error and consumer drop. Known request credentials are redacted before
persistence; arbitrary response bodies and headers are not copied wholesale.
Credential extraction includes URL userinfo and known query keys, including
origin-form request URIs. Diagnostic comparisons account for percent escapes
and control removal without rewriting safe diagnostic text. Hosts can reuse
`observe::diagnostic_url_secrets` for endpoint configuration; its returned
secrets must stay in memory and must never be persisted or logged.
The normalized Gemini unary API closes after normalization, so an empty
decoded response closes as an error; the raw API retains its decode boundary.
Error closures also preserve the original typed error boundary before report
conversion erases HTTP subtypes: request construction, provider response,
response decoding, typed transport termination, or unknown. An opaque client
error stays unknown; its message is never used to infer a boundary. This field
does not change the error returned to the caller or its retryability.
`ObservationLog::with_clock` optionally stamps individual facts with host elapsed
time. Rig does not calculate run, handler or transport intervals. These stamps
describe the executing environment, including replay, and do not establish live
provider performance.
Partial-frame facts count raw bytes after the last blank SSE delimiter without
retaining their contents. Complete-frame counts include only frames passed to
the adapter driver, excluding provider-recognized analysis-only frames (Gemini
frames containing only a valid string `responseId`). Unknown, malformed and
otherwise-empty frames still count. Corrupt-frame ordinals start at one using
the same counting rule. `TransportEof`
records body completeness independently: a provider terminal followed by
an incomplete trailing frame retains both its terminal closure and partial
EOF evidence. The context does not promise cross-execution identity. Response bodies and
credentials are not copied into these boundary facts.

---

### Overview (`crates/rig-ecs/README.md`)

# rig-ecs

The kernel of the rig coding agent, on [Bevy](https://bevy.org) and
[Rig](https://github.com/0xPlaygrounds/rig)'s rig-core, for building your own
agent harness. It holds only what every agent needs, whatever its interface.
Subagents, compaction, usage and the effect log are plugins on its public API.

- `agent`: an agent is an entity whose components hold its conversation, model,
  reasoning setting, system prompt and tool access. A running turn is an entity
  `TurnOf` its agent, and its calls are entities `CallOf` the turn.
- `stage`: every step of an agent's work is an effect entity. Each frame the
  `AgentPass` schedule runs its stages (Arrive, Review, Commit, Admit, Prepare,
  Gate, Start) until nothing moves, and a plugin steps in with an ordinary
  system in a stage.
- `turn`: the turn loop: model requests, tool calls, retries, stops, and how a
  turn ends (`TurnOutcome`).
- `tools`, `prompt`: tools and system-prompt sections are entities plugins
  register.
- `inbox`: a message to an agent is an effect too (`Incoming`, sent by
  `Deliver`).
- `model`: the agent's catalog model and how it is connected.
- `tasks`, `effects`: work off the main thread on Bevy's task pools, and the
  one path every effect is dispatched and recorded through.
- `session_log`, `restore`: the session log in any rig-memory `Store`, the
  restore after a restart, and state kept with the session (`Saved`).

The rules a plugin follows, with an example per extension point, are in
rig-harness's [plugin guide](../rig-harness/PLUGINS.md).

`AgentPlugin` adds Bevy's `TimePlugin` when the app has none. The crate has no
features and builds for `wasm32-unknown-unknown`. The
[`rig-harness`](../rig-harness) terminal coding agent is built on it.

This crate replaces an older, unrelated `rig-ecs` (0.43–0.44) on crates.io.

---

### Overview (`crates/rig-fastembed/README.md`)

## Fastembed integration with Rig
This crate allows you to use [`fastembed-rs`](https://github.com/Anush008/fastembed-rs) with Rig.

Unlike the providers found in the core crate, `fastembed` does not compile to the `wasm32-unknown-unknown` target.

## Installation

```toml
[dependencies]
rig-fastembed = "0.4.0"
rig-core = "0.36.0"
```

The default features enable Hugging Face model downloads and ONNX Runtime binary
downloads through `fastembed`. The root `rig` facade exposes this crate with the
`fastembed`, `fastembed-hf-hub`, and `fastembed-ort-download-binaries` features.

See [`examples/vector_search_fastembed.rs`](./examples/vector_search_fastembed.rs)
and [`examples/vector_search_fastembed_local.rs`](./examples/vector_search_fastembed_local.rs)
for end-to-end vector search examples.

---

### Overview (`crates/rig-gemini-grpc/README.md`)

# Rig-Gemini-gRPC

This companion crate sends the Google Gemini API's `GenerateContent` and `EmbedContent` calls over gRPC instead of REST. The request is the Gemini REST provider's request transcoded to protobuf, and each reply is transcoded once to its REST JSON and read by the same decoder, so the history rules, typed options and replies match `rig_core::providers::gemini`.

## Usage

Add the companion crate to your `Cargo.toml`, along with the rig-core crate:

```toml
[dependencies]
rig-gemini-grpc = "0.44.0"
rig-core = "0.44.0"
```

You can also run `cargo add rig-gemini-grpc rig-core` to add the most recent versions of the dependencies to your project, or enable the `rig` facade's `gemini-grpc` feature and use `rig::gemini_grpc`.

See the [`/examples`](./examples) folder for usage examples.

## Setup

Set your Gemini API key as an environment variable:

```shell
export GEMINI_API_KEY=your_api_key_here
```

## Example

```rust,no_run
use rig_core::completion::CompletionRequest;
use rig_core::providers::gemini::GeminiConfig;
use rig_gemini_grpc::GeminiGrpc;

# async fn example() -> anyhow::Result<()> {
let transport = GeminiGrpc::connect(&GeminiConfig::from_env()?).await?;
let reply = transport
    .completion("gemini-2.5-flash")
    .call(CompletionRequest::new("Hello!"))
    .await?;
# let _ = reply;
# Ok(())
# }
```

`GeminiGrpc::connect` reads the same `GeminiConfig` as the REST client and
connects to its base URL. `GeminiGrpc::with_channel` sends through a tonic
channel the host built instead.

## Features

- Completion, unary and streamed
- Embedding generation
- Tool calling
- Reasoning and thought signatures
- Image input
- Typed options and reply extras (`extension::GeminiGrpcExt`)

---

### Overview (`crates/rig-harness-protocol/README.md`)

# rig-harness-protocol

The protocol between the `rig` launcher and the rig-harness agent it builds and
runs: the `RIG_HOME` layout, session ids, the environment the launcher passes
to the agent, and the exit code with which the agent asks for a restart. It
uses only std, so the launcher stays dependency-free.

---

### Overview (`crates/rig-harness/README.md`)

# rig-harness

The harness of the rig coding agent: a terminal coding agent that is a
[Bevy](https://bevy.org) app made of plugins, on the [`rig-ecs`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-ecs)
kernel and [Rig](https://github.com/0xPlaygrounds/rig)'s rig-core. The `rig`
launcher generates a small Cargo project for it from `RIG_HOME/plugins.toml`,
builds it and runs it; `/reload` rebuilds it with changed plugins and restarts
in the same session. The [agent guide](https://github.com/0xPlaygrounds/rig/blob/main/docs/agent.md)
describes using it.

## A kernel, a harness and plugins

- **The kernel** is rig-ecs: agents, turns and calls as entities, message
  delivery, tools as entities, the session log and its restore.
- **The harness** is this crate: what the binary needs to run and be
  relaunched, and the services every plugin shares. `HeadlessPlugins` is
  Bevy's `MinimalPlugins` with the log, a clean exit on signals and a loop that
  sleeps until there is work. `RigHarnessPlugins` adds the session and its
  store (`StoreOpener`, which refuses, unchanged, a session of another
  format), the kernel, the launcher protocol
  (`Invoked`, `AppStart`, `Secrets`, `Exit`), slash commands, fronts, and the
  shared services every agent uses: settings, the workspace and child
  processes, file watches, plugin data, questions and pickers, and transcript
  notes. The generated `main.rs` ends with `rig_harness::run`.
- **Everything else is a plugin** in a crate of the repository's
  [`plugins/`](https://github.com/0xPlaygrounds/rig/tree/main/plugins) folder, listed in `plugins.toml` and built only
  on public API, exactly as a third-party plugin is: the compiler holds them
  to it. Most are in [`rig-basics`](https://github.com/0xPlaygrounds/rig/tree/main/plugins/rig-basics), one module
  per group, with the services only some plugins use: approvals, the preview
  and the checkpoint of tool calls, and the session picker. The launcher's
  generated `main.rs` adds each with `load`, which records it as an entity
  (`PluginSource`) and marks what it adds (`ProvidedBy`). Any entry can be
  removed.

The default `plugins.toml`, in order:

| Crate | Plugin | What it adds |
|---|---|---|
| `rig-basics` | `prompt::DefaultPromptPlugin` | rig's own system prompt for an agent without one: one fixed text, the same on every request |
| `rig-basics` | `agents_md::AgentsMdPlugin` | the `AGENTS.md` files from the working directory up to the repository root, and `RIG_HOME/AGENTS.md`, in the system prompt |
| `rig-basics` | `skills::SkillsPlugin` | the skills (`SKILL.md`) of the project, `RIG_HOME/skills` and `~/.agents/skills`, listed in the system prompt; `/skill:<name>` |
| `rig-basics` | `models::ModelsPlugin` | `/model`, `/effort` and the model picker |
| `rig-basics` | `login::LoginPlugin` | `/login` and `/logout`: the ChatGPT plan, and providers' API keys |
| `rig-basics` | `models::defaults::DefaultsPlugin` | a new session starts on the last model and reasoning chosen |
| `rig-basics` | `sessions::SessionsPlugin` | `/new`, `/resume`, `/name` |
| `rig-basics` | `sessions::agents::AgentsPlugin` | `/fork`, `/agents` and rewind (Alt+Up to an earlier message, then Enter): the agents of this session |
| `rig-basics` | `compaction::CompactionPlugin` | summarizing a conversation near the context window; `/compact` |
| `rig-basics` | `telemetry::usage::UsagePlugin` | what model calls cost; `/usage` |
| `rig-basics` | `activity::ActivityPlugin` | what each agent is doing, for views |
| `rig-basics` | `coding_tools::ReadTool`, `coding_tools::EditTool`, `coding_tools::WriteTool`, `coding_tools::ShellTool` | the coding tools of [`rig-tools`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-tools), one plugin each, acting in the app's workspace; the library's `search`, `list`, `delete` and `move` are left to `shell` |
| `rig-basics` | `coding_tools::AttachPlugin` | the files the user names as `@path`, from the agent's directory, go with the message |
| `rig-basics` | `coding_tools::remote::RemoteHostsPlugin` | a path written `HOST:PATH` is that host's for every tool, through the transport `[remote.hosts]` in `settings.toml` gives it (such as `ssh` or `koh exec`); nothing without hosts |
| `rig-basics` | `reload::ReloadPlugin` | `/reload` and the `reload` tool, with which the agent rebuilds itself, and, when the launcher started the agent, the system prompt's fixed `self_extension` section on how it writes plugins |
| `rig-basics` | `reload::plugins::PluginsPlugin` | `/plugins`: every entry of `plugins.toml`, enabled and disabled, in a picker that turns them on and off and rebuilds at once |
| `rig-basics` | `BasicCommandsPlugin` | `/help`, `/retry`, `/quit` |
| `rig-basics` | `print::PrintPlugin` | `--print` and `--json`: the fronts of a run with `-p`, with stdin piped in, or driven by JSON lines (`rig help-agents`) |
| `rig-tui` | `TuiPlugin` | the terminal view: the front of a run on an interactive terminal, with the status line and pickers other plugins add to |

Listed disabled by default, each a commented-out entry that `/plugins` or
`rig plugin enable <type>` enables:

| Crate | Plugin | What it adds |
|---|---|---|
| `rig-basics` | `approval::ApprovalPlugin` | one question for every effect a plugin wants approved; nothing asks by default, and `SubagentsPlugin` adds it |
| `rig-basics` | `preview::PreviewPlugin` | what a tool call would change on disk, before it runs, for gates that ask |
| `rig-basics` | `checkpoint::CheckpointPlugin` | what each tool call changed on disk, from snapshots before and after |
| `rig-basics` | `telemetry::effect_log::EffectLogPlugin` | every model and tool call in the session's `effects.jsonl` |
| `rig-basics` | `helpers::HelpersPlugin` | the `helper` tool: tasks for `rig -p --json` child processes, one, side by side or in a chain |
| `rig-basics` | `subagents::SubagentsPlugin` | the `task`, `message` and `wait` tools, and the agents at work in the status line |
| `rig-basics` | `telemetry::diagnostics::DiagnosticsPlugin` | the process's warnings and errors, Bevy's included, as the `Diagnostics` resource |
| `rig-inspect` | `InspectPlugin` | the `inspect` tool, with which the agent reads its own running world |
| `rig-steel` | `SteelPlugin` | the `run_steel` tool, whose Steel program orchestrates agents and tools |

An entry names its crate, `crate = "rig-tui"`, and no source: the agent
builds rig's own crates from crates.io at the launcher's version, or from
the rig checkout it is built from (`RIG_SOURCE`). A plugin crate that adds to the terminal view (status
line items, pickers, tool renderers) does so behind a default `tui` cargo
feature, so it also builds without `rig-tui`.

The optional [`rig-inspect`](https://github.com/0xPlaygrounds/rig/tree/main/plugins/rig-inspect) plugin adds the
`inspect` tool, with which the agent reads its own Bevy world, read-only.
[`rig-steel`](https://github.com/0xPlaygrounds/rig/tree/main/plugins/rig-steel) adds code mode.

[`PLUGINS.md`](https://github.com/0xPlaygrounds/rig/blob/main/crates/rig-harness/PLUGINS.md) shows how to write a plugin: an index from task to
section, then one example per extension point.
[`keep_last_compaction`](https://github.com/0xPlaygrounds/rig/blob/main/corpus/ct/src/keep_last_compaction.rs) replaces
a default plugin, the compaction, on public API alone. A plugin's tests run a
small app frame by frame, and `stage_conflicts` checks that its stage systems
are ordered against those of the plugins it is used with
([testing a plugin](https://github.com/0xPlaygrounds/rig/blob/main/crates/rig-harness/PLUGINS.md#testing-a-plugin)).

## Install

```bash
git clone https://github.com/0xPlaygrounds/rig && cd rig
cargo install --path .
rig
```

It needs Rust 1.97.1 or newer and runs on Linux and macOS. The optional
`rig-inspect` plugin needs more to build on Linux; its README says what.

---

### Overview (`crates/rig-helixdb/README.md`)

# Rig HelixDB integration
This crate integrates HelixDB into Rig, allowing you to easily use RAG with this database.

## Installation
To install this crate, run the following command in a Rust project directory which will add `rig-helixdb` as a dependency (requires `rig-core` added for intended usage):
```bash
cargo add rig-helixdb
```

There's a few different ways you can run HelixDB:
- Through HelixDB's cloud offering
- Running it locally through their `helix start` command (requires the Helix CLI to be installed).
  - For local dev, you will likely want to use `helix push dev` for continuous iteration.

## How to run the example
Before running the example, you will need to ensure that you are running an instance of HelixDB which you can do with `helix dockerdev run`.

Once done, you will then need to deploy your queries/schema. **The queries/schema in the `examples/helixdb-cfg` folder are a required minimum to be use this integration.** `rig-helixdb` also additionally provides a way to get a manual handle on the client yourself so that you can invoke your own queries should you need to.

Assuming `rig-helixdb` is your current working directory, to deploy a minimum viable configuration for HelixDB (with `rig-helixdb`) you will need to `cd` into the `helixdb-cfg` folder and then run the following:
```bash
helix push dev
```

This will then deploy the queries/schema into your instance.

To run the example, add your OpenAI API key as an environment variable:
```bash
export OPENAI_API_KEY=my_key
```

Finally, use the following command below to run the example:
```bash
cargo run --example vector_search_helixdb --features rig/derive
```

---

### Overview (`crates/rig-http/README.md`)

# rig-http

The transport contracts every [Rig](https://github.com/0xPlaygrounds/rig)
provider sends through:

- `http_client`: `HttpClientExt`, the trait a transport implements; `DynHttpClient`, an erased
  client with middleware (`HttpMiddleware`); request framing, multipart forms and the error type;
- `ws_client` (feature `websocket`): the websocket connection contract the
  Responses WebSocket session is written against;
- `wasm_compat`: the `Send`/`Sync` bounds and boxed futures that relax on
  browser wasm;
- `runtime` (feature `tokio`, native): the tokio runtime the bundled
  transports poll their I/O in, from any executor.

This crate has no transport of its own. `rig-reqwest` (HTTP) and
`rig-tungstenite` (websocket) are the bundled transports, and depend only on
this crate. `rig-core` re-exports these modules at `rig_core::http_client`,
`rig_core::ws_client` and `rig_core::wasm_compat`, so provider code and
users reach them there.

A transport author depends on `rig-http` alone and implements
`HttpClientExt`:

```rust
use rig_http::http_client::{HttpClientExt, DynHttpClient};

fn erase(client: impl HttpClientExt + Clone + 'static) -> DynHttpClient {
    DynHttpClient::new(client)
}
```

The `test-utils` feature adds HTTP client doubles for tests.

---

### Overview (`crates/rig-lancedb/README.md`)

## Rig-Lancedb
This companion crate implements a Rig vector store based on Lancedb.

## Pre-requisites
If you are using `rig-lancedb` locally, you must ensure you have `protoc` (the [Protobuf Compiler](https://protobuf.dev/installation/)) installed.

## Usage

Add the companion crate to your `Cargo.toml`, along with the rig-core crate:

```toml
[dependencies]
rig-lancedb = "0.4.5"
rig-core = "0.36.0"
```

You can also run `cargo add rig-lancedb rig-core` to add the most recent versions of the dependencies to your project.

See the [`/examples`](./examples) folder for usage examples.

---

### Overview (`crates/rig-memory/README.md`)

# rig-memory

Saved conversations for [Rig](https://github.com/0xPlaygrounds/rig): recent messages,
compaction, and sessions saved as JSON-lines logs (`rig-session/4`) that can be resumed,
rewritten, and an agent's log forked up to a turn boundary. With default features it is pure data and builds for wasm32;
feature `fs` adds `DirStore` and `DirFiles`. `load_async` and `save_async` keep a conversation in
any `AsyncStore`, such as a database or IndexedDB, and every local store is one; rig-agent's
`AgentBuilder::memory` keeps an agent's conversation there. A store appends to a log only if the
log is still as long as the writer read it, checking and appending in one step, so of two saves
that read one log the second is refused. See the crate docs for the API.

---

### Overview (`crates/rig-milvus/README.md`)

# Rig Milvus integration
This crate integrates [Milvus](https://milvus.io/) into Rig, allowing you to easily use RAG with this database.

## Installation
To install this crate, run the following command in a Rust project directory which will add `rig-milvus` as a dependency (requires `rig-core` added for intended usage):
```bash
cargo add rig-milvus
```

There's a couple of different ways you can run Milvus:
- [Through a Docker container, either locally or on Docker-compatible architecture](https://milvus.io/docs/install_standalone-docker.md)
  - Their Docker container requires using an install script to use which is listed on the page and can be found directly on the Milvus repo.
  - They also support Kubernetes, although you will probably need to already know how to use Kubernetes to do this.
- [Using the Zilliz cloud offering](https://zilliz.com/cloud)
  - Using the cloud offering you can manage your Milvus instance through their web UI.

Before creating a collection that is compliant with `rig-milvus`, ensure you set up your Milvus related environment variables:
```bash
export MILVUS_DATABASE_NAME=
export MILVUS_COLLECTION_NAME=
export MILVUS_USERNAME=
export MILVUS_PASSWORD=
export MILVUS_BASE_URL=
```

To create a collection, you will need the bash script below.
```bash
export TOKEN="${MILVUS_USERNAME}:${MILVUS_PASSWORD}"

curl --request POST \
--url "${MILVUS_BASE_URL}/v2/vectordb/collections/create" \
--header "Authorization: Bearer ${TOKEN}" \
--header "Content-Type: application/json" \
-d '{
    "collectionName": "${MILVUS_COLLECTION_NAME}",
    "dbName": "${MILVUS_DATABASE_NAME}",
    "schema": {
        "autoId": true,
        "enabledDynamicField": false,
        "fields": [
            {
                "fieldName": "embedding",
                "dataType": "FloatVector",
                "elementTypeParams": {
                    "dim": "1536"
                }
            },
            {
                "fieldName": "document",
                "dataType": "JSON",
            },
            {
                "fieldName": "embeddedText",
                "dataType": "VarChar"
            }
        ]
    }
}'
```

This will create a collection with the following fields:
- an ID field (auto-generated as i64 but converted to String when using `top_n()`)
- an `embedding` field for storing vectors
- a `document` field (for storing metadata)
- an `embeddedText` field (for the actual item that was embedded)

Once done, you'll now be ready to use!

## How to run the example
To run the example, add your OpenAI API key as an environment variable. Don't forget your Milvus username, password, database & collection name and endpoint URL if you're executing this from a new terminal tab/window:
```bash
export OPENAI_API_KEY=my_key
export MILVUS_DATABASE_NAME=
export MILVUS_COLLECTION_NAME=
export MILVUS_USERNAME=
export MILVUS_PASSWORD=
export MILVUS_BASE_URL=
```

Finally, use the following command below to run the example:
```bash
cargo run --example vector_search_milvus --features rig/derive
```

---

### Overview (`crates/rig-mongodb/README.md`)

<div style="display: flex; align-items: center; justify-content: center;">
    <picture>
        <source media="(prefers-color-scheme: dark)" srcset="../img/rig_logo_dark.svg">
        <source media="(prefers-color-scheme: light)" srcset="../img/rig_logo.svg">
        <img src="../img/rig_logo.svg" width="200" alt="Rig logo">
    </picture>
    <span style="font-size: 48px; margin: 0 20px; font-weight: regular; font-family: Open Sans, sans-serif;"> + </span>
    <picture>
        <source media="(prefers-color-scheme: dark)" srcset="https://companieslogo.com/img/orig/MDB_BIG.D-96d632a9.png?t=1720244492">
        <source media="(prefers-color-scheme: light)" srcset="https://cdn.iconscout.com/icon/free/png-256/free-mongodb-logo-icon-download-in-svg-png-gif-file-formats--wordmark-programming-langugae-freebies-pack-logos-icons-1175140.png?f=webp&w=256">
        <img src="https://cdn.iconscout.com/icon/free/png-256/free-mongodb-logo-icon-download-in-svg-png-gif-file-formats--wordmark-programming-langugae-freebies-pack-logos-icons-1175140.png?f=webp&w=256" width="200" alt="MongoDB logo">
    </picture>
</div>

<br><br>

## Rig-MongoDB
This companion crate implements a Rig vector store based on MongoDB.

## Usage

Add the companion crate to your `Cargo.toml`, along with the rig-core crate:

```toml
[dependencies]
rig-mongodb = "0.4.5"
rig-core = "0.36.0"
```

You can also run `cargo add rig-mongodb rig-core` to add the most recent versions of the dependencies to your project.

See the [`/examples`](./examples) folder for usage examples.

---

### Overview (`crates/rig-neo4j/README.md`)

<div style="display: flex; align-items: center; justify-content: center;">
    <picture>
        <source media="(prefers-color-scheme: dark)" srcset="../img/rig_logo_dark.svg">
        <source media="(prefers-color-scheme: light)" srcset="../img/rig_logo.svg">
        <img src="../img/rig_logo.svg" width="200" alt="Rig logo">
    </picture>
    <span style="font-size: 48px; margin: 0 20px; font-weight: regular; font-family: Open Sans, sans-serif;"> + </span>
    <picture>
        <source media="(prefers-color-scheme: dark)" srcset="https://cdn.prod.website-files.com/653986a9412d138f23c5b8cb/65c3ee6c93dc929503742ff6_1_E5u7PfGGOQ32_H5dUVGerQ%402x.png">
        <source media="(prefers-color-scheme: light)" srcset="https://commons.wikimedia.org/wiki/File:Neo4j-logo_color.png">
        <img src="https://commons.wikimedia.org/wiki/File:Neo4j-logo_color.png" width="200" alt="Neo4j logo">
    </picture>

</div>

<br><br>

This companion crate implements a Rig vector store based on Neo4j Graph database. It uses the [neo4rs](https://github.com/neo4j-labs/neo4rs) crate to interact with Neo4j. Note that the neo4rs crate is a work in progress and does not yet support all Neo4j features. Further documentation on Neo4j & vector search integration can be found on the [neo4rs docs](https://neo4j.com/docs/cypher-manual/current/indexes/semantic-indexes/vector-indexes/).

## Prerequisites

The GenAI plugin is enabled by default in Neo4j Aura.

The plugin needs to be installed on self-managed instances. This is done by moving the neo4j-genai.jar file from /products to /plugins in the Neo4j home directory, or, if you are using Docker, by starting the Docker container with the extra parameter --env NEO4J_PLUGINS='["genai"]'. For more information, see Operations Manual → Configure plugins.


## Usage

Add the companion crate to your `Cargo.toml`, along with the rig-core crate:

```toml
[dependencies]
rig-neo4j = "0.5.5"
rig-core = "0.36.0"
```

You can also run `cargo add rig-neo4j rig-core` to add the most recent versions of the dependencies to your project.

See the [examples](./examples) folder for usage examples.

- [examples/vector_search_simple.rs](examples/vector_search_simple.rs) shows how to create an index on simple data.
- [examples/vector_search_movies_consume.rs](examples/vector_search_movies_consume.rs) shows how to query an existing index.
- [examples/vector_search_movies_create.rs](examples/vector_search_movies_create.rs) shows how to create embeddings & index on a large DB and query it in one go.

## Notes

- The `rig-neo4j::vector_index` module offers utility functions to create and query a Neo4j vector index. You can also create indexes using the Neo4j browser or directly call cypther queries with the Neo4rs crate. See the [Neo4j documentation](https://neo4j.com/docs/genai/tutorials/embeddings-vector-indexes/setup/vector-index/) for more information. Example [examples/vector_search_simple.rs](examples/vector_search_simple.rs) shows how to create an index on existing data.

```Cypher
CREATE VECTOR INDEX moviePlots
FOR (m:Movie)
ON m.embedding
OPTIONS {indexConfig: {
    `vector.dimensions`: 1536,
    `vector.similarity_function`: 'cosine'
}}
```

## Roadmap

- Add support for creating the vector index through RIG.
- Add support for adding embeddings to an existing database
- Add support for uploading documents to an existing database

---

### Overview (`crates/rig-postgres/README.md`)

<div style="display: flex; align-items: center; justify-content: center;">
    <picture>
        <source media="(prefers-color-scheme: dark)" srcset="../img/rig_logo_dark.svg">
        <source media="(prefers-color-scheme: light)" srcset="../img/rig_logo.svg">
        <img src="../img/rig_logo.svg" width="200" alt="Rig logo">
    </picture>
    <span style="font-size: 48px; margin: 0 20px; font-weight: regular; font-family: Open Sans, sans-serif;"> + </span>
    <picture>
        <source srcset="https://www.postgresql.org/media/img/about/press/elephant.png">
        <img src="https://www.postgresql.org/media/img/about/press/elephant.png" width="200" alt="Postgres logo">
    </picture>
</div>

<br><br>

## Rig-postgres

This companion crate implements a Rig vector store based on PostgreSQL.

## Usage

Add the companion crate to your `Cargo.toml`, along with the rig-core crate:

```toml
[dependencies]
rig-core = "0.36.0"
rig-postgres = "0.2.5"
```

You can also run `cargo add rig-core rig-postgres` to add the most recent versions of the dependencies to your project.

## PostgreSQL setup

The crate utilizes [pgvector](https://github.com/pgvector/pgvector) extension, which is available for PostgreSQL version 13 and later. Use any of the [official](https://www.postgresql.org/download/) or alternative methods to install psql.

You can install Postgres using Docker:

```sh
docker pull pgvector/pgvector:pg17

docker run -e POSTGRES_USER=myuser \
           -e POSTGRES_PASSWORD=mypassword \
           -e POSTGRES_DB=mydatabase \
           --name my_postgres \
           -p 5432:5432 \
           -d ankane/pgvector
```

Now you can configure Postgres, the recommended way is using sqlx and migrations (you can find an example inside integration tests folder).

Example sql:

```sql
-- ensure PgVector extension is installed
CREATE EXTENSION IF NOT EXISTS vector;

-- create table with embeddings using 1536 dimensions (based on OpenAI model text-embedding-3-small)
CREATE TABLE documents (
  id uuid DEFAULT gen_random_uuid(), -- we can have repeated entries
  document jsonb NOT NULL,
  embedded_text text NOT NULL,
  embedding vector(1536)
);

-- create index on embeddings
CREATE INDEX IF NOT EXISTS document_embeddings_idx ON documents
USING hnsw(embedding vector_cosine_ops); -- recommended for text embeddings

```

You can change the table name and the number of dimensions but keep the same fields schema.

You can use different indexes depending the type of distance method you want to use, check [PgVector documentation](https://github.com/pgvector/pgvector?tab=readme-ov-file#querying).

## Usage

Declare the database URL:

```sh
export DATABASE_URL="postgres://myuser:mypassword@localhost:5432/mydatabase"
```

Define the document you want to index, it has to implement `Embed`, `Serialize` and `Deserialize`.

> Note: you can index different type of documents in the same table.

Example:

```rust
#[derive(Embed, Clone, Serialize, Deserialize, Debug)]
pub struct Product {
    name: String,
    category: String,
    #[embed]
    description: String,
    price: f32
}
```

Example usage

```rust
    // OpenAI's embedding model, erased once: the builder and the store each
    // take a clone of the handle.
    let model = rig::providers::openai::OpenAI::from_env()?
            .embedding(rig::providers::openai::TEXT_EMBEDDING_3_SMALL, None)
        .erase();

    // connect to Postgres
    let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL not set");
    let pool = PgPoolOptions::new() .connect(&database_url) .await?;

    // run migrations (optional but recommended)
    sqlx::migrate!("./migrations").run(&pool).await?;

    // init documents
    let products: Vec<Product> = ...;

    let documents = EmbeddingsBuilder::new(model.clone())
        .documents(products)
        .unwrap()
        .build()
        .await?;

    // Create your index
    let vector_store = PostgresVectorStore::with_defaults(model, pool);

    // store documents
    vector_store.insert_documents(documents).await?;

    // retrieve embeddings
    let req = VectorSearchRequest::builder()
        .query("Which phones have more than 16Gb and support 5G")
        .samples(50)
        .build();
    let results = vector_store.top_n::<Product>(req).await?;

    ...

```

---

### Overview (`crates/rig-qdrant/README.md`)

# Rig-Qdrant
Vector store index integration for [Qdrant](https://qdrant.tech/). This integration supports dense vector retrieval using Rig's embedding providers. It is also extensible to allow all [hybrid queries](https://qdrant.tech/documentation/concepts/hybrid-queries/) supported by Qdrant.

## Installation

```toml
[dependencies]
rig-qdrant = "0.2.5"
rig-core = "0.36.0"
```

The root `rig` facade also exposes this crate behind the `qdrant` feature.

## Examples

See [`examples/qdrant_vector_search.rs`](./examples/qdrant_vector_search.rs)
for an end-to-end example using a Qdrant collection with a Rig embedding model.

Filtered searches use the crate-level `QdrantFilter` type:

```rust
use rig_core::vector_store::request::{SearchFilter, VectorSearchRequest};
use rig_qdrant::QdrantFilter;

let req = VectorSearchRequest::<QdrantFilter>::builder()
    .query("What is a linglingdong?")
    .samples(1)
    .filter(QdrantFilter::eq(
        "id",
        serde_json::json!("f9e17d59-32e5-440c-be02-b2759a654824"),
    ))
    .build();
```

---

### Overview (`crates/rig-reqwest/README.md`)

# rig-reqwest

The bundled [`reqwest`](https://docs.rs/reqwest) HTTP transport for [Rig](https://crates.io/crates/rig): `ReqwestClient` and `ReqwestMiddlewareClient` implement `HttpClientExt`. It depends only on `rig-http`, which holds that contract. `ReqwestClient::default()` is the process-wide client, built once on first use and shared by every clone; `ReqwestClient::from(client)` wraps a host-built `reqwest::Client`. With rig-core's `reqwest` feature (on in the `rig` facade), a provider client built with no transport named, such as `OpenAI::from_env()?`, sends through `ReqwestClient::default()`; any other client goes in with the configuration's `.connect(client)`. Construction never fails: when the reqwest client cannot be built, every send reports the build failure in-band.

`rig-core` without its `reqwest` feature has no reqwest/Tokio dependency; this crate is where both live. On native targets, requests and lazy response bodies capture the caller's Tokio context or, outside Tokio (Bevy task pools, smol, `futures::executor`), a lazily started fallback reactor. Each poll enters that context; no detached per-request or stream-forwarding worker owns the operation. Dropping a request, response body or stream releases its local operation without waiting for another upstream chunk. It cannot undo a remotely accepted request.

Non-success responses preserve their status and headers and retain at most 64 KiB of body bytes for diagnostics. An observed byte beyond that limit adds `[Response body truncated after 64 KiB]`; an exact-limit body is not marked. Read failures retain any partial body, with reqwest's charset and replacement-character decoding on native targets and UTF-8 decoding in browsers. The configured client timeout still governs stalled error-body reads; no separate timeout is imposed.

A host runtime must enable I/O and timers and remain driven. Context detection does not prove this; a runtime without its reactor can panic during I/O. A handle alone does not keep a runtime alive. Stop admitting work, cancel/drain owned operations, release shared clients, then shut down the runtime. Supplied endpoint, proxy, redirect, TLS and timeout policy remains with the host-built client. WASM uses its native browser transport path, not the native Tokio fallback.

---

### Overview (`crates/rig-rmcp/README.md`)

# rig-rmcp

MCP (Model Context Protocol) client support for [Rig](https://crates.io/crates/rig), built on the [`rmcp`](https://docs.rs/rmcp) SDK. Depends on **rig-core only**.

- `McpConnection` owns a connected rmcp client (`McpConnection::new(client_config.serve(transport).await?)`). Clones share it, and it closes when the last clone, or every tool holding one, is dropped, or on `close()`.
- `McpTool` is one server tool, from `McpConnection::tools` or `McpTool::new(definition, &connection)`. It keeps its connection open, so an agent that holds its tools keeps the server connected. Convert it into a `rig_core::tool::DynamicTool` (`TryFrom`, failing on an empty tool name) and register it wherever dynamic tools go. The dynamic tool carries a liveness probe bound to the transport, so registries retire it on disconnect.
- `McpTool::with_name` offers the tool to the model under another name, while calls still send the server the tool's MCP name: namespace one server's tools, or tell apart two servers' tools of the same name.
- Per call, an `McpMeta` in the runtime's `rig_core::tool::ToolContext` is forwarded as the request's `_meta`, and `McpTool::with_call_meta` adds entries derived from the call itself (the MCP name and the exact arguments sent), for signatures or idempotency keys. The raw `CallToolResult` (with the response's `structuredContent` and `_meta`) is published to the context's result map for hooks (`McpCallToolResult`). A tool that declares an output schema answers the model with its structured content alone.
- `McpConnection::resources` and `read_resource` list and read the server's resources. `resource_context(uris)` is a retrieval handler that reads them again for every model call: pass it to rig-agent's `AgentBuilder::dynamic_context_handler` to give an agent live resources as context.
- `McpClientHandler<S: rig_core::tool::ManagedToolSink>` is an rmcp client handler that keeps a tool registry in sync with `notifications/tools/list_changed`. rig-agent's `ToolServerHandle` implements `ManagedToolSink`, so `McpClientHandler::new(client_config, tool_server_handle.clone()).connect(transport)` is the agent usage. The registry owns those tools, so hold the `McpConnection` that `connect` returns for as long as they should work.

The `rig` facade re-exports this crate as `rig::tool::rmcp` behind its `rmcp` feature. The `rmcp_example` package shows a local server's tools and resources and a hosted server over streamable HTTP.

Native-only: rmcp's `ClientHandler` requires `Send + Sync` unconditionally, which rig's wasm tool registry cannot satisfy; building for `wasm32` fails with a single explanatory `compile_error!`.

---

### Overview (`crates/rig-s3vectors/README.md`)

# Rig AWS S3Vectors integration
This crate integrates AWS S3Vectors into Rig, allowing you to easily use RAG with AWS' newest offering.

## Installation

```toml
[dependencies]
rig-s3vectors = "0.2.5"
rig-core = "0.36.0"
```

You can also run `cargo add rig-s3vectors rig-core` to add the latest published
versions. The root `rig` facade exposes this crate behind the `s3vectors` feature.

This integration will require that you have an AWS account, although the region does not matter.

See [`examples/s3vectors_vector_search.rs`](./examples/s3vectors_vector_search.rs)
for a vector search example.

---

### Overview (`crates/rig-scylladb/README.md`)

# Rig-ScyllaDB

Vector store implementation for [ScyllaDB](https://www.scylladb.com/). This integration provides vector storage and similarity search using ScyllaDB as the backend.

## Usage

```rust
use rig::{
    Embed,
    providers::openai,
    vector_store::{VectorStoreIndex, request::VectorSearchRequest},
};
use rig_scylladb::{ScyllaDbVectorStore, create_session};

#[derive(Embed, serde::Deserialize, serde::Serialize, Debug)]
struct Document {
    id: String,
    #[embed]
    text: String,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create ScyllaDB session
    let session = create_session("127.0.0.1:9042").await?;
    
    // OpenAI's embedding model
    let model = openai::OpenAI::from_env()?.embedding(openai::TEXT_EMBEDDING_ADA_002, None);
    
    // Create vector store
    let vector_store = ScyllaDbVectorStore::new(
        model,
        session,
        "vector_db",    // keyspace
        "documents",    // table
        1536,          // embedding dimensions
    ).await?;
    
    // Query the store
    let req = VectorSearchRequest::builder()
        .query("search query")
        .samples(5)
        .build();
    let results = vector_store.top_n::<Document>(req).await?;
    
    for result in results {
        println!(
            "Score: {}, ID: {}, Document: {:?}",
            result.score, result.id, result.document
        );
    }
    
    Ok(())
}
```

See the [`/examples`](./examples) folder for usage examples.

## Notes

- Uses application-level cosine similarity search (similar to SQLite and SurrealDB implementations)
- Suitable for small to medium datasets (< 100k vectors)
- Provides ScyllaDB's operational benefits: high availability, horizontal scaling, low latency
- Future-ready for ScyllaDB's native vector search capabilities

---

### Overview (`crates/rig-sqlite/README.md`)

<div style="display: flex; align-items: center; justify-content: center;">
    <picture>
        <source media="(prefers-color-scheme: dark)" srcset="../img/rig_logo_dark.svg">
        <source media="(prefers-color-scheme: light)" srcset="../img/rig_logo.svg">
        <img src="../img/rig_logo.svg" width="200" alt="Rig logo">
    </picture>
    <span style="font-size: 48px; margin: 0 20px; font-weight: regular; font-family: Open Sans, sans-serif;"> + </span>
    <picture>
        <source media="(prefers-color-scheme: dark)" srcset="https://www.sqlite.org/images/sqlite370_banner.gif">
        <source media="(prefers-color-scheme: light)" srcset="https://www.sqlite.org/images/sqlite370_banner.gif">
        <img src="https://www.sqlite.org/images/sqlite370_banner.gif" width="200" alt="SQLite logo">
    </picture>
</div>

<br><br>

## Rig-SQLite

This companion crate implements a Rig vector store based on SQLite.

## Usage

Add the companion crate to your `Cargo.toml`, along with the rig-core crate:

```toml
[dependencies]
rig-sqlite = "0.2.6"
rig-core = "0.37.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
```

You can also run `cargo add rig-sqlite rig-core serde_json` and
`cargo add serde --features derive` to add the most recent versions of the
dependencies to your project.

See the [`/examples`](./examples) folder for usage examples.

## Important Note

Before using the SQLite vector store, you must [initialize the SQLite vector extension](https://alexgarcia.xyz/sqlite-vec/rust.html). Add this code before creating your connection:

```rust
use rusqlite::ffi::sqlite3_auto_extension;
use sqlite_vec::sqlite3_vec_init;

unsafe {
    sqlite3_auto_extension(Some(std::mem::transmute(sqlite3_vec_init as *const ())));
}
```

## Storing JSON Metadata

Declare JSON metadata columns with `Column::new("metadata", "JSON")` and store
the value as `serde_json::Value`. Rig writes the value as JSON text and parses
it back as structured JSON when documents are returned from vector searches.

```rust
use rig_core::Embed;
use rig_sqlite::{Column, ColumnValue, SqliteVectorStoreTable};
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Deserialize, Embed, Serialize)]
struct Document {
    id: String,
    #[embed]
    text: String,
    metadata: serde_json::Value,
}

impl SqliteVectorStoreTable for Document {
    fn name() -> &'static str {
        "documents"
    }

    fn schema() -> Vec<Column> {
        vec![
            Column::new("id", "TEXT PRIMARY KEY"),
            Column::new("text", "TEXT"),
            Column::new("metadata", "JSON"),
        ]
    }

    fn id(&self) -> String {
        self.id.clone()
    }

    fn column_values(&self) -> Vec<(&'static str, Box<dyn ColumnValue>)> {
        vec![
            ("id", Box::new(self.id.clone())),
            ("text", Box::new(self.text.clone())),
            ("metadata", Box::new(self.metadata.clone())),
        ]
    }
}
```

## Filtering JSON Metadata

SQLite filters can target document-table columns that store JSON text. Use
SQLite's JSON extraction operators in the filter key:

```rust
use rig_core::vector_store::request::{SearchFilter, VectorSearchRequest};
use rig_sqlite::SqliteSearchFilter;

let req = VectorSearchRequest::builder()
    .query("release notes")
    .samples(5)
    .filter(SqliteSearchFilter::eq(
        "metadata->>'$.source'",
        serde_json::json!("docs"),
    ))
    .build();
```

Use `->>` when you want SQLite to compare a JSON value as a SQL scalar, such as
text, number, or boolean. Use `->` when you want to compare against JSON text;
Rig serializes the right-hand side as JSON for that form.

JSON metadata filters are applied after sqlite-vec candidate search because
they reference the document table, not sqlite-vec metadata columns. This keeps
results correct, including with `samples(1)`, but requires exhaustive candidate
retrieval. For frequently-used scalar filters, prefer storing the value in a
regular column marked with `Column::indexed()` so sqlite-vec can apply it during
candidate search.

---

### Overview (`crates/rig-surrealdb/README.md`)

# Rig SurrealDB integration
This crate integrates SurrealDB into Rig, allowing you to easily use RAG with this database.

## Installation

```toml
[dependencies]
rig-surrealdb = "0.2.5"
rig-core = "0.36.0"
```

You can also run `cargo add rig-surrealdb rig-core` to add the latest published
versions. The root `rig` facade exposes this crate behind the `surrealdb` feature.

There's a few different ways you can run SurrealDB:
- [Install it locally and run it](https://surrealdb.com/docs/surrealdb/installation/linux)
- [Through a Docker container, either locally or on Docker-compatible architecture](https://surrealdb.com/docs/surrealdb/installation/running/docker)
  - `docker run --rm --pull always -p 8000:8000 surrealdb/surrealdb:latest start --username root --password root` starts up a SurrealDB instance at port 8000 with the username and password as "root".
- [Using SurrealDB's cloud offering](https://surrealdb.com/cloud)
  - Using the cloud offering you can manage your SurrealDB instance through their web UI.

## How to run the example
To run the example, add your OpenAI API key as an environment variable:
```bash
export OPENAI_API_KEY=my_key
```

Finally, use the following command below to run the example:
```bash
cargo run --example vector_search_surreal --features rig/derive
```

---

### Overview (`crates/rig-tools/README.md`)

# rig-tools

Coding tools for [Rig](https://github.com/0xPlaygrounds/rig) agents:

| Tool     | What it does                                                        |
|----------|---------------------------------------------------------------------|
| `read`   | A text file's lines, with an offset and limit                       |
| `write`  | A whole file, written atomically                                    |
| `edit`   | Several exact (or whitespace- and quote-tolerant) replacements, applied in order, all or nothing, answered with the changed lines' numbers and those lines, with a few around each, as they now are |
| `search` | A regex over the tree as git ignores it, hidden files too when asked or when the glob names one, or the names of the files with a match (every file for an empty pattern); `find` returns the hits as data, `render` shows them |
| `list`   | A directory's files as git ignores them, down to a depth, optionally matching a glob, hidden files as for `search` |
| `shell`  | `sh -c` in its own process group, as a `Launcher` starts it, run until it ends unless the call sets a timeout, stdout and stderr interleaved line by line without terminal escape codes such as colours, and an optional `OutputLimit` that keeps both ends of a long output and the first and last errors from between |
| `delete`, `move` | A file removed, or moved or renamed |

Tools publish facts beside their text (such as `FilesChanged`), and every tool acts on a
`Workspace`: this machine (`Local`) or another. The pieces the tools are built from, such as
checkpoints, the overlay workspace and process groups, are listed in the crate docs.

`remote` reaches other machines through a transport: a command that runs its last argument,
a shell line, there with the remote user's shell, keeps stdout and stderr apart and ends with
255 when it failed itself, as `ssh HOST` and `koh exec HOST --sh` do. `Remote` is such a
machine's workspace, and `Hosts` wraps one so that a path written `HOST:PATH`, as `scp`
reads it, is that host's and every other path stays the inner workspace's:

```rust
use std::sync::Arc;
use rig_tools::process::Launcher;
use rig_tools::remote::{Hosts, Remote};
use rig_tools::workspace::Local;
use rig_tools::Read;

let koh = ["koh", "exec", "bench", "--sh"];
let ssh = ["ssh", "-o", "BatchMode=yes", "dev@box"];
let hosts = Hosts::new(
    Arc::new(Local),
    [
        Remote::new("bench", koh, Launcher::default())?,
        Remote::new("box", ssh, Launcher::default())?,
    ],
);
// `read` of `bench:~/strata/src/lib.rs` reads that file on bench.
let read = Read { workspace: Arc::new(hosts), ..Read::default() };
```

Each file operation there is one short `sh` script through the transport, at most two at
once per host; a read whose transport failed (exit 255) or was killed is sent once more, a write never: when a write's transport ends some other way, such as a
link dropped mid-write, the file is hashed there again and the error says whether the write
happened, and `edit` forgets the text it kept of that file. A `read` of some lines of a
file over 50 KB is sent only those lines; a smaller file comes whole. An
`edit` of a file read (any part of it, when it is that small), or edited before, is one call that writes only if the file still
holds the text it was made from. A `shell` command there runs under a short `sh` script that
stops the command's process group when the transport's stdin closes. This side holds that
stdin open until the call ends, so a timeout, an interrupt, rig quitting or a transport killed
on this machine stops the command there too, with any transport that forwards stdin and
closes it when it goes (`ssh` does). Without `setsid` there, only the command's own process
is stopped.

Every tool then takes such paths: `read`, `edit` and `write` a file there, `search` with
`path: "bench:~/strata"`, `shell` with `cwd: "bench:~/strata"`, and the paths they print and
publish keep the `HOST:` form.

The tools run on the file system and processes, so the crate is native only.
The [`rig-harness`](../rig-harness) coding agent registers them.

---

### Overview (`crates/rig-tungstenite/README.md`)

# rig-tungstenite

The bundled [`tokio-tungstenite`](https://docs.rs/tokio-tungstenite) websocket backend for [Rig](https://crates.io/crates/rig): `TungsteniteClient`, a `WebSocketClientExt` implementation. It depends only on `rig-http`, which holds the websocket contract.

`rig-core` owns the websocket *protocol*: the OpenAI Responses session, its event envelopes and its turn state machine all live in `rig_core::providers::openai::responses_api::websocket`, written against the transport-agnostic `rig_http::ws_client` contract (re-exported at `rig_core::ws_client`). This crate owns only the socket, exactly as `rig-reqwest` owns only the HTTP transport. With rig-core's `tungstenite` feature, a session opens over this backend with no backend named:

```rust,ignore
let session = model.responses_websocket().connect().await?;
```

Without it, pass any backend: `model.responses_websocket().connect_with(&TungsteniteClient::new())`.

It also works when the caller has no tokio runtime (Bevy task pools, smol, `futures::executor`): the caller still owns and polls the socket, and each operation enters the context of `rig_http::runtime`, the lazily started fallback runtime `rig-reqwest` uses too. A cancelled receive leaves unread frames in the socket, and dropping the connection closes it. Reading on demand is the backpressure for slow consumers.

---

### Overview (`crates/rig-typesafeai/README.md`)

# rig-typesafeai

Experimental TypeSafe Jev judgments for Rig. Enable the root `rig` feature
`typesafeai` to use `rig::typesafeai`, or depend on this crate and pair it with a
Rig HTTP transport explicitly. It uses Rig's `Operation`, `Wire`, `Model`, and
shared driver.
There are no Jev derive macros or schema-generation traits.

## One structure for questions and answers

Implement `Query` on a generic named struct. Its associated `Output` replaces
question fields with their answer types while preserving their names and layout:

```rust
use rig_core::error::ProviderError;
use rig_typesafeai::{Noul, NoulAnswer, Query};
use serde::{Serialize, Deserialize};

// The fields are declared once, for both questions and answers.
#[derive(Serialize, Deserialize)]
struct Assessment<R, V> {
    ready: R,
    needs_review: V,
}

impl<R: Query, V: Query> Query for Assessment<R, V> {
    type Response = Assessment<R::Response, V::Response>;
    type Output = Assessment<R::Output, V::Output>;

    fn decode(&self, response: Self::Response) -> Result<Self::Output, ProviderError> {
        Ok(Assessment {
            ready: self.ready.decode(response.ready)?,
            needs_review: self.needs_review.decode(response.needs_review)?,
        })
    }
}

let query = Assessment {
    ready: Noul::new("Is this ready to ship?")?,
    needs_review: Noul::new("Does this need human review?")?,
};
# Ok::<(), ProviderError>(())
```

With a Jev model (`Jev::from_env()?.evaluation()`), evaluate it directly:

```rust,ignore
let answers: Assessment<NoulAnswer, NoulAnswer> =
    jev.evaluate(&state, &query).await?.answers;
```

The query is reusable by reference. `.await?` handles construction-independent
transport and response errors once; the result contains ordinary typed fields.
Serde field names and `rename` attributes supply question IDs. The request is
serialized directly, and Serde reconstructs the matching response struct before
`decode` validates each answer against its question. No manual map construction
or lookup is needed. Use `#[serde(flatten)]` to compose named groups.
See [triage](../../examples/typesafeai_triage/src/main.rs) for a complete example
using Choice, Score, and Noul in the same generic struct. Its query and answer
forms are concrete instantiations of one type, with no type erasure.

## Question builders

- `Choice::new(instructions, [(variant, description), ...])` returns
  `ChoiceAnswer<T>` on evaluation. Values implement `Serialize`,
  `DeserializeOwned`, and `Ord`; their Serde representation must be a string.
- `Score::new(instructions, [(level, description), ...])` returns
  `ScoreAnswer<L>` with enum-keyed probabilities. **Array order** determines
  numeric positions, independently of enum discriminants and `Ord`.
  Evaluation requires `L: Ord + Clone` and the WASM-compatible transport bounds.
- `DynamicScore::new(instructions, descriptions)` returns `DynamicScoreAnswer`
  with numeric indices and a legend when rubric identities are runtime-defined.
- `Noul::new(instructions)` returns `NoulAnswer`, the probability of yes.
  `.criteria(yes, no)?` accepts structured descriptions for both outcomes.

Fixed arrays check choice counts (2–255) and score counts (2–10) at compile time.
`try_from_iter` constructors check runtime collections and reject duplicate
labels or levels. Builders accept serializable descriptions, including structs,
JSON objects, arrays, strings, and null. State accepts strings, objects, or arrays.

For runtime IDs, maps of questions implement `Query`, and `.named("ready")?`
gives a standalone question an ID. `.join(other)` flattens two query objects;
`.map(...)` projects validated answers. Empty or duplicate IDs and missing or
unexpected response IDs are rejected at the evaluation boundary.
For a fully runtime-defined question set, use `DynamicQuery::new(definitions)?`.
Its output is explicitly `BTreeMap<String, types::Answer>`; construction validates
the definitions and evaluation validates the provider's answers.

`Jev::from_env()` reads `JEV_TOKEN` without loading a secrets file. Its default
model is `jev-latest`; `JevConfig::model(...)` selects another provider model
identifier. Configure timeouts and retry middleware on the Rig transport and
pass it with `JevConfig::connect`.

Questions share state but cannot consume each other's answers in one request.
Confidence measures concentration, not correctness. Keep distributions and put
threshold policy in application code. Validation preserves the provider's values,
including bounded hundredth-rounding error; it does not silently renormalize them.

## Examples and verification

Run the interactive console from the repository root with `JEV_TOKEN` already
exported:

```sh
cargo run -p typesafeai_chat
```

Add `-- --agent` for `gpt-5.6-sol` replies using `OPENAI_API_KEY`. The default
mode uses Jev alone and displays decisions plus deterministic next actions.
Both modes support `/reset`, `/quit`, and conversational history.

- [Interactive chat](../../examples/typesafeai_chat): named generic query structs and
  batched judgments over conversation state.
- [Mixed triage](../../examples/typesafeai_triage): all three primitives in one call.

Run `cargo test -p rig-typesafeai` to replay the synthetic, live-recorded
[plain](fixtures/triage.json), [structured](fixtures/structured.json), and
[rounded matrix](fixtures/rounded.json) fixtures
offline, check local validation invariants, and run compile-fail doctests.
The fixtures contain no authentication headers. They were recorded against
`jev-1.13.0`; replay checks request JSON and decoded answers, not future model
accuracy or calibration. Mutation tests exercise invalid responses separately.

Protocol sources: [API](https://docs.typesafe.ai/api),
[Choice](https://docs.typesafe.ai/primitives/choice),
[Score](https://docs.typesafe.ai/primitives/score),
[Noul](https://docs.typesafe.ai/primitives/noul).

---

### Overview (`crates/rig-vectorize/README.md`)

# rig-vectorize

Vector store integration for [Cloudflare Vectorize](https://developers.cloudflare.com/vectorize/). This integration supports vector similarity search and document insertion using Rig's embedding providers.

## Installation

```toml
[dependencies]
rig-vectorize = "0.2.5"
rig-core = "0.36.0"
```

The root `rig` facade also exposes this crate behind the `vectorize` feature.

You can find end-to-end examples [here](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-vectorize/examples).

For Vectorize-specific questions, ask in the [Cloudflare Developers Discord](https://discord.com/channels/595317990191398933/1152193114522525726).

## Running Integration Tests

Integration tests require a real Cloudflare Vectorize index.

### 1. Create a Vectorize index

```bash
npx wrangler vectorize create rig-integration-test --dimensions=1536 --metric=cosine
```

### 2. Set environment variables and run tests

```bash
export CLOUDFLARE_ACCOUNT_ID="your-account-id"
export CLOUDFLARE_API_TOKEN="your-api-token"
export VECTORIZE_INDEX_NAME="rig-integration-test"
cargo test --package rig-vectorize --test integration_tests -- --test-threads=1
```

Tests run sequentially (`--test-threads=1`) to avoid conflicts since they clear the index before each test.

**Note:** Vectorize has eventual consistency. Tests wait 5 seconds after inserting documents before querying (configured via `EVENTUAL_CONSISTENCY_DELAY` constant).

### 3. (Optional) Enable filter tests

Filter tests require metadata indexes. Without them, filter tests will be skipped:

```bash
npx wrangler vectorize create-metadata-index rig-integration-test --property-name=category --type=string
npx wrangler vectorize create-metadata-index rig-integration-test --property-name=id --type=string
```

---

### Overview (`crates/rig-vertexai/README.md`)

## Rig-VertexAI

This companion crate integrates Google Cloud Vertex AI as a model provider with Rig: Gemini through `GenerateContent`, and Anthropic's Claude through the `anthropic` publisher's `rawPredict` endpoint.

## Usage

Add the companion crate to your `Cargo.toml`, along with the rig-core crate:

```toml
[dependencies]
rig-vertexai = "0.42.0"
rig-core = "0.42.0"
```

You can also run `cargo add rig-vertexai rig-core` to add the most recent versions of the dependencies to your project.

See the [`/examples`](./examples) folder for usage examples.
## Raw responses

A response's `raw` is the reply's REST JSON: the SDK's `GenerateContentResponse`
serialized, with enum values spelled by name. It can be recovered from a
normalized response without another RPC:

```rust
use google_cloud_aiplatform_v1::model::GenerateContentResponse;
use rig_core::{completion::CompletionResponse, serde_json};

fn recover(response: CompletionResponse) -> Result<GenerateContentResponse, serde_json::Error> {
    serde_json::from_value(response.raw)
}
```

## Claude

`VertexAi::anthropic(model)` sends Anthropic's Messages API to Vertex AI's
`rawPredict` endpoint (`streamRawPredict` when streamed) with the client's
Google credentials. Requests, replies, Anthropic typed options and history
rules are Anthropic's; the model is named in the URL and the body carries
`anthropic_version: "vertex-2023-10-16"`. Use Vertex AI's model ids, such as
`claude-sonnet-4-6` or `claude-haiku-4-5@20251001`. `global` and the `us` and
`eu` multi-regions have their own hosts; any other location is a region.

```rust,no_run
use rig_core::completion::CompletionRequest;
use rig_vertexai::VertexAi;

# async fn example() -> anyhow::Result<()> {
let claude = VertexAi::from_env()?.anthropic("claude-sonnet-4-6");
let reply = claude.call(CompletionRequest::new("Hello, Claude").max_tokens(256)).await?;
# let _ = reply;
# Ok(())
# }
```

Claude requests travel over HTTP, through the shared reqwest client unless
`VertexAiBuilder::with_http_client` supplies another. They need the
credentials the client resolves, so a client built on a supplied prediction
service cannot send them.

## Setup

Make sure to have Google Cloud credentials configured. You can use Application Default Credentials (ADC) by running:

```shell
gcloud auth application-default login
```

## Supplying your own SDK client

`VertexAi::from_env()` (and `VertexAi::builder()` without either explicit credentials
or a supplied service) resolves Application Default Credentials. That resolution builds
`google-cloud-auth`'s token cache, which **spawns a refresh task on the current
Tokio runtime while it is being constructed**: call it inside a runtime
context, and keep that runtime alive and driven for as long as the client is
used. The refresh task belongs to the runtime, not to any one completion.

A host that would rather own the connection and its credential lifetime can
build the Google SDK client itself and hand it over. Rig then takes the
endpoint, credentials, transport, retry policy and universe domain as given,
and never rebuilds the client or reads ADC:

```rust,no_run
use google_cloud_aiplatform_v1::client::PredictionService;

# async fn example() -> anyhow::Result<()> {
let service = PredictionService::builder()
    .with_endpoint("https://us-central1-aiplatform.googleapis.com")
    .build()
    .await?;

let client = rig_vertexai::VertexAi::builder()
    .with_project("my-project")
    .with_location("us-central1")
    .with_prediction_service(service)
    .build()?;
# let _ = client;
# Ok(())
# }
```

`project` and `location` are still configured on the Rig client: they name the
model resource in the request, not the connection. Supplying both a
prediction service and `with_credentials(...)` is rejected at build time —
the supplied client's credentials are already fixed.

---

### Overview (`examples/agent_no_tokio/README.md`)

# agent_no_tokio

Runs a rig agent on Bevy's `AsyncComputeTaskPool` with no tokio dependency in
this crate's manifest.

`AgentRunner::run_channel` splits a run into a runtime-agnostic future and a bounded
`RunEvents` feed. The future is spawned on the pool; the main thread acts as a
frame loop, draining events with the non-blocking `RunEvents::try_next` and
checking the task with `bevy_tasks::futures::check_ready` — the shape a Bevy
system or any other tick-driven host would use.

```sh
OPENAI_API_KEY=… cargo run -p agent_no_tokio
```

Tokio appears only transitively, inside `rig-reqwest`, which drives the HTTP
wire on a private runtime:

```sh
cargo tree -p agent_no_tokio -e normal -i tokio
```

---

### Overview (`examples/candle_local/README.md`)

# Local rig-candle example

This workspace example downloads three pinned Hugging Face files and passes their owned
bytes through the root `rig` facade to `rig-candle`. All filesystem and network
access stays in the example application; `rig-candle` only receives `ModelData`.

From the repository root:

```bash
./examples/candle_local/download_model.sh
cargo run -p candle_local -- "Explain ownership in one sentence."
```

The default model is SmolLM2-360M-Instruct in Q4_K_M GGUF form. Its
270,590,880-byte checkpoint produces useful instruction responses while staying
small enough for the browser example.
The example prints generated fragments as they arrive, then reports final usage,
finish reason, context limits, prefill time, time to first token, total duration,
and throughput.

Set `MODEL_DIR` for both commands to store and load the three files elsewhere.
The downloader verifies immutable revisions and SHA-256 checksums and preserves
only already complete files.

---

### Overview (`examples/candle_pose/README.md`)

# Local rig-candle pose example

This workspace example estimates human poses with a YOLOv8 pose checkpoint
through the root `rig` facade. It reads the checkpoint and decodes the images
itself; `rig-candle` only receives the checkpoint bytes and RGB frames.

From the repository root:

```bash
./examples/candle_pose/download_model.sh
cargo run --release -p candle_pose
cargo run --release -p candle_pose -- frame-001.jpg frame-002.jpg frame-003.jpg
```

The downloader fetches Candle's safetensors copy of `yolov8n-pose` and a photo
of a cycling race, both pinned to immutable revisions and checked by SHA-256.
Every image named on the command line is one frame of a video; each frame's
people are printed as soon as it is estimated.

The YOLOv8 pose weights are Ultralytics' and are licensed under AGPL-3.0.
Check that license before you ship them.

Set `MODEL_DIR` for both commands to store and load the files elsewhere.

---

### Overview (`examples/candle_wasm_chat/README.md`)

# Local rig-candle WASM chat

This example compiles SmolLM2-360M-Instruct Q4_K_M into the WebAssembly module and
runs Rig and Candle entirely inside a browser Web Worker. Chat messages never
leave the page.

From the repository root:

```bash
./examples/candle_wasm_chat/build.sh
./examples/candle_wasm_chat/serve.sh
```

Then open <http://127.0.0.1:8080>. Set `PORT` when running `serve.sh` to use a
different port.

`build.sh` first runs `download_model.sh`, which downloads `config.json`,
`tokenizer.json`, and `model.gguf` into the ignored `model/` directory.
The Rust build script copies those bytes into Cargo's build output and
`include_bytes!` embeds them in the final WASM module. `wasm-pack` writes the
browser package to the ignored `www/pkg/` directory. Direct builds verify the
pinned size and SHA-256 of every artifact before copying hundreds of megabytes;
a partial or modified model directory fails early with a recovery instruction.

Set `MODEL_DIR` to choose the artifact directory for both downloading and
embedding. The downloader intentionally pins immutable model revisions and
checksums; it does not accept model overrides.

The model configuration and tokenizer are pinned to the official
[`HuggingFaceTB/SmolLM2-360M-Instruct`](https://huggingface.co/HuggingFaceTB/SmolLM2-360M-Instruct)
repository. The 270,590,880-byte checkpoint is Bartowski's Q4_K_M GGUF. Q4_K_M
is used because Candle supports its GGUF tensor kernels on native and WASM and it
is substantially smaller than full-precision weights while retaining useful
instruction output. The three embedded inputs total 272,696,282 bytes.

The Web Worker prevents synchronous WASM inference from freezing the page. The
Rust side retains a rolling window of recent Rig chat history until the
**Clear** button is pressed. User messages are limited to 1,024 UTF-8 bytes and
the retained window is bounded by both message count and serialized size, so a
long-running demo does not inevitably become stuck beyond the model's 4,096-token
context. Reinitialization is idempotent, avoiding a transient second model
allocation in wasm32's constrained linear memory.
Generation is currently returned after completion because synchronous Candle
inference cannot yield incremental fragments to JavaScript on the same worker.
After the page, worker, and WASM package load, inference makes no network
requests.

The worker controller has a model-independent Node regression test:

```bash
node --test examples/candle_wasm_chat/www/worker-runtime.test.mjs
```

After `build.sh`, exercise the actual embedded model, idempotent initialization,
chat history, and Clear behavior with:

```bash
node examples/candle_wasm_chat/smoke.mjs
```

## Measured reference build

On an Apple Silicon development machine, the release build produced a
277,885,159-byte WASM file. A direct JavaScript/WASM smoke test loaded the package
from the included local server and initialized the 272,696,282 embedded artifact
bytes in 496 ms. The required conversation produced `Hello!`, `Paris`, `Green`,
and then recalled `Green`; turn times were 14.5, 20.2, 26.2, and 31.6 seconds as
the full history grew. After Clear, the model no longer recalled green.

The borrowed embedded-artifact loader avoids a transient 270,590,880-byte GGUF
copy. Node observed about 1.76 GB RSS immediately after initialization and about
1.23 GB after the full conversation, with roughly 760 MB classified as
external/WASM memory. Browser engines, hardware, and optimization settings will
vary, so treat these numbers as a practical reference rather than a guarantee.

The equivalent unoptimized native run generated the same answer in 17,536 ms
(0.46 generated tokens/s), including a 13,914 ms prefill. Release builds are
strongly recommended for actual use.

---

### Overview (`examples/litellm/README.md`)

# Rig with LiteLLM

Run a Rig agent through the [LiteLLM Proxy](https://docs.litellm.ai/docs/proxy/quick_start).
Rig sends OpenAI-compatible requests to the proxy, which routes them to the
configured provider. No LiteLLM Rust dependency is needed.

## Start a local proxy

Install the proxy with `uv`:

```sh
uv tool install 'litellm[proxy]'
```

From the repository root, in a shell with `OPENAI_API_KEY` exported, run:

```sh
litellm --config examples/litellm/config.yaml --host 127.0.0.1 --port 4000
```

The supplied configuration maps the public alias `rig-chat` to
`openai/gpt-4o-mini`. The provider key stays in the proxy's environment.
To use another provider, change `litellm_params` in `config.yaml` and supply
that provider's credentials to the proxy.

## Run the agent

In another shell, from the repository root:

```sh
LITELLM_API_KEY=unused cargo run -p litellm
```

The local configuration has no proxy authentication, so `unused` is a dummy
key. The example sends one prompt and prints the response.

For an existing authenticated proxy, export its key as `LITELLM_API_KEY` and
set the URL and model alias to match your deployment:

```sh
export LITELLM_BASE_URL=https://your-litellm-host/v1
export LITELLM_MODEL=your-model-alias
cargo run -p litellm
```

| Variable | Meaning | Default |
| --- | --- | --- |
| `LITELLM_API_KEY` | Proxy key, or `unused` for the local unauthenticated configuration | Required |
| `LITELLM_BASE_URL` | Proxy API base URL, including `/v1` | `http://localhost:4000/v1` |
| `LITELLM_MODEL` | A `model_name` alias from the proxy configuration | `rig-chat` |

The Rust process needs the proxy key, not the upstream provider key.
`client.chat(model)` explicitly selects `/chat/completions`, producing
`/v1/chat/completions` with the default base URL. Rig's OpenAI client otherwise
defaults to the Responses API. The model value is the proxy alias, not the
provider-qualified `litellm_params.model` value.

---

### Overview (`examples/typesafeai_chat/README.md`)

# Interactive Jev + Rig chat

Run from the repository root in a shell that already exports `JEV_TOKEN`:

```sh
cargo run -p typesafeai_chat
```

Try: `I upgraded yesterday, but exports still say plan limit exceeded.`
Then add evidence in the next message. Jev sees the current input and the last
8 completed turns. Each call batches a typed route, an urgency rubric, and the
probability that clarification is needed. A generic `Assessment` struct implements `Query`, retaining the same fields for
questions and answers. `AssessmentQuery::new()` builds it once for reuse.
The default mode prints decisions and
a deterministic suggested action; Jev does not generate conversational prose.

For replies from an ordinary Rig agent, also export `OPENAI_API_KEY`, then run:

```sh
cargo run -p typesafeai_chat -- --agent
```

This calls Jev first, then uses its assessment to choose instructions for
`gpt-5.6-sol`. Your OpenAI endpoint must support that model and have available
credits. Failed requests print an error and leave conversation history unchanged.
Requests time out after 90 seconds. No secrets files are loaded by the example.

Use `/reset` to clear history, `/quit` or EOF to exit, and `--help` for usage
(which requires no credentials). The thresholds are illustrative application
policy; confidence measures distribution concentration, not correctness.

---

### Overview (`examples/typesafeai_triage/README.md`)

# Typed Jev triage

Requires `JEV_TOKEN` in the environment. From the workspace root:

```sh
cargo run -p typesafeai_triage
```

This sends a synthetic support ticket as a Serde struct and evaluates Choice,
Score, and Noul together. The selected route is a Rust enum. The example prints
distributions and confidence instead of hiding uncertainty behind that selection.
Score is an expected zero-based rubric index; Noul is a probability of yes.

The questions are independent: they share the ticket, not each other's answers.
See the [Jev primitives documentation](https://docs.typesafe.ai/primitives).

`Assessment<R, U, D>` declares its fields once. Its `Query` implementation maps
each field type to its associated output type. `AssessmentQuery::new()` supplies
explicit choice and score variants; no Jev derives are involved.

`client.evaluate(&ticket, AssessmentQuery::new()?).await?.answers` returns the
same named structure populated with typed answers.

---

### Overview (`plugins/claude-cloak/README.md`)

# Claude Cloak for Rig (opt in)

This separate plugin adds `claude-cloak/*` catalog entries and
`/claude-cloak login | logout`. It does not replace `anthropic/*`, read or
change `ANTHROPIC_API_KEY`, install global HTTP middleware, select a model, or
start login automatically. It is **not enabled by default**. This unpublished
workspace plugin supports Linux and macOS. Its public plugin type and runtime
dependencies are gated to those platforms. Windows and WASM are unsupported.

## Enable and use

Only if you want subscription authentication, explicitly add the plugin from
this checkout:

```sh
rig plugin add claude_cloak::ClaudeCloakPlugin --path ./plugins/claude-cloak
```

For an existing disabled entry, use
`rig plugin enable claude_cloak::ClaudeCloakPlugin` instead.

Then reload Rig yourself. Run `/claude-cloak login` to open the browser,
complete authorization, and use `/model` to select a `claude-cloak/*`
entry. The entries copy the installed Rig catalog's Anthropic models; listing
is not proof that your subscription authorizes a particular model.

If a saved session selected an `anthropic-oauth/*` entry, select the corresponding
`claude-cloak/*` entry after reloading. Existing credentials remain usable; the
credential directory is unchanged.

Login uses browser OAuth PKCE and a state-checked loopback callback, preferring
port 54545 and falling back to an OS-assigned port. It uses `xdg-open` on Linux
and `/usr/bin/open` on macOS. Browser-open and callback waits are bounded.
There is no manual code-paste or headless flow. This plugin never imports
OMP's credentials.

Credentials are stored at `$RIG_HOME/anthropic-oauth/credentials.json`, or
`~/.rig/anthropic-oauth/credentials.json`, in a private directory with mode
0700 and files with mode 0600. Token writes use atomic replacement. Refresh is
serialized across processes and preserves refresh tokens omitted by the
server. Rotated tokens are saved before optional account lookup, so cancelling
that lookup cannot discard a successful refresh. The token and account identity
are read as one locked snapshot.
`/claude-cloak logout` deletes local credentials, not the server grant or
device identity. It refuses while login or refresh owns the credential lock;
retry after that operation finishes. Already-dispatched requests are not
cancelled. Authentication errors omit token response bodies and browser URLs.

## Compatibility and provenance

The initial implementation was adapted from OMP commit
`d5cd750e41fede277f9eb933455da92a0738915e`, particularly its Anthropic provider
and OAuth modules. The retained OMP corpus is a historical control. Its
`2.1.280` version was an offline fallback, with an updater and server-required
version adoption, rather than a permanent compatibility target.

The current request profile targets observed Claude Code **2.1.307**,
TypeScript SDK **0.128.0**, and runtime header **v26.3.0**. `src/profile.rs`
contains the coupled identity, beta flags, cache default, billing entrypoint,
checksum rules and compression threshold. These describe wire compatibility.
`tests/fixtures/claude-code/profile-2.1.307.json` records the structural TUI
reference and official binary digest.

The current checksum hashes the original compact JSON with its billing
placeholder zeroed. Its byte scanner omits model string values, fallback
arrays, unsigned `max_tokens` fields and fallback-credit tokens, including
adjacent field separators. It preserves the remaining bytes and property
order. The recipe was checked independently against the native exclusion
scanner and original primary and auxiliary TUI request bytes. Redacted or
reserialized bytes cannot establish checksum correctness. Already billing-
prefixed system prompts are rejected to prevent duplicated attestation blocks.

Agent calls use the primary beta profile even with tools and thinking off.
Thinking defaults to `display: "updates"` with the corresponding beta; an
explicit display remains authoritative. Rig does not generate Claude Code's
session-title calls, copy its tool inventory, inject optional billing lifecycle
identifiers or diagnostics, or mark its system prompt for global cache scope.
Those fields depend on the application's own prompt, request and lifecycle
state. The live comparison records these differences rather than asserting
whole-request equality. HTTP 200 alone does not establish agent-loop parity.

Agent requests select the first human message through Conversation authorship,
using its stored text, not raw keystrokes, before injected context is serialized.
When an authored human message exists, tool results and plugin-authored context
cannot displace it as the fingerprint source. The agent saves the derived
three-hex suffix, its source message ID, three selected UTF-16 units and the
profile version. It does not save another copy
of the prompt. The selected units allow a version update to recompute the
suffix after the original human message has been compacted. Compaction and
resume retain the seed; a fork inherits it when it retains that human message.
A fork before the source resets both suffix and seed. Every fork receives a
new session UUID.

Earlier saves contain only a suffix and source ID. On their first request
under the new profile, the first retained authored human supplies a new seed
and source ID. If no authored human remains, migration uses the empty-text
seed and clears the unavailable source ID. This starts a new profile provenance
boundary while retaining the OAuth session UUID. Later turns keep that seed.
An image-only or empty first human also uses the empty-text seed. Direct
provider calls and delegated requests without an authoritative human retain
the conversational wire-text fallback.

An omitted cache preference selects the profile's long retention. Explicit
`none` adds no automatic profile markers, including on the identity block.
Caller-supplied block markers and raw top-level `cache_control` are explicit
overrides and remain authoritative, so `none` is not a promise to erase those
controls. A raw root control delegates its moving breakpoint to the server;
Cloak adds no automatic profile markers alongside it. Explicit block markers
remain, and a final explicit block must have the root's TTL. With no explicit
controls, `none` sends no markers. Short and long retention govern automatic
placement; the final shaped body is checked for marker budget and TTL order.
These preferences control request markers, not the provider's internal retention
or privacy policy. This is a deliberate correction
to the pinned OMP identity-marker behavior.

Kernel-built requests defer their eager catalog cache default in Prepare Input, before
normal request editors run. A provider-bound adapter supplies the long default
and private identity only when the final connection is Claude Cloak. Routed
and cloned requests do not carry private identity parameters to other providers.
A causally linked fallback that shares its predecessor's started request keeps
an omitted cache preference omitted. Editing that request creates a separate
request value and preserves the caller's explicit preference.

The plugin reuses Rig's generic Anthropic completion implementation for
message conversion, reasoning, streaming, tools and usage. Its own HTTP
client is used only by this separate vendor. OAuth and inference use the same
workspace reqwest implementation and normal TLS verification; child-scoped
`SSL_CERT_FILE` trust configuration applies to both. It refuses destinations other
than HTTPS `api.anthropic.com/v1/messages` before reading credentials and
never follows redirects. The wrapper supplies OAuth bearer authentication,
the observed profile headers and beta selection, billing and Claude Code
identity blocks, explicit cache-marker placement and retention, thinking
controls, ordered JavaScript-compatible JSON and the current cch XXHash64
attestation. Gzip starts at 4096 UTF-16 units after attestation and is disabled
with proxy or custom-CA environment configuration. Proxy captures therefore
verify uncompressed application requests, not native TLS or gzip equivalence.
Different DEFLATE implementations can produce different compressed bytes.
Custom tool names receive OMP's `_` prefix on the wire; the handler removes
one prefix from streamed and completed replies before Rig dispatches tools.

Device metadata derives from a private installation seed and account UUID.
Each Rig agent has a saved session UUID, attached only to this provider's
requests. Direct provider calls outside an agent retain a process-local
fallback UUID. Device initialization can fail closed if another process is
creating its first seed at exactly the same time; retrying then reads the
completed file.

This is not the OMP agent or Claude Code. It does not replicate their full
system prompt, built-in tools, telemetry, compression-rejection recovery,
retries, request scheduling, context editing, compaction, model discovery, plan/quota
UI, or OAuth status command. Normal Rig message encoding and capabilities
still apply. No claim is made that subscriptions permit third-party clients,
that every model is entitled, or that Anthropic accepts this fingerprint.
Use only an account you are authorized to access and review provider terms.

## Verification

Offline tests cover OAuth PKCE/state, private storage and locking, refresh
merging, destination guards, ordered billing attestation, tool-name reversal,
and vendor isolation. The checked-in corpus under `tests/fixtures/omp/`
contains 48 requests generated by the historical OMP source. A localhost
regression sends the same inputs through Rig's actual provider. It validates
both the historical checksum and the current checksum, checks current profile
headers and human fingerprints, then compares the remaining decoded JSON bytes
and property order. Its declared projection replaces profile billing text,
adds current thinking display defaults, and removes only the historical
`cache_none` identity marker. Original fixture files remain unchanged.
A separate structural current-TUI control freezes current headers, beta sets
and thinking settings. Another
18 source-generated schema fixtures test strict budgets and normalization, and a two-request
streaming test covers tool-call decoding and history replay. The comparison
scope and limitations are detailed below. Run from the workspace:

```sh
cargo nextest run --locked --profile local -p claude-cloak
cargo clippy --locked -p claude-cloak --all-targets -- -D warnings
cargo fmt -p claude-cloak --check
```

Normal checks never use stored credentials or make authenticated API calls.
An ignored live acceptance test requires explicit opt-in and an existing login:

```sh
CLAUDE_CLOAK_LIVE=1 \
CLAUDE_CLOAK_LIVE_MODEL=claude-cloak/claude-opus-5-5 \
cargo test --locked -p claude-cloak --lib real_api_three_probes_without_tokio_runtime -- --ignored --nocapture
```

This sends four billable requests across three probes: unary completion,
streamed echo-tool history replay, and explicit cache-point budgeting. It uses
synthetic prompts, never reads project files, and reports only status, marker
counts and usage. It does not deliberately interrupt a real token refresh or
force malformed server streams; those cases remain offline regressions.

A portable, explicitly authorized actual-agent matrix and structural logging
observer are documented in [the acceptance harness README](tests/acceptance/README.md).
They support supplied builds rather than machine-specific paths, isolate runtime
state, cap requests and duration, and classify missing evidence as unknown.
Their offline parser, guard and sequencing tests run in the repository's tooling
check. Live fux/provider execution of that maintained runner still requires a
separately authorized run; process mocks do not establish live success.

A separate expiry probe takes up to 70 minutes and at most nine requests, with
16 output tokens per request. It uses independent short-expiry, long-window and
long-expiry prefixes so the six-minute read cannot extend the prefix observed
at 65 minutes. It aborts as inconclusive if eligibility, usage or timing cannot
be established. Its 4,096-read threshold is a conservative evidence criterion,
not a claimed model cache minimum. Only run it after approving account usage:

```sh
CLAUDE_CLOAK_LIVE_EXPIRY=1 \
CLAUDE_CLOAK_LIVE_MODEL=claude-cloak/claude-opus-5-5 \
cargo test --locked -p claude-cloak --lib transport::expiry::real_cache_expiry -- --ignored --exact --nocapture
```

The ignored `oauth_client_uses_child_scoped_certificate_trust` regression uses
local TLS and ephemeral certificates, not an account or inference. It requires
`openssl` and verifies untrusted rejection and child-only CA trust in fresh
processes without changing global trust settings.

## Request comparison method

The reference captures execute OMP's `streamAnthropic` with an injected fetch
that records the original destination and sends to a localhost HTTP server.
Rig executes its catalog connection, completion adapter, Anthropic encoder and
OAuth transport using test-only credentials, identity and sender. The sender
redirects to localhost after the production destination guard and request
shaping. Captures pin test-only platform headers to the reference Linux/arm64
profile, independent of the test host. Production headers use the host platform.
Both servers return synthetic responses; production restrictions remain unchanged.

Historical inputs use identical prompts, messages, fake access token, account
UUID, session UUID and installation seed. URL and stable application headers
are compared directly. Profile-specific user agent, SDK, beta sets and
compression mode are checked against the current reference and current
compression policy. Both original and current billing fingerprints and
checksums are verified before profile text is projected away. The declared
thinking-display and cache-none corrections are the only other body projections;
tool schemas, cache topology, metadata and remaining JSON order are compared
as bytes. This establishes the preserved historical contract, not current
byte equality with OMP. Real TUI comparison and its limits are described in
`tests/acceptance/README.md`.

### Coverage

The 48 request fixtures cover Sonnet 4.6 and budget-thinking Sonnet 4.5:

- Plain text, absent system prompt and requests above the compression threshold.
- Custom tools, auto/required/none/named choices, underscored names, the buffered
  yield tool and tool-result history.
- Adaptive and explicit budget thinking with output-token reservation.
- Disabled, short and long cache retention with multiple conversation turns.
- Unicode, multi-block text, JSON escapes, numeric schema keys and floats.
- Schema constraint spilling and strict eligibility for the covered schemas.
- Sampling with thinking and stop-sequence truncation.
- Cache boundaries at 19/20/21 and 29/30/31 messages with one/three system blocks.
- Required, named and disabled tools with adaptive/budget thinking.
- Serialized ASCII/emoji lengths of 1023/1024/1025 UTF-16 units and UTF-8 bytes.

Historical compression fixtures retain their original 1024-byte OMP boundary.
Current compression tests use the 4096-UTF-16-unit threshold and verify proxy
and custom-CA suppression separately. The effort beta retains original thinking intent even when forced tools
remove thinking from the final body. Internal request metadata retains caller
intent before the generic encoder resolves it and is removed before sending.
Explicit cache points reserve the four-marker budget before automatic anchors
are added. Invalid marker counts or TTL ordering fail before sending.

The 18 schema cases cover strict allowlists, opt-outs, optional-property
budgets at 23/24/25, union budgets at 15/16/17, strict-tool limits at 19/20/21,
failed-candidate budget rollback, references, combinators, enums, constraints
and legacy schemas. Tests compare normalized schema structure and serialized
schema property order to OMP output. Tool wrapper streaming/cache fields are
excluded from these helper-level comparisons, not full-request comparisons.

`src/transport/roundtrip.rs` exercises two interleaved streamed tool calls
through the catalog/handler/transport, then builds a second request from their
decoded results. It checks names, IDs, fragmented arguments, success/error
results, ordering, caller metadata, unknown native fields and absence of doubled
prefixes across saved-history serialization. This is a synthetic
end-to-end regression, not an additional OMP differential capture.

### Reproduction and limits

The normal regression needs no OMP installation, credentials or external
network. It uses localhost servers and fails on body-byte or application-header
differences. Fixture provenance is in [the request corpus README](tests/fixtures/omp/README.md)
and [the schema corpus README](tests/fixtures/omp-schema/README.md).
The ignored request-capture helper accepts an explicit `OMP_PARITY_DIR` with
caller inputs and synthetic identity files, and emits Rig captures there.
External OMP tooling and temporary research artifacts are not shipped here.

Equality is established for these profiles, not every OMP feature.
OMP-specific developer/per-call-message provenance, synthetic continuation
pads, context editing, compaction and retry state are not fully replicated.
Rig's typed messages cannot preserve every arbitrary OMP input distinction,
such as scalar text versus a one-element text array. Other models, schemas and
provider options need their own differential coverage. ArkType AST/comment
conversion and exhaustive reference/legacy-draft collision behavior are not
replicated or exhaustively tested. Internal OMP `NO_STRICT`/`disableStrictTools`
switches are not exposed. Compression-rejection retry/latching is not copied.
Schema postprocessing deliberately traverses only schema-valued positions.
Unlike the pinned OMP walkers, it preserves literal instance data in `const`,
`default`, `enum` and examples, even when that data resembles a schema.

Browser login is independently tested but is not covered by dummy inference
comparisons. Manual-code/headless login, monthly warning UI, server acceptance,
subscription entitlements, TLS fingerprint equivalence and full agent-loop
equivalence are not claimed.

---

### Overview (`plugins/rig-basics/README.md`)

# rig-basics

The default plugins of the [rig coding agent](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-harness), and
the services only some plugins add. One crate, one module per group, all on
rig-harness's public API. Each plugin is its own entry in `plugins.toml`,
so each can be turned off alone.

- the root: `BasicCommandsPlugin` (`/help`, `/retry`, `/quit`);
- `prompt`: rig's fixed system prompt for an agent without one
  (`DefaultPromptPlugin`, `DEFAULT_SYSTEM_PROMPT`);
- `agents_md`: the project's and the user's `AGENTS.md` files in the system
  prompt (`AgentsMdPlugin`);
- `skills`: the skills listed in the system prompt, and `/skill:<name>`
  (`SkillsPlugin`);
- `models`: `/model`, `/effort` and the model picker (`ModelsPlugin`), and
  the model a new session starts on (`defaults::DefaultsPlugin`);
- `login`: `/login` and `/logout` for the ChatGPT plan's models and the
  providers' API keys (`LoginPlugin`);
- `session_picker`: the session and conversation lists of `/resume`,
  `/fork` and `/agents` (`SessionPickerPlugin`, which the plugins of those
  commands add, so it is not listed on its own);
- `sessions`: `/new`, `/resume` and `/name` (`SessionsPlugin`), and `/fork`,
  `/agents` and rewind (`agents::AgentsPlugin`);
- `compaction`: summarizing a conversation near the model's window, and
  `/compact` (`CompactionPlugin`);
- `telemetry`: what model calls cost and `/usage` (`usage::UsagePlugin`),
  the effect log (`effect_log::EffectLogPlugin`), and warnings and errors
  as data (`diagnostics::DiagnosticsPlugin`);
- `activity`: what each agent is doing, for views (`ActivityPlugin`);
- `coding_tools`: the `read`, `edit`, `write` and `shell` tools (`ReadTool`,
  `EditTool`, `WriteTool`, `ShellTool`), `@path` attachments
  (`AttachPlugin`), and `HOST:PATH` paths on other machines
  (`RemoteHostsPlugin`, below);
- `approval`: one question for every effect a plugin wants approved
  (`ApprovalPlugin`);
- `preview`: what a tool call would change on disk, before it runs
  (`PreviewPlugin`);
- `checkpoint`: what each tool call changed on disk (`CheckpointPlugin`);
- `reload`: `/reload` and the `reload` tool (`ReloadPlugin`), and
  `/plugins` (`plugins::PluginsPlugin`);
- `print`: `--print` and `--json`, the fronts of a run with `-p`, piped
  stdin or JSON lines (`PrintPlugin`), and the loop of any run nobody sits
  at (`AppHeadlessExt`);
- `helpers`: the `helper` tool, which runs `rig -p --json` children
  (`HelpersPlugin`);
- `subagents`: the `task`, `message` and `wait` tools (`SubagentsPlugin`).

In TUI, print and JSON runs, omitting `--tools` preserves each agent's tool access.
`--tools=` permits no tools; `--tools=read,write` intersects each agent's
existing access with those names before its first request and at execution.
Unknown names grant no access, so an entirely unknown list selects no tools.
This limit also applies to subagents, including their normally supplied
`message` and `wait` tools: include those names when children need them.
Subagents cannot use this flag to broaden their inherited restrictions.
Access is intersected in Prepare Input and already-built tool advertisements
are pruned in Prepare Edit, before dispatch and in a defined order with session
persistence.

`/usage` reports provider-supplied cost when available, otherwise API list-price
estimates. These are not subscription charges or verified invoices. Missing
usage details or prices leave a partial estimate; previously saved estimates
are not retroactively recalculated.

An entry names the crate and the plugin's type by the module that defines
it, the one path the type has, which `/plugins` and inspect show too:

```toml
[[plugin]]
crate = "rig-basics"
plugin = "rig_basics::compaction::CompactionPlugin"
```

`rig plugin disable rig_basics::compaction::CompactionPlugin` comments an
entry out, and `rig plugin enable <type>` puts it back; `/plugins` does the
same from inside the agent. `ApprovalPlugin`, `PreviewPlugin`,
`CheckpointPlugin`, `HelpersPlugin`, `SubagentsPlugin`, `EffectLogPlugin` and
`DiagnosticsPlugin` are listed disabled by default. A plugin that needs
another adds it when it is missing, as `SessionsPlugin` adds
`SessionPickerPlugin` and `SubagentsPlugin` adds `ApprovalPlugin`.

With the default `tui` feature, each plugin also draws its part in the
terminal view; `default-features = false` leaves that out.

`coding_tools::remote::RemoteHostsPlugin` makes a path written `HOST:PATH` another
machine's for every tool: `read bench:~/app/src/lib.rs`, `edit` and `write`
there, and `shell` with `cwd: "bench:~/app"`, which runs the command there.
Other paths stay on this machine. Each host is a transport, a command that
runs its last argument (a shell line) on the host, in
`RIG_HOME/settings.toml`:

```toml
[remote.hosts]
# ssh's options go in the command.
box = ["ssh", "-o", "BatchMode=yes", "dev@box"]
# koh exec runs the line given to `--sh` with the remote shell.
bench = ["koh", "exec", "bench", "--sh"]
```

With hosts configured, the system prompt names them and the path form, and
the tools' `path` and `cwd` arguments say they take it. Exit status 255 from
a transport means the transport failed: a file operation fails saying so,
and a `shell` call ends with that status. A transport must forward its stdin
and close it there when it ends: a `shell` command on the host stops when
that stdin closes, so a call stopped at its timeout, interrupted, or cut off
by a transport killed on this machine leaves nothing running there. `ssh`
does this; `koh exec` does on a stop or when its stdin closes. A project's
`.rig/settings.toml` cannot add hosts.

---

### Overview (`plugins/rig-inspect/README.md`)

# rig-inspect

An optional plugin of the [`rig-harness`](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-harness) coding agent: the
`inspect` tool, with which the agent reads its own running Bevy world through
Bevy Remote, answered in process (no transport, no port) and read-only. The
agent asks it which plugins are loaded and what each added, its agents, the
state saved with the session, the warnings logged (the `Diagnostics`
resource), schedules and every reflected type, before reading rig's source.

It is not in the default `plugins.toml`, because Bevy Remote makes the agent's
build heavier and, on Linux, needs the ALSA development headers
(`libasound2-dev` on Debian and Ubuntu, `alsa-lib-devel` on Fedora,
`alsa-lib` on Arch). Enable it by uncommenting its entry in
`$RIG_HOME/plugins.toml`, or with:

```bash
rig plugin add rig_inspect::InspectPlugin --crate rig-inspect
```

Only read-only methods are sent; a plugin
that adds its own Bevy Remote method marks it with `ReadOnlyMethod` (see the
crate docs). An answer over 16 KiB is cut and kept whole in the session's
spill directory for the agent to read.

---

### Overview (`plugins/rig-steel/README.md`)

# rig-steel

Code mode (`SteelPlugin`), an optional plugin of the
[rig coding agent](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-harness), not in the default
`plugins.toml`: the `run_steel` tool, whose
[Steel](https://github.com/mattwparas/steel) (Scheme) program spawns agents,
sends them requests, waits for their replies and calls the model's own tools,
in Steel's sandboxed engine. It builds on rig-ecs alone, so any rig-ecs app can
add it. The [agent guide](https://github.com/0xPlaygrounds/rig/blob/main/docs/agent.md) shows how to
enable it and an example program.

---

### Overview (`plugins/rig-tui/README.md`)

# rig-tui

The terminal view of the [rig coding agent](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-harness), and the
front of a run on an interactive terminal. `TuiPlugin` is `TuiViewPlugin`
(terminal, keys, frame) plus `DefaultScreenPlugin` (transcript, status line,
input), and the default screen is built from the same public widgets a plugin
places in panels of its own.

The view tells the terminal when a turn of an agent nothing spawned runs, as a
shell does a command: OSC 133 marks (`C` as it starts, `D;0` or `D;1` as it
ends, then `A` and `B`), a state report (`133;S;activity=busy` or `reading`)
for a multiplexer such as fux, and the title (`rig · working`, `rig · idle`).

Plugins add panels, transcript looks, status line items, key bindings and
completion without touching the view. The plugin guide shows each
([PLUGINS.md](https://github.com/0xPlaygrounds/rig/blob/main/crates/rig-harness/PLUGINS.md#the-transcript-notes-looks-and-scrolling)),
and the crate docs list every type.

A crate that also builds without the view puts its rig-tui code behind a
default `tui` feature. `rig_tui::ratatui` and `rig_tui::crossterm` are
re-exported. Without rig-tui the agent answers `-p` or piped stdin only. Tests
run the view without a terminal by adding the plugin and then `add_view`, put
keys in `PendingInput`, and read a frame from a `TuiTestScreen`.

---

### Overview (`plugins/claude-cloak/tests/acceptance/README.md`)

# Claude Cloak actual-agent acceptance

The default commands are offline and use only Python's standard library (Python
3.11 or newer, on Linux or macOS). They do not read authentication files, install programs, start a
server or contact a provider:

```sh
python3 -m unittest discover -s plugins/claude-cloak/tests/acceptance -v
python3 plugins/claude-cloak/tests/acceptance/run.py
python3 plugins/claude-cloak/tests/acceptance/run.py --analyze /path/to/private/evidence
```

Linux-only private-capture and live-preflight tests are skipped when Linux or `/dev/shm` is unavailable.
Offline analysis treats imported records as untrusted. It exports only validated
structural fields, including nested usage/fact/marker values; arbitrary headers,
bodies, identity strings and unknown fields are dropped. Evidence paths with
symlink components or symlink input/output files are rejected. Reports are
written using private temporary files and atomic replacement, not by following
an existing destination link.

An optional local fake-process smoke exercises two real fux/bubblewrap
launch/send/quit cycles and cleanup, using a temporary `/bin/sh` script and an
empty fake auth directory. It never launches Rig or contacts an API:

```sh
CLOAK_ACCEPTANCE_PROCESS_SMOKE=1 python3 -m unittest discover \
  -s plugins/claude-cloak/tests/acceptance -v
```

The smoke is Linux-only and skipped by default and when fux, bubblewrap, `/dev/shm` or user
namespaces are unavailable. Normal CI remains offline without process servers.

These tests verify the observer and driver, not Rig's inference implementation.
Actual agent-loop/mock-HTTP regressions belong to the Rust test suite. Mock usage
cannot prove server-side caching. No live execution of this maintained driver is
claimed by its offline tests.

## Authorized live execution

Obtain separate authorization for stored-credential use and charges before
running. Explain the selected scenarios and all three caps first. The live mode
requires Linux, bubblewrap (`bwrap`), `mitmdump` with its Python package, writable
`/dev/shm`, and a verified built Rig containing Claude Cloak. The TUI matrix also
requires `fux`. Nothing is installed automatically. Inspect the recorded source
revision and plugin manifest, and independently establish that the supplied
binary was built from them. A binary hash alone does not establish that binding.

The small mode launches two actual Rig print sessions with `--tools=` and the
same synthetic prefix. It measures fresh-prefix and repeated-prefix behavior;
it is not a multi-turn conversation test. The optional `--matrix` runs the
actual TUI on a dedicated, randomly named fux server:

1. Seed synthetic facts, then recall without restating values.
2. Read `facts.txt` successfully, then read a nonexistent, randomly named
   `probe-<random>.txt` without retry. The neutral name does not reveal its outcome.
3. Switch effort low to medium and recall; return to low.
4. Give a neutral answer, manually compact, recall and rewarm. Recall asks for
   the second read's filename, outcome and attempt count without providing those
   answers. Structural booleans must confirm the exact filename and one failure.
5. Optionally `--fork-resume`: fork, recall, quit, resume the same isolated
   session and recall. `/fork` creates an agent, not a separate session directory.
6. Start a separate session with the reduced trigger, seed, append a large newest
   message, observe a real compaction effect and final answer, recall old/new
   facts and rewarm.
7. Restart with an empty tool roster and repeat the same prefix in two fresh
   sessions.

A future authorized invocation has this form. Replace every uppercase argument;
this is not an instruction to use existing credentials now:

```sh
python3 plugins/claude-cloak/tests/acceptance/run.py \
  --live --confirm-stored-credentials --matrix --fork-resume \
  --binary /absolute/path/to/verified/rig \
  --source /absolute/path/to/verified/source \
  --plugin-config /absolute/path/to/build/plugins.toml \
  --credentials /absolute/path/to/original/anthropic-oauth \
  --build-record /private/build-record.json \
  --ledger /private/live-ledger.json \
  --output /absolute/path/outside/repository/NEW-private-evidence \
  --requests APPROVED_REQUEST_CAP \
  --output-tokens APPROVED_PER_REQUEST_OUTPUT_CAP \
  --seconds APPROVED_ELAPSED_SECONDS
```

The request cap is 2..24 Messages attempts at the proxy boundary, including
retries; elapsed time is 1..1800 seconds. The output cap is 1..128000 **advertised
maximum tokens per request**, not a promise of small actual output. The prior
Opus profile advertised 128000 for ordinary calls. A lower approved cap rejects
such a request locally instead of silently rewriting it. Requests ask for brief
answers, but the authorization must account for the worst-case cap. The full
matrix normally needs about 20 calls; retries or extra tools can exhaust its cap.
The driver stops at the first failed/unknown phase. There is no dollar estimate
or invoice assertion. Authentication refresh calls are not inference requests
and are not counted against that cap.

The proxy does not rewrite model inputs, cache controls or output budgets. It
narrows `Accept-Encoding` to `gzip, deflate, identity`, allowing the observer to
decode negotiated compression. It blocks wrong models, unexpected tools, excess
output limits and excess requests. Credential-like `sk-ant-` tokens in decoded
model JSON, including tool results and JSON-escaped text, are rejected locally
before forwarding with `credential_like_input`. Normal authorization headers
remain allowed. Matched values are never logged. This narrow guard is not a
general secret detector or a substitute for filesystem isolation. A model request bypassing the configured
proxy would not be counted: this is a child-scoped audit, not a network firewall.
Use only the verified transport that honors proxy/trust settings. No observed
calls is **unknown**, never a successful acceptance run.

## Isolation and privacy

The runner supplies an allowlisted child environment, isolated HOME, XDG
locations, RIG_HOME, workspace, settings and sessions. It never changes the
active user's model, plugins, configuration or conversations. Its generated
workspace contains only synthetic facts. The original `anthropic-oauth`
directory is bind-mounted into the child so normal atomic credential refresh
uses one store; refresh grants are never copied. The proxy CA and proxy variables
apply only to children. TLS verification is not disabled. No system trust or
proxy settings are changed. No existing fux server or pane is inspected. Every
fux start/controller/cleanup call uses the same mode-0700 `scratch/runtime` as
`XDG_RUNTIME_DIR`, keeping sockets and terminal journals in private tmpfs. The
server is started explicitly, and every launch has a unique tab name even when
previous exited panes still exist.

Only structural allowlisted summaries are exported. Neither authorization nor
cookies, raw headers, user identifiers, tool inputs, arbitrary error strings,
response text, thinking or signatures are persisted in evidence. Exact synthetic
input text is recorded separately as provenance. Response content is reduced to
synthetic-fact booleans. Provider request/message IDs, session identity, device
identity and the pinned human-fingerprint suffix are HMAC pseudonyms. The
private `.pseudonym-key` is created once with mode 0600 and reused on addon
reload; keep it private and do not include it in public artifacts.

Ordinary Rig session/effect logging still exists inside the child's private
`/dev/shm` scratch, including internal/signed content. It is not exported. The
runner imports only structural effect outcomes, then removes its owned scratch.
A force-kill or machine failure can leave private scratch behind; inspect and
remove only that run's directory. This does not claim raw Rig session storage is
redacted. Bubblewrap makes the host root read-only except for owned scratch and
the authorized credential bind; it is not a secrecy sandbox against an arbitrary
malicious binary. Supply a verified binary, not untrusted code. Proxy stdout,
stderr, flows and terminal screenshots are not saved. The only durable outputs
are the private sanitized evidence directory.

## Evidence format and assertions

`audit.jsonl` contains `request`, `headers`, `response` and `effect` records:

- `call`: monotonically increasing capture number, restored after addon reload.
- `scenario`: allowlisted driver phase, not model-supplied text.
- `requested_model` / `served_model`: expected model or `unexpected`, with exact
  equality checks against `claude-opus-5-5`.
- `markers`: safe indexed protocol path, ephemeral-type validity and 5m/1h TTL.
  Only actual tools/system/message-content/root marker slots count, in protocol
  order. Null controls and schema/input/nested-result data keys do not count.
  Full block contents are deliberately absent.
- `request_id` and `message_id`: stable pseudonyms of actual transport IDs. An
  effect joins by its actual provider request ID, never by timing or list order.
  Joined effect ID, purpose and usage are retained, not just a correlation flag.
- `human_fingerprint`: pseudonym of the pinned billing suffix, when present.
  Comparing it does not validate a billing checksum or current Claude Code.
- `usage`: numeric input/output/cache-read/cache-creation counters, including
  separate five-minute and one-hour creation buckets when returned.
- `terminal`, `final_answer`, `close`: a terminal model result followed by
  cancellation is distinguished from a response lacking its terminal. A normal
  EOF with an incomplete compression trailer or unfinished SSE frame fails the
  observer-integrity check, even after a terminal. Explicit terminal-plus-CANCEL
  retains its separate completed classification. Tool-use
  terminals are not final answers. `max_tokens` stops do not pass.
- `parse_error`, `compression_complete`, `pending_frame`: decoding evidence.
  Unsupported observer compression produces capture **unknown**, not a provider
  failure. It is not silently treated as uncompressed bytes. Gzip and
  RFC zlib-wrapped deflate are supported; raw nonstandard deflate is not.
- `stop_reason`: the exact allowlisted protocol reason, including `refusal` and
  `max_tokens`; an unrecognized value becomes `other` without exporting it.
- `local_guard`: a local rejection, never represented as a provider rejection.
- `facts`: exact synthetic-value matches in the response, without exporting it.
- `second_read`: exact randomized filename and failure-with-one-attempt evidence
  from the formatted recall response. No arbitrary response text is exported.

`audit.py` accepts ordinary effect records and delta envelopes' `record` field.
It reads outcomes only; it does not reconstruct delta-compressed input history
or implement a second production Anthropic decoder. Summary effects must have
purpose `compaction`. The TUI driver waits for a final wire response and its
matching settled effect before advancing. It checks positive cache reads on
specified warm phases, not fixed production counts or a completely cold first
call. It checks tool-result success/error evidence, effort, fact retention,
smaller post-manual-compaction input, summary presence during auto-trigger, and
an exact full synthetic newest-message substring in the post-summary request.
Optional fork/resume checks preserve fingerprint provenance and expected session
identity continuity. These are empirical acceptance assertions, not proof of
retention for arbitrary messages or a provider context limit.

`provenance.json` records commit, dirty-tree status, binary and plugin-manifest
hashes, model, settings, caps and synthetic inputs. `matrix-inputs.json` records
matrix prompts. `matrix.json` records phase pass/fail/unknown. `summary.json` is
the machine report; `REPORT.md` is the concise human report. Missing phases, responses, usage, identity or compaction evidence stay unknown.
Two absent identity values never establish identity continuity. Failures are retained rather than retried until
a green result appears. If a transient failure warrants another authorized run,
use a new evidence directory and keep both reports.

## Boundaries and remaining coverage

The reduced policy uses `reserve = 970000`, `summary_tokens = 1024`,
`keep_recent = 1024`, `keep_asked = 0`. It tests a local roughly 30k trigger, not
million-token server overflow. The live driver deliberately does not trigger
summary failure, deliberate token-refresh cancellation, malformed provider
output, TTL expiration, or expensive true context exhaustion. Those require
mock-HTTP regressions or separately approved expensive/long-running experiments.

The bounded matrix's process commands and sequencing are covered offline. When
fux is installed, its native `check` validates command grammar without executing
actions or starting a server. Timeout options follow their pane/options stack
receiver; status 124 is accepted busy state, with completion still bounded by
the wire/effect observer deadline. Status 3 is accepted only for an expected
`/quit`. The fake-process smoke also checks that the bubblewrap `/dev` mount
precedes rebinding `/dev/shm` scratch, so the workspace remains reachable. Live
fux interaction and provider behavior must still be exercised with explicit
approval. If a build changes command names, event purpose, output budget or
profile fingerprint format, the driver should fail/mark unknown instead of
inventing equivalence. The printed phase plan also serves as a manual checklist
for a dedicated fux tab if diagnosing an interactive failure. Do not switch to
a user's existing tab, weaken an assertion or add unbounded sleeps to make a
phase appear successful.

## Matched Rig and Claude Code runs

The maintained driver supports Claude Code's TUI with `--clients claude`, or
Rig alone with `--clients rig`. Separate runs share `--ledger` and use the same
synthetic prompts and workspace contents. `--compare` runs both, Claude Code
first, within one run's cap. Its default `--scope probes` runs seed, warm recall,
successful read and failed-read continuation. `--scope full --fork-resume` adds
the lifecycle matrix. Each client stops at its first failed or unknown phase
by default. The explicit Rig read-refusal continuation described below can
observe later independent phases while retaining that failure.
A planned second client is still observed after an upstream refusal; permanent
local guards stop the experiment. Keep the failed evidence.

Claude Code uses `--safe-mode`, empty setting sources and MCP configuration,
`--strict-mcp-config`, read-only tools, `--permission-mode dontAsk`, explicit
Opus 5.5/low effort and disabled suggestions. `--name "synthetic parity"`
suppresses automatic title generation. The child also sets
`CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1` and
`CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1`. These controls deliberately exclude
auxiliary title traffic from the primary comparison. The observer still counts
and blocks any such requests. `claude-haiku-5-5` is the explicit known title
model; additional known auxiliary IDs require `--blocked-auxiliary-model ID`.
Auxiliary calls are never forwarded. Unexpected models or tools stop the run.
Requested fallback models are also checked: only an explicit model-array
shape naming the approved primary model can pass. Default routing, malformed
fallback shapes and any other model are rejected locally as `fallback_model`.
Safe profiles record only known public fallback IDs or `unexpected`. Earlier
observer versions checked the primary request and served model only; a native
request could therefore advertise a different fallback without triggering
that guard. Historical captures retain that limitation, even when their
actual served model was the approved primary.
The Claude Code roster is exactly `Read` and `DeferredToolPlaceholder`; Rig's
is exactly `read`/`_read`. Permanent guards return HTTP 400
`invalid_request_error` with `x-should-retry: false`, rather than retryable 429s.
Expected auxiliary blocks have classification `blocked_auxiliary` and cannot
establish a passing primary scenario.

The Claude Code child reads its existing access token into memory and writes
minimal identity/onboarding configuration only into private tmpfs. It never
copies a refresh grant or writes the original configuration. The token must
remain valid through the bounded deadline plus one minute, otherwise startup
fails before inference. Refresh the original client store normally first if
needed. Supply its original home with `--claude-auth-home` and the exact isolated
CLI binary with `--claude-binary`. Offline preflight records its version, binary
and help hashes and verifies the required isolation/session flags.

`--claude-auth stored` is the default. For an account-controlled comparison,
`--claude-auth rig-access-token` loads the exact existing Rig access token from
the supplied `--credentials` directory inside the child. It requires validity
through the deadline plus one minute. The private Claude Code configuration
uses that store's account and optional organization UUID, retaining only its
own installation identity from the original Claude Code configuration. Other
account details and email are omitted. No refresh grant is copied and neither
host credential store is modified. Provenance records the selected auth mode
and booleans comparing the stored and selected accounts with Rig; no account
identifiers or tokens are exported. Different account entitlements remain a
confound until a comparison uses the same access token.

Rig's reduced automatic-compaction threshold is specific to Rig. Claude Code's
matching large-append session uses explicit `/compact` after the append, then
recalls old/new facts and rewarms. Its comparison report says
`manual-after-append`; it does not claim to exercise Claude Code's automatic
threshold. Claude Code forks using its supported `--resume --fork-session` TUI
launch, discovers the new private persisted session ID and resumes that fork.
No raw session IDs are exported.

Each read phase reconstructs the streamed tool input in memory and requires
exactly one invocation of the client's read tool on the expected synthetic
file. Absolute Claude Code paths must resolve inside the owned workspace.
Only invocation counts and target-match booleans persist. Historical tool
results cannot satisfy a new phase. Rig retains its seed fingerprint through
manual compaction, fork and resume. Claude Code can change its human-message
fingerprint after compaction; the observer records that transition, then
requires continuity through recall, rewarm, fork and resume.

Record the successful build after source changes finish:

```sh
python3 plugins/claude-cloak/tests/acceptance/run.py \
  --record-build /private/build-record.json \
  --binary /absolute/path/to/rig-agent --source /absolute/path/to/rig \
  --plugin-config /private/build-home/plugins.toml \
  --generated-main /private/build-home/project/src/main.rs \
  --build-command 'THE EXACT SUCCESSFUL BUILD COMMAND'
```

Live preflight checks the head, tracked diff, untracked file names/content hashes,
manifest, generated main and binary
against that record. It checks enabled registrations for Claude Cloak, TUI,
print/tool limits, read, sessions/agents, compaction, basic commands and effect
logging. It also runs the actual binary with `RIG_DESCRIBE` in an isolated home
without credentials, rejecting stage conflicts before inference. The recorded
command is a build provenance assertion, not a compiler attestation.

Add `--clients claude --scope probes --claude-binary PATH --claude-auth-home HOME`
to the live example for a Claude Code run. Run Rig separately with
`--clients rig --match-inputs /private/claude-run` and the same ledger. This
reuses the exact randomized missing filename. An explicit `--missing-file
probe-<16 hex digits>.txt` can also pin that synthetic input. Full matrices
generally need separate runs.
The locked ledger counts every Messages attempt, including auxiliary and local
blocks, across evidence directories and addon reloads. The default limit is
48 aggregate attempts. Once the user explicitly authorizes an increase, pass
`--aggregate-requests 96` or `--aggregate-requests 200` on each subsequent run
to use that explicitly authorized ceiling on the same ledger. The ledger
records the approved ceiling and each run's chosen
ceiling; counts, elapsed time and earlier run policies are preserved. Legacy
runs retain their 48-attempt ceiling. Provenance and policy record the selected
ceiling. Per-run limits remain 24 attempts, 128000 advertised output tokens
per request and 1800 aggregate live seconds. The last permitted attempt
is rejected locally as a reserve to stop before another turn. An unfinished
run remains active in the ledger for inspection. Never reset consumed budgets.

Before a model run, use `--matrix --clients claude --startup-only` with the
same live options to validate actual private TUI launch and prompt readiness.
This sends only `/quit`. The proxy rejects every Messages request with a local
nonretryable error during this mode. A passing startup report requires ready
clients and zero Messages attempts; it proves no model behavior. Startup
diagnostics contain fixed event codes only. Raw screens and exception text
are never exported. Short private server names keep nested Unix socket paths
within the platform limit.

The separate `--root-ttl-probe /absolute/compiled/claude_cloak_test_binary`
mode runs the ignored test
`transport::live::tests::real_root_ttl_regression_and_control` through this
same proxy and ledger. Use a non-matrix run with `--requests 3 --seconds 90`:
two Messages can be forwarded before the reserved final slot. Offline
preflight verifies that exact test exists. The test checks Long and Short
retention with explicit raw root 5m caching, and requires complete READY
answers. Its stdout/stderr are discarded; the report requires exit zero and
two observed successful end-turn READY responses. The compiled test binary's
hash is recorded alongside the normal verified Rig build.

The separate `--tool-result-probe /absolute/compiled/claude_cloak_test_binary
--captured-request /dev/shm/PRIVATE/rig-6.json` mode runs
`transport::live::tests::tool_result::real_tool_result_representation_probe`.
The captured original must remain an owned private file in private tmpfs
directories, with no symlinks. It is mounted read-only into the child and
never copied into the evidence directory. Use `--requests 3 --seconds 90
--output-tokens 128000`. The Rust probe permits exactly two requests: the
captured baseline and a variant changing only the failed text tool result's
array/string representation. `--probe-variant tool_result_string` is that
default. `--probe-variant thinking_binding_absent` instead removes only
`thinking.block_binding` from the same captured baseline; the field must be
present. Both variants retain all signed history, `is_error`, cache markers,
output limits and beta headers. The Rust probe independently checks the final
request bytes. Its safe detail report records the selected closed-enum
variant and `variant_only_declared_field_and_cch` control. Older string-only
control records are interpreted only as that string variant.
`--probe-variant final_error_cache_absent` instead removes only the final
failed tool result's cache marker. Another cache marker must remain. History,
tool-result contents, `is_error`, all other markers and their TTLs, output
limit and beta headers stay unchanged; caching remains enabled.
`--probe-variant request_class_main` instead adds only the documented
`x-claude-code-request-class: main` header. Body bytes and checksum remain
identical. Its report requires `body_equal` and
`headers_only_declared_change` to be true, with `headers_equal` false;
body-changing variants still require equal headers.
`--probe-variant previous_request_id --probe-metadata /dev/shm/PRIVATE/metadata.json`
adds only `cc_prev_req` with the captured Rig history's actual preceding provider
request ID to the billing block, then recomputes its checksum. Metadata must be
an owned private tmpfs file containing exactly `{"previous_request_id":"req_..."}`.
The ID must come from that Rig history, not from another client. The metadata
file is mounted read-only into private child scratch; its contents and path
are not exported into provenance or reports. This variant requires equal
headers and preserves all other body fields.
`--probe-variant neutral_default_identity` changes exactly one system-block
prefix from `You are rig, a coding agent` to `You are a coding agent` and
recomputes the checksum. The protocol identity and every functional instruction,
message, tool, cache marker and header remain unchanged. Missing or duplicate
prefixes are rejected. This variant takes no metadata file.
`--probe-order baseline-first` is the default. Explicit `--probe-order variant-first`
reverses only the send order after both requests have passed their byte and
header guards. Reports record the order and label each observed outcome as
`baseline` or `variant`; a reversed pair does not retry either condition.
This permits a counterbalanced diagnostic without changing the request pair.
The proxy retains normal model, read-tool, output and budget guards.
`tool-result-probe.json` records only exit status and timeout. A complete
diagnostic exits zero but its overall classification remains unknown; refusal
calls remain failed observations. This mode cannot establish matrix acceptance
or turn an earlier failed run into a passing run.

`profiles.jsonl` records allowlisted application headers/betas, body keys,
system/message structure, tool schema hashes, thinking/display controls,
diagnostics presence and marker TTL/scope topology. Literal text, schemas,
tool-result contents and signatures are not exported. An optional
`--checksum-verifier PATH` module exposes `verify(original_decoded_bytes)` and
returns `True`, `False` or `None`; only match/mismatch/unknown is saved. It sees
the exact decoded bytes before JSON reserialization. No verifier means unknown.

Responses record whether structured stop details and an explanation were
present, plus a closed refusal type/category. The official native schema names
`cyber` and `bio` categories while leaving the category string open; other
categories become `other`, and absent categories become `absent`. Arbitrary
category strings and explanation text are never exported.

Native correlation headers use a separate closed roster: session, prompt,
agent/parent-agent, request IDs and class, agent type, previous tool durations,
compaction indicators, signature, additional protection and wiggly-dove
metadata. The observer exports presence and private pseudonyms, plus verified
closed request-class roles and `teammate`/`custom` agent roles. Other role
values remain opaque. It records whether a present session header matches the
body's metadata session. Unknown header names and values are never exported;
only their distinct-name count remains after excluding known HTTP/auth/cookie
headers. Profile comparison checks presence and roles because separate runs
have different pseudonym keys. Tool-result profiles distinguish an absent
`is_error` field from explicit false and true values.

For private checksum investigation, `--raw-capture --pause-after-seed` retains
original decoded request bodies only in owned `/dev/shm` scratch. After each
client's seed, `paused.json` names that private directory. Inspect the originals,
then create an empty `continue-claude` or `continue-rig` file in the evidence
directory. The same deadline applies to the pause. Cleanup deletes raw scratch.
Never export these files or verify checksums from redacted or rebuilt JSON.

Use `--raw-capture --pause-after-phase read_error` to inspect the matched read
continuation before cleanup, whether that phase passes or fails.
`--pause-on-failure` pauses at the first failed or unknown phase. Both require
`--matrix`; `--pause-after-seed` remains available for seed inspection.
`paused.json` records the phase, unchanged outcome, private raw directory and
waiting/released state. Create `continue-CLIENT` to release the pause. The
original failure then stops the client as usual. Inspection uses the same
live deadline and never authorizes another request.

`--scope full --continue-after-read-refusal` is a separate explicit option for
Rig. It permits later independent phases after a complete HTTP 200 refusal
following exactly one read of the expected missing file and exactly one new
error result. Both read-phase responses must have complete SSE framing,
expected model/tool roster and valid cache markers, and both must correlate
with actual Rig effects. The TUI must return to input readiness. Local guards,
HTTP failures, truncated streams, unexpected models/tools and other failed
phases still stop the client. The next prompt defaults to the different effort
recall prompt. Explicit `--read-refusal-next manual_compact` instead advances
directly to the existing compaction command, leaving effort and neutral checks
unknown. Recall still requires all facts and the genuine failed-read outcome,
and its input must shrink relative to the refused read request.
`continuations.jsonl` records the reason and next phase without
text; `read_error` and the overall report remain failed. There is no retry of
the refused prompt and later assertions are unchanged.

Rig-only `--scope auto-controls` runs the existing isolated automatic-compaction
session and two fresh zero-tool controls. It uses the same seed, large append,
reduced trigger settings and assertions as the full matrix. It does not add
context or retry the failed conversation. Manual/error lifecycle, effort,
fork and resume checks remain unknown in this separate report; earlier failed
full reports are unchanged. Successful isolated controls therefore do not make
the overall full matrix pass. This scope excludes `--fork-resume`,
`--startup-only` and refusal continuation.

Rig-only `--scope full --collect-failures` explicitly collects failures across
the fixed full matrix. After a fully observed HTTP 200 refusal or normal
end-turn semantic assertion failure, it may advance to the next distinct
phase once actual effects settle and the TUI is ready. It requires complete
SSE framing, expected primary and fallback models, valid cache markers and
the expected tool dispatch. No-tool phases require zero tool invocations;
read phases reject wrong targets and repeated calls. A read refused before
dispatch can be collected as a failure, without inventing its outcome for
later retention checks. Those checks also require evidence of the actual
missing-file read and its single error result. A cancellation after a fully
validated terminal SSE frame may retain the completed response; cancellation
with missing framing or pending data cannot. HTTP/transport errors, local
guards, unknown capture,
incomplete responses and tool-dispatch violations still stop the run.
`failure-continuations.jsonl` records only the known phases, reason and proof
booleans. Each failed check and the overall failure remain visible even if
later phases pass. This mode injects no history and retries no phase. It
cannot be combined with startup-only or the narrower refusal continuations.

Compare separate runs offline:

```sh
python3 plugins/claude-cloak/tests/acceptance/run.py \
  --compare-reports /private/rig-run /private/claude-run \
  --output /private/NEW-comparison
```

`comparison.json` pairs primary requests by phase and prompt/tool-continuation
role, then position within that role. It reports structural equality booleans
and each verifier's result. Optional observations missing from either capture
are `null` (unknown), including fields added by newer observer versions.
Historical captures are not backfilled. Application prompts, tools, summaries and responses
can explain unequal structure. Those differences alone establish neither a
failure nor byte equality. Claude Code completion is wire evidence; only Rig
responses can join to settled Rig effects.

---

### Agent (`docs/agent.md`)

# The rig coding agent

`rig` is a terminal coding agent. It is a Bevy app built on the
[`rig-harness`](../crates/rig-harness) crate, and every part of it but the
harness is a plugin, listed in `plugins.toml` and built from a plugin crate.
The `rig` command is its launcher: it generates a small Cargo project for the
agent with those plugins, builds it, and runs it.

The agent's coding tools come from the [`rig-tools`](../crates/rig-tools)
crate, usable by any rig agent, and its kernel (agents, turns and tool calls
as Bevy entities, one recorded effect path, session logs) is the
[`rig-ecs`](../crates/rig-ecs) crate, for building your own harness.

- [Install and run](#install-and-run)
- [Models and sign-in](#models-and-sign-in)
- [The terminal view](#the-terminal-view)
- [Sessions](#sessions)
- [Context, compaction and retries](#context-compaction-and-retries)
- [Tools](#tools)
- [Subagents](#subagents)
- [Without the terminal view](#without-the-terminal-view)
- [Reload and builds](#reload-and-builds)
- [Plugins](#plugins)
- [Files](#files)
- [Running from a checkout](#running-from-a-checkout)

## Install and run

```bash
cargo install rig
export OPENAI_API_KEY=...   # or any other provider key in the model catalog
rig                         # builds the agent (later starts rebuild what changed), then opens it
```

The agent builds from rig's crates on crates.io, at the launcher's version,
or from a rig checkout ([Running from a checkout](#running-from-a-checkout)).
It needs Rust 1.97.1 or newer and runs on Linux and macOS; Windows is not
supported yet.

## Models and sign-in

`/model` picks a model (providers that need no key, such as a local Ollama,
are listed last), and `/effort` its reasoning setting. `-m vendor/model` picks
one on the command line; otherwise a session starts with the last model and
reasoning chosen, else the first model with a key.

`/login` without arguments lets you pick the ChatGPT plan or a provider that
takes an API key. `/login <provider>`, such as `/login anthropic`, asks for
the provider's key, hidden as you type or paste it. The key is saved in
`RIG_HOME/auth/keys/<provider>.json`, readable by you alone, and is used
before the provider's environment variable (`ANTHROPIC_API_KEY`). With a saved
key the provider's other variables, such as a base URL, are not read. If the
agent has no model yet, the model picker opens on the provider's models.
`/logout` lets you pick what to remove of what `/login` stored, or takes its
name (`/logout anthropic`); environment variables are never touched.

A ChatGPT subscription can pay for the model calls instead of an API key.
`/login chatgpt` opens your browser on the ChatGPT sign-in page and shows its
URL in case the browser does not open; the page returns to a one-shot
listener on `127.0.0.1` port 1455 (1457 when 1455 is taken). Without a
graphical session (no `DISPLAY` or `WAYLAND_DISPLAY` on Linux and the BSDs),
over SSH, or when both ports are taken, it shows a code to enter at
`https://auth.openai.com/codex/device` instead, as `/login chatgpt --device`
does. Print mode prints the URL or code, and `rig -p "/login chatgpt"` signs in
without the terminal view. Esc or a second `/login` cancels a sign-in that waits.

Once signed in, `/model` lists the plan's models as `chatgpt/...`, marked
"(ChatGPT plan)", and a successful sign-in switches the agent to the plan's
latest frontier model. The credential is kept in `RIG_HOME/auth/chatgpt.json`,
readable by you alone, and refreshed before a request when it has expired, so
a long session keeps working (rig processes running at once take turns
refreshing it). `/logout chatgpt` asks OpenAI to revoke it, then deletes it.
It is rig's own sign-in, separate from the Codex CLI's. A
`CHATGPT_ACCESS_TOKEN` in the environment (with `CHATGPT_ACCOUNT_ID`) is used
instead when set.

## The terminal view

The input takes several lines. Enter sends; Shift+Enter, Ctrl+J or `\` before
Enter starts a new line; Up and Down move between lines and through the
prompts sent before (kept in `RIG_HOME/data/tui/history.jsonl`); the usual
emacs keys edit (Ctrl+A/E/K/U/W, Alt+B/F/D); Ctrl+C clears the input, and
Ctrl+D pressed twice on an empty input quits. A
leading `/` completes command names and `@` completes the project's paths
(skipping what `.gitignore` leaves out); Tab or Enter takes the selected one.
A command that is unknown or refused comes back in the input with the reason,
and Enter then sends it to the model as it is. `/help` lists the commands.

While nothing is typed, the input's top row shows the keys and its bottom row
the status line: the model, the reasoning setting, what the agent is doing,
the session's tokens (uncached input, output, cache reads and writes), its
cost (the provider's figure, else the catalog's list price; `+` when some
calls had no price) and the context in use against the model's window.
`/usage` breaks them down.

Typing while the agent works is fine. Enter steers the running turn (the
message goes to the model with its next call, after the tool results it waits
for) and Tab queues a follow-up that is sent when the turn would end. Both
wait under the transcript. Esc stops the turn and puts them back in the input
(a plugin may keep them for the next turn instead); what the reply had
streamed stays in the transcript, saved as stopped, and is not sent to the
model again. Messages still waiting when the agent exits come back when the
session is resumed.

`@path` to a PNG, JPEG, GIF or WebP file attaches the image when the model
reads images. Ctrl+V pastes the clipboard's image (through `wl-paste`, `xclip`
or `pngpaste`) as such a path, and a dropped image file becomes one too.

Answers are drawn as markdown, edits as diffs, and each built-in tool's call
in its own way, always whole: every call's arguments and result, every diff
and every note, nothing cut or folded. A call's row is tinted by where it
stands (waiting, running, done, failed). Code is highlighted in the terminal's
own palette, so it follows the terminal's theme. PageUp, PageDown and
Shift+Up/Down scroll the transcript. With `NO_COLOR` set, nothing is drawn in
color. A plugin can draw its own tools' calls with `app.add_tool_renderer`
(`rig_tui::AppToolRenderersExt`).

## Sessions

Each `rig` starts a new session. `/quit` ends it, `/new` starts a new one and
`/name` names it. `/resume` lists the earlier sessions (name or first
message, cost, directory, age) and resumes the one picked, in its own
directory; `rig --resume <id or name>` (or `/resume <id or name>`) resumes a
given one, such as after a crash.

The session is written as it happens. Each agent, subagents included, has a
log, `sessions/<id>/logs/<agent-id>.jsonl`, of its messages, settings, usage
and compactions, appended to as it runs and rewritten only through
rig-memory (a plugin's redaction, say, keeps each record's place and time).
Images are stored once in `files/blobs/`, and `files/meta.json` caches what
`/resume` lists. An older session format is resumed and written on in its
own; one from before `rig-session/2` is refused, unchanged.

A tool call that may change something starts only once the reply that asked
for it, and a mark that it started, are on disk. After a crash, a closed
terminal or a `/quit` mid-turn, resuming the session reads the logs back and
settles what was left half done: a tool call that may change something and
had started is answered as interrupted by the restart (or with its result, if
it finished before the crash), and a plugin may answer it first; any other
unfinished call, one still waiting for your approval included, goes through
the permission checks again and runs; and a turn that waited on the model
carries on.

## Context, compaction and retries

The system prompt is the same on every request of a session, so the
provider's prompt cache keeps the whole conversation behind it: no date or
tool rules in it, and each tool says in its own description when to pick it.
Models whose catalog entry lists a prompt cache get it, with the agent's id
as the cache key where the provider takes one.

Two default plugins add to it when the agent starts. `AgentsMdPlugin` adds
the `AGENTS.md` files of the working directory and of each directory above
it up to the repository root (the nearest holding `.git`), nearest last,
after your own `RIG_HOME/AGENTS.md`. `CLAUDE.md` is not read; a repository
that has only one can link `AGENTS.md` to it. `SkillsPlugin` lists your
skills: each directory with a `SKILL.md` (the Agent Skills format, a `name`
and a `description` in its frontmatter) under `.rig/skills` or
`.agents/skills` of those same directories, `RIG_HOME/skills` or
`~/.agents/skills`. The model reads a skill's file when a task matches its
description, and `/skill:<name> [text]` sends one with your message. A skill
with `disable-model-invocation: true` is left out of the list and only sent
by hand. Both are read once, so an edit takes effect on `/reload` or `/new`.

When the conversation comes within 16k tokens of the model's window, older
tool outputs are cleared and, if that is not enough, the older messages are
summarized by the model into a structured checkpoint (goal, progress,
decisions, next steps, files read and changed) that requests send in their
place; the newest 20k tokens stay as they are. When the conversation outgrows
the window anyway, older tool outputs are cleared and the call is sent again.
`/compact` compacts now, and `/compact <focus>` says what the summary should
keep. The summarized messages stay in the transcript, under a line that shows
the summary; a resumed session starts from the messages the summary kept.

A model call that fails on a rate limit, an overloaded provider or a dropped
connection is retried up to four times, waiting as long as the provider's
`Retry-After` asks (or 2, 4, 8, 16 seconds). If it still fails, your message
stays in the conversation and `/retry` sends it again.

## Tools

The default tools are `read`, `edit`, `write` and `shell`, each its own
plugin. A `shell` call runs until its command ends, unless the model sets
`timeout_secs` or you interrupt it; either stops the command, on a remote
host too. The whole output
of a `shell` call that was cut is kept in `RIG_HOME/spill/`, and the model
reads it by the path the cut output names.

With `rig_basics::checkpoint::CheckpointPlugin` enabled (it is listed
disabled by default), every `shell` call, and any other call that may write
anywhere, reports the files it changed, from snapshots of the project before
and after it kept in the session's scratch directory, never in the
repository's `.git`. `[checkpoints] enabled = false` in
`RIG_HOME/settings.toml` turns that off.

With `[remote.hosts]` in `RIG_HOME/settings.toml`, such as
`bench = ["koh", "exec", "bench", "--sh"]` or `box = ["ssh", "dev@box"]`, a
path written `HOST:PATH` (`bench:~/app/src/lib.rs`) is that host's for every
tool: `read`, `edit`, `write` and `search` act on the file there, and `shell`
with `cwd: "box:~/app"` runs the command there, through the host's command,
while other paths stay on this machine
([rig-basics](../plugins/rig-basics/README.md)).

## Subagents

With `rig_basics::subagents::SubagentsPlugin` enabled (it is listed disabled
by default; `/plugins` or `rig plugin enable
rig_basics::subagents::SubagentsPlugin` enables it), the model can hand work
to subagents with the `task` tool. Each is a new agent in the same process,
with its own conversation and, if the call asks, another model, reasoning
setting or a subset of the tools. `task` returns the subagent's id at once,
and `message` sends one of the model's own subagents a follow-up, which it
reads with its conversation kept.

Each `task` or `message` call is a request, and exactly one report for it
(done, failed or stopped) arrives later as a message to the agent that asked,
which starts a turn of an idle agent or follows the running one (a stopped
subagent's report is a note that starts no turn); the reports of one reply's
`task` calls reach it together. Nothing waits for them, so you can keep
talking to the main agent, or steer it, meanwhile. An agent that needs a
message before it goes on calls `wait`, which keeps its turn open until that
agent sends it one, and a subagent whose turn ends while a request it made is
open is waiting, not done: it reports the answer it gives once that request
is answered.

Esc stops the shown agent's turn and the turns of every subagent below it,
even when the shown agent is idle: stopping follows who started whom.
`/agents` (`rig_basics::sessions::agents::AgentsPlugin`) lists every agent
with its model and state, and shows the one picked: its transcript, and what
you type then goes to it. A subagent can start subagents of its own, one
level deep. Subagents started with `peers` can also send each other requests
with `message` and `wait` for each other; a request or `wait` that would make
agents wait on each other is refused. In the effect log, a subagent's model
calls name the call that gave it its work as their parent.

## Without the terminal view

`rig -p "fix the failing test"` answers one prompt and exits: the answer goes
to stdout, failures to stderr, and the exit code is 0 when the turn ended with
an answer, 2 for a refused slash command. Text piped in follows the prompt
(`git diff | rig -p "review this"`), and piped in without `-p` it is the
prompt: `rig` runs the terminal view only on an interactive terminal.

`-r <id or name>` resumes a given session instead of starting a new one; it
goes wherever an option goes, but not among a command's words or after `--`,
and `rig -r <id or name> <command>` runs a plugin's command in that session.
`rig -r <id or name> -p` with no prompt finishes the turn the session left
unfinished, such as one a crash cut short, and prints its answer; with nothing
to finish it exits 2. A plugin may keep such a run going, sending the agent
work over time (`KeepRunning`); each answer is then printed as it comes.

To drive rig from a script or another agent, over pipes and nothing else,
`rig -p --json "task"` writes the run as JSON lines, ending with an `exit`
line that carries the final answer, and `rig --json` stays up, one request
per line on stdin and one event per line on stdout, until stdin closes.
`rig help-agents` is the whole protocol, with a Python driver. The optional
`rig_basics::helpers::HelpersPlugin` gives the model the `helper` tool, which
hands tasks to such one-shot rig children (one, up to eight side by side, or a
chain) and returns their final answers.

## Reload and builds

`/reload` rebuilds the agent and restarts it on the same session. Typed while
a turn runs, it waits until no turn runs (`/reload cancel` drops it), and a
turn started during the build delays the restart until it ends. It shows
cargo's latest line, keeps the current build running if the new one does not
compile (its error notice names the build log), and rolls back to it if the
new one crashes during startup.

The agent can ask for a reload itself with its `reload` tool, which waits the
same way and shows a notice. A reload the model asked for reports back: one
message that starts a turn says the new build started, carries the build's
errors, or says the new build was rolled back, so the model carries on without
a second prompt. It may ask for at most 3 reloads per message of yours, and
Esc or `/reload cancel` drops the report. A `-p` run whose model asks for a
reload waits for it and carries on on the new build too.

Every build, the one before each start and `/reload`'s, writes its whole
output (the launcher's and cargo's) to `RIG_HOME/build.log`, and a failure
message names its reason and first compiler error. The agent sees build
failures too: the reason, the first errors and the paths of `build.log`,
`plugins.toml` and the generated project go into its conversation as a message
from the `build` plugin, which starts no turn when the agent is idle (unless
the model asked for that reload) and is saved with the session. Ask it why the
build failed and it can read the log, fix the cause, and reload.

`rig build` regenerates and builds the agent without starting it.
`CARGO_BUILD_JOBS` sets cargo's `-j` for these builds, as for any cargo build.

## Plugins

Plugins are Bevy plugins, listed in `$RIG_HOME/plugins.toml`. Everything but
the harness (the session and the launcher protocol) is an entry in that list:
the project context, models, sign-in, sessions, compaction, usage, activity,
each tool, `@path` attachments, `/reload`, `/plugins`, the commands,
`--print` and the terminal view, each from a plugin crate in
[`plugins/`](../plugins), most from [`rig-basics`](../plugins/rig-basics), so
any of them can be removed or replaced
([`crates/rig-harness`](../crates/rig-harness/README.md) lists them):

```toml
[[plugin]]
crate = "rig-basics"              # one of rig's own plugin crates: no source
plugin = "rig_basics::coding_tools::ReadTool"

[[plugin]]
crate = "rig-tui"
plugin = "rig_tui::TuiPlugin"

[[plugin]]
crate = "hello"                   # the package name
path = "plugins/hello"            # relative to plugins.toml; or git = "..." (branch, rev), or version = "..."
plugin = "hello::HelloPlugin"     # implements Plugin + Default
```

`rig plugin new hello` makes that crate in `$RIG_HOME/plugins/hello`, outside
any workspace, and adds its entry; `rig plugin add <type> --path <dir>` (or
`--git`, `--version`) adds an entry for an existing crate, and `rig plugin
remove <type>` takes one out (its crate stays), each checked before
`plugins.toml` is written. A commented-out entry is a disabled plugin: the
template lists every plugin rig ships, the ones not on by default commented
out. `rig plugin list` shows each entry, `on` or `off`, with its crate and the
comment above it, and `rig plugin enable <type>` and `disable <type>`
uncomment and comment out an entry, keeping every other line. In the agent,
`/plugins` is a picker of the same list: tick and untick plugins, press Enter,
and the agent rebuilds and restarts at once (without a terminal it prints the
list).

`rig plugin check` checks the list. With `--build` it also builds the agent
with its plugins in `$RIG_HOME/target` without staging it, and names the
plugins whose systems in one agent-loop stage use the same data with no order
between them; a debug build of the agent warns of those at start.

A plugin crate depends on `rig-harness`, and on another plugin crate whose
types it uses (such as `rig-tui` for a terminal panel), and registers tools,
slash commands, tool renderers, terminal panels or a window the way the
built-in ones do, never by editing rig's crates.
[`crates/rig-harness/PLUGINS.md`](../crates/rig-harness/PLUGINS.md) is the
plugin guide, with a copy-ready example of each kind, its names from
`rig_harness::prelude` and the plugin crates, and the `src/lib.rs` that `rig
plugin new` writes is a working, commented slash command. An agent started by
the launcher knows its plugins, commands and tools, and how to write and add
its own plugins, from its system prompt.

The optional [`rig-inspect`](../plugins/rig-inspect) plugin adds the `inspect`
tool, with which the agent reads its own Bevy world, read-only.

Code mode is the optional [`rig-steel`](../plugins/rig-steel) plugin: a
`run_steel` tool whose [Steel](https://github.com/mattwparas/steel) (Scheme)
program spawns agents, messages them and calls tools, in a sandbox. Enable it
with:

```toml
[[plugin]]
crate = "rig-steel"
plugin = "rig_steel::SteelPlugin"
```

A fan-out that asks three agents at once and keeps the shortest answer:

```scheme
(define agents (map (lambda (i) (spawn-agent (string-append "solver-" (number->string i)))) (range 0 3)))
(define requests (map (lambda (agent) (send agent "How does src/lib.rs load plugins? Three sentences.")) agents))
(define answers (map reply requests))
(foldl (lambda (answer best) (if (< (string-length answer) (string-length best)) answer best))
       (car answers) (cdr answers))
```

## Files

Every file lives under `RIG_HOME` (default `~/.rig`), and several `rig`
processes can share one:

| Path | What it holds |
| --- | --- |
| `plugins.toml` | The plugin list. |
| `settings.toml` | Settings, such as `[shell]`, `[checkpoints]` and `[remote.hosts]`. |
| `auth/` | `/login`'s credentials: `chatgpt.json` and the API keys in `keys/`. |
| `project/`, `target/`, `bin/` | The generated agent project, cargo's build of it (a few gigabytes), and the builds. |
| `build.log` | The last build's whole output. |
| `defaults.json` | The last chosen model and reasoning, which a new session starts with. |
| `data/` | Plugins' data across sessions, such as the prompt history `data/tui/history.jsonl`. |
| `projects/` | Plugins' data per project. |
| `sessions/<id>/` | A session: the agent logs in `logs/`, the name `/name` gave it in `name`, and `files/` (`meta.json`, the images in `blobs/`, the effect log `effects.jsonl` with `EffectLogPlugin`, plugins' files). |
| `spill/` | The whole output of a `shell` call that was cut. |
| `agent.log` | The agent's log. |

Set `RIG_HOME` to put everything elsewhere, for example under a cache
directory.

## Running from a checkout

To run the agent from a rig checkout instead of crates.io, install the
launcher from it or point `RIG_SOURCE` at it:

```bash
cd rig && cargo install --path . --root /some/dir
RIG_HOME=/some/dir/home RIG_SOURCE=$PWD /some/dir/bin/rig
```

---

--- METRICS ---
- Files Extracted: 48
- Estimated Token Budget: ~70676 tokens
- Canonical Reference: https://codewiki.google/github.com/0xPlaygrounds/rig
