# Agents Source: https://anyagent.mintlify.app/agents The catalog, what each agent can do, per-agent quirks, and how to connect one that isn't listed. anyagent ships a catalog of known agents. Each entry says how to find the CLI, how to launch it in protocol mode, and the quirks verified against a real install. Guessed flags never ship. ## Catalog | Agent | id | CLI | Wire | Install | | ------------ | ------------- | ------------------------ | ----------------------------------------- | ------------------------------------------------------------------------- | | Claude Code | `claude` | `claude` | native (stream-json) | `npm install -g @anthropic-ai/claude-code` | | Codex | `codex` | `codex` | native (app-server) | `npm install -g @openai/codex` | | opencode | `opencode` | `opencode` | native (HTTP + SSE) | `brew install sst/tap/opencode` | | pi | `pi` | `pi` | native (pi RPC) | `npm install -g @earendil-works/pi-coding-agent` | | Antigravity | `antigravity` | `agy`, or its ACP server | native (stream-json), ACP with the server | [antigravity.google](https://antigravity.google), then `agy install` | | Cursor CLI | `cursor` | `cursor-agent` | ACP | `curl https://cursor.com/install -fsS \| bash` | | Grok | `grok` | `grok` | ACP | `npm install -g @xai-official/grok` | | Hermes Agent | `hermes` | `hermes` | ACP | [NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent) | | Kiro CLI | `kiro` | `kiro-cli` | ACP | [kiro.dev](https://kiro.dev) | | Qwen Code | `qwen` | `qwen` | ACP | `npm install -g @qwen-code/qwen-code` | Native wires are driven directly by an adapter written for that agent. ACP agents share one adapter that speaks the [Agent Client Protocol](https://agentclientprotocol.com). ## Capability matrix What each adapter reports after `open`. Read it from `session.info().details.capabilities` at runtime; never hardcode this table. | Capability | claude | codex | opencode | pi | antigravity (agy) | ACP agents | | --------------- | :----: | :---: | :------: | :-: | :---------------: | :------------: | | `Images` | ✓ | ✓ | ✓ | ✓ | | handshake | | `Resume` | ✓ | ✓ | ✓ | ✓ | ✓ | handshake | | `Steer` | | ✓ | | ✓ | | handshake | | `Permissions` | ✓ | ✓ | ✓ | | | ✓ | | `Questions` | ✓ | | ✓ | ✓ | | on first sight | | `Rollback` | ✓ | ✓ | ✓ | | | | | `RollbackFiles` | ✓ | | | | | | | `Fork` | ✓ | ✓ | ✓ | | | | | `Compact` | ✓ | ✓ | ✓ | ✓ | | | | `SlashCommands` | ✓ | ✓ | ✓ | ✓ | | ✓ | | `Plan` | ✓ | ✓ | ✓ | | | ✓ | | `Subagents` | ✓ | ✓ | ✓ | | | | | `ContextUsage` | ✓ | ✓ | ✓ | ✓ | ✓ | on first sight | | `PlanUsage` | ✓ | ✓ | | | | | * **handshake**: the ACP `initialize` response says whether the agent takes images, loads sessions, and accepts steering. Cursor, for example, reports images and resume; Antigravity's server reports no steer. * **on first sight**: ACP has no handshake flag for these. The capability is added, with a `SessionUpdated`, the first time the agent sends one. Every gate holds the same way: an unsupported call fails typed with `UnsupportedFeature` instead of misbehaving. ## Config options by agent | Agent | `model` | `effort` | `fast` | `mode` | other | | ----------------- | :------------------: | :-----------------------------------------------: | :------------------------------: | :-----------------------------------: | -------------------------------------- | | claude | live | live, from the model | live, when the model supports it | | | | codex | live | live, from its catalog | live | | `sandbox`, approval policy | | opencode | live (`provider/id`) | live, as the model's `variant` | | | | | pi | live | live, thinking levels | | | | | antigravity (agy) | at open only | | | at open only (`accept-edits`, `plan`) | reopen with the resume token to change | | antigravity (ACP) | live | | | live | Gemini models only | | cursor | live | per model | per model | live (`agent`, `plan`, `ask`) | `thinking`, `context` per model | | grok | live | live, with grok's labels | | | | | kiro | live | live (`low` … `max`), absent on models without it | | | | | hermes, qwen | live | none | | | | ## Per-agent notes The fullest native adapter. `fast` appears when the selected model reports `supportsFastMode`; switching it resumes the process under the hood with the same `Session` handle and conversation. Rollback can restore files. Opts into the app-server's experimental API. Approval policy and sandbox switch live. Images ride as `localImage` items. Client MCP servers ride as launch overrides (stdio and HTTP; no SSE). `rollback` maps to `thread/revert`; files are never restored. Questions are translated defensively but the capability stays off until observed live. anyagent starts `opencode serve` on a private localhost port per session, gated by a per-session secret, and drives its HTTP + SSE wire. Task-tool child sessions nest as subagents. No steer (mid-turn prompts queue) and no plan quota. Pass a model as `configure("model", "provider/id")`; opencode's free `opencode/…` models need no key. To drive it over ACP instead: `AgentInstallation::acp("opencode", path, ["acp"])`. Provider-key based: log in with `/login ` inside the CLI, or set the provider's API-key variable. `generate` launches with `--no-tools --no-session`, so it is text-only and nothing is saved. Discovery picks the richer wire that is installed: ```text theme={"theme":"vesper"} discover("antigravity") ├─ ACP server found ──► open over ACP: permissions, questions, │ in-process cancel, live mode and model switch, │ images, PDFs, diffs, slash commands │ (Gemini models only) └─ only `agy` found ──► open over agy's headless stream-json wire: text, tools, resume, context usage, cancel by respawn; every model `agy models` lists → installation.upgrade names the server ``` The headless `agy` wire cannot prompt: in `Ask` mode every permission-gated tool is denied by the agent itself (the tool shows as failed), `AutoApprove` launches with `--dangerously-skip-permissions`, questions are skipped with a diagnostic, and a mid-turn prompt queues. Cancel kills the process and resumes the conversation in a fresh one, about two seconds. The ACP server is Google's own (`antigravity-acp` in the [ACP registry](https://github.com/agentclientprotocol/registry/blob/main/antigravity-acp/agent.json), a 314 MB download). It reuses the `agy` login: anyagent calls its `authenticate` once per open (about 2 s, no browser), or set `auth.type` in `~/.gemini/antigravity-acp/settings.json` to skip that. When only `agy` is found, the installation carries `upgrade: Some(MissingAgent)` with the download hint: ```rust theme={"theme":"vesper"} if let Some(upgrade) = &agent.upgrade { ui.offer_install(&upgrade.name, &upgrade.install_hint); } ``` Force the headless CLI with `ANYAGENT_ANTIGRAVITY_BIN=~/.local/bin/agy`. Google's terms restrict third-party tools driving a personal Antigravity login; the ACP server is the sanctioned path. Surface that choice to users. Runs over `cursor-agent acp`. Permission requests gate shell commands outside its allowlist; file edits never ask, so sandbox accordingly. Before the handshake anyagent runs `cursor-agent about` for the version and account and fails typed with `cursor-agent login` when logged out. A model switch adopts that model's own options (`effort`, `fast`, `thinking`, `context`). Cursor's extension requests are answered: `ask_question` becomes a `Question`, `update_todos` drives the plan, `task` (a subagent run) is surfaced as a header only. Cursor reports no context usage, so each turn ends with an estimate (4 chars ≈ 1 token) labelled `anyagent/estimated` in extensions. Its credential lives in a sqlite row that exists logged out too, so discovery reports `Unknown` and `probe` answers for real. `effort` is exposed as a live option even though ACP advertises none: the adapter reads the level from kiro's metadata and switches it by running `/effort ` as a hidden prompt between turns. Only its file tools ask permission. Its terminal tool runs without asking, so a denied write does not stop it from writing via shell. Hermes never sends tool status updates. No `effort` option. Plain ACP. Grok exposes `effort` from the model's `reasoningEfforts`; qwen exposes none. Both log in through their own TUI. Compaction is native-adapter only. ACP has no compaction in schema 1.7, so ACP agents refuse with `UnsupportedFeature`. ## Any other ACP agent No catalog entry needed. Build the installation yourself: ```rust theme={"theme":"vesper"} use anyagent::{AgentInstallation, Runtime, SessionOptions}; let agent = AgentInstallation::acp( "my-agent", // display name "/usr/local/bin/my-agent", // executable vec!["acp".into()], // args that put it in ACP mode ); let (session, events) = Runtime::new().open(&agent, SessionOptions::in_dir(".")).await?; ``` Events, requests, and capabilities are identical downstream. What you lose is the catalog's verified quirks: launch flags, login commands, auth error hints. ## Pointing at a specific binary Every catalog entry reads `ANYAGENT__BIN` first: ```bash theme={"theme":"vesper"} ANYAGENT_CLAUDE_BIN=/path/to/claude cargo run --example probe ``` Or skip discovery with `AgentInstallation::at(id, executable)`. ## Windows Every catalog agent ships for Windows and every installer below puts its directory on `PATH`, so discovery finds them without extra configuration. npm installs are `.cmd` shims; discovery skips the bare bash shim next to them. Cursor's Windows installer is `irm 'https://cursor.com/install?win32=true' | iex`. | Agent | Installed via | Found at | | ------------------------------------ | ---------------- | ---------------------------------------------- | | Claude Code | native installer | `%USERPROFILE%\.local\bin\claude.exe` | | Codex, Grok, pi, Qwen Code, opencode | npm | `%APPDATA%\npm\.cmd` | | Antigravity | `agy install` | `%LOCALAPPDATA%\agy\bin\agy.exe` | | Cursor CLI | installer | `%LOCALAPPDATA%\cursor-agent\cursor-agent.cmd` | | Hermes Agent | installer | `%LOCALAPPDATA%\hermes\bin\hermes.exe` | | Kiro CLI | installer | `%LOCALAPPDATA%\Kiro-Cli\kiro-cli.exe` | ## Several logins of the same agent ```rust theme={"theme":"vesper"} SessionOptions::in_dir(".").config_home("/path/to/work-account") ``` anyagent sets the agent's own config-home variable (`CLAUDE_CONFIG_DIR`, `CODEX_HOME`, …) for that process. An agent with no such variable fails with `InvalidConfiguration`; an isolation request is never silently dropped. # Architecture Source: https://anyagent.mintlify.app/architecture How anyagent is built: three layers, what each one owns, and how one prompt travels through them. ## TLDR anyagent has three layers. Your app talks to the top one. The bottom one talks to the agent process. The middle one is where every rule lives. ```text theme={"theme":"vesper"} ┌───────────────────────────────────────────────────────────┐ │ 1. Public API Runtime · Session · Events │ what your app sees ├───────────────────────────────────────────────────────────┤ │ 2. Engine one task per session │ the rules: turns, queue, │ (src/session.rs) │ requests, status, cleanup ├───────────────────────────────────────────────────────────┤ │ 3. Adapters claude · codex · opencode · pi · │ one protocol each, │ antigravity · acp │ zero rules └───────────────────────────────────────────────────────────┘ │ agent process (stdio or HTTP) ``` The one idea to keep: **adapters translate, the engine decides.** An adapter turns wire frames into a small vocabulary and back. It never decides when a turn ended, whether a prompt should steer or queue, or which requests are still open. The engine does all of that once, the same way for every agent. That is why every agent behaves identically through anyagent. ## Codebase map ```text theme={"theme":"vesper"} src/ ├── lib.rs public exports ├── runtime.rs layer 1: Runtime (discover, probe, open, generate, plan_usage) ├── session.rs layer 2: the engine, plus Session and Events handles ├── event.rs the public event and request types ├── agent.rs AgentInstallation, AgentDetails, Capability, SessionOptions ├── error.rs AgentError ├── catalog.rs per-agent facts as data: ids, launch flags, login commands ├── discovery.rs finds executables; never launches ├── process.rs spawns children, guarantees cleanup └── adapter/ layer 3 ├── mod.rs the seam: Adapter trait, DriverCommand, DriverEvent ├── claude.rs native stream-json ├── codex.rs native app-server JSON-RPC ├── opencode.rs native HTTP + SSE against `opencode serve` ├── pi.rs native pi RPC ├── antigravity.rs native agy headless stream-json ├── acp.rs one adapter for every ACP agent ├── wire.rs line-delimited JSON over stdio, plus the recorder ├── attach.rs attachment loading shared by adapters ├── mock.rs scripted adapter (feature `mock`) └── conformance.rs engine contract tests, run over the mock ``` | Layer | File | Owns | Never does | | ---------- | -------------- | -------------------------------------------------------------------------- | ----------------------------------- | | Public API | `runtime.rs` | catalog, discovery, one adapter per agent, opening sessions | turn logic | | Engine | `session.rs` | start a turn, steer or queue, request lifetimes, turn end, status, cleanup | parse a wire, know an agent's flags | | Adapter | `adapter/*.rs` | launch, handshake, translate frames both ways | decide anything about turns | ## The seam between engine and adapter The engine and an adapter talk through two private enums. This is the whole contract an adapter has to meet. ```text theme={"theme":"vesper"} DriverCommand (engine → adapter) ────────────────────────────────► Engine StartTurn · Steer · Answer · Configure Adapter Rollback · Compact · Cancel · Close ◄──────────────────────────────── DriverEvent (adapter → engine) Event { kind, parent_tool_id, extensions } TurnAck · TurnEnded · Steered · InfoChanged AuthLost · Exited ``` | `DriverEvent` | The adapter is saying | | -------------------- | ----------------------------------------------------------------- | | `Event { kind, .. }` | "the agent produced this content" (text, tool, request, usage) | | `TurnAck` | "I've started the turn you asked for; what follows belongs to it" | | `TurnEnded(reason)` | "the wire says the turn is over" | | `Steered(bool)` | "your steer was accepted" or "rejected, requeue it" | | `InfoChanged` | "the agent changed its settings or capabilities" | | `AuthLost` | "credentials stopped working mid-session" | | `Exited` | "the process died; here is stderr" | An adapter never sends `TurnStarted`, `TurnEnded`, `RequestClosed`, `SessionUpdated`, or `StatusChanged` as content. The engine owns those and drops any attempt. `Adapter::connect` is the only other thing an adapter implements. It launches the process, does the handshake, and returns a `DriverInfo`: the agent's details and capabilities, the initial configuration, the resume token, and whether this wire ends turns with its own frame. ## One prompt, end to end ```text theme={"theme":"vesper"} your app session.prompt("fix the bug") │ ▼ Session Command::Prompt ─────────────────────────────┐ ▼ Engine idle? → DriverCommand::StartTurn ─┐ running+Steer? → DriverCommand::Steer │ running, no? → push onto queue, reply Queued ▼ Adapter builds the wire frame, writes it ──────► agent │ Adapter reads frames back ◄──────────────────────────┘ translates each into a DriverEvent: TurnAck Event(TextDelta) … Event(RequestOpened) ← engine remembers the id Event(ToolUpdated) … TurnEnded(Completed) Engine stamps sequence, session_id, turn_info emits TurnStarted → … → TurnEnded emits StatusChanged only when the state flips then: promote the next queued prompt, if any │ ▼ Events your app's match on EventKind ``` Three things happen in the engine that no adapter has to think about: 1. **Attribution.** Frames that arrive before `TurnAck` belong to the previous turn and are dropped instead of leaking into the new one. 2. **Request tracking.** Every `RequestOpened` is remembered until your answer or a cancel closes it, so `NeedsInput` and `RequestClosed` are always right. An answer the request did not offer is rejected without touching the wire. 3. **Promotion.** When a turn ends and the queue is not empty, the next prompt starts immediately, and the status stays `Working` without flashing `Idle`. ## How a turn ends Not every wire says "done". The engine handles both cases and tells you which one you got through `CompletionSource`. | Wire | How the engine ends the turn | You see | | ----------------------- | ---------------------------------------------------------------------------------- | -------------------------------- | | Sends its own end frame | On that frame | `Completed { source: Protocol }` | | Does not | After the quiet window passes with no frames, no running tool, and no open request | `Completed { source: Inferred }` | Every shipped adapter ends prompted turns with its own frame. ACP agents have no frame for agent-originated turns (the agent waking itself after background work), so those are inferred. Frames that arrive after the end and carry no new work (a late tool status, a usage update) are applied without a turn. A subagent's end frame never ends its parent's turn. ## Native vs ACP There are two kinds of adapter. The choice per agent is about what the wire can express. | | Native adapter | ACP adapter | | -------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | | Agents | claude, codex, opencode, pi, antigravity (agy) | cursor, grok, hermes, kiro, qwen, antigravity server, any custom agent | | What it speaks | the agent's own protocol | the shared [Agent Client Protocol](https://agentclientprotocol.com) | | Capabilities | fixed in the adapter, verified live | read from the `initialize` handshake, plus some added on first sight | | Why | the agent's own wire exposes more: rollback, fork, compact, plan usage, subagents | one adapter covers every ACP agent for free | | Cost | one file per agent to keep in step with upstream | capped at what ACP schema 1.7 offers | When an agent has both wires installed, discovery prefers the richer one. ## Adding an agent | Step | Where | What | | ---- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | 1 | `src/catalog.rs` | A profile: id, executable names, args for protocol mode, login command, install hint, config-home env var. Only flags verified against a real install | | 2 | `src/adapter/.rs` | Implement `Adapter::connect`. Translate frames into `DriverEvent`s. Report only capabilities you have seen work | | 3 | `src/runtime.rs` | Register the adapter in `Runtime::new` | | 4 | `tests/.rs`, `tests/fixtures//` | Recorded frames so `cargo test` covers it offline | | 5 | `tests/live.rs` | Add the harness to the roster so the live matrix runs against it | For an ACP agent, skip step 2: the shared adapter already speaks it. A catalog entry is only needed for verified quirks and login commands. The conformance suite in `src/adapter/conformance.rs` runs the engine over the mock adapter and pins the contract every adapter benefits from: one `TurnEnded` per turn, steer or queue, request lifetimes, cancel, close. Because the rules live in the engine, a new adapter gets them without writing any. # Building an app Source: https://anyagent.mintlify.app/building-an-app A step-by-step walkthrough of a real chat app on anyagent: pick an agent, log in, open a session, stream, answer requests, persist, show usage. This is the shape of an app like Comet, laptop-agent, or T3 Code: a thread list on the left, a chat on the right, a settings menu, and a usage page. Follow the steps in order; each one is a few lines of glue over the [Core API](/core-api). ```text theme={"theme":"vesper"} ┌──────────────── your app ────────────────┐ │ thread list │ chat view │ settings │ │ badges │ text, tools, │ model, │ │ │ permissions │ effort │ └──────┬──────┴───────▲────────┴─────┬─────┘ │ │ │ StatusChanged Events configure() │ │ │ └──────► Session ◄────────────┘ │ agent process ``` ## 1. Find the agents and pick one `discover` is instant and gives you both lists: installed, and missing with install hints. Show both. ```rust theme={"theme":"vesper"} let runtime = Runtime::new(); runtime.prewarm(); // start scanning while the window opens let report = runtime.discover().await; for agent in &report.agents { ui.agent_row(&agent.name); // login state comes from probe, not discovery } for missing in &report.missing { ui.install_row(&missing.name, &missing.install_hint); } let agent = report.require(&chosen_id)?.clone(); ``` ## 2. Check login, show the login command `probe` asks the agent itself. If it is logged out you get the exact command or env var to show; anyagent never runs a login flow. ```rust theme={"theme":"vesper"} let details = runtime.probe(&agent).await?; if let AuthStatus::Unauthenticated { login } = &details.auth { for method in login { match method { LoginMethod::Terminal { command, description, .. } => ui.login_button(command, description), LoginMethod::EnvVar { name } => ui.env_hint(name), _ => {} } } return Ok(()); } ``` Keep `details`: its `capabilities` and `config_options` drive the next steps. ## 3. Open a session Build the options from the thread's state. This is the whole decision table an app needs: ```rust theme={"theme":"vesper"} let mut options = SessionOptions::in_dir(&thread.repo_dir); if let Some(token) = &thread.resume_token { options = options.resume(token.clone()); // continue an old conversation } if thread.unattended { options = options.permission_mode(PermissionMode::AutoApprove); } if let Some(model) = &thread.model { options = options.configure("model", model.as_str()); } let (session, events) = match runtime.open(&agent, options).await { Ok(pair) => pair, Err(AgentError::ResumeFailed(_)) => { // the token went stale: start fresh, keep your transcript runtime.open(&agent, SessionOptions::in_dir(&thread.repo_dir)).await? } Err(AgentError::AuthRequired { login }) => return ui.show_login(login), Err(e) => return Err(e.into()), }; thread.save_info(session.info()); // resume token, capabilities, options ``` ## 4. One task per session, persist before render Hand `Events` to its own task. Keep a `Session` clone wherever the UI sends commands. Store every event first; replay needs nothing else because each carries `sequence` and `occurred_at`. ```rust theme={"theme":"vesper"} let thread_id = thread.id; let ui = ui.clone(); tokio::spawn(async move { while let Some(event) = events.next().await { let event = match event { Ok(e) => e, Err(err) => { ui.thread_failed(thread_id, err); break } }; store.append(thread_id, &event); render(&ui, thread_id, event); } ui.thread_closed(thread_id); // the stream ended: close() or the agent died }); ``` ## 5. Render the events One `match` covers every agent. Skip nested events for the main transcript and render them under their parent tool. ```rust theme={"theme":"vesper"} fn render(ui: &Ui, thread: ThreadId, event: Event) { let nested = event.turn_info.as_ref().is_some_and(|t| t.parent_tool_id.is_some()); match event.kind { // content EventKind::TextDelta { message_id, text } if !nested => ui.append(thread, message_id, text), EventKind::ReasoningDelta { text, .. } if !nested => ui.append_thinking(thread, text), EventKind::MessageEnded { message_id } => ui.finish_message(thread, message_id), // tools: ToolUpdated is a full snapshot, replace by id EventKind::ToolUpdated(tool) => ui.upsert_tool(thread, event.turn_info, tool), EventKind::ToolOutputDelta { tool_id, text } => ui.append_tool_output(thread, tool_id, text), EventKind::PlanUpdated { entries } => ui.set_plan(thread, entries), // replaces, not appends // requests: step 6 EventKind::RequestOpened(request) => ui.open_request(thread, request), EventKind::RequestClosed { request_id } => ui.close_request(thread, request_id), // session state EventKind::StatusChanged(status) => ui.set_badge(thread, status), EventKind::SessionUpdated(info) => { store.save_info(thread, &info); ui.rebuild_settings(thread, &info) } EventKind::ContextUsage { used_tokens, window_tokens, .. } => ui.set_gauge(thread, used_tokens, window_tokens), // turn boundaries EventKind::TurnStarted { .. } => ui.start_turn(thread), EventKind::TurnEnded { stop, background } => ui.end_turn(thread, stop, background), EventKind::Diagnostic(d) => tracing::warn!(?d), _ => {} } } ``` | `StopReason` | Show | | -------------------------------- | --------------------------------------------------------------------- | | `Completed { source: Protocol }` | done | | `Completed { source: Inferred }` | idle. anyagent stopped waiting; the agent did not say it was finished | | `Cancelled` | stopped | | `Refused` | the agent declined | | `Failed { message }` | error, with the message | `background` lists tools still running after the turn (subagents, backgrounded shells). Their completion usually shows up as a later turn with `TurnOrigin::Agent`. ## 6. Permissions and questions Render exactly the choices the agent offered. Answer once. Clear the dialog on `RequestClosed`, which also fires when a cancelled turn withdraws the request. ```rust theme={"theme":"vesper"} fn open_request(ui: &Ui, session: Session, request: Request) { match request { Request::Permission(r) => { // r.tool is the ToolUpdate awaiting approval (title, input, diffs) ui.permission_dialog(r.tool, r.detail, r.options, move |choice| { let session = session.clone(); async move { session.answer(r.id.clone(), Answer::Permission(choice)).await } }); } Request::Question(r) => { // one answer per question, in order; choices or free text per q.allows_free_text ui.question_form(r.questions, move |answers: Vec| { let session = session.clone(); async move { session.answer(r.id.clone(), Answer::Question(answers)).await } }); } } } ``` While a request is open the thread's status is `NeedsInput`, so the thread list badge comes for free from step 5. ## 7. Send, steer, cancel The send box calls `prompt` no matter what the session is doing. The `Delivery` tells you whether it started a turn, steered the running one, or queued. ```rust theme={"theme":"vesper"} async fn send(session: &Session, text: String, files: Vec) -> Result<(), AgentError> { let mut input = Input::text(text); for f in files { input = input.attach(f); } match session.prompt(input).await?.kind { DeliveryKind::Started { .. } | DeliveryKind::Steered { .. } => {} DeliveryKind::Queued { position } => ui.show_queued(position), } Ok(()) } // stop button session.cancel(false).await?; // true also drops queued prompts ``` Show attachments inline only when `capabilities.supports(Capability::Images)`; otherwise the agent reads them by path. ## 8. Settings menu Render `config_options` as-is: each `Select` is a dropdown, each `Boolean` a toggle. Rebuild the menu from every `SessionUpdated`, because switching `model` changes which other options exist. ```rust theme={"theme":"vesper"} fn rebuild_settings(ui: &Ui, session: &Session, info: &SessionInfo) { ui.clear_settings(); for option in &info.details.config_options { let session = session.clone(); let id = option.id.clone(); match &option.kind { ConfigKind::Select { choices } => ui.dropdown(&option.name, choices, &option.current, move |v| { let s = session.clone(); let id = id.clone(); async move { s.configure(id, v).await } }), ConfigKind::Boolean => ui.toggle(&option.name, &option.current, move |b| { let s = session.clone(); let id = id.clone(); async move { s.configure(id, b).await } }), _ => {} } } } ``` Feature buttons are gated the same way, never by agent name: ```rust theme={"theme":"vesper"} let caps = &info.details.capabilities; ui.show_fork(caps.supports(Capability::Fork)); ui.show_rollback(caps.supports(Capability::Rollback), caps.supports(Capability::RollbackFiles)); ui.show_compact(caps.supports(Capability::Compact)); ``` ## 9. Persist and resume Two things per thread: the event log (step 4) and the latest `SessionInfo` (step 3 and every `SessionUpdated`). The info carries the `resume_token`. ```rust theme={"theme":"vesper"} // tomorrow, after restart let info = store.load_info(thread_id); let token = info.resume_token.clone().ok_or("this agent cannot resume")?; let (session, events) = runtime .open(&info.agent, SessionOptions::in_dir(&repo).resume(token)) .await?; ``` Resume brings back the agent's context, not your transcript, so render the stored events first, then attach the new stream. The token is opaque; store it as-is. ## 10. Titles, commit messages, PR bodies These need text, not a conversation. `generate` opens a throwaway session with tools off, returns the reply, and closes. ```rust theme={"theme":"vesper"} let title = runtime .generate(&agent, SessionOptions::in_dir(&repo), format!("five-word title for: {first_prompt}")) .await?; store.set_title(thread_id, title); ``` ## 11. Usage page Two gauges. Per thread: the `ContextUsage` event from step 5. Per account: ```rust theme={"theme":"vesper"} for entry in runtime.plan_usage_all().await { // one call, every installed agent match entry.usage { Ok(usage) => ui.quota_card(&entry.agent.name, usage), // plan name, 5-hour and weekly windows Err(_) => {} // no subscription or no PlanUsage capability } } ``` ## 12. Headless and background runs A worker that runs unattended (a kanban card, a scheduled task) is the same code with two option changes and no dialogs: ```rust theme={"theme":"vesper"} let options = SessionOptions::in_dir(&repo) .permission_mode(PermissionMode::AutoApprove) // allow every tool request .configure("mode", "accept-edits"); // if the agent has such a mode ``` Questions still arrive as `RequestOpened`; a headless worker should answer them with a default or cancel the turn. Watch `Diagnostic` events for stall warnings (`stall_after`, default 120 s of silence). ## 13. Test it without an agent installed Run the same code over a scripted agent with the `mock` feature. The engine, turn rules, and event shapes are real. ```rust theme={"theme":"vesper"} let runtime = Runtime::with_mock(Script::default().turn(vec![ Step::Emit(text("m1", "hi")), Step::Emit(permission("p1")), Step::AwaitAnswer, Step::End(completed()), ])); let agent = runtime.discover().await.require("mock")?.clone(); // steps 3 to 7 run unchanged ``` ## What your app never does | Not your job | anyagent does it | | ------------------------------------- | -------------------------------- | | Parse a wire or know an agent's flags | the adapters | | Decide when a turn ended | one `TurnEnded` per turn, always | | Decide steer vs queue | `prompt` returns the `Delivery` | | Track which requests are open | `RequestClosed`, `NeedsInput` | | Hardcode models or feature support | `config_options`, `capabilities` | | Read credentials or call vendor HTTP | `probe`, `plan_usage` | # Contributing Source: https://anyagent.mintlify.app/contributing Development setup, the test tiers, and what a PR needs. ## Flow 1. Open an issue first and get it approved. 2. Fork, branch, make the change. 3. Open a PR with the proof described below. ## Setup ```bash theme={"theme":"vesper"} git clone https://github.com/spotta85/anyagent-rs && cd anyagent-rs cargo install just # the dev front door; `just` lists every command just check # fmt, clippy, offline tests ``` Rust 1.88 or newer. For live tests, the agent you are touching must be installed and logged in. ## Code Write the smallest amount of code that does the job. No AI slop. Comments: * Every function gets a 1-2 line doc comment: what it does, what flows in and out. * Non-trivial chunks inside a function can get a short comment. * Nothing else. Main functions hold the high-level flow and read top to bottom; helpers sit below them. Adapters never decide turn rules; see [Architecture](/architecture#the-seam). ### Platforms macOS, Linux, and Windows all build and test in CI. When something differs per OS, pick the first row that fits. Never put `#[cfg]` inside a function body. | Situation | Do this | Example | | ----------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------- | | std has a cross-platform API | Use it, no cfg | `std::env::split_paths`, `home_dir` in `discovery.rs` | | One value differs | One `#[cfg(unix)]` / `#[cfg(windows)]` const pair | `EXE_SUFFIXES` in `discovery.rs` | | Whole behaviour differs | Two same-signature functions, one per OS; callers see no cfg | `request_exit` in `process.rs` | | Early return on one OS | `if cfg!(windows) { return ... }`, so both branches still type-check | `capture_login_shell_path` | | Needs an OS API | A crate that owns both halves | `command-group` (process group on unix, Job Object on Windows) | | A test needs a script on disk | Write it per OS in `tests/common/mod.rs` | `shim`, `stub` | Check the other OS without leaving your machine: ```sh theme={"theme":"vesper"} rustup target add x86_64-pc-windows-msvc cargo clippy --all-targets --target x86_64-pc-windows-msvc -- -D warnings ``` ## Test tiers | Tier | Command | What it proves | Needs | | ----------------- | ------------------------- | -------------------------------------------------------------------------- | --------------------------------- | | Offline | `just check` | Engine contract (`conformance.rs`), each adapter against recorded fixtures | nothing | | Live, one feature | `just live claude cancel` | The feature against the real agent | the agent installed and logged in | | Live, all | `just live all` | The full matrix in `tests/live.rs` | every agent | `just features` lists the live feature names. `just live` always runs with one test thread; parallel runs open many real sessions at once. Live rules: * Never touch real auth: no deleting or moving `~/.claude`, `~/.hermes`, `~/.local/share/opencode`, no logouts. * `SKIP` lines are passes. Capability gates and missing keys skip with a reason. * A failure on model output only (wrong word, empty text) with correct structure: rerun once. Two identical failures are a finding. A structural failure (missing event, wrong error type, hung) is a finding immediately. ## What a PR needs | Change | Proof to include | | ---------------------------- | -------------------------------------------------------------------------------- | | Any | `just check` passes | | Touches a wire or adapter | `just live ` output for every harness it touches | | Adds or changes a feature | Its test in `tests/live.rs` added or updated | | Changes the public interface | Say so in the description, with what and why. Docs updated in the same PR | | Fixes a wire bug | A `record_wire` capture or fixture showing the bug, and the test that now passes | A PR that changes the interface without updating the docs will not be merged. ## Recording a wire When an agent misbehaves, capture exactly what crossed the wire: ```rust theme={"theme":"vesper"} SessionOptions::in_dir(".").record_wire("/tmp/claude.jsonl") ``` Recordings are unredacted. Strip anything sensitive before turning one into a fixture under `tests/fixtures//`. ## Docs The docs are Mintlify pages under `docs/`. Keep each page to one job: | Page | Job | | --------------- | -------------------------------------------------------------- | | Core API | The interface. One row per method or variant, no feature prose | | Features | What each feature does, a few sentences and one snippet | | Agents | Catalog, capability matrix, per-agent quirks | | Building an app | Glue code for a real app | | Architecture | Internals | If a fact belongs on another page, link to it rather than repeating it. # Core API Source: https://anyagent.mintlify.app/core-api The whole public interface on one page: Runtime, Session, SessionOptions, Events, requests, errors. anyagent's whole public surface fits on one page. You create a `Runtime`, ask it to open a session, and from then on you send commands through `Session` and read what the agent does from `Events`. Everything below is exported from the crate root; full signatures are on [docs.rs](https://docs.rs/anyagent). ```text theme={"theme":"vesper"} Runtime ──open──► (Session, Events) │ │ │ │ discover │ prompt │ Event { kind: EventKind, turn_info, extensions } │ probe │ answer │ │ generate │ configure │ plan_usage │ rollback / compact / cancel / close ``` ## Runtime The entry point. Create one when your app starts and keep it around: it knows the agent catalog, scans the machine for installed agents, and opens sessions on them. It is also where the one-off calls live that don't need a conversation, like one-shot text generation and reading account quota. | Method | Returns | What it does | | ----------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------ | | `discover()` | `DiscoveryReport` | Instant, read-only scan: installed agents plus missing ones with install hints | | `prewarm()` | | Start the discovery scan early | | `probe(&agent)` | `AgentDetails` | Opens a throwaway session (\~1 s) to learn version, auth, capabilities, config options, commands | | `probe_auth(&agent)` | `AuthStatus` | Cheaper probe when you only need the login state | | `open(&agent, options)` | `(Session, Events)` | Spawn the agent and hand back the two halves | | `generate(&agent, options, prompt)` | `String` | One-shot text: opens, prompts, declines tools, closes | | `plan_usage(&agent)` | `PlanUsage` | The account's subscription quota, from the agent itself | | `plan_usage_all()` | `Vec` | The same for every installed agent | | `with_mock(script)` | `Runtime` | Feature `mock`: the real engine over a scripted agent | ```rust theme={"theme":"vesper"} let report = runtime.discover().await; let agent = report.require("codex")?; // AgentError::NotInstalled if absent for missing in &report.missing { // MissingAgent { name, install_hint, .. } println!("{}: {}", missing.name, missing.install_hint); } ``` Discovery hands you `AgentInstallation` values, and every other `Runtime` call takes one. You can also build one by hand: `AgentInstallation::at(id, path)` points at a specific binary, and `AgentInstallation::acp(name, path, args)` describes an ACP agent that isn't in the catalog. ## SessionOptions Everything `open` needs to know before the agent starts: where to run, whether to continue an old conversation, how to handle permissions, which settings to apply first. Start with `in_dir` and chain whatever else applies. Anything you can change later goes through `Session::configure` instead. | Builder | What it does | | ----------------------- | ------------------------------------------------------------------------------------- | | `in_dir(path)` | Working directory for the agent. Required. | | `configure(id, value)` | Set a config option before the first turn (`"model"`, `"effort"`, `"fast"`, `"mode"`) | | `resume(token)` | Reopen an earlier session | | `fork_from(token, at)` | Branch a new session off an old one, at a message or at the end | | `mcp_server(server)` | Hand the agent one of your MCP servers (stdio, HTTP, SSE) | | `permission_mode(mode)` | `Ask` (forward requests) or `AutoApprove` | | `config_home(dir)` | Separate config directory, for several logins of one agent | | `quiet_window(dur)` | How long silence may run before completion is inferred | | `stall_after(dur)` | Silence before a stall `Diagnostic` (default 120 s). Never ends the turn | | `record_wire(path)` | Dump the raw protocol traffic to a JSONL file. Unredacted, treat as sensitive | ## Session Your handle on one live conversation. Everything you tell the agent goes through it: prompts, answers to its requests, setting changes, cancel, close. It is cheap to clone and every clone talks to the same session, so hand copies to whichever parts of your app need to send commands. Most calls return as soon as the command is accepted; the table says which event confirms the result. | Method | What it does | Confirmed by | | ---------------------------- | -------------------------------------------------------------------- | ------------------------------------------------ | | `prompt(input)` | Send text or an `Input` with attachments. Returns a `Delivery` | `TurnStarted`, or `Delivery::Steered` / `Queued` | | `dequeue(prompt_id)` | Drop a queued prompt before it starts | | | `answer(request_id, answer)` | Answer a permission or question, exactly once | `RequestClosed` | | `configure(id, value)` | Change a live config option | `SessionUpdated` | | `rollback(turns, scope)` | Rewind completed turns in place. Session must be idle | `SessionUpdated`, or a `Diagnostic` on refusal | | `compact()` | Ask the agent to summarize its context. Session must be idle | `ContextCompacted` | | `cancel(clear_queue)` | Stop the running turn; optionally drop the queue too | `TurnEnded { Cancelled }` | | `close()` | End the agent session and wait for cleanup | stream ends | | `info()` | Snapshot: agent, details, configuration, resume token, title, status | | | `status()` | `Idle`, `Working`, or `NeedsInput`, without reading the stream | | `prompt` never fails for being busy. Instead it returns a `Delivery` that says what happened to your text: | `DeliveryKind` | Meaning | | --------------------- | ------------------------------------------------------------------------- | | `Started { turn_id }` | Session was idle; this prompt began a turn | | `Steered { turn_id }` | A turn was running and the agent supports `Steer`; it went into that turn | | `Queued { position }` | A turn was running and the agent cannot steer; it waits its turn | A prompt is a plain `&str` or an `Input`, which is text plus file paths: `Input::text("…").attach("shot.png")`. On agents with `Capability::Images` the image bytes go inline; on the others the attachment becomes a path in the prompt text that the agent can open with its own tools. ## Events The other half of `open`: a stream of everything the agent does, in order. Text as it streams, tool calls as they change, requests that need an answer, usage numbers, and the start and end of every turn. The same `EventKind`s arrive for every agent, so one `match` covers all of them. It is a `Stream>`. Read it continuously from its own task: it buffers 1024 events, and a consumer that falls a full buffer behind is treated as gone and the session closes. ```rust theme={"theme":"vesper"} pub struct Event { pub sequence: u64, // 1, 2, 3… per session; the true order pub occurred_at: SystemTime, // wall clock, for stored transcripts pub session_id: SessionId, pub turn_info: Option, // { id, parent_tool_id } when inside a turn pub kind: EventKind, pub extensions: Extensions, // provider data, keyed "provider/name" } ``` `EventKind` is `#[non_exhaustive]`. Always keep a `_ => {}` arm so a new variant does not break your build. ### Turn boundaries A turn is one stretch of agent work. It starts when you prompt (or when the agent wakes itself to finish background work) and ends exactly once. These two events bracket everything else. | Variant | Payload | Notes | | ------------- | -------------------- | -------------------------------------------------------------------------------------- | | `TurnStarted` | `origin` | `Prompt(id)` for yours, `Agent` when the agent woke itself (background work finishing) | | `TurnEnded` | `stop`, `background` | Exactly once per turn. `background` lists tools still running | `StopReason`: | Variant | Meaning | | -------------------------------- | ------------------------------------------------------------------------ | | `Completed { source: Protocol }` | The wire said the turn was done | | `Completed { source: Inferred }` | The wire went quiet; anyagent called it. Show as "idle", not a checkmark | | `Cancelled` | You called `cancel` | | `Refused` | The agent declined the work | | `Failed { message }` | The turn broke | ### Content The agent's words. Deltas arrive as they stream and are grouped by `message_id`; append them in order. | Variant | Payload | Notes | | ---------------- | -------------------- | ----------------------------------------------------------------------------------------------- | | `TextDelta` | `message_id`, `text` | Assistant output. Append | | `ReasoningDelta` | `message_id`, `text` | The agent's thinking. Render separately or collapse | | `UserMessage` | `message_id`, `text` | Provider-originated user content (a parent steering a subagent). Never your own prompt replayed | | `MessageEnded` | `message_id` | No more deltas for this message | ### Tools and plans What the agent is doing. Each tool call is one `ToolUpdate` that you replace whole every time it changes, so there is no delta merging to get wrong. | Variant | Payload | Notes | | ----------------- | ----------------- | ------------------------------------------------------------------ | | `ToolUpdated` | `ToolUpdate` | Cumulative snapshot of one tool call. Replace by `id`, don't merge | | `ToolOutputDelta` | `tool_id`, `text` | Streamed output of a running command | | `PlanUpdated` | `entries` | The agent's full task list. Replaces the previous one | ```rust theme={"theme":"vesper"} pub struct ToolUpdate { pub id: ToolId, pub kind: ToolKind, // Read, Edit, Execute, Mcp { server, tool }, Subagent, … pub title: String, // ready to display pub status: ToolStatus, pub input: ToolInput, pub output: Option, pub diffs: Vec, // typed, for edits pub locations: Vec, // files it touched pub raw: Option, // the agent's own name and raw input } ``` When `kind` is `Subagent`, every event the child produces carries that tool's id in `turn_info.parent_tool_id`. That is how you nest it in a UI. ### Requests Moments where the agent stops and waits for a person: permission to run a tool, or a question with choices. The turn does not continue until you answer, and the session's status is `NeedsInput` meanwhile. | Variant | Payload | Notes | | --------------- | ------------ | --------------------------------------------------------------------------------------------------- | | `RequestOpened` | `Request` | A permission or question. Answer once with `session.answer` | | `RequestClosed` | `request_id` | Resolved, by your answer or by the agent withdrawing it. Clear your UI here, not on your own answer | ```rust theme={"theme":"vesper"} use anyagent::{Answer, PermissionChoice, QuestionAnswer, Request}; match request { Request::Permission(r) => { // r.options is what this agent actually offers; answers outside it are rejected session.answer(r.id, Answer::Permission(PermissionChoice::AllowOnce)).await?; } Request::Question(r) => { // one QuestionAnswer per question, in order let answers = r.questions.iter().map(|_| QuestionAnswer::Text("yes".into())).collect(); session.answer(r.id, Answer::Question(answers)).await?; } } ``` `PermissionChoice` is `AllowOnce`, `AllowAlways`, `DenyOnce`, `DenyAlways`. `QuestionAnswer` is `Text(String)` or `Choices(Vec)`. ### Session state and usage Everything about the session that isn't part of the conversation: its settings, its UI state, how full its context window is, and how much of the account's quota is used. | Variant | Payload | Notes | | ------------------ | ------------------------------------------ | ------------------------------------------------------------------------------------- | | `SessionUpdated` | `SessionInfo` | New snapshot after `configure`, `rollback`, or an agent rename | | `StatusChanged` | `SessionStatus` | `Idle`, `Working`, `NeedsInput`. Emitted only on change | | `ContextUsage` | `used_tokens`, `window_tokens`, `cost_usd` | How full this session's window is | | `ContextCompacted` | | The agent compacted; the next `ContextUsage` drops | | `PlanUsageUpdated` | `PlanUsage` | Account quota, pushed without asking | | `Diagnostic` | `Diagnostic` | Non-fatal but worth surfacing: a rejected rollback, a stall, a wire oddity. Log these | ## AgentDetails and capabilities What the agent told anyagent about itself: its version, whether it is logged in, what it can do, which settings it exposes, and which slash commands it has. You get it from `probe` before opening, and from `session.info().details` once a session is live. It is the single source for feature-gating and for building a settings menu. ```rust theme={"theme":"vesper"} pub struct AgentDetails { pub version: Option, pub auth: AuthStatus, // Authenticated { kind, account } | Unauthenticated { login } | Unknown pub capabilities: Capabilities, // .supports(Capability::Fork) pub config_options: Vec, // id, name, kind (Select | Boolean), current, live pub commands: Vec, } ``` A `Capability` is one optional thing this agent can do on this connection. Check `capabilities.supports(..)` before offering a feature, and never special-case an agent by name: the same agent can report different capabilities over different wires. The full matrix is on the [Agents](/agents#capability-matrix) page. | `Capability` | Unlocks | | --------------- | ------------------------------------------------------------- | | `Images` | Attachments go inline instead of as a path | | `Resume` | `SessionOptions::resume` | | `Steer` | A mid-turn prompt steers instead of queueing | | `Permissions` | The agent asks before tools; `generate` can run hands-off | | `Questions` | `Request::Question` can arrive | | `Rollback` | `session.rollback(.., Conversation)` | | `RollbackFiles` | `session.rollback(.., ConversationAndFiles)` | | `Fork` | `SessionOptions::fork_from` | | `Compact` | `session.compact()` | | `SlashCommands` | `details.commands` is populated and sendable as prompts | | `Plan` | `PlanUpdated` events | | `Subagents` | Nested `Subagent` tools with `parent_tool_id` on their events | | `ContextUsage` | `ContextUsage` events | | `PlanUsage` | `runtime.plan_usage` returns real numbers | ## Errors Every failure a caller can see is one `AgentError` variant, so your app can match on the cause instead of parsing messages. The two worth handling specially are `AuthRequired`, which carries the login steps to show the user, and `ResumeFailed`, after which most apps simply open a fresh session. The enum is `#[non_exhaustive]`. | Variant | When | | ---------------------------------- | ----------------------------------------------------------------------- | | `NotInstalled(id)` | `report.require` for an absent agent | | `SpawnFailed` | The process would not start | | `AuthRequired { login }` | Logged out. `login` carries runnable `LoginMethod`s to show the user | | `HandshakeTimeout` | Launch plus handshake exceeded 30 s | | `UnsupportedFeature` | You called something the capability set does not include | | `InvalidConfiguration` | Bad option id or value, or an isolation request the agent cannot honour | | `InvalidRequest` | Answering a closed request, dequeueing a started prompt | | `ResumeFailed` | The token did not resolve | | `SessionBusy` | `rollback` or `compact` while a turn is running | | `ProtocolFailed` | The wire misbehaved | | `ProcessExited { status, stderr }` | The agent died | | `SessionClosed` | A call after `close` | # Features Source: https://anyagent.mintlify.app/features Everything anyagent does. Each section says what it is for, how to use it, what confirms it, and which capability gates it. Every section has the same four parts: **what** it does, **how** to call it, what **confirms** it happened, and what **gates** it. Types and signatures are on the [Core API](/core-api) page. Which agents support what is on the [Agents](/agents#capability-matrix) page. ```text theme={"theme":"vesper"} Runtime discover · probe · open · generate · plan_usage Session prompt · answer · configure · cancel · rollback · compact · close Events text · tools · plans · requests · status · usage · turn boundaries ``` ## Discovery **What:** find every supported agent on the machine, instantly, without launching anything. Checks env overrides, `PATH`, the login-shell `PATH` (GUI apps often miss it), and known install directories. Agents that are supported but missing come back with an install hint. ```rust theme={"theme":"vesper"} let report = runtime.discover().await; for agent in &report.agents { println!("{} at {}", agent.name, agent.executable_path.display()); } for missing in &report.missing { println!("{} not installed: {}", missing.name, missing.install_hint); } let agent = report.require("claude")?; // AgentError::NotInstalled if absent ``` **Confirms:** the report itself. Discovery never guesses login state; `probe` or `probe_auth` asks the agent. **Gates:** none. Call `runtime.prewarm()` at startup to scan early. **Not found?** `MissingAgent.searched` lists every directory that was checked, so an app can show it and offer a file picker. A user with an unusual install sets `ANYAGENT__BIN` (see [Agents](/agents#pointing-at-a-specific-binary)). An installer that edits the user's `PATH` is invisible to an app that is already running: say "installed? restart the app", not "not found". Some agents have a richer, separately installed runtime (Antigravity's ACP server). When only the base CLI is found, `agent.upgrade` names what to install for more capabilities. ## Probe and auth **What:** ask the agent itself what it can do. Opens a throwaway session (about 1 s) and returns version, real login state, capabilities, config options, and slash commands. `probe_auth` is the cheaper call when you only need the login state. ```rust theme={"theme":"vesper"} let details = runtime.probe(&agent).await?; match &details.auth { AuthStatus::Authenticated { kind, account } => {} // Subscription | ApiKey | CloudProvider AuthStatus::Unauthenticated { login } => { for method in login { match method { LoginMethod::Terminal { command, .. } => ui.show_command(command), // e.g. ["claude", "auth", "login"] LoginMethod::EnvVar { name } => ui.show_env_var(name), _ => {} } } } AuthStatus::Unknown => {} } ``` **Confirms:** `AgentDetails`. The same struct is available later as `session.info().details`. **Gates:** none. anyagent never reads or holds credentials and never drives a login flow. It tells you the exact command or env var to show the user. Keep several logins of one agent apart with `SessionOptions::config_home(dir)`. ## Sessions **What:** one live conversation with one agent process. `open` spawns the agent, handshakes, and returns two halves: `Session` (commands, cheap to clone) and `Events` (the stream, one consumer). ```rust theme={"theme":"vesper"} let (session, events) = runtime .open(&agent, SessionOptions::in_dir("/path/to/repo")) .await?; ``` **Confirms:** `open` returns. `session.info()` has the resume token, capabilities, and config options. **Gates:** none. Fails typed: `AuthRequired { login }` when logged out, `HandshakeTimeout` after 30 s, `ResumeFailed` for a bad token. Drain `Events` continuously. It buffers 1024 events; a consumer that falls a full buffer behind is treated as gone and the session closes. `close()` ends the process; dropping `Events` does the same. ## Prompting **What:** send text, slash commands, and file attachments. What happens depends on session state, and the returned `Delivery` tells you which. ```rust theme={"theme":"vesper"} let delivery = session .prompt(Input::text("what's wrong here?").attach("screenshot.png")) .await?; match delivery.kind { DeliveryKind::Started { turn_id } => {} // session was idle; a turn began DeliveryKind::Steered { turn_id } => {} // a turn was running; this went into it DeliveryKind::Queued { position } => {} // a turn was running; this waits its turn } ``` | Session state | Agent has `Steer` | Result | | ------------- | ----------------- | ------------------------------------------------------- | | Idle | any | `Started` | | Turn running | yes | `Steered`: the agent changes course mid-turn | | Turn running | no | `Queued`: starts its own turn when the current one ends | **Confirms:** `TurnStarted { origin: Prompt(id) }` for started and queued prompts. **Gates:** `Steer` decides steer vs queue. `Images` decides whether an attachment goes inline or degrades to a path in the prompt text. Drop a queued prompt before it starts with `session.dequeue(prompt_id)`. Slash commands from `details.commands` are sent as plain text: `session.prompt("/review")`. An unknown slash prompt is just text. ## Permissions and questions **What:** the agent's "may I run this?" and "which one?" moments, forwarded as typed requests with the choices the agent actually offers. Answer exactly once. ```rust theme={"theme":"vesper"} EventKind::RequestOpened(Request::Permission(r)) => { // r.tool is the ToolUpdate awaiting approval; r.options is what the agent accepts session.answer(r.id, Answer::Permission(PermissionChoice::AllowOnce)).await?; } EventKind::RequestOpened(Request::Question(r)) => { // one QuestionAnswer per question, in order; Choices(ids) or Text(string) let answers = r.questions.iter().map(|q| QuestionAnswer::Choices(vec![q.choices[0].id.clone()])).collect(); session.answer(r.id, Answer::Question(answers)).await?; } EventKind::RequestClosed { request_id } => ui.clear(request_id), ``` **Confirms:** `RequestClosed`. It also arrives when a cancelled turn withdraws the request, so clear your UI on that event, not on your own answer. While a request is open, `session.status()` is `NeedsInput`. **Gates:** `Permissions` and `Questions`. An agent without `Permissions` runs tools without asking. For unattended runs, open with `SessionOptions::permission_mode(PermissionMode::AutoApprove)`: anyagent allows each request once without forwarding it. ## Cancel **What:** stop the running turn. The session and its context survive. ```rust theme={"theme":"vesper"} session.cancel(false).await?; // stop the turn, keep queued prompts session.cancel(true).await?; // stop the turn, drop the queue too ``` **Confirms:** `TurnEnded { stop: Cancelled }`, then `RequestClosed` for any request the turn had open. **Gates:** none. ## Configuration **What:** models, effort, mode, sandbox, switched without a restart. Agents advertise their settings as `ConfigOption`s with typed choices, so a picker renders what the agent offers instead of a hardcoded list. ```rust theme={"theme":"vesper"} // at open: creation-only and live options alike let options = SessionOptions::in_dir(".").configure("model", "sonnet").configure("effort", "low"); // mid-session: options with `live: true` session.configure("fast", true).await?; ``` | id | Kind | Notes | | --------- | ------- | --------------------------------------------------------------------- | | `model` | select | Switching it rebuilds the other options' choices | | `effort` | select | Choices follow the model; a model without levels drops the option | | `fast` | boolean | Only when the selected model supports it. Lower latency, higher usage | | `mode` | select | Agent-specific: `plan`, `accept-edits`, `ask`, … | | `sandbox` | select | Codex | **Confirms:** `SessionUpdated`, carrying the new `configuration` and the rebuilt `config_options`. Wait for it rather than for `configure` to return. **Gates:** the option must be in `details.config_options`, else `InvalidConfiguration`. ## Status **What:** the one value a thread list needs. `Idle`, `Working`, or `NeedsInput`, pushed on every change and readable on demand. ```rust theme={"theme":"vesper"} EventKind::StatusChanged(status) => ui.set_badge(thread, status), let now = session.status(); // for a component that mounts late ``` **Confirms:** `StatusChanged` is emitted only on change. A turn ending with another prompt queued stays `Working`, never flashing `Idle`. **Gates:** none. ## One-shot generation **What:** prompt in, string out, no session to manage. For thread titles, commit messages, branch names, PR bodies. ```rust theme={"theme":"vesper"} let title = runtime .generate(&agent, SessionOptions::in_dir("."), "a five-word title for: fix the parser") .await?; ``` It opens a throwaway session, disables tools where the wire allows (claude, pi) and declines every permission elsewhere, collects the text until the turn ends, and closes. **Confirms:** the returned `String`. **Gates:** `Permissions`, or a wire that can launch without tools. Requires a new session: `resume` and `fork_from` options are rejected. A tool event or a question needing a choice cancels generation. Put context inline; attachments cannot be opened without tools. ## Resume **What:** continue a conversation from a new process. Every session mints an opaque `resume_token`; store it with your transcript. ```rust theme={"theme":"vesper"} let token = session.info().resume_token.clone().unwrap(); // grab it while alive // later, any process let (session, events) = runtime .open(&agent, SessionOptions::in_dir(".").resume(token)) .await?; ``` **Confirms:** `open` returns; the agent has its context back. No old events are replayed, so keep your own transcript. **Gates:** `Resume`, else `ResumeFailed`. The token is agent-owned: store it as-is, never parse it. The session id is not a resume token. ## Fork **What:** a new session starting from an old one's history. The original is untouched, which is what "try this a different way" needs. ```rust theme={"theme":"vesper"} let options = SessionOptions::in_dir(".").fork_from(token, Some(message_id)); // None = from the end let (branch, branch_events) = runtime.open(&agent, options).await?; ``` **Confirms:** `open` returns a new session with its own resume token. **Gates:** `Fork`. ## Rollback **What:** rewind this session in place by whole turns. ```rust theme={"theme":"vesper"} session.rollback(NonZeroU32::new(2).unwrap(), RollbackScope::Conversation).await?; session.rollback(NonZeroU32::new(1).unwrap(), RollbackScope::ConversationAndFiles).await?; ``` | Scope | Rewinds | Gate | | ---------------------- | ----------------------------------------- | --------------- | | `Conversation` | The agent's context only | `Rollback` | | `ConversationAndFiles` | Context and the files those turns changed | `RollbackFiles` | **Confirms:** `SessionUpdated`. A refusal comes back as a `Diagnostic`, not an error from the call. **Gates:** above, plus an idle session (`SessionBusy` otherwise). `ConversationAndFiles` writes to the working tree. Check `RollbackFiles` before offering the button and tell the user what it does. ## Compact **What:** ask the agent to summarize its own context, freeing room in the window without losing the thread. ```rust theme={"theme":"vesper"} session.compact().await?; ``` **Confirms:** `ContextCompacted`, then a lower `ContextUsage`. Compaction runs as an agent-originated turn: prompts sent meanwhile queue behind it. An agent that finds nothing to summarize says so as a `Diagnostic`. **Gates:** `Compact` (claude, codex, opencode, pi) and an idle session. ## Subagents **What:** agents that spawn agents render as a tree, not interleaved noise. ```rust theme={"theme":"vesper"} EventKind::ToolUpdated(tool) if tool.kind == ToolKind::Subagent => ui.open_child(tool.id), // every event the child produces: let parent = event.turn_info.as_ref().and_then(|t| t.parent_tool_id.clone()); ``` **Confirms:** `parent_tool_id` on the child's events. A child can never end the parent's turn; anyagent consumes the child's turn bookkeeping. **Gates:** `Subagents`. ## Context and plan usage **What:** the two gauges apps show. They measure different things. | Gauge | Source | Measures | | ------------- | ------------------------------------------------------------------ | ---------------------------------------------------------- | | Context usage | `EventKind::ContextUsage { used_tokens, window_tokens, cost_usd }` | How full this session's window is | | Plan usage | `runtime.plan_usage(&agent)`, `EventKind::PlanUsageUpdated` | How much of the account's subscription is used, per window | ```rust theme={"theme":"vesper"} let usage = runtime.plan_usage(&agent).await?; // plan name, 5-hour and weekly windows for entry in runtime.plan_usage_all().await { // every installed agent, for a dashboard ui.card(&entry.agent.name, entry.usage); } ``` **Confirms:** the events and return values above. Numbers come from the agent itself: no HTTP endpoints, no token handling. **Gates:** `ContextUsage` and `PlanUsage`. Plan usage also needs a subscription login (`AuthKind::Subscription`). ## MCP servers **What:** hand the agent your app's MCP servers for this session. ```rust theme={"theme":"vesper"} let options = SessionOptions::in_dir(".") .mcp_server(McpServer::stdio("local", "/usr/bin/my-server", ["--stdio"])) .mcp_server(McpServer::http("remote", "https://example.com/mcp").with("Authorization", "Bearer …")) .mcp_server(McpServer::sse("stream", "https://example.com/sse")); ``` **Confirms:** calls into your servers arrive as `ToolUpdated` with `ToolKind::Mcp { server, tool }`. **Gates:** the agent's supported transports. An unsupported transport fails `open` typed rather than dropping the server. ## Wire recording **What:** a bug report that contains the actual bug. Tees every raw protocol frame, both directions, to a JSONL file. ```rust theme={"theme":"vesper"} SessionOptions::in_dir(".").record_wire("/tmp/claude.jsonl") ``` **Confirms:** the file. A recording failure never fails a turn. **Gates:** none. The recording is unredacted: prompts, file contents, command output, paths. Treat it as sensitive and delete it after debugging. ## Testing without a subprocess **What:** run your app over a scripted agent. The engine, turn rules, and event shapes are real; only the agent is fake. ```toml Cargo.toml theme={"theme":"vesper"} [dev-dependencies] anyagent = { version = "0.0.1", features = ["mock"] } ``` ```rust theme={"theme":"vesper"} use anyagent::mock::{Script, Step, completed, permission, text}; let script = Script::default().turn(vec![ Step::Emit(text("m1", "hello")), // TextDelta Step::Emit(permission("p1")), // RequestOpened Step::AwaitAnswer, // pause until your app answers Step::End(completed()), // TurnEnded ]); let runtime = Runtime::with_mock(script); let agent = runtime.discover().await.require("mock")?.clone(); ``` **Confirms:** the same events your real code handles. **Gates:** feature `mock`. `Script` flags (`steer`, `deterministic`, `buffer`, …) model the wire shapes the engine has to cope with. # Overview Source: https://anyagent.mintlify.app/index One Rust interface to the coding agents installed on a machine.

