1use std::time::Instant;
11
12use behest_core::id::RunId;
13use behest_core::message::Message;
14use behest_core::run::RunState;
15use behest_core::tool_types::ToolCall;
16use tokio_util::sync::CancellationToken;
17
18use crate::{
19 EventSink, HookContext, MemoryContext, ReadonlyContext, RunBudget, RunContext, RunSnapshot,
20 SessionContext, SessionState, ToolContext,
21};
22
23#[derive(Debug, Clone)]
25pub struct AppContext {
26 pub invocation_id: String,
28 pub session_id: String,
30 pub user_id: String,
32 pub app_name: String,
34}
35
36impl ReadonlyContext for AppContext {
37 fn invocation_id(&self) -> &str {
38 &self.invocation_id
39 }
40
41 fn session_id(&self) -> &str {
42 &self.session_id
43 }
44
45 fn user_id(&self) -> &str {
46 &self.user_id
47 }
48
49 fn app_name(&self) -> &str {
50 &self.app_name
51 }
52}
53
54#[derive(Debug)]
56pub struct SessionContextImpl {
57 pub app: AppContext,
59 pub state: SessionState,
61}
62
63impl ReadonlyContext for SessionContextImpl {
64 fn invocation_id(&self) -> &str {
65 self.app.invocation_id()
66 }
67
68 fn session_id(&self) -> &str {
69 self.app.session_id()
70 }
71
72 fn user_id(&self) -> &str {
73 self.app.user_id()
74 }
75
76 fn app_name(&self) -> &str {
77 self.app.app_name()
78 }
79}
80
81impl SessionContext for SessionContextImpl {
82 fn session_state(&self) -> &SessionState {
83 &self.state
84 }
85
86 fn session_state_mut(&mut self) -> &mut SessionState {
87 &mut self.state
88 }
89}
90
91#[derive(Debug)]
93pub struct RunContextImpl {
94 pub session: SessionContextImpl,
96 pub run_id: RunId,
98 pub cancel: CancellationToken,
100 pub deadline: Option<Instant>,
102 pub sink: EventSink,
104 pub budget: RunBudget,
106}
107
108impl ReadonlyContext for RunContextImpl {
109 fn invocation_id(&self) -> &str {
110 self.session.invocation_id()
111 }
112
113 fn session_id(&self) -> &str {
114 self.session.session_id()
115 }
116
117 fn user_id(&self) -> &str {
118 self.session.user_id()
119 }
120
121 fn app_name(&self) -> &str {
122 self.session.app_name()
123 }
124}
125
126impl SessionContext for RunContextImpl {
127 fn session_state(&self) -> &SessionState {
128 self.session.session_state()
129 }
130
131 fn session_state_mut(&mut self) -> &mut SessionState {
132 self.session.session_state_mut()
133 }
134}
135
136impl RunContext for RunContextImpl {
137 fn run_id(&self) -> &RunId {
138 &self.run_id
139 }
140
141 fn cancellation_token(&self) -> &CancellationToken {
142 &self.cancel
143 }
144
145 fn deadline(&self) -> Option<Instant> {
146 self.deadline
147 }
148
149 fn event_sink(&self) -> &EventSink {
150 &self.sink
151 }
152
153 fn budget(&self) -> &RunBudget {
154 &self.budget
155 }
156}
157
158#[derive(Debug)]
160pub struct ToolContextImpl {
161 pub run: RunContextImpl,
163 pub tool_call: ToolCall,
165}
166
167impl ReadonlyContext for ToolContextImpl {
168 fn invocation_id(&self) -> &str {
169 self.run.invocation_id()
170 }
171
172 fn session_id(&self) -> &str {
173 self.run.session_id()
174 }
175
176 fn user_id(&self) -> &str {
177 self.run.user_id()
178 }
179
180 fn app_name(&self) -> &str {
181 self.run.app_name()
182 }
183}
184
185impl SessionContext for ToolContextImpl {
186 fn session_state(&self) -> &SessionState {
187 self.run.session_state()
188 }
189
190 fn session_state_mut(&mut self) -> &mut SessionState {
191 self.run.session_state_mut()
192 }
193}
194
195impl RunContext for ToolContextImpl {
196 fn run_id(&self) -> &RunId {
197 self.run.run_id()
198 }
199
200 fn cancellation_token(&self) -> &CancellationToken {
201 self.run.cancellation_token()
202 }
203
204 fn deadline(&self) -> Option<Instant> {
205 self.run.deadline()
206 }
207
208 fn event_sink(&self) -> &EventSink {
209 self.run.event_sink()
210 }
211
212 fn budget(&self) -> &RunBudget {
213 self.run.budget()
214 }
215}
216
217impl ToolContext for ToolContextImpl {
218 fn tool_call(&self) -> &ToolCall {
219 &self.tool_call
220 }
221}
222
223pub struct MemoryContextImpl {
225 pub session: SessionContextImpl,
227 pub window: Vec<Message>,
229}
230
231impl ReadonlyContext for MemoryContextImpl {
232 fn invocation_id(&self) -> &str {
233 self.session.invocation_id()
234 }
235
236 fn session_id(&self) -> &str {
237 self.session.session_id()
238 }
239
240 fn user_id(&self) -> &str {
241 self.session.user_id()
242 }
243
244 fn app_name(&self) -> &str {
245 self.session.app_name()
246 }
247}
248
249impl SessionContext for MemoryContextImpl {
250 fn session_state(&self) -> &SessionState {
251 self.session.session_state()
252 }
253
254 fn session_state_mut(&mut self) -> &mut SessionState {
255 self.session.session_state_mut()
256 }
257}
258
259impl MemoryContext for MemoryContextImpl {
260 fn active_window(&self) -> &[Message] {
261 &self.window
262 }
263
264 fn demote(&self, messages: Vec<Message>) -> Result<(), String> {
265 let count = messages.len();
266 if count == 0 {
267 return Ok(());
268 }
269 let mut state = self.session_state().clone();
271 let key = format!("memory:demoted:{}", chrono::Utc::now().timestamp());
272 let value = serde_json::to_value(&messages).map_err(|e| e.to_string())?;
273 state.set(key, value);
274 Ok(())
275 }
276
277 fn compact(&self, _messages: Vec<Message>) -> Result<String, String> {
278 Ok(String::new())
281 }
282}
283
284pub struct HookContextImpl {
286 pub app: AppContext,
288 pub state: RunState,
290 pub run_id: RunId,
292 pub iteration: usize,
294 pub tokens_used: usize,
296}
297
298impl ReadonlyContext for HookContextImpl {
299 fn invocation_id(&self) -> &str {
300 &self.app.invocation_id
301 }
302
303 fn session_id(&self) -> &str {
304 &self.app.session_id
305 }
306
307 fn user_id(&self) -> &str {
308 &self.app.user_id
309 }
310
311 fn app_name(&self) -> &str {
312 &self.app.app_name
313 }
314}
315
316impl HookContext for HookContextImpl {
317 fn current_state(&self) -> &RunState {
318 &self.state
319 }
320
321 fn snapshot(&self) -> RunSnapshot {
322 RunSnapshot {
323 state: self.state.clone(),
324 run_id: self.run_id,
325 session_id: self.session_id().to_string(),
326 iteration: self.iteration,
327 tokens_used: self.tokens_used,
328 }
329 }
330}
331
332#[cfg(test)]
333#[allow(clippy::unwrap_used)]
334mod tests {
335 use super::*;
336 use serde_json::Value;
337
338 #[test]
339 fn session_state_set_and_get() {
340 let mut state = SessionState::new();
341 state.set("user:name", Value::String("Alice".to_string()));
342 assert_eq!(
343 state.get("user:name"),
344 Some(&Value::String("Alice".to_string()))
345 );
346 assert!(state.get("nonexistent").is_none());
347 }
348
349 #[test]
350 fn session_state_remove() {
351 let mut state = SessionState::new();
352 state.set("temp:key", Value::Bool(true));
353 assert!(state.remove("temp:key").is_some());
354 assert!(state.get("temp:key").is_none());
355 }
356
357 #[test]
358 fn run_budget_tracking() {
359 let mut budget = RunBudget::new(Some(1000));
360 assert_eq!(budget.remaining(), Some(1000));
361 budget.consume(300);
362 assert_eq!(budget.remaining(), Some(700));
363 assert_eq!(budget.used(), 300);
364 }
365
366 #[test]
367 fn run_budget_unlimited() {
368 let budget = RunBudget::new(None);
369 assert_eq!(budget.remaining(), None);
370 assert_eq!(budget.used(), 0);
371 }
372
373 #[test]
374 fn event_sink_emit_and_subscribe() {
375 let sink = EventSink::new();
376 let mut rx = sink.subscribe();
377 sink.emit(serde_json::json!({"type": "test"}));
378 let val = rx.borrow_and_update().clone();
380 assert!(val.is_some());
381 }
382
383 #[test]
384 fn tool_context_emits_progress() {
385 let app = AppContext {
386 invocation_id: "inv-1".to_string(),
387 session_id: "sess-1".to_string(),
388 user_id: "user-1".to_string(),
389 app_name: "test".to_string(),
390 };
391 let session = SessionContextImpl {
392 app,
393 state: SessionState::new(),
394 };
395 let sink = EventSink::new();
396 let mut sub = sink.subscribe();
397 let run = RunContextImpl {
398 session,
399 run_id: RunId::new(),
400 cancel: CancellationToken::new(),
401 deadline: None,
402 sink,
403 budget: RunBudget::new(None),
404 };
405 let ctx = ToolContextImpl {
406 run,
407 tool_call: ToolCall::new("call_1", "test_tool", Value::Null),
408 };
409
410 ctx.emit_progress("working", serde_json::json!({"percent": 50}));
411
412 let emitted = sub.borrow_and_update().clone();
413 assert!(emitted.is_some());
414 let event = emitted.unwrap();
415 assert_eq!(event["type"], "tool_progress");
416 assert_eq!(event["call_id"], "call_1");
417 }
418}