Skip to main content

everruns_core/
tool_context.rs

1//! Tool context assembly, extension services, and availability validation.
2
3use crate::connection_services::{ProviderCredentialStore, UserConnectionResolver};
4use crate::delegation_services::{
5    SessionCreationAuthority, SubagentNestingPolicy, SubagentSpawnStore,
6};
7use crate::event_emitter::EventEmitter;
8use crate::events::EventRequest;
9use crate::execution_loading::{AgentStore, SessionStore};
10use crate::image_services::ImageArtifactStore;
11use crate::session_files::SessionFileSystem;
12use crate::session_services::{
13    LeasedResourceStore, SessionResourceRegistry, SessionScheduleStore, SessionStorageStore,
14};
15use crate::tool_execution::{BudgetChecker, PaymentAuthority};
16use crate::typed_id::{SessionId, WorkspaceId};
17use std::any::{Any, TypeId};
18use std::collections::{HashMap, HashSet};
19use std::sync::Arc;
20
21/// A live, shared handle to the reasoning effort for the current turn (EVE-595).
22///
23/// Each internal LLM step within a single `run_turn` re-reads this handle when
24/// building its provider request. A tool (or any in-turn actor) can call
25/// [`ReasoningEffortHandle::set`] mid-turn so that *subsequent* LLM steps in the
26/// same turn use the new effort, without waiting for the next turn.
27///
28/// When the handle holds `None`, the engine reason phase falls back to
29/// the effort resolved from the latest user message's `controls` — so callers
30/// that never set an override see no behavior change.
31#[derive(Clone, Default)]
32pub struct ReasoningEffortHandle {
33    inner: Arc<std::sync::RwLock<Option<everruns_provider::model::ReasoningEffort>>>,
34}
35
36impl ReasoningEffortHandle {
37    /// Create an empty handle (no override).
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    /// Create a handle pre-seeded with an effort override.
43    pub fn with_effort(effort: everruns_provider::model::ReasoningEffort) -> Self {
44        Self {
45            inner: Arc::new(std::sync::RwLock::new(Some(effort))),
46        }
47    }
48
49    /// Set (or replace) the override effort. Subsequent LLM steps in the same
50    /// turn pick this up. Pass `None` to clear the override and fall back to the
51    /// message-derived effort.
52    pub fn set(&self, effort: Option<everruns_provider::model::ReasoningEffort>) {
53        // Recover from a poisoned lock so a panic elsewhere never silently
54        // disables mid-turn overrides; the stored value is a plain Option with
55        // no broken invariant to worry about.
56        let mut guard = self.inner.write().unwrap_or_else(|e| e.into_inner());
57        *guard = effort;
58    }
59
60    /// Read the current override effort, if any.
61    pub fn get(&self) -> Option<everruns_provider::model::ReasoningEffort> {
62        // Recover from a poisoned lock rather than silently reverting to the
63        // message-derived effort mid-turn (see `set`).
64        let guard = self.inner.read().unwrap_or_else(|e| e.into_inner());
65        *guard
66    }
67}
68
69impl std::fmt::Debug for ReasoningEffortHandle {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        f.debug_struct("ReasoningEffortHandle")
72            .field("effort", &self.get())
73            .finish()
74    }
75}
76
77/// Runtime-owned services that may be exposed to tools.
78///
79/// Tools declare the subset they require through
80/// [`crate::tools::Tool::required_context_services`]. Runtime hosts validate
81/// those declarations before advertising tools to the model.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
83pub enum ToolContextService {
84    SessionFileSystem,
85    SessionStorageStore,
86    ImageArtifactStore,
87    ProviderCredentialStore,
88    UtilityLlmService,
89    McpInvoker,
90    EgressService,
91    MessageRetriever,
92    SessionStore,
93    AgentStore,
94    ConnectionResolver,
95    ScheduleStore,
96    SubagentSessionDelegate,
97    LeasedResourceStore,
98    SessionResourceRegistry,
99    SessionTaskRegistry,
100    EventEmitter,
101    CapabilityRegistry,
102    ToolRegistry,
103    OrgId,
104    BudgetChecker,
105    PaymentAuthority,
106    SessionCreationAuthority,
107    SubagentSpawnStore,
108    ReasoningEffortHandle,
109}
110
111impl ToolContextService {
112    pub const fn name(self) -> &'static str {
113        match self {
114            Self::SessionFileSystem => "SessionFileSystem",
115            Self::SessionStorageStore => "SessionStorageStore",
116            Self::ImageArtifactStore => "ImageArtifactStore",
117            Self::ProviderCredentialStore => "ProviderCredentialStore",
118            Self::UtilityLlmService => "UtilityLlmService",
119            Self::McpInvoker => "McpInvoker",
120            Self::EgressService => "EgressService",
121            Self::MessageRetriever => "MessageRetriever",
122            Self::SessionStore => "SessionStore",
123            Self::AgentStore => "AgentStore",
124            Self::ConnectionResolver => "ConnectionResolver",
125            Self::ScheduleStore => "SessionScheduleStore",
126            Self::SubagentSessionDelegate => "SubagentSessionDelegate",
127            Self::LeasedResourceStore => "LeasedResourceStore",
128            Self::SessionResourceRegistry => "SessionResourceRegistry",
129            Self::SessionTaskRegistry => "SessionTaskRegistry",
130            Self::EventEmitter => "EventEmitter",
131            Self::CapabilityRegistry => "CapabilityRegistry",
132            Self::ToolRegistry => "ToolRegistry",
133            Self::OrgId => "OrgId",
134            Self::BudgetChecker => "BudgetChecker",
135            Self::PaymentAuthority => "PaymentAuthority",
136            Self::SessionCreationAuthority => "SessionCreationAuthority",
137            Self::SubagentSpawnStore => "SubagentSpawnStore",
138            Self::ReasoningEffortHandle => "ReasoningEffortHandle",
139        }
140    }
141}
142
143/// Cloneable snapshot of all services a runtime host makes available to tools.
144///
145/// Production act execution constructs each [`ToolContext`] from this bundle;
146/// per-call metadata is applied afterwards.
147#[derive(Clone, Default)]
148pub struct ToolContextServices {
149    pub file_store: Option<Arc<dyn SessionFileSystem>>,
150    pub storage_store: Option<Arc<dyn SessionStorageStore>>,
151    pub image_store: Option<Arc<dyn ImageArtifactStore>>,
152    pub provider_credential_store: Option<Arc<dyn ProviderCredentialStore>>,
153    pub utility_llm_service: Option<Arc<dyn crate::UtilityLlmService>>,
154    pub mcp_invoker: Option<Arc<dyn crate::McpToolInvoker>>,
155    pub egress_service: Option<Arc<dyn crate::EgressService>>,
156    pub message_retriever: Option<Arc<dyn crate::message_retriever::MessageRetriever>>,
157    pub session_store: Option<Arc<dyn SessionStore>>,
158    pub agent_store: Option<Arc<dyn AgentStore>>,
159    pub connection_resolver: Option<Arc<dyn UserConnectionResolver>>,
160    pub schedule_store: Option<Arc<dyn SessionScheduleStore>>,
161    pub subagent_delegate: Option<Arc<dyn crate::subagent_delegation::SubagentSessionDelegate>>,
162    pub extensions: ToolContextExtensions,
163    pub leased_resource_store: Option<Arc<dyn LeasedResourceStore>>,
164    pub session_resource_registry: Option<Arc<dyn SessionResourceRegistry>>,
165    pub session_task_registry: Option<Arc<dyn crate::session_task::SessionTaskRegistry>>,
166    pub event_emitter: Option<Arc<dyn EventEmitter>>,
167    pub capability_registry: Option<crate::capabilities::CapabilityRegistry>,
168    pub tool_registry: Option<Arc<crate::tools::ToolRegistry>>,
169    pub org_id: Option<crate::typed_id::OrgId>,
170    pub network_access: Option<crate::network_access::NetworkAccessList>,
171    pub budget_checker: Option<Arc<dyn BudgetChecker>>,
172    pub payment_authority: Option<Arc<dyn PaymentAuthority>>,
173    pub session_creation_authority: Option<Arc<dyn SessionCreationAuthority>>,
174    pub subagent_spawn_store: Option<Arc<dyn SubagentSpawnStore>>,
175    pub subagent_nesting_policy: SubagentNestingPolicy,
176    pub reasoning_effort_handle: Option<ReasoningEffortHandle>,
177}
178
179impl ToolContextServices {
180    pub fn provides(&self, service: ToolContextService) -> bool {
181        match service {
182            ToolContextService::SessionFileSystem => self.file_store.is_some(),
183            ToolContextService::SessionStorageStore => self.storage_store.is_some(),
184            ToolContextService::ImageArtifactStore => self.image_store.is_some(),
185            ToolContextService::ProviderCredentialStore => self.provider_credential_store.is_some(),
186            ToolContextService::UtilityLlmService => self.utility_llm_service.is_some(),
187            ToolContextService::McpInvoker => self.mcp_invoker.is_some(),
188            ToolContextService::EgressService => self.egress_service.is_some(),
189            ToolContextService::MessageRetriever => self.message_retriever.is_some(),
190            ToolContextService::SessionStore => self.session_store.is_some(),
191            ToolContextService::AgentStore => self.agent_store.is_some(),
192            ToolContextService::ConnectionResolver => self.connection_resolver.is_some(),
193            ToolContextService::ScheduleStore => self.schedule_store.is_some(),
194            ToolContextService::SubagentSessionDelegate => self.subagent_delegate.is_some(),
195            ToolContextService::LeasedResourceStore => self.leased_resource_store.is_some(),
196            ToolContextService::SessionResourceRegistry => self.session_resource_registry.is_some(),
197            ToolContextService::SessionTaskRegistry => self.session_task_registry.is_some(),
198            ToolContextService::EventEmitter => self.event_emitter.is_some(),
199            ToolContextService::CapabilityRegistry => self.capability_registry.is_some(),
200            ToolContextService::ToolRegistry => self.tool_registry.is_some(),
201            ToolContextService::OrgId => self.org_id.is_some(),
202            ToolContextService::BudgetChecker => self.budget_checker.is_some(),
203            ToolContextService::PaymentAuthority => self.payment_authority.is_some(),
204            ToolContextService::SessionCreationAuthority => {
205                self.session_creation_authority.is_some()
206            }
207            ToolContextService::SubagentSpawnStore => self.subagent_spawn_store.is_some(),
208            ToolContextService::ReasoningEffortHandle => self.reasoning_effort_handle.is_some(),
209        }
210    }
211}
212
213/// Type-erased bag of host-supplied extension services carried on a
214/// [`ToolContext`]. Crates layered above core (e.g. `everruns-platform`) insert
215/// a concrete wrapper type and resolve it by type, so core need not name the
216/// hosted service (EVE-839).
217#[derive(Clone, Default)]
218pub struct ToolContextExtensions {
219    values: Arc<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>,
220}
221
222impl ToolContextExtensions {
223    /// Insert (or replace) an extension keyed by its concrete type.
224    pub fn insert<T: Any + Send + Sync>(&mut self, value: Arc<T>) {
225        Arc::make_mut(&mut self.values).insert(TypeId::of::<T>(), value);
226    }
227
228    /// Resolve an extension by its concrete type.
229    pub fn get<T: Any + Send + Sync>(&self) -> Option<Arc<T>> {
230        self.values
231            .get(&TypeId::of::<T>())
232            .and_then(|value| value.clone().downcast::<T>().ok())
233    }
234
235    pub fn is_empty(&self) -> bool {
236        self.values.is_empty()
237    }
238}
239
240impl std::fmt::Debug for ToolContextExtensions {
241    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242        f.debug_struct("ToolContextExtensions")
243            .field("len", &self.values.len())
244            .finish()
245    }
246}
247
248/// Runtime context provided to tools during execution.
249///
250/// This context contains:
251/// - Session ID for scoping operations
252/// - Optional stores for tools that need external access
253///
254/// Tools that need context-aware execution (like filesystem tools) can use
255/// the `execute_with_context` method on the Tool trait.
256#[derive(Clone)]
257pub struct ToolContext {
258    /// The session ID for the current execution
259    pub session_id: SessionId,
260    /// The workspace this session is attached to — the key for the virtual
261    /// file store. For the default 1:1 session this equals
262    /// `WorkspaceId::from_uuid(session_id.uuid())`; for a shared workspace it
263    /// differs. File-system tools MUST key by this (via `workspace_fs_key`)
264    /// rather than `session_id` so shared-workspace sessions read/write the
265    /// attached workspace's files. See knowledge/runtime-resources/workspace.md.
266    pub workspace_id: WorkspaceId,
267
268    /// Optional file store for filesystem operations
269    pub file_store: Option<Arc<dyn SessionFileSystem>>,
270
271    /// Optional storage store for key/value and secret storage
272    pub storage_store: Option<Arc<dyn SessionStorageStore>>,
273
274    /// Optional durable image artifact store for tool-side media persistence.
275    pub image_store: Option<Arc<dyn ImageArtifactStore>>,
276
277    /// Optional provider credential store for tool-side API clients.
278    pub provider_credential_store: Option<Arc<dyn ProviderCredentialStore>>,
279
280    /// Optional system utility LLM service for capability internals.
281    pub utility_llm_service: Option<Arc<dyn crate::UtilityLlmService>>,
282
283    /// Optional scoped-MCP tool invoker for capability internals that need to
284    /// call an MCP server out-of-band (e.g. the guardrails `mcp` check
285    /// delegating a decision to an external guardrail endpoint). The invoker
286    /// resolves connections and credentials per the current session/org, so
287    /// tenant scoping is enforced by the host that supplies it.
288    pub mcp_invoker: Option<Arc<dyn crate::McpToolInvoker>>,
289
290    /// Optional outbound egress service for HTTP/API traffic.
291    pub egress_service: Option<Arc<dyn crate::EgressService>>,
292
293    /// Optional message retriever for tools that need conversation history access
294    pub message_retriever: Option<Arc<dyn crate::message_retriever::MessageRetriever>>,
295
296    /// Optional session store for tools that need session metadata access.
297    pub session_store: Option<Arc<dyn SessionStore>>,
298
299    /// Optional agent store for tools that need agent metadata access.
300    pub agent_store: Option<Arc<dyn AgentStore>>,
301
302    /// Optional resolver for user connection tokens (lazy GitHub token lookup, etc.)
303    pub connection_resolver: Option<Arc<dyn UserConnectionResolver>>,
304
305    /// Optional session schedule store for scheduling tools.
306    pub schedule_store: Option<Arc<dyn SessionScheduleStore>>,
307
308    /// Optional narrow child-session delegate for host-provided orchestration.
309    /// The hosted `PlatformStore` seam itself is not named by core; a host
310    /// adapter implements this contract and platform tools use typed extensions.
311    pub subagent_delegate: Option<Arc<dyn crate::subagent_delegation::SubagentSessionDelegate>>,
312    /// Type-erased, host-supplied extensions keyed by concrete type. Lets crates
313    /// layered above core (e.g. `everruns-platform`) hang typed services on the
314    /// tool context without core naming them (EVE-839).
315    pub extensions: ToolContextExtensions,
316    /// Optional leased resource store for lifecycle-managed provider resources.
317    pub leased_resource_store: Option<Arc<dyn LeasedResourceStore>>,
318
319    /// Optional session resource registry — generic registry of active resources.
320    pub session_resource_registry: Option<Arc<dyn SessionResourceRegistry>>,
321
322    /// Optional session task registry — background work owned by the session
323    /// (knowledge/runtime-resources/session-tasks.md).
324    pub session_task_registry: Option<Arc<dyn crate::session_task::SessionTaskRegistry>>,
325
326    /// Optional event emitter for tools that need to stream progress updates.
327    /// When set, tools can emit `tool.progress` events during execution.
328    pub event_emitter: Option<Arc<dyn EventEmitter>>,
329
330    /// Event context for correlating progress events with the current tool call.
331    /// Set by ActAtom when constructing the ToolContext.
332    pub event_context: Option<crate::events::EventContext>,
333
334    /// The tool call ID for the current execution (set by ActAtom).
335    /// Used by tools to emit correlated progress events.
336    pub tool_call_id: Option<String>,
337    /// Optional capability registry for blueprint lookups.
338    pub capability_registry: Option<crate::capabilities::CapabilityRegistry>,
339
340    /// Optional registry of active built-in tools for meta-tools such as
341    /// `spawn_background` that need to inspect or delegate to sibling tools.
342    pub tool_registry: Option<Arc<crate::tools::ToolRegistry>>,
343
344    /// Optional allowlist of tools visible to the model for this turn.
345    /// Registry-introspecting tools must filter through this before returning
346    /// sibling tool metadata, because the execution registry can be a superset.
347    pub visible_tool_names: Option<Arc<HashSet<String>>>,
348
349    /// Optional org ID for org-scoped operations.
350    pub org_id: Option<crate::typed_id::OrgId>,
351
352    /// Merged network access list (harness ∩ agent ∩ session).
353    /// When set, tools that make HTTP requests must check URLs against this list.
354    pub network_access: Option<crate::network_access::NetworkAccessList>,
355
356    /// Resolved locale for localized tool behavior (BCP 47, e.g. `uk-UA`).
357    /// When set, tools that support localization use this to produce
358    /// locale-appropriate descriptions, error messages, and prompts.
359    pub locale: Option<String>,
360
361    /// Optional budget checker for the check_budget tool.
362    pub budget_checker: Option<Arc<dyn BudgetChecker>>,
363
364    /// Optional internal payment authority for paid capability tools.
365    pub payment_authority: Option<Arc<dyn PaymentAuthority>>,
366
367    /// Optional host authority for detached peer-session creation.
368    pub session_creation_authority: Option<Arc<dyn SessionCreationAuthority>>,
369
370    /// Optional durable spawn handle store for subagent reattach (EVE-535).
371    /// When set, subagent delegation uses claim/settle to prevent duplicate spawning
372    /// on parent worker reclaim.
373    pub subagent_spawn_store: Option<Arc<dyn SubagentSpawnStore>>,
374
375    /// Resolved subagent nesting policy for this turn.
376    pub subagent_nesting_policy: SubagentNestingPolicy,
377
378    /// Optional live reasoning-effort handle (EVE-595). When set, a tool can
379    /// change the reasoning effort mid-turn; subsequent LLM steps in the same
380    /// `run_turn` re-read it and use the new effort.
381    pub reasoning_effort_handle: Option<ReasoningEffortHandle>,
382
383    /// Cooperative cancellation for this tool call.
384    ///
385    /// The act atom cancels it when the call ends *by any means* — the turn was
386    /// cancelled, the act future was dropped, or the tool simply returned. A
387    /// tool that only awaits inside its own future needs nothing here: dropping
388    /// the future already stops it. This exists for work a tool starts and
389    /// leaves running — a child process, a detached watcher task, a prompt
390    /// waiting on an answer — which would otherwise outlive the call that
391    /// created it. Clone the token into that work and let it die with the call.
392    pub cancellation: Option<tokio_util::sync::CancellationToken>,
393}
394
395impl ToolContext {
396    /// The virtual-file-store key for this execution, derived from the attached
397    /// workspace. Carried through the `SessionFileSystem` trait's `SessionId`
398    /// parameter (the store keys by `.uuid()`), so a shared-workspace session
399    /// addresses the workspace's files rather than its own session-id keyspace.
400    pub fn workspace_fs_key(&self) -> SessionId {
401        SessionId::from_uuid(self.workspace_id.uuid())
402    }
403
404    /// Override the attached workspace (default is the 1:1 session-derived id).
405    pub fn with_workspace_id(mut self, workspace_id: WorkspaceId) -> Self {
406        self.workspace_id = workspace_id;
407        self
408    }
409
410    /// Create a new tool context with just a session ID
411    pub fn new(session_id: SessionId) -> Self {
412        Self {
413            session_id,
414            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
415            file_store: None,
416            storage_store: None,
417            image_store: None,
418            provider_credential_store: None,
419            utility_llm_service: None,
420            mcp_invoker: None,
421            egress_service: None,
422            message_retriever: None,
423            session_store: None,
424            agent_store: None,
425            connection_resolver: None,
426            schedule_store: None,
427            subagent_delegate: None,
428            extensions: ToolContextExtensions::default(),
429            leased_resource_store: None,
430            session_resource_registry: None,
431            session_task_registry: None,
432            event_emitter: None,
433            event_context: None,
434            tool_call_id: None,
435            capability_registry: None,
436            tool_registry: None,
437            visible_tool_names: None,
438            org_id: None,
439            network_access: None,
440            locale: None,
441            budget_checker: None,
442            payment_authority: None,
443            session_creation_authority: None,
444            subagent_spawn_store: None,
445            subagent_nesting_policy: SubagentNestingPolicy::default(),
446            reasoning_effort_handle: None,
447            cancellation: None,
448        }
449    }
450
451    /// Construct a production tool context from a validated runtime service snapshot.
452    pub fn from_services(session_id: SessionId, services: &ToolContextServices) -> Self {
453        Self {
454            session_id,
455            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
456            file_store: services.file_store.clone(),
457            storage_store: services.storage_store.clone(),
458            image_store: services.image_store.clone(),
459            provider_credential_store: services.provider_credential_store.clone(),
460            utility_llm_service: services.utility_llm_service.clone(),
461            mcp_invoker: services.mcp_invoker.clone(),
462            egress_service: services.egress_service.clone(),
463            message_retriever: services.message_retriever.clone(),
464            session_store: services.session_store.clone(),
465            agent_store: services.agent_store.clone(),
466            connection_resolver: services.connection_resolver.clone(),
467            schedule_store: services.schedule_store.clone(),
468            subagent_delegate: services.subagent_delegate.clone(),
469            extensions: services.extensions.clone(),
470            leased_resource_store: services.leased_resource_store.clone(),
471            session_resource_registry: services.session_resource_registry.clone(),
472            session_task_registry: services.session_task_registry.clone(),
473            event_emitter: services.event_emitter.clone(),
474            event_context: None,
475            tool_call_id: None,
476            capability_registry: services.capability_registry.clone(),
477            tool_registry: services.tool_registry.clone(),
478            visible_tool_names: None,
479            org_id: services.org_id,
480            network_access: services.network_access.clone(),
481            locale: None,
482            budget_checker: services.budget_checker.clone(),
483            payment_authority: services.payment_authority.clone(),
484            session_creation_authority: services.session_creation_authority.clone(),
485            subagent_spawn_store: services.subagent_spawn_store.clone(),
486            subagent_nesting_policy: services.subagent_nesting_policy,
487            reasoning_effort_handle: services.reasoning_effort_handle.clone(),
488            cancellation: None,
489        }
490    }
491
492    /// Create a context with a file store
493    pub fn with_file_store(session_id: SessionId, file_store: Arc<dyn SessionFileSystem>) -> Self {
494        Self {
495            session_id,
496            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
497            file_store: Some(file_store),
498            storage_store: None,
499            image_store: None,
500            provider_credential_store: None,
501            utility_llm_service: None,
502            mcp_invoker: None,
503            egress_service: None,
504            message_retriever: None,
505            session_store: None,
506            agent_store: None,
507            connection_resolver: None,
508            schedule_store: None,
509            subagent_delegate: None,
510            extensions: ToolContextExtensions::default(),
511            leased_resource_store: None,
512            session_resource_registry: None,
513            session_task_registry: None,
514            event_emitter: None,
515            event_context: None,
516            tool_call_id: None,
517            capability_registry: None,
518            tool_registry: None,
519            visible_tool_names: None,
520            org_id: None,
521            network_access: None,
522            locale: None,
523            budget_checker: None,
524            payment_authority: None,
525            session_creation_authority: None,
526            subagent_spawn_store: None,
527            subagent_nesting_policy: SubagentNestingPolicy::default(),
528            reasoning_effort_handle: None,
529            cancellation: None,
530        }
531    }
532
533    /// Create a context with a storage store
534    pub fn with_storage_store(
535        session_id: SessionId,
536        storage_store: Arc<dyn SessionStorageStore>,
537    ) -> Self {
538        Self {
539            session_id,
540            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
541            file_store: None,
542            storage_store: Some(storage_store),
543            image_store: None,
544            provider_credential_store: None,
545            utility_llm_service: None,
546            mcp_invoker: None,
547            egress_service: None,
548            message_retriever: None,
549            session_store: None,
550            agent_store: None,
551            connection_resolver: None,
552            schedule_store: None,
553            subagent_delegate: None,
554            extensions: ToolContextExtensions::default(),
555            leased_resource_store: None,
556            session_resource_registry: None,
557            session_task_registry: None,
558            event_emitter: None,
559            event_context: None,
560            tool_call_id: None,
561            capability_registry: None,
562            tool_registry: None,
563            visible_tool_names: None,
564            org_id: None,
565            network_access: None,
566            locale: None,
567            budget_checker: None,
568            payment_authority: None,
569            session_creation_authority: None,
570            subagent_spawn_store: None,
571            subagent_nesting_policy: SubagentNestingPolicy::default(),
572            reasoning_effort_handle: None,
573            cancellation: None,
574        }
575    }
576
577    /// Create a context with both file store and storage store
578    pub fn with_stores(
579        session_id: SessionId,
580        file_store: Arc<dyn SessionFileSystem>,
581        storage_store: Arc<dyn SessionStorageStore>,
582    ) -> Self {
583        Self {
584            session_id,
585            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
586            file_store: Some(file_store),
587            storage_store: Some(storage_store),
588            image_store: None,
589            provider_credential_store: None,
590            utility_llm_service: None,
591            mcp_invoker: None,
592            egress_service: None,
593            message_retriever: None,
594            session_store: None,
595            agent_store: None,
596            connection_resolver: None,
597            schedule_store: None,
598            subagent_delegate: None,
599            extensions: ToolContextExtensions::default(),
600            leased_resource_store: None,
601            session_resource_registry: None,
602            session_task_registry: None,
603            event_emitter: None,
604            event_context: None,
605            tool_call_id: None,
606            capability_registry: None,
607            tool_registry: None,
608            visible_tool_names: None,
609            org_id: None,
610            network_access: None,
611            locale: None,
612            budget_checker: None,
613            payment_authority: None,
614            session_creation_authority: None,
615            subagent_spawn_store: None,
616            subagent_nesting_policy: SubagentNestingPolicy::default(),
617            reasoning_effort_handle: None,
618            cancellation: None,
619        }
620    }
621
622    /// Add a message retriever to this context
623    pub fn with_message_retriever(
624        mut self,
625        retriever: Arc<dyn crate::message_retriever::MessageRetriever>,
626    ) -> Self {
627        self.message_retriever = Some(retriever);
628        self
629    }
630
631    /// Add a session store to this context.
632    pub fn with_session_store(mut self, store: Arc<dyn SessionStore>) -> Self {
633        self.session_store = Some(store);
634        self
635    }
636
637    /// Add a live reasoning-effort handle (EVE-595). Tools can call
638    /// [`ReasoningEffortHandle::set`] on it to change the effort used by
639    /// subsequent LLM steps within the same turn.
640    /// Attach a cancellation token to this context (see
641    /// [`ToolContext::cancellation`]).
642    pub fn with_cancellation(mut self, token: tokio_util::sync::CancellationToken) -> Self {
643        self.cancellation = Some(token);
644        self
645    }
646
647    /// Whether this call has already been cancelled. A context with no token
648    /// never reports cancellation.
649    pub fn is_cancelled(&self) -> bool {
650        self.cancellation
651            .as_ref()
652            .is_some_and(|token| token.is_cancelled())
653    }
654
655    pub fn with_reasoning_effort_handle(mut self, handle: ReasoningEffortHandle) -> Self {
656        self.reasoning_effort_handle = Some(handle);
657        self
658    }
659
660    /// Add an agent store to this context.
661    pub fn with_agent_store(mut self, store: Arc<dyn AgentStore>) -> Self {
662        self.agent_store = Some(store);
663        self
664    }
665
666    /// Add a connection resolver to this context
667    pub fn with_connection_resolver(mut self, resolver: Arc<dyn UserConnectionResolver>) -> Self {
668        self.connection_resolver = Some(resolver);
669        self
670    }
671
672    /// Create a context with an image artifact store.
673    pub fn with_image_store(
674        session_id: SessionId,
675        image_store: Arc<dyn ImageArtifactStore>,
676    ) -> Self {
677        Self {
678            session_id,
679            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
680            file_store: None,
681            storage_store: None,
682            image_store: Some(image_store),
683            provider_credential_store: None,
684            utility_llm_service: None,
685            mcp_invoker: None,
686            egress_service: None,
687            message_retriever: None,
688            session_store: None,
689            agent_store: None,
690            connection_resolver: None,
691            schedule_store: None,
692            subagent_delegate: None,
693            extensions: ToolContextExtensions::default(),
694            leased_resource_store: None,
695            session_resource_registry: None,
696            session_task_registry: None,
697            event_emitter: None,
698            event_context: None,
699            tool_call_id: None,
700            capability_registry: None,
701            tool_registry: None,
702            visible_tool_names: None,
703            org_id: None,
704            network_access: None,
705            locale: None,
706            budget_checker: None,
707            payment_authority: None,
708            session_creation_authority: None,
709            subagent_spawn_store: None,
710            subagent_nesting_policy: SubagentNestingPolicy::default(),
711            reasoning_effort_handle: None,
712            cancellation: None,
713        }
714    }
715
716    /// Set the provider credential store on this context.
717    pub fn with_provider_credential_store(
718        mut self,
719        store: Arc<dyn ProviderCredentialStore>,
720    ) -> Self {
721        self.provider_credential_store = Some(store);
722        self
723    }
724
725    /// Set the utility LLM service on this context.
726    pub fn with_utility_llm_service(mut self, service: Arc<dyn crate::UtilityLlmService>) -> Self {
727        self.utility_llm_service = Some(service);
728        self
729    }
730
731    /// Set the scoped-MCP tool invoker on this context.
732    pub fn with_mcp_invoker(mut self, invoker: Arc<dyn crate::McpToolInvoker>) -> Self {
733        self.mcp_invoker = Some(invoker);
734        self
735    }
736
737    /// Set the outbound egress service on this context.
738    pub fn with_egress_service(mut self, service: Arc<dyn crate::EgressService>) -> Self {
739        self.egress_service = Some(service);
740        self
741    }
742
743    /// Set the outbound egress service on this context when available.
744    /// Preserves any already-set service when `service` is `None`.
745    pub fn with_egress_service_opt(
746        mut self,
747        service: Option<Arc<dyn crate::EgressService>>,
748    ) -> Self {
749        if let Some(service) = service {
750            self.egress_service = Some(service);
751        }
752        self
753    }
754
755    /// Set the session storage store on this context (builder method).
756    pub fn with_storage_store_arc(mut self, store: Arc<dyn SessionStorageStore>) -> Self {
757        self.storage_store = Some(store);
758        self
759    }
760
761    /// Add a session schedule store to this context.
762    pub fn with_schedule_store(mut self, store: Arc<dyn SessionScheduleStore>) -> Self {
763        self.schedule_store = Some(store);
764        self
765    }
766
767    /// Add a narrow child-session delegate to this context (EVE-839).
768    pub fn with_subagent_delegate(
769        mut self,
770        delegate: Arc<dyn crate::subagent_delegation::SubagentSessionDelegate>,
771    ) -> Self {
772        self.subagent_delegate = Some(delegate);
773        self
774    }
775
776    /// Insert a type-keyed host extension (e.g. `everruns-platform`'s
777    /// `PlatformStoreExt`) and return the updated context.
778    pub fn with_extension<T: std::any::Any + Send + Sync>(mut self, value: Arc<T>) -> Self {
779        self.extensions.insert(value);
780        self
781    }
782
783    /// Resolve a type-keyed host extension previously inserted via
784    /// [`Self::with_extension`].
785    pub fn extension<T: std::any::Any + Send + Sync>(&self) -> Option<Arc<T>> {
786        self.extensions.get::<T>()
787    }
788
789    /// Add a leased resource store to this context.
790    pub fn with_leased_resource_store(mut self, store: Arc<dyn LeasedResourceStore>) -> Self {
791        self.leased_resource_store = Some(store);
792        self
793    }
794
795    /// Add a session resource registry to this context.
796    pub fn with_session_resource_registry(
797        mut self,
798        registry: Arc<dyn SessionResourceRegistry>,
799    ) -> Self {
800        self.session_resource_registry = Some(registry);
801        self
802    }
803
804    /// Add a session task registry to this context.
805    pub fn with_session_task_registry(
806        mut self,
807        registry: Arc<dyn crate::session_task::SessionTaskRegistry>,
808    ) -> Self {
809        self.session_task_registry = Some(registry);
810        self
811    }
812
813    /// Set org ID for org-scoped operations.
814    pub fn with_org_id(mut self, org_id: crate::typed_id::OrgId) -> Self {
815        self.org_id = Some(org_id);
816        self
817    }
818
819    /// Set the active built-in tool registry on this context.
820    pub fn with_tool_registry(mut self, registry: Arc<crate::tools::ToolRegistry>) -> Self {
821        self.tool_registry = Some(registry);
822        self
823    }
824
825    /// Set the tool names visible to the model in this turn.
826    pub fn with_visible_tool_names(mut self, names: Arc<HashSet<String>>) -> Self {
827        self.visible_tool_names = Some(names);
828        self
829    }
830
831    /// Set the merged network access list for URL filtering.
832    pub fn with_network_access(
833        mut self,
834        network_access: Option<crate::network_access::NetworkAccessList>,
835    ) -> Self {
836        self.network_access = network_access;
837        self
838    }
839
840    /// Set the internal payment authority for paid capability operations.
841    pub fn with_payment_authority(mut self, authority: Arc<dyn PaymentAuthority>) -> Self {
842        self.payment_authority = Some(authority);
843        self
844    }
845
846    /// Set the durable subagent spawn handle store (EVE-535).
847    pub fn with_subagent_spawn_store(mut self, store: Arc<dyn SubagentSpawnStore>) -> Self {
848        self.subagent_spawn_store = Some(store);
849        self
850    }
851
852    /// Set the resolved subagent nesting policy.
853    pub fn with_subagent_nesting_policy(mut self, policy: SubagentNestingPolicy) -> Self {
854        self.subagent_nesting_policy = policy;
855        self
856    }
857
858    /// Emit a `tool.progress` event if an event emitter and context are available.
859    ///
860    /// This is a best-effort helper: failures are logged but not propagated,
861    /// so tools never fail just because a progress event couldn't be sent.
862    pub async fn emit_progress(&self, tool_name: &str, message: &str) {
863        let (Some(emitter), Some(ctx), Some(call_id)) =
864            (&self.event_emitter, &self.event_context, &self.tool_call_id)
865        else {
866            return;
867        };
868        if let Err(e) = emitter
869            .emit(EventRequest::new(
870                self.session_id,
871                ctx.clone(),
872                crate::events::ToolProgressData {
873                    tool_call_id: call_id.clone(),
874                    tool_name: tool_name.to_string(),
875                    message: message.to_string(),
876                    display_name: None,
877                },
878            ))
879            .await
880        {
881            tracing::debug!(
882                tool_call_id = call_id,
883                tool_name,
884                error = %e,
885                "Failed to emit tool.progress event"
886            );
887        }
888    }
889
890    /// Emit a `tool.output.delta` event if an event emitter and context are available.
891    ///
892    /// Streams incremental output chunks (e.g., stdout/stderr lines) for live
893    /// rendering in UI and CLI. Best-effort: failures are logged, not propagated.
894    pub async fn emit_tool_output(&self, tool_name: &str, delta: &str, stream: &str) {
895        let (Some(emitter), Some(ctx), Some(call_id)) =
896            (&self.event_emitter, &self.event_context, &self.tool_call_id)
897        else {
898            return;
899        };
900        if let Err(e) = emitter
901            .emit(EventRequest::new(
902                self.session_id,
903                ctx.clone(),
904                crate::events::ToolOutputDeltaData {
905                    tool_call_id: call_id.clone(),
906                    tool_name: tool_name.to_string(),
907                    delta: delta.to_string(),
908                    stream: stream.to_string(),
909                },
910            ))
911            .await
912        {
913            tracing::debug!(
914                tool_call_id = call_id,
915                tool_name,
916                error = %e,
917                "Failed to emit tool.output.delta event"
918            );
919        }
920    }
921}
922
923impl std::fmt::Debug for ToolContext {
924    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
925        f.debug_struct("ToolContext")
926            .field("session_id", &self.session_id)
927            .field("file_store", &self.file_store.is_some())
928            .field("storage_store", &self.storage_store.is_some())
929            .field("image_store", &self.image_store.is_some())
930            .field(
931                "provider_credential_store",
932                &self.provider_credential_store.is_some(),
933            )
934            .field("utility_llm_service", &self.utility_llm_service.is_some())
935            .field("egress_service", &self.egress_service.is_some())
936            .field("message_retriever", &self.message_retriever.is_some())
937            .field("session_store", &self.session_store.is_some())
938            .field("agent_store", &self.agent_store.is_some())
939            .field("connection_resolver", &self.connection_resolver.is_some())
940            .field("schedule_store", &self.schedule_store.is_some())
941            .field("subagent_delegate", &self.subagent_delegate.is_some())
942            .field(
943                "leased_resource_store",
944                &self.leased_resource_store.is_some(),
945            )
946            .field("event_emitter", &self.event_emitter.is_some())
947            .field("tool_registry", &self.tool_registry.is_some())
948            .field("payment_authority", &self.payment_authority.is_some())
949            .field("subagent_spawn_store", &self.subagent_spawn_store.is_some())
950            .field("subagent_nesting_policy", &self.subagent_nesting_policy)
951            .field("org_id", &self.org_id)
952            .finish()
953    }
954}