ag-ui-rust

July 24, 2026 ยท View on GitHub

A Rust implementation of the AG-UI (Agent-User Interaction) protocol โ€” the open, event-based protocol for streaming structured events between AI agents and frontend applications.

๐ŸŒ ag-ui-rust.arlo-ai.app

Crates

CrateDescription
ag-ui-protocolWire-compatible data types for all 33 AG-UI events, messages, capabilities, and input payloads. Zero runtime deps.
ag-ui-serverServer runtime: Agent trait, lifecycle pipeline, protocol verification, SSE encoding, and optional Axum integration.

Quick Start

[dependencies]
ag-ui-server = { version = "0.1", features = ["axum"] }
use ag_ui_server::axum::agent_router;
use ag_ui_server::{Agent, AgentError, EventEmitter, RunOutcome};
use ag_ui_protocol::RunAgentInput;

struct MyAgent;

impl Agent for MyAgent {
    async fn run(
        &self,
        input: RunAgentInput,
        emitter: EventEmitter,
    ) -> Result<RunOutcome, AgentError> {
        let msg = emitter.start_text_message().await?;
        msg.content("Hello from Rust!").await?;
        msg.end().await?;
        Ok(RunOutcome::success())
    }
}

#[tokio::main]
async fn main() {
    let listener = tokio::net::TcpListener::bind("0.0.0.0:8000").await.unwrap();
    axum::serve(listener, agent_router(MyAgent)).await.unwrap();
}

What You Get

  • Protocol fidelity โ€” 98% spec coverage, wire-compatible with the TypeScript and Python SDKs
  • Lifecycle framing โ€” RUN_STARTED / RUN_FINISHED / RUN_ERROR emitted automatically
  • Protocol verification โ€” ordering violations are caught and surfaced as RUN_ERROR before reaching the client
  • HITL support โ€” pause a run with RunOutcome::Interrupt, resume via input.resume entries
  • Typed streaming handles โ€” TextMessageHandle and ToolCallHandle prevent invalid event sequences at the type level
  • State management โ€” RFC 6902 JSON Patch diffing via diff_states()
  • Framework flexibility โ€” use the Axum integration or call run_agent() directly with any HTTP framework

HITL (Human-in-the-Loop)

The server crate has first-class support for interrupting a run to request human approval before executing a tool, then resuming with the user's decision.

Pause: return RunOutcome::Interrupt from your agent with one Interrupt per pending action:

use ag_ui_protocol::Interrupt;
use ag_ui_server::RunOutcome;

return Ok(RunOutcome::Interrupt(vec![Interrupt {
    id: tool_call_id.clone(),
    reason: "tool_call:read_file".into(),
    message: Some(format!("Agent wants to call read_file with args: {args}")),
    tool_call_id: Some(tool_call_id),
    response_schema: Some(serde_json::json!({
        "type": "object",
        "properties": { "approved": { "type": "boolean" } },
        "required": ["approved"]
    })),
    expires_at: None,
    metadata: None,
}]));

This produces a RUN_FINISHED event with outcome.type = "interrupt" in the SSE stream. The frontend shows an approval UI.

Resume: the client POSTs the next run with input.resume containing one ResumeEntry per interrupt:

// input.resume is populated by the client:
// [{ interruptId: "...", status: "resolved"|"cancelled", payload: {...} }]

if let Some(entries) = input.resume.take() {
    for entry in &entries {
        match entry.status {
            ResumeStatus::Resolved  => { /* execute the approved action */ }
            ResumeStatus::Cancelled => { /* skip โ€” inject a cancelled message */ }
        }
    }
}

Frontend: if you use @assistant-ui/react-ag-ui, the useAgUiInterrupts() and useAgUiSubmitInterruptResponses() hooks handle the interrupt/resume cycle automatically.

Project Structure

ag-ui-rust/
  crates/
    ag-ui-protocol/   # Pure data types (serde only)
    ag-ui-server/     # Async runtime (tokio + futures-util)

Building

cargo build --workspace
cargo test --workspace

License

MIT