Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ adheres to [Semantic Versioning](https://semver.org/).

## Unreleased

### Docs

- **A skill for agents building on yoagent**: `skills/yoagent/SKILL.md` (AgentSkills; ships in the crate) — which feature for which job, recipes compiled by `tests/skill_test.rs`, tips and pitfalls, what changed per version — and `llms.txt`, a map of every docs page.

### yoagent-frontend (experimental, new, not published)

- **One frontend protocol** (`integrations/yoagent-frontend`): a `Session` runs one agent for any number of frontends — a pi-tui terminal UI (a rutis plugin) and browsers over WebSocket — with commands in (prompt, steer, follow-up, abort, reset, quit), and out every event (text deltas merged), a run lifecycle that is always delivered (`runStart` / `runEnd` with an `outcome`: completed, aborted, rejected or error — also when the agent's task or the session's driver fails), plugins' questions (`uiRequest`: confirm, select, input, notify; the first answer that fits wins, a safe default without a frontend or in time; `uiResolved` says why it closed), `notice`s, and `closed`, and **UI plugins**: a rutis plugin offers the browser an ES module (`renderTool` for a tool's results, `mountPanel` for a side panel). `PluginHost` replaces the rutis setup (runtimes, loader, explicit per-row runtime routing; `load` waits until plugins run — though a TypeScript plugin reads as running before its async start-up finishes, so wait for the handlers you need — and drops a batch that fails). The browser's WebSocket needs the token in the URL the server prints. Example `coding_agent` (terminal or `--web`), tried live in Chrome. yoagent's core and yoagent-rutis's API do not depend on it.
Expand Down
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,10 @@ CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner cargo test -

Native dev-dependencies (Tokio `full`, tempfile, wiremock) are under `[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]` because Cargo builds every dev-dependency for the target under test; only `--test wasm32` is built for wasm32, and a new wasm32 test belongs in that file, not in a native test target.

### Agent-facing docs

