1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
use crate::{
Action, CompactionStage, Context, FixPatch, GuideId, ModelOutput, SensorId, Signal, ToolResult,
};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
/// All 29 lifecycle events the framework emits (DESIGN.md §10).
///
/// Lifetimes are intentionally borrowed: hooks must not own these references
/// past the call.
#[derive(Debug)]
#[non_exhaustive]
pub enum Event<'a> {
// session
SessionStart {
source: SessionSource,
},
SessionEnd,
/// One acceptance check answered.
///
/// Emitted per check, pass or fail. Until this existed the verdict reached
/// the caller in the outcome and reached nobody else: an audit trail could
/// show every tool the agent called and not whether anything ever agreed
/// the work was done.
AcceptanceChecked {
name: &'a str,
passed: bool,
/// The check's own words on a failure; empty on a pass.
reason: &'a str,
},
/// A sealed acceptance contract changed while the run was in flight.
///
/// Separate from a failed [`Event::AcceptanceChecked`] because they warrant
/// different responses: a failed check is work not finished, this is the
/// measuring instrument having been moved by the party being measured. It
/// is the one event in this enum a host may reasonably want to page on.
SealBreached {
detail: &'a str,
},
// tool
PreToolUse {
action: &'a Action,
},
PostToolUse {
action: &'a Action,
result: &'a ToolResult,
},
PermissionRequest {
action: &'a Action,
},
// compaction
PreCompact {
stage: CompactionStage,
},
PostCompact {
stage: CompactionStage,
/// Estimated context tokens before this stage ran.
before: u32,
/// …and after. The difference is what the stage actually bought, which
/// is otherwise unknowable from outside: compaction is the component
/// whose whole job is to spend less, and a `stage` label alone says it
/// happened, not whether it worked.
after: u32,
},
// guides
PreGuide {
guide: &'a GuideId,
},
PostGuide {
guide: &'a GuideId,
},
// sensors
PreSensor {
sensor: &'a SensorId,
},
PostSensor {
sensor: &'a SensorId,
signals: &'a [Signal],
},
// auto-fix patches (audit #7: sensor-emitted RunCommand etc. were applied
// silently — hooks can now intercept and Deny per-patch).
PreAutoFix {
patch: &'a FixPatch,
},
PostAutoFix {
patch: &'a FixPatch,
applied: bool,
},
// model
PreModel {
ctx: &'a Context,
},
PostModel {
out: &'a ModelOutput,
},
/// Streaming-only: a text fragment arrived from `Model::stream()`. Fires
/// 0..N times between `PreModel` and `PostModel` when the AgentLoop is
/// in streaming mode. `text` is the new fragment (not the accumulator).
/// Tool-call deltas are NOT surfaced here — the loop assembles those
/// and emits the final `PostModel` with full `tool_calls`.
ModelTokenDelta {
text: &'a str,
},
// subagents
SubagentStart {
name: &'a str,
},
SubagentReport {
status: SubagentStatus,
},
// filesystem
FileChanged {
path: &'a PathBuf,
},
CwdChanged {
from: &'a PathBuf,
to: &'a PathBuf,
},
// blueprint
BlueprintNodeEnter {
node: &'a str,
},
BlueprintNodeExit {
node: &'a str,
},
// misc
TaskCompleted,
BudgetWarning {
ratio: f32,
},
Notification {
kind: NotificationKind,
},
Error {
message: &'a str,
},
Stop,
Heartbeat {
iter: u32,
},
Custom {
name: &'a str,
data: &'a serde_json::Value,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum SessionSource {
Startup,
Resume,
Clear,
Compact,
}
/// Subagent self-report (Superpowers convention).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum SubagentStatus {
Done,
DoneWithConcerns,
Blocked,
NeedsContext,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum NotificationKind {
PermissionPrompt,
IdlePrompt,
AuthSuccess,
ElicitationDialog,
ElicitationComplete,
ElicitationResponse,
}
impl<'a> Event<'a> {
/// Stable string discriminant for matchers and serialization.
pub fn name(&self) -> &'static str {
match self {
Event::SessionStart { .. } => "SessionStart",
Event::SessionEnd => "SessionEnd",
Event::AcceptanceChecked { .. } => "AcceptanceChecked",
Event::SealBreached { .. } => "SealBreached",
Event::PreToolUse { .. } => "PreToolUse",
Event::PostToolUse { .. } => "PostToolUse",
Event::PermissionRequest { .. } => "PermissionRequest",
Event::PreCompact { .. } => "PreCompact",
Event::PostCompact { .. } => "PostCompact",
Event::PreGuide { .. } => "PreGuide",
Event::PostGuide { .. } => "PostGuide",
Event::PreSensor { .. } => "PreSensor",
Event::PostSensor { .. } => "PostSensor",
Event::PreAutoFix { .. } => "PreAutoFix",
Event::PostAutoFix { .. } => "PostAutoFix",
Event::PreModel { .. } => "PreModel",
Event::PostModel { .. } => "PostModel",
Event::ModelTokenDelta { .. } => "ModelTokenDelta",
Event::SubagentStart { .. } => "SubagentStart",
Event::SubagentReport { .. } => "SubagentReport",
Event::FileChanged { .. } => "FileChanged",
Event::CwdChanged { .. } => "CwdChanged",
Event::BlueprintNodeEnter { .. } => "BlueprintNodeEnter",
Event::BlueprintNodeExit { .. } => "BlueprintNodeExit",
Event::TaskCompleted => "TaskCompleted",
Event::BudgetWarning { .. } => "BudgetWarning",
Event::Notification { .. } => "Notification",
Event::Error { .. } => "Error",
Event::Stop => "Stop",
Event::Heartbeat { .. } => "Heartbeat",
Event::Custom { .. } => "Custom",
}
}
}