agentix 0.10.2

Multi-provider LLM client for Rust — streaming, non-streaming, tool calls, MCP, DeepSeek, OpenAI, Anthropic, Gemini
Documentation

agentix

crates.io docs.rs license

Multi-provider LLM client for Rust — streaming, non-streaming, tool calls, agentic loops, and MCP support.

DeepSeek · OpenAI · Anthropic · Gemini · Kimi · GLM · MiniMax · Grok — one unified API.


Philosophy: Stream as Agent Structure

An agent is not an object. It is a Stream.

agentix models agents as lazy, composable streams rather than stateful objects or DAG frameworks:

// token-level stream — full control, live progress
let mut stream = agent(tools, http, request, history, None);
while let Some(event) = stream.next().await { ... }

// turn-level stream — one CompleteResponse per LLM turn
let result = agent_turns(tools, http, request, history, None)
    .last_content().await;

// multi-agent pipeline — just Rust concurrency
let findings = join_all(questions.iter().map(|q| {
    agent_turns(tools.clone(), http.clone(), request.clone(), vec![q], None)
        .last_content()
})).await;

Concurrency is join_all. Pipelines are sequential .await. No orchestrator, no DAG, no magic — just streams composed with ordinary Rust.


Quick Start

use agentix::{Request, LlmEvent};
use futures::StreamExt;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let http = reqwest::Client::new();

    let mut stream = Request::deepseek(std::env::var("DEEPSEEK_API_KEY")?)
        .system_prompt("You are a helpful assistant.")
        .user("What is the capital of France?")
        .stream(&http)
        .await?;

    while let Some(event) = stream.next().await {
        match event {
            LlmEvent::Token(t) => print!("{t}"),
            LlmEvent::Done     => break,
            _ => {}
        }
    }
    println!();
    Ok(())
}

vs. other frameworks

agentix rig llm-chain LangGraph
Language Rust Rust Rust Python
Agentic loop agent() manual manual ✅ graph nodes
Multi-agent pipeline join_all + streams manual manual ✅ graph edges
Streaming tokens
Streaming tool calls
MCP support ✅ (partial)
Proc-macro tools #[tool] #[rig_tool]
Concurrent tool execution
Provider support 8 10+ 4 30+
Agent abstraction Stream Object Chain DAG

vs LangGraph: LangGraph models agents as DAGs with explicit nodes and edges. agentix models them as Streams — no graph definition, no state schema, no framework lock-in. Multi-agent pipelines are just join_all and sequential .await.

vs rig's #[rig_tool]: rig requires one annotated function per tool, with descriptions passed as attribute arguments and return type fixed to Result<T, ToolError>. agentix uses doc comments for descriptions, accepts any return type, and lets you group related tools in a single impl block with shared state:

// rig: one #[rig_tool] per function
#[rig_tool(
    description = "Add two numbers",
    params(a = "first number", b = "second number")
)]
fn add(a: i32, b: i32) -> Result<i32, rig::tool::ToolError> { Ok(a + b) }

#[rig_tool(
    description = "Multiply two numbers",
    params(a = "first number", b = "second number")
)]
fn multiply(a: i32, b: i32) -> Result<i32, rig::tool::ToolError> { Ok(a * b) }

// agentix: one #[tool] for the whole impl block, descriptions from doc comments
struct MathTools { precision: u8 }  // shared state across all methods

#[tool]
impl Tool for MathTools {
    /// Add two numbers.
    /// a: first number  b: second number
    async fn add(&self, a: f64, b: f64) -> f64 { ... }

    /// Multiply two numbers.
    /// a: first number  b: second number
    async fn multiply(&self, a: f64, b: f64) -> f64 { ... }
}

// standalone fn also works — doc comment = description
/// Square root of x.
/// x: input value
#[tool]
async fn sqrt(x: f64) -> f64 { x.sqrt() }

let bundle = sqrt + MathTools { precision: 4 };  // compose with +

Installation

[dependencies]
agentix = "0.9"

# Optional: Model Context Protocol (MCP) tool support
# agentix = { version = "0.9", features = ["mcp"] }

Providers

Eight built-in providers, all using the same API:

use agentix::Request;

