agentwerk 0.1.0

A minimal Rust crate that gives any application agentic capabilities.
Documentation

Installation

cargo add agentwerk

Quick Start

use std::sync::Arc;
use agentwerk::{AgentBuilder, AnthropicProvider, GlobTool};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let (provider, model) = AnthropicProvider::from_env()?;

    let output = AgentBuilder::new()
        .provider(Arc::new(provider))
        .model(model)
        .instruction_prompt("Find all Rust source files.")
        .tool(GlobTool)
        .run()
        .await?;

    println!("{}", output.response_raw);
    Ok(())
}

Use Cases

Example applications built with this project.

Consider setting your LLM provider's environment variables for key, model or base URL.

Project Scanner

Two-phase pipeline: a discovery agent finds files worth reading, then a pipeline of agents summarizes each file in parallel.

make use_case name=project-scanner -- ./

Output:

{
  "languages": ["config", "docs", "rust"],
  "files": [
    {
      "file": "README.md",
      "summary": "Project documentation for agentwerk, a Rust crate for building agentic LLM applications.",
      "language": "docs"
    },
    {
      "file": "crates/agentwerk/src/agent/loop.rs",
      "summary": "Implements the main agent loop that calls an LLM iteratively and executes tool calls.",
      "language": "rust"
    }
  ]
}

Deep Research

Spawns three researcher sub-agents in parallel, then aggregates their findings into a structured decision. Requires BRAVE_API_KEY for web search.

make use_case name=deep-research args="What constitutes a good life?"

Output:

{
  "title": "What Constitutes a Good Life: A Multi-Perspective Analysis",
  "research": "A good life emerges from the convergence of philosophical wisdom, scientific research, and cultural understanding. Key elements include meaningful relationships and social connections, a sense of purpose and personal growth, physical and mental well-being, contributing to something beyond oneself, and living in accordance with personal values. While cultural contexts vary, common themes across traditions emphasize virtue, balance, gratitude, and the cultivation of both inner fulfillment and positive impact on others."
}

Model Pricing Tracker

Spawns a model checker and pricing researcher in parallel to gather current model pricing from provider websites, then outputs structured JSON.

make use_case name=model-pricing-tracker

Output:

{
  "models": [
    {
      "input_per_million": 3.0,
      "model_id": "claude-sonnet-4-20250514",
      "output_per_million": 15.0,
      "provider": "anthropic"
    },
    {
      "input_per_million": 1.0,
      "model_id": "claude-haiku-4-5-20251001",
      "output_per_million": 5.0,
      "provider": "anthropic"
    }
  ]
}

API

An agent is configured with a provider, model, tools, and prompt. Running it returns an output with the response and statistics. Events are emitted during execution for streaming and observability.

LlmProvider

Providers for Anthropic, OpenAI-compatible, Mistral, and LiteLLM. Each owns a reqwest::Client for connection pooling and SSE streaming.

use agentwerk::{AnthropicProvider, MistralProvider, OpenAiProvider, LiteLlmProvider};

let provider = AnthropicProvider::from_api_key(key);
let provider = MistralProvider::from_api_key(key);
let provider = OpenAiProvider::from_api_key(key);
let provider = LiteLlmProvider::from_api_key(key);

// share a connection pool
let client = reqwest::Client::new();
let provider = AnthropicProvider::new(key, client);

AgentBuilder

Configures the agent's identity, tools, provider, and runtime options.

use agentwerk::AgentBuilder;

let output = AgentBuilder::new()
    .identity_prompt("You are a helpful assistant.")
    .instruction_prompt("What does src/main.rs do?")
    .model("claude-sonnet-4-20250514")
    .tool(ReadFileTool)
    .provider(provider)
    .run()
    .await?;

Prompting

Method File variant Purpose
identity_prompt identity_prompt_file Persistent identity of the agent
instruction_prompt instruction_prompt_file Task for the current run
context_prompt context_prompt_file Additional context alongside the instruction
behavior_prompt behavior_prompt_file Behavioral directives appended to the system prompt
AgentBuilder::new()
    .identity_prompt_file("prompts/identity.md")
    .instruction_prompt("Summarize the project.")
    .behavior_prompt_file(BehaviorPrompt::Communication, "prompts/style.md")

Use {key} placeholders in the identity prompt and fill them with template_variable:

AgentBuilder::new()
    .identity_prompt("You are {role}. Respond in {language}.")
    .template_variable("role", json!("a code reviewer"))
    .template_variable("language", json!("German"))

Sub-agents

Building an agent returns a shareable handle for registration as a sub-agent. Without an explicit model, a sub-agent inherits its parent's model at runtime. Clone the builder to create multiple similar agents:

let researcher_base = AgentBuilder::new()
    .model("claude-haiku-4-5-20251001")
    .identity_prompt("Research this topic.")
    .tool(brave_search_tool())
    .max_turns(3);

let r1 = researcher_base.clone().name("researcher_1").build()?;
let r2 = researcher_base.clone().name("researcher_2").build()?;

let output = AgentBuilder::new()
    .name("orchestrator")
    .identity_prompt("Coordinate research.")
    .sub_agent(r1)
    .sub_agent(r2)

Guardrails

Set limits for agentic execution. You can set UNLIMITED to disable a limit.

Method Default What it does
.max_turns(10) UNLIMITED Stop after N agentic loop iterations
.max_tokens(4096) UNLIMITED Cap output tokens per LLM request
.max_schema_retries(3) 10 Retry structured output compliance
.max_request_retries(5) 3 Retry on transient API errors (429, 529, 5xx)
.request_retry_backoff_ms(2000) 10,000 Base delay for exponential backoff (ms * 2^attempt)
.cancel_signal(signal) Abort execution from outside the agent

