1use std::sync::{Arc, Mutex, PoisonError};
4
5use serde::{Deserialize, Serialize};
6use starweaver_core::{
7 AgentEvent, AgentExecutionNode, AgentId, ConversationId, Metadata, RunId,
8 RunLifecycle as RunStatus, TaskId,
9};
10use starweaver_model::{ModelResponse, ModelResponseStreamEvent, ToolCallPart, ToolReturnPart};
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 RunCancelled {
257 run_id: RunId,
259 reason: String,
261 },
262 RunFailed {
264 run_id: RunId,
266 error_kind: String,
268 message: String,
270 },
271}
272
273impl AgentStreamEvent {
274 #[must_use]
276 pub fn sideband_event(&self) -> Option<AgentSidebandEvent> {
277 match self {
278 Self::Custom { event } => AgentSidebandEvent::from_agent_event(event),
279 _ => None,
280 }
281 }
282}
283
284#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
286#[serde(rename_all = "snake_case")]
287pub enum AgentStreamSourceKind {
288 Subagent,
290}
291
292#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
294pub struct AgentStreamSource {
295 pub kind: AgentStreamSourceKind,
297 pub agent_id: AgentId,
299 pub agent_name: String,
301 #[serde(default, skip_serializing_if = "Option::is_none")]
303 pub task_id: Option<TaskId>,
304 #[serde(default, skip_serializing_if = "Option::is_none")]
306 pub run_id: Option<RunId>,
307 #[serde(default, skip_serializing_if = "Option::is_none")]
309 pub parent_run_id: Option<RunId>,
310 pub source_sequence: usize,
312}
313
314impl AgentStreamSource {
315 #[must_use]
317 pub fn subagent(
318 agent_id: AgentId,
319 agent_name: impl Into<String>,
320 task_id: TaskId,
321 run_id: Option<RunId>,
322 parent_run_id: Option<RunId>,
323 source_sequence: usize,
324 ) -> Self {
325 Self {
326 kind: AgentStreamSourceKind::Subagent,
327 agent_id,
328 agent_name: agent_name.into(),
329 task_id: Some(task_id),
330 run_id,
331 parent_run_id,
332 source_sequence,
333 }
334 }
335}
336
337#[derive(Clone, Debug, Default)]
339pub struct AgentStreamSink {
340 records: Arc<Mutex<Vec<AgentStreamRecord>>>,
341}
342
343impl AgentStreamSink {
344 pub fn push(&self, record: AgentStreamRecord) {
346 self.records
347 .lock()
348 .unwrap_or_else(PoisonError::into_inner)
349 .push(record);
350 }
351
352 pub fn extend(&self, records: impl IntoIterator<Item = AgentStreamRecord>) {
354 self.records
355 .lock()
356 .unwrap_or_else(PoisonError::into_inner)
357 .extend(records);
358 }
359
360 #[must_use]
362 pub fn drain(&self) -> Vec<AgentStreamRecord> {
363 self.records
364 .lock()
365 .unwrap_or_else(PoisonError::into_inner)
366 .drain(..)
367 .collect()
368 }
369
370 #[must_use]
372 pub fn is_empty(&self) -> bool {
373 self.records
374 .lock()
375 .unwrap_or_else(PoisonError::into_inner)
376 .is_empty()
377 }
378}
379
380#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
382pub struct AgentStreamRecord {
383 pub sequence: usize,
385 #[serde(default, skip_serializing_if = "Option::is_none")]
387 pub source: Option<AgentStreamSource>,
388 pub event: AgentStreamEvent,
390}
391
392impl starweaver_core::VersionedRecord for AgentStreamRecord {
393 const SCHEMA: &'static str = "starweaver.runtime.stream_record";
394 const ALLOW_BARE_V0: bool = true;
395}
396
397impl AgentStreamRecord {
398 #[must_use]
400 pub const fn new(sequence: usize, event: AgentStreamEvent) -> Self {
401 Self {
402 sequence,
403 source: None,
404 event,
405 }
406 }
407
408 #[must_use]
410 pub fn with_source(mut self, source: AgentStreamSource) -> Self {
411 self.source = Some(source);
412 self
413 }
414
415 #[must_use]
417 pub const fn with_sequence(mut self, sequence: usize) -> Self {
418 self.sequence = sequence;
419 self
420 }
421
422 pub fn to_raw_json(&self) -> serde_json::Result<serde_json::Value> {
428 serde_json::to_value(self)
429 }
430}