182 lines
4.1 KiB
Rust
182 lines
4.1 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
use serde_json::Value;
|
|
|
|
/// Fine-grained agent lifecycle events, inspired by pi's event system.
|
|
///
|
|
/// Covers the full agent execution lifecycle with enough granularity
|
|
/// for UI rendering, telemetry, and extension hooks.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "type", rename_all = "snake_case")]
|
|
pub enum AgentEvent {
|
|
// === Agent lifecycle ===
|
|
AgentStart,
|
|
AgentEnd {
|
|
messages: Vec<AgentEventMessage>,
|
|
total_input_tokens: u64,
|
|
total_output_tokens: u64,
|
|
},
|
|
|
|
// === Turn lifecycle ===
|
|
TurnStart {
|
|
turn_index: usize,
|
|
},
|
|
TurnEnd {
|
|
turn_index: usize,
|
|
assistant_text: Option<String>,
|
|
tool_call_count: usize,
|
|
},
|
|
|
|
// === Message lifecycle ===
|
|
MessageStart {
|
|
role: MessageRole,
|
|
},
|
|
MessageTextDelta {
|
|
index: usize,
|
|
delta: String,
|
|
},
|
|
MessageThinkingDelta {
|
|
index: usize,
|
|
delta: String,
|
|
},
|
|
MessageEnd {
|
|
role: MessageRole,
|
|
},
|
|
|
|
// === Tool execution lifecycle ===
|
|
ToolExecutionStart {
|
|
tool_call_id: String,
|
|
tool_name: String,
|
|
arguments: Value,
|
|
},
|
|
ToolExecutionUpdate {
|
|
tool_call_id: String,
|
|
tool_name: String,
|
|
partial_output: String,
|
|
},
|
|
ToolExecutionEnd {
|
|
tool_call_id: String,
|
|
tool_name: String,
|
|
output: Option<Value>,
|
|
error: Option<String>,
|
|
elapsed_ms: i64,
|
|
},
|
|
|
|
// === Steering / follow-up ===
|
|
SteeringMessagesInjected {
|
|
count: usize,
|
|
},
|
|
FollowUpMessagesInjected {
|
|
count: usize,
|
|
},
|
|
|
|
// === Context management ===
|
|
ContextCompacted {
|
|
messages_compacted: usize,
|
|
tokens_saved: i64,
|
|
},
|
|
BranchSummaryCreated {
|
|
entry_count: usize,
|
|
summary_length: usize,
|
|
},
|
|
|
|
// === Model switching ===
|
|
ModelSwitched {
|
|
from_model: String,
|
|
to_model: String,
|
|
reason: String,
|
|
},
|
|
|
|
// === Error and retry ===
|
|
ErrorClassified {
|
|
category: String,
|
|
message: String,
|
|
will_retry: bool,
|
|
retry_delay_ms: Option<u64>,
|
|
},
|
|
RetryAttempt {
|
|
attempt: usize,
|
|
max_attempts: usize,
|
|
delay_ms: u64,
|
|
},
|
|
}
|
|
|
|
/// Simplified message role for event display.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum MessageRole {
|
|
User,
|
|
Assistant,
|
|
ToolResult,
|
|
System,
|
|
}
|
|
|
|
/// A simplified message representation for `AgentEnd` events.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AgentEventMessage {
|
|
pub role: MessageRole,
|
|
pub content: String,
|
|
pub tool_calls: Vec<EventToolCall>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct EventToolCall {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub arguments: Value,
|
|
pub output: Option<Value>,
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
/// An async-friendly event sink that collects or broadcasts events.
|
|
pub struct EventSink {
|
|
senders: Vec<tokio::sync::mpsc::UnboundedSender<AgentEvent>>,
|
|
}
|
|
|
|
impl EventSink {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
senders: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Subscribe to events, returns a receiver.
|
|
pub fn subscribe(
|
|
&mut self,
|
|
) -> tokio::sync::mpsc::UnboundedReceiver<AgentEvent> {
|
|
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
|
|
self.senders.push(tx);
|
|
rx
|
|
}
|
|
|
|
/// Emit an event to all subscribers. Non-blocking; drops if receiver disconnected.
|
|
pub fn emit(&self, event: AgentEvent) {
|
|
for sender in &self.senders {
|
|
let _ = sender.send(event.clone());
|
|
}
|
|
}
|
|
|
|
/// Check if there are any active subscribers.
|
|
pub fn has_subscribers(&self) -> bool {
|
|
!self.senders.is_empty()
|
|
}
|
|
|
|
/// Remove disconnected senders.
|
|
pub fn cleanup(&mut self) {
|
|
self.senders.retain(|s| !s.is_closed());
|
|
}
|
|
}
|
|
|
|
impl Default for EventSink {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl Clone for EventSink {
|
|
fn clone(&self) -> Self {
|
|
Self {
|
|
senders: self.senders.clone(),
|
|
}
|
|
}
|
|
}
|