1use std::sync::{Arc, Mutex, PoisonError};
4
5use serde::{Deserialize, Serialize};
6use starweaver_context::AgentEvent;
7use starweaver_core::{AgentId, ConversationId, Metadata, RunId, TaskId};
8use starweaver_model::{ModelResponse, ModelResponseStreamEvent, ToolCallPart, ToolReturnPart};
9
10use crate::{executor::AgentExecutionNode, run::RunStatus, AgentResult};
11
12#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
14#[serde(rename_all = "snake_case")]
15pub enum AgentSidebandEventCategory {
16 Run,
18 Model,
20 Tool,
22 ToolSearch,
24 Hitl,
26 Skill,
28 Task,
30 Note,
32 File,
34 Media,
36 HostOperation,
38 Subagent,
40 Message,
42 Usage,
44 Compact,
46 Steering,
48 Goal,
50 Background,
52 Capability,
54}
55
56#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
58pub struct AgentSidebandEvent {
59 pub category: AgentSidebandEventCategory,
61 pub kind: String,
63 #[serde(default)]
65 pub payload: serde_json::Value,
66 #[serde(default, skip_serializing_if = "Metadata::is_empty")]
68 pub metadata: Metadata,
69}
70
71impl AgentSidebandEvent {
72 #[must_use]
74 pub fn from_agent_event(event: &AgentEvent) -> Option<Self> {
75 Self::category_for_kind(&event.kind).map(|category| Self {
76 category,
77 kind: event.kind.clone(),
78 payload: event.payload.clone(),
79 metadata: event.metadata.clone(),
80 })
81 }
82
83 #[must_use]
85 pub fn category_for_kind(kind: &str) -> Option<AgentSidebandEventCategory> {
86 match kind {
87 "run_start" | "run_complete" | "run_failed" | "run_waiting" | "run_cancelled" => {
88 Some(AgentSidebandEventCategory::Run)
89 }
90 "model_error_retry" | "model_stream_resume" => Some(AgentSidebandEventCategory::Model),
91 "tools_unavailable"
92 | "toolset_initialized"
93 | "toolset_unavailable"
94 | "toolset_failed"
95 | "toolset_refreshed"
96 | "toolset_closed" => Some(AgentSidebandEventCategory::Tool),
97 "tool_search_loaded" | "tool_search_initialized" | "tool_search_refreshed" => {
98 Some(AgentSidebandEventCategory::ToolSearch)
99 }
100 "hitl_resolved" => Some(AgentSidebandEventCategory::Hitl),
101 "skills_scanned" | "skill_activated" | "skills_reloaded" => {
102 Some(AgentSidebandEventCategory::Skill)
103 }
104 "task_snapshot" => Some(AgentSidebandEventCategory::Task),
105 "usage_snapshot" => Some(AgentSidebandEventCategory::Usage),
106 "compact_start" | "compact_failed" | "compact_complete" => {
107 Some(AgentSidebandEventCategory::Compact)
108 }
109 "steering_received" | "steering_submitted" => {
110 Some(AgentSidebandEventCategory::Steering)
111 }
112 "goal_iteration" | "goal_complete" => Some(AgentSidebandEventCategory::Goal),
113 "background_shell_complete" => Some(AgentSidebandEventCategory::Background),
114 "message_received" => Some(AgentSidebandEventCategory::Message),
115 "subagent_started" | "subagent_completed" | "subagent_failed" => {
116 Some(AgentSidebandEventCategory::Subagent)
117 }
118 _ if kind.starts_with("task_") => Some(AgentSidebandEventCategory::Task),
119 _ if kind.starts_with("note_") => Some(AgentSidebandEventCategory::Note),
120 _ if kind.starts_with("file_") => Some(AgentSidebandEventCategory::File),
121 _ if kind.starts_with("media_") => Some(AgentSidebandEventCategory::Media),
122 _ if kind.starts_with("host_") => Some(AgentSidebandEventCategory::HostOperation),
123 _ if kind.starts_with("tool_search_") => Some(AgentSidebandEventCategory::ToolSearch),
124 _ if kind.starts_with("tool_") || kind.starts_with("toolset_") => {
125 Some(AgentSidebandEventCategory::Tool)
126 }
127 _ if kind.starts_with("approval_")
128 || kind.starts_with("deferred_")
129 || kind.starts_with("hitl_") =>
130 {
131 Some(AgentSidebandEventCategory::Hitl)
132 }
133 _ if kind.starts_with("skill_") || kind.starts_with("skills_") => {
134 Some(AgentSidebandEventCategory::Skill)
135 }
136 _ if kind.starts_with("subagent_") => Some(AgentSidebandEventCategory::Subagent),
137 _ if kind.starts_with("message_") => Some(AgentSidebandEventCategory::Message),
138 _ if kind.starts_with("usage_") => Some(AgentSidebandEventCategory::Usage),
139 _ if kind.starts_with("compact_") => Some(AgentSidebandEventCategory::Compact),
140 _ if kind.starts_with("steering_") => Some(AgentSidebandEventCategory::Steering),
141 _ if kind.starts_with("goal_") => Some(AgentSidebandEventCategory::Goal),
142 _ if kind.starts_with("background_") => Some(AgentSidebandEventCategory::Background),
143 _ if kind.starts_with("capability_") => Some(AgentSidebandEventCategory::Capability),
144 _ => None,
145 }
146 }
147}
148
149#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
151#[serde(tag = "kind", rename_all = "snake_case")]
152pub enum AgentStreamEvent {
153 RunStart {
155 run_id: RunId,
157 conversation_id: ConversationId,
159 },
160 NodeStart {
162 node: AgentExecutionNode,
164 step: usize,
166 status: RunStatus,
168 },
169 NodeComplete {
171 node: AgentExecutionNode,
173 step: usize,
175 status: RunStatus,
177 },
178 Custom {
180 event: AgentEvent,
182 },
183 ModelRequest {
185 step: usize,
187 },
188 ModelStream {
190 step: usize,
192 event: ModelResponseStreamEvent,
194 },
195 ModelResponse {
197 step: usize,
199 response: ModelResponse,
201 },
202 Checkpoint {
204 node: AgentExecutionNode,
206 step: usize,
208 },
209 Suspended {
211 node: AgentExecutionNode,
213 reason: String,
215 },
216 ToolCall {
218 step: usize,
220 call: ToolCallPart,
222 },
223 ToolReturn {
225 step: usize,
227 tool_return: ToolReturnPart,
229 },
230 OutputRetry {
232 retries: usize,
234 prompt: String,
236 },
237 SteeringGuard {
239 step: usize,
241 prompt: String,
243 },
244 RunComplete {
246 run_id: RunId,
248 output: String,
250 },
251 RunFailed {
253 run_id: RunId,
255 error_kind: String,
257 message: String,
259 },
260}
261
262impl AgentStreamEvent {
263 #[must_use]
265 pub fn sideband_event(&self) -> Option<AgentSidebandEvent> {
266 match self {
267 Self::Custom { event } => AgentSidebandEvent::from_agent_event(event),
268 _ => None,
269 }
270 }
271}
272
273#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
275#[serde(rename_all = "snake_case")]
276pub enum AgentStreamSourceKind {
277 Subagent,
279}
280
281#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
283pub struct AgentStreamSource {
284 pub kind: AgentStreamSourceKind,
286 pub agent_id: AgentId,
288 pub agent_name: String,
290 #[serde(default, skip_serializing_if = "Option::is_none")]
292 pub task_id: Option<TaskId>,
293 #[serde(default, skip_serializing_if = "Option::is_none")]
295 pub run_id: Option<RunId>,
296 #[serde(default, skip_serializing_if = "Option::is_none")]
298 pub parent_run_id: Option<RunId>,
299 pub source_sequence: usize,
301}
302
303impl AgentStreamSource {
304 #[must_use]
306 pub fn subagent(
307 agent_id: AgentId,
308 agent_name: impl Into<String>,
309 task_id: TaskId,
310 run_id: Option<RunId>,
311 parent_run_id: Option<RunId>,
312 source_sequence: usize,
313 ) -> Self {
314 Self {
315 kind: AgentStreamSourceKind::Subagent,
316 agent_id,
317 agent_name: agent_name.into(),
318 task_id: Some(task_id),
319 run_id,
320 parent_run_id,
321 source_sequence,
322 }
323 }
324}
325
326#[derive(Clone, Debug, Default)]
328pub struct AgentStreamSink {
329 records: Arc<Mutex<Vec<AgentStreamRecord>>>,
330}
331
332impl AgentStreamSink {
333 pub fn push(&self, record: AgentStreamRecord) {
335 self.records
336 .lock()
337 .unwrap_or_else(PoisonError::into_inner)
338 .push(record);
339 }
340
341 pub fn extend(&self, records: impl IntoIterator<Item = AgentStreamRecord>) {
343 self.records
344 .lock()
345 .unwrap_or_else(PoisonError::into_inner)
346 .extend(records);
347 }
348
349 #[must_use]
351 pub fn drain(&self) -> Vec<AgentStreamRecord> {
352 self.records
353 .lock()
354 .unwrap_or_else(PoisonError::into_inner)
355 .drain(..)
356 .collect()
357 }
358
359 #[must_use]
361 pub fn is_empty(&self) -> bool {
362 self.records
363 .lock()
364 .unwrap_or_else(PoisonError::into_inner)
365 .is_empty()
366 }
367}
368
369#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
371pub struct AgentStreamRecord {
372 pub sequence: usize,
374 #[serde(default, skip_serializing_if = "Option::is_none")]
376 pub source: Option<AgentStreamSource>,
377 pub event: AgentStreamEvent,
379}
380
381impl AgentStreamRecord {
382 #[must_use]
384 pub const fn new(sequence: usize, event: AgentStreamEvent) -> Self {
385 Self {
386 sequence,
387 source: None,
388 event,
389 }
390 }
391
392 #[must_use]
394 pub fn with_source(mut self, source: AgentStreamSource) -> Self {
395 self.source = Some(source);
396 self
397 }
398
399 #[must_use]
401 pub const fn with_sequence(mut self, sequence: usize) -> Self {
402 self.sequence = sequence;
403 self
404 }
405}
406
407#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
409pub struct AgentStreamResult {
410 pub result: AgentResult,
412 pub events: Vec<AgentStreamRecord>,
414}
415
416impl AgentStreamResult {
417 #[must_use]
419 pub fn events(&self) -> &[AgentStreamRecord] {
420 &self.events
421 }
422
423 #[must_use]
425 pub const fn result(&self) -> &AgentResult {
426 &self.result
427 }
428}
429
430pub(crate) fn push_stream_event(
431 events: &mut Option<&mut Vec<AgentStreamRecord>>,
432 event: AgentStreamEvent,
433) {
434 if let Some(events) = events.as_deref_mut() {
435 events.push(AgentStreamRecord::new(events.len(), event));
436 }
437}
438
439pub(crate) fn push_stream_record(
440 events: &mut Option<&mut Vec<AgentStreamRecord>>,
441 record: AgentStreamRecord,
442) {
443 if let Some(events) = events.as_deref_mut() {
444 events.push(record.with_sequence(events.len()));
445 }
446}