Skip to main content

starweaver_context/
context_handle.rs

1use std::{
2    collections::BTreeSet,
3    sync::{Arc, Mutex, MutexGuard},
4};
5
6use starweaver_usage::Usage;
7
8use crate::{AgentContext, AgentEvent, Task, TaskManager, ToolSearchInvalidation, ToolSearchState};
9
10/// Stable grant name for handoff and auto-load context mutations.
11pub const CONTEXT_HANDOFF_CAPABILITY: &str = "starweaver.context.handoff";
12/// Stable grant name for task-manager mutations and snapshots.
13pub const CONTEXT_TASKS_CAPABILITY: &str = "starweaver.context.tasks";
14/// Stable grant name for usage-ledger mutations.
15pub const CONTEXT_USAGE_CAPABILITY: &str = "starweaver.context.usage";
16/// Stable grant name for tool-search state and event mutations.
17pub const CONTEXT_TOOL_SEARCH_CAPABILITY: &str = "starweaver.context.tool_search";
18
19/// Shared compatibility handle for tools explicitly using the Legacy dependency profile.
20#[derive(Clone)]
21pub struct AgentContextHandle {
22    inner: Arc<Mutex<AgentContext>>,
23}
24
25impl AgentContextHandle {
26    /// Create a handle from a complete context snapshot.
27    #[must_use]
28    pub fn new(context: AgentContext) -> Self {
29        Self {
30            inner: Arc::new(Mutex::new(context)),
31        }
32    }
33
34    /// Return the latest context snapshot held by this handle.
35    #[must_use]
36    pub fn snapshot(&self) -> AgentContext {
37        lock(&self.inner).clone()
38    }
39
40    /// Replace the context snapshot held by this handle.
41    pub fn replace(&self, context: AgentContext) {
42        *lock(&self.inner) = context;
43    }
44
45    /// Mutate the context snapshot held by this handle.
46    pub fn update(&self, update: impl FnOnce(&mut AgentContext)) {
47        update(&mut lock(&self.inner));
48    }
49}
50
51impl Default for AgentContextHandle {
52    fn default() -> Self {
53        Self::new(AgentContext::default())
54    }
55}
56
57impl std::fmt::Debug for AgentContextHandle {
58    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        formatter
60            .debug_struct("AgentContextHandle")
61            .field("snapshot", &self.snapshot())
62            .finish()
63    }
64}
65
66#[derive(Clone, Debug)]
67struct HandoffMutation {
68    handoff_message: Option<String>,
69    auto_load_files: Vec<String>,
70}
71
72/// Capability-specific mutable handle for context handoff state.
73#[derive(Clone)]
74pub struct ContextHandoffHandle {
75    inner: Arc<Mutex<HandoffMutation>>,
76}
77
78impl ContextHandoffHandle {
79    /// Create a narrow handoff handle from current context values.
80    #[must_use]
81    pub fn from_context(context: &AgentContext) -> Self {
82        Self {
83            inner: Arc::new(Mutex::new(HandoffMutation {
84                handoff_message: context.handoff_message.clone(),
85                auto_load_files: context.tools.auto_load_files.clone(),
86            })),
87        }
88    }
89
90    /// Set the rendered handoff and merge auto-load file paths.
91    pub fn set_handoff(&self, rendered: String, auto_load_files: &[String]) {
92        let mut mutation = lock(&self.inner);
93        mutation.handoff_message = Some(rendered);
94        for file in auto_load_files {
95            if !mutation.auto_load_files.contains(file) {
96                mutation.auto_load_files.push(file.clone());
97            }
98        }
99    }
100
101    /// Apply the isolated handoff state to a context.
102    pub fn apply_to(&self, context: &mut AgentContext) {
103        let mutation = lock(&self.inner);
104        context
105            .handoff_message
106            .clone_from(&mutation.handoff_message);
107        context
108            .tools
109            .auto_load_files
110            .clone_from(&mutation.auto_load_files);
111    }
112}
113
114/// Capability-specific mutable handle for task-manager state.
115#[derive(Clone)]
116pub struct TaskContextHandle {
117    inner: Arc<Mutex<TaskManager>>,
118}
119
120impl TaskContextHandle {
121    /// Create a narrow task handle from current context values.
122    #[must_use]
123    pub fn from_context(context: &AgentContext) -> Self {
124        Self {
125            inner: Arc::new(Mutex::new(context.tools.tasks.clone())),
126        }
127    }
128
129    /// Mutate only the task manager and return its latest snapshot.
130    pub fn update<R>(&self, update: impl FnOnce(&mut TaskManager) -> R) -> (R, Vec<Task>) {
131        let mut manager = lock(&self.inner);
132        let result = update(&mut manager);
133        let snapshot = manager.list_all();
134        drop(manager);
135        (result, snapshot)
136    }
137
138    /// Read the current task snapshot.
139    #[must_use]
140    pub fn snapshot(&self) -> Vec<Task> {
141        lock(&self.inner).list_all()
142    }
143
144    /// Apply the isolated task state and publish its durable snapshot to a context.
145    pub fn apply_to(&self, context: &mut AgentContext) {
146        context.tools.tasks = lock(&self.inner).clone();
147        let snapshot = context.tools.tasks.list_all();
148        context.set_tasks(snapshot);
149        context.publish_task_snapshot_event();
150    }
151}
152
153/// Capability-specific mutable handle for usage accounting.
154#[derive(Clone)]
155pub struct UsageContextHandle {
156    inner: Arc<Mutex<Usage>>,
157}
158
159impl UsageContextHandle {
160    /// Create a narrow usage handle from current context values.
161    #[must_use]
162    pub fn from_context(context: &AgentContext) -> Self {
163        Self {
164            inner: Arc::new(Mutex::new(context.usage.clone())),
165        }
166    }
167
168    /// Add usage to the isolated usage cell.
169    pub fn add_usage(&self, usage: &Usage) {
170        lock(&self.inner).add_assign(usage);
171    }
172
173    fn apply_to(&self, context: &mut AgentContext) {
174        context.usage = lock(&self.inner).clone();
175    }
176}
177
178#[derive(Clone, Debug)]
179struct ToolSearchMutation {
180    state: ToolSearchState,
181    events: Vec<AgentEvent>,
182}
183
184/// Capability-specific mutable handle for tool-search state and sideband events.
185#[derive(Clone)]
186pub struct ToolSearchContextHandle {
187    inner: Arc<Mutex<ToolSearchMutation>>,
188}
189
190impl ToolSearchContextHandle {
191    /// Create a narrow tool-search handle from current context values.
192    #[must_use]
193    pub fn from_context(context: &AgentContext) -> Self {
194        Self {
195            inner: Arc::new(Mutex::new(ToolSearchMutation {
196                state: context.tool_search_state(),
197                events: Vec::new(),
198            })),
199        }
200    }
201
202    /// Return the current loaded tool-search state.
203    #[must_use]
204    pub fn state(&self) -> ToolSearchState {
205        lock(&self.inner).state.clone()
206    }
207
208    /// Record loaded tools and namespaces.
209    pub fn record_loaded(
210        &self,
211        tools: impl IntoIterator<Item = impl Into<String>>,
212        namespaces: impl IntoIterator<Item = impl Into<String>>,
213    ) {
214        let mut mutation = lock(&self.inner);
215        for tool in tools {
216            push_unique(&mut mutation.state.loaded_tools, tool.into());
217        }
218        for namespace in namespaces {
219            push_unique(&mut mutation.state.loaded_namespaces, namespace.into());
220        }
221    }
222
223    /// Clear all loaded search state.
224    #[must_use]
225    pub fn clear_loaded(&self) -> ToolSearchInvalidation {
226        let mut mutation = lock(&self.inner);
227        ToolSearchInvalidation {
228            removed_tools: std::mem::take(&mut mutation.state.loaded_tools),
229            removed_namespaces: std::mem::take(&mut mutation.state.loaded_namespaces),
230        }
231    }
232
233    /// Publish a tool-search sideband event for absorption after tool execution.
234    pub fn publish_event(&self, event: AgentEvent) {
235        lock(&self.inner).events.push(event);
236    }
237
238    /// Apply isolated tool-search state and events to a context.
239    pub fn apply_to(&self, context: &mut AgentContext) {
240        let mutation = lock(&self.inner);
241        context
242            .tools
243            .loaded_tool_names
244            .clone_from(&mutation.state.loaded_tools);
245        context
246            .tools
247            .loaded_tool_namespaces
248            .clone_from(&mutation.state.loaded_namespaces);
249        for event in &mutation.events {
250            context.publish_event(event.clone());
251        }
252    }
253}
254
255/// Runtime-owned collection of isolated context mutation cells for one tool call.
256#[derive(Clone)]
257pub struct ContextMutationHandles {
258    handoff: ContextHandoffHandle,
259    tasks: TaskContextHandle,
260    usage: UsageContextHandle,
261    tool_search: ToolSearchContextHandle,
262}
263
264impl ContextMutationHandles {
265    /// Capture only the domains that narrow handles can mutate.
266    #[must_use]
267    pub fn from_context(context: &AgentContext) -> Self {
268        Self {
269            handoff: ContextHandoffHandle::from_context(context),
270            tasks: TaskContextHandle::from_context(context),
271            usage: UsageContextHandle::from_context(context),
272            tool_search: ToolSearchContextHandle::from_context(context),
273        }
274    }
275
276    /// Insert runtime-recognized capability handles into a dependency store.
277    pub fn insert_grants(
278        &self,
279        dependencies: &mut crate::DependencyStore,
280        requested: &BTreeSet<String>,
281    ) {
282        for capability in requested {
283            match capability.as_str() {
284                CONTEXT_HANDOFF_CAPABILITY => dependencies.insert(self.handoff.clone()),
285                CONTEXT_TASKS_CAPABILITY => dependencies.insert(self.tasks.clone()),
286                CONTEXT_USAGE_CAPABILITY => dependencies.insert(self.usage.clone()),
287                CONTEXT_TOOL_SEARCH_CAPABILITY => dependencies.insert(self.tool_search.clone()),
288                _ => {}
289            }
290        }
291    }
292
293    /// Apply only explicitly authorized mutation domains back to the runtime context.
294    pub fn apply_to(&self, context: &mut AgentContext, authorized: &BTreeSet<String>) {
295        for capability in authorized {
296            match capability.as_str() {
297                CONTEXT_HANDOFF_CAPABILITY => self.handoff.apply_to(context),
298                CONTEXT_TASKS_CAPABILITY => self.tasks.apply_to(context),
299                CONTEXT_USAGE_CAPABILITY => self.usage.apply_to(context),
300                CONTEXT_TOOL_SEARCH_CAPABILITY => self.tool_search.apply_to(context),
301                _ => {}
302            }
303        }
304    }
305}
306
307impl std::fmt::Debug for ContextHandoffHandle {
308    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
309        formatter.write_str("ContextHandoffHandle")
310    }
311}
312
313impl std::fmt::Debug for TaskContextHandle {
314    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315        formatter.write_str("TaskContextHandle")
316    }
317}
318
319impl std::fmt::Debug for UsageContextHandle {
320    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
321        formatter.write_str("UsageContextHandle")
322    }
323}
324
325impl std::fmt::Debug for ToolSearchContextHandle {
326    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
327        formatter.write_str("ToolSearchContextHandle")
328    }
329}
330
331impl std::fmt::Debug for ContextMutationHandles {
332    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
333        formatter.write_str("ContextMutationHandles")
334    }
335}
336
337fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
338    match mutex.lock() {
339        Ok(guard) => guard,
340        Err(error) => error.into_inner(),
341    }
342}
343
344fn push_unique(values: &mut Vec<String>, value: String) {
345    if !values.contains(&value) {
346        values.push(value);
347    }
348}