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