Skip to main content

everruns_core/
traits.rs

1// Core traits for pluggable backends
2//
3// These traits allow the agent loop to be used with different backends:
4// - In-memory implementations for examples and testing
5// - Database implementations for production
6// - Channel-based implementations for streaming
7
8use crate::agent::Agent;
9use crate::harness::Harness;
10use crate::provider::DriverId;
11use crate::session_file::{
12    FileInfo, FileStat, GrepMatch, GrepOptions, GrepSearchResult, InitialFile, SessionFile,
13};
14use crate::tool_types::{ToolCall, ToolDefinition, ToolResult};
15use crate::typed_id::{AgentId, HarnessId, ImageId, MessageId, ModelId, SessionId, WorkspaceId};
16use async_trait::async_trait;
17use chrono::{DateTime, Utc};
18use std::any::{Any, TypeId};
19use std::collections::{HashMap, HashSet};
20use std::sync::Arc;
21use uuid::Uuid;
22
23/// Build a map of tool names to definitions for efficient lookup
24fn build_tool_map(tool_defs: &[ToolDefinition]) -> HashMap<&str, &ToolDefinition> {
25    tool_defs.iter().map(|def| (def.name(), def)).collect()
26}
27
28use crate::error::Result;
29
30// ============================================================================
31// ReasoningEffortHandle - live, turn-scoped reasoning-effort override
32// ============================================================================
33
34/// A live, shared handle to the reasoning effort for the current turn (EVE-595).
35///
36/// Each internal LLM step within a single `run_turn` re-reads this handle when
37/// building its provider request. A tool (or any in-turn actor) can call
38/// [`ReasoningEffortHandle::set`] mid-turn so that *subsequent* LLM steps in the
39/// same turn use the new effort, without waiting for the next turn.
40///
41/// When the handle holds `None`, the [`crate::atoms::ReasonAtom`] falls back to
42/// the effort resolved from the latest user message's `controls` — so callers
43/// that never set an override see no behavior change.
44#[derive(Clone, Default)]
45pub struct ReasoningEffortHandle {
46    inner: Arc<std::sync::RwLock<Option<String>>>,
47}
48
49impl ReasoningEffortHandle {
50    /// Create an empty handle (no override).
51    pub fn new() -> Self {
52        Self::default()
53    }
54
55    /// Create a handle pre-seeded with an effort override.
56    pub fn with_effort(effort: impl Into<String>) -> Self {
57        Self {
58            inner: Arc::new(std::sync::RwLock::new(Some(effort.into()))),
59        }
60    }
61
62    /// Set (or replace) the override effort. Subsequent LLM steps in the same
63    /// turn pick this up. Pass `None` to clear the override and fall back to the
64    /// message-derived effort.
65    pub fn set(&self, effort: Option<String>) {
66        // Recover from a poisoned lock so a panic elsewhere never silently
67        // disables mid-turn overrides; the stored value is a plain Option<String>
68        // with no broken invariant to worry about.
69        let mut guard = self.inner.write().unwrap_or_else(|e| e.into_inner());
70        *guard = effort;
71    }
72
73    /// Read the current override effort, if any.
74    pub fn get(&self) -> Option<String> {
75        // Recover from a poisoned lock rather than silently reverting to the
76        // message-derived effort mid-turn (see `set`).
77        let guard = self.inner.read().unwrap_or_else(|e| e.into_inner());
78        guard.clone()
79    }
80}
81
82impl std::fmt::Debug for ReasoningEffortHandle {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        f.debug_struct("ReasoningEffortHandle")
85            .field("effort", &self.get())
86            .finish()
87    }
88}
89
90// ============================================================================
91// AgentStore - For retrieving agent configurations
92// ============================================================================
93
94/// Trait for retrieving agent configurations
95///
96/// Implementations can:
97/// - Load agents from a database
98/// - Keep agents in memory for testing
99/// - Load agents from a configuration file
100#[async_trait]
101pub trait AgentStore: Send + Sync {
102    /// Get an agent by ID
103    async fn get_agent(&self, agent_id: AgentId) -> Result<Option<Agent>>;
104}
105
106#[async_trait]
107impl<T: AgentStore + ?Sized> AgentStore for std::sync::Arc<T> {
108    async fn get_agent(&self, agent_id: AgentId) -> Result<Option<Agent>> {
109        (**self).get_agent(agent_id).await
110    }
111}
112
113// ============================================================================
114// HarnessStore - For retrieving harness configurations
115// ============================================================================
116
117/// Trait for retrieving harness configurations
118///
119/// Implementations can:
120/// - Load harnesses from a database
121/// - Keep harnesses in memory for testing
122///
123/// Returns the harness inheritance chain (root-to-leaf) so the caller
124/// can fold each harness as an `AgentConfigOverlay`. DB-backed stores
125/// return the raw chain; gRPC-backed stores may return a single
126/// pre-merged harness (functionally equivalent when folded).
127#[async_trait]
128pub trait HarnessStore: Send + Sync {
129    /// Get the harness inheritance chain, root-to-leaf.
130    ///
131    /// Returns `Ok(vec![])` if the harness does not exist.
132    /// A harness with no parent returns a single-element vec.
133    async fn get_harness_chain(&self, harness_id: HarnessId) -> Result<Vec<Harness>>;
134}
135
136#[async_trait]
137impl<T: HarnessStore + ?Sized> HarnessStore for std::sync::Arc<T> {
138    async fn get_harness_chain(&self, harness_id: HarnessId) -> Result<Vec<Harness>> {
139        (**self).get_harness_chain(harness_id).await
140    }
141}
142
143// ============================================================================
144// SessionStore - For retrieving session information
145// ============================================================================
146
147use crate::capability_types::AgentCapabilityConfig;
148use crate::leased_resource::{LeasedResource, UpsertLeasedResource};
149use crate::session::Session;
150
151/// Trait for retrieving session configurations
152///
153/// Implementations can:
154/// - Load sessions from a database
155/// - Keep sessions in memory for testing
156#[async_trait]
157pub trait SessionStore: Send + Sync {
158    /// Get a session by ID
159    async fn get_session(&self, session_id: SessionId) -> Result<Option<Session>>;
160}
161
162#[async_trait]
163impl<T: SessionStore + ?Sized> SessionStore for std::sync::Arc<T> {
164    async fn get_session(&self, session_id: SessionId) -> Result<Option<Session>> {
165        (**self).get_session(session_id).await
166    }
167}
168
169/// Trait for updating mutable session metadata.
170#[async_trait]
171pub trait SessionMutator: Send + Sync {
172    /// Update a session's human-readable title.
173    async fn update_session_title(&self, session_id: SessionId, title: String) -> Result<Session>;
174
175    /// Add or replace a session-scoped capability atomically.
176    ///
177    /// Session backends that support live capability reconfiguration override
178    /// this method. The default keeps existing backends source-compatible and
179    /// fails closed instead of pretending the capability was applied.
180    async fn upsert_session_capability(
181        &self,
182        _session_id: SessionId,
183        _capability: AgentCapabilityConfig,
184    ) -> Result<Session> {
185        Err(crate::error::AgentLoopError::store(
186            "session backend does not support live capability reconfiguration",
187        ))
188    }
189
190    /// Remove a session-scoped capability atomically.
191    async fn remove_session_capability(
192        &self,
193        _session_id: SessionId,
194        _capability_id: &str,
195    ) -> Result<Session> {
196        Err(crate::error::AgentLoopError::store(
197            "session backend does not support live capability reconfiguration",
198        ))
199    }
200}
201
202#[async_trait]
203impl<T: SessionMutator + ?Sized> SessionMutator for std::sync::Arc<T> {
204    async fn update_session_title(&self, session_id: SessionId, title: String) -> Result<Session> {
205        (**self).update_session_title(session_id, title).await
206    }
207
208    async fn upsert_session_capability(
209        &self,
210        session_id: SessionId,
211        capability: AgentCapabilityConfig,
212    ) -> Result<Session> {
213        (**self)
214            .upsert_session_capability(session_id, capability)
215            .await
216    }
217
218    async fn remove_session_capability(
219        &self,
220        session_id: SessionId,
221        capability_id: &str,
222    ) -> Result<Session> {
223        (**self)
224            .remove_session_capability(session_id, capability_id)
225            .await
226    }
227}
228
229// ============================================================================
230// ProviderStore - For retrieving LLM provider configurations
231// ============================================================================
232
233/// Legacy runtime model input retained for the 0.17.x compatibility surface.
234///
235/// Execution converts this value into a credential-free [`crate::ModelSpec`]
236/// plus a runtime provider configuration before selecting a driver. New
237/// application code should use `ModelSpec` and `Provider` directly.
238#[derive(Debug, Clone)]
239pub struct ResolvedModel {
240    pub model: String,
241    pub provider_type: DriverId,
242    pub api_key: Option<String>,
243    pub base_url: Option<String>,
244    pub provider_metadata: Option<crate::driver_registry::ProviderMetadata>,
245}
246
247impl ResolvedModel {
248    pub fn provider_key(&self) -> crate::ProviderKey {
249        self.provider_metadata
250            .as_ref()
251            .and_then(|metadata| metadata.extra.as_ref())
252            .and_then(|extra| extra.get("provider_id"))
253            .and_then(serde_json::Value::as_str)
254            .map(crate::ProviderKey::new)
255            .unwrap_or_else(|| crate::ProviderKey::new(self.provider_type.as_str()))
256    }
257
258    pub fn canonical_parts(&self) -> (crate::ModelSpec, crate::ProviderConfig) {
259        let provider = self.provider_key();
260        let spec = crate::ModelSpec::on(provider.clone(), self.model.clone());
261        let config = crate::ProviderConfig {
262            provider,
263            provider_type: self.provider_type.clone(),
264            api_key: self.api_key.clone(),
265            base_url: self.base_url.clone(),
266            metadata: self.provider_metadata.clone().unwrap_or_default(),
267        };
268        (spec, config)
269    }
270}
271
272/// Trait for retrieving model identity and LLM provider configurations.
273///
274/// Model lookup is credential-free. Provider configuration is resolved
275/// separately by provider identity before execution. The inline credential
276/// fields on [`ResolvedModel`] remain only for the 0.17.x compatibility input.
277///
278/// Implementations can:
279/// - Load from a database with encrypted API keys
280/// - Use in-memory configurations for testing
281/// - Load from environment variables for development
282#[async_trait]
283pub trait ProviderStore: Send + Sync {
284    /// Get model with provider info by model ID
285    ///
286    /// Hosted implementations return model and provider identity only. Legacy
287    /// embedders may still return inline provider configuration during 0.17.x;
288    /// execution immediately converts it into the canonical provider path.
289    async fn get_resolved_model(&self, model_id: ModelId) -> Result<Option<ResolvedModel>>;
290
291    /// Get the default model with provider info
292    ///
293    /// Returns the system default model when an agent has no default_model_id set.
294    async fn get_default_model(&self) -> Result<Option<ResolvedModel>>;
295
296    /// Resolve runtime service configuration independently from the model.
297    /// Application-supplied providers registered directly on the platform do
298    /// not need a stored config, so the default maps the open provider id to an
299    /// equally named integration kind without credentials.
300    async fn get_provider_config(
301        &self,
302        _provider: &crate::ProviderKey,
303    ) -> Result<Option<crate::ProviderConfig>> {
304        Ok(None)
305    }
306}
307
308#[async_trait]
309impl<T: ProviderStore + ?Sized> ProviderStore for std::sync::Arc<T> {
310    async fn get_resolved_model(&self, model_id: ModelId) -> Result<Option<ResolvedModel>> {
311        (**self).get_resolved_model(model_id).await
312    }
313
314    async fn get_default_model(&self) -> Result<Option<ResolvedModel>> {
315        (**self).get_default_model().await
316    }
317
318    async fn get_provider_config(
319        &self,
320        provider: &crate::ProviderKey,
321    ) -> Result<Option<crate::ProviderConfig>> {
322        (**self).get_provider_config(provider).await
323    }
324}
325
326// ============================================================================
327// ImageArtifactStore - For durable image persistence from tools
328// ============================================================================
329
330/// Metadata for a stored image artifact.
331#[derive(Debug, Clone)]
332pub struct StoredImageInfo {
333    pub id: ImageId,
334    pub filename: String,
335    pub content_type: String,
336    pub size_bytes: i64,
337    pub metadata: serde_json::Value,
338    pub created_at: DateTime<Utc>,
339}
340
341/// Stored image artifact with binary data.
342#[derive(Debug, Clone)]
343pub struct StoredImage {
344    pub info: StoredImageInfo,
345    pub data: Vec<u8>,
346}
347
348/// Input for creating a stored image artifact.
349#[derive(Debug, Clone)]
350pub struct CreateStoredImage {
351    pub filename: String,
352    pub content_type: String,
353    pub data: Vec<u8>,
354    pub metadata: serde_json::Value,
355}
356
357#[async_trait]
358pub trait ImageArtifactStore: Send + Sync {
359    /// Persist an image artifact and return its durable metadata.
360    async fn create_image(&self, input: CreateStoredImage) -> Result<StoredImageInfo>;
361
362    /// Load a stored image artifact including bytes.
363    async fn get_image(&self, image_id: ImageId) -> Result<Option<StoredImage>>;
364
365    /// Load stored image metadata without binary data.
366    async fn get_image_info(&self, image_id: ImageId) -> Result<Option<StoredImageInfo>>;
367}
368
369// ============================================================================
370// ProviderCredentialStore - For tool-side provider credential resolution
371// ============================================================================
372
373/// Provider credentials resolved for tool-side API clients.
374#[derive(Debug, Clone)]
375pub struct ProviderCredentials {
376    pub api_key: String,
377    pub base_url: Option<String>,
378}
379
380#[async_trait]
381pub trait ProviderCredentialStore: Send + Sync {
382    /// Resolve default credentials for a provider type (for example `openai`).
383    ///
384    /// Implementations may apply environment fallbacks internally, but tools
385    /// should never read provider env vars directly.
386    async fn get_default_provider_credentials(
387        &self,
388        provider_type: &str,
389    ) -> Result<Option<ProviderCredentials>>;
390}
391
392// ============================================================================
393// ToolExecutor - For executing tool calls
394// ============================================================================
395
396/// Trait for executing tool calls
397///
398/// Implementations handle the actual tool execution:
399/// - Webhook calls
400/// - Built-in function execution
401/// - Mock execution for testing
402#[async_trait]
403pub trait ToolExecutor: Send + Sync {
404    /// Execute a single tool call (without context)
405    ///
406    /// This is the legacy method that doesn't provide context to tools.
407    /// Use `execute_with_context` when context is available.
408    async fn execute(&self, tool_call: &ToolCall, tool_def: &ToolDefinition) -> Result<ToolResult>;
409
410    /// Execute a single tool call with context
411    ///
412    /// This method provides runtime context to tools that need it (like filesystem tools).
413    /// The default implementation delegates to `execute()`.
414    async fn execute_with_context(
415        &self,
416        tool_call: &ToolCall,
417        tool_def: &ToolDefinition,
418        _context: &ToolContext,
419    ) -> Result<ToolResult> {
420        // Default: delegate to execute(), ignoring context
421        self.execute(tool_call, tool_def).await
422    }
423
424    /// Execute multiple tool calls (default: sequential)
425    async fn execute_batch(
426        &self,
427        tool_calls: &[ToolCall],
428        tool_defs: &[ToolDefinition],
429    ) -> Result<Vec<ToolResult>> {
430        let mut results = Vec::with_capacity(tool_calls.len());
431
432        let tool_map = build_tool_map(tool_defs);
433
434        for tool_call in tool_calls {
435            let tool_def = tool_map.get(tool_call.name.as_str()).ok_or_else(|| {
436                crate::error::AgentLoopError::tool(format!(
437                    "Tool definition not found: {}",
438                    tool_call.name
439                ))
440            })?;
441
442            results.push(self.execute(tool_call, tool_def).await?);
443        }
444
445        Ok(results)
446    }
447
448    /// Execute multiple tool calls in parallel
449    async fn execute_parallel(
450        &self,
451        tool_calls: &[ToolCall],
452        tool_defs: &[ToolDefinition],
453    ) -> Result<Vec<ToolResult>>
454    where
455        Self: Sized,
456    {
457        use futures::future::join_all;
458
459        let tool_map = build_tool_map(tool_defs);
460
461        let futures: Vec<_> = tool_calls
462            .iter()
463            .map(|tool_call| async {
464                let tool_def = tool_map.get(tool_call.name.as_str()).ok_or_else(|| {
465                    crate::error::AgentLoopError::tool(format!(
466                        "Tool definition not found: {}",
467                        tool_call.name
468                    ))
469                })?;
470                self.execute(tool_call, tool_def).await
471            })
472            .collect();
473
474        let results = join_all(futures).await;
475        results.into_iter().collect()
476    }
477}
478
479/// Delegating impl so callers can hold a `ToolExecutor` as a trait object
480/// (e.g. to choose between a plain registry and an MCP-routing composite at
481/// runtime without monomorphizing the consumer).
482#[async_trait]
483impl ToolExecutor for std::sync::Arc<dyn ToolExecutor> {
484    async fn execute(&self, tool_call: &ToolCall, tool_def: &ToolDefinition) -> Result<ToolResult> {
485        (**self).execute(tool_call, tool_def).await
486    }
487
488    async fn execute_with_context(
489        &self,
490        tool_call: &ToolCall,
491        tool_def: &ToolDefinition,
492        context: &ToolContext,
493    ) -> Result<ToolResult> {
494        (**self)
495            .execute_with_context(tool_call, tool_def, context)
496            .await
497    }
498
499    async fn execute_batch(
500        &self,
501        tool_calls: &[ToolCall],
502        tool_defs: &[ToolDefinition],
503    ) -> Result<Vec<ToolResult>> {
504        (**self).execute_batch(tool_calls, tool_defs).await
505    }
506}
507
508// ============================================================================
509// SessionFileSystem - For session filesystem operations
510// ============================================================================
511
512/// Trait for session filesystem operations
513///
514/// This trait abstracts the session filesystem contract for tools and hosts.
515/// Implementations can:
516/// - Store files in a database (production)
517/// - Use an in-memory filesystem for testing
518/// - Project files onto real disk or object storage
519#[async_trait]
520pub trait SessionFileSystem: Send + Sync {
521    /// Human-facing root path for this filesystem.
522    ///
523    /// `/workspace` is the stable agent namespace and the default. Direct
524    /// host-backed stores may override this for host-side integrations, while
525    /// [`MountFs`](crate::mount_fs::MountFs) restores the agent-facing root.
526    fn display_root(&self) -> String {
527        crate::session_path::WORKSPACE_PREFIX.to_string()
528    }
529
530    /// Convert a canonical session path into a human-facing path.
531    ///
532    /// The default renders the `/workspace` alias. Direct host-backed stores may
533    /// override it, while [`MountFs`](crate::mount_fs::MountFs) presents primary
534    /// workspace paths through the stable agent-facing namespace.
535    fn display_path(&self, path: &str) -> String {
536        crate::session_path::to_display_path(path)
537    }
538
539    /// Resolve an input path (any accepted spelling, relative or absolute) to an
540    /// absolute path within this filesystem's namespace. Relative inputs resolve
541    /// against the filesystem's current directory.
542    ///
543    /// This is how a shell seeds its working directory: [`MountFs`] returns a
544    /// path in its stable agent-facing namespace, so the shell and file tools
545    /// share the same identity. The default is the flat VFS session form.
546    /// Security decorators may authorize the returned path, so providers that
547    /// accept additional aliases must use the same contained mapping here and
548    /// in their I/O methods. An alias must never resolve to one workspace path
549    /// here and a different storage object during the subsequent operation.
550    ///
551    /// [`MountFs`]: crate::mount_fs::MountFs
552    fn resolve_path(&self, input: &str) -> String {
553        crate::session_path::to_session_path(input)
554    }
555
556    /// Whether this store is already a mount-based resolver ([`MountFs`]).
557    ///
558    /// Used to avoid re-wrapping nested mount tables when building tool context.
559    fn is_mount_resolver(&self) -> bool;
560
561    /// Read a file by path
562    async fn read_file(&self, session_id: SessionId, path: &str) -> Result<Option<SessionFile>>;
563
564    /// Write/create a file
565    async fn write_file(
566        &self,
567        session_id: SessionId,
568        path: &str,
569        content: &str,
570        encoding: &str,
571    ) -> Result<SessionFile>;
572
573    /// Write a file only if its current content snapshot still matches.
574    ///
575    /// Implementations backed by transactional storage should override this
576    /// with an atomic compare-and-set update.
577    async fn write_file_if_content_matches(
578        &self,
579        session_id: SessionId,
580        path: &str,
581        expected_content: &str,
582        expected_encoding: &str,
583        content: &str,
584        encoding: &str,
585    ) -> Result<Option<SessionFile>> {
586        let Some(existing) = self.read_file(session_id, path).await? else {
587            return Ok(None);
588        };
589
590        if existing.is_directory {
591            return Ok(None);
592        }
593
594        let current_content = existing.content.unwrap_or_default();
595        if current_content != expected_content || existing.encoding != expected_encoding {
596            return Ok(None);
597        }
598
599        self.write_file(session_id, path, content, encoding)
600            .await
601            .map(Some)
602    }
603
604    /// Delete a file or directory
605    async fn delete_file(&self, session_id: SessionId, path: &str, recursive: bool)
606    -> Result<bool>;
607
608    /// List files in a directory
609    async fn list_directory(&self, session_id: SessionId, path: &str) -> Result<Vec<FileInfo>>;
610
611    /// Get file metadata
612    async fn stat_file(&self, session_id: SessionId, path: &str) -> Result<Option<FileStat>>;
613
614    /// Search file contents with Rust regex syntax, optionally filtering canonical paths by glob.
615    ///
616    /// Implementations compile the content pattern once before scanning and
617    /// return an error for invalid regex. Basename-only globs match at any
618    /// depth. Non-glob path filters retain legacy substring matching; see
619    /// `knowledge/runtime-resources/file-store.md`.
620    async fn grep_files(
621        &self,
622        session_id: SessionId,
623        pattern: &str,
624        path_pattern: Option<&str>,
625    ) -> Result<Vec<GrepMatch>>;
626
627    /// Search with match pagination and bounded before/after context.
628    ///
629    /// Backends should override this to collect context during their content
630    /// scan. The default preserves compatibility for third-party stores that
631    /// only implement the original zero-context method.
632    async fn grep_files_with_options(
633        &self,
634        session_id: SessionId,
635        pattern: &str,
636        options: &GrepOptions,
637    ) -> Result<GrepSearchResult> {
638        if options.before_context != 0 || options.after_context != 0 {
639            return Err(crate::error::AgentLoopError::tool(
640                "this file store does not support grep context",
641            ));
642        }
643        let all = self
644            .grep_files(session_id, pattern, options.path_pattern.as_deref())
645            .await?;
646        Ok(crate::session_file::bound_grep_matches(all, options))
647    }
648
649    /// Create a directory
650    async fn create_directory(&self, session_id: SessionId, path: &str) -> Result<FileInfo>;
651
652    /// Seed a starter file into a session workspace.
653    async fn seed_initial_file(&self, session_id: SessionId, file: &InitialFile) -> Result<()> {
654        if file.is_readonly {
655            return Err(crate::error::AgentLoopError::store(
656                "read-only initial files require a SessionFileSystem-specific seed implementation",
657            ));
658        }
659        self.write_file(session_id, &file.path, &file.content, &file.encoding)
660            .await?;
661        Ok(())
662    }
663}
664
665/// A [`SessionFileSystem`] decorator that pins every operation to a fixed
666/// workspace key, ignoring the per-call `session_id`.
667///
668/// Used to re-key file I/O for a session attached to a shared workspace (where
669/// `workspace.id != session.id`): wrap the session's file store once with the
670/// session's `workspace_id`, and all downstream capability/tool access then
671/// addresses the attached workspace rather than the session's own keyspace. For
672/// the default 1:1 session the key equals the session id, so the wrapper is a
673/// transparent pass-through. See `knowledge/runtime-resources/workspace.md`.
674pub struct WorkspaceScopedFileSystem {
675    inner: Arc<dyn SessionFileSystem>,
676    key: SessionId,
677}
678
679impl WorkspaceScopedFileSystem {
680    /// Wrap `inner`, pinning all operations to `workspace_id`'s key.
681    pub fn wrap(
682        inner: Arc<dyn SessionFileSystem>,
683        workspace_id: WorkspaceId,
684    ) -> Arc<dyn SessionFileSystem> {
685        Arc::new(Self {
686            inner,
687            key: SessionId::from_uuid(workspace_id.uuid()),
688        })
689    }
690}
691
692#[async_trait]
693impl SessionFileSystem for WorkspaceScopedFileSystem {
694    async fn read_file(&self, _session_id: SessionId, path: &str) -> Result<Option<SessionFile>> {
695        self.inner.read_file(self.key, path).await
696    }
697    async fn write_file(
698        &self,
699        _session_id: SessionId,
700        path: &str,
701        content: &str,
702        encoding: &str,
703    ) -> Result<SessionFile> {
704        self.inner
705            .write_file(self.key, path, content, encoding)
706            .await
707    }
708    async fn write_file_if_content_matches(
709        &self,
710        _session_id: SessionId,
711        path: &str,
712        expected_content: &str,
713        expected_encoding: &str,
714        content: &str,
715        encoding: &str,
716    ) -> Result<Option<SessionFile>> {
717        self.inner
718            .write_file_if_content_matches(
719                self.key,
720                path,
721                expected_content,
722                expected_encoding,
723                content,
724                encoding,
725            )
726            .await
727    }
728    async fn delete_file(
729        &self,
730        _session_id: SessionId,
731        path: &str,
732        recursive: bool,
733    ) -> Result<bool> {
734        self.inner.delete_file(self.key, path, recursive).await
735    }
736    async fn list_directory(&self, _session_id: SessionId, path: &str) -> Result<Vec<FileInfo>> {
737        self.inner.list_directory(self.key, path).await
738    }
739    async fn stat_file(&self, _session_id: SessionId, path: &str) -> Result<Option<FileStat>> {
740        self.inner.stat_file(self.key, path).await
741    }
742    async fn grep_files(
743        &self,
744        _session_id: SessionId,
745        pattern: &str,
746        path_pattern: Option<&str>,
747    ) -> Result<Vec<GrepMatch>> {
748        self.inner.grep_files(self.key, pattern, path_pattern).await
749    }
750    async fn grep_files_with_options(
751        &self,
752        _session_id: SessionId,
753        pattern: &str,
754        options: &GrepOptions,
755    ) -> Result<GrepSearchResult> {
756        self.inner
757            .grep_files_with_options(self.key, pattern, options)
758            .await
759    }
760    async fn create_directory(&self, _session_id: SessionId, path: &str) -> Result<FileInfo> {
761        self.inner.create_directory(self.key, path).await
762    }
763    async fn seed_initial_file(&self, _session_id: SessionId, file: &InitialFile) -> Result<()> {
764        self.inner.seed_initial_file(self.key, file).await
765    }
766
767    fn display_root(&self) -> String {
768        self.inner.display_root()
769    }
770
771    fn display_path(&self, path: &str) -> String {
772        self.inner.display_path(path)
773    }
774
775    fn resolve_path(&self, input: &str) -> String {
776        self.inner.resolve_path(input)
777    }
778
779    fn is_mount_resolver(&self) -> bool {
780        self.inner.is_mount_resolver()
781    }
782}
783
784#[async_trait]
785impl<T: SessionFileSystem + ?Sized> SessionFileSystem for std::sync::Arc<T> {
786    fn display_root(&self) -> String {
787        (**self).display_root()
788    }
789
790    fn display_path(&self, path: &str) -> String {
791        (**self).display_path(path)
792    }
793
794    fn resolve_path(&self, input: &str) -> String {
795        (**self).resolve_path(input)
796    }
797
798    fn is_mount_resolver(&self) -> bool {
799        (**self).is_mount_resolver()
800    }
801
802    async fn read_file(&self, session_id: SessionId, path: &str) -> Result<Option<SessionFile>> {
803        (**self).read_file(session_id, path).await
804    }
805
806    async fn write_file(
807        &self,
808        session_id: SessionId,
809        path: &str,
810        content: &str,
811        encoding: &str,
812    ) -> Result<SessionFile> {
813        (**self)
814            .write_file(session_id, path, content, encoding)
815            .await
816    }
817
818    async fn write_file_if_content_matches(
819        &self,
820        session_id: SessionId,
821        path: &str,
822        expected_content: &str,
823        expected_encoding: &str,
824        content: &str,
825        encoding: &str,
826    ) -> Result<Option<SessionFile>> {
827        (**self)
828            .write_file_if_content_matches(
829                session_id,
830                path,
831                expected_content,
832                expected_encoding,
833                content,
834                encoding,
835            )
836            .await
837    }
838
839    async fn delete_file(
840        &self,
841        session_id: SessionId,
842        path: &str,
843        recursive: bool,
844    ) -> Result<bool> {
845        (**self).delete_file(session_id, path, recursive).await
846    }
847
848    async fn list_directory(&self, session_id: SessionId, path: &str) -> Result<Vec<FileInfo>> {
849        (**self).list_directory(session_id, path).await
850    }
851
852    async fn stat_file(&self, session_id: SessionId, path: &str) -> Result<Option<FileStat>> {
853        (**self).stat_file(session_id, path).await
854    }
855
856    async fn grep_files(
857        &self,
858        session_id: SessionId,
859        pattern: &str,
860        path_pattern: Option<&str>,
861    ) -> Result<Vec<GrepMatch>> {
862        (**self).grep_files(session_id, pattern, path_pattern).await
863    }
864
865    async fn grep_files_with_options(
866        &self,
867        session_id: SessionId,
868        pattern: &str,
869        options: &GrepOptions,
870    ) -> Result<GrepSearchResult> {
871        (**self)
872            .grep_files_with_options(session_id, pattern, options)
873            .await
874    }
875
876    async fn create_directory(&self, session_id: SessionId, path: &str) -> Result<FileInfo> {
877        (**self).create_directory(session_id, path).await
878    }
879
880    async fn seed_initial_file(&self, session_id: SessionId, file: &InitialFile) -> Result<()> {
881        (**self).seed_initial_file(session_id, file).await
882    }
883}
884
885/// Backward-compatible alias for the old session filesystem trait name.
886pub use SessionFileSystem as SessionFileStore;
887
888/// Host-supplied values used by platform file-system factories.
889///
890/// The context is intentionally type-erased so `everruns-core` can own the
891/// platform contract without depending on server-only types such as
892/// `StorageBackend` or future object-storage clients.
893#[derive(Clone, Default)]
894pub struct SessionFileSystemFactoryContext {
895    values: Arc<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>,
896}
897
898impl SessionFileSystemFactoryContext {
899    pub fn new() -> Self {
900        Self::default()
901    }
902
903    pub fn with<T: Any + Send + Sync>(mut self, value: Arc<T>) -> Self {
904        let values = Arc::make_mut(&mut self.values);
905        values.insert(TypeId::of::<T>(), value);
906        self
907    }
908
909    pub fn get<T: Any + Send + Sync>(&self) -> Option<Arc<T>> {
910        self.values
911            .get(&TypeId::of::<T>())
912            .and_then(|value| value.clone().downcast::<T>().ok())
913    }
914
915    pub fn with_workspace_roots(self, roots: Arc<crate::WorkspaceRootSet>) -> Self {
916        self.with(roots)
917    }
918
919    pub fn workspace_roots(&self) -> Option<Arc<crate::WorkspaceRootSet>> {
920        self.get::<crate::WorkspaceRootSet>()
921    }
922}
923
924/// Factory for deployment-selected session filesystem implementations.
925#[async_trait]
926pub trait SessionFileSystemFactory: Send + Sync {
927    /// Human-readable factory name for diagnostics.
928    fn name(&self) -> &'static str {
929        "SessionFileSystemFactory"
930    }
931
932    /// Whether this factory intentionally leaves filesystem selection to the
933    /// runtime default.
934    fn is_disabled(&self) -> bool {
935        false
936    }
937
938    /// Resolve a live filesystem from host-provided dependencies.
939    async fn create_session_file_system(
940        &self,
941        context: SessionFileSystemFactoryContext,
942    ) -> Result<Arc<dyn SessionFileSystem>>;
943}
944
945/// Default factory used when a platform does not configure session files.
946#[derive(Debug, Clone, Default)]
947pub struct DisabledSessionFileSystemFactory;
948
949#[async_trait]
950impl SessionFileSystemFactory for DisabledSessionFileSystemFactory {
951    fn name(&self) -> &'static str {
952        "DisabledSessionFileSystemFactory"
953    }
954
955    fn is_disabled(&self) -> bool {
956        true
957    }
958
959    async fn create_session_file_system(
960        &self,
961        _context: SessionFileSystemFactoryContext,
962    ) -> Result<Arc<dyn SessionFileSystem>> {
963        Err(crate::error::AgentLoopError::config(
964            "session filesystem is disabled",
965        ))
966    }
967}
968
969// ============================================================================
970// SessionStorageStore - For session key/value and secret storage
971// ============================================================================
972
973/// Info about a stored key (without its value)
974#[derive(Debug, Clone)]
975pub struct KeyInfo {
976    pub key: String,
977    pub created_at: chrono::DateTime<chrono::Utc>,
978    pub updated_at: chrono::DateTime<chrono::Utc>,
979}
980
981/// Info about a stored secret (without its value)
982#[derive(Debug, Clone)]
983pub struct SecretInfo {
984    pub name: String,
985    pub created_at: chrono::DateTime<chrono::Utc>,
986    pub updated_at: chrono::DateTime<chrono::Utc>,
987}
988
989/// Trait for session key/value and secret storage operations
990///
991/// This trait abstracts storage operations for tools that need to persist
992/// data within a session. Implementations can:
993/// - Store data in a database (production)
994/// - Use in-memory storage for testing
995///
996/// A single ranked hit from a Knowledge Base search. Public-id surface only;
997/// no internal UUIDs. See knowledge/runtime-resources/knowledge-bases.md and knowledge/runtime-resources/okf-adoption.md.
998#[derive(Debug, Clone, serde::Serialize)]
999pub struct KnowledgeSearchHit {
1000    /// Entry public id (`kbe_…`).
1001    pub id: String,
1002    /// Owning Knowledge Base public id (`kb_…`).
1003    pub kb_id: String,
1004    pub title: String,
1005    pub kind: String,
1006    pub tags: Vec<String>,
1007    /// Short body excerpt for the LLM.
1008    pub snippet: String,
1009    /// Optional OKF resource URI, when set on the entry.
1010    pub resource: Option<String>,
1011}
1012
1013/// Org-scoped search over curated Knowledge Bases, backing the agent-facing
1014/// `search_knowledge` tool. Implementations MUST scope to `org_id` and silently
1015/// ignore KB ids not owned by that org (no existence leak across tenants).
1016#[async_trait]
1017pub trait KnowledgeStore: Send + Sync {
1018    async fn search_knowledge(
1019        &self,
1020        org_id: crate::typed_id::OrgId,
1021        kb_public_ids: &[String],
1022        query: &str,
1023        kind: Option<&str>,
1024        tags: &[String],
1025        limit: usize,
1026    ) -> Result<Vec<KnowledgeSearchHit>>;
1027}
1028
1029/// Storage for session-scoped key/value pairs and secrets.
1030///
1031/// Key/value storage is for general data that doesn't need encryption.
1032/// Secret storage is for sensitive data that is encrypted at rest.
1033#[async_trait]
1034pub trait SessionStorageStore: Send + Sync {
1035    // Key/Value operations (plain text)
1036
1037    /// Set a key/value pair (creates or updates)
1038    async fn set_value(&self, session_id: SessionId, key: &str, value: &str) -> Result<()>;
1039
1040    /// Get a value by key
1041    async fn get_value(&self, session_id: SessionId, key: &str) -> Result<Option<String>>;
1042
1043    /// Delete a key/value pair
1044    async fn delete_value(&self, session_id: SessionId, key: &str) -> Result<bool>;
1045
1046    /// List all keys in a session
1047    async fn list_keys(&self, session_id: SessionId) -> Result<Vec<KeyInfo>>;
1048
1049    // Secret operations (encrypted)
1050
1051    /// Set a secret (creates or updates, value is encrypted before storage)
1052    async fn set_secret(&self, session_id: SessionId, name: &str, value: &str) -> Result<()>;
1053
1054    /// Get a secret by name (value is decrypted before returning)
1055    async fn get_secret(&self, session_id: SessionId, name: &str) -> Result<Option<String>>;
1056
1057    /// Delete a secret
1058    async fn delete_secret(&self, session_id: SessionId, name: &str) -> Result<bool>;
1059
1060    /// List all secret names in a session (without values)
1061    async fn list_secrets(&self, session_id: SessionId) -> Result<Vec<SecretInfo>>;
1062}
1063
1064// ============================================================================
1065// SessionScheduleStore - For session-scoped schedule operations
1066// ============================================================================
1067
1068use crate::session_schedule::SessionSchedule;
1069use crate::typed_id::ScheduleId;
1070
1071/// Trait for session schedule CRUD operations.
1072///
1073/// Used by scheduling tools to create, cancel, and list schedules.
1074#[async_trait]
1075pub trait SessionScheduleStore: Send + Sync {
1076    /// Create a new schedule for a session.
1077    async fn create_schedule(
1078        &self,
1079        session_id: SessionId,
1080        description: String,
1081        cron_expression: Option<String>,
1082        scheduled_at: Option<chrono::DateTime<chrono::Utc>>,
1083        timezone: String,
1084    ) -> Result<SessionSchedule>;
1085
1086    /// Create a new schedule after enforcing create-time limits in the same
1087    /// store operation. Backends with shared mutable state must override this
1088    /// to make the check-and-create sequence atomic.
1089    async fn create_schedule_enforcing_limits(
1090        &self,
1091        session_id: SessionId,
1092        description: String,
1093        cron_expression: Option<String>,
1094        scheduled_at: Option<chrono::DateTime<chrono::Utc>>,
1095        timezone: String,
1096    ) -> std::result::Result<SessionSchedule, crate::session_schedule::ScheduleLimitError> {
1097        let per_session = self
1098            .count_active_schedules(session_id)
1099            .await
1100            .map_err(crate::session_schedule::ScheduleLimitError::Store)?;
1101        if per_session >= crate::session_schedule::MAX_ACTIVE_SCHEDULES_PER_SESSION {
1102            return Err(crate::session_schedule::ScheduleLimitError::Rejected(
1103                format!(
1104                    "Maximum {} active schedules per session. Cancel an existing schedule first.",
1105                    crate::session_schedule::MAX_ACTIVE_SCHEDULES_PER_SESSION
1106                ),
1107            ));
1108        }
1109
1110        let max_per_org = crate::session_schedule::max_active_schedules_per_org();
1111        let per_org = self
1112            .count_active_org_schedules()
1113            .await
1114            .map_err(crate::session_schedule::ScheduleLimitError::Store)?;
1115        if i64::from(per_org) >= max_per_org {
1116            return Err(crate::session_schedule::ScheduleLimitError::Rejected(
1117                format!(
1118                    "Maximum {max_per_org} active schedules per org reached. Cancel an existing schedule first."
1119                ),
1120            ));
1121        }
1122
1123        if let Some(cron) = cron_expression.as_deref() {
1124            crate::session_schedule::validate_cron_min_interval(cron)
1125                .map_err(crate::session_schedule::ScheduleLimitError::Rejected)?;
1126        }
1127
1128        self.create_schedule(
1129            session_id,
1130            description,
1131            cron_expression,
1132            scheduled_at,
1133            timezone,
1134        )
1135        .await
1136        .map_err(crate::session_schedule::ScheduleLimitError::Store)
1137    }
1138
1139    /// Cancel (disable) a schedule.
1140    async fn cancel_schedule(
1141        &self,
1142        session_id: SessionId,
1143        schedule_id: ScheduleId,
1144    ) -> Result<SessionSchedule>;
1145
1146    /// List schedules for a session.
1147    async fn list_schedules(&self, session_id: SessionId) -> Result<Vec<SessionSchedule>>;
1148
1149    /// Count active (enabled) schedules for a session.
1150    async fn count_active_schedules(&self, session_id: SessionId) -> Result<u32>;
1151
1152    /// Count active (enabled) schedules across the whole org this store is
1153    /// scoped to. Used to enforce a per-org cap independent of session count:
1154    /// `count_active_schedules` only bounds one session, so unlimited sessions
1155    /// would otherwise imply unlimited active schedules per org.
1156    async fn count_active_org_schedules(&self) -> Result<u32>;
1157}
1158
1159// ============================================================================
1160// SessionResourceRegistry - Generic session-scoped resource registry
1161// ============================================================================
1162
1163/// Generic registry of resources active alongside a session.
1164///
1165/// Capabilities register resources here (sandboxes, subagents, browser sessions).
1166/// Agents query it ("what's running?"), infrastructure scans it for cleanup.
1167/// See `knowledge/runtime-resources/session-resources.md`.
1168#[async_trait]
1169pub trait SessionResourceRegistry: Send + Sync {
1170    /// Register a resource (or update if resource_id already exists for this session).
1171    async fn register(
1172        &self,
1173        entry: crate::session_resource::RegisterSessionResource,
1174    ) -> Result<crate::session_resource::SessionResourceEntry>;
1175
1176    /// Update the status of a registered resource.
1177    async fn update_status(
1178        &self,
1179        session_id: SessionId,
1180        resource_id: &str,
1181        status: crate::session_resource::SessionResourceStatus,
1182    ) -> Result<Option<crate::session_resource::SessionResourceEntry>>;
1183
1184    /// Get a specific resource by ID.
1185    async fn get(
1186        &self,
1187        session_id: SessionId,
1188        resource_id: &str,
1189    ) -> Result<Option<crate::session_resource::SessionResourceEntry>>;
1190
1191    /// List resources for a session, optionally filtered.
1192    async fn list(
1193        &self,
1194        session_id: SessionId,
1195        filter: Option<&crate::session_resource::SessionResourceFilter>,
1196    ) -> Result<Vec<crate::session_resource::SessionResourceEntry>>;
1197
1198    /// Remove a resource from the registry.
1199    async fn deregister(&self, session_id: SessionId, resource_id: &str) -> Result<bool>;
1200}
1201
1202// ============================================================================
1203// LeasedResourceStore - For lifecycle-managed external resources
1204// ============================================================================
1205
1206/// Trait for session-scoped leased resource operations.
1207///
1208/// Tools use this store to register or refresh leases when they create or use
1209/// external provider resources. Cleanup workers operate through control-plane
1210/// storage APIs directly so they can claim work across organizations.
1211#[async_trait]
1212pub trait LeasedResourceStore: Send + Sync {
1213    /// Create or refresh a leased resource for a session.
1214    ///
1215    /// Implementations must treat this as an idempotent upsert keyed by the
1216    /// provider-specific resource identity so repeated tool usage extends the
1217    /// same lease instead of creating duplicate rows.
1218    async fn upsert_resource(&self, input: UpsertLeasedResource) -> Result<LeasedResource>;
1219
1220    /// Mark a leased resource as explicitly released.
1221    ///
1222    /// This is the fast path for explicit user intent such as "close browser"
1223    /// or "delete sandbox". It should transition the resource to `released`
1224    /// without waiting for the durable cleanup worker to observe lease expiry.
1225    async fn release_resource(
1226        &self,
1227        session_id: SessionId,
1228        provider: &str,
1229        resource_type: &str,
1230        external_id: &str,
1231    ) -> Result<Option<LeasedResource>>;
1232
1233    /// List leased resources currently associated with a session.
1234    ///
1235    /// Session surfaces use this for visibility. Released resources remain
1236    /// visible so operators can inspect cleanup outcomes and failure history.
1237    async fn list_resources(&self, session_id: SessionId) -> Result<Vec<LeasedResource>>;
1238}
1239
1240// ============================================================================
1241// ToolContext - Runtime context for tool execution
1242// ============================================================================
1243
1244/// Default maximum child depth for subagent delegation. Top-level sessions are
1245/// depth 0, their children are depth 1, and grandchildren are depth 2.
1246pub const DEFAULT_MAX_SUBAGENT_DEPTH: u32 = 2;
1247pub const DEFAULT_MAX_ACTIVE_DESCENDANT_SUBAGENT_TASKS: u32 = 16;
1248pub const DEFAULT_MAX_TOTAL_DESCENDANT_SUBAGENT_TASKS: u32 = 200;
1249/// Governance for detached peer spawns (EVE-767): a detached spawn resets depth
1250/// (it is a peer, not a lifecycle child) but is still counted against the origin
1251/// subagent tree's root so a loop of `spawn_agent(lifetime=detached)` cannot run
1252/// unbounded (TM-DOS). Detached peers are full independent sessions, so the
1253/// default ceiling is tighter than the subagent descendant caps.
1254pub const DEFAULT_MAX_ACTIVE_DETACHED_TASKS: u32 = 8;
1255pub const DEFAULT_MAX_TOTAL_DETACHED_TASKS: u32 = 50;
1256
1257/// Resolved subagent spawn governance policy for a tool execution context.
1258#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1259pub struct SubagentNestingPolicy {
1260    pub platform_default: u32,
1261    pub org_override: Option<u32>,
1262    pub agent_override: Option<u32>,
1263    pub platform_default_max_active_descendant_tasks: u32,
1264    pub org_override_max_active_descendant_tasks: Option<u32>,
1265    pub agent_override_max_active_descendant_tasks: Option<u32>,
1266    pub platform_default_max_total_descendant_tasks: u32,
1267    pub org_override_max_total_descendant_tasks: Option<u32>,
1268    pub agent_override_max_total_descendant_tasks: Option<u32>,
1269    pub platform_default_max_active_detached_tasks: u32,
1270    pub org_override_max_active_detached_tasks: Option<u32>,
1271    pub agent_override_max_active_detached_tasks: Option<u32>,
1272    pub platform_default_max_total_detached_tasks: u32,
1273    pub org_override_max_total_detached_tasks: Option<u32>,
1274    pub agent_override_max_total_detached_tasks: Option<u32>,
1275}
1276
1277impl Default for SubagentNestingPolicy {
1278    fn default() -> Self {
1279        Self {
1280            platform_default: DEFAULT_MAX_SUBAGENT_DEPTH,
1281            org_override: None,
1282            agent_override: None,
1283            platform_default_max_active_descendant_tasks:
1284                DEFAULT_MAX_ACTIVE_DESCENDANT_SUBAGENT_TASKS,
1285            org_override_max_active_descendant_tasks: None,
1286            agent_override_max_active_descendant_tasks: None,
1287            platform_default_max_total_descendant_tasks:
1288                DEFAULT_MAX_TOTAL_DESCENDANT_SUBAGENT_TASKS,
1289            org_override_max_total_descendant_tasks: None,
1290            agent_override_max_total_descendant_tasks: None,
1291            platform_default_max_active_detached_tasks: DEFAULT_MAX_ACTIVE_DETACHED_TASKS,
1292            org_override_max_active_detached_tasks: None,
1293            agent_override_max_active_detached_tasks: None,
1294            platform_default_max_total_detached_tasks: DEFAULT_MAX_TOTAL_DETACHED_TASKS,
1295            org_override_max_total_detached_tasks: None,
1296            agent_override_max_total_detached_tasks: None,
1297        }
1298    }
1299}
1300
1301impl SubagentNestingPolicy {
1302    pub fn max_subagent_depth(self) -> u32 {
1303        self.agent_override
1304            .or(self.org_override)
1305            .unwrap_or(self.platform_default)
1306    }
1307
1308    pub fn max_active_descendant_tasks(self) -> u32 {
1309        self.agent_override_max_active_descendant_tasks
1310            .or(self.org_override_max_active_descendant_tasks)
1311            .unwrap_or(self.platform_default_max_active_descendant_tasks)
1312    }
1313
1314    pub fn max_total_descendant_tasks(self) -> u32 {
1315        self.agent_override_max_total_descendant_tasks
1316            .or(self.org_override_max_total_descendant_tasks)
1317            .unwrap_or(self.platform_default_max_total_descendant_tasks)
1318    }
1319
1320    pub fn max_active_detached_tasks(self) -> u32 {
1321        self.agent_override_max_active_detached_tasks
1322            .or(self.org_override_max_active_detached_tasks)
1323            .unwrap_or(self.platform_default_max_active_detached_tasks)
1324    }
1325
1326    pub fn max_total_detached_tasks(self) -> u32 {
1327        self.agent_override_max_total_detached_tasks
1328            .or(self.org_override_max_total_detached_tasks)
1329            .unwrap_or(self.platform_default_max_total_detached_tasks)
1330    }
1331
1332    pub fn with_platform_default(mut self, depth: u32) -> Self {
1333        self.platform_default = depth;
1334        self
1335    }
1336
1337    pub fn with_org_override(mut self, depth: Option<u32>) -> Self {
1338        self.org_override = depth;
1339        self
1340    }
1341
1342    pub fn with_agent_override(mut self, depth: Option<u32>) -> Self {
1343        self.agent_override = depth;
1344        self
1345    }
1346
1347    pub fn with_agent_task_caps_override(
1348        mut self,
1349        max_active: Option<u32>,
1350        max_total: Option<u32>,
1351    ) -> Self {
1352        self.agent_override_max_active_descendant_tasks = max_active;
1353        self.agent_override_max_total_descendant_tasks = max_total;
1354        self
1355    }
1356
1357    pub fn with_agent_detached_task_caps_override(
1358        mut self,
1359        max_active: Option<u32>,
1360        max_total: Option<u32>,
1361    ) -> Self {
1362        self.agent_override_max_active_detached_tasks = max_active;
1363        self.agent_override_max_total_detached_tasks = max_total;
1364        self
1365    }
1366}
1367
1368/// Type alias for the session SQL DB store trait object.
1369pub type SessionSqlDbStoreRef = Arc<dyn crate::session_sqldb::SessionSqlDbStore>;
1370
1371/// Resolves user connection tokens (e.g. GitHub) lazily at tool execution time.
1372///
1373/// Instead of eagerly injecting tokens at session creation, tools call this
1374/// resolver when they need a token. If the user hasn't connected, returns None.
1375#[async_trait]
1376pub trait UserConnectionResolver: Send + Sync {
1377    /// Get a decrypted connection token for the given provider.
1378    /// Returns None if the user has no connection for this provider.
1379    async fn get_connection_token(
1380        &self,
1381        session_id: SessionId,
1382        provider: &str,
1383    ) -> Result<Option<String>>;
1384
1385    /// Resolve the user ID of the connection used for a session/provider pair.
1386    ///
1387    /// This is used by leased resources to bind cleanup to the same provider
1388    /// identity that created the remote resource.
1389    async fn get_connection_user(
1390        &self,
1391        _session_id: SessionId,
1392        _provider: &str,
1393    ) -> Result<Option<Uuid>> {
1394        Ok(None)
1395    }
1396
1397    /// Resolve a provider token for a specific user.
1398    ///
1399    /// Cleanup workers use this to avoid "first org member wins" behavior when
1400    /// cleaning resources created by a specific provider connection owner.
1401    async fn get_connection_token_for_user(
1402        &self,
1403        _user_id: Uuid,
1404        _provider: &str,
1405    ) -> Result<Option<String>> {
1406        Ok(None)
1407    }
1408
1409    /// Get provider-specific metadata stored alongside the connection.
1410    /// Returns None if no metadata is stored or no connection exists.
1411    async fn get_connection_metadata(
1412        &self,
1413        _session_id: SessionId,
1414        _provider: &str,
1415    ) -> Result<Option<serde_json::Value>> {
1416        Ok(None)
1417    }
1418}
1419
1420// ============================================================================
1421// BudgetChecker - For querying budget status from tools
1422// ============================================================================
1423
1424/// Trait for checking budget status from within tool execution.
1425///
1426/// Implemented by gRPC adapters (worker → server) and direct adapters (in-process).
1427/// Used by the `check_budget` tool to return real budget data to agents.
1428/// The org_id is captured at construction time by the implementing adapter.
1429#[async_trait]
1430pub trait BudgetChecker: Send + Sync {
1431    /// Check all budgets for a session and return a tool-friendly response.
1432    async fn check_budgets(&self, session_id: &str) -> Result<crate::budget::BudgetToolResponse>;
1433}
1434
1435// ============================================================================
1436// PaymentAuthority - For capability-internal machine payments
1437// ============================================================================
1438
1439/// Internal authority for paid capability operations.
1440///
1441/// Capabilities call this with fixed, typed requests. The model never receives a
1442/// generic paid HTTP tool, wallet credentials, or payment payloads.
1443#[async_trait]
1444pub trait PaymentAuthority: Send + Sync {
1445    async fn execute_machine_payment(
1446        &self,
1447        session_id: SessionId,
1448        request: crate::payment::MachinePaymentRequest,
1449    ) -> Result<crate::payment::MachinePaymentResponse>;
1450}
1451
1452// ============================================================================
1453// SessionCreationAuthority - For detached subagent session creation
1454// ============================================================================
1455
1456/// Host-provided authority for creating detached peer sessions.
1457///
1458/// The host resolves the current session owner and evaluates session-management
1459/// permission. Keeping this outside model-authored arguments prevents a tool
1460/// call from choosing or forging its authorization identity.
1461#[async_trait]
1462pub trait SessionCreationAuthority: Send + Sync {
1463    /// Authorize creation and return the org-validated budget root for the
1464    /// current session. Returning the root from the authority keeps detached
1465    /// chains linked without exposing internal root metadata to model input.
1466    async fn authorize_session_creation(&self, session_id: SessionId) -> Result<SessionId>;
1467}
1468
1469// OutboundToolRateLimiter - Per-org outbound tool-call rate limiting (TM-TOOL-009)
1470// ============================================================================
1471
1472/// Per-org gate on outbound tool execution.
1473///
1474/// Returns `true` if the call is within the per-org budget, `false` if the
1475/// org has exceeded its outbound tool rate limit for this window.
1476/// Implementations must be fail-open: Valkey/backend errors should return `true`
1477/// rather than blocking legitimate tool calls.
1478#[async_trait]
1479pub trait OutboundToolRateLimiter: Send + Sync {
1480    /// Key by the public org UUID (keyed string representation).
1481    async fn check_org(&self, org_id: &crate::typed_id::OrgId) -> bool;
1482}
1483
1484// ============================================================================
1485// DurableToolResultStore — per-tool-call idempotency (EVE-530)
1486// ============================================================================
1487
1488/// Result of a claim attempt on the per-tool-call idempotency store.
1489#[derive(Debug)]
1490pub enum ToolCallClaimResult {
1491    /// First claim for this (turn_id, tool_call_id); caller should execute the tool.
1492    /// `claim_token` must be passed to `settle_tool_call` to verify ownership.
1493    Claimed { claim_token: uuid::Uuid },
1494    /// A prior run already settled this call; replay the stored result.
1495    AlreadySettled {
1496        result_json: serde_json::Value,
1497        args_fingerprint: String,
1498    },
1499    /// A prior run started but never settled. For `AtMostOnce` tools the
1500    /// caller should NOT re-execute; for `Pure`/`Idempotent` tools the caller
1501    /// may re-execute and then try to settle (the settle CAS will be a no-op if
1502    /// a different claimer wins first).
1503    AlreadyRunning { args_fingerprint: String },
1504    /// A settled row exists but its `args_fingerprint` does not match the
1505    /// current call — this is a determinism violation (workflow replay with
1506    /// different inputs). The workflow should be failed loudly.
1507    DeterminismViolation {
1508        stored_fingerprint: String,
1509        current_fingerprint: String,
1510    },
1511}
1512
1513/// Read-only status of a tool call in durable storage (EVE-533).
1514#[derive(Debug, Clone)]
1515pub enum DurableToolCallStatus {
1516    /// Tool completed successfully or with an error; result is stored.
1517    Settled { result_json: serde_json::Value },
1518    /// Tool was settled with `interrupted` status; result may contain error details.
1519    Interrupted {
1520        result_json: Option<serde_json::Value>,
1521    },
1522    /// A claim exists but the tool never finished.
1523    Running,
1524}
1525
1526/// Durable per-tool-call idempotency store (EVE-530).
1527///
1528/// Implements the claim/settle CAS that prevents double-execution of
1529/// `AtMostOnce` tools on worker reclaim/replay.
1530#[async_trait]
1531pub trait DurableToolResultStore: Send + Sync + 'static {
1532    /// Atomically claim `(turn_id, tool_call_id)` before tool dispatch.
1533    ///
1534    /// - Inserts a `running` row if none exists → `Claimed`.
1535    /// - Finds an existing `settled` row → `AlreadySettled`.
1536    /// - Finds an existing `running` row → `AlreadyRunning`.
1537    /// - Finds a `settled` row with a mismatched `args_fingerprint`
1538    ///   (determinism violation) → `DeterminismViolation`.
1539    async fn try_claim_tool_call(
1540        &self,
1541        turn_id: &str,
1542        tool_call_id: &str,
1543        tool_name: &str,
1544        args_fingerprint: &str,
1545    ) -> Result<ToolCallClaimResult>;
1546
1547    /// Settle a previously claimed tool call with its result.
1548    ///
1549    /// `claim_token` must match the token returned by `try_claim_tool_call`.
1550    /// Returns `Ok(true)` if the row was updated, `Ok(false)` if the claim
1551    /// token no longer matches (ownership lost — treat as a warning).
1552    async fn settle_tool_call(
1553        &self,
1554        turn_id: &str,
1555        tool_call_id: &str,
1556        result_json: serde_json::Value,
1557        status: &str,
1558        claim_token: uuid::Uuid,
1559    ) -> Result<bool>;
1560
1561    /// Read-only lookup of a tool call's current status in durable storage (EVE-533).
1562    ///
1563    /// Used by transcript repair to decide whether to replay a stored result or
1564    /// synthesize an interrupted placeholder. Returns `None` if no row exists.
1565    async fn get_tool_call_status(
1566        &self,
1567        turn_id: &str,
1568        tool_call_id: &str,
1569    ) -> Result<Option<DurableToolCallStatus>>;
1570}
1571
1572/// No-op implementation — used when no durable store is configured (dev/test).
1573/// Every call is treated as a fresh first execution; no replay or ownership checks.
1574pub struct NoopDurableToolResultStore;
1575
1576#[async_trait]
1577impl DurableToolResultStore for NoopDurableToolResultStore {
1578    async fn try_claim_tool_call(
1579        &self,
1580        _turn_id: &str,
1581        _tool_call_id: &str,
1582        _tool_name: &str,
1583        _args_fingerprint: &str,
1584    ) -> Result<ToolCallClaimResult> {
1585        Ok(ToolCallClaimResult::Claimed {
1586            claim_token: uuid::Uuid::new_v4(),
1587        })
1588    }
1589
1590    async fn settle_tool_call(
1591        &self,
1592        _turn_id: &str,
1593        _tool_call_id: &str,
1594        _result_json: serde_json::Value,
1595        _status: &str,
1596        _claim_token: uuid::Uuid,
1597    ) -> Result<bool> {
1598        Ok(true)
1599    }
1600
1601    async fn get_tool_call_status(
1602        &self,
1603        _turn_id: &str,
1604        _tool_call_id: &str,
1605    ) -> Result<Option<DurableToolCallStatus>> {
1606        Ok(None)
1607    }
1608}
1609
1610// ============================================================================
1611// StreamHeartbeater — per-stream liveness signal for Reason activity (EVE-531)
1612// ============================================================================
1613
1614/// Progress snapshot carried in each stream heartbeat.
1615#[derive(Debug, Clone)]
1616pub struct StreamProgress {
1617    /// Accumulated text + thinking length (characters) at the time of heartbeat.
1618    pub accumulated_len: usize,
1619    /// Wall-clock time of the most recent received token (Unix seconds).
1620    pub last_delta_at: u64,
1621}
1622
1623/// Heartbeater the Reason streaming loop calls on delta batches and a keepalive
1624/// timer, signalling that the provider connection is alive.
1625///
1626/// Implementations bridge to the durable-execution layer (e.g. gRPC).
1627/// The no-op is used in dev/test where no durable store is present.
1628#[async_trait]
1629pub trait StreamHeartbeater: Send + Sync {
1630    /// Signal stream liveness with current progress.
1631    ///
1632    /// Must be best-effort: errors must not propagate to the caller.
1633    /// Cancel-safety is critical — if the worker dies the heartbeat stops
1634    /// and the existing task-level reclaim takes over.
1635    async fn heartbeat(&self, progress: StreamProgress);
1636}
1637
1638/// No-op heartbeater — treats every stream as perpetually alive (dev/test).
1639pub struct NoopStreamHeartbeater;
1640
1641#[async_trait]
1642impl StreamHeartbeater for NoopStreamHeartbeater {
1643    async fn heartbeat(&self, _progress: StreamProgress) {}
1644}
1645
1646// ============================================================================
1647// PartialStreamStore — partial-stream recovery for Reason activity (EVE-532)
1648// ============================================================================
1649
1650/// State of a partially-streamed assistant message detected in the event log.
1651#[derive(Debug, Clone)]
1652pub struct PartialStreamState {
1653    /// Stable public id from the latest `output.message.started` event.
1654    pub message_id: MessageId,
1655
1656    /// Accumulated text from the last `output.message.delta` for the turn.
1657    /// Empty when `output.message.started` was emitted but no delta arrived.
1658    pub accumulated: String,
1659}
1660
1661/// Consults the persisted event log to detect whether a `reason` activity
1662/// was interrupted after `output.message.started` but before
1663/// `output.message.completed` or `output.message.replaced`.
1664///
1665/// Used by `ReasonAtom` on re-entry to apply the ContinuePartial recovery
1666/// policy (EVE-532): finalize the partial text without a second provider call,
1667/// or restart clean if the partial is unusable.
1668#[async_trait]
1669pub trait PartialStreamStore: Send + Sync {
1670    /// Return the partial-stream state for `(session_id, turn_id)` if an
1671    /// in-flight assistant message exists (started but not completed).
1672    async fn get_partial_stream(
1673        &self,
1674        session_id: SessionId,
1675        turn_id: &str,
1676    ) -> Result<Option<PartialStreamState>>;
1677}
1678
1679/// No-op — always reports no partial stream (dev/test / in-memory mode).
1680pub struct NoopPartialStreamStore;
1681
1682#[async_trait]
1683impl PartialStreamStore for NoopPartialStreamStore {
1684    async fn get_partial_stream(
1685        &self,
1686        _session_id: SessionId,
1687        _turn_id: &str,
1688    ) -> Result<Option<PartialStreamState>> {
1689        Ok(None)
1690    }
1691}
1692
1693/// Runtime-owned services that may be exposed to tools.
1694///
1695/// Tools declare the subset they require through
1696/// [`crate::tools::Tool::required_context_services`]. Runtime hosts validate
1697/// those declarations before advertising tools to the model.
1698#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1699pub enum ToolContextService {
1700    SessionFileSystem,
1701    SessionStorageStore,
1702    ImageArtifactStore,
1703    ProviderCredentialStore,
1704    UtilityLlmService,
1705    McpInvoker,
1706    EgressService,
1707    SessionSqlDbStore,
1708    MessageRetriever,
1709    SessionStore,
1710    SessionMutator,
1711    AgentStore,
1712    ConnectionResolver,
1713    ScheduleStore,
1714    SubagentSessionDelegate,
1715    KnowledgeStore,
1716    KnowledgeIndexSearch,
1717    LeasedResourceStore,
1718    SessionResourceRegistry,
1719    SessionTaskRegistry,
1720    EventEmitter,
1721    CapabilityRegistry,
1722    ToolRegistry,
1723    OrgId,
1724    BudgetChecker,
1725    PaymentAuthority,
1726    SessionCreationAuthority,
1727    SubagentSpawnStore,
1728    ReasoningEffortHandle,
1729}
1730
1731impl ToolContextService {
1732    pub const fn name(self) -> &'static str {
1733        match self {
1734            Self::SessionFileSystem => "SessionFileSystem",
1735            Self::SessionStorageStore => "SessionStorageStore",
1736            Self::ImageArtifactStore => "ImageArtifactStore",
1737            Self::ProviderCredentialStore => "ProviderCredentialStore",
1738            Self::UtilityLlmService => "UtilityLlmService",
1739            Self::McpInvoker => "McpInvoker",
1740            Self::EgressService => "EgressService",
1741            Self::SessionSqlDbStore => "SessionSqlDbStore",
1742            Self::MessageRetriever => "MessageRetriever",
1743            Self::SessionStore => "SessionStore",
1744            Self::SessionMutator => "SessionMutator",
1745            Self::AgentStore => "AgentStore",
1746            Self::ConnectionResolver => "ConnectionResolver",
1747            Self::ScheduleStore => "SessionScheduleStore",
1748            Self::SubagentSessionDelegate => "SubagentSessionDelegate",
1749            Self::KnowledgeStore => "KnowledgeStore",
1750            Self::KnowledgeIndexSearch => "KnowledgeIndexSearch",
1751            Self::LeasedResourceStore => "LeasedResourceStore",
1752            Self::SessionResourceRegistry => "SessionResourceRegistry",
1753            Self::SessionTaskRegistry => "SessionTaskRegistry",
1754            Self::EventEmitter => "EventEmitter",
1755            Self::CapabilityRegistry => "CapabilityRegistry",
1756            Self::ToolRegistry => "ToolRegistry",
1757            Self::OrgId => "OrgId",
1758            Self::BudgetChecker => "BudgetChecker",
1759            Self::PaymentAuthority => "PaymentAuthority",
1760            Self::SessionCreationAuthority => "SessionCreationAuthority",
1761            Self::SubagentSpawnStore => "SubagentSpawnStore",
1762            Self::ReasoningEffortHandle => "ReasoningEffortHandle",
1763        }
1764    }
1765}
1766
1767/// Cloneable snapshot of all services a runtime host makes available to tools.
1768///
1769/// Production act execution constructs each [`ToolContext`] from this bundle;
1770/// per-call metadata is applied afterwards.
1771#[derive(Clone, Default)]
1772pub struct ToolContextServices {
1773    pub file_store: Option<Arc<dyn SessionFileSystem>>,
1774    pub storage_store: Option<Arc<dyn SessionStorageStore>>,
1775    pub image_store: Option<Arc<dyn ImageArtifactStore>>,
1776    pub provider_credential_store: Option<Arc<dyn ProviderCredentialStore>>,
1777    pub utility_llm_service: Option<Arc<dyn crate::UtilityLlmService>>,
1778    pub mcp_invoker: Option<Arc<dyn crate::McpToolInvoker>>,
1779    pub egress_service: Option<Arc<dyn crate::EgressService>>,
1780    pub sqldb_store: Option<SessionSqlDbStoreRef>,
1781    pub message_retriever: Option<Arc<dyn crate::message_retriever::MessageRetriever>>,
1782    pub session_store: Option<Arc<dyn SessionStore>>,
1783    pub session_mutator: Option<Arc<dyn SessionMutator>>,
1784    pub agent_store: Option<Arc<dyn AgentStore>>,
1785    pub connection_resolver: Option<Arc<dyn UserConnectionResolver>>,
1786    pub schedule_store: Option<Arc<dyn SessionScheduleStore>>,
1787    pub subagent_delegate: Option<Arc<dyn crate::subagent_delegation::SubagentSessionDelegate>>,
1788    pub extensions: ToolContextExtensions,
1789    pub knowledge_store: Option<Arc<dyn KnowledgeStore>>,
1790    pub knowledge_index_search: Option<Arc<dyn crate::vector_store::KnowledgeIndexSearch>>,
1791    pub leased_resource_store: Option<Arc<dyn LeasedResourceStore>>,
1792    pub session_resource_registry: Option<Arc<dyn SessionResourceRegistry>>,
1793    pub session_task_registry: Option<Arc<dyn crate::session_task::SessionTaskRegistry>>,
1794    pub event_emitter: Option<Arc<dyn EventEmitter>>,
1795    pub capability_registry: Option<crate::capabilities::CapabilityRegistry>,
1796    pub tool_registry: Option<Arc<crate::tools::ToolRegistry>>,
1797    pub org_id: Option<crate::typed_id::OrgId>,
1798    pub network_access: Option<crate::network_access::NetworkAccessList>,
1799    pub budget_checker: Option<Arc<dyn BudgetChecker>>,
1800    pub payment_authority: Option<Arc<dyn PaymentAuthority>>,
1801    pub session_creation_authority: Option<Arc<dyn SessionCreationAuthority>>,
1802    pub subagent_spawn_store: Option<Arc<dyn SubagentSpawnStore>>,
1803    pub subagent_nesting_policy: SubagentNestingPolicy,
1804    pub reasoning_effort_handle: Option<ReasoningEffortHandle>,
1805}
1806
1807impl ToolContextServices {
1808    pub fn provides(&self, service: ToolContextService) -> bool {
1809        match service {
1810            ToolContextService::SessionFileSystem => self.file_store.is_some(),
1811            ToolContextService::SessionStorageStore => self.storage_store.is_some(),
1812            ToolContextService::ImageArtifactStore => self.image_store.is_some(),
1813            ToolContextService::ProviderCredentialStore => self.provider_credential_store.is_some(),
1814            ToolContextService::UtilityLlmService => self.utility_llm_service.is_some(),
1815            ToolContextService::McpInvoker => self.mcp_invoker.is_some(),
1816            ToolContextService::EgressService => self.egress_service.is_some(),
1817            ToolContextService::SessionSqlDbStore => self.sqldb_store.is_some(),
1818            ToolContextService::MessageRetriever => self.message_retriever.is_some(),
1819            ToolContextService::SessionStore => self.session_store.is_some(),
1820            ToolContextService::SessionMutator => self.session_mutator.is_some(),
1821            ToolContextService::AgentStore => self.agent_store.is_some(),
1822            ToolContextService::ConnectionResolver => self.connection_resolver.is_some(),
1823            ToolContextService::ScheduleStore => self.schedule_store.is_some(),
1824            ToolContextService::SubagentSessionDelegate => self.subagent_delegate.is_some(),
1825            ToolContextService::KnowledgeStore => self.knowledge_store.is_some(),
1826            ToolContextService::KnowledgeIndexSearch => self.knowledge_index_search.is_some(),
1827            ToolContextService::LeasedResourceStore => self.leased_resource_store.is_some(),
1828            ToolContextService::SessionResourceRegistry => self.session_resource_registry.is_some(),
1829            ToolContextService::SessionTaskRegistry => self.session_task_registry.is_some(),
1830            ToolContextService::EventEmitter => self.event_emitter.is_some(),
1831            ToolContextService::CapabilityRegistry => self.capability_registry.is_some(),
1832            ToolContextService::ToolRegistry => self.tool_registry.is_some(),
1833            ToolContextService::OrgId => self.org_id.is_some(),
1834            ToolContextService::BudgetChecker => self.budget_checker.is_some(),
1835            ToolContextService::PaymentAuthority => self.payment_authority.is_some(),
1836            ToolContextService::SessionCreationAuthority => {
1837                self.session_creation_authority.is_some()
1838            }
1839            ToolContextService::SubagentSpawnStore => self.subagent_spawn_store.is_some(),
1840            ToolContextService::ReasoningEffortHandle => self.reasoning_effort_handle.is_some(),
1841        }
1842    }
1843}
1844
1845/// Type-erased bag of host-supplied extension services carried on a
1846/// [`ToolContext`]. Crates layered above core (e.g. `everruns-platform`) insert
1847/// a concrete wrapper type and resolve it by type, so core need not name the
1848/// hosted service (EVE-839).
1849#[derive(Clone, Default)]
1850pub struct ToolContextExtensions {
1851    values: Arc<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>,
1852}
1853
1854impl ToolContextExtensions {
1855    /// Insert (or replace) an extension keyed by its concrete type.
1856    pub fn insert<T: Any + Send + Sync>(&mut self, value: Arc<T>) {
1857        Arc::make_mut(&mut self.values).insert(TypeId::of::<T>(), value);
1858    }
1859
1860    /// Resolve an extension by its concrete type.
1861    pub fn get<T: Any + Send + Sync>(&self) -> Option<Arc<T>> {
1862        self.values
1863            .get(&TypeId::of::<T>())
1864            .and_then(|value| value.clone().downcast::<T>().ok())
1865    }
1866
1867    pub fn is_empty(&self) -> bool {
1868        self.values.is_empty()
1869    }
1870}
1871
1872impl std::fmt::Debug for ToolContextExtensions {
1873    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1874        f.debug_struct("ToolContextExtensions")
1875            .field("len", &self.values.len())
1876            .finish()
1877    }
1878}
1879
1880/// Runtime context provided to tools during execution.
1881///
1882/// This context contains:
1883/// - Session ID for scoping operations
1884/// - Optional stores for tools that need external access
1885///
1886/// Tools that need context-aware execution (like filesystem tools) can use
1887/// the `execute_with_context` method on the Tool trait.
1888#[derive(Clone)]
1889pub struct ToolContext {
1890    /// The session ID for the current execution
1891    pub session_id: SessionId,
1892    /// The workspace this session is attached to — the key for the virtual
1893    /// file store. For the default 1:1 session this equals
1894    /// `WorkspaceId::from_uuid(session_id.uuid())`; for a shared workspace it
1895    /// differs. File-system tools MUST key by this (via `workspace_fs_key`)
1896    /// rather than `session_id` so shared-workspace sessions read/write the
1897    /// attached workspace's files. See knowledge/runtime-resources/workspace.md.
1898    pub workspace_id: WorkspaceId,
1899
1900    /// Optional file store for filesystem operations
1901    pub file_store: Option<Arc<dyn SessionFileSystem>>,
1902
1903    /// Optional storage store for key/value and secret storage
1904    pub storage_store: Option<Arc<dyn SessionStorageStore>>,
1905
1906    /// Optional durable image artifact store for tool-side media persistence.
1907    pub image_store: Option<Arc<dyn ImageArtifactStore>>,
1908
1909    /// Optional provider credential store for tool-side API clients.
1910    pub provider_credential_store: Option<Arc<dyn ProviderCredentialStore>>,
1911
1912    /// Optional system utility LLM service for capability internals.
1913    pub utility_llm_service: Option<Arc<dyn crate::UtilityLlmService>>,
1914
1915    /// Optional scoped-MCP tool invoker for capability internals that need to
1916    /// call an MCP server out-of-band (e.g. the guardrails `mcp` check
1917    /// delegating a decision to an external guardrail endpoint). The invoker
1918    /// resolves connections and credentials per the current session/org, so
1919    /// tenant scoping is enforced by the host that supplies it.
1920    pub mcp_invoker: Option<Arc<dyn crate::McpToolInvoker>>,
1921
1922    /// Optional outbound egress service for HTTP/API traffic.
1923    pub egress_service: Option<Arc<dyn crate::EgressService>>,
1924
1925    /// Optional session SQL database store
1926    pub sqldb_store: Option<SessionSqlDbStoreRef>,
1927
1928    /// Optional message retriever for tools that need conversation history access
1929    pub message_retriever: Option<Arc<dyn crate::message_retriever::MessageRetriever>>,
1930
1931    /// Optional session store for tools that need session metadata access.
1932    pub session_store: Option<Arc<dyn SessionStore>>,
1933
1934    /// Optional session mutator for tools that need to update session metadata.
1935    pub session_mutator: Option<Arc<dyn SessionMutator>>,
1936
1937    /// Optional agent store for tools that need agent metadata access.
1938    pub agent_store: Option<Arc<dyn AgentStore>>,
1939
1940    /// Optional resolver for user connection tokens (lazy GitHub token lookup, etc.)
1941    pub connection_resolver: Option<Arc<dyn UserConnectionResolver>>,
1942
1943    /// Optional session schedule store for scheduling tools.
1944    pub schedule_store: Option<Arc<dyn SessionScheduleStore>>,
1945
1946    /// Optional narrow child-session delegate for portable subagent/handoff
1947    /// orchestration (EVE-839). The hosted `PlatformStore` seam itself is no
1948    /// longer named by core; the platform crate implements this delegate and,
1949    /// for its own management tools, is resolved via [`Self::extension`].
1950    pub subagent_delegate: Option<Arc<dyn crate::subagent_delegation::SubagentSessionDelegate>>,
1951    /// Type-erased, host-supplied extensions keyed by concrete type. Lets crates
1952    /// layered above core (e.g. `everruns-platform`) hang typed services on the
1953    /// tool context without core naming them (EVE-839).
1954    pub extensions: ToolContextExtensions,
1955    /// Optional knowledge store backing the `search_knowledge` tool.
1956    pub knowledge_store: Option<Arc<dyn KnowledgeStore>>,
1957
1958    /// Optional hybrid retrieval over bound Knowledge Indexes for the
1959    /// `search_index` tool. Server-implemented; populated only on the server
1960    /// act path alongside `platform_store` / `connection_resolver`.
1961    pub knowledge_index_search: Option<Arc<dyn crate::vector_store::KnowledgeIndexSearch>>,
1962
1963    /// Optional leased resource store for lifecycle-managed provider resources.
1964    pub leased_resource_store: Option<Arc<dyn LeasedResourceStore>>,
1965
1966    /// Optional session resource registry — generic registry of active resources.
1967    pub session_resource_registry: Option<Arc<dyn SessionResourceRegistry>>,
1968
1969    /// Optional session task registry — background work owned by the session
1970    /// (knowledge/runtime-resources/session-tasks.md).
1971    pub session_task_registry: Option<Arc<dyn crate::session_task::SessionTaskRegistry>>,
1972
1973    /// Optional event emitter for tools that need to stream progress updates.
1974    /// When set, tools can emit `tool.progress` events during execution.
1975    pub event_emitter: Option<Arc<dyn EventEmitter>>,
1976
1977    /// Event context for correlating progress events with the current tool call.
1978    /// Set by ActAtom when constructing the ToolContext.
1979    pub event_context: Option<crate::events::EventContext>,
1980
1981    /// The tool call ID for the current execution (set by ActAtom).
1982    /// Used by tools to emit correlated progress events.
1983    pub tool_call_id: Option<String>,
1984    /// Optional capability registry for blueprint lookups.
1985    pub capability_registry: Option<crate::capabilities::CapabilityRegistry>,
1986
1987    /// Optional registry of active built-in tools for meta-tools such as
1988    /// `spawn_background` that need to inspect or delegate to sibling tools.
1989    pub tool_registry: Option<Arc<crate::tools::ToolRegistry>>,
1990
1991    /// Optional allowlist of tools visible to the model for this turn.
1992    /// Registry-introspecting tools must filter through this before returning
1993    /// sibling tool metadata, because the execution registry can be a superset.
1994    pub visible_tool_names: Option<Arc<HashSet<String>>>,
1995
1996    /// Optional org ID for org-scoped operations.
1997    pub org_id: Option<crate::typed_id::OrgId>,
1998
1999    /// Merged network access list (harness ∩ agent ∩ session).
2000    /// When set, tools that make HTTP requests must check URLs against this list.
2001    pub network_access: Option<crate::network_access::NetworkAccessList>,
2002
2003    /// Resolved locale for localized tool behavior (BCP 47, e.g. `uk-UA`).
2004    /// When set, tools that support localization use this to produce
2005    /// locale-appropriate descriptions, error messages, and prompts.
2006    pub locale: Option<String>,
2007
2008    /// Optional budget checker for the check_budget tool.
2009    pub budget_checker: Option<Arc<dyn BudgetChecker>>,
2010
2011    /// Optional internal payment authority for paid capability tools.
2012    pub payment_authority: Option<Arc<dyn PaymentAuthority>>,
2013
2014    /// Optional host authority for detached peer-session creation.
2015    pub session_creation_authority: Option<Arc<dyn SessionCreationAuthority>>,
2016
2017    /// Optional durable spawn handle store for subagent reattach (EVE-535).
2018    /// When set, subagent delegation uses claim/settle to prevent duplicate spawning
2019    /// on parent worker reclaim.
2020    pub subagent_spawn_store: Option<Arc<dyn SubagentSpawnStore>>,
2021
2022    /// Resolved subagent nesting policy for this turn.
2023    pub subagent_nesting_policy: SubagentNestingPolicy,
2024
2025    /// Optional live reasoning-effort handle (EVE-595). When set, a tool can
2026    /// change the reasoning effort mid-turn; subsequent LLM steps in the same
2027    /// `run_turn` re-read it and use the new effort.
2028    pub reasoning_effort_handle: Option<ReasoningEffortHandle>,
2029
2030    /// Cooperative cancellation for this tool call.
2031    ///
2032    /// The act atom cancels it when the call ends *by any means* — the turn was
2033    /// cancelled, the act future was dropped, or the tool simply returned. A
2034    /// tool that only awaits inside its own future needs nothing here: dropping
2035    /// the future already stops it. This exists for work a tool starts and
2036    /// leaves running — a child process, a detached watcher task, a prompt
2037    /// waiting on an answer — which would otherwise outlive the call that
2038    /// created it. Clone the token into that work and let it die with the call.
2039    pub cancellation: Option<tokio_util::sync::CancellationToken>,
2040}
2041
2042impl ToolContext {
2043    /// The virtual-file-store key for this execution, derived from the attached
2044    /// workspace. Carried through the `SessionFileSystem` trait's `SessionId`
2045    /// parameter (the store keys by `.uuid()`), so a shared-workspace session
2046    /// addresses the workspace's files rather than its own session-id keyspace.
2047    pub fn workspace_fs_key(&self) -> SessionId {
2048        SessionId::from_uuid(self.workspace_id.uuid())
2049    }
2050
2051    /// Override the attached workspace (default is the 1:1 session-derived id).
2052    pub fn with_workspace_id(mut self, workspace_id: WorkspaceId) -> Self {
2053        self.workspace_id = workspace_id;
2054        self
2055    }
2056
2057    /// Create a new tool context with just a session ID
2058    pub fn new(session_id: SessionId) -> Self {
2059        Self {
2060            session_id,
2061            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
2062            file_store: None,
2063            storage_store: None,
2064            image_store: None,
2065            provider_credential_store: None,
2066            utility_llm_service: None,
2067            mcp_invoker: None,
2068            egress_service: None,
2069            sqldb_store: None,
2070            message_retriever: None,
2071            session_store: None,
2072            session_mutator: None,
2073            agent_store: None,
2074            connection_resolver: None,
2075            schedule_store: None,
2076            subagent_delegate: None,
2077            extensions: ToolContextExtensions::default(),
2078            knowledge_store: None,
2079            knowledge_index_search: None,
2080            leased_resource_store: None,
2081            session_resource_registry: None,
2082            session_task_registry: None,
2083            event_emitter: None,
2084            event_context: None,
2085            tool_call_id: None,
2086            capability_registry: None,
2087            tool_registry: None,
2088            visible_tool_names: None,
2089            org_id: None,
2090            network_access: None,
2091            locale: None,
2092            budget_checker: None,
2093            payment_authority: None,
2094            session_creation_authority: None,
2095            subagent_spawn_store: None,
2096            subagent_nesting_policy: SubagentNestingPolicy::default(),
2097            reasoning_effort_handle: None,
2098            cancellation: None,
2099        }
2100    }
2101
2102    /// Construct a production tool context from a validated runtime service snapshot.
2103    pub fn from_services(session_id: SessionId, services: &ToolContextServices) -> Self {
2104        Self {
2105            session_id,
2106            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
2107            file_store: services.file_store.clone(),
2108            storage_store: services.storage_store.clone(),
2109            image_store: services.image_store.clone(),
2110            provider_credential_store: services.provider_credential_store.clone(),
2111            utility_llm_service: services.utility_llm_service.clone(),
2112            mcp_invoker: services.mcp_invoker.clone(),
2113            egress_service: services.egress_service.clone(),
2114            sqldb_store: services.sqldb_store.clone(),
2115            message_retriever: services.message_retriever.clone(),
2116            session_store: services.session_store.clone(),
2117            session_mutator: services.session_mutator.clone(),
2118            agent_store: services.agent_store.clone(),
2119            connection_resolver: services.connection_resolver.clone(),
2120            schedule_store: services.schedule_store.clone(),
2121            subagent_delegate: services.subagent_delegate.clone(),
2122            extensions: services.extensions.clone(),
2123            knowledge_store: services.knowledge_store.clone(),
2124            knowledge_index_search: services.knowledge_index_search.clone(),
2125            leased_resource_store: services.leased_resource_store.clone(),
2126            session_resource_registry: services.session_resource_registry.clone(),
2127            session_task_registry: services.session_task_registry.clone(),
2128            event_emitter: services.event_emitter.clone(),
2129            event_context: None,
2130            tool_call_id: None,
2131            capability_registry: services.capability_registry.clone(),
2132            tool_registry: services.tool_registry.clone(),
2133            visible_tool_names: None,
2134            org_id: services.org_id,
2135            network_access: services.network_access.clone(),
2136            locale: None,
2137            budget_checker: services.budget_checker.clone(),
2138            payment_authority: services.payment_authority.clone(),
2139            session_creation_authority: services.session_creation_authority.clone(),
2140            subagent_spawn_store: services.subagent_spawn_store.clone(),
2141            subagent_nesting_policy: services.subagent_nesting_policy,
2142            reasoning_effort_handle: services.reasoning_effort_handle.clone(),
2143            cancellation: None,
2144        }
2145    }
2146
2147    /// Create a context with a file store
2148    pub fn with_file_store(session_id: SessionId, file_store: Arc<dyn SessionFileSystem>) -> Self {
2149        Self {
2150            session_id,
2151            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
2152            file_store: Some(file_store),
2153            storage_store: None,
2154            image_store: None,
2155            provider_credential_store: None,
2156            utility_llm_service: None,
2157            mcp_invoker: None,
2158            egress_service: None,
2159            sqldb_store: None,
2160            message_retriever: None,
2161            session_store: None,
2162            session_mutator: None,
2163            agent_store: None,
2164            connection_resolver: None,
2165            schedule_store: None,
2166            subagent_delegate: None,
2167            extensions: ToolContextExtensions::default(),
2168            knowledge_store: None,
2169            knowledge_index_search: None,
2170            leased_resource_store: None,
2171            session_resource_registry: None,
2172            session_task_registry: None,
2173            event_emitter: None,
2174            event_context: None,
2175            tool_call_id: None,
2176            capability_registry: None,
2177            tool_registry: None,
2178            visible_tool_names: None,
2179            org_id: None,
2180            network_access: None,
2181            locale: None,
2182            budget_checker: None,
2183            payment_authority: None,
2184            session_creation_authority: None,
2185            subagent_spawn_store: None,
2186            subagent_nesting_policy: SubagentNestingPolicy::default(),
2187            reasoning_effort_handle: None,
2188            cancellation: None,
2189        }
2190    }
2191
2192    /// Create a context with a storage store
2193    pub fn with_storage_store(
2194        session_id: SessionId,
2195        storage_store: Arc<dyn SessionStorageStore>,
2196    ) -> Self {
2197        Self {
2198            session_id,
2199            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
2200            file_store: None,
2201            storage_store: Some(storage_store),
2202            image_store: None,
2203            provider_credential_store: None,
2204            utility_llm_service: None,
2205            mcp_invoker: None,
2206            egress_service: None,
2207            sqldb_store: None,
2208            message_retriever: None,
2209            session_store: None,
2210            session_mutator: None,
2211            agent_store: None,
2212            connection_resolver: None,
2213            schedule_store: None,
2214            subagent_delegate: None,
2215            extensions: ToolContextExtensions::default(),
2216            knowledge_store: None,
2217            knowledge_index_search: None,
2218            leased_resource_store: None,
2219            session_resource_registry: None,
2220            session_task_registry: None,
2221            event_emitter: None,
2222            event_context: None,
2223            tool_call_id: None,
2224            capability_registry: None,
2225            tool_registry: None,
2226            visible_tool_names: None,
2227            org_id: None,
2228            network_access: None,
2229            locale: None,
2230            budget_checker: None,
2231            payment_authority: None,
2232            session_creation_authority: None,
2233            subagent_spawn_store: None,
2234            subagent_nesting_policy: SubagentNestingPolicy::default(),
2235            reasoning_effort_handle: None,
2236            cancellation: None,
2237        }
2238    }
2239
2240    /// Create a context with both file store and storage store
2241    pub fn with_stores(
2242        session_id: SessionId,
2243        file_store: Arc<dyn SessionFileSystem>,
2244        storage_store: Arc<dyn SessionStorageStore>,
2245    ) -> Self {
2246        Self {
2247            session_id,
2248            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
2249            file_store: Some(file_store),
2250            storage_store: Some(storage_store),
2251            sqldb_store: None,
2252            image_store: None,
2253            provider_credential_store: None,
2254            utility_llm_service: None,
2255            mcp_invoker: None,
2256            egress_service: None,
2257            message_retriever: None,
2258            session_store: None,
2259            session_mutator: None,
2260            agent_store: None,
2261            connection_resolver: None,
2262            schedule_store: None,
2263            subagent_delegate: None,
2264            extensions: ToolContextExtensions::default(),
2265            knowledge_store: None,
2266            knowledge_index_search: None,
2267            leased_resource_store: None,
2268            session_resource_registry: None,
2269            session_task_registry: None,
2270            event_emitter: None,
2271            event_context: None,
2272            tool_call_id: None,
2273            capability_registry: None,
2274            tool_registry: None,
2275            visible_tool_names: None,
2276            org_id: None,
2277            network_access: None,
2278            locale: None,
2279            budget_checker: None,
2280            payment_authority: None,
2281            session_creation_authority: None,
2282            subagent_spawn_store: None,
2283            subagent_nesting_policy: SubagentNestingPolicy::default(),
2284            reasoning_effort_handle: None,
2285            cancellation: None,
2286        }
2287    }
2288
2289    /// Add a SQL database store to this context
2290    pub fn with_sqldb_store(mut self, sqldb_store: SessionSqlDbStoreRef) -> Self {
2291        self.sqldb_store = Some(sqldb_store);
2292        self
2293    }
2294
2295    /// Add a message retriever to this context
2296    pub fn with_message_retriever(
2297        mut self,
2298        retriever: Arc<dyn crate::message_retriever::MessageRetriever>,
2299    ) -> Self {
2300        self.message_retriever = Some(retriever);
2301        self
2302    }
2303
2304    /// Add a session store to this context.
2305    pub fn with_session_store(mut self, store: Arc<dyn SessionStore>) -> Self {
2306        self.session_store = Some(store);
2307        self
2308    }
2309
2310    /// Add a session mutator to this context.
2311    pub fn with_session_mutator(mut self, mutator: Arc<dyn SessionMutator>) -> Self {
2312        self.session_mutator = Some(mutator);
2313        self
2314    }
2315
2316    /// Add a live reasoning-effort handle (EVE-595). Tools can call
2317    /// [`ReasoningEffortHandle::set`] on it to change the effort used by
2318    /// subsequent LLM steps within the same turn.
2319    /// Attach a cancellation token to this context (see
2320    /// [`ToolContext::cancellation`]).
2321    pub fn with_cancellation(mut self, token: tokio_util::sync::CancellationToken) -> Self {
2322        self.cancellation = Some(token);
2323        self
2324    }
2325
2326    /// Whether this call has already been cancelled. A context with no token
2327    /// never reports cancellation.
2328    pub fn is_cancelled(&self) -> bool {
2329        self.cancellation
2330            .as_ref()
2331            .is_some_and(|token| token.is_cancelled())
2332    }
2333
2334    pub fn with_reasoning_effort_handle(mut self, handle: ReasoningEffortHandle) -> Self {
2335        self.reasoning_effort_handle = Some(handle);
2336        self
2337    }
2338
2339    /// Add an agent store to this context.
2340    pub fn with_agent_store(mut self, store: Arc<dyn AgentStore>) -> Self {
2341        self.agent_store = Some(store);
2342        self
2343    }
2344
2345    /// Add a connection resolver to this context
2346    pub fn with_connection_resolver(mut self, resolver: Arc<dyn UserConnectionResolver>) -> Self {
2347        self.connection_resolver = Some(resolver);
2348        self
2349    }
2350
2351    /// Create a context with an image artifact store.
2352    pub fn with_image_store(
2353        session_id: SessionId,
2354        image_store: Arc<dyn ImageArtifactStore>,
2355    ) -> Self {
2356        Self {
2357            session_id,
2358            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
2359            file_store: None,
2360            storage_store: None,
2361            image_store: Some(image_store),
2362            provider_credential_store: None,
2363            utility_llm_service: None,
2364            mcp_invoker: None,
2365            egress_service: None,
2366            sqldb_store: None,
2367            message_retriever: None,
2368            session_store: None,
2369            session_mutator: None,
2370            agent_store: None,
2371            connection_resolver: None,
2372            schedule_store: None,
2373            subagent_delegate: None,
2374            extensions: ToolContextExtensions::default(),
2375            knowledge_store: None,
2376            knowledge_index_search: None,
2377            leased_resource_store: None,
2378            session_resource_registry: None,
2379            session_task_registry: None,
2380            event_emitter: None,
2381            event_context: None,
2382            tool_call_id: None,
2383            capability_registry: None,
2384            tool_registry: None,
2385            visible_tool_names: None,
2386            org_id: None,
2387            network_access: None,
2388            locale: None,
2389            budget_checker: None,
2390            payment_authority: None,
2391            session_creation_authority: None,
2392            subagent_spawn_store: None,
2393            subagent_nesting_policy: SubagentNestingPolicy::default(),
2394            reasoning_effort_handle: None,
2395            cancellation: None,
2396        }
2397    }
2398
2399    /// Set the provider credential store on this context.
2400    pub fn with_provider_credential_store(
2401        mut self,
2402        store: Arc<dyn ProviderCredentialStore>,
2403    ) -> Self {
2404        self.provider_credential_store = Some(store);
2405        self
2406    }
2407
2408    /// Set the utility LLM service on this context.
2409    pub fn with_utility_llm_service(mut self, service: Arc<dyn crate::UtilityLlmService>) -> Self {
2410        self.utility_llm_service = Some(service);
2411        self
2412    }
2413
2414    /// Set the scoped-MCP tool invoker on this context.
2415    pub fn with_mcp_invoker(mut self, invoker: Arc<dyn crate::McpToolInvoker>) -> Self {
2416        self.mcp_invoker = Some(invoker);
2417        self
2418    }
2419
2420    /// Set the outbound egress service on this context.
2421    pub fn with_egress_service(mut self, service: Arc<dyn crate::EgressService>) -> Self {
2422        self.egress_service = Some(service);
2423        self
2424    }
2425
2426    /// Set the outbound egress service on this context when available.
2427    /// Preserves any already-set service when `service` is `None`.
2428    pub fn with_egress_service_opt(
2429        mut self,
2430        service: Option<Arc<dyn crate::EgressService>>,
2431    ) -> Self {
2432        if let Some(service) = service {
2433            self.egress_service = Some(service);
2434        }
2435        self
2436    }
2437
2438    /// Set the session storage store on this context (builder method).
2439    pub fn with_storage_store_arc(mut self, store: Arc<dyn SessionStorageStore>) -> Self {
2440        self.storage_store = Some(store);
2441        self
2442    }
2443
2444    /// Add a session schedule store to this context.
2445    pub fn with_schedule_store(mut self, store: Arc<dyn SessionScheduleStore>) -> Self {
2446        self.schedule_store = Some(store);
2447        self
2448    }
2449
2450    /// Add a narrow child-session delegate to this context (EVE-839).
2451    pub fn with_subagent_delegate(
2452        mut self,
2453        delegate: Arc<dyn crate::subagent_delegation::SubagentSessionDelegate>,
2454    ) -> Self {
2455        self.subagent_delegate = Some(delegate);
2456        self
2457    }
2458
2459    /// Insert a type-keyed host extension (e.g. `everruns-platform`'s
2460    /// `PlatformStoreExt`) and return the updated context.
2461    pub fn with_extension<T: std::any::Any + Send + Sync>(mut self, value: Arc<T>) -> Self {
2462        self.extensions.insert(value);
2463        self
2464    }
2465
2466    /// Resolve a type-keyed host extension previously inserted via
2467    /// [`Self::with_extension`].
2468    pub fn extension<T: std::any::Any + Send + Sync>(&self) -> Option<Arc<T>> {
2469        self.extensions.get::<T>()
2470    }
2471
2472    /// Add a Knowledge Index search service to this context (for `search_index`).
2473    pub fn with_knowledge_index_search(
2474        mut self,
2475        search: Arc<dyn crate::vector_store::KnowledgeIndexSearch>,
2476    ) -> Self {
2477        self.knowledge_index_search = Some(search);
2478        self
2479    }
2480
2481    /// Add a leased resource store to this context.
2482    pub fn with_leased_resource_store(mut self, store: Arc<dyn LeasedResourceStore>) -> Self {
2483        self.leased_resource_store = Some(store);
2484        self
2485    }
2486
2487    /// Add a session resource registry to this context.
2488    pub fn with_session_resource_registry(
2489        mut self,
2490        registry: Arc<dyn SessionResourceRegistry>,
2491    ) -> Self {
2492        self.session_resource_registry = Some(registry);
2493        self
2494    }
2495
2496    /// Add a session task registry to this context.
2497    pub fn with_session_task_registry(
2498        mut self,
2499        registry: Arc<dyn crate::session_task::SessionTaskRegistry>,
2500    ) -> Self {
2501        self.session_task_registry = Some(registry);
2502        self
2503    }
2504
2505    /// Set org ID for org-scoped operations.
2506    pub fn with_org_id(mut self, org_id: crate::typed_id::OrgId) -> Self {
2507        self.org_id = Some(org_id);
2508        self
2509    }
2510
2511    /// Set the active built-in tool registry on this context.
2512    pub fn with_tool_registry(mut self, registry: Arc<crate::tools::ToolRegistry>) -> Self {
2513        self.tool_registry = Some(registry);
2514        self
2515    }
2516
2517    /// Set the tool names visible to the model in this turn.
2518    pub fn with_visible_tool_names(mut self, names: Arc<HashSet<String>>) -> Self {
2519        self.visible_tool_names = Some(names);
2520        self
2521    }
2522
2523    /// Set the merged network access list for URL filtering.
2524    pub fn with_network_access(
2525        mut self,
2526        network_access: Option<crate::network_access::NetworkAccessList>,
2527    ) -> Self {
2528        self.network_access = network_access;
2529        self
2530    }
2531
2532    /// Set the internal payment authority for paid capability operations.
2533    pub fn with_payment_authority(mut self, authority: Arc<dyn PaymentAuthority>) -> Self {
2534        self.payment_authority = Some(authority);
2535        self
2536    }
2537
2538    /// Set the durable subagent spawn handle store (EVE-535).
2539    pub fn with_subagent_spawn_store(mut self, store: Arc<dyn SubagentSpawnStore>) -> Self {
2540        self.subagent_spawn_store = Some(store);
2541        self
2542    }
2543
2544    /// Set the resolved subagent nesting policy.
2545    pub fn with_subagent_nesting_policy(mut self, policy: SubagentNestingPolicy) -> Self {
2546        self.subagent_nesting_policy = policy;
2547        self
2548    }
2549
2550    /// Emit a `tool.progress` event if an event emitter and context are available.
2551    ///
2552    /// This is a best-effort helper: failures are logged but not propagated,
2553    /// so tools never fail just because a progress event couldn't be sent.
2554    pub async fn emit_progress(&self, tool_name: &str, message: &str) {
2555        let (Some(emitter), Some(ctx), Some(call_id)) =
2556            (&self.event_emitter, &self.event_context, &self.tool_call_id)
2557        else {
2558            return;
2559        };
2560        if let Err(e) = emitter
2561            .emit(EventRequest::new(
2562                self.session_id,
2563                ctx.clone(),
2564                crate::events::ToolProgressData {
2565                    tool_call_id: call_id.clone(),
2566                    tool_name: tool_name.to_string(),
2567                    message: message.to_string(),
2568                    display_name: None,
2569                },
2570            ))
2571            .await
2572        {
2573            tracing::debug!(
2574                tool_call_id = call_id,
2575                tool_name,
2576                error = %e,
2577                "Failed to emit tool.progress event"
2578            );
2579        }
2580    }
2581
2582    /// Emit a `tool.output.delta` event if an event emitter and context are available.
2583    ///
2584    /// Streams incremental output chunks (e.g., stdout/stderr lines) for live
2585    /// rendering in UI and CLI. Best-effort: failures are logged, not propagated.
2586    pub async fn emit_tool_output(&self, tool_name: &str, delta: &str, stream: &str) {
2587        let (Some(emitter), Some(ctx), Some(call_id)) =
2588            (&self.event_emitter, &self.event_context, &self.tool_call_id)
2589        else {
2590            return;
2591        };
2592        if let Err(e) = emitter
2593            .emit(EventRequest::new(
2594                self.session_id,
2595                ctx.clone(),
2596                crate::events::ToolOutputDeltaData {
2597                    tool_call_id: call_id.clone(),
2598                    tool_name: tool_name.to_string(),
2599                    delta: delta.to_string(),
2600                    stream: stream.to_string(),
2601                },
2602            ))
2603            .await
2604        {
2605            tracing::debug!(
2606                tool_call_id = call_id,
2607                tool_name,
2608                error = %e,
2609                "Failed to emit tool.output.delta event"
2610            );
2611        }
2612    }
2613}
2614
2615impl std::fmt::Debug for ToolContext {
2616    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2617        f.debug_struct("ToolContext")
2618            .field("session_id", &self.session_id)
2619            .field("file_store", &self.file_store.is_some())
2620            .field("storage_store", &self.storage_store.is_some())
2621            .field("image_store", &self.image_store.is_some())
2622            .field(
2623                "provider_credential_store",
2624                &self.provider_credential_store.is_some(),
2625            )
2626            .field("utility_llm_service", &self.utility_llm_service.is_some())
2627            .field("egress_service", &self.egress_service.is_some())
2628            .field("sqldb_store", &self.sqldb_store.is_some())
2629            .field("message_retriever", &self.message_retriever.is_some())
2630            .field("session_store", &self.session_store.is_some())
2631            .field("session_mutator", &self.session_mutator.is_some())
2632            .field("agent_store", &self.agent_store.is_some())
2633            .field("connection_resolver", &self.connection_resolver.is_some())
2634            .field("schedule_store", &self.schedule_store.is_some())
2635            .field("subagent_delegate", &self.subagent_delegate.is_some())
2636            .field(
2637                "knowledge_index_search",
2638                &self.knowledge_index_search.is_some(),
2639            )
2640            .field(
2641                "leased_resource_store",
2642                &self.leased_resource_store.is_some(),
2643            )
2644            .field("event_emitter", &self.event_emitter.is_some())
2645            .field("tool_registry", &self.tool_registry.is_some())
2646            .field("payment_authority", &self.payment_authority.is_some())
2647            .field("subagent_spawn_store", &self.subagent_spawn_store.is_some())
2648            .field("subagent_nesting_policy", &self.subagent_nesting_policy)
2649            .field("org_id", &self.org_id)
2650            .finish()
2651    }
2652}
2653
2654// ============================================================================
2655// EventEmitter - For emitting events
2656// ============================================================================
2657
2658use crate::events::{Event, EventRequest};
2659
2660/// Trait for emitting events following the standard event protocol
2661///
2662/// Implementations can:
2663/// - Store events in a database
2664/// - Keep events in memory for testing
2665/// - Stream events via SSE/WebSocket
2666/// - Log events for debugging
2667///
2668/// Events follow a consistent schema: id, type, ts, context, data.
2669/// See knowledge/execution/events.md for the full event protocol specification.
2670#[async_trait]
2671pub trait EventEmitter: Send + Sync {
2672    /// Emit an event request
2673    ///
2674    /// Takes an EventRequest (without id/sequence) and returns the stored Event
2675    /// with id and sequence assigned by the storage layer.
2676    async fn emit(&self, request: EventRequest) -> Result<Event>;
2677}
2678
2679/// Blanket impl: `Arc<E>` delegates to the inner emitter.
2680#[async_trait]
2681impl<E: EventEmitter + ?Sized> EventEmitter for Arc<E> {
2682    async fn emit(&self, request: EventRequest) -> Result<Event> {
2683        (**self).emit(request).await
2684    }
2685}
2686
2687/// No-op event emitter for when event emission is not needed
2688///
2689/// This is useful for testing or when event observability is disabled.
2690#[derive(Debug, Clone, Default)]
2691pub struct NoopEventEmitter;
2692
2693#[async_trait]
2694impl EventEmitter for NoopEventEmitter {
2695    async fn emit(&self, request: EventRequest) -> Result<Event> {
2696        // Return a dummy event with sequence 0
2697        Ok(request.into_event(crate::typed_id::EventId::new(), 0))
2698    }
2699}
2700
2701// Note: EventListener trait has been moved to event_listeners.rs module.
2702// Use `everruns_core::EventListener` or `everruns_core::event_listeners::EventListener`.
2703
2704// ============================================================================
2705// ImageResolver - For resolving image_file content to actual image data
2706// ============================================================================
2707
2708/// Resolved image data for LLM consumption
2709///
2710/// This struct contains the actual image data in a format suitable for
2711/// sending to LLM providers. Both OpenAI and Anthropic accept base64-encoded
2712/// images with media type information.
2713#[derive(Debug, Clone)]
2714pub struct ResolvedImage {
2715    /// Base64-encoded image data (without data URL prefix)
2716    pub base64: String,
2717    /// MIME type (e.g., "image/png", "image/jpeg")
2718    pub media_type: String,
2719}
2720
2721impl ResolvedImage {
2722    /// Create a new resolved image
2723    pub fn new(base64: impl Into<String>, media_type: impl Into<String>) -> Self {
2724        Self {
2725            base64: base64.into(),
2726            media_type: media_type.into(),
2727        }
2728    }
2729
2730    /// Convert to a data URL suitable for OpenAI Vision API
2731    ///
2732    /// Format: `data:{media_type};base64,{base64_data}`
2733    pub fn to_data_url(&self) -> String {
2734        format!("data:{};base64,{}", self.media_type, self.base64)
2735    }
2736}
2737
2738/// Trait for resolving image_file content parts to actual image data
2739///
2740/// When building LLM messages, `image_file` content parts contain only
2741/// a reference (UUID) to an uploaded image. This trait allows resolving
2742/// those references to actual image data.
2743///
2744/// # Provider-specific formatting
2745///
2746/// The resolved image data is then converted to provider-specific formats:
2747///
2748/// **OpenAI Vision:**
2749/// ```json
2750/// {
2751///   "type": "image_url",
2752///   "image_url": { "url": "data:image/png;base64,..." }
2753/// }
2754/// ```
2755///
2756/// **Anthropic Vision:**
2757/// ```json
2758/// {
2759///   "type": "image",
2760///   "source": { "type": "base64", "media_type": "image/png", "data": "..." }
2761/// }
2762/// ```
2763///
2764/// # Implementation notes
2765///
2766/// Implementations should:
2767/// - Fetch image data from storage (database, S3, etc.)
2768/// - Return base64-encoded data with media type
2769/// - Handle missing images gracefully (return None)
2770#[async_trait]
2771pub trait ImageResolver: Send + Sync {
2772    /// Resolve an image_file reference to actual image data
2773    ///
2774    /// Returns `None` if the image is not found.
2775    async fn resolve_image(&self, image_id: Uuid) -> Result<Option<ResolvedImage>>;
2776}
2777
2778// ============================================================================
2779// SubagentSpawnStore — durable spawn handles for subagent reattach (EVE-535)
2780// ============================================================================
2781
2782/// Result of attempting to claim a subagent spawn slot.
2783#[derive(Debug)]
2784pub enum SpawnClaimResult {
2785    /// First claim — child session does not yet exist.
2786    /// Proceed to create the child, then call `register_child_session`.
2787    Claimed {
2788        spawn_handle_id: uuid::Uuid,
2789        claim_token: uuid::Uuid,
2790    },
2791    /// Row exists but `child_session_id` was never registered (crash between
2792    /// claim and `register_child_session`). Re-create the child and call
2793    /// `register_child_session` — same flow as `Claimed`.
2794    ClaimedPendingChild {
2795        spawn_handle_id: uuid::Uuid,
2796        claim_token: uuid::Uuid,
2797    },
2798    /// Child session was created and is still running.
2799    /// Reattach: wait for the existing child and settle with the stored claim_token.
2800    AlreadyRunning {
2801        child_session_id: crate::typed_id::SessionId,
2802        /// Stored claim token — must be used for `settle_spawn` on this replay.
2803        claim_token: uuid::Uuid,
2804    },
2805    /// Child already finished on a previous execution.
2806    /// Fast-path: return the stored result immediately without waiting.
2807    AlreadySettled {
2808        child_session_id: crate::typed_id::SessionId,
2809        /// The `wait_for_idle` return value from the original execution.
2810        terminal_status: String,
2811        terminal_result: String,
2812    },
2813}
2814
2815/// Durable spawn handle store for subagent idempotency (EVE-535).
2816///
2817/// Maps `(parent_session_id, tool_call_id) → child_session_id` so that when
2818/// a parent's `act` is reclaimed mid-`wait_for_idle`, the tool can reattach
2819/// to the existing child instead of spawning a duplicate.
2820///
2821/// Lifecycle: claim → register_child_session → settle_spawn.
2822#[async_trait]
2823pub trait SubagentSpawnStore: Send + Sync + 'static {
2824    /// Attempt to claim a spawn slot for `(parent_session_id, tool_call_id)`.
2825    ///
2826    /// Does NOT accept `child_session_id` — the child session does not exist yet.
2827    /// Call `register_child_session` with the actual child ID after creating it.
2828    async fn try_claim_spawn(
2829        &self,
2830        parent_session_id: crate::typed_id::SessionId,
2831        tool_call_id: &str,
2832        claim_token: uuid::Uuid,
2833    ) -> Result<SpawnClaimResult>;
2834
2835    /// Register the actual child session ID after it has been created.
2836    ///
2837    /// Must be called after `try_claim_spawn` returns `Claimed` or
2838    /// `ClaimedPendingChild`, before waiting for the child to complete.
2839    async fn register_child_session(
2840        &self,
2841        spawn_handle_id: uuid::Uuid,
2842        claim_token: uuid::Uuid,
2843        child_session_id: crate::typed_id::SessionId,
2844    ) -> Result<()>;
2845
2846    /// Record the terminal result once the child has completed.
2847    ///
2848    /// `claim_token` must match the stored token. `terminal_status` is the
2849    /// `wait_for_idle` return value ("idle", "error", "timeout", etc.) and
2850    /// `terminal_result` is the last agent message.
2851    async fn settle_spawn(
2852        &self,
2853        parent_session_id: crate::typed_id::SessionId,
2854        tool_call_id: &str,
2855        claim_token: uuid::Uuid,
2856        terminal_status: &str,
2857        terminal_result: &str,
2858    ) -> Result<()>;
2859}
2860
2861/// Blanket impl: `Arc<S>` delegates to the inner store.
2862#[async_trait]
2863impl<S: SubagentSpawnStore + ?Sized> SubagentSpawnStore for Arc<S> {
2864    async fn try_claim_spawn(
2865        &self,
2866        parent_session_id: crate::typed_id::SessionId,
2867        tool_call_id: &str,
2868        claim_token: uuid::Uuid,
2869    ) -> Result<SpawnClaimResult> {
2870        (**self)
2871            .try_claim_spawn(parent_session_id, tool_call_id, claim_token)
2872            .await
2873    }
2874
2875    async fn register_child_session(
2876        &self,
2877        spawn_handle_id: uuid::Uuid,
2878        claim_token: uuid::Uuid,
2879        child_session_id: crate::typed_id::SessionId,
2880    ) -> Result<()> {
2881        (**self)
2882            .register_child_session(spawn_handle_id, claim_token, child_session_id)
2883            .await
2884    }
2885
2886    async fn settle_spawn(
2887        &self,
2888        parent_session_id: crate::typed_id::SessionId,
2889        tool_call_id: &str,
2890        claim_token: uuid::Uuid,
2891        terminal_status: &str,
2892        terminal_result: &str,
2893    ) -> Result<()> {
2894        (**self)
2895            .settle_spawn(
2896                parent_session_id,
2897                tool_call_id,
2898                claim_token,
2899                terminal_status,
2900                terminal_result,
2901            )
2902            .await
2903    }
2904}
2905
2906/// No-op spawn store — used when no durable store is configured (dev/test).
2907///
2908/// Always claims (no dedup); settle and register are no-ops.
2909pub struct NoopSubagentSpawnStore;
2910
2911#[async_trait]
2912impl SubagentSpawnStore for NoopSubagentSpawnStore {
2913    async fn try_claim_spawn(
2914        &self,
2915        _parent_session_id: crate::typed_id::SessionId,
2916        _tool_call_id: &str,
2917        claim_token: uuid::Uuid,
2918    ) -> Result<SpawnClaimResult> {
2919        Ok(SpawnClaimResult::Claimed {
2920            spawn_handle_id: uuid::Uuid::new_v4(),
2921            claim_token,
2922        })
2923    }
2924
2925    async fn register_child_session(
2926        &self,
2927        _spawn_handle_id: uuid::Uuid,
2928        _claim_token: uuid::Uuid,
2929        _child_session_id: crate::typed_id::SessionId,
2930    ) -> Result<()> {
2931        Ok(())
2932    }
2933
2934    async fn settle_spawn(
2935        &self,
2936        _parent_session_id: crate::typed_id::SessionId,
2937        _tool_call_id: &str,
2938        _claim_token: uuid::Uuid,
2939        _terminal_status: &str,
2940        _terminal_result: &str,
2941    ) -> Result<()> {
2942        Ok(())
2943    }
2944}
2945
2946// ============================================================================
2947// Tests
2948// ============================================================================
2949
2950#[cfg(test)]
2951mod tests {
2952    use super::*;
2953
2954    #[test]
2955    fn test_resolved_image_new() {
2956        let image = ResolvedImage::new("SGVsbG8=", "image/png");
2957        assert_eq!(image.base64, "SGVsbG8=");
2958        assert_eq!(image.media_type, "image/png");
2959    }
2960
2961    #[test]
2962    fn test_resolved_image_to_data_url() {
2963        let image = ResolvedImage::new("SGVsbG8=", "image/png");
2964        let data_url = image.to_data_url();
2965        assert_eq!(data_url, "data:image/png;base64,SGVsbG8=");
2966    }
2967
2968    #[test]
2969    fn test_resolved_image_jpeg() {
2970        let image = ResolvedImage::new("base64data", "image/jpeg");
2971        let data_url = image.to_data_url();
2972        assert!(data_url.starts_with("data:image/jpeg;base64,"));
2973    }
2974}