wasm-agent 0.1.1

ReAct agent loop for WASM/edge environments: Thought-Action-Observation with tool dispatch
Documentation
# wasm-agent


[![crates.io](https://img.shields.io/crates/v/wasm-agent.svg)](https://crates.io/crates/wasm-agent)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

A ReAct (Thought, Action, Observation, Final Answer) agent loop in plain synchronous Rust, with no async runtime and no I/O, so it builds for `wasm32-unknown-unknown` and runs in browsers, edge workers and plugins.

Most agent frameworks assume Tokio, threads and an HTTP client. This crate keeps only the part that is pure logic: parse what the model said, dispatch the tool it asked for, record the observation, and keep the conversation within a token budget. You make the LLM call however your platform allows (`fetch` in a Worker, a host function in a plugin) and hand the response text to the runner.

## Features

- **`parse_react_step`**: parses `Thought: ...`, `Action: tool(input)`, `Action: tool: input` and `Final Answer: ...` into a `ReActStep`.
- **`ToolRegistry`**: register synchronous tools (`Box<dyn Fn(&str) -> ToolResult>`, no `Send` needed) with a `ToolSpec` (name, description, JSON schema), dispatch by name, and generate a tools section for the system prompt with `tools_prompt()`.
- **`LoopRunner`**: `step(llm_response)` parses one response, runs the tool on an `Action`, and appends both the response and the tool output to history. `run_scripted` replays a list of responses for deterministic tests.
- **`ConversationHistory`**: token-limited history that evicts the oldest non-system messages when full.
- **`AgentConfig`**: max iterations, context token limit, timeout and model name.
- Dependencies are only `serde`, `serde_json` and `thiserror`.

## Quick start

```bash
cargo add wasm-agent
```

```rust
use wasm_agent::{AgentConfig, LoopRunner, ReActStep, ToolRegistry, ToolSpec};
use wasm_agent::types::ToolResult;

fn main() -> Result<(), wasm_agent::AgentError> {
    let mut tools = ToolRegistry::new();
    tools.register(
        ToolSpec::new("capital", "Returns the capital of a country", r#"{"type":"string"}"#),
        Box::new(|country: &str| ToolResult {
            tool_name: "capital".into(),
            output: if country == "France" { "Paris".into() } else { "unknown".into() },
            success: true,
        }),
    )?;
    println!("{}", tools.tools_prompt()); // put this in your system prompt

    let config = AgentConfig::default();
    let mut runner = LoopRunner::new(&config, &tools);

    // In a real agent each string comes from your LLM call.
    runner.step("Thought: I should look up the capital.")?;
    runner.step("Action: capital(France)")?; // runs the tool, records "Paris"
    match runner.step("Final Answer: Paris")? {
        ReActStep::FinalAnswer(answer) => println!("answer: {answer}"),
        other => println!("not done: {other:?}"),
    }
    println!("{} messages in history", runner.history().len());
    Ok(())
}
```

Build for WebAssembly:

```bash
rustup target add wasm32-unknown-unknown
cargo build --target wasm32-unknown-unknown --release
```

## How it works

| File | What it holds |
|---|---|
| `src/loop_runner.rs` | `parse_react_step`, `LoopRunner` |
| `src/tools.rs` | `ToolRegistry`, `ToolSpec`, `ToolFn` |
| `src/history.rs` | `ConversationHistory` with token-limit eviction |
| `src/types.rs` | `ReActStep`, `Message`, `Role`, `AgentConfig`, `ToolResult` |
| `src/error.rs` | `AgentError` |

The runner never calls a model itself. That keeps it deterministic and testable, and lets the same code run natively or in WASM.

## Status and limitations

Version 0.1.

- No LLM client and no JS bindings are included; you supply the model call (for example via `wasm-bindgen` and `fetch`).
- Token counts are estimates, not a real tokenizer.
- `AgentConfig::timeout_ms` is stored but not enforced by the runner.
- The parser expects one step per response and matches the prefixes at the start of the text.

```bash
cargo test
cargo bench
```

## License

MIT, see [LICENSE](LICENSE).

---

Part of a set of Rust crates for LLM agents, see [rust-crates](https://gitlab.com/mattbusel/rust-crates).