Skip to main content

omne_cli/
events.rs

1//! Event types for omne pipe execution.
2//!
3//! Seven v1 event types per the v2 design doc. Serialized as JSON Lines
4//! to per-run `.omne/var/runs/<run_id>/events.jsonl`. Each event is
5//! discriminated by a top-level `type` tag.
6//!
7//! Call sites land in later units (`event_log`, `executor`); the module
8//! carries `#[allow(dead_code)]` until then.
9
10#![allow(dead_code)]
11
12use serde::{Deserialize, Serialize};
13
14/// All v1 event types.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(tag = "type")]
17pub enum Event {
18    #[serde(rename = "pipe.started")]
19    PipeStarted(PipeStarted),
20
21    #[serde(rename = "pipe.completed")]
22    PipeCompleted(PipeCompleted),
23
24    #[serde(rename = "pipe.aborted")]
25    PipeAborted(PipeAborted),
26
27    #[serde(rename = "node.started")]
28    NodeStarted(NodeStarted),
29
30    #[serde(rename = "node.completed")]
31    NodeCompleted(NodeCompleted),
32
33    #[serde(rename = "node.failed")]
34    NodeFailed(NodeFailed),
35
36    #[serde(rename = "gate.passed")]
37    GatePassed(GatePassed),
38
39    #[serde(rename = "iteration.started")]
40    IterationStarted(IterationStarted),
41}
42
43impl Event {
44    /// Returns the event type tag.
45    pub fn event_type(&self) -> &'static str {
46        match self {
47            Event::PipeStarted(_) => "pipe.started",
48            Event::PipeCompleted(_) => "pipe.completed",
49            Event::PipeAborted(_) => "pipe.aborted",
50            Event::NodeStarted(_) => "node.started",
51            Event::NodeCompleted(_) => "node.completed",
52            Event::NodeFailed(_) => "node.failed",
53            Event::GatePassed(_) => "gate.passed",
54            Event::IterationStarted(_) => "iteration.started",
55        }
56    }
57
58    /// Returns the run_id for this event.
59    pub fn run_id(&self) -> &str {
60        match self {
61            Event::PipeStarted(e) => &e.run_id,
62            Event::PipeCompleted(e) => &e.run_id,
63            Event::PipeAborted(e) => &e.run_id,
64            Event::NodeStarted(e) => &e.run_id,
65            Event::NodeCompleted(e) => &e.run_id,
66            Event::NodeFailed(e) => &e.run_id,
67            Event::GatePassed(e) => &e.run_id,
68            Event::IterationStarted(e) => &e.run_id,
69        }
70    }
71}
72
73/// Node execution kind, carried on `node.started`.
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(rename_all = "snake_case")]
76pub enum NodeKind {
77    Command,
78    Prompt,
79    Bash,
80    Loop,
81}
82
83/// Gate-pass method. In v1 only `Hook` is emitted; `HumanCli` and `McpTool`
84/// are reserved for post-v1 and included now to avoid a schema break.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(rename_all = "snake_case")]
87pub enum GateMethod {
88    Hook,
89    HumanCli,
90    McpTool,
91}
92
93/// Reason a node failed. Serializes as snake_case under `error.kind`.
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
95#[serde(rename_all = "snake_case")]
96pub enum ErrorKind {
97    HostMissing,
98    Timeout,
99    Blocked,
100    GateFailed,
101    GateTimeout,
102    Crash,
103    MaxIterationsExceeded,
104}
105
106/// Structured error payload nested under `NodeFailed.error`.
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct NodeError {
109    pub kind: ErrorKind,
110}
111
112/// One `--input key=value` pair carried on `pipe.started`.
113///
114/// Wire shape is an object — `{"key":"task","value":"add auth"}` —
115/// rather than a 2-element array so jq queries against `events.jsonl`
116/// can address fields by name (`.inputs[].value`).
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct Input {
119    pub key: String,
120    pub value: String,
121}
122
123/// Field conventions shared by every event type:
124/// - `id`: monotonic ULID for this event (lowercase, distinct from `run_id`)
125/// - `ts`: ISO-8601 / RFC 3339 UTC timestamp, e.g. `2026-04-15T12:00:00Z`
126/// - `run_id`: `<pipe-name>-<lowercase-ulid>` from the `ulid` allocator
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct PipeStarted {
129    pub id: String,
130    pub ts: String,
131    pub run_id: String,
132    pub pipe: String,
133    #[serde(default, skip_serializing_if = "Vec::is_empty")]
134    pub inputs: Vec<Input>,
135    #[serde(default, skip_serializing_if = "String::is_empty")]
136    pub distro_version: String,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct PipeCompleted {
141    pub id: String,
142    pub ts: String,
143    pub run_id: String,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct PipeAborted {
148    pub id: String,
149    pub ts: String,
150    pub run_id: String,
151    pub reason: String,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct NodeStarted {
156    pub id: String,
157    pub ts: String,
158    pub run_id: String,
159    pub node_id: String,
160    pub kind: NodeKind,
161    #[serde(default, skip_serializing_if = "Option::is_none")]
162    pub name: Option<String>,
163    #[serde(default, skip_serializing_if = "Option::is_none")]
164    pub model: Option<String>,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct NodeCompleted {
169    pub id: String,
170    pub ts: String,
171    pub run_id: String,
172    pub node_id: String,
173    /// Path to the node's captured output, **relative to the volume
174    /// root** with forward-slash separators (e.g.
175    /// `.omne/var/runs/<run_id>/nodes/<node_id>.out`). Forward slashes
176    /// keep the wire format portable across Windows + Unix readers.
177    pub output_path: String,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct NodeFailed {
182    pub id: String,
183    pub ts: String,
184    pub run_id: String,
185    pub node_id: String,
186    pub error: NodeError,
187    /// Optional human-readable failure detail. Unit 11 executor
188    /// populates this from the subprocess stderr tail (or gate stderr
189    /// tail); absent for failures with no additional context beyond
190    /// `error.kind`. Kept optional so agent-native readers get a
191    /// human-readable string without forcing every producer to invent
192    /// one.
193    #[serde(default, skip_serializing_if = "Option::is_none")]
194    pub message: Option<String>,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct GatePassed {
199    pub id: String,
200    pub ts: String,
201    pub run_id: String,
202    pub node_id: String,
203    pub gate: String,
204    pub method: GateMethod,
205    /// Optional tail (last 1024 bytes, UTF-8-boundary-aligned) of the
206    /// gate hook's stdout. Populated by the executor when the hook
207    /// wrote anything to stdout so agents reading `gate.passed` can see
208    /// validation summaries or coverage numbers the hook produced.
209    /// Absent when the hook was silent.
210    #[serde(default, skip_serializing_if = "Option::is_none")]
211    pub stdout: Option<String>,
212}
213
214/// Marks the start of one iteration inside a loop node. Emitted once
215/// per iteration from the executor. The `byte_offset` is the zero-based
216/// byte position in the node's capture file (`nodes/<id>.out`) at which
217/// this iteration's assistant output begins — immediately after the
218/// executor's iteration marker line. Agents reconstruct per-iteration
219/// text by slicing `[byte_offset_N .. byte_offset_{N+1})` (or to EOF for
220/// the final iteration) without having to parse the marker format.
221#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct IterationStarted {
223    pub id: String,
224    pub ts: String,
225    pub run_id: String,
226    pub node_id: String,
227    pub iteration: u32,
228    pub byte_offset: u64,
229}