Skip to main content

harness_core/
event.rs

1use crate::{
2    Action, CompactionStage, Context, FixPatch, GuideId, ModelOutput, SensorId, Signal, ToolResult,
3};
4use serde::{Deserialize, Serialize};
5use std::path::PathBuf;
6
7/// All 27 lifecycle events the framework emits (DESIGN.md §10).
8///
9/// Lifetimes are intentionally borrowed: hooks must not own these references
10/// past the call.
11#[derive(Debug)]
12#[non_exhaustive]
13pub enum Event<'a> {
14    // session
15    SessionStart {
16        source: SessionSource,
17    },
18    SessionEnd,
19
20    // tool
21    PreToolUse {
22        action: &'a Action,
23    },
24    PostToolUse {
25        action: &'a Action,
26        result: &'a ToolResult,
27    },
28    PermissionRequest {
29        action: &'a Action,
30    },
31
32    // compaction
33    PreCompact {
34        stage: CompactionStage,
35    },
36    PostCompact {
37        stage: CompactionStage,
38    },
39
40    // guides
41    PreGuide {
42        guide: &'a GuideId,
43    },
44    PostGuide {
45        guide: &'a GuideId,
46    },
47
48    // sensors
49    PreSensor {
50        sensor: &'a SensorId,
51    },
52    PostSensor {
53        sensor: &'a SensorId,
54        signals: &'a [Signal],
55    },
56
57    // auto-fix patches (audit #7: sensor-emitted RunCommand etc. were applied
58    // silently — hooks can now intercept and Deny per-patch).
59    PreAutoFix {
60        patch: &'a FixPatch,
61    },
62    PostAutoFix {
63        patch: &'a FixPatch,
64        applied: bool,
65    },
66
67    // model
68    PreModel {
69        ctx: &'a Context,
70    },
71    PostModel {
72        out: &'a ModelOutput,
73    },
74    /// Streaming-only: a text fragment arrived from `Model::stream()`. Fires
75    /// 0..N times between `PreModel` and `PostModel` when the AgentLoop is
76    /// in streaming mode. `text` is the new fragment (not the accumulator).
77    /// Tool-call deltas are NOT surfaced here — the loop assembles those
78    /// and emits the final `PostModel` with full `tool_calls`.
79    ModelTokenDelta {
80        text: &'a str,
81    },
82
83    // subagents
84    SubagentStart {
85        name: &'a str,
86    },
87    SubagentReport {
88        status: SubagentStatus,
89    },
90
91    // filesystem
92    FileChanged {
93        path: &'a PathBuf,
94    },
95    CwdChanged {
96        from: &'a PathBuf,
97        to: &'a PathBuf,
98    },
99
100    // blueprint
101    BlueprintNodeEnter {
102        node: &'a str,
103    },
104    BlueprintNodeExit {
105        node: &'a str,
106    },
107
108    // misc
109    TaskCompleted,
110    BudgetWarning {
111        ratio: f32,
112    },
113    Notification {
114        kind: NotificationKind,
115    },
116    Error {
117        message: &'a str,
118    },
119    Stop,
120    Heartbeat {
121        iter: u32,
122    },
123    Custom {
124        name: &'a str,
125        data: &'a serde_json::Value,
126    },
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
130#[serde(rename_all = "lowercase")]
131#[non_exhaustive]
132pub enum SessionSource {
133    Startup,
134    Resume,
135    Clear,
136    Compact,
137}
138
139/// Subagent self-report (Superpowers convention).
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
141#[serde(rename_all = "kebab-case")]
142#[non_exhaustive]
143pub enum SubagentStatus {
144    Done,
145    DoneWithConcerns,
146    Blocked,
147    NeedsContext,
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
151#[serde(rename_all = "kebab-case")]
152#[non_exhaustive]
153pub enum NotificationKind {
154    PermissionPrompt,
155    IdlePrompt,
156    AuthSuccess,
157    ElicitationDialog,
158    ElicitationComplete,
159    ElicitationResponse,
160}
161
162impl<'a> Event<'a> {
163    /// Stable string discriminant for matchers and serialization.
164    pub fn name(&self) -> &'static str {
165        match self {
166            Event::SessionStart { .. } => "SessionStart",
167            Event::SessionEnd => "SessionEnd",
168            Event::PreToolUse { .. } => "PreToolUse",
169            Event::PostToolUse { .. } => "PostToolUse",
170            Event::PermissionRequest { .. } => "PermissionRequest",
171            Event::PreCompact { .. } => "PreCompact",
172            Event::PostCompact { .. } => "PostCompact",
173            Event::PreGuide { .. } => "PreGuide",
174            Event::PostGuide { .. } => "PostGuide",
175            Event::PreSensor { .. } => "PreSensor",
176            Event::PostSensor { .. } => "PostSensor",
177            Event::PreAutoFix { .. } => "PreAutoFix",
178            Event::PostAutoFix { .. } => "PostAutoFix",
179            Event::PreModel { .. } => "PreModel",
180            Event::PostModel { .. } => "PostModel",
181            Event::ModelTokenDelta { .. } => "ModelTokenDelta",
182            Event::SubagentStart { .. } => "SubagentStart",
183            Event::SubagentReport { .. } => "SubagentReport",
184            Event::FileChanged { .. } => "FileChanged",
185            Event::CwdChanged { .. } => "CwdChanged",
186            Event::BlueprintNodeEnter { .. } => "BlueprintNodeEnter",
187            Event::BlueprintNodeExit { .. } => "BlueprintNodeExit",
188            Event::TaskCompleted => "TaskCompleted",
189            Event::BudgetWarning { .. } => "BudgetWarning",
190            Event::Notification { .. } => "Notification",
191            Event::Error { .. } => "Error",
192            Event::Stop => "Stop",
193            Event::Heartbeat { .. } => "Heartbeat",
194            Event::Custom { .. } => "Custom",
195        }
196    }
197}