Behavior prompts

Agents ship with default behavior prompts appended to the identity prompt. Override any:

Variant Default behavior
TaskExecution Read before modifying, don't add unrequested features, diagnose failures
ToolUsage Use dedicated tools over bash, parallelize independent calls
SafetyConcerns Consider reversibility and impact, prefer reversible operations
Communication Be direct, concise, lead with the answer
use agentwerk::BehaviorPrompt;

AgentBuilder::new()
    .behavior_prompt(BehaviorPrompt::TaskExecution, "Follow instructions exactly.")
    .behavior_prompt(BehaviorPrompt::Communication, "Always respond in JSON.")

Pipelines

Execute multiple agents with controlled parallelism. Each agent is fully configured with its own provider, prompts, and tools. Results are returned in push order. Individual failures do not abort the pipeline.

use agentwerk::{Pipeline, AgentBuilder, ReadFileTool};

let mut pipeline = Pipeline::new()
    .batch_size(10)
    .max_request_retries(5);

pipeline.push(
    AgentBuilder::new()
        .provider(provider.clone())
        .instruction_prompt("Summarize document A")
        .tool(ReadFileTool)
);
pipeline.push(
    AgentBuilder::new()
        .provider(provider.clone())
        .instruction_prompt("Summarize document B")
        .tool(ReadFileTool)
);

let results = pipeline.run().await;

Events

Emitted via AgentBuilder.event_handler() during execution.

Event Description
Agent AgentStart Agent begins execution
AgentEnd Agent finishes with turn count
AgentError Agent encountered an error
TurnStart / TurnEnd Turn boundaries
LLM Provider RequestStart / RequestEnd LLM request lifecycle
ResponseTextChunk Streamed text token
TokenUsage Token counts for a request
Tool Usage ToolCallStart / ToolCallEnd Tool execution lifecycle

Tools

Tools are functions the agent can call. Implement the Tool trait or use ToolBuilder for closures. Read-only tools run concurrently.

use agentwerk::{ToolBuilder, ToolResult};

let tool = ToolBuilder::new("greet", "Say hello")
    .schema(json!({...}))
    .read_only(true)
    .handler(|input, ctx| Box::pin(async move {
        Ok(ToolResult::success("Hello!"))
    }))
    .build();

Built-in tools:

Tool Description
File ReadFileTool Read a file with line numbers, offset, and limit
WriteFileTool Create or overwrite a file
EditFileTool Find-and-replace in a file
Search GlobTool Find files by pattern (e.g., **/*.rs)
GrepTool Search file contents by substring
ListDirectoryTool List directory entries with type and size
Shell BashTool Execute a shell command (unrestricted)
BashGlobTool Execute shell commands matching a glob pattern
Web WebFetchTool Fetch a URL and return its content as text
Utility SpawnAgentTool Delegate work to a sub-agent
TaskTool Persistent task management (create, update, list, get)
ToolSearchTool Discover available tools by keyword

AgentOutput

The result of running an agent.

output.response_raw            // free-form LLM text
output.response                // validated JSON if output_schema was set

output.statistics.input_tokens // total input tokens
output.statistics.output_tokens// total output tokens
output.statistics.requests     // number of LLM calls
output.statistics.tool_calls   // number of tool executions
output.statistics.turns        // number of agentic turns

With an output schema, the agent returns validated JSON:

let output = AgentBuilder::new()
    .output_schema(json!({
        "type": "object",
        "properties": { "category": { "type": "string" } },
        "required": ["category"]
    }))
    .max_schema_retries(3)  // retry if agent doesn't comply (default: 3)

    .run().await?;

output.response.unwrap()["category"]  // "billing"

Development

Building and testing

make                # build (warnings are errors)
make test           # unit tests
make fmt            # format code
make clean          # remove build artifacts
make update         # update dependencies

Integration tests

Consider configuring your LLM provider (see Environment).

make test_integration                     # run all
make test_integration name=bash_usage     # run one

Use cases

make use_case                                              # list available
make use_case name=project-scanner -- ./                   # run one
make use_case name=deep-research args="What is a good life?"  # with arguments

Publishing

make bump                  # bump patch version
make bump part=minor       # bump minor version
make publish               # publish to crates.io (runs tests first)

LiteLLM proxy

Start a local LiteLLM proxy on port 4000 that forwards to a provider. Requires Docker.

make litellm                       # default: anthropic
make litellm provider=openai       # use OpenAI
make litellm provider=mistral      # use Mistral

Environment

Use cases and integration tests pick up the LLM provider from these environment variables:

Anthropic

Variable Description
ANTHROPIC_API_KEY API key (required)
ANTHROPIC_BASE_URL API URL (default: https://api.anthropic.com)
ANTHROPIC_MODEL Model (default: claude-sonnet-4-20250514)

Mistral

Variable Description
MISTRAL_API_KEY API key (required)
MISTRAL_BASE_URL API URL (default: https://api.mistral.ai)
MISTRAL_MODEL Model (default: mistral-medium-2508)

OpenAI

Variable Description
OPENAI_API_KEY API key (required)
OPENAI_BASE_URL API URL (default: https://api.openai.com)
OPENAI_MODEL Model (default: gpt-4o)

LiteLLM proxy

Variable Description
LITELLM_API_URL Proxy URL (default: http://localhost:4000)
LITELLM_API_KEY Auth key (optional)
LITELLM_MODEL Model (default: claude-sonnet-4-20250514)