// Shortcut constructors (provider + default model in one call)
let req = Request::deepseek("sk-...");
let req = Request::openai("sk-...");
let req = Request::anthropic("sk-ant-...");
let req = Request::gemini("AIza...");
let req = Request::kimi("...");       // Moonshot AI — kimi-k2.5
let req = Request::glm("...");        // Zhipu AI — glm-5
let req = Request::minimax("...");    // MiniMax — MiniMax-M2.7 (Anthropic API)
let req = Request::grok("xai-...");

// Any OpenAI-compatible endpoint (e.g. OpenRouter)
let req = Request::openai("sk-or-...")
    .base_url("https://openrouter.ai/api/v1")
    .model("openrouter/free");

Request API

Request is a self-contained value type — it carries provider, credentials, model, messages, tools, and tuning. Call stream() or complete() with a shared reqwest::Client.

stream() — streaming completion

let http = reqwest::Client::new();
let mut stream = Request::new(Provider::OpenAI, "sk-...")
    .system_prompt("You are helpful.")
    .user("Hello!")
    .stream(&http)
    .await?;

while let Some(event) = stream.next().await {
    match event {
        LlmEvent::Token(t)         => print!("{t}"),
        LlmEvent::Reasoning(r)     => print!("[think] {r}"),
        LlmEvent::ToolCall(tc)     => println!("tool: {}({})", tc.name, tc.arguments),
        LlmEvent::Usage(u)         => println!("tokens: {}", u.total_tokens),
        LlmEvent::Error(e)         => eprintln!("error: {e}"),
        LlmEvent::Done             => break,
        _                          => {}
    }
}

complete() — non-streaming completion

let resp = Request::new(Provider::OpenAI, "sk-...")
    .user("What is 2+2?")
    .complete(&http)
    .await?;
println!("{}", resp.content.unwrap_or_default());
println!("reasoning: {:?}", resp.reasoning);
println!("tool_calls: {:?}", resp.tool_calls);
println!("usage: {:?}", resp.usage);

Builder methods

let req = Request::new(Provider::DeepSeek, "sk-...")
    .model("deepseek-reasoner")
    .base_url("https://custom.api/v1")
    .system_prompt("You are helpful.")
    .max_tokens(4096)
    .temperature(0.7)
    .retries(5, 2000)           // max retries, initial delay ms
    .user("Hello!")             // convenience for adding a user message
    .message(msg)               // add any Message variant
    .messages(vec![...])        // set full history
    .tools(tool_defs);          // set tool definitions

LlmEvent (what you receive from stream())

  • Token(String) — incremental response text
  • Reasoning(String) — thinking/reasoning trace (e.g. DeepSeek-R1)
  • ToolCallChunk(ToolCallChunk) — partial tool call for real-time UI
  • ToolCall(ToolCall) — completed tool call
  • Usage(UsageStats) — token usage for the turn
  • Done — stream ended
  • Error(String) — provider error

Defining Tools

Two styles are supported: standalone function (simpler) and impl block (multiple tools in one struct).

Standalone function

use agentix::tool;

/// Add two numbers.
/// a: first number
/// b: second number
#[agentix::tool]
async fn add(a: i64, b: i64) -> i64 {
    a + b
}

/// Divide a by b.
#[agentix::tool]
async fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 { Err("division by zero".into()) } else { Ok(a / b) }
}

// Combine with + operator
let tools = add + divide;
let mut stream = agentix::agent(tools, http, request, history, Some(25_000));

The macro generates a unit struct with the same name as the function and implements Tool for it.

Impl block (multiple methods per struct)

struct Calculator;

#[tool]
impl agentix::Tool for Calculator {
    /// Add two numbers.
    /// a: first number
    /// b: second number
    async fn add(&self, a: i64, b: i64) -> i64 {
        a + b
    }

    /// Divide a by b.
    async fn divide(&self, a: f64, b: f64) -> Result<f64, String> {
        if b == 0.0 { Err("division by zero".into()) } else { Ok(a / b) }
    }
}
  • Doc comment → tool description
  • /// param: description lines → argument descriptions
  • Result::Err automatically propagates as {"error": "..."} to the LLM

Streaming tools

Add #[streaming] to yield ToolOutput::Progress / ToolOutput::Result incrementally:

use agentix::{tool, ToolOutput};

struct ProgressTool;

