7.10 agui: AG-UI 1.0 events and SSE encoder
// crates/agui/src/lib.rs (depends on axum, serde, serde_json)
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RunAgentInput {
pub thread_id: String,
pub run_id: String,
#[serde(default)] pub state: Value,
#[serde(default)] pub messages: Vec<Value>,
#[serde(default)] pub tools: Vec<Value>,
#[serde(default)] pub context: Vec<Value>,
#[serde(default)] pub forwarded_props: Value,
#[serde(default)] pub resume: Vec<ResumeEntry>,
pub protocol_version: Option<String>, // absent => pre-1.0 client
}
#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ResumeEntry { pub interrupt_id: String, pub status: String, pub payload: Option<Value> }
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Interrupt {
pub id: String, pub reason: String,
#[serde(skip_serializing_if = "Option::is_none")] pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] pub response_schema: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")] pub expires_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] pub metadata: Option<Value>,
}
#[derive(Debug, Serialize, Clone)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum RunOutcome { Success, Interrupt { interrupts: Vec<Interrupt> } }
/// Absent fields are omitted, never null (AG-UI 1.0 rule).
#[derive(Debug, Serialize, Clone)]
#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE", rename_all_fields = "camelCase")]
pub enum Event {
RunStarted { thread_id: String, run_id: String, #[serde(skip_serializing_if = "Option::is_none")] protocol_version: Option<String> },
RunFinished { thread_id: String, run_id: String,
#[serde(skip_serializing_if = "Option::is_none")] result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")] outcome: Option<RunOutcome> },
RunError { message: String, #[serde(skip_serializing_if = "Option::is_none")] code: Option<String> },
StepStarted { step_name: String },
StepFinished { step_name: String },
TextMessageStart { message_id: String, role: String },
TextMessageContent { message_id: String, delta: String },
TextMessageEnd { message_id: String },
ToolCallStart { tool_call_id: String, tool_call_name: String, #[serde(skip_serializing_if = "Option::is_none")] parent_message_id: Option<String> },
ToolCallArgs { tool_call_id: String, delta: String },
ToolCallEnd { tool_call_id: String },
ToolCallResult { message_id: String, tool_call_id: String, content: String },
StateSnapshot { snapshot: Value },
StateDelta { delta: Vec<Value> }, // RFC 6902 JSON Patch operations
MessagesSnapshot { messages: Vec<Value> },
Custom { name: String, value: Value },
}
pub fn supports_interrupt_outcome(input: &RunAgentInput) -> bool {
input.protocol_version.as_deref()
.is_some_and(|v| v.split('.').next().and_then(|m| m.parse::<u32>().ok()).unwrap_or(0) >= 1)
}
/// Axum SSE encoding: one `data:` line per JSON event.
pub fn to_sse(ev: &Event) -> axum::response::sse::Event {
axum::response::sse::Event::default().data(serde_json::to_string(ev).expect("serializable"))
}
Once ag-ui PR #2778 publishes an ag-ui 1.0 crate, replace this module with a re-export. A conformance test checks the JSON against the AG-UI 1.0 schema.