theway_core/agent/assembly/
events.rs1use std::sync::Arc;
2
3use tokio::sync::broadcast;
4
5use crate::types::AgentMessage;
6
7use super::AgentHarness;
8
9impl AgentHarness {
10 pub fn subscribe_harness(&self, listener: SessionListener) -> Box<dyn FnOnce() + Send> {
15 self.harness_listeners.lock().push(listener.clone());
16 let target = Arc::as_ptr(&listener) as *const () as usize;
17 let listeners = Arc::clone(&self.harness_listeners);
18 Box::new(move || {
19 let mut guard = listeners.lock();
20 if let Some(index) = guard
21 .iter()
22 .position(|item| (Arc::as_ptr(item) as *const () as usize) == target)
23 {
24 guard.remove(index);
25 }
26 })
27 }
28
29 pub fn subscribe_session_broadcast(&self) -> broadcast::Receiver<SessionEvent> {
32 self.session_broadcast_tx.subscribe()
33 }
34
35 pub(crate) fn emit_harness_event(&self, event: SessionEvent) {
37 let listeners = self.harness_listeners.lock().clone();
38 for listener in listeners {
39 let event = event.clone();
40 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || listener(event)));
41 }
42 let _ = self.session_broadcast_tx.send(event);
43 }
44
45 pub(super) async fn ensure_session_start_emitted(&self) {
46 let should_emit = {
47 let mut emitted = self.session_start_emitted.lock();
48 if *emitted {
49 false
50 } else {
51 *emitted = true;
52 true
53 }
54 };
55 if !should_emit {
56 return;
57 }
58 let messages_replayed = self.agent.state().messages.len();
59 self.runtime_extensions.ensure_session_start().await;
60 self.emit_harness_event(SessionEvent::Started { messages_replayed });
61 }
62
63 pub async fn start_runtime_extensions(&self) {
66 self.ensure_session_start_emitted().await;
67 }
68
69 pub async fn shutdown_runtime_extensions(&self) {
72 self.abort();
73 if tokio::time::timeout(SHUTDOWN_IDLE_TIMEOUT, self.agent.wait_until_idle())
77 .await
78 .is_err()
79 {
80 tracing::warn!("runtime-extension shutdown: run did not go idle in time; continuing");
81 }
82 self.runtime_extensions.shutdown().await;
83 }
84}
85
86const SHUTDOWN_IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(45);
88
89#[derive(Clone, Debug)]
91pub enum SessionEvent {
92 Started { messages_replayed: usize },
94 Compaction {
96 from_hook: bool,
97 summary: String,
98 tokens_before: u64,
99 },
100 Branch {
102 from_entry_id: Option<String>,
103 to_entry_id: Option<String>,
104 summary_entry_id: Option<String>,
105 },
106 PersistenceError { context: String, message: String },
108 TurnDecision {
110 decision: &'static str,
111 continuation_count: u32,
112 reason: Option<String>,
113 next_prompt_preview: Option<String>,
114 },
115 SkillsReloaded { total: usize },
117 ExtensionCommandOutcome {
119 outcome: theway_contract::extension::ExtensionCommandOutcome,
120 },
121}
122
123pub type SessionListener = Arc<dyn Fn(SessionEvent) + Send + Sync>;
124
125#[derive(Clone)]
127pub struct OnTurnEndContext {
128 pub transcript: Vec<AgentMessage>,
129 pub continuation_count: u32,
130 pub last_user_prompt: Option<String>,
131}
132
133#[derive(Clone, Debug)]
135pub enum TurnEndAction {
136 Noop,
137 Stop,
138 Pause { reason: String },
139 Continue { prompt: String },
140}
141
142impl TurnEndAction {
143 pub fn as_audit_str(&self) -> Option<&'static str> {
145 match self {
146 Self::Noop => None,
147 Self::Stop => Some("stop"),
148 Self::Pause { .. } => Some("pause"),
149 Self::Continue { .. } => Some("continue"),
150 }
151 }
152}
153
154#[derive(Clone, Debug)]
156pub struct TurnEndDecision {
157 pub action: TurnEndAction,
158 pub payload: Option<serde_json::Value>,
159}
160
161impl From<TurnEndAction> for TurnEndDecision {
162 fn from(action: TurnEndAction) -> Self {
163 Self {
164 action,
165 payload: None,
166 }
167 }
168}
169
170pub type OnTurnEndHook = Arc<
171 dyn Fn(
172 OnTurnEndContext,
173 tokio_util::sync::CancellationToken,
174 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = TurnEndDecision> + Send>>
175 + Send
176 + Sync,
177>;
178
179pub const DEFAULT_TURN_CONTINUATION_CAP: u32 = 25;