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.
Crates
| Crate | Description |
|---|---|
ag-ui-protocol | Wire-compatible data types for all 33 AG-UI events, messages, capabilities, and input payloads. Zero runtime deps. |
ag-ui-server | Server 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_ERRORemitted automatically - Protocol verification โ ordering violations are caught and surfaced as
RUN_ERRORbefore reaching the client - HITL support โ pause a run with
RunOutcome::Interrupt, resume viainput.resumeentries - Typed streaming handles โ
TextMessageHandleandToolCallHandleprevent 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