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::{AgentResult, executor::AgentExecutionNode, run::RunStatus};
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 HostEvent,
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"
91 | "model_stream_resume"
92 | "model_transport_selected"
93 | "model_transport_fallback" => Some(AgentSidebandEventCategory::Model),
94 "tools_unavailable"
95 | "toolset_initialized"
96 | "toolset_unavailable"
97 | "toolset_failed"
98 | "toolset_refreshed"
99 | "toolset_closed" => Some(AgentSidebandEventCategory::Tool),
100 "tool_search_loaded" | "tool_search_initialized" | "tool_search_refreshed" => {
101 Some(AgentSidebandEventCategory::ToolSearch)
102 }
103 "hitl_resolved" => Some(AgentSidebandEventCategory::Hitl),
104 "skills_scanned" | "skill_activated" | "skills_reloaded" => {
105 Some(AgentSidebandEventCategory::Skill)
106 }
107 "task_snapshot" => Some(AgentSidebandEventCategory::Task),
108 "usage_snapshot" => Some(AgentSidebandEventCategory::Usage),
109 "compact_start" | "compact_failed" | "compact_complete" => {
110 Some(AgentSidebandEventCategory::Compact)
111 }
112 "steering_received" | "steering_submitted" => {
113 Some(AgentSidebandEventCategory::Steering)
114 }
115 "goal_iteration" | "goal_complete" => Some(AgentSidebandEventCategory::Goal),
116 "background_shell_complete" => Some(AgentSidebandEventCategory::Background),
117 "message_received" => Some(AgentSidebandEventCategory::Message),
118 "subagent_started" | "subagent_completed" | "subagent_failed" => {
119 Some(AgentSidebandEventCategory::Subagent)
120 }
121 _ if kind.starts_with("model_") => Some(AgentSidebandEventCategory::Model),
122 _ if kind.starts_with("task_") => Some(AgentSidebandEventCategory::Task),
123 _ if kind.starts_with("note_") => Some(AgentSidebandEventCategory::Note),
124 _ if kind.starts_with("file_") => Some(AgentSidebandEventCategory::File),
125 _ if kind.starts_with("media_") => Some(AgentSidebandEventCategory::Media),
126 _ if kind.starts_with("host_") => Some(AgentSidebandEventCategory::HostEvent),
127 _ if kind.starts_with("tool_search_") => Some(AgentSidebandEventCategory::ToolSearch),
128 _ if kind.starts_with("tool_") || kind.starts_with("toolset_") => {
129 Some(AgentSidebandEventCategory::Tool)
130 }
131 _ if kind.starts_with("approval_")
132 || kind.starts_with("deferred_")
133 || kind.starts_with("hitl_") =>
134 {
135 Some(AgentSidebandEventCategory::Hitl)
136 }
137 _ if kind.starts_with("skill_") || kind.starts_with("skills_") => {
138 Some(AgentSidebandEventCategory::Skill)
139 }
140 _ if kind.starts_with("subagent_") => Some(AgentSidebandEventCategory::Subagent),
141 _ if kind.starts_with("message_") => Some(AgentSidebandEventCategory::Message),
142 _ if kind.starts_with("usage_") => Some(AgentSidebandEventCategory::Usage),
143 _ if kind.starts_with("compact_") => Some(AgentSidebandEventCategory::Compact),
144 _ if kind.starts_with("steering_") => Some(AgentSidebandEventCategory::Steering),
145 _ if kind.starts_with("goal_") => Some(AgentSidebandEventCategory::Goal),
146 _ if kind.starts_with("background_") => Some(AgentSidebandEventCategory::Background),
147 _ if kind.starts_with("capability_") => Some(AgentSidebandEventCategory::Capability),
148 _ => None,
149 }
150 }
151}
152
153#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
155#[serde(tag = "kind", rename_all = "snake_case")]
156pub enum AgentStreamEvent {
157 RunStart {
159 run_id: RunId,
161 conversation_id: ConversationId,
163 },
164 NodeStart {
166 node: AgentExecutionNode,
168 step: usize,
170 status: RunStatus,
172 },
173 NodeComplete {
175 node: AgentExecutionNode,
177 step: usize,
179 status: RunStatus,
181 },
182 Custom {
184 event: AgentEvent,
186 },
187 ModelRequest {
189 step: usize,
191 },
192 ModelStream {
194 step: usize,
196 event: ModelResponseStreamEvent,
198 },
199 ModelResponse {
201 step: usize,
203 response: ModelResponse,
205 },
206 Checkpoint {
208 node: AgentExecutionNode,
210 step: usize,
212 },
213 Suspended {
215 node: AgentExecutionNode,
217 reason: String,
219 },
220 ToolCall {
222 step: usize,
224 call: ToolCallPart,
226 },
227 ToolReturn {
229 step: usize,
231 tool_return: ToolReturnPart,
233 },
234 OutputRetry {
236 retries: usize,
238 prompt: String,
240 },
241 SteeringGuard {
243 step: usize,
245 prompt: String,
247 },
248 RunComplete {
250 run_id: RunId,
252 output: String,
254 },
255 RunFailed {
257 run_id: RunId,
259 error_kind: String,
261 message: String,
263 },
264}
265
266impl AgentStreamEvent {
267 #[must_use]
269 pub fn sideband_event(&self) -> Option<AgentSidebandEvent> {
270 match self {
271 Self::Custom { event } => AgentSidebandEvent::from_agent_event(event),
272 _ => None,
273 }
274 }
275}
276
277#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
279#[serde(rename_all = "snake_case")]
280pub enum AgentStreamSourceKind {
281 Subagent,
283}
284
285#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
287pub struct AgentStreamSource {
288 pub kind: AgentStreamSourceKind,
290 pub agent_id: AgentId,
292 pub agent_name: String,
294 #[serde(default, skip_serializing_if = "Option::is_none")]
296 pub task_id: Option<TaskId>,
297 #[serde(default, skip_serializing_if = "Option::is_none")]
299 pub run_id: Option<RunId>,
300 #[serde(default, skip_serializing_if = "Option::is_none")]
302 pub parent_run_id: Option<RunId>,
303 pub source_sequence: usize,
305}
306
307impl AgentStreamSource {
308 #[must_use]
310 pub fn subagent(
311 agent_id: AgentId,
312 agent_name: impl Into<String>,
313 task_id: TaskId,
314 run_id: Option<RunId>,
315 parent_run_id: Option<RunId>,
316 source_sequence: usize,
317 ) -> Self {
318 Self {
319 kind: AgentStreamSourceKind::Subagent,
320 agent_id,
321 agent_name: agent_name.into(),
322 task_id: Some(task_id),
323 run_id,
324 parent_run_id,
325 source_sequence,
326 }
327 }
328}
329
330#[derive(Clone, Debug, Default)]
332pub struct AgentStreamSink {
333 records: Arc<Mutex<Vec<AgentStreamRecord>>>,
334}
335
336impl AgentStreamSink {
337 pub fn push(&self, record: AgentStreamRecord) {
339 self.records
340 .lock()
341 .unwrap_or_else(PoisonError::into_inner)
342 .push(record);
343 }
344
345 pub fn extend(&self, records: impl IntoIterator<Item = AgentStreamRecord>) {
347 self.records
348 .lock()
349 .unwrap_or_else(PoisonError::into_inner)
350 .extend(records);
351 }
352
353 #[must_use]
355 pub fn drain(&self) -> Vec<AgentStreamRecord> {
356 self.records
357 .lock()
358 .unwrap_or_else(PoisonError::into_inner)
359 .drain(..)
360 .collect()
361 }
362
363 #[must_use]
365 pub fn is_empty(&self) -> bool {
366 self.records
367 .lock()
368 .unwrap_or_else(PoisonError::into_inner)
369 .is_empty()
370 }
371}
372
373#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
375pub struct AgentStreamRecord {
376 pub sequence: usize,
378 #[serde(default, skip_serializing_if = "Option::is_none")]
380 pub source: Option<AgentStreamSource>,
381 pub event: AgentStreamEvent,
383}
384
385impl AgentStreamRecord {
386 #[must_use]
388 pub const fn new(sequence: usize, event: AgentStreamEvent) -> Self {
389 Self {
390 sequence,
391 source: None,
392 event,
393 }
394 }
395
396 #[must_use]
398 pub fn with_source(mut self, source: AgentStreamSource) -> Self {
399 self.source = Some(source);
400 self
401 }
402
403 #[must_use]
405 pub const fn with_sequence(mut self, sequence: usize) -> Self {
406 self.sequence = sequence;
407 self
408 }
409
410 pub fn to_raw_json(&self) -> serde_json::Result<serde_json::Value> {
416 serde_json::to_value(self)
417 }
418}
419
420#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
422pub struct AgentStreamResult {
423 pub result: AgentResult,
425 pub events: Vec<AgentStreamRecord>,
427}
428
429impl AgentStreamResult {
430 #[must_use]
432 pub fn events(&self) -> &[AgentStreamRecord] {
433 &self.events
434 }
435
436 #[must_use]
438 pub const fn result(&self) -> &AgentResult {
439 &self.result
440 }
441
442 pub fn raw_json_records(&self) -> serde_json::Result<Vec<serde_json::Value>> {
448 self.events
449 .iter()
450 .map(AgentStreamRecord::to_raw_json)
451 .collect()
452 }
453}
454
455pub(crate) fn push_stream_event(
456 events: &mut Option<&mut Vec<AgentStreamRecord>>,
457 event: AgentStreamEvent,
458) {
459 if let Some(events) = events.as_deref_mut() {
460 events.push(AgentStreamRecord::new(events.len(), event));
461 }
462}
463
464pub(crate) fn push_stream_record(
465 events: &mut Option<&mut Vec<AgentStreamRecord>>,
466 record: AgentStreamRecord,
467) {
468 if let Some(events) = events.as_deref_mut() {
469 events.push(record.with_sequence(events.len()));
470 }
471}