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 = "startedAt")]
99 pub started_at: String,
100 #[serde(rename = "finishedAt", skip_serializing_if = "Option::is_none")]
101 pub finished_at: Option<String>,
102 #[serde(rename = "updatedAt")]
103 pub updated_at: String,
104 pub status: RunStatus,
105 pub input: Value,
106 pub outputs: BTreeMap<String, Value>,
107 pub results: BTreeMap<String, NodeResult>,
108 pub steps: Vec<StepRecord>,
109 #[serde(rename = "currentNode", skip_serializing_if = "Option::is_none")]
110 pub current_node: Option<String>,
111 #[serde(rename = "currentAttemptId", skip_serializing_if = "Option::is_none")]
112 pub current_attempt_id: Option<String>,
113 #[serde(
114 rename = "currentNodeStartedAt",
115 skip_serializing_if = "Option::is_none"
116 )]
117 pub current_node_started_at: Option<String>,
118 #[serde(rename = "statusDetail", skip_serializing_if = "Option::is_none")]
119 pub status_detail: Option<String>,
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub paused: Option<bool>,
122 #[serde(rename = "waitingOn", skip_serializing_if = "Option::is_none")]
123 pub waiting_on: Option<String>,
124 #[serde(rename = "finalOutput", skip_serializing_if = "Option::is_none")]
125 pub final_output: Option<Value>,
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub error: Option<String>,
128}
129
130#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
131pub struct NodeResult {
132 #[serde(rename = "attemptId")]
133 pub attempt_id: String,
134 #[serde(rename = "nodeId")]
135 pub node_id: String,
136 #[serde(rename = "nodeType")]
137 pub node_type: String,
138 pub outcome: NodeOutcome,
139 #[serde(rename = "startedAt")]
140 pub started_at: String,
141 #[serde(rename = "finishedAt")]
142 pub finished_at: String,
143 #[serde(rename = "durationMs")]
144 pub duration_ms: f64,
145 #[serde(skip_serializing_if = "Option::is_none")]
146 pub output: Option<Value>,
147 #[serde(skip_serializing_if = "Option::is_none")]
148 pub error: Option<String>,
149}
150
151#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
152pub struct StepRecord {
153 #[serde(rename = "attemptId")]
154 pub attempt_id: String,
155 #[serde(rename = "nodeId")]
156 pub node_id: String,
157 #[serde(rename = "nodeType")]
158 pub node_type: String,
159 pub outcome: NodeOutcome,
160 #[serde(rename = "startedAt")]
161 pub started_at: String,
162 #[serde(rename = "finishedAt")]
163 pub finished_at: String,
164 pub prompt: Value,
167 pub output: Value,
168 #[serde(skip_serializing_if = "Option::is_none")]
169 pub error: Option<String>,
170 #[serde(skip_serializing_if = "Option::is_none")]
171 pub action: Option<ActionReceipt>,
172 #[serde(skip_serializing_if = "Option::is_none")]
173 pub conversation: Option<ConversationRange>,
174}
175
176#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
177pub struct ActionReceipt {
178 #[serde(rename = "actionType")]
179 pub action_type: String,
180 #[serde(skip_serializing_if = "Option::is_none")]
181 pub command: Option<String>,
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub args: Option<Vec<String>>,
184 #[serde(skip_serializing_if = "Option::is_none")]
185 pub cwd: Option<String>,
186 #[serde(rename = "exitCode", skip_serializing_if = "Option::is_none")]
187 pub exit_code: Option<Value>,
188 #[serde(skip_serializing_if = "Option::is_none")]
189 pub signal: Option<Value>,
190 #[serde(rename = "durationMs", skip_serializing_if = "Option::is_none")]
191 pub duration_ms: Option<f64>,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195pub struct ConversationRange {
196 #[serde(rename = "firstEntryId")]
197 pub first_entry_id: String,
198 #[serde(rename = "lastEntryId")]
199 pub last_entry_id: String,
200}
201
202#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
203pub struct TraceEvent {
204 pub seq: u64,
205 pub at: String,
206 pub scope: String,
207 #[serde(rename = "type")]
208 pub event_type: String,
209 #[serde(rename = "runId")]
210 pub run_id: String,
211 #[serde(rename = "nodeId", skip_serializing_if = "Option::is_none")]
212 pub node_id: Option<String>,
213 #[serde(rename = "attemptId", skip_serializing_if = "Option::is_none")]
214 pub attempt_id: Option<String>,
215 pub payload: Value,
216}
217
218#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
219pub struct SessionBinding {
220 pub schema: String,
221 #[serde(rename = "runId")]
222 pub run_id: String,
223 #[serde(rename = "piSessionId")]
224 pub pi_session_id: String,
225 #[serde(rename = "piSessionFile", skip_serializing_if = "Option::is_none")]
226 pub pi_session_file: Option<String>,
227 pub cwd: String,
228 #[serde(rename = "boundAt")]
229 pub bound_at: String,
230}
231
232#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
233pub struct SessionEntryRecord {
234 pub seq: u64,
235 pub at: String,
236 pub entry: Value,
237}
238
239#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
240pub struct SessionEventRecord {
241 pub seq: u64,
242 pub at: String,
243 #[serde(rename = "nodeId")]
244 pub node_id: String,
245 #[serde(rename = "attemptId")]
246 pub attempt_id: String,
247 #[serde(rename = "turnId", skip_serializing_if = "Option::is_none")]
248 pub turn_id: Option<String>,
249 #[serde(rename = "messageId", skip_serializing_if = "Option::is_none")]
250 pub message_id: Option<String>,
251 #[serde(rename = "toolCallId", skip_serializing_if = "Option::is_none")]
252 pub tool_call_id: Option<String>,
253 #[serde(rename = "type")]
254 pub event_type: String,
255 pub payload: Value,
256}
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
259#[serde(rename_all = "snake_case")]
260pub enum SessionCaptureStatus {
261 Recording,
262 Complete,
263 Failed,
264}
265
266#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
267pub struct SessionCaptureFailure {
268 #[serde(rename = "failedAt")]
269 pub failed_at: String,
270 pub code: String,
271 pub message: String,
272}
273
274#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
275pub struct SessionCapture {
276 pub schema: String,
277 #[serde(rename = "eventSchema")]
278 pub event_schema: String,
279 pub status: SessionCaptureStatus,
280 #[serde(rename = "eventCount")]
281 pub event_count: u64,
282 #[serde(rename = "entryCount")]
283 pub entry_count: u64,
284 #[serde(rename = "lastEventSeq")]
285 pub last_event_seq: u64,
286 #[serde(skip_serializing_if = "Option::is_none")]
287 pub failure: Option<SessionCaptureFailure>,
288}
289
290#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
293pub struct DefinitionSnapshot {
294 pub schema: String,
295 pub name: String,
296 #[serde(rename = "startAt")]
297 pub start_at: String,
298 pub nodes: serde_json::Map<String, Value>,
301 pub edges: Vec<EdgeDef>,
302}
303
304impl DefinitionSnapshot {
305 pub fn node_type(&self, node_id: &str) -> Option<&str> {
306 self.nodes
307 .get(node_id)?
308 .get("nodeType")
309 .and_then(Value::as_str)
310 }
311
312 pub fn node_ids(&self) -> impl Iterator<Item = &str> {
313 self.nodes.keys().map(String::as_str)
314 }
315}
316
317#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
319#[serde(untagged)]
320pub enum EdgeDef {
321 Simple { from: String, to: String },
322 Switch { from: String, switch: SwitchDef },
323}
324
325#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
326pub struct SwitchDef {
327 pub on: String,
328 pub cases: serde_json::Map<String, Value>,
330}
331
332#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
334pub struct ArtifactRef {
335 pub path: String,
336 #[serde(rename = "mediaType")]
337 pub media_type: String,
338 pub bytes: u64,
339 pub sha256: String,
340}
341
342pub fn as_artifact_ref(value: &Value) -> Option<ArtifactRef> {
344 let object = value.as_object()?;
345 if object.len() != 1 {
346 return None;
347 }
348 serde_json::from_value(object.get("$artifact")?.clone()).ok()
349}
350
351pub fn as_escaped(value: &Value) -> Option<&Value> {
353 let object = value.as_object()?;
354 if object.len() != 1 {
355 return None;
356 }
357 object.get("$escaped")
358}