Skip to main content

behest_context/
impls.rs

1//! Concrete implementations of the layered context traits.
2//!
3//! These structs provide the actual backing storage and behavior for each
4//! context level. They are designed to be composed hierarchically:
5//!
6//! ```text
7//! AppContext → SessionContextImpl → RunContextImpl → ToolContextImpl
8//! ```
9
10use 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/// Application-level context: global configuration and identity.
24#[derive(Debug, Clone)]
25pub struct AppContext {
26    /// Unique invocation identifier.
27    pub invocation_id: String,
28    /// Session identifier.
29    pub session_id: String,
30    /// Authenticated user identifier.
31    pub user_id: String,
32    /// Application name.
33    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/// Session-level context with mutable state.
55#[derive(Debug)]
56pub struct SessionContextImpl {
57    /// The underlying application context.
58    pub app: AppContext,
59    /// Mutable session state (key-value store).
60    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/// Run-level context with cancellation, deadline, event sink, and budget.
92#[derive(Debug)]
93pub struct RunContextImpl {
94    /// The underlying session context.
95    pub session: SessionContextImpl,
96    /// The unique run identifier.
97    pub run_id: RunId,
98    /// Cancellation token for cooperative cancellation.
99    pub cancel: CancellationToken,
100    /// Deadline for this run, if any.
101    pub deadline: Option<Instant>,
102    /// Event sink for emitting structured events.
103    pub sink: EventSink,
104    /// Token budget tracker.
105    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/// Tool execution context.
159#[derive(Debug)]
160pub struct ToolContextImpl {
161    /// The underlying run context.
162    pub run: RunContextImpl,
163    /// The tool call being executed.
164    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
223/// Memory context implementation with active window management.
224pub struct MemoryContextImpl {
225    /// The underlying session context.
226    pub session: SessionContextImpl,
227    /// Short-term active window messages.
228    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        // Default: demotion stores messages as JSON in session state
270        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        // Default: no-op compaction. Implementations should override this
279        // with LLM-based summarization.
280        Ok(String::new())
281    }
282}
283
284/// Hook context implementation for observing run state.
285pub struct HookContextImpl {
286    /// The application context for identity.
287    pub app: AppContext,
288    /// The current run state.
289    pub state: RunState,
290    /// The run identifier.
291    pub run_id: RunId,
292    /// Current iteration count.
293    pub iteration: usize,
294    /// Current token usage.
295    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        // The receiver should have the latest value
379        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}