Skip to main content

behest_context/
lib.rs

1//! Layered context traits for the behest agent runtime.
2//!
3//! Inspired by ADK-Rust's `ReadonlyContext → CallbackContext → InvocationContext`
4//! hierarchy, this module defines progressively richer context interfaces that
5//! give agents, tools, hooks, and memory operations access to exactly the
6//! capabilities they need — no more, no less.
7//!
8//! # Hierarchy
9//!
10//! ```text
11//! ReadonlyContext         (identity: invocation_id, session_id, user_id, app_name)
12//!   ├── SessionContext    (+ session state, session store)
13//!   │     ├── RunContext  (+ run_id, cancellation, deadline, event sink, budget)
14//!   │     │     └── ToolContext (+ tool_call, emit_progress, search_memory)
15//!   │     └── MemoryContext (+ active_window, demote, compact)
16//!   └── HookContext       (+ current_state, snapshot)
17//! ```
18//!
19//! # Design constraints
20//!
21//! - Each layer adds specific, well-defined capabilities.
22//! - Context does not become a "global service locator" — each trait is
23//!   narrowly scoped to what the consumer needs.
24//! - Tool can access session info and stream progress, but cannot access
25//!   internal runtime state.
26//! - Every trait has clear ownership and lifetime semantics.
27
28#![forbid(unsafe_code)]
29#![deny(missing_docs)]
30#![deny(unreachable_pub)]
31
32use std::time::Instant;
33
34use behest_core::id::RunId;
35use behest_core::message::Message;
36use behest_core::run::RunState;
37use behest_core::tool_types::ToolCall;
38use serde_json::Value;
39use tokio::sync::watch;
40
41mod factory;
42mod impls;
43pub use factory::{
44    ContextAdapter, ContextFactory, ContextInput, ContextOutput, ContextResult, FunctionAdapter,
45    StaticAdapter,
46};
47pub use impls::{
48    AppContext, HookContextImpl, MemoryContextImpl, RunContextImpl, SessionContextImpl,
49    ToolContextImpl,
50};
51
52/// The minimum context available to any participant in the system.
53///
54/// Provides read-only identity information: who is making the request,
55/// which session it belongs to, and which application is running.
56pub trait ReadonlyContext {
57    /// Returns the unique invocation identifier for this run.
58    fn invocation_id(&self) -> &str;
59
60    /// Returns the session identifier.
61    fn session_id(&self) -> &str;
62
63    /// Returns the authenticated user identifier.
64    fn user_id(&self) -> &str;
65
66    /// Returns the application name.
67    fn app_name(&self) -> &str;
68}
69
70/// Context available during a session's lifetime.
71///
72/// Extends [`ReadonlyContext`] with mutable session state and access
73/// to the session store for persistence.
74pub trait SessionContext: ReadonlyContext {
75    /// Returns the current session state (key-value store scoped to the session).
76    fn session_state(&self) -> &SessionState;
77
78    /// Returns mutable access to session state.
79    fn session_state_mut(&mut self) -> &mut SessionState;
80}
81
82/// A key-value state store scoped to a single session.
83///
84/// Supports typed key prefixes (`user:`, `app:`, `temp:`) for
85/// different scoping semantics.
86#[derive(Debug, Clone, Default)]
87pub struct SessionState {
88    entries: std::collections::HashMap<String, Value>,
89}
90
91impl SessionState {
92    /// Creates an empty session state.
93    #[must_use]
94    pub fn new() -> Self {
95        Self::default()
96    }
97
98    /// Sets a value in the session state.
99    pub fn set(&mut self, key: impl Into<String>, value: Value) {
100        self.entries.insert(key.into(), value);
101    }
102
103    /// Gets a value from the session state.
104    #[must_use]
105    pub fn get(&self, key: &str) -> Option<&Value> {
106        self.entries.get(key)
107    }
108
109    /// Removes a value from the session state.
110    pub fn remove(&mut self, key: &str) -> Option<Value> {
111        self.entries.remove(key)
112    }
113
114    /// Returns all entries in the session state.
115    #[must_use]
116    pub fn all(&self) -> &std::collections::HashMap<String, Value> {
117        &self.entries
118    }
119}
120
121/// Context available during a single run invocation.
122///
123/// Extends [`SessionContext`] with run-level controls: cancellation,
124/// deadlines, event output, and token budget tracking.
125pub trait RunContext: SessionContext {
126    /// Returns the unique run identifier.
127    fn run_id(&self) -> &RunId;
128
129    /// Returns a cancellation token for cooperative cancellation.
130    fn cancellation_token(&self) -> &tokio_util::sync::CancellationToken;
131
132    /// Returns the deadline for this run, if any.
133    fn deadline(&self) -> Option<Instant>;
134
135    /// Returns the event sink for emitting structured events.
136    fn event_sink(&self) -> &EventSink;
137
138    /// Returns the token budget tracker for this run.
139    fn budget(&self) -> &RunBudget;
140}
141
142/// A sink for emitting events during a run.
143///
144/// Events are sent to all registered subscribers (local broadcast)
145/// and optionally forwarded to external publishers.
146#[derive(Clone)]
147pub struct EventSink {
148    tx: watch::Sender<Option<Value>>,
149}
150
151impl std::fmt::Debug for EventSink {
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        f.debug_struct("EventSink")
154            .field("receiver_count", &self.tx.receiver_count())
155            .finish()
156    }
157}
158
159impl EventSink {
160    /// Creates a new event sink.
161    #[must_use]
162    pub fn new() -> Self {
163        let (tx, _) = watch::channel(None);
164        Self { tx }
165    }
166
167    /// Emits an event to all subscribers.
168    ///
169    /// Returns the number of active subscribers that received the event.
170    pub fn emit(&self, event: Value) -> usize {
171        self.tx.send_if_modified(|current| {
172            *current = Some(event);
173            true
174        });
175        self.tx.receiver_count()
176    }
177
178    /// Creates a receiver for this sink.
179    #[must_use]
180    pub fn subscribe(&self) -> watch::Receiver<Option<Value>> {
181        self.tx.subscribe()
182    }
183}
184
185impl Default for EventSink {
186    fn default() -> Self {
187        Self::new()
188    }
189}
190
191/// Tracks the token budget for a single run.
192#[derive(Debug, Clone)]
193pub struct RunBudget {
194    max_tokens: Option<usize>,
195    used_tokens: usize,
196}
197
198impl RunBudget {
199    /// Creates a new budget with an optional maximum.
200    #[must_use]
201    pub fn new(max_tokens: Option<usize>) -> Self {
202        Self {
203            max_tokens,
204            used_tokens: 0,
205        }
206    }
207
208    /// Records token usage.
209    pub fn consume(&mut self, tokens: usize) {
210        self.used_tokens += tokens;
211    }
212
213    /// Returns the remaining token budget.
214    /// Returns `None` if there is no maximum.
215    #[must_use]
216    pub fn remaining(&self) -> Option<usize> {
217        self.max_tokens
218            .map(|max| max.saturating_sub(self.used_tokens))
219    }
220
221    /// Returns the total tokens consumed so far.
222    #[must_use]
223    pub fn used(&self) -> usize {
224        self.used_tokens
225    }
226}
227
228/// Context for tool execution.
229///
230/// Extends [`RunContext`] with tool-specific capabilities:
231/// - Access to the current tool call
232/// - Streaming progress output
233/// - Memory search (for tools that need context from long-term memory)
234pub trait ToolContext: RunContext {
235    /// Returns the tool call being executed.
236    fn tool_call(&self) -> &ToolCall;
237
238    /// Emits a progress update for the current tool execution.
239    ///
240    /// Progress events are streamed to consumers so they can observe
241    /// long-running tool operations in real time.
242    fn emit_progress(&self, status: &str, data: Value) {
243        let progress = serde_json::json!({
244            "type": "tool_progress",
245            "call_id": self.tool_call().id,
246            "tool_name": self.tool_call().name,
247            "status": status,
248            "data": data,
249        });
250        self.event_sink().emit(progress);
251    }
252
253    /// Searches long-term memory for context relevant to the current tool call.
254    ///
255    /// The default implementation returns an empty result. Implementations
256    /// with access to an embedding store should override this.
257    fn search_memory(&self, _query: &str, _limit: usize) -> Vec<MemoryEntry> {
258        Vec::new()
259    }
260}
261
262/// An entry retrieved from long-term memory.
263#[derive(Debug, Clone)]
264pub struct MemoryEntry {
265    /// The memory content.
266    pub content: String,
267    /// Relevance score (higher is more relevant).
268    pub score: f32,
269    /// Source identifier (e.g., session ID, document name).
270    pub source: Option<String>,
271}
272
273/// Context for memory operations (demotion, compaction).
274///
275/// Extends [`SessionContext`] with access to the active window
276/// and the ability to demote or compact messages.
277pub trait MemoryContext: SessionContext {
278    /// Returns the current active window (recent messages in context).
279    fn active_window(&self) -> &[Message];
280
281    /// Demotes messages from the active window to long-term storage.
282    ///
283    /// Returns an error if the demotion hook is not configured or
284    /// the storage backend is unavailable.
285    fn demote(&self, messages: Vec<Message>) -> Result<(), String>;
286
287    /// Compacts messages into a summary and injects it back into the
288    /// active window.
289    ///
290    /// Returns the generated summary text.
291    fn compact(&self, messages: Vec<Message>) -> Result<String, String>;
292}
293
294/// Context available to hooks observing the system.
295///
296/// Extends [`ReadonlyContext`] with the ability to inspect the current
297/// run state and take a snapshot for later replay.
298pub trait HookContext: ReadonlyContext {
299    /// Returns the current run state.
300    fn current_state(&self) -> &RunState;
301
302    /// Returns a snapshot of the current run for audit/replay.
303    fn snapshot(&self) -> RunSnapshot;
304}
305
306/// A snapshot of the current run state for audit and replay.
307#[derive(Debug, Clone)]
308pub struct RunSnapshot {
309    /// The state at the time of the snapshot.
310    pub state: RunState,
311    /// The run identifier.
312    pub run_id: RunId,
313    /// The session identifier.
314    pub session_id: String,
315    /// The number of iterations completed so far.
316    pub iteration: usize,
317    /// The current token usage.
318    pub tokens_used: usize,
319}