1use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::collections::BTreeMap;
8
9pub const RUN_BUNDLE_SCHEMA: &str = "pi-workflows.run-bundle.v1";
10pub const RUN_STATE_SCHEMA: &str = "pi-workflows.run-state.v1";
11pub const DEFINITION_SNAPSHOT_SCHEMA: &str = "pi-workflows.definition-snapshot.v1";
12pub const SESSION_BINDING_SCHEMA: &str = "pi-workflows.session-binding.v1";
13pub const SESSION_EVENT_SCHEMA: &str = "pi-workflows.session-event.v1";
14pub const SESSION_CAPTURE_SCHEMA: &str = "pi-workflows.session-capture.v1";
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum RunStatus {
19 Running,
20 Waiting,
21 Completed,
22 Failed,
23 TimedOut,
24 Cancelled,
25}
26
27impl RunStatus {
28 pub fn is_terminal(self) -> bool {
29 !matches!(self, RunStatus::Running)
30 }
31
32 pub fn label(self) -> &'static str {
33 match self {
34 RunStatus::Running => "running",
35 RunStatus::Waiting => "waiting",
36 RunStatus::Completed => "completed",
37 RunStatus::Failed => "failed",
38 RunStatus::TimedOut => "timed_out",
39 RunStatus::Cancelled => "cancelled",
40 }
41 }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case")]
46pub enum NodeOutcome {
47 Ok,
48 TimedOut,
49 Failed,
50 Cancelled,
51}
52
53#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
54pub struct Manifest {
55 pub schema: String,
56 #[serde(rename = "runId")]
57 pub run_id: String,
58 #[serde(rename = "workflowName")]
59 pub workflow_name: String,
60 #[serde(rename = "runTitle", skip_serializing_if = "Option::is_none")]
61 pub run_title: Option<String>,
62 #[serde(rename = "workflowPath", skip_serializing_if = "Option::is_none")]
63 pub workflow_path: Option<String>,
64 #[serde(rename = "startedAt")]
65 pub started_at: String,
66 #[serde(rename = "finishedAt", skip_serializing_if = "Option::is_none")]
67 pub finished_at: Option<String>,
68 pub status: RunStatus,
69 #[serde(rename = "traceSchema")]
70 pub trace_schema: String,
71 pub paths: ManifestPaths,
72}
73
74#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
75pub struct ManifestPaths {
76 pub workflow: String,
77 pub state: String,
78 pub trace: String,
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub session: Option<String>,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub artifacts: Option<String>,
83}
84
85#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
86pub struct RunState {
87 pub schema: String,
88 #[serde(rename = "traceSeq")]
89 pub trace_seq: u64,
90 #[serde(rename = "runId")]
91 pub run_id: String,
92 #[serde(rename = "workflowName")]
93 pub workflow_name: String,
94 #[serde(rename = "runTitle", skip_serializing_if = "Option::is_none")]
95 pub run_title: Option<String>,
96 #[serde(rename = "workflowPath", skip_serializing_if = "Option::is_none")]
97 pub workflow_path: Option<String>,
98 #[serde(rename = "workflowHash", skip_serializing_if = "Option::is_none")]
99 pub workflow_hash: Option<String>,
100 #[serde(rename = "parentRunId", skip_serializing_if = "Option::is_none")]
101 pub parent_run_id: Option<String>,
102 #[serde(rename = "startedAt")]
103 pub started_at: String,
104 #[serde(rename = "finishedAt", skip_serializing_if = "Option::is_none")]
105 pub finished_at: Option<String>,
106 #[serde(rename = "updatedAt")]
107 pub updated_at: String,
108 pub status: RunStatus,
109 pub input: Value,
110 pub outputs: BTreeMap<String, Value>,
111 pub results: BTreeMap<String, NodeResult>,
112 pub steps: Vec<StepRecord>,
113 #[serde(rename = "currentNode", skip_serializing_if = "Option::is_none")]
114 pub current_node: Option<String>,
115 #[serde(rename = "currentAttemptId", skip_serializing_if = "Option::is_none")]
116 pub current_attempt_id: Option<String>,
117 #[serde(
118 rename = "currentNodeStartedAt",
119 skip_serializing_if = "Option::is_none"
120 )]
121 pub current_node_started_at: Option<String>,
122 #[serde(rename = "statusDetail", skip_serializing_if = "Option::is_none")]
123 pub status_detail: Option<String>,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub paused: Option<bool>,
126 #[serde(rename = "waitingOn", skip_serializing_if = "Option::is_none")]
127 pub waiting_on: Option<String>,
128 #[serde(rename = "finalOutput", skip_serializing_if = "Option::is_none")]
129 pub final_output: Option<Value>,
130 #[serde(skip_serializing_if = "Option::is_none")]
131 pub error: Option<String>,
132}
133
134#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
135pub struct NodeResult {
136 #[serde(rename = "attemptId")]
137 pub attempt_id: String,
138 #[serde(rename = "nodeId")]
139 pub node_id: String,
140 #[serde(rename = "nodeType")]
141 pub node_type: String,
142 pub outcome: NodeOutcome,
143 #[serde(rename = "startedAt")]
144 pub started_at: String,
145 #[serde(rename = "finishedAt")]
146 pub finished_at: String,
147 #[serde(rename = "durationMs")]
148 pub duration_ms: f64,
149 #[serde(skip_serializing_if = "Option::is_none")]
150 pub output: Option<Value>,
151 #[serde(skip_serializing_if = "Option::is_none")]
152 pub error: Option<String>,
153}
154
155#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
156pub struct StepRecord {
157 #[serde(rename = "attemptId")]
158 pub attempt_id: String,
159 #[serde(rename = "nodeId")]
160 pub node_id: String,
161 #[serde(rename = "nodeType")]
162 pub node_type: String,
163 pub outcome: NodeOutcome,
164 #[serde(rename = "startedAt")]
165 pub started_at: String,
166 #[serde(rename = "finishedAt")]
167 pub finished_at: String,
168 pub prompt: Value,
171 pub output: Value,
172 #[serde(skip_serializing_if = "Option::is_none")]
173 pub error: Option<String>,
174 #[serde(skip_serializing_if = "Option::is_none")]
175 pub action: Option<ActionReceipt>,
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub conversation: Option<ConversationRange>,
178}
179
180#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
181pub struct ActionReceipt {
182 #[serde(rename = "actionType")]
183 pub action_type: String,
184 #[serde(skip_serializing_if = "Option::is_none")]
185 pub command: Option<String>,
186 #[serde(skip_serializing_if = "Option::is_none")]
187 pub args: Option<Vec<String>>,
188 #[serde(skip_serializing_if = "Option::is_none")]
189 pub cwd: Option<String>,
190 #[serde(rename = "exitCode", skip_serializing_if = "Option::is_none")]
191 pub exit_code: Option<Value>,
192 #[serde(skip_serializing_if = "Option::is_none")]
193 pub signal: Option<Value>,
194 #[serde(rename = "durationMs", skip_serializing_if = "Option::is_none")]
195 pub duration_ms: Option<f64>,
196}
197
198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
199pub struct ConversationRange {
200 #[serde(rename = "firstEntryId")]
201 pub first_entry_id: String,
202 #[serde(rename = "lastEntryId")]
203 pub last_entry_id: String,
204}
205
206#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
207pub struct TraceEvent {
208 pub seq: u64,
209 pub at: String,
210 pub scope: String,
211 #[serde(rename = "type")]
212 pub event_type: String,
213 #[serde(rename = "runId")]
214 pub run_id: String,
215 #[serde(rename = "nodeId", skip_serializing_if = "Option::is_none")]
216 pub node_id: Option<String>,
217 #[serde(rename = "attemptId", skip_serializing_if = "Option::is_none")]
218 pub attempt_id: Option<String>,
219 pub payload: Value,
220}
221
222#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
223pub struct SessionBinding {
224 pub schema: String,
225 #[serde(rename = "runId")]
226 pub run_id: String,
227 #[serde(rename = "piSessionId")]
228 pub pi_session_id: String,
229 #[serde(rename = "piSessionFile", skip_serializing_if = "Option::is_none")]
230 pub pi_session_file: Option<String>,
231 pub cwd: String,
232 #[serde(rename = "boundAt")]
233 pub bound_at: String,
234}
235
236#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
237pub struct SessionEntryRecord {
238 pub seq: u64,
239 pub at: String,
240 pub entry: Value,
241}
242
243#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
244pub struct SessionEventRecord {
245 pub seq: u64,
246 pub at: String,
247 #[serde(rename = "nodeId")]
248 pub node_id: String,
249 #[serde(rename = "attemptId")]
250 pub attempt_id: String,
251 #[serde(rename = "turnId", skip_serializing_if = "Option::is_none")]
252 pub turn_id: Option<String>,
253 #[serde(rename = "messageId", skip_serializing_if = "Option::is_none")]
254 pub message_id: Option<String>,
255 #[serde(rename = "toolCallId", skip_serializing_if = "Option::is_none")]
256 pub tool_call_id: Option<String>,
257 #[serde(rename = "type")]
258 pub event_type: String,
259 pub payload: Value,
260}
261
262#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
263#[serde(rename_all = "snake_case")]
264pub enum SessionCaptureStatus {
265 Recording,
266 Complete,
267 Failed,
268}
269
270#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
271pub struct SessionCaptureFailure {
272 #[serde(rename = "failedAt")]
273 pub failed_at: String,
274 pub code: String,
275 pub message: String,
276}
277
278#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
279pub struct SessionCapture {
280 pub schema: String,
281 #[serde(rename = "eventSchema")]
282 pub event_schema: String,
283 pub status: SessionCaptureStatus,
284 #[serde(rename = "eventCount")]
285 pub event_count: u64,
286 #[serde(rename = "entryCount")]
287 pub entry_count: u64,
288 #[serde(rename = "lastEventSeq")]
289 pub last_event_seq: u64,
290 #[serde(skip_serializing_if = "Option::is_none")]
291 pub failure: Option<SessionCaptureFailure>,
292}
293
294#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
297pub struct DefinitionSnapshot {
298 pub schema: String,
299 pub name: String,
300 #[serde(rename = "startAt")]
301 pub start_at: String,
302 pub nodes: serde_json::Map<String, Value>,
305 pub edges: Vec<EdgeDef>,
306}
307
308impl DefinitionSnapshot {
309 pub fn node_type(&self, node_id: &str) -> Option<&str> {
310 self.nodes
311 .get(node_id)?
312 .get("nodeType")
313 .and_then(Value::as_str)
314 }
315
316 pub fn node_ids(&self) -> impl Iterator<Item = &str> {
317 self.nodes.keys().map(String::as_str)
318 }
319}
320
321#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
323#[serde(untagged)]
324pub enum EdgeDef {
325 Simple { from: String, to: String },
326 Switch { from: String, switch: SwitchDef },
327}
328
329#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
330pub struct SwitchDef {
331 pub on: String,
332 pub cases: serde_json::Map<String, Value>,
334}
335
336#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
338pub struct ArtifactRef {
339 pub path: String,
340 #[serde(rename = "mediaType")]
341 pub media_type: String,
342 pub bytes: u64,
343 pub sha256: String,
344}
345
346pub fn as_artifact_ref(value: &Value) -> Option<ArtifactRef> {
348 let object = value.as_object()?;
349 if object.len() != 1 {
350 return None;
351 }
352 serde_json::from_value(object.get("$artifact")?.clone()).ok()
353}
354
355pub fn as_escaped(value: &Value) -> Option<&Value> {
357 let object = value.as_object()?;
358 if object.len() != 1 {
359 return None;
360 }
361 object.get("$escaped")
362}