#[tool]
impl agentix::Tool for ProgressTool {
    /// Run a long job and stream progress.
    /// steps: number of steps
    #[streaming]
    fn long_job(&self, steps: u32) {
        async_stream::stream! {
            for i in 1..=steps {
                yield ToolOutput::Progress(format!("{i}/{steps}"));
            }
            yield ToolOutput::Result(serde_json::json!({ "done": true }));
        }
    }
}

Normal and streaming methods can be freely mixed in the same #[tool] block.


MCP Tools

Use external processes as tools via the Model Context Protocol:

use agentix::McpTool;
use std::time::Duration;

let tool = McpTool::stdio("npx", &["-y", "@playwright/mcp"]).await?
    .with_timeout(Duration::from_secs(60));

// Add to a ToolBundle alongside regular tools
let mut bundle = agentix::ToolBundle::new();
bundle.push(tool);

Runtime add / remove

let mut bundle = agentix::ToolBundle::default();
bundle += Calculator;          // AddAssign — add tool in-place
bundle -= Calculator;          // SubAssign — remove all functions Calculator provides
let bundle2 = bundle + Calculator - Calculator;  // Sub — returns new bundle

Structured Output

Constrain the model to emit JSON matching a Rust struct using Request::json_schema(). Derive schemars::JsonSchema on your struct and pass the generated schema:

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, JsonSchema)]
struct Review {
    rating: f32,
    summary: String,
    pros: Vec<String>,
}

let schema = serde_json::to_value(schemars::schema_for!(Review))?;

let response = Request::openai(api_key)
    .system_prompt("You are a film critic.")
    .user("Review Inception (2010).")
    .json_schema("review", schema, true)   // strict=true enforces the schema
    .complete(&http)
    .await?;

let review: Review = response.json()?;

See examples/08_structured_output.rs for a runnable example.

Provider support:

  • OpenAI — full json_schema support (gpt-4o and later)
  • GeminiresponseSchema + responseMimeType: application/json (fully supported)
  • DeepSeekjson_object only; json_schema is automatically degraded with a tracing::warn
  • Anthropicresponse_format is ignored; use prompt engineering instead

Reliability

  • Automatic retries — exponential backoff for 429 / 5xx responses
  • Usage tracking — per-request token accounting across all providers; AgentEvent::Done contains cumulative totals across all turns

Agent (agentic loop)

agentix::agent() drives the full LLM ↔ tool-call loop and yields typed AgentEvents. Pass it a ToolBundle, a base Request, and an initial history — it handles repeated LLM calls, tool execution, and history accumulation automatically.

use agentix::{AgentEvent, Request, Provider, ToolBundle};
use futures::StreamExt;

#[tokio::main]
async fn main() {
    let http = reqwest::Client::new();
    let request = Request::new(Provider::DeepSeek, std::env::var("DEEPSEEK_API_KEY").unwrap())
        .system_prompt("You are helpful.");

    let mut stream = agentix::agent(ToolBundle::default(), http, request, vec![], None);
    while let Some(event) = stream.next().await {
        match event {
            AgentEvent::Token(t)                          => print!("{t}"),
            AgentEvent::ToolCallStart(tc)                 => println!("{}({})", tc.name, tc.arguments),
            AgentEvent::ToolResult { name, content, .. }  => println!("← [{name}] {content}"),
            AgentEvent::Usage(u)                          => println!("tokens: {}", u.total_tokens),
            AgentEvent::Error(e)                          => eprintln!("error: {e}"),
            _ => {}
        }
    }
}

AgentEvent variants

  • Token(String) — incremental response text
  • Reasoning(String) — thinking trace
  • ToolCallChunk(ToolCallChunk) — streaming partial tool call
  • ToolCallStart(ToolCall) — complete tool call, about to execute
  • ToolProgress { id, name, progress } — intermediate tool output
  • ToolResult { id, name, content } — final tool result
  • Usage(UsageStats) — token usage per LLM request
  • Done(UsageStats) — emitted once when the loop finishes normally; contains cumulative totals across all turns
  • Warning(String) — recoverable stream error
  • Error(String) — fatal error

agentix::agent() returns a BoxStream<'static, AgentEvent> — drop it to abort.


License

MIT OR Apache-2.0