1use serde::{Deserialize, Serialize};
16use serde_json::Value;
17
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20pub struct MuseRecord {
21 pub schema_version: u32,
23 pub id: String,
25 pub stream: StreamRef,
27 pub sequence: u64,
29 pub recorded_at: u64,
31 pub record_type: RecordType,
32 pub durability: Durability,
33 pub causation_id: String,
35 pub payload_type: String,
37 pub payload_schema_version: u32,
39 pub payload: Value,
41}
42
43impl MuseRecord {
44 pub fn typed_payload(&self) -> serde_json::Result<MusePayload> {
50 MusePayload::from_parts(&self.payload_type, self.payload.clone())
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
56pub struct StreamRef {
57 pub kind: StreamKind,
58 pub id: String,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
63#[serde(rename_all = "snake_case")]
64pub enum StreamKind {
65 Session,
66 Run,
67 Task,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
72#[serde(rename_all = "snake_case")]
73pub enum RecordType {
74 Reconciliation,
76 Event,
78 Status,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
84#[serde(rename_all = "snake_case")]
85pub enum Durability {
86 Durable,
87 Ephemeral,
88}
89
90#[derive(Debug, Clone, PartialEq)]
92pub enum MusePayload {
93 CommandAccepted(CommandAccepted),
95 SessionRunLinked(SessionRunLinked),
97 TurnInputUser(TurnInputUser),
99 RunStarted(RunStarted),
101 ModelConfigured(ModelConfigured),
103 RunOutputDelta(RunOutputDelta),
105 ToolResult(ToolResult),
107 RunTerminal(RunTerminal),
109 TaskStreamLinked(TaskStreamLinked),
111 TaskLifecycle(TaskLifecycle),
113 Unknown {
115 payload_type: String,
116 payload: Value,
117 },
118}
119
120impl MusePayload {
121 pub fn from_parts(payload_type: &str, payload: Value) -> serde_json::Result<Self> {
122 Ok(match payload_type {
123 "runtime.command.accepted" => {
124 MusePayload::CommandAccepted(serde_json::from_value(payload)?)
125 }
126 "session.run.linked" => MusePayload::SessionRunLinked(serde_json::from_value(payload)?),
127 "turn.input.user" => MusePayload::TurnInputUser(serde_json::from_value(payload)?),
128 "run.lifecycle.started" => MusePayload::RunStarted(serde_json::from_value(payload)?),
129 "run.model.configured" => {
130 MusePayload::ModelConfigured(serde_json::from_value(payload)?)
131 }
132 "tool.result" => MusePayload::ToolResult(serde_json::from_value(payload)?),
133 "run.output.delta" => MusePayload::RunOutputDelta(serde_json::from_value(payload)?),
134 t if t.starts_with("run.terminal.") => {
135 MusePayload::RunTerminal(serde_json::from_value(payload)?)
136 }
137 "task.stream.linked" => MusePayload::TaskStreamLinked(serde_json::from_value(payload)?),
138 t if t.starts_with("task.lifecycle.") => {
139 MusePayload::TaskLifecycle(serde_json::from_value(payload)?)
140 }
141 other => MusePayload::Unknown {
142 payload_type: other.to_string(),
143 payload,
144 },
145 })
146 }
147}
148
149#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
152pub struct CommandAccepted {
153 pub kind: String,
154 pub command_id: String,
155 pub command_kind: String,
156 pub client_id: Option<String>,
157 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
158 pub extra: serde_json::Map<String, Value>,
159}
160
161#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
163pub struct SessionRunLinked {
164 pub kind: String,
165 pub command_id: String,
166 pub run_stream: StreamRef,
167 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
168 pub extra: serde_json::Map<String, Value>,
169}
170
171#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
173pub struct TurnInputUser {
174 pub kind: String,
175 pub command_id: String,
176 pub prompt: String,
177 pub run_stream: StreamRef,
178 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
179 pub extra: serde_json::Map<String, Value>,
180}
181
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
184pub struct RunStarted {
185 pub kind: String,
186 pub command_id: String,
187 pub prompt: String,
188 pub run_stream: StreamRef,
189 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
190 pub extra: serde_json::Map<String, Value>,
191}
192
193#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
195pub struct RunOutputDelta {
196 pub kind: String,
197 pub command_id: String,
198 pub run_stream: StreamRef,
199 pub text: String,
200 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
201 pub extra: serde_json::Map<String, Value>,
202}
203
204#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
207pub struct ModelConfigured {
208 pub kind: String,
209 pub command_id: String,
210 pub run_stream: StreamRef,
211 pub model_id: String,
212 pub display_label: String,
213 pub profile_id: String,
214 pub provider_id: String,
215 pub source: String,
217 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
218 pub extra: serde_json::Map<String, Value>,
219}
220
221#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
223pub struct ToolResult {
224 pub kind: String,
225 pub command_id: String,
226 pub run_stream: StreamRef,
227 pub call_id: String,
229 pub text: String,
231 pub correlation_facts: Value,
233 #[serde(default, skip_serializing_if = "Option::is_none")]
235 pub edit_facts: Option<Value>,
236 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
237 pub extra: serde_json::Map<String, Value>,
238}
239
240#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
244pub struct RunTerminal {
245 pub kind: String,
246 pub command_id: String,
247 pub run_stream: StreamRef,
248 pub terminal: String,
249 pub reason: Option<String>,
250 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub text: Option<String>,
252 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
253 pub extra: serde_json::Map<String, Value>,
254}
255
256#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
258pub struct TaskStreamLinked {
259 pub kind: String,
260 pub command_id: String,
261 pub run_stream: StreamRef,
262 pub task_id: String,
263 pub task_stream: StreamRef,
264 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
265 pub extra: serde_json::Map<String, Value>,
266}
267
268#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
270pub struct TaskLifecycle {
271 pub kind: String,
272 pub command_id: String,
273 pub run_stream: StreamRef,
274 pub task_id: String,
275 pub task_stream: StreamRef,
276 pub event: TaskLifecycleEvent,
277 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
278 pub extra: serde_json::Map<String, Value>,
279}
280
281#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
286#[serde(tag = "kind", rename_all = "snake_case")]
287pub enum TaskLifecycleEvent {
288 Proposed {
289 task_id: String,
290 task_kind: String,
293 },
294 Accepted {
295 task_id: String,
296 },
297 Started {
298 task_id: String,
299 #[serde(default, skip_serializing_if = "Option::is_none")]
301 span_id: Option<String>,
302 },
303 Scheduled {
304 task_id: String,
305 idempotency_key: String,
306 },
307 SideEffectIntent {
308 task_id: String,
309 idempotency_key: String,
310 operation: String,
311 policy_decision: String,
312 parent_task_id: Option<String>,
313 cancellation_handle: Option<Value>,
314 },
315 Status {
318 task_id: String,
319 message: String,
320 details: Value,
321 },
322 Output {
324 task_id: String,
325 chunk: String,
326 },
327 Completed {
328 task_id: String,
329 },
330 Cancelled {
331 task_id: String,
332 reason: String,
333 },
334 Rejected {
335 task_id: String,
336 reason: String,
337 },
338 Failed {
339 task_id: String,
340 reason: String,
341 },
342 #[serde(untagged)]
344 Unknown(Value),
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350 use serde_json::json;
351
352 #[test]
353 fn unknown_payload_type_is_preserved_not_error() {
354 let p = MusePayload::from_parts("subagent.lifecycle.spawned", json!({"x": 1})).unwrap();
355 match p {
356 MusePayload::Unknown {
357 payload_type,
358 payload,
359 } => {
360 assert_eq!(payload_type, "subagent.lifecycle.spawned");
361 assert_eq!(payload, json!({"x": 1}));
362 }
363 other => panic!("expected Unknown, got {other:?}"),
364 }
365 }
366
367 #[test]
368 fn task_lifecycle_failed_carries_reason() {
369 let e: TaskLifecycleEvent = serde_json::from_value(json!({
370 "kind": "failed",
371 "task_id": "t1",
372 "reason": "provider does not support base instructions"
373 }))
374 .unwrap();
375 assert!(matches!(e, TaskLifecycleEvent::Failed { ref reason, .. }
376 if reason.contains("base instructions")));
377 }
378}