yoagent 0.25.9

Simple, effective agent loop with tool execution and event streaming
Documentation
# Testing Your Agent

An agent built on yoagent can be tested without a network or an API key:
`MockProvider` plays the model, returning scripted responses one turn at a
time, and everything else (the loop, your tools, middleware, events) runs for
real. The examples below are compiled and run as
[`tests/testing_guide_test.rs`](https://github.com/yologdev/yoagent/blob/main/tests/testing_guide_test.rs).

## A scripted run

Give `MockProvider` the turns you expect, pair it with `ModelConfig::mock()`,
and assert on the events or the final history:

```rust
use yoagent::provider::mock::{MockResponse, MockToolCall};
use yoagent::provider::{MockProvider, ModelConfig};
use yoagent::*;

#[tokio::test]
async fn the_agent_calls_the_tool_then_answers() {
    // Each response is one model turn, in order.
    let provider = MockProvider::new(vec![
        MockResponse::ToolCalls(vec![MockToolCall {
            name: "echo".into(),
            arguments: serde_json::json!({"text": "hi"}),
            provider_metadata: None,
        }]),
        MockResponse::Text("The tool said hi.".into()),
    ]);
    let mut agent =
        Agent::from_provider(provider, ModelConfig::mock()).with_tools(vec![Box::new(Echo)]);

    let mut rx = agent.prompt("say hi through the tool").await;
    let mut tool_output = None;
    while let Some(event) = rx.recv().await {
        if let AgentEvent::ToolExecutionEnd { result, .. } = event {
            tool_output = result.content.first().cloned();
        }
    }
    agent.finish().await;

    assert!(matches!(tool_output, Some(Content::Text { text }) if text == "hi"));
}
```

`Echo` is an ordinary `AgentTool`; the test file has it in full. For a single
text answer, `MockProvider::text("…")` is enough.

`MockProvider` also checks that every request pairs tool calls with their
results (no unanswered call, no stray result, nothing in between), the shape a
real provider would reject. On a violation it panics with an explanation;
under `Agent::prompt` that panic is in the spawned loop task, so the run ends
early and your assertions on its output fail. When a malformed sequence is
the point of the test, opt out:
`MockProvider::new(responses).without_transcript_validation()`.

## A middleware, without an agent

`ToolCallRequest::new(id, name, &args)` builds the request a
[`ToolMiddleware`](../concepts/tools.md) receives, so a policy can be tested
on its own:

```rust
struct NoRm;

#[async_trait::async_trait]
impl ToolMiddleware for NoRm {
    async fn before_tool(&self, call: &ToolCallRequest<'_>) -> ToolDecision {
        let command = call.args["command"].as_str().unwrap_or("");
        if call.tool_name == "bash" && command.contains("rm ") {
            ToolDecision::Deny("no deleting".into())
        } else {
            ToolDecision::Allow
        }
    }
}

#[tokio::test]
async fn the_middleware_denies_rm() {
    let args = serde_json::json!({"command": "rm -rf build"});
    let call = ToolCallRequest::new("call-1", "bash", &args);
    assert!(matches!(NoRm.before_tool(&call).await, ToolDecision::Deny(_)));
}
```

When the policy depends on what the user asked, give the request that
context: `.with_run_prompts(&prompts)` for the run's prompts, or
`.with_messages(&history)` for the whole conversation. A policy should read
`call.user_request_parts()`; `user_request()` is prose whose format may
change.

## Abort

`abort()` cancels the run; `finish()` waits for it and leaves the agent
usable. This shows the call pattern — with an instant mock the run may well
finish before the abort lands:

```rust
#[tokio::test]
async fn an_abort_ends_the_run() {
    let mut agent = Agent::from_provider(MockProvider::text("hello"), ModelConfig::mock());
    let mut rx = agent.prompt("hi").await;
    agent.abort();
    while rx.recv().await.is_some() {}
    agent.finish().await;
    assert!(!agent.is_streaming());
}
```

## Beyond the mock

- **Streaming and provider quirks** are tested against a local HTTP server
  (`wiremock`) serving hand-built bodies in each provider's wire format (SSE;
  binary event stream for Bedrock) — see `tests/*_stream_test.rs` in the
  repository.
- **Live runs** belong in an example run on demand, not in `cargo test`: the
  repository's `examples/release_smoke.rs` checks a real provider end to end.