`skills/yoagent/SKILL.md` (AgentSkills format; packaged with the crate, so it is in every user's cargo registry) maps jobs to features with tips and pitfalls; `llms.txt` maps every docs page. `tests/skill_test.rs`: the skill loads with `SkillSet`; every repository path both cite exists **in the published crate** (`integrations/` crates are linked as URLs) and every `#anchor` is a heading; every line of the skill's ```rust recipes appears in the test file, where they are compiled (a recipe edited only in the skill fails); the verifier recipe runs; `prose_names` compiles each API named only in prose. **Release PRs update the skill's "What changed" section** (and its "Written for" version line).

### Testing

All unit tests use `MockProvider` (`provider/mock.rs`) to simulate LLM responses without network. Test files are in `tests/` — `agent_test.rs`, `agent_loop_test.rs`, `tools_test.rs`. Follow the existing pattern of constructing a `MockProvider` with predetermined responses.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ in [CONTRIBUTING](CONTRIBUTING.md).
- **[The book](https://yologdev.github.io/yoagent/)** — concepts, guides, a page per provider, and the [architecture and module map](https://yologdev.github.io/yoagent/architecture/overview.html) ([source](docs/))
- **[Design philosophy](https://yologdev.github.io/yoagent/design-philosophy.html)** — why yoagent is a loop library, the one extension contract, and what that costs
- **[API reference](https://docs.rs/yoagent)** — built with all features enabled
- **For coding agents: [the yoagent skill](skills/yoagent/SKILL.md)** — which feature for which job, tips and pitfalls, what changed per version (AgentSkills `SKILL.md`; also in the published crate) — and [`llms.txt`](llms.txt), a map of every docs page
- **[CHANGELOG](CHANGELOG.md)** — every release
- **[CONTRIBUTING](CONTRIBUTING.md)** — how to build, test, and send a PR

Expand Down
1 change: 1 addition & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ yoagent 自己的预算、工具闸门和输入守卫也是这样构建的。六
- **[手册](https://yologdev.github.io/yoagent/)**——概念、指南、每个提供商一页,以及[架构和模块地图](https://yologdev.github.io/yoagent/architecture/overview.html)([源码](docs/))
- **[设计哲学](https://yologdev.github.io/yoagent/design-philosophy.zh-CN.html)**——为什么 yoagent 是循环库、那一份扩展契约,以及它的代价
- **[API 参考](https://docs.rs/yoagent)**——启用全部 feature 构建
- **给编程 agent 用的:[yoagent skill](skills/yoagent/SKILL.md)**——什么任务用哪个功能、技巧与陷阱、每个版本的变化(AgentSkills `SKILL.md`,也随 crate 发布);以及 [`llms.txt`](llms.txt),全部文档页的索引
- **[CHANGELOG](CHANGELOG.md)**——每一次发布
- **[CONTRIBUTING](CONTRIBUTING.md)**——如何构建、测试和提交 PR

Expand Down
52 changes: 52 additions & 0 deletions llms.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# yoagent

> The agent loop for Rust: stream from 7 LLM protocols, run tools, loop until done. A loop library, not an agent: everything that changes what the loop does goes through one contract, `Extension`. For agents building on it, start with the skill: skills/yoagent/SKILL.md (which feature for which job, tips and pitfalls, what changed per version).

Book: https://yologdev.github.io/yoagent/ · API: https://docs.rs/yoagent · Source: https://github.com/yologdev/yoagent

## Start here

- [Skill: which feature for which job](skills/yoagent/SKILL.md): agent-facing map, tips, pitfalls, what changed
- [Quick start](docs/getting-started/quick-start.md): an agent with tools in a few lines
- [Installation](docs/getting-started/installation.md): features (`native`, `openapi`, `gasp`, `decision`)
- [Design philosophy](docs/design-philosophy.md): what belongs in yoagent and what does not

## Concepts

- [The agent loop](docs/concepts/agent-loop.md): turns, steering, follow-ups, cancellation
- [Extensions](docs/concepts/extensions.md): the plug-in contract (tools, policies, notes, verifiers, events, Budget)
- [Tools](docs/concepts/tools.md): built-ins, custom tools, ToolSource
- [Sub-agents](docs/concepts/sub-agents.md): delegation, limits, shared state, spend
- [Messages and events](docs/concepts/messages-events.md): Message, AgentMessage, AgentEvent
- [Context management](docs/concepts/context-management.md): limits, compaction
- [Prompt caching](docs/concepts/prompt-caching.md)
- [Retry](docs/concepts/retry.md): backoff, ProviderRetry, retry_safe_events
- [Structured outputs](docs/concepts/structured-outputs.md)
- [Pricing](docs/concepts/pricing.md): opt-in prices, cost buckets
- [Skills](docs/concepts/skills.md): AgentSkills SKILL.md loading
- [Session trees](docs/concepts/session-trees.md)
- [Persistence](docs/concepts/persistence.md) · [GASP](docs/concepts/gasp.md) · [Telemetry](docs/concepts/telemetry.md)
- [Callbacks](docs/concepts/callbacks.md): the older hooks
- [Decision models](docs/concepts/decision-models.md): typed judgments, tool gate, input guard

## Guides

- [Testing your agent](docs/guides/testing.md): MockProvider
- [MCP](docs/guides/mcp.md) · [OpenAPI](docs/guides/openapi.md)
- [WebAssembly and Cloudflare Workers](docs/guides/wasm-workers.md)

## Providers

- [Overview](docs/providers/overview.md) · [Model presets](docs/providers/model-presets.md)
- [Anthropic](docs/providers/anthropic.md) · [OpenAI-compatible](docs/providers/openai-compat.md) · [OpenAI Responses](docs/providers/openai-responses.md) · [Azure OpenAI](docs/providers/azure-openai.md) · [Google](docs/providers/google.md) · [Bedrock](docs/providers/bedrock.md) · [OpenCode](docs/providers/opencode.md)

## Companion crates

- [yoagent-rutis](https://github.com/yologdev/yoagent/tree/main/integrations/yoagent-rutis): plugins at runtime (Rust, TypeScript on Node or Bun, Python, Go); pi extensions and DeepSeek Harness plugins unchanged
- [Plugin compatibility](docs/evals/plugin-compatibility.md): measured, top 20 per ecosystem
- [yoagent-workers](https://github.com/yologdev/yoagent/tree/main/integrations/yoagent-workers): Cloudflare Workers bindings

## Optional

- [Architecture overview](docs/architecture/overview.md): module map
- [CHANGELOG](CHANGELOG.md)
227 changes: 227 additions & 0 deletions skills/yoagent/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
---
name: yoagent
description: Use when building, extending or upgrading an agent on the yoagent Rust crate. Maps each job (tools, policies, verifiers, budgets, sub-agents, plugins from other ecosystems, retries, structured output, tests) to the yoagent feature for it, with tips, pitfalls and what changed per version.
---

# yoagent: which feature for which job

Written for **yoagent 0.25.2** (and yoagent-rutis 0.3.0); the recipes
below are compiled by `tests/skill_test.rs`, line for line. Check what you
build against: `cargo tree -p yoagent` or `Cargo.lock`. If you are on an
older version, read [What changed](#what-changed) first.

yoagent is a loop library. It runs the loop (stream a reply, run the tools
it asks for, feed results back, repeat) and its guarantees: retries,
cancellation, limits, compaction, cost accounting. **Everything that
changes what the loop does goes through one contract, `Extension`.**
Before writing loop code of your own, find the hook below.

## Pick the feature

| You want to… | Use | Read |
|---|---|---|
| build an agent | `Agent::from_config(ModelConfig::…)` (provider and API key follow the config) | `docs/getting-started/quick-start.md` |
| give it tools | `impl AgentTool`; `.with_tools(…)` (built-ins: `tools::default_tools()`) | `docs/concepts/tools.md` |
| allow / rewrite / deny tool calls | `RunHooks::before_tool` in an `Extension` | `docs/concepts/extensions.md` |
| redact or edit a tool's output | `RunHooks::after_tool` (+ `.filters_tool_output()`) | same |
| refuse an input before the run | `RunHooks::on_input` | same |
| add a note to every model request | `RunHooks::before_model` → `TurnDecision::Note` | same |
| check the final answer, send the agent back | `RunHooks::on_stop` → `StopDecision::Continue(why)` | same |
| count, log or record what happened | `RunHooks::on_event` (sync, cheap), `finish` | same |
| cap spend in dollars | `extension::Budget` | `docs/concepts/extensions.md#budget` |
| cap turns / tokens / time | `ExecutionLimits` | `docs/concepts/context-management.md` |
| delegate to a child agent | `SubAgentTool` (own model, tools, limits) | `docs/concepts/sub-agents.md` |
| apply one policy to every sub-agent | `.with_tree_extension(…)` | `docs/concepts/extensions.md` |
| offer tools that change between runs | `ToolSource` (consulted once per run) | `docs/concepts/tools.md` |
| load plugins at runtime (Rust, TS on Node/Bun, Python, Go), or pi / DeepSeek Harness plugins unchanged | the **yoagent-rutis** crate (`RutisBridge`, one `Extension`) | [yoagent-rutis README](https://github.com/yologdev/yoagent/tree/main/integrations/yoagent-rutis) |
| use MCP servers | `.with_mcp_server_stdio(…)` / `_http(…)` / `_http_transport(…)` | `docs/guides/mcp.md` |
| get typed JSON back | `agent.prompt_structured::<T>(text, schema)` | `docs/concepts/structured-outputs.md` |
| steer a running agent | `agent.steer(msg)` (between tool batches), `follow_up(msg)` | `docs/concepts/agent-loop.md` |
| show streamed text without retry duplicates | `retry::retry_safe_events(rx)` | `docs/concepts/retry.md` |
| branch / restore history | `session::Session` (a tree, JSONL) | `docs/concepts/session-trees.md` |
| know what it cost | `agent.total_cost_usd()`, `SessionStats` buckets | `docs/concepts/pricing.md` |
| test without a network | `MockProvider` + `Agent::from_provider` | `docs/guides/testing.md` |
| run on Cloudflare Workers | `default-features = false` (wasm32) | `docs/guides/wasm-workers.md` |

## Recipes

### A policy (deny, rewrite)

```rust
use yoagent::extension::{ClonedHooks, RunHooks};
use yoagent::{ToolCallRequest, ToolDecision};

#[derive(Clone)]
struct ProtectGuards;

#[async_trait::async_trait]
impl RunHooks for ProtectGuards {
async fn before_tool(&self, call: &ToolCallRequest<'_>) -> ToolDecision {
let path = call.args["path"].as_str().unwrap_or_default();
if path.starts_with(".github/") {
ToolDecision::Deny("CI files are off limits".into())
} else {
ToolDecision::Allow
}
}
}

let agent = agent.with_extension(ClonedHooks::new("protect-guards", ProtectGuards).required());
```

`ClonedHooks` clones the hooks per run, so state starts fresh every run.
`.required()` makes a failing hook fail the run (default: advisory, the
failure is logged and that hook skipped).

### A verifier (don't accept "done" without evidence)

```rust
use yoagent::extension::{RunHooks, StopContext, StopDecision};
use yoagent::{AgentMessage, Content, Message};

#[derive(Clone)]
struct RequireTests;

#[async_trait::async_trait]
impl RunHooks for RequireTests {
async fn on_stop(&mut self, stop: &StopContext<'_>) -> StopDecision {
let ran_tests = stop.messages.iter().any(|m| match m {
AgentMessage::Llm(Message::Assistant { content, .. }) => content.iter().any(|c| {
matches!(c, Content::ToolCall { name, arguments, .. }
if name == "bash" && arguments["command"].as_str().is_some_and(|cmd| cmd.contains("cargo test")))
}),
_ => false,
});
if ran_tests {
StopDecision::Accept
} else {
StopDecision::Continue("Run `cargo test` before saying you are done.".into())
}
}
}
```

Read the evidence from `stop.messages` (the conversation so far). Don't
try to set a flag from `on_event`: it takes `&self`, and a shared
`Arc<AtomicBool>` would carry over between runs, since `ClonedHooks`
clones the `Arc`. Continues are capped per run (`max_stop_continues`,
default 3); a required extension that still continues at the cap fails the
run.

### A dollar budget

```rust
use yoagent::extension::Budget;

yoagent::provider::prices::enable_bundled(); // pricing is opt-in since 0.25
let config = ModelConfig::claude_sonnet_5();
if let Some(budget) = Budget::for_model(2.00, &config) { // None when unpriced
agent = agent.with_extension(budget.across_runs()); // one total for the session
}
```

It is checked before every model call: once the spend has reached the
limit, the run stops there (one call may cross it). It counts sub-agents,
decision models and compaction too.

### A sub-agent with its own limits, under the parent's policy

```rust
use yoagent::context::ExecutionLimits;

let child = SubAgentTool::from_config("researcher", ModelConfig::claude_sonnet_5())
.with_system_prompt("Research, then report.")
.with_execution_limits(ExecutionLimits::default().with_max_turns(20));
let agent = agent
.with_tools(vec![Box::new(child)])
.with_tree_extension(ClonedHooks::new("protect-guards", ProtectGuards).required());
```

### Plugins from other ecosystems (yoagent-rutis)

Plugins register a handler with the bridge; the bridge is one `Extension`.
pi extensions and DeepSeek Harness (DSH) plugins load unchanged through
adapters, as far as yoagent can honour them; what it cannot (UI, commands)
is ignored, and what would decide things yoagent cannot enforce is
**refused**. Measured: of the 20 most-downloaded packages, pi 12 and DSH 4
are usable (`docs/evals/plugin-compatibility.md`). Start from the
[yoagent-rutis README](https://github.com/yologdev/yoagent/tree/main/integrations/yoagent-rutis)
and its examples.

## Tips and pitfalls

- **Pricing is opt-in (0.25).** Call `prices::enable_bundled()` once,
*before* building configs, on a path tests reach too (tests never run
`main`). Without it every cost is `None`, `Budget::for_model` is `None`.
- **`with_tools` replaces the list.** An agent starts with no tools; the
built-ins come from `tools::default_tools()`. `with_mcp_server_*` adds to
that list, so call `with_tools` first — a later `with_tools` drops the
MCP tools.
- **Policies fail closed.** A `before_tool` that panics, or is still
running when the run is cancelled, denies the call (a plugin policy
through yoagent-rutis also denies on an error or its timeout); an
`after_tool` that fails withholds the output. A deny is a normal tool
result the model reads and adapts to.
- **`before_tool`, `after_tool`, `on_event` take `&self`** and run
concurrently for parallel calls; keep shared state in a `Mutex`/atomic.
- **Never rewrite the system prompt per turn.** It breaks the prompt cache.
Per-turn context goes in `before_model` notes (added to the latest user
turn, never stored).
- **Cancelling**: a cancel during a model call ends that message as
`StopReason::Aborted`; a cancel while tools run or between turns appends
`[Agent stopped: cancelled]`. Either way `on_error` is not called. A
cancelled sub-agent arrives as `ToolError::Cancelled` (cancelled before
its first answer), `Sub-agent '<name>' failed: [Agent stopped: cancelled]`,
or `Sub-agent '<name>' failed: Cancelled` (mid-stream) — key on the cancel
token, not the text.
- **`bash` runs in its own process group (0.25.2)**: the terminal's Ctrl+C
does not reach a running command any more. A terminal host must catch
Ctrl+C and call `agent.abort()`; the cancel then kills the whole group.
- **Retries emit `AgentEvent::ProviderRetry`.** A streamed attempt that
failed was already shown; look at the next event before treating an error
`MessageEnd` as final, or use `retry_safe_events`.
- **Limits are per run** and `max_total_tokens` counts the whole prompt
every turn, so it fills faster than the visible conversation.
- **Spend is bucketed**: the agent's own, `sub_agents`, `decision`,
`compaction`; `total_cost_usd()` adds them. Unknown cost is `None`,
never zero.
- **Plugin messages are capped at 16 MiB** (rutis 0.9 closes the channel
above that). Large images in a conversation reach plugins as text.
- **Tests**: script whole multi-turn tool conversations with `MockProvider`
(`Agent::from_provider(MockProvider::new(…), ModelConfig::mock())`); it
honours cancellation, so abort and steering paths are testable.
- **The older hooks still work** (`ToolMiddleware`, input filters,
`TurnHook`, callbacks), but new work should use `Extension`: one place,
per-run state, sub-agent coverage.

## What changed

Newest first; the full list is `CHANGELOG.md`.

- **0.25.2** — MCP `HttpTransport::with_header`; `bash` kills its process
group (Ctrl+C: see above).
- **0.25.1** — GASP records plugin log lines on their run
(`GaspRecorder::extension()` + `plugin_log_layer()`).
- **0.25.0** — `Extension` / `RunHooks` (the contract above), `Budget`;
pricing opt-in (`enable_bundled`); `AgentMessage::Extension` renamed
`Custom`; `ToolGate` / `InputGuard` run as extensions; encrypted
reasoning opt-in for the Responses API. **Breaking for event consumers:**
every message now gets its `MessageStart` / `MessageEnd` (a cancel during
a retry backoff emits an `Aborted` end), every `TurnStart` a `TurnEnd`,
and a cancelled run no longer runs its pending tool calls. **Breaking for
free-function users:** `AgentLoopConfig` is `#[non_exhaustive]` — build it
with `AgentLoopConfig::new(…)`; `provider::sse` is private.
- **0.24.3** — `SubAgentTool::with_execution_limits`; 503 / 529 and
in-stream overloads retried; the `[Agent stopped: cancelled]` marker.
- **0.24.2** — built-in tool sandbox fixes (path escapes, `search` flag
injection, Gemini key out of the URL, tool panics contained).
- **0.24.1** — `retry_safe_events`.
- **yoagent-rutis 0.3.0** — rutis 0.9 (Rust 1.88); Bun and Go plugins;
the 16 MiB guard.

## More

The book: <https://yologdev.github.io/yoagent/> (source `docs/`), the
design philosophy (`docs/design-philosophy.md`), the API reference
(<https://docs.rs/yoagent>), and `llms.txt` at the repository root for a
map of every page.
Loading
Loading