Users already have Claude Code, Codex, Cursor, opencode, Kiro, and friends installed and logged in. Each one has its own CLI, protocol, and quirks, so an app built on top of them ends up with a driver per agent.

anyagent is that layer, written once. It finds the agents, speaks each one's protocol (native or ACP), and gives you one typed API.

## Three objects ```text theme={"theme":"vesper"} Runtime::discover() -> Runtime::open() -> (Session, Events) your app ── prompt / answer / configure ──► Session │ ▼ agent process │ your UI ◄── text / tools / requests / usage ── Events ``` | Object | Role | You call it for | | --------- | ---------------------------------------- | -------------------------------------------------------------- | | `Runtime` | Finds agents, opens sessions | `discover`, `probe`, `open`, `generate`, `plan_usage` | | `Session` | Command handle for one live conversation | `prompt`, `answer`, `configure`, `cancel`, `rollback`, `close` | | `Events` | The stream everything comes back on | one `match` on `EventKind` | ## Who owns what | anyagent owns | Your app owns | | ----------------------------------------------------------------------- | ------------------------------------ | | Finding and launching agent processes | The transcript store | | Speaking each wire | Titles, threads, worktrees | | Turn rules: one `TurnEnded` per turn, steer or queue, request lifetimes | The UI and the policy behind it | | Normalizing events and tool calls | What to do with a permission request | ## Design rules 1. **Same types for every agent.** Provider-specific data rides in `extensions` instead of leaking into the API. 2. **Capabilities are reported, not assumed.** After `open`, the session tells you what this agent can do on this connection. Gate features on that, not on the agent's name. 3. **A turn ends exactly once.** Whether the wire said so or anyagent inferred it, you get one `TurnEnded`. ## Where to go Install, discover, open, prompt, stream, answer one permission. Runtime, Session, SessionOptions, every EventKind, requests, errors. Each thing anyagent does, in a few sentences and one snippet. The catalog, the capability matrix, per-agent quirks, custom ACP agents. How the pieces fit in a real chat app: threads, streaming, permissions, persistence. The codebase map and how an event travels from wire to your UI. ## License MIT or Apache-2.0. # Quickstart Source: https://anyagent.mintlify.app/quickstart Install anyagent and run one turn against a real agent. ## Requirements * Rust 1.88 or newer (edition 2024) * A [supported agent](/agents) installed and logged in * A Tokio runtime ## Install ```bash theme={"theme":"vesper"} cargo add anyagent futures tokio --features tokio/rt-multi-thread,tokio/macros ``` That adds these to `Cargo.toml`: ```toml Cargo.toml theme={"theme":"vesper"} [dependencies] anyagent = "0.0.1" futures = "0.3" tokio = { version = "1", features = ["rt-multi-thread", "macros"] } ``` ## One turn ```rust src/main.rs theme={"theme":"vesper"} use anyagent::{EventKind, Runtime, SessionOptions}; use futures::StreamExt; #[tokio::main] async fn main() -> Result<(), Box> { let runtime = Runtime::new(); // 1. Find what's on this machine, then insist on one of them. let report = runtime.discover().await; let agent = report.require("claude")?; // 2. Open a session: a command handle and an event stream. let (session, mut events) = runtime.open(agent, SessionOptions::in_dir(".")).await?; // 3. Prompt, then drain events until the turn ends. session.prompt("explain this repo").await?; while let Some(event) = events.next().await { match event?.kind { EventKind::TextDelta { text, .. } => print!("{text}"), EventKind::TurnEnded { .. } => break, _ => {} } } session.close().await?; Ok(()) } ``` What just happened: ```text theme={"theme":"vesper"} discover() → report.agents (installed) + report.missing (with install hints) open() → spawns the agent, handshakes, returns (Session, Events) prompt() → starts a turn Events → TurnStarted, TextDelta…, TurnEnded close() → ends the process ``` `Session` is cheap to clone and every clone talks to the same engine task, so you can prompt from one task while another drains `Events`. ## Run the examples The repo's [`examples/`](https://github.com/spotta85/anyagent-rs/tree/main/examples) folder has three small programs that run against the agents on your machine: ```bash theme={"theme":"vesper"} cargo run --example chat -- claude # prompt, stream, answer, steer cargo run --example sessions -- claude # several sessions at once, then resume cargo run --example probe # what's installed, logged in, and capable ``` ## Next The full public interface on one page. Threads, streaming, permissions, model picker, persistence.