Skip to main content

soothe_client/
events.rs

1//! Client-facing event namespace constants for the Soothe daemon wire protocol.
2//!
3//! Internal catalog types (`soothe.internal.*`) are server-only and are never
4//! broadcast to WebSocket clients.
5
6use crate::verbosity::VerbosityTier;
7
8// ---------------------------------------------------------------------------
9// Plan / goal
10// ---------------------------------------------------------------------------
11
12/// Plan created.
13pub const EVENT_PLAN_CREATED: &str = "soothe.cognition.plan.created";
14/// Plan batch started.
15pub const EVENT_PLAN_BATCH_STARTED: &str = "soothe.cognition.plan.batch.started";
16/// Plan reflected.
17pub const EVENT_PLAN_REFLECTED: &str = "soothe.cognition.plan.reflected";
18
19/// Goal created.
20pub const EVENT_GOAL_CREATED: &str = "soothe.cognition.goal.created";
21/// Goal completed.
22pub const EVENT_GOAL_COMPLETED: &str = "soothe.cognition.goal.completed";
23/// Goal failed.
24pub const EVENT_GOAL_FAILED: &str = "soothe.cognition.goal.failed";
25/// Goal deferred.
26pub const EVENT_GOAL_DEFERRED: &str = "soothe.cognition.goal.deferred";
27/// Goal batch started.
28pub const EVENT_GOAL_BATCH_STARTED: &str = "soothe.cognition.goal.batch.started";
29/// Goal reported.
30pub const EVENT_GOAL_REPORTED: &str = "soothe.cognition.goal.reported";
31/// Goal directives applied.
32pub const EVENT_GOAL_DIRECTIVES_APPLIED: &str = "soothe.cognition.goal.directives.applied";
33
34// ---------------------------------------------------------------------------
35// Subagents
36// ---------------------------------------------------------------------------
37
38/// Explorer started.
39pub const EVENT_EXPLORER_STARTED: &str = "soothe.subagent.explorer.started";
40/// Explorer milestone.
41pub const EVENT_EXPLORER_MILESTONE: &str = "soothe.subagent.explorer.milestone";
42/// Explorer step completed.
43pub const EVENT_EXPLORER_STEP_COMPLETED: &str = "soothe.subagent.explorer.step.completed";
44/// Explorer completed.
45pub const EVENT_EXPLORER_COMPLETED: &str = "soothe.subagent.explorer.completed";
46
47/// Deep research started.
48pub const EVENT_DEEP_RESEARCH_STARTED: &str = "soothe.subagent.deep_research.started";
49/// Deep research progress.
50pub const EVENT_DEEP_RESEARCH_PROGRESS: &str = "soothe.subagent.deep_research.progress";
51/// Deep research step completed.
52pub const EVENT_DEEP_RESEARCH_STEP_COMPLETED: &str = "soothe.subagent.deep_research.step.completed";
53/// Deep research gather summary.
54pub const EVENT_DEEP_RESEARCH_GATHER_SUMMARY: &str = "soothe.subagent.deep_research.gather.summary";
55/// Deep research crawl summary.
56pub const EVENT_DEEP_RESEARCH_CRAWL_SUMMARY: &str = "soothe.subagent.deep_research.crawl.summary";
57/// Deep research completed.
58pub const EVENT_DEEP_RESEARCH_COMPLETED: &str = "soothe.subagent.deep_research.completed";
59
60// ---------------------------------------------------------------------------
61// Control / cards / tools
62// ---------------------------------------------------------------------------
63
64/// Replay complete control-plane frame.
65pub const EVENT_REPLAY_COMPLETE: &str = "replay_complete";
66/// Loop reattached control-plane frame.
67pub const EVENT_LOOP_REATTACHED_WIRE: &str = "loop_reattached";
68
69/// Card replay begin.
70pub const EVENT_CARD_REPLAY_BEGIN: &str = "card.replay_begin";
71/// Card created.
72pub const EVENT_CARD_CREATED: &str = "card.created";
73/// Card replay end.
74pub const EVENT_CARD_REPLAY_END: &str = "card.replay_end";
75
76/// Tool started.
77pub const EVENT_TOOL_STARTED: &str = "soothe.tool.execution.started";
78/// Tool completed.
79pub const EVENT_TOOL_COMPLETED: &str = "soothe.tool.execution.completed";
80/// Tool error.
81pub const EVENT_TOOL_ERROR: &str = "soothe.tool.execution.error";
82
83/// Stream tool call update.
84pub const EVENT_STREAM_TOOL_CALL_UPDATE: &str = "soothe.stream.tool_call.update";
85/// Tool call updates batch.
86pub const EVENT_TOOL_CALL_UPDATES_BATCH: &str = "tool_call_updates_batch";
87
88// ---------------------------------------------------------------------------
89// StrangeLoop / branch / protocol / output / autopilot / error
90// ---------------------------------------------------------------------------
91
92/// Strange loop started.
93pub const EVENT_STRANGE_LOOP_STARTED: &str = "soothe.cognition.strange_loop.started";
94/// Strange loop completed.
95pub const EVENT_STRANGE_LOOP_COMPLETED: &str = "soothe.cognition.strange_loop.completed";
96/// Strange loop plan decision.
97pub const EVENT_STRANGE_LOOP_PLAN_DECISION: &str = "soothe.cognition.strange_loop.plan.decision";
98/// Strange loop reasoned.
99pub const EVENT_STRANGE_LOOP_REASONED: &str = "soothe.cognition.strange_loop.reasoned";
100/// Strange loop step started.
101pub const EVENT_STRANGE_LOOP_STEP_STARTED: &str = "soothe.cognition.strange_loop.step.started";
102/// Strange loop step queued.
103pub const EVENT_STRANGE_LOOP_STEP_QUEUED: &str = "soothe.cognition.strange_loop.step.queued";
104/// Strange loop step completed.
105pub const EVENT_STRANGE_LOOP_STEP_COMPLETED: &str = "soothe.cognition.strange_loop.step.completed";
106/// Strange loop context compacted.
107pub const EVENT_STRANGE_LOOP_CONTEXT_COMPACTED: &str =
108    "soothe.cognition.strange_loop.context.compacted";
109
110/// Branch created.
111pub const EVENT_BRANCH_CREATED: &str = "soothe.cognition.branch.created";
112/// Branch retry started.
113pub const EVENT_BRANCH_RETRY_STARTED: &str = "soothe.cognition.branch.retry.started";
114
115/// Message received.
116pub const EVENT_MESSAGE_RECEIVED: &str = "soothe.protocol.message.received";
117/// Message sent.
118pub const EVENT_MESSAGE_SENT: &str = "soothe.protocol.message.sent";
119
120/// Final report.
121pub const EVENT_FINAL_REPORT: &str = "soothe.output.autonomous.final_report.reported";
122
123/// Autopilot status changed.
124pub const EVENT_AUTOPILOT_STATUS_CHANGED: &str = "soothe.system.autopilot.status.changed";
125/// Autopilot goal created.
126pub const EVENT_AUTOPILOT_GOAL_CREATED: &str = "soothe.system.autopilot.goal.created";
127/// Autopilot goal progress.
128pub const EVENT_AUTOPILOT_GOAL_PROGRESS: &str = "soothe.system.autopilot.goal.reported";
129/// Autopilot goal completed.
130pub const EVENT_AUTOPILOT_GOAL_COMPLETED: &str = "soothe.system.autopilot.goal.completed";
131/// Autopilot goal suspended.
132pub const EVENT_AUTOPILOT_GOAL_SUSPENDED: &str = "soothe.system.autopilot.goal.suspended";
133/// Autopilot goal blocked.
134pub const EVENT_AUTOPILOT_GOAL_BLOCKED: &str = "soothe.system.autopilot.goal.blocked";
135/// Autopilot dreaming entered.
136pub const EVENT_AUTOPILOT_DREAMING_ENTERED: &str = "soothe.system.autopilot.dreaming.started";
137/// Autopilot dreaming exited.
138pub const EVENT_AUTOPILOT_DREAMING_EXITED: &str = "soothe.system.autopilot.dreaming.completed";
139
140/// General failure.
141pub const EVENT_GENERAL_FAILED: &str = "soothe.error.general.failed";
142
143/// Parsed 4-segment namespace components.
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct ParsedNamespace {
146    /// Domain (e.g. `cognition`).
147    pub domain: String,
148    /// Component (e.g. `plan`).
149    pub component: String,
150    /// Action (e.g. `created`).
151    pub action: String,
152}
153
154/// Split a 4-segment event namespace into domain / component / action.
155///
156/// Returns `None` for non-`soothe.*` paths, short paths, or `soothe.internal.*`.
157pub fn parse_namespace(ns: &str) -> Option<ParsedNamespace> {
158    let parts: Vec<&str> = ns.split('.').collect();
159    if parts.len() < 4 || parts[0] != "soothe" {
160        return None;
161    }
162    if parts[1] == "internal" {
163        return None;
164    }
165    Some(ParsedNamespace {
166        domain: parts[1].to_string(),
167        component: parts[2].to_string(),
168        action: parts[3].to_string(),
169    })
170}
171
172/// Classify an event type / namespace into a verbosity tier.
173pub fn classify_event_verbosity(event_type_or_namespace: &str) -> VerbosityTier {
174    if let Some(parsed) = parse_namespace(event_type_or_namespace) {
175        return classify_by_domain(&parsed.domain, event_type_or_namespace);
176    }
177    classify_by_event_type_string(event_type_or_namespace)
178}
179
180fn classify_by_domain(domain: &str, full: &str) -> VerbosityTier {
181    match domain {
182        "cognition" => VerbosityTier::Normal,
183        "protocol" => VerbosityTier::Detailed,
184        "tool" => VerbosityTier::Internal,
185        "subagent" => classify_subagent_event(full),
186        "autopilot" | "system" => VerbosityTier::Normal,
187        "output" | "error" => VerbosityTier::Quiet,
188        _ => VerbosityTier::Normal,
189    }
190}
191
192fn classify_subagent_event(full: &str) -> VerbosityTier {
193    let Some(parsed) = parse_namespace(full) else {
194        return VerbosityTier::Normal;
195    };
196    match parsed.action.as_str() {
197        "started" | "completed" => VerbosityTier::Normal,
198        _ => VerbosityTier::Detailed,
199    }
200}
201
202fn classify_by_event_type_string(event_type: &str) -> VerbosityTier {
203    if event_type == EVENT_FINAL_REPORT || event_type == EVENT_GENERAL_FAILED {
204        return VerbosityTier::Quiet;
205    }
206    if event_type == EVENT_TOOL_STARTED {
207        return VerbosityTier::Internal;
208    }
209    VerbosityTier::Normal
210}
211
212/// Whether the event type represents a completion milestone.
213pub fn is_completion_event(event_type: &str) -> bool {
214    event_type.ends_with(".completed")
215        || event_type.ends_with(".failed")
216        || event_type == EVENT_GENERAL_FAILED
217}
218
219/// Lifecycle subagent events (started/completed) for progress UI.
220pub fn is_subagent_progress_event(event_type: &str) -> bool {
221    let Some(parsed) = parse_namespace(event_type) else {
222        return false;
223    };
224    parsed.domain == "subagent" && matches!(parsed.action.as_str(), "started" | "completed")
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[test]
232    fn parse_valid() {
233        let p = parse_namespace("soothe.cognition.plan.created").unwrap();
234        assert_eq!(p.domain, "cognition");
235        assert_eq!(p.component, "plan");
236        assert_eq!(p.action, "created");
237    }
238
239    #[test]
240    fn reject_internal() {
241        assert!(parse_namespace("soothe.internal.loop.completed").is_none());
242    }
243
244    #[test]
245    fn classify_quiet() {
246        assert_eq!(
247            classify_event_verbosity(EVENT_FINAL_REPORT),
248            VerbosityTier::Quiet
249        );
250    }
251}