Skip to main content

starweaver_context/
agent_context.rs

1use std::{
2    collections::{BTreeMap, BTreeSet},
3    sync::Arc,
4};
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use starweaver_core::{
9    AgentId, ConversationId, Metadata, RunAttachments, RunId, SessionId, TaskId, TraceContext,
10};
11use starweaver_model::{
12    ContentPart, ModelMessage, ModelRequest, ModelRequestPart, ModelResponsePart, ToolReturnPart,
13};
14use starweaver_usage::{
15    PricingEstimate, Usage, UsageAgentTotal, UsageSnapshot, UsageSnapshotEntry,
16    add_optional_pricing,
17};
18
19use crate::{
20    AgentEvent, AgentInfo, AgentStreamQueueRegistry, AgentToolState, BusMessage, DependencyStore,
21    EventBus, HostCapabilities, MessageBus, ModelConfig, NoteStore, ResumableExportOptions,
22    ResumableState, RuntimeEphemeralState, SecurityConfig, ShellEnvironmentSnapshot, StateStore,
23    TASK_SNAPSHOT_EVENT_KIND, Task, TaskManager, TaskSnapshot, ToolCapabilityGrant, ToolConfig,
24    ToolIdWrapper, ToolRuntimeSnapshot, ToolSearchInvalidation, ToolSearchState, runtime_context,
25};
26
27/// Lifecycle-wide agent context.
28#[derive(Clone, Debug, Deserialize, Serialize)]
29pub struct AgentContext {
30    /// Agent identifier.
31    pub agent_id: AgentId,
32    /// Current run identifier.
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub run_id: Option<RunId>,
35    /// Stable logical session affinity identifier.
36    #[serde(default, skip_serializing_if = "Option::is_none")]
37    pub session_id: Option<SessionId>,
38    /// Parent run identifier if this context belongs to a delegated child run.
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub parent_run_id: Option<RunId>,
41    /// Parent-scoped delegated task identifier if this context executes a lightweight task.
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub parent_task_id: Option<TaskId>,
44    /// Conversation identifier.
45    pub conversation_id: ConversationId,
46    /// Canonical message history.
47    #[serde(default, skip_serializing_if = "Vec::is_empty")]
48    pub message_history: Vec<ModelMessage>,
49    /// Tool returns to inject at the start of the next run.
50    #[serde(default, skip_serializing_if = "Vec::is_empty")]
51    pub pending_tool_returns: Vec<ToolReturnPart>,
52    /// Subagent message history keyed by agent id.
53    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
54    pub subagent_history: BTreeMap<String, Vec<ModelMessage>>,
55    /// User prompt content collected for the current run.
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub user_prompts: Option<Vec<ContentPart>>,
58    /// Visible assistant response immediately before the current user prompt.
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub previous_assistant_response_reference: Option<String>,
61    /// Accumulated user steering messages for compact restore.
62    #[serde(default, skip_serializing_if = "Vec::is_empty")]
63    pub steering_messages: Vec<String>,
64    /// Rendered handoff message for post-compact or post-handoff restore.
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub handoff_message: Option<String>,
67    /// Runtime-only state, flattened to preserve the established serialized context shape.
68    #[serde(flatten)]
69    pub runtime: RuntimeEphemeralState,
70    /// Agent-owned durable state used by tool bundles.
71    #[serde(flatten)]
72    pub tools: AgentToolState,
73    /// Agent registry keyed by agent id.
74    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
75    pub agent_registry: BTreeMap<String, AgentInfo>,
76
77    /// Accumulated usage.
78    #[serde(default)]
79    pub usage: Usage,
80    /// Per-run cumulative usage ledger entries keyed by stable source id.
81    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
82    pub usage_snapshot_entries: BTreeMap<String, UsageSnapshotEntry>,
83    /// Model/runtime configuration used for injected runtime context and tool policies.
84    #[serde(default, skip_serializing_if = "ModelConfig::is_default")]
85    pub model_config: ModelConfig,
86    /// Tool-level configuration used by first-party and host tools.
87    #[serde(default, skip_serializing_if = "ToolConfig::is_default")]
88    pub tool_config: ToolConfig,
89    /// Security-related runtime configuration.
90    #[serde(default, skip_serializing_if = "SecurityConfig::is_default")]
91    pub security: SecurityConfig,
92    /// Context creation/entry time used for elapsed runtime context.
93    #[serde(default = "Utc::now")]
94    pub started_at: DateTime<Utc>,
95    /// Context exit time.
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub ended_at: Option<DateTime<Utc>>,
98    /// State store.
99    #[serde(default)]
100    pub state: StateStore,
101    /// Event bus.
102    #[serde(default)]
103    pub events: EventBus,
104    /// Persisted notes.
105    #[serde(default, skip_serializing_if = "NoteStore::is_empty")]
106    pub notes: NoteStore,
107    /// Message bus.
108    #[serde(default)]
109    pub messages: MessageBus,
110    /// Trace correlation context.
111    #[serde(default, skip_serializing_if = "TraceContext::is_empty")]
112    pub trace_context: TraceContext,
113    /// Context metadata.
114    #[serde(default, skip_serializing_if = "Metadata::is_empty")]
115    pub metadata: Metadata,
116    /// Typed dependencies, skipped from serialization.
117    #[serde(skip)]
118    pub dependencies: DependencyStore,
119}
120
121impl AgentContext {
122    /// Create a fresh context.
123    #[must_use]
124    pub fn new(agent_id: AgentId) -> Self {
125        let mut agent_registry = BTreeMap::new();
126        agent_registry.insert(
127            agent_id.as_str().to_string(),
128            AgentInfo::new(agent_id.as_str(), agent_id.as_str()),
129        );
130        let mut messages = MessageBus::new();
131        messages.subscribe(agent_id.as_str());
132        Self {
133            agent_id,
134            run_id: None,
135            session_id: None,
136            parent_run_id: None,
137            parent_task_id: None,
138            conversation_id: ConversationId::new(),
139            message_history: Vec::new(),
140            pending_tool_returns: Vec::new(),
141            subagent_history: BTreeMap::new(),
142            user_prompts: None,
143            previous_assistant_response_reference: None,
144            steering_messages: Vec::new(),
145            handoff_message: None,
146            runtime: RuntimeEphemeralState::default(),
147            tools: AgentToolState::default(),
148            agent_registry,
149            usage: Usage::default(),
150            usage_snapshot_entries: BTreeMap::new(),
151            model_config: ModelConfig::default(),
152            tool_config: ToolConfig::default(),
153            security: SecurityConfig::default(),
154            started_at: Utc::now(),
155            ended_at: None,
156            state: StateStore::new(),
157            events: EventBus::new(),
158            notes: NoteStore::new(),
159            messages,
160            trace_context: TraceContext::default(),
161            metadata: Metadata::default(),
162            dependencies: DependencyStore::new(),
163        }
164    }
165
166    /// Restore a context from serialized state.
167    #[must_use]
168    pub fn from_state(state: ResumableState) -> Self {
169        let mut context = Self::new(state.agent_id.clone());
170        context.run_id = state.run_id;
171        context.session_id = state.session_id;
172        context.parent_run_id = state.parent_run_id;
173        context.parent_task_id = state.parent_task_id;
174        context.conversation_id = state.conversation_id.unwrap_or_default();
175        context.message_history = state.message_history;
176        context.pending_tool_returns = state.pending_tool_returns;
177        context.subagent_history = state.subagent_history;
178        context.user_prompts = state.user_prompts;
179        context.previous_assistant_response_reference = state.previous_assistant_response_reference;
180        context.steering_messages = state.steering_messages;
181        context.handoff_message = state.handoff_message;
182        context.tools.shell_environment = state.shell_env;
183        context.tools.deferred_call_metadata = state.deferred_tool_metadata;
184        context.agent_registry = state.agent_registry;
185        if context.agent_registry.is_empty() {
186            context.agent_registry.insert(
187                context.agent_id.as_str().to_string(),
188                AgentInfo::new(context.agent_id.as_str(), context.agent_id.as_str()),
189            );
190        }
191        context.security = state.security;
192        context.tools.auto_load_files = state.auto_load_files;
193        context.tools.tasks = TaskManager::from_exported(state.tasks);
194        context.notes = NoteStore::from_map(state.notes);
195        context.tools.loaded_tool_names = state.tool_search_loaded_tools;
196        context.tools.loaded_tool_namespaces = state.tool_search_loaded_namespaces;
197        context.usage = state.usage;
198        context.usage_snapshot_entries = state.usage_snapshot_entries;
199        context.model_config = state.model_config;
200        context.tool_config = state.tool_config;
201        context.started_at = state.started_at;
202        context.state = state.state;
203        context.messages = state.message_bus;
204        context.trace_context = state.trace_snapshot;
205        context.metadata = state.metadata;
206        context
207    }
208
209    /// Export curated portable context state for session restoration.
210    #[must_use]
211    pub fn export_state(&self) -> ResumableState {
212        self.export_state_with_options(ResumableExportOptions::curated())
213    }
214
215    /// Export full Starweaver runtime context state.
216    #[must_use]
217    pub fn export_full_state(&self) -> ResumableState {
218        self.export_state_with_options(ResumableExportOptions::full())
219    }
220
221    /// Return JSON-compatible run attachments for this context.
222    ///
223    /// This is a typed view over context metadata. Generic SDK code keeps these
224    /// attachments local to Starweaver execution and does not forward them to
225    /// provider HTTP headers or provider-specific request fields.
226    #[must_use]
227    pub fn run_attachments(&self) -> RunAttachments {
228        RunAttachments::from_metadata(self.metadata.clone())
229    }
230
231    /// Return the underlying JSON-compatible run attachment values.
232    #[must_use]
233    pub const fn run_attachment_values(&self) -> &Metadata {
234        &self.metadata
235    }
236
237    /// Return mutable underlying JSON-compatible run attachment values.
238    pub const fn run_attachment_values_mut(&mut self) -> &mut Metadata {
239        &mut self.metadata
240    }
241
242    /// Insert or replace one run attachment value.
243    pub fn set_run_attachment(
244        &mut self,
245        key: impl Into<String>,
246        value: serde_json::Value,
247    ) -> Option<serde_json::Value> {
248        self.metadata.insert(key.into(), value)
249    }
250
251    /// Merge run attachments into the context. Incoming keys replace existing values.
252    pub fn merge_run_attachments(&mut self, attachments: impl Into<Metadata>) {
253        self.metadata.extend(attachments.into());
254    }
255
256    /// Export context state with explicit export options.
257    #[must_use]
258    #[allow(clippy::too_many_lines)]
259    pub fn export_state_with_options(&self, options: ResumableExportOptions) -> ResumableState {
260        ResumableState {
261            agent_id: self.agent_id.clone(),
262            run_id: options
263                .include_starweaver_extensions()
264                .then(|| self.run_id.clone())
265                .flatten(),
266            session_id: self.session_id.clone(),
267            parent_run_id: options
268                .include_starweaver_extensions()
269                .then(|| self.parent_run_id.clone())
270                .flatten(),
271            parent_task_id: options
272                .include_starweaver_extensions()
273                .then(|| self.parent_task_id.clone())
274                .flatten(),
275            conversation_id: options
276                .include_starweaver_extensions()
277                .then(|| self.conversation_id.clone()),
278            message_history: if options.include_starweaver_extensions() {
279                self.message_history.clone()
280            } else {
281                Vec::new()
282            },
283            pending_tool_returns: if options.include_starweaver_extensions() {
284                self.pending_tool_returns.clone()
285            } else {
286                Vec::new()
287            },
288            subagent_history: if options.include_subagent() {
289                self.subagent_history.clone()
290            } else {
291                BTreeMap::new()
292            },
293            user_prompts: self.user_prompts.clone(),
294            previous_assistant_response_reference: self
295                .previous_assistant_response_reference
296                .clone(),
297            steering_messages: self.steering_messages.clone(),
298            handoff_message: self.handoff_message.clone(),
299            shell_env: BTreeMap::new(),
300            deferred_tool_metadata: self.tools.deferred_call_metadata.clone(),
301            agent_registry: if options.include_subagent() {
302                self.agent_registry.clone()
303            } else {
304                BTreeMap::new()
305            },
306            approval_required_tools: Vec::new(),
307            approval_required_mcp_servers: Vec::new(),
308            security: if options.include_runtime_policy() {
309                self.security.clone()
310            } else {
311                SecurityConfig::default()
312            },
313            auto_load_files: self.tools.auto_load_files.clone(),
314            tasks: self.tools.tasks.export_tasks(),
315            notes: self.notes.to_map(),
316            tool_search_loaded_tools: self.tools.loaded_tool_names.clone(),
317            tool_search_loaded_namespaces: self.tools.loaded_tool_namespaces.clone(),
318            usage: if options.include_starweaver_extensions() {
319                self.usage.clone()
320            } else {
321                Usage::default()
322            },
323            usage_snapshot_entries: if options.include_usage_ledger() {
324                self.usage_snapshot_entries.clone()
325            } else {
326                BTreeMap::new()
327            },
328            model_config: if options.include_runtime_policy() {
329                self.model_config.clone()
330            } else {
331                ModelConfig::default()
332            },
333            tool_config: if options.include_runtime_policy() {
334                self.tool_config.clone()
335            } else {
336                ToolConfig::default()
337            },
338            started_at: if options.include_starweaver_extensions() {
339                self.started_at
340            } else {
341                DateTime::<Utc>::UNIX_EPOCH
342            },
343            state: if options.include_starweaver_extensions() {
344                self.state.clone()
345            } else {
346                StateStore::new()
347            },
348            message_bus: if options.include_starweaver_extensions() {
349                self.messages.clone()
350            } else {
351                MessageBus::new()
352            },
353            trace_snapshot: if options.include_starweaver_extensions() {
354                self.trace_context.clone()
355            } else {
356                TraceContext::default()
357            },
358            metadata: if options.include_starweaver_extensions() {
359                self.metadata.clone()
360            } else {
361                Metadata::default()
362            },
363            extra: BTreeMap::new(),
364        }
365    }
366
367    /// Replace context with serialized state.
368    pub fn restore_state(&mut self, state: ResumableState) {
369        let dependencies = self.dependencies.clone();
370        let security = self.security.clone();
371        *self = Self::from_state(state);
372        self.dependencies = dependencies;
373        // Current runtime security wins unless caller explicitly constructs a context from state
374        // with `from_state`.
375        self.security = security;
376    }
377
378    /// Set the stable logical session affinity identifier.
379    pub fn set_session_id(&mut self, session_id: SessionId) {
380        self.session_id = Some(session_id);
381    }
382
383    /// Return the stable logical session affinity identifier.
384    #[must_use]
385    pub const fn session_id(&self) -> Option<&SessionId> {
386        self.session_id.as_ref()
387    }
388
389    /// Prepare context for a new run.
390    pub fn prepare_new_run(&mut self) {
391        self.run_id = Some(RunId::new());
392        self.started_at = Utc::now();
393        self.ended_at = None;
394        self.runtime.lifecycle.entered = true;
395        self.runtime.lifecycle.stream_queue_enabled = false;
396        self.runtime.lifecycle.compact_depth = 0;
397        self.runtime.run_toolsets_closed = false;
398        self.runtime.tool_id_wrapper.clear();
399        self.runtime.agent_stream_queues = AgentStreamQueueRegistry::default();
400        if self.parent_run_id.is_none() && !self.metadata.contains_key("parent_agent_id") {
401            self.usage_snapshot_entries.clear();
402        }
403        self.tools.deferred_call_metadata.clear();
404        self.runtime.force_inject_context = false;
405        self.previous_assistant_response_reference = None;
406        self.messages.subscribe(self.agent_id.as_str());
407    }
408
409    /// Mark the active run as finished.
410    pub fn finish_run(&mut self) {
411        self.ended_at = Some(Utc::now());
412        self.runtime.lifecycle.entered = false;
413    }
414
415    /// Create a child context for subagent execution using the same value for id and name.
416    #[must_use]
417    pub fn subagent_context(&self, agent_id: impl Into<String>) -> Self {
418        let agent_id = agent_id.into();
419        self.subagent_context_with_agent_id(agent_id.clone(), agent_id)
420    }
421
422    /// Create a child context for subagent execution with separate display name and stable id.
423    #[must_use]
424    pub fn subagent_context_with_agent_id(
425        &self,
426        agent_name: impl Into<String>,
427        agent_id: impl Into<String>,
428    ) -> Self {
429        let agent_name = agent_name.into();
430        let agent_id = AgentId::from_string(agent_id);
431        let mut child = self.clone();
432        child.agent_id = agent_id.clone();
433        child.run_id = None;
434        child.parent_run_id.clone_from(&self.run_id);
435        child.parent_task_id = None;
436        child.message_history = self
437            .subagent_history
438            .get(agent_id.as_str())
439            .cloned()
440            .unwrap_or_default();
441        child.user_prompts = None;
442        child.previous_assistant_response_reference = None;
443        child.steering_messages = Vec::new();
444        child.handoff_message = None;
445        child.runtime.tool_id_wrapper = ToolIdWrapper::default();
446        child.runtime.tool_tags = Vec::new();
447        child.started_at = Utc::now();
448        child.ended_at = None;
449        child.security = self.security.clone();
450        child.metadata.insert(
451            "parent_agent_id".to_string(),
452            serde_json::json!(self.agent_id.as_str()),
453        );
454        child.metadata.insert(
455            "agent_name".to_string(),
456            serde_json::json!(agent_name.as_str()),
457        );
458        if let Some(run_id) = &self.run_id {
459            child.metadata.insert(
460                "parent_run_id".to_string(),
461                serde_json::json!(run_id.as_str()),
462            );
463        }
464        child.agent_registry.insert(
465            agent_id.as_str().to_string(),
466            AgentInfo::new(agent_id.as_str(), agent_name)
467                .with_parent_agent_id(self.agent_id.as_str()),
468        );
469        child.messages.subscribe(agent_id.as_str());
470        child
471    }
472
473    /// Absorb child context state that should survive successful subagent execution.
474    pub fn absorb_subagent_context(&mut self, child: &Self) {
475        let event_cursor = self.events.len();
476        self.usage = child.usage.clone();
477        self.usage_snapshot_entries
478            .clone_from(&child.usage_snapshot_entries);
479        self.notes = child.notes.clone();
480        self.tools.tasks = child.tools.tasks.clone();
481        self.state = child.state.clone();
482        self.messages = child.messages.clone();
483        self.agent_registry = child.agent_registry.clone();
484        self.subagent_history.insert(
485            child.agent_id.as_str().to_string(),
486            child.message_history.clone(),
487        );
488        for (agent_id, history) in &child.subagent_history {
489            self.subagent_history
490                .entry(agent_id.clone())
491                .or_insert_with(|| history.clone());
492        }
493        for event in child.events.events().iter().skip(event_cursor) {
494            self.events.publish(event.clone());
495        }
496    }
497
498    /// Attach trace correlation context.
499    #[must_use]
500    pub fn with_trace_context(mut self, trace_context: TraceContext) -> Self {
501        self.trace_context = trace_context;
502        self
503    }
504
505    /// Replace trace correlation context.
506    pub fn set_trace_context(&mut self, trace_context: TraceContext) {
507        self.trace_context = trace_context;
508    }
509
510    /// Record a tool name loaded through dynamic tool search.
511    pub fn record_tool_search_loaded_tool(&mut self, tool_name: impl Into<String>) {
512        push_unique(&mut self.tools.loaded_tool_names, tool_name.into());
513    }
514
515    /// Record a namespace loaded through dynamic tool search.
516    pub fn record_tool_search_loaded_namespace(&mut self, namespace: impl Into<String>) {
517        push_unique(&mut self.tools.loaded_tool_namespaces, namespace.into());
518    }
519
520    /// Record loaded tool-search state in one update.
521    pub fn record_tool_search_loaded(
522        &mut self,
523        tools: impl IntoIterator<Item = impl Into<String>>,
524        namespaces: impl IntoIterator<Item = impl Into<String>>,
525    ) {
526        for tool in tools {
527            self.record_tool_search_loaded_tool(tool);
528        }
529        for namespace in namespaces {
530            self.record_tool_search_loaded_namespace(namespace);
531        }
532    }
533
534    /// Clear all loaded tool-search state and return the removed values.
535    pub fn clear_tool_search_loaded(&mut self) -> ToolSearchInvalidation {
536        ToolSearchInvalidation {
537            removed_tools: std::mem::take(&mut self.tools.loaded_tool_names),
538            removed_namespaces: std::mem::take(&mut self.tools.loaded_tool_namespaces),
539        }
540    }
541
542    /// Retain only loaded tool-search entries accepted by the supplied predicates.
543    pub fn retain_tool_search_loaded(
544        &mut self,
545        mut keep_tool: impl FnMut(&str) -> bool,
546        mut keep_namespace: impl FnMut(&str) -> bool,
547    ) -> ToolSearchInvalidation {
548        ToolSearchInvalidation {
549            removed_tools: retain_matching(&mut self.tools.loaded_tool_names, |tool| {
550                keep_tool(tool)
551            }),
552            removed_namespaces: retain_matching(
553                &mut self.tools.loaded_tool_namespaces,
554                |namespace| keep_namespace(namespace),
555            ),
556        }
557    }
558
559    /// Return the current tool-search state snapshot.
560    #[must_use]
561    pub fn tool_search_state(&self) -> ToolSearchState {
562        ToolSearchState {
563            loaded_tools: self.tools.loaded_tool_names.clone(),
564            loaded_namespaces: self.tools.loaded_tool_namespaces.clone(),
565        }
566    }
567
568    /// Record a model message in context history.
569    pub fn push_message(&mut self, message: ModelMessage) {
570        self.message_history.push(message);
571    }
572
573    /// Record usage in the context ledger.
574    pub const fn add_usage(&mut self, usage: &Usage) {
575        self.usage.add_assign(usage);
576    }
577
578    /// Update one cumulative usage snapshot ledger entry and return the latest run snapshot.
579    #[must_use]
580    #[allow(clippy::too_many_arguments)]
581    pub fn update_usage_snapshot_entry(
582        &mut self,
583        agent_id: impl Into<String>,
584        agent_name: impl Into<String>,
585        model_id: impl Into<String>,
586        usage: Usage,
587        estimate_pricing: Option<PricingEstimate>,
588        usage_id: Option<String>,
589        source: impl Into<String>,
590        ledger_key: Option<String>,
591    ) -> UsageSnapshot {
592        let agent_id = agent_id.into();
593        let entry = UsageSnapshotEntry {
594            agent_id: agent_id.clone(),
595            agent_name: agent_name.into(),
596            model_id: model_id.into(),
597            usage,
598            estimate_pricing,
599            usage_id,
600            source: source.into(),
601        };
602        self.usage_snapshot_entries
603            .insert(ledger_key.unwrap_or(agent_id), entry);
604        self.build_usage_snapshot()
605    }
606
607    /// Update one cumulative external usage ledger entry.
608    ///
609    /// This helper is for host services, sub-systems, and adapters that need to
610    /// include non-model usage in the run snapshot without pretending the usage
611    /// came from the active model request.
612    #[must_use]
613    pub fn update_external_usage_snapshot_entry(
614        &mut self,
615        source_id: impl Into<String>,
616        source_name: impl Into<String>,
617        model_id: impl Into<String>,
618        usage: Usage,
619        estimate_pricing: Option<PricingEstimate>,
620        usage_id: Option<String>,
621    ) -> UsageSnapshot {
622        let source_id = source_id.into();
623        let ledger_key = usage_id.as_ref().map_or_else(
624            || format!("external:{source_id}"),
625            |usage_id| format!("external:{source_id}:{usage_id}"),
626        );
627        self.update_usage_snapshot_entry(
628            source_id,
629            source_name,
630            model_id,
631            usage,
632            estimate_pricing,
633            usage_id,
634            "external",
635            Some(ledger_key),
636        )
637    }
638
639    /// Build a cumulative usage snapshot for this run.
640    #[must_use]
641    pub fn build_usage_snapshot(&self) -> UsageSnapshot {
642        let mut total_usage = Usage::default();
643        let mut estimate_pricing = None;
644        let mut agent_usages = BTreeMap::<String, UsageAgentTotal>::new();
645        let mut model_usages = BTreeMap::<String, Usage>::new();
646        let mut model_estimate_pricing = BTreeMap::<String, PricingEstimate>::new();
647        let mut entries = self
648            .usage_snapshot_entries
649            .values()
650            .cloned()
651            .collect::<Vec<_>>();
652        entries.sort_by(|left, right| left.agent_id.cmp(&right.agent_id));
653        for entry in &entries {
654            total_usage.add_assign(&entry.usage);
655            add_optional_pricing(&mut estimate_pricing, entry.estimate_pricing.as_ref());
656            if let Some(pricing) = &entry.estimate_pricing {
657                model_estimate_pricing
658                    .entry(entry.model_id.clone())
659                    .or_default()
660                    .add_assign(pricing);
661            }
662            if let Some(agent_total) = agent_usages.get_mut(&entry.agent_id) {
663                agent_total.usage.add_assign(&entry.usage);
664                if agent_total.model_id != entry.model_id {
665                    agent_total.model_id = "multiple".to_string();
666                }
667                if agent_total.usage_id != entry.usage_id {
668                    agent_total.usage_id = None;
669                }
670                add_optional_pricing(
671                    &mut agent_total.estimate_pricing,
672                    entry.estimate_pricing.as_ref(),
673                );
674            } else {
675                agent_usages.insert(
676                    entry.agent_id.clone(),
677                    UsageAgentTotal {
678                        agent_name: entry.agent_name.clone(),
679                        model_id: entry.model_id.clone(),
680                        usage: entry.usage.clone(),
681                        estimate_pricing: entry.estimate_pricing.clone(),
682                        usage_id: entry.usage_id.clone(),
683                        source: entry.source.clone(),
684                    },
685                );
686            }
687            model_usages
688                .entry(entry.model_id.clone())
689                .or_default()
690                .add_assign(&entry.usage);
691        }
692        UsageSnapshot {
693            run_id: self
694                .parent_run_id
695                .as_ref()
696                .or(self.run_id.as_ref())
697                .map_or_else(String::new, |run_id| run_id.as_str().to_string()),
698            latest_usage: None,
699            total_usage,
700            estimate_pricing,
701            entries,
702            agent_usages,
703            model_usages,
704            model_estimate_pricing,
705        }
706    }
707
708    /// Return the latest model request usage reported by the provider.
709    #[must_use]
710    pub fn latest_request_usage(&self) -> Option<&Usage> {
711        self.message_history.iter().rev().find_map(|message| {
712            let ModelMessage::Response(response) = message else {
713                return None;
714            };
715            (!response.usage.is_empty()).then_some(&response.usage)
716        })
717    }
718
719    /// Return the latest model request token usage reported by the provider.
720    #[must_use]
721    pub fn latest_request_total_tokens(&self) -> Option<u64> {
722        self.latest_request_usage()
723            .and_then(|usage| (usage.total_tokens > 0).then_some(usage.total_tokens))
724    }
725
726    /// Append synthetic error tool returns for any unclosed tool calls in message history.
727    ///
728    /// This is used when a run fails or is interrupted after a provider has emitted tool calls but
729    /// before every tool return has been recorded. It keeps recovered history acceptable to
730    /// providers that require every tool call to be closed by a matching tool return.
731    pub fn repair_dangling_tool_calls(&mut self, reason: impl Into<String>) -> usize {
732        let reason = reason.into();
733        let mut pending = Vec::<(String, String)>::new();
734        for message in &self.message_history {
735            match message {
736                ModelMessage::Response(response) => {
737                    for part in &response.parts {
738                        match part {
739                            ModelResponsePart::ToolCall(call)
740                            | ModelResponsePart::ProviderToolCall { call, .. } => {
741                                pending.push((call.id.clone(), call.name.clone()));
742                            }
743                            ModelResponsePart::Text { .. }
744                            | ModelResponsePart::ProviderText { .. }
745                            | ModelResponsePart::Thinking { .. }
746                            | ModelResponsePart::ProviderThinking { .. }
747                            | ModelResponsePart::NativeToolCall { .. }
748                            | ModelResponsePart::NativeToolReturn { .. }
749                            | ModelResponsePart::File { .. }
750                            | ModelResponsePart::Compaction { .. }
751                            | ModelResponsePart::ProviderOpaque { .. } => {}
752                        }
753                    }
754                }
755                ModelMessage::Request(request) => {
756                    for part in &request.parts {
757                        if let ModelRequestPart::ToolReturn(tool_return) = part {
758                            pending.retain(|(id, _)| id != &tool_return.tool_call_id);
759                        }
760                    }
761                }
762            }
763        }
764        if pending.is_empty() {
765            return 0;
766        }
767        let repaired_count = pending.len();
768        let mut parts = Vec::with_capacity(repaired_count);
769        for (tool_call_id, name) in pending {
770            let mut metadata = Metadata::default();
771            metadata.insert(
772                "starweaver.repaired_dangling_tool_call".to_string(),
773                serde_json::json!(true),
774            );
775            metadata.insert("reason".to_string(), serde_json::json!(reason.clone()));
776            parts.push(ModelRequestPart::ToolReturn(
777                ToolReturnPart::new(
778                    tool_call_id,
779                    name,
780                    serde_json::json!({
781                        "error": "tool_call_interrupted",
782                        "message": reason.clone(),
783                    }),
784                )
785                .with_error(true)
786                .with_metadata(metadata),
787            ));
788        }
789        self.message_history
790            .push(ModelMessage::Request(ModelRequest {
791                parts,
792                timestamp: Some(Utc::now()),
793                instructions: None,
794                run_id: self.run_id.clone(),
795                conversation_id: Some(self.conversation_id.clone()),
796                metadata: serde_json::json!({
797                    "starweaver.repaired_dangling_tool_calls": true,
798                })
799                .as_object()
800                .cloned()
801                .unwrap_or_default(),
802            }));
803        repaired_count
804    }
805
806    /// Publish an event.
807    pub fn publish_event(&mut self, event: AgentEvent) {
808        self.events.publish(event);
809    }
810
811    /// Return all tasks from the typed task manager.
812    #[must_use]
813    pub fn tasks(&self) -> Vec<Task> {
814        self.tools.tasks.list_all()
815    }
816
817    /// Replace all tasks in the typed task manager.
818    pub fn set_tasks(&mut self, tasks: Vec<Task>) {
819        self.tools.tasks.replace_all(tasks);
820    }
821
822    /// Return a full task snapshot.
823    #[must_use]
824    pub fn task_snapshot(&self) -> TaskSnapshot {
825        TaskSnapshot {
826            tasks: self.tasks(),
827        }
828    }
829
830    /// Publish a full task snapshot event.
831    pub fn publish_task_snapshot_event(&mut self) {
832        self.publish_event(AgentEvent::new(
833            TASK_SNAPSHOT_EVENT_KIND,
834            self.task_snapshot().into_payload(),
835        ));
836    }
837
838    /// Enqueue a message.
839    pub fn enqueue_message(&mut self, message: BusMessage) {
840        self.messages.send(message);
841    }
842
843    /// Send a bus message idempotently.
844    pub fn send_message(&mut self, message: BusMessage) -> BusMessage {
845        self.messages.send(message)
846    }
847
848    /// Consume unread bus messages for this context agent.
849    pub fn consume_messages(&mut self) -> Vec<BusMessage> {
850        self.messages.consume(self.agent_id.as_str())
851    }
852
853    /// Consume unread bus messages for a specific agent id.
854    pub fn consume_messages_for(&mut self, agent_id: &str) -> Vec<BusMessage> {
855        self.messages.consume(agent_id)
856    }
857
858    /// Consume unread bus messages matching a predicate for this context agent.
859    pub fn consume_messages_matching(
860        &mut self,
861        predicate: impl Fn(&BusMessage) -> bool,
862    ) -> Vec<BusMessage> {
863        self.messages
864            .consume_matching(self.agent_id.as_str(), predicate)
865    }
866
867    /// Subscribe the current agent to the message bus.
868    pub fn subscribe_messages(&mut self) {
869        self.messages.subscribe(self.agent_id.as_str());
870    }
871
872    /// Return wrapper metadata with built-in context fields and user overrides.
873    #[must_use]
874    pub fn get_wrapper_metadata(&self) -> Metadata {
875        let mut metadata = Metadata::default();
876        if let Some(run_id) = &self.run_id {
877            metadata.insert("run_id".to_string(), serde_json::json!(run_id.as_str()));
878        }
879        if let Some(parent_run_id) = &self.parent_run_id {
880            metadata.insert(
881                "parent_run_id".to_string(),
882                serde_json::json!(parent_run_id.as_str()),
883            );
884        }
885        if let Some(parent_task_id) = &self.parent_task_id {
886            metadata.insert(
887                "parent_task_id".to_string(),
888                serde_json::json!(parent_task_id.as_str()),
889            );
890        }
891        metadata.insert(
892            "agent_id".to_string(),
893            serde_json::json!(self.agent_id.as_str()),
894        );
895        for (key, value) in &self.runtime.wrapper_metadata {
896            metadata.insert(key.clone(), value.clone());
897        }
898        metadata
899    }
900
901    /// Build the typed dependency store supplied to one tool call.
902    ///
903    /// Host capability handles are cloned from the context dependency store, while
904    /// model/tool limits and shell environment values are exposed through a narrow,
905    /// immutable snapshot instead of cloning the complete `AgentContext`.
906    #[must_use]
907    pub fn tool_dependency_store(&self) -> DependencyStore {
908        let mut dependencies = self.dependencies.clone();
909        dependencies.insert(self.host_capabilities());
910        dependencies.insert(self.tool_runtime_snapshot());
911        dependencies
912    }
913
914    /// Build an opt-in filtered dependency store for one tool call.
915    ///
916    /// Direct application dependencies remain available for compatibility. Generated host
917    /// capabilities are filtered, the generated runtime snapshot omits shell values, and the
918    /// runtime-generated broad context handle is assembled by the caller rather than here.
919    #[must_use]
920    pub fn filtered_tool_dependency_store(
921        &self,
922        host_capability_names: &BTreeSet<String>,
923        shell_environment: bool,
924    ) -> DependencyStore {
925        let mut dependencies = self.dependencies.clone();
926        dependencies.insert(self.host_capabilities_subset(host_capability_names));
927        dependencies.insert(self.filtered_tool_runtime_snapshot());
928        if shell_environment {
929            dependencies.insert(self.shell_environment_snapshot());
930        }
931        dependencies
932    }
933
934    /// Build a strict least-authority dependency store for one tool call.
935    ///
936    /// Unlike the compatibility-oriented filtered profile, application dependencies are not
937    /// copied directly. The tool can reach only the named host capability subset and generated
938    /// immutable projections explicitly requested in its metadata.
939    #[must_use]
940    pub fn strict_tool_dependency_store(
941        &self,
942        host_capability_names: &BTreeSet<String>,
943        shell_environment: bool,
944    ) -> DependencyStore {
945        let mut dependencies = DependencyStore::new();
946        dependencies.insert(self.host_capabilities_subset(host_capability_names));
947        dependencies.insert(self.filtered_tool_runtime_snapshot());
948        if shell_environment {
949            dependencies.insert(self.shell_environment_snapshot());
950        }
951        dependencies
952    }
953
954    /// Capture the read-only host capabilities supplied to tool calls.
955    #[must_use]
956    pub fn host_capabilities(&self) -> HostCapabilities {
957        HostCapabilities::new(self.dependencies.clone())
958    }
959
960    /// Capture a filtered read-only host capability subset.
961    #[must_use]
962    pub fn host_capabilities_subset(&self, names: &BTreeSet<String>) -> HostCapabilities {
963        HostCapabilities::new(self.dependencies.subset(names))
964    }
965
966    /// Capture the read-only runtime configuration supplied to tool calls.
967    #[must_use]
968    pub fn tool_runtime_snapshot(&self) -> ToolRuntimeSnapshot {
969        ToolRuntimeSnapshot::new(
970            self.model_config.clone(),
971            self.tool_config.clone(),
972            self.tools.shell_environment.clone(),
973        )
974    }
975
976    /// Capture runtime configuration without configured shell environment values.
977    #[must_use]
978    pub fn filtered_tool_runtime_snapshot(&self) -> ToolRuntimeSnapshot {
979        ToolRuntimeSnapshot::filtered(self.model_config.clone(), self.tool_config.clone())
980    }
981
982    /// Capture configured shell environment values in a dedicated projection.
983    #[must_use]
984    pub fn shell_environment_snapshot(&self) -> ShellEnvironmentSnapshot {
985        ShellEnvironmentSnapshot::new(self.tools.shell_environment.clone())
986    }
987
988    /// Insert a typed dependency.
989    pub fn insert_dependency<T>(&mut self, value: T)
990    where
991        T: Send + Sync + 'static,
992    {
993        self.dependencies.insert(value);
994    }
995
996    /// Insert a named typed dependency.
997    pub fn insert_named_dependency<T>(&mut self, name: impl Into<String>, value: T)
998    where
999        T: Send + Sync + 'static,
1000    {
1001        self.dependencies.insert_named(name, value);
1002    }
1003
1004    /// Authorize dependency grants for one Strict tool name.
1005    pub fn grant_tool_capabilities(
1006        &mut self,
1007        tool_name: impl Into<String>,
1008        grant: ToolCapabilityGrant,
1009    ) {
1010        self.runtime
1011            .tool_capability_grants
1012            .insert(tool_name.into(), grant);
1013    }
1014
1015    /// Return host-authorized dependency grants for one Strict tool name.
1016    #[must_use]
1017    pub fn tool_capability_grant(&self, tool_name: &str) -> ToolCapabilityGrant {
1018        self.runtime
1019            .tool_capability_grants
1020            .get(tool_name)
1021            .cloned()
1022            .unwrap_or_default()
1023    }
1024
1025    /// Get a typed dependency.
1026    #[must_use]
1027    pub fn dependency<T>(&self) -> Option<Arc<T>>
1028    where
1029        T: Send + Sync + 'static,
1030    {
1031        self.dependencies.get::<T>()
1032    }
1033
1034    /// Get a named typed dependency.
1035    #[must_use]
1036    pub fn named_dependency<T>(&self, name: &str) -> Option<Arc<T>>
1037    where
1038        T: Send + Sync + 'static,
1039    {
1040        self.dependencies.get_named::<T>(name)
1041    }
1042
1043    /// Set the context window exposed in model-facing runtime context.
1044    pub const fn set_context_window(&mut self, context_window: Option<u64>) {
1045        self.model_config.context_window = context_window;
1046    }
1047
1048    /// Merge runtime model defaults into this context.
1049    pub fn merge_model_config(&mut self, model_config: ModelConfig) {
1050        self.model_config.merge_from(model_config);
1051    }
1052
1053    /// Replace the tool config for this context.
1054    pub fn set_tool_config(&mut self, mut tool_config: ToolConfig) {
1055        tool_config.normalize();
1056        self.tool_config = tool_config;
1057    }
1058
1059    /// Merge runtime tool defaults into this context.
1060    pub fn merge_tool_config(&mut self, mut tool_config: ToolConfig) {
1061        tool_config.normalize();
1062        let existing_dynamic_patterns = self.tool_config.view_relaxed_text_dynamic_patterns.clone();
1063        for (source, patterns) in existing_dynamic_patterns {
1064            tool_config
1065                .view_relaxed_text_dynamic_patterns
1066                .entry(source)
1067                .or_insert(patterns);
1068        }
1069        self.tool_config = tool_config;
1070    }
1071
1072    /// Render runtime context for model-facing requests.
1073    #[must_use]
1074    pub fn render_runtime_context(&self, is_user_prompt: bool) -> Option<String> {
1075        runtime_context::render_runtime_context(self, is_user_prompt)
1076    }
1077}
1078
1079fn push_unique(values: &mut Vec<String>, value: String) {
1080    if !value.is_empty() && !values.contains(&value) {
1081        values.push(value);
1082    }
1083}
1084
1085fn retain_matching(values: &mut Vec<String>, mut keep: impl FnMut(&str) -> bool) -> Vec<String> {
1086    let mut retained = Vec::with_capacity(values.len());
1087    let mut removed = Vec::new();
1088    for value in std::mem::take(values) {
1089        if keep(&value) {
1090            retained.push(value);
1091        } else {
1092            removed.push(value);
1093        }
1094    }
1095    *values = retained;
1096    removed
1097}
1098
1099impl Default for AgentContext {
1100    fn default() -> Self {
1101        Self::new(AgentId::default())
1102    }
1103}