1use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use std::collections::BTreeMap;
7
8pub const RUN_BUNDLE_SCHEMA: &str = "pi-workflows.run-run.v1";
9pub const RUN_STATE_SCHEMA: &str = "pi-workflows.run-state.v1";
10pub const DEFINITION_SNAPSHOT_SCHEMA: &str = "pi-workflows.definition-snapshot.v1";
11pub const SESSION_BINDING_SCHEMA: &str = "pi-workflows.session-binding.v1";
12pub const SESSION_EVENT_SCHEMA: &str = "pi-workflows.session-event.v1";
13pub const SESSION_CAPTURE_SCHEMA: &str = "pi-workflows.session-capture.v1";
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum RunStatus {
18 Queued,
19 Running,
20 Waiting,
21 Paused,
22 Completed,
23 Failed,
24 TimedOut,
25 Cancelled,
26 Ambiguous,
27}
28
29impl RunStatus {
30 pub fn is_terminal(self) -> bool {
31 matches!(
32 self,
33 RunStatus::Completed | RunStatus::Failed | RunStatus::TimedOut | RunStatus::Cancelled
34 )
35 }
36
37 pub fn label(self) -> &'static str {
38 match self {
39 RunStatus::Queued => "queued",
40 RunStatus::Running => "running",
41 RunStatus::Waiting => "waiting",
42 RunStatus::Paused => "paused",
43 RunStatus::Completed => "completed",
44 RunStatus::Failed => "failed",
45 RunStatus::TimedOut => "timed_out",
46 RunStatus::Cancelled => "cancelled",
47 RunStatus::Ambiguous => "ambiguous",
48 }
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(rename_all = "snake_case")]
54pub enum NodeOutcome {
55 Ok,
56 TimedOut,
57 Failed,
58 Cancelled,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(tag = "kind", rename_all = "snake_case")]
63pub enum WorkflowSource {
64 Builtin { id: String, revision: String },
65 File { path: String, hash: String },
66}
67
68impl WorkflowSource {
69 pub fn display(&self) -> String {
70 match self {
71 WorkflowSource::Builtin { id, revision } => format!("builtin:{id}@{revision}"),
72 WorkflowSource::File { path, .. } => path.clone(),
73 }
74 }
75}
76
77#[cfg(test)]
78mod workflow_source_tests {
79 use super::WorkflowSource;
80
81 #[test]
82 fn parses_and_displays_each_source_kind() {
83 let builtin: WorkflowSource =
84 serde_json::from_str(r#"{"kind":"builtin","id":"monitor","revision":"1"}"#)
85 .expect("built-in source should parse");
86 let file: WorkflowSource =
87 serde_json::from_str(r#"{"kind":"file","path":"/tmp/demo.workflow.ts","hash":"abc"}"#)
88 .expect("file source should parse");
89
90 assert_eq!(builtin.display(), "builtin:monitor@1");
91 assert_eq!(file.display(), "/tmp/demo.workflow.ts");
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96pub struct Manifest {
97 pub schema: String,
98 #[serde(rename = "runId")]
99 pub run_id: String,
100 #[serde(rename = "workflowName")]
101 pub workflow_name: String,
102 #[serde(rename = "runTitle", skip_serializing_if = "Option::is_none")]
103 pub run_title: Option<String>,
104 #[serde(rename = "workflowSource", skip_serializing_if = "Option::is_none")]
105 pub workflow_source: Option<WorkflowSource>,
106 #[serde(rename = "startedAt")]
107 pub started_at: String,
108 #[serde(rename = "finishedAt", skip_serializing_if = "Option::is_none")]
109 pub finished_at: Option<String>,
110 pub status: RunStatus,
111 #[serde(rename = "traceSchema")]
112 pub trace_schema: String,
113 pub paths: ManifestPaths,
114}
115
116#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
117pub struct ManifestPaths {
118 pub workflow: String,
119 pub state: String,
120 pub trace: String,
121 #[serde(skip_serializing_if = "Option::is_none")]
122 pub session: Option<String>,
123 #[serde(skip_serializing_if = "Option::is_none")]
124 pub artifacts: Option<String>,
125}
126
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
128pub struct RunState {
129 pub schema: String,
130 #[serde(rename = "traceSeq")]
131 pub trace_seq: u64,
132 #[serde(rename = "runId")]
133 pub run_id: String,
134 #[serde(rename = "workflowName")]
135 pub workflow_name: String,
136 #[serde(rename = "runTitle", skip_serializing_if = "Option::is_none")]
137 pub run_title: Option<String>,
138 #[serde(rename = "workflowSource", skip_serializing_if = "Option::is_none")]
139 pub workflow_source: Option<WorkflowSource>,
140 #[serde(rename = "parentRunId", skip_serializing_if = "Option::is_none")]
141 pub parent_run_id: Option<String>,
142 #[serde(rename = "carriedStepCount", skip_serializing_if = "Option::is_none")]
143 pub carried_step_count: Option<u64>,
144 #[serde(rename = "workflowSources", skip_serializing_if = "Option::is_none")]
145 pub workflow_sources: Option<Vec<Value>>,
146 #[serde(rename = "definitionDigest", skip_serializing_if = "Option::is_none")]
147 pub definition_digest: Option<String>,
148 #[serde(rename = "startedAt")]
149 pub started_at: String,
150 #[serde(rename = "finishedAt", skip_serializing_if = "Option::is_none")]
151 pub finished_at: Option<String>,
152 #[serde(rename = "updatedAt")]
153 pub updated_at: String,
154 pub status: RunStatus,
155 pub input: Value,
156 pub outputs: BTreeMap<String, Value>,
157 pub results: BTreeMap<String, Value>,
158 pub steps: Vec<StepRecord>,
159 #[serde(skip_serializing_if = "Option::is_none")]
160 pub updates: Option<Vec<Value>>,
161 #[serde(rename = "currentNode", skip_serializing_if = "Option::is_none")]
162 pub current_node: Option<String>,
163 #[serde(rename = "currentAttemptId", skip_serializing_if = "Option::is_none")]
164 pub current_attempt_id: Option<String>,
165 #[serde(
166 rename = "currentNodeStartedAt",
167 skip_serializing_if = "Option::is_none"
168 )]
169 pub current_node_started_at: Option<String>,
170 #[serde(
171 rename = "currentSettingsScopeId",
172 skip_serializing_if = "Option::is_none"
173 )]
174 pub current_settings_scope_id: Option<String>,
175 #[serde(
176 rename = "currentSettingsChangeNumber",
177 skip_serializing_if = "Option::is_none"
178 )]
179 pub current_settings_change_number: Option<u64>,
180 #[serde(
181 rename = "currentSettingsHash",
182 skip_serializing_if = "Option::is_none"
183 )]
184 pub current_settings_hash: Option<String>,
185 #[serde(rename = "statusDetail", skip_serializing_if = "Option::is_none")]
186 pub status_detail: Option<String>,
187 #[serde(rename = "humanDecision", skip_serializing_if = "Option::is_none")]
188 pub human_decision: Option<Value>,
189 #[serde(skip_serializing_if = "Option::is_none")]
190 pub paused: Option<bool>,
191 #[serde(rename = "waitingOn", skip_serializing_if = "Option::is_none")]
192 pub waiting_on: Option<String>,
193 #[serde(rename = "finalOutput", skip_serializing_if = "Option::is_none")]
194 pub final_output: Option<Value>,
195 #[serde(skip_serializing_if = "Option::is_none")]
196 pub error: Option<String>,
197}
198
199#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
200pub struct NodeResult {
201 #[serde(rename = "attemptId")]
202 pub attempt_id: String,
203 #[serde(rename = "nodeId")]
204 pub node_id: String,
205 #[serde(rename = "nodeType")]
206 pub node_type: String,
207 pub outcome: NodeOutcome,
208 #[serde(rename = "startedAt")]
209 pub started_at: String,
210 #[serde(rename = "finishedAt")]
211 pub finished_at: String,
212 #[serde(rename = "durationMs")]
213 pub duration_ms: f64,
214 #[serde(skip_serializing_if = "Option::is_none")]
215 pub output: Option<Value>,
216 #[serde(skip_serializing_if = "Option::is_none")]
217 pub error: Option<String>,
218}
219
220#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
221pub struct StepRecord {
222 #[serde(rename = "attemptId")]
223 pub attempt_id: String,
224 #[serde(rename = "nodeId")]
225 pub node_id: String,
226 #[serde(rename = "nodeType")]
227 pub node_type: String,
228 pub outcome: NodeOutcome,
229 #[serde(rename = "startedAt")]
230 pub started_at: String,
231 #[serde(rename = "finishedAt")]
232 pub finished_at: String,
233 pub prompt: Value,
236 pub output: Value,
237 #[serde(skip_serializing_if = "Option::is_none")]
238 pub error: Option<String>,
239 #[serde(skip_serializing_if = "Option::is_none")]
240 pub action: Option<ActionReceipt>,
241 #[serde(rename = "assistantMessage", skip_serializing_if = "Option::is_none")]
242 pub assistant_message: Option<Value>,
243 #[serde(skip_serializing_if = "Option::is_none")]
244 pub conversation: Option<ConversationRange>,
245 #[serde(rename = "settingsScopeId", skip_serializing_if = "Option::is_none")]
246 pub settings_scope_id: Option<String>,
247 #[serde(
248 rename = "settingsChangeNumber",
249 skip_serializing_if = "Option::is_none"
250 )]
251 pub settings_change_number: Option<u64>,
252 #[serde(rename = "settingsHash", skip_serializing_if = "Option::is_none")]
253 pub settings_hash: Option<String>,
254}
255
256#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
257pub struct ActionReceipt {
258 #[serde(rename = "actionType")]
259 pub action_type: String,
260 #[serde(skip_serializing_if = "Option::is_none")]
261 pub command: Option<String>,
262 #[serde(skip_serializing_if = "Option::is_none")]
263 pub args: Option<Vec<String>>,
264 #[serde(skip_serializing_if = "Option::is_none")]
265 pub cwd: Option<String>,
266 #[serde(rename = "exitCode", skip_serializing_if = "Option::is_none")]
267 pub exit_code: Option<Value>,
268 #[serde(skip_serializing_if = "Option::is_none")]
269 pub signal: Option<Value>,
270 #[serde(rename = "durationMs", skip_serializing_if = "Option::is_none")]
271 pub duration_ms: Option<f64>,
272}
273
274#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
275pub struct ConversationRange {
276 #[serde(rename = "firstEntryId")]
277 pub first_entry_id: String,
278 #[serde(rename = "lastEntryId")]
279 pub last_entry_id: String,
280}
281
282#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
283pub struct TraceEvent {
284 pub seq: u64,
285 pub at: String,
286 pub scope: String,
287 #[serde(rename = "type")]
288 pub event_type: String,
289 #[serde(rename = "runId")]
290 pub run_id: String,
291 #[serde(rename = "nodeId", skip_serializing_if = "Option::is_none")]
292 pub node_id: Option<String>,
293 #[serde(rename = "attemptId", skip_serializing_if = "Option::is_none")]
294 pub attempt_id: Option<String>,
295 pub payload: Value,
296}
297
298#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
299pub struct SessionBinding {
300 pub schema: String,
301 #[serde(rename = "runId")]
302 pub run_id: String,
303 #[serde(rename = "piSessionId")]
304 pub pi_session_id: String,
305 #[serde(rename = "piSessionFile", skip_serializing_if = "Option::is_none")]
306 pub pi_session_file: Option<String>,
307 pub cwd: String,
308 #[serde(rename = "boundAt")]
309 pub bound_at: String,
310}
311
312#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
313pub struct SessionEntryRecord {
314 pub seq: u64,
315 pub at: String,
316 pub entry: Value,
317}
318
319#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
320pub struct SessionEventRecord {
321 pub seq: u64,
322 pub at: String,
323 #[serde(rename = "nodeId")]
324 pub node_id: String,
325 #[serde(rename = "attemptId")]
326 pub attempt_id: String,
327 #[serde(rename = "turnId", skip_serializing_if = "Option::is_none")]
328 pub turn_id: Option<String>,
329 #[serde(rename = "messageId", skip_serializing_if = "Option::is_none")]
330 pub message_id: Option<String>,
331 #[serde(rename = "toolCallId", skip_serializing_if = "Option::is_none")]
332 pub tool_call_id: Option<String>,
333 #[serde(rename = "type")]
334 pub event_type: String,
335 pub payload: Value,
336}
337
338#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
339#[serde(rename_all = "snake_case")]
340pub enum SessionCaptureStatus {
341 Recording,
342 Complete,
343 Failed,
344}
345
346#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
347pub struct SessionCaptureFailure {
348 #[serde(rename = "failedAt")]
349 pub failed_at: String,
350 pub code: String,
351 pub message: String,
352}
353
354#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
355pub struct SessionCapture {
356 pub schema: String,
357 #[serde(rename = "eventSchema")]
358 pub event_schema: String,
359 pub status: SessionCaptureStatus,
360 #[serde(rename = "eventCount")]
361 pub event_count: u64,
362 #[serde(rename = "entryCount")]
363 pub entry_count: u64,
364 #[serde(rename = "lastEventSeq")]
365 pub last_event_seq: u64,
366 #[serde(skip_serializing_if = "Option::is_none")]
367 pub failure: Option<SessionCaptureFailure>,
368}
369
370#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
373pub struct DefinitionSnapshot {
374 pub schema: String,
375 pub name: String,
376 #[serde(rename = "startAt")]
377 pub start_at: String,
378 pub nodes: serde_json::Map<String, Value>,
381 pub edges: Vec<EdgeDef>,
382}
383
384impl DefinitionSnapshot {
385 pub fn node_type(&self, node_id: &str) -> Option<&str> {
386 self.nodes
387 .get(node_id)?
388 .get("nodeType")
389 .and_then(Value::as_str)
390 }
391
392 pub fn node_action_execution(&self, node_id: &str) -> Option<&str> {
393 self.nodes
394 .get(node_id)?
395 .get("actionExecution")
396 .and_then(Value::as_str)
397 }
398
399 pub fn node_ids(&self) -> impl Iterator<Item = &str> {
400 self.nodes.keys().map(String::as_str)
401 }
402}
403
404#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
406#[serde(untagged)]
407pub enum EdgeDef {
408 Simple { from: String, to: String },
409 Switch { from: String, switch: SwitchDef },
410}
411
412#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
413pub struct SwitchDef {
414 pub on: String,
415 pub cases: serde_json::Map<String, Value>,
417}
418
419#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
421pub struct ArtifactRef {
422 pub path: String,
423 #[serde(rename = "mediaType")]
424 pub media_type: String,
425 pub bytes: u64,
426 pub sha256: String,
427}
428
429pub fn as_artifact_ref(value: &Value) -> Option<ArtifactRef> {
431 let object = value.as_object()?;
432 if object.len() != 1 {
433 return None;
434 }
435 serde_json::from_value(object.get("$artifact")?.clone()).ok()
436}
437
438pub fn as_escaped(value: &Value) -> Option<&Value> {
440 let object = value.as_object()?;
441 if object.len() != 1 {
442 return None;
443 }
444 object.get("$escaped")
445}