# motosan-agent-loop

> Standalone ReAct agent loop for Rust — LlmClient + AgentLoop with no platform dependencies. Bring your own LLM backend, register tools, and let AgentLoop handle iterative reasoning and tool execution.

- Rust 0.3.0
- GitHub: https://github.com/motosan-dev/motosan-agent-loop
- crates.io: https://crates.io/crates/motosan-agent-loop
- docs.rs: https://docs.rs/motosan-agent-loop

## Install

```toml
[dependencies]
motosan-agent-loop = "0.3"

# Optional: enable LlmClient impl for motosan-ai::Client
motosan-agent-loop = { version = "0.3", features = ["motosan-ai"] }
```

## Features

| Feature      | Description                                        |
|--------------|----------------------------------------------------|
| `motosan-ai` | Enables `LlmClient` impl for `motosan-ai::Client` |

---

## Core Types

### AgentLoop

The core ReAct loop. Drives an LLM through iterative reasoning + tool execution until a final text answer or max iterations.

```rust
use motosan_agent_loop::{AgentLoop, AgentEvent, Message};

let agent = AgentLoop::builder()
    .tool(my_tool)              // Arc<dyn Tool>
    .tools(vec![tool_a, tool_b]) // batch register
    .context(my_provider)       // impl ContextProvider
    .contexts(vec![...])        // batch register
    .max_iterations(10)         // default: 10
    .build();

let result = agent
    .run(&llm, vec![Message::user("Hello")], |event| {
        match &event {
            AgentEvent::ToolStarted { name } => println!("started: {name}"),
            AgentEvent::ToolCompleted { name, result } => println!("done: {name}"),
            AgentEvent::TextChunk(text) => print!("{text}"),
        }
    })
    .await?;

println!("{}", result.answer);       // final text answer
println!("{:?}", result.tool_calls); // Vec<(name, args)>
println!("{}", result.iterations);   // LLM round-trips
```

### LlmClient (trait)

Implement for your LLM backend (OpenAI, Anthropic, local models, test doubles).

```rust
use async_trait::async_trait;
use motosan_agent_loop::{LlmClient, LlmResponse, Message, Result};
use motosan_agent_tool::ToolDef;

#[async_trait]
impl LlmClient for MyBackend {
    async fn chat(&self, messages: &[Message], tools: &[ToolDef]) -> Result<LlmResponse> {
        // Call your LLM API here
        Ok(LlmResponse::Message("response".into()))
    }
}
```

### LlmResponse

```rust
enum LlmResponse {
    Message(String),              // plain text reply
    ToolCalls(Vec<ToolCallItem>), // model wants to call tools
}

// Convenience for single tool call:
LlmResponse::single_tool_call(id, name, args)
```

### ToolCallItem

```rust
struct ToolCallItem {
    id: String,                // unique call ID
    name: String,              // tool name
    args: serde_json::Value,   // parsed JSON arguments
}
```

### Message

```rust
Message::system("You are helpful")
Message::user("Hello")
Message::assistant("Hi there")
Message::assistant_with_tool_call("Let me check", tool_call_ref)
Message::assistant_with_tool_calls("Checking multiple", vec![...])
Message::tool_result("call_id", "result content")
```

### ToolCallRef

Stored on assistant messages to correlate tool calls with results.

```rust
struct ToolCallRef {
    id: String,
    name: String,
    args: serde_json::Value,
}
```

### ContextProvider (trait)

Inject dynamic context (RAG docs, user profiles, etc.) as system messages before each run.

```rust
use async_trait::async_trait;
use motosan_agent_loop::{ContextProvider, Result};

#[async_trait]
impl ContextProvider for MyRagProvider {
    async fn build(&self, query: &str) -> Result<String> {
        // Fetch relevant docs for the query
        Ok("Relevant context here".into())
    }
}
```

- Called once per `AgentLoop::run()`, before the main loop
- Non-empty results become `System` messages prepended to conversation
- Empty strings are skipped
- Multiple providers: invoked in registration order

### AgentEvent

Observability callbacks emitted during the loop.

```rust
enum AgentEvent {
    ToolStarted { name: String },
    ToolCompleted { name: String, result: ToolResult },
    TextChunk(String),
}
```

### AgentError

```rust
enum AgentError {
    Llm(String),           // LLM backend error
    Tool(String),          // tool invocation error
    Serde(serde_json::Error), // JSON serialization error
    MaxIterations(usize),  // loop exceeded max iterations
}
```

---

## Tool Registration

Tools implement `motosan_agent_tool::Tool` trait:

```rust
use motosan_agent_tool::{Tool, ToolDef, ToolContext, ToolResult};
use std::sync::Arc;

let tool: Arc<dyn Tool> = Arc::new(MyTool::new());

let agent = AgentLoop::builder()
    .tool(tool)
    .build();
```

- Tools are executed concurrently when the LLM requests multiple tool calls
- Unknown tool names produce an error `ToolResult` (not a panic)

---

## Dependency Tree

```
motosan-agent-loop
  +-- motosan-agent-tool   (Tool / ToolDef / ToolResult traits)
  +-- async-trait
  +-- futures              (join_all for parallel tool execution)
  +-- serde / serde_json
  +-- thiserror
```

---

## Release

Tag convention: `vX.Y.Z` → triggers `publish.yml` → crates.io

```bash
# 1. Bump Cargo.toml version
# 2. Update CHANGELOG.md
# 3. Commit
git add Cargo.toml CHANGELOG.md
git commit -m "chore: release vX.Y.Z"

# 4. Tag + push
git tag -a vX.Y.Z -m "vX.Y.Z — summary"
git push origin main vX.Y.Z
```

See `docs/release.md` for full checklist.
