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-owned services that may be exposed to tools.
1647///
1648/// Tools declare the subset they require through
1649/// [`crate::tools::Tool::required_context_services`]. Runtime hosts validate
1650/// those declarations before advertising tools to the model.
1651#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1652pub enum ToolContextService {
1653    SessionFileSystem,
1654    SessionStorageStore,
1655    ImageArtifactStore,
1656    ProviderCredentialStore,
1657    UtilityLlmService,
1658    McpInvoker,
1659    EgressService,
1660    SessionSqlDbStore,
1661    MessageRetriever,
1662    SessionStore,
1663    SessionMutator,
1664    AgentStore,
1665    ConnectionResolver,
1666    ScheduleStore,
1667    PlatformStore,
1668    KnowledgeStore,
1669    KnowledgeIndexSearch,
1670    LeasedResourceStore,
1671    SessionResourceRegistry,
1672    SessionTaskRegistry,
1673    EventEmitter,
1674    CapabilityRegistry,
1675    ToolRegistry,
1676    OrgId,
1677    BudgetChecker,
1678    PaymentAuthority,
1679    SessionCreationAuthority,
1680    SubagentSpawnStore,
1681    ReasoningEffortHandle,
1682}
1683
1684impl ToolContextService {
1685    pub const fn name(self) -> &'static str {
1686        match self {
1687            Self::SessionFileSystem => "SessionFileSystem",
1688            Self::SessionStorageStore => "SessionStorageStore",
1689            Self::ImageArtifactStore => "ImageArtifactStore",
1690            Self::ProviderCredentialStore => "ProviderCredentialStore",
1691            Self::UtilityLlmService => "UtilityLlmService",
1692            Self::McpInvoker => "McpInvoker",
1693            Self::EgressService => "EgressService",
1694            Self::SessionSqlDbStore => "SessionSqlDbStore",
1695            Self::MessageRetriever => "MessageRetriever",
1696            Self::SessionStore => "SessionStore",
1697            Self::SessionMutator => "SessionMutator",
1698            Self::AgentStore => "AgentStore",
1699            Self::ConnectionResolver => "ConnectionResolver",
1700            Self::ScheduleStore => "SessionScheduleStore",
1701            Self::PlatformStore => "PlatformStore",
1702            Self::KnowledgeStore => "KnowledgeStore",
1703            Self::KnowledgeIndexSearch => "KnowledgeIndexSearch",
1704            Self::LeasedResourceStore => "LeasedResourceStore",
1705            Self::SessionResourceRegistry => "SessionResourceRegistry",
1706            Self::SessionTaskRegistry => "SessionTaskRegistry",
1707            Self::EventEmitter => "EventEmitter",
1708            Self::CapabilityRegistry => "CapabilityRegistry",
1709            Self::ToolRegistry => "ToolRegistry",
1710            Self::OrgId => "OrgId",
1711            Self::BudgetChecker => "BudgetChecker",
1712            Self::PaymentAuthority => "PaymentAuthority",
1713            Self::SessionCreationAuthority => "SessionCreationAuthority",
1714            Self::SubagentSpawnStore => "SubagentSpawnStore",
1715            Self::ReasoningEffortHandle => "ReasoningEffortHandle",
1716        }
1717    }
1718}
1719
1720/// Cloneable snapshot of all services a runtime host makes available to tools.
1721///
1722/// Production act execution constructs each [`ToolContext`] from this bundle;
1723/// per-call metadata is applied afterwards.
1724#[derive(Clone, Default)]
1725pub struct ToolContextServices {
1726    pub file_store: Option<Arc<dyn SessionFileSystem>>,
1727    pub storage_store: Option<Arc<dyn SessionStorageStore>>,
1728    pub image_store: Option<Arc<dyn ImageArtifactStore>>,
1729    pub provider_credential_store: Option<Arc<dyn ProviderCredentialStore>>,
1730    pub utility_llm_service: Option<Arc<dyn crate::UtilityLlmService>>,
1731    pub mcp_invoker: Option<Arc<dyn crate::McpToolInvoker>>,
1732    pub egress_service: Option<Arc<dyn crate::EgressService>>,
1733    pub sqldb_store: Option<SessionSqlDbStoreRef>,
1734    pub message_retriever: Option<Arc<dyn crate::message_retriever::MessageRetriever>>,
1735    pub session_store: Option<Arc<dyn SessionStore>>,
1736    pub session_mutator: Option<Arc<dyn SessionMutator>>,
1737    pub agent_store: Option<Arc<dyn AgentStore>>,
1738    pub connection_resolver: Option<Arc<dyn UserConnectionResolver>>,
1739    pub schedule_store: Option<Arc<dyn SessionScheduleStore>>,
1740    pub platform_store: Option<Arc<dyn crate::platform_store::PlatformStore>>,
1741    pub knowledge_store: Option<Arc<dyn KnowledgeStore>>,
1742    pub knowledge_index_search: Option<Arc<dyn crate::vector_store::KnowledgeIndexSearch>>,
1743    pub leased_resource_store: Option<Arc<dyn LeasedResourceStore>>,
1744    pub session_resource_registry: Option<Arc<dyn SessionResourceRegistry>>,
1745    pub session_task_registry: Option<Arc<dyn crate::session_task::SessionTaskRegistry>>,
1746    pub event_emitter: Option<Arc<dyn EventEmitter>>,
1747    pub capability_registry: Option<crate::capabilities::CapabilityRegistry>,
1748    pub tool_registry: Option<Arc<crate::tools::ToolRegistry>>,
1749    pub org_id: Option<crate::typed_id::OrgId>,
1750    pub network_access: Option<crate::network_access::NetworkAccessList>,
1751    pub budget_checker: Option<Arc<dyn BudgetChecker>>,
1752    pub payment_authority: Option<Arc<dyn PaymentAuthority>>,
1753    pub session_creation_authority: Option<Arc<dyn SessionCreationAuthority>>,
1754    pub subagent_spawn_store: Option<Arc<dyn SubagentSpawnStore>>,
1755    pub subagent_nesting_policy: SubagentNestingPolicy,
1756    pub reasoning_effort_handle: Option<ReasoningEffortHandle>,
1757}
1758
1759impl ToolContextServices {
1760    pub fn provides(&self, service: ToolContextService) -> bool {
1761        match service {
1762            ToolContextService::SessionFileSystem => self.file_store.is_some(),
1763            ToolContextService::SessionStorageStore => self.storage_store.is_some(),
1764            ToolContextService::ImageArtifactStore => self.image_store.is_some(),
1765            ToolContextService::ProviderCredentialStore => self.provider_credential_store.is_some(),
1766            ToolContextService::UtilityLlmService => self.utility_llm_service.is_some(),
1767            ToolContextService::McpInvoker => self.mcp_invoker.is_some(),
1768            ToolContextService::EgressService => self.egress_service.is_some(),
1769            ToolContextService::SessionSqlDbStore => self.sqldb_store.is_some(),
1770            ToolContextService::MessageRetriever => self.message_retriever.is_some(),
1771            ToolContextService::SessionStore => self.session_store.is_some(),
1772            ToolContextService::SessionMutator => self.session_mutator.is_some(),
1773            ToolContextService::AgentStore => self.agent_store.is_some(),
1774            ToolContextService::ConnectionResolver => self.connection_resolver.is_some(),
1775            ToolContextService::ScheduleStore => self.schedule_store.is_some(),
1776            ToolContextService::PlatformStore => self.platform_store.is_some(),
1777            ToolContextService::KnowledgeStore => self.knowledge_store.is_some(),
1778            ToolContextService::KnowledgeIndexSearch => self.knowledge_index_search.is_some(),
1779            ToolContextService::LeasedResourceStore => self.leased_resource_store.is_some(),
1780            ToolContextService::SessionResourceRegistry => self.session_resource_registry.is_some(),
1781            ToolContextService::SessionTaskRegistry => self.session_task_registry.is_some(),
1782            ToolContextService::EventEmitter => self.event_emitter.is_some(),
1783            ToolContextService::CapabilityRegistry => self.capability_registry.is_some(),
1784            ToolContextService::ToolRegistry => self.tool_registry.is_some(),
1785            ToolContextService::OrgId => self.org_id.is_some(),
1786            ToolContextService::BudgetChecker => self.budget_checker.is_some(),
1787            ToolContextService::PaymentAuthority => self.payment_authority.is_some(),
1788            ToolContextService::SessionCreationAuthority => {
1789                self.session_creation_authority.is_some()
1790            }
1791            ToolContextService::SubagentSpawnStore => self.subagent_spawn_store.is_some(),
1792            ToolContextService::ReasoningEffortHandle => self.reasoning_effort_handle.is_some(),
1793        }
1794    }
1795}
1796
1797/// Runtime context provided to tools during execution.
1798///
1799/// This context contains:
1800/// - Session ID for scoping operations
1801/// - Optional stores for tools that need external access
1802///
1803/// Tools that need context-aware execution (like filesystem tools) can use
1804/// the `execute_with_context` method on the Tool trait.
1805#[derive(Clone)]
1806pub struct ToolContext {
1807    /// The session ID for the current execution
1808    pub session_id: SessionId,
1809    /// The workspace this session is attached to — the key for the virtual
1810    /// file store. For the default 1:1 session this equals
1811    /// `WorkspaceId::from_uuid(session_id.uuid())`; for a shared workspace it
1812    /// differs. File-system tools MUST key by this (via `workspace_fs_key`)
1813    /// rather than `session_id` so shared-workspace sessions read/write the
1814    /// attached workspace's files. See specs/workspace.md.
1815    pub workspace_id: WorkspaceId,
1816
1817    /// Optional file store for filesystem operations
1818    pub file_store: Option<Arc<dyn SessionFileSystem>>,
1819
1820    /// Optional storage store for key/value and secret storage
1821    pub storage_store: Option<Arc<dyn SessionStorageStore>>,
1822
1823    /// Optional durable image artifact store for tool-side media persistence.
1824    pub image_store: Option<Arc<dyn ImageArtifactStore>>,
1825
1826    /// Optional provider credential store for tool-side API clients.
1827    pub provider_credential_store: Option<Arc<dyn ProviderCredentialStore>>,
1828
1829    /// Optional system utility LLM service for capability internals.
1830    pub utility_llm_service: Option<Arc<dyn crate::UtilityLlmService>>,
1831
1832    /// Optional scoped-MCP tool invoker for capability internals that need to
1833    /// call an MCP server out-of-band (e.g. the guardrails `mcp` check
1834    /// delegating a decision to an external guardrail endpoint). The invoker
1835    /// resolves connections and credentials per the current session/org, so
1836    /// tenant scoping is enforced by the host that supplies it.
1837    pub mcp_invoker: Option<Arc<dyn crate::McpToolInvoker>>,
1838
1839    /// Optional outbound egress service for HTTP/API traffic.
1840    pub egress_service: Option<Arc<dyn crate::EgressService>>,
1841
1842    /// Optional session SQL database store
1843    pub sqldb_store: Option<SessionSqlDbStoreRef>,
1844
1845    /// Optional message retriever for tools that need conversation history access
1846    pub message_retriever: Option<Arc<dyn crate::message_retriever::MessageRetriever>>,
1847
1848    /// Optional session store for tools that need session metadata access.
1849    pub session_store: Option<Arc<dyn SessionStore>>,
1850
1851    /// Optional session mutator for tools that need to update session metadata.
1852    pub session_mutator: Option<Arc<dyn SessionMutator>>,
1853
1854    /// Optional agent store for tools that need agent metadata access.
1855    pub agent_store: Option<Arc<dyn AgentStore>>,
1856
1857    /// Optional resolver for user connection tokens (lazy GitHub token lookup, etc.)
1858    pub connection_resolver: Option<Arc<dyn UserConnectionResolver>>,
1859
1860    /// Optional session schedule store for scheduling tools.
1861    pub schedule_store: Option<Arc<dyn SessionScheduleStore>>,
1862
1863    /// Optional platform store for org-level management tools.
1864    pub platform_store: Option<Arc<dyn crate::platform_store::PlatformStore>>,
1865    /// Optional knowledge store backing the `search_knowledge` tool.
1866    pub knowledge_store: Option<Arc<dyn KnowledgeStore>>,
1867
1868    /// Optional hybrid retrieval over bound Knowledge Indexes for the
1869    /// `search_index` tool. Server-implemented; populated only on the server
1870    /// act path alongside `platform_store` / `connection_resolver`.
1871    pub knowledge_index_search: Option<Arc<dyn crate::vector_store::KnowledgeIndexSearch>>,
1872
1873    /// Optional leased resource store for lifecycle-managed provider resources.
1874    pub leased_resource_store: Option<Arc<dyn LeasedResourceStore>>,
1875
1876    /// Optional session resource registry — generic registry of active resources.
1877    pub session_resource_registry: Option<Arc<dyn SessionResourceRegistry>>,
1878
1879    /// Optional session task registry — background work owned by the session
1880    /// (specs/session-tasks.md).
1881    pub session_task_registry: Option<Arc<dyn crate::session_task::SessionTaskRegistry>>,
1882
1883    /// Optional event emitter for tools that need to stream progress updates.
1884    /// When set, tools can emit `tool.progress` events during execution.
1885    pub event_emitter: Option<Arc<dyn EventEmitter>>,
1886
1887    /// Event context for correlating progress events with the current tool call.
1888    /// Set by ActAtom when constructing the ToolContext.
1889    pub event_context: Option<crate::events::EventContext>,
1890
1891    /// The tool call ID for the current execution (set by ActAtom).
1892    /// Used by tools to emit correlated progress events.
1893    pub tool_call_id: Option<String>,
1894    /// Optional capability registry for blueprint lookups.
1895    pub capability_registry: Option<crate::capabilities::CapabilityRegistry>,
1896
1897    /// Optional registry of active built-in tools for meta-tools such as
1898    /// `spawn_background` that need to inspect or delegate to sibling tools.
1899    pub tool_registry: Option<Arc<crate::tools::ToolRegistry>>,
1900
1901    /// Optional allowlist of tools visible to the model for this turn.
1902    /// Registry-introspecting tools must filter through this before returning
1903    /// sibling tool metadata, because the execution registry can be a superset.
1904    pub visible_tool_names: Option<Arc<HashSet<String>>>,
1905
1906    /// Optional org ID for org-scoped operations.
1907    pub org_id: Option<crate::typed_id::OrgId>,
1908
1909    /// Merged network access list (harness ∩ agent ∩ session).
1910    /// When set, tools that make HTTP requests must check URLs against this list.
1911    pub network_access: Option<crate::network_access::NetworkAccessList>,
1912
1913    /// Resolved locale for localized tool behavior (BCP 47, e.g. `uk-UA`).
1914    /// When set, tools that support localization use this to produce
1915    /// locale-appropriate descriptions, error messages, and prompts.
1916    pub locale: Option<String>,
1917
1918    /// Optional budget checker for the check_budget tool.
1919    pub budget_checker: Option<Arc<dyn BudgetChecker>>,
1920
1921    /// Optional internal payment authority for paid capability tools.
1922    pub payment_authority: Option<Arc<dyn PaymentAuthority>>,
1923
1924    /// Optional host authority for detached peer-session creation.
1925    pub session_creation_authority: Option<Arc<dyn SessionCreationAuthority>>,
1926
1927    /// Optional durable spawn handle store for subagent reattach (EVE-535).
1928    /// When set, subagent delegation uses claim/settle to prevent duplicate spawning
1929    /// on parent worker reclaim.
1930    pub subagent_spawn_store: Option<Arc<dyn SubagentSpawnStore>>,
1931
1932    /// Resolved subagent nesting policy for this turn.
1933    pub subagent_nesting_policy: SubagentNestingPolicy,
1934
1935    /// Optional live reasoning-effort handle (EVE-595). When set, a tool can
1936    /// change the reasoning effort mid-turn; subsequent LLM steps in the same
1937    /// `run_turn` re-read it and use the new effort.
1938    pub reasoning_effort_handle: Option<ReasoningEffortHandle>,
1939
1940    /// Cooperative cancellation for this tool call.
1941    ///
1942    /// The act atom cancels it when the call ends *by any means* — the turn was
1943    /// cancelled, the act future was dropped, or the tool simply returned. A
1944    /// tool that only awaits inside its own future needs nothing here: dropping
1945    /// the future already stops it. This exists for work a tool starts and
1946    /// leaves running — a child process, a detached watcher task, a prompt
1947    /// waiting on an answer — which would otherwise outlive the call that
1948    /// created it. Clone the token into that work and let it die with the call.
1949    pub cancellation: Option<tokio_util::sync::CancellationToken>,
1950}
1951
1952impl ToolContext {
1953    /// The virtual-file-store key for this execution, derived from the attached
1954    /// workspace. Carried through the `SessionFileSystem` trait's `SessionId`
1955    /// parameter (the store keys by `.uuid()`), so a shared-workspace session
1956    /// addresses the workspace's files rather than its own session-id keyspace.
1957    pub fn workspace_fs_key(&self) -> SessionId {
1958        SessionId::from_uuid(self.workspace_id.uuid())
1959    }
1960
1961    /// Override the attached workspace (default is the 1:1 session-derived id).
1962    pub fn with_workspace_id(mut self, workspace_id: WorkspaceId) -> Self {
1963        self.workspace_id = workspace_id;
1964        self
1965    }
1966
1967    /// Create a new tool context with just a session ID
1968    pub fn new(session_id: SessionId) -> Self {
1969        Self {
1970            session_id,
1971            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
1972            file_store: None,
1973            storage_store: None,
1974            image_store: None,
1975            provider_credential_store: None,
1976            utility_llm_service: None,
1977            mcp_invoker: None,
1978            egress_service: None,
1979            sqldb_store: None,
1980            message_retriever: None,
1981            session_store: None,
1982            session_mutator: None,
1983            agent_store: None,
1984            connection_resolver: None,
1985            schedule_store: None,
1986            platform_store: None,
1987            knowledge_store: None,
1988            knowledge_index_search: None,
1989            leased_resource_store: None,
1990            session_resource_registry: None,
1991            session_task_registry: None,
1992            event_emitter: None,
1993            event_context: None,
1994            tool_call_id: None,
1995            capability_registry: None,
1996            tool_registry: None,
1997            visible_tool_names: None,
1998            org_id: None,
1999            network_access: None,
2000            locale: None,
2001            budget_checker: None,
2002            payment_authority: None,
2003            session_creation_authority: None,
2004            subagent_spawn_store: None,
2005            subagent_nesting_policy: SubagentNestingPolicy::default(),
2006            reasoning_effort_handle: None,
2007            cancellation: None,
2008        }
2009    }
2010
2011    /// Construct a production tool context from a validated runtime service snapshot.
2012    pub fn from_services(session_id: SessionId, services: &ToolContextServices) -> Self {
2013        Self {
2014            session_id,
2015            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
2016            file_store: services.file_store.clone(),
2017            storage_store: services.storage_store.clone(),
2018            image_store: services.image_store.clone(),
2019            provider_credential_store: services.provider_credential_store.clone(),
2020            utility_llm_service: services.utility_llm_service.clone(),
2021            mcp_invoker: services.mcp_invoker.clone(),
2022            egress_service: services.egress_service.clone(),
2023            sqldb_store: services.sqldb_store.clone(),
2024            message_retriever: services.message_retriever.clone(),
2025            session_store: services.session_store.clone(),
2026            session_mutator: services.session_mutator.clone(),
2027            agent_store: services.agent_store.clone(),
2028            connection_resolver: services.connection_resolver.clone(),
2029            schedule_store: services.schedule_store.clone(),
2030            platform_store: services.platform_store.clone(),
2031            knowledge_store: services.knowledge_store.clone(),
2032            knowledge_index_search: services.knowledge_index_search.clone(),
2033            leased_resource_store: services.leased_resource_store.clone(),
2034            session_resource_registry: services.session_resource_registry.clone(),
2035            session_task_registry: services.session_task_registry.clone(),
2036            event_emitter: services.event_emitter.clone(),
2037            event_context: None,
2038            tool_call_id: None,
2039            capability_registry: services.capability_registry.clone(),
2040            tool_registry: services.tool_registry.clone(),
2041            visible_tool_names: None,
2042            org_id: services.org_id,
2043            network_access: services.network_access.clone(),
2044            locale: None,
2045            budget_checker: services.budget_checker.clone(),
2046            payment_authority: services.payment_authority.clone(),
2047            session_creation_authority: services.session_creation_authority.clone(),
2048            subagent_spawn_store: services.subagent_spawn_store.clone(),
2049            subagent_nesting_policy: services.subagent_nesting_policy,
2050            reasoning_effort_handle: services.reasoning_effort_handle.clone(),
2051            cancellation: None,
2052        }
2053    }
2054
2055    /// Create a context with a file store
2056    pub fn with_file_store(session_id: SessionId, file_store: Arc<dyn SessionFileSystem>) -> Self {
2057        Self {
2058            session_id,
2059            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
2060            file_store: Some(file_store),
2061            storage_store: None,
2062            image_store: None,
2063            provider_credential_store: None,
2064            utility_llm_service: None,
2065            mcp_invoker: None,
2066            egress_service: None,
2067            sqldb_store: None,
2068            message_retriever: None,
2069            session_store: None,
2070            session_mutator: None,
2071            agent_store: None,
2072            connection_resolver: None,
2073            schedule_store: None,
2074            platform_store: None,
2075            knowledge_store: None,
2076            knowledge_index_search: None,
2077            leased_resource_store: None,
2078            session_resource_registry: None,
2079            session_task_registry: None,
2080            event_emitter: None,
2081            event_context: None,
2082            tool_call_id: None,
2083            capability_registry: None,
2084            tool_registry: None,
2085            visible_tool_names: None,
2086            org_id: None,
2087            network_access: None,
2088            locale: None,
2089            budget_checker: None,
2090            payment_authority: None,
2091            session_creation_authority: None,
2092            subagent_spawn_store: None,
2093            subagent_nesting_policy: SubagentNestingPolicy::default(),
2094            reasoning_effort_handle: None,
2095            cancellation: None,
2096        }
2097    }
2098
2099    /// Create a context with a storage store
2100    pub fn with_storage_store(
2101        session_id: SessionId,
2102        storage_store: Arc<dyn SessionStorageStore>,
2103    ) -> Self {
2104        Self {
2105            session_id,
2106            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
2107            file_store: None,
2108            storage_store: Some(storage_store),
2109            image_store: None,
2110            provider_credential_store: None,
2111            utility_llm_service: None,
2112            mcp_invoker: None,
2113            egress_service: None,
2114            sqldb_store: None,
2115            message_retriever: None,
2116            session_store: None,
2117            session_mutator: None,
2118            agent_store: None,
2119            connection_resolver: None,
2120            schedule_store: None,
2121            platform_store: None,
2122            knowledge_store: None,
2123            knowledge_index_search: None,
2124            leased_resource_store: None,
2125            session_resource_registry: None,
2126            session_task_registry: None,
2127            event_emitter: None,
2128            event_context: None,
2129            tool_call_id: None,
2130            capability_registry: None,
2131            tool_registry: None,
2132            visible_tool_names: None,
2133            org_id: None,
2134            network_access: None,
2135            locale: None,
2136            budget_checker: None,
2137            payment_authority: None,
2138            session_creation_authority: None,
2139            subagent_spawn_store: None,
2140            subagent_nesting_policy: SubagentNestingPolicy::default(),
2141            reasoning_effort_handle: None,
2142            cancellation: None,
2143        }
2144    }
2145
2146    /// Create a context with both file store and storage store
2147    pub fn with_stores(
2148        session_id: SessionId,
2149        file_store: Arc<dyn SessionFileSystem>,
2150        storage_store: Arc<dyn SessionStorageStore>,
2151    ) -> Self {
2152        Self {
2153            session_id,
2154            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
2155            file_store: Some(file_store),
2156            storage_store: Some(storage_store),
2157            sqldb_store: None,
2158            image_store: None,
2159            provider_credential_store: None,
2160            utility_llm_service: None,
2161            mcp_invoker: None,
2162            egress_service: None,
2163            message_retriever: None,
2164            session_store: None,
2165            session_mutator: None,
2166            agent_store: None,
2167            connection_resolver: None,
2168            schedule_store: None,
2169            platform_store: None,
2170            knowledge_store: None,
2171            knowledge_index_search: None,
2172            leased_resource_store: None,
2173            session_resource_registry: None,
2174            session_task_registry: None,
2175            event_emitter: None,
2176            event_context: None,
2177            tool_call_id: None,
2178            capability_registry: None,
2179            tool_registry: None,
2180            visible_tool_names: None,
2181            org_id: None,
2182            network_access: None,
2183            locale: None,
2184            budget_checker: None,
2185            payment_authority: None,
2186            session_creation_authority: None,
2187            subagent_spawn_store: None,
2188            subagent_nesting_policy: SubagentNestingPolicy::default(),
2189            reasoning_effort_handle: None,
2190            cancellation: None,
2191        }
2192    }
2193
2194    /// Add a SQL database store to this context
2195    pub fn with_sqldb_store(mut self, sqldb_store: SessionSqlDbStoreRef) -> Self {
2196        self.sqldb_store = Some(sqldb_store);
2197        self
2198    }
2199
2200    /// Add a message retriever to this context
2201    pub fn with_message_retriever(
2202        mut self,
2203        retriever: Arc<dyn crate::message_retriever::MessageRetriever>,
2204    ) -> Self {
2205        self.message_retriever = Some(retriever);
2206        self
2207    }
2208
2209    /// Add a session store to this context.
2210    pub fn with_session_store(mut self, store: Arc<dyn SessionStore>) -> Self {
2211        self.session_store = Some(store);
2212        self
2213    }
2214
2215    /// Add a session mutator to this context.
2216    pub fn with_session_mutator(mut self, mutator: Arc<dyn SessionMutator>) -> Self {
2217        self.session_mutator = Some(mutator);
2218        self
2219    }
2220
2221    /// Add a live reasoning-effort handle (EVE-595). Tools can call
2222    /// [`ReasoningEffortHandle::set`] on it to change the effort used by
2223    /// subsequent LLM steps within the same turn.
2224    /// Attach a cancellation token to this context (see
2225    /// [`ToolContext::cancellation`]).
2226    pub fn with_cancellation(mut self, token: tokio_util::sync::CancellationToken) -> Self {
2227        self.cancellation = Some(token);
2228        self
2229    }
2230
2231    /// Whether this call has already been cancelled. A context with no token
2232    /// never reports cancellation.
2233    pub fn is_cancelled(&self) -> bool {
2234        self.cancellation
2235            .as_ref()
2236            .is_some_and(|token| token.is_cancelled())
2237    }
2238
2239    pub fn with_reasoning_effort_handle(mut self, handle: ReasoningEffortHandle) -> Self {
2240        self.reasoning_effort_handle = Some(handle);
2241        self
2242    }
2243
2244    /// Add an agent store to this context.
2245    pub fn with_agent_store(mut self, store: Arc<dyn AgentStore>) -> Self {
2246        self.agent_store = Some(store);
2247        self
2248    }
2249
2250    /// Add a connection resolver to this context
2251    pub fn with_connection_resolver(mut self, resolver: Arc<dyn UserConnectionResolver>) -> Self {
2252        self.connection_resolver = Some(resolver);
2253        self
2254    }
2255
2256    /// Create a context with an image artifact store.
2257    pub fn with_image_store(
2258        session_id: SessionId,
2259        image_store: Arc<dyn ImageArtifactStore>,
2260    ) -> Self {
2261        Self {
2262            session_id,
2263            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
2264            file_store: None,
2265            storage_store: None,
2266            image_store: Some(image_store),
2267            provider_credential_store: None,
2268            utility_llm_service: None,
2269            mcp_invoker: None,
2270            egress_service: None,
2271            sqldb_store: None,
2272            message_retriever: None,
2273            session_store: None,
2274            session_mutator: None,
2275            agent_store: None,
2276            connection_resolver: None,
2277            schedule_store: None,
2278            platform_store: None,
2279            knowledge_store: None,
2280            knowledge_index_search: None,
2281            leased_resource_store: None,
2282            session_resource_registry: None,
2283            session_task_registry: None,
2284            event_emitter: None,
2285            event_context: None,
2286            tool_call_id: None,
2287            capability_registry: None,
2288            tool_registry: None,
2289            visible_tool_names: None,
2290            org_id: None,
2291            network_access: None,
2292            locale: None,
2293            budget_checker: None,
2294            payment_authority: None,
2295            session_creation_authority: None,
2296            subagent_spawn_store: None,
2297            subagent_nesting_policy: SubagentNestingPolicy::default(),
2298            reasoning_effort_handle: None,
2299            cancellation: None,
2300        }
2301    }
2302
2303    /// Set the provider credential store on this context.
2304    pub fn with_provider_credential_store(
2305        mut self,
2306        store: Arc<dyn ProviderCredentialStore>,
2307    ) -> Self {
2308        self.provider_credential_store = Some(store);
2309        self
2310    }
2311
2312    /// Set the utility LLM service on this context.
2313    pub fn with_utility_llm_service(mut self, service: Arc<dyn crate::UtilityLlmService>) -> Self {
2314        self.utility_llm_service = Some(service);
2315        self
2316    }
2317
2318    /// Set the scoped-MCP tool invoker on this context.
2319    pub fn with_mcp_invoker(mut self, invoker: Arc<dyn crate::McpToolInvoker>) -> Self {
2320        self.mcp_invoker = Some(invoker);
2321        self
2322    }
2323
2324    /// Set the outbound egress service on this context.
2325    pub fn with_egress_service(mut self, service: Arc<dyn crate::EgressService>) -> Self {
2326        self.egress_service = Some(service);
2327        self
2328    }
2329
2330    /// Set the outbound egress service on this context when available.
2331    /// Preserves any already-set service when `service` is `None`.
2332    pub fn with_egress_service_opt(
2333        mut self,
2334        service: Option<Arc<dyn crate::EgressService>>,
2335    ) -> Self {
2336        if let Some(service) = service {
2337            self.egress_service = Some(service);
2338        }
2339        self
2340    }
2341
2342    /// Set the session storage store on this context (builder method).
2343    pub fn with_storage_store_arc(mut self, store: Arc<dyn SessionStorageStore>) -> Self {
2344        self.storage_store = Some(store);
2345        self
2346    }
2347
2348    /// Add a session schedule store to this context.
2349    pub fn with_schedule_store(mut self, store: Arc<dyn SessionScheduleStore>) -> Self {
2350        self.schedule_store = Some(store);
2351        self
2352    }
2353
2354    /// Add a platform store to this context.
2355    pub fn with_platform_store(
2356        mut self,
2357        store: Arc<dyn crate::platform_store::PlatformStore>,
2358    ) -> Self {
2359        self.platform_store = Some(store);
2360        self
2361    }
2362
2363    /// Add a Knowledge Index search service to this context (for `search_index`).
2364    pub fn with_knowledge_index_search(
2365        mut self,
2366        search: Arc<dyn crate::vector_store::KnowledgeIndexSearch>,
2367    ) -> Self {
2368        self.knowledge_index_search = Some(search);
2369        self
2370    }
2371
2372    /// Add a leased resource store to this context.
2373    pub fn with_leased_resource_store(mut self, store: Arc<dyn LeasedResourceStore>) -> Self {
2374        self.leased_resource_store = Some(store);
2375        self
2376    }
2377
2378    /// Add a session resource registry to this context.
2379    pub fn with_session_resource_registry(
2380        mut self,
2381        registry: Arc<dyn SessionResourceRegistry>,
2382    ) -> Self {
2383        self.session_resource_registry = Some(registry);
2384        self
2385    }
2386
2387    /// Add a session task registry to this context.
2388    pub fn with_session_task_registry(
2389        mut self,
2390        registry: Arc<dyn crate::session_task::SessionTaskRegistry>,
2391    ) -> Self {
2392        self.session_task_registry = Some(registry);
2393        self
2394    }
2395
2396    /// Set org ID for org-scoped operations.
2397    pub fn with_org_id(mut self, org_id: crate::typed_id::OrgId) -> Self {
2398        self.org_id = Some(org_id);
2399        self
2400    }
2401
2402    /// Set the active built-in tool registry on this context.
2403    pub fn with_tool_registry(mut self, registry: Arc<crate::tools::ToolRegistry>) -> Self {
2404        self.tool_registry = Some(registry);
2405        self
2406    }
2407
2408    /// Set the tool names visible to the model in this turn.
2409    pub fn with_visible_tool_names(mut self, names: Arc<HashSet<String>>) -> Self {
2410        self.visible_tool_names = Some(names);
2411        self
2412    }
2413
2414    /// Set the merged network access list for URL filtering.
2415    pub fn with_network_access(
2416        mut self,
2417        network_access: Option<crate::network_access::NetworkAccessList>,
2418    ) -> Self {
2419        self.network_access = network_access;
2420        self
2421    }
2422
2423    /// Set the internal payment authority for paid capability operations.
2424    pub fn with_payment_authority(mut self, authority: Arc<dyn PaymentAuthority>) -> Self {
2425        self.payment_authority = Some(authority);
2426        self
2427    }
2428
2429    /// Set the durable subagent spawn handle store (EVE-535).
2430    pub fn with_subagent_spawn_store(mut self, store: Arc<dyn SubagentSpawnStore>) -> Self {
2431        self.subagent_spawn_store = Some(store);
2432        self
2433    }
2434
2435    /// Set the resolved subagent nesting policy.
2436    pub fn with_subagent_nesting_policy(mut self, policy: SubagentNestingPolicy) -> Self {
2437        self.subagent_nesting_policy = policy;
2438        self
2439    }
2440
2441    /// Emit a `tool.progress` event if an event emitter and context are available.
2442    ///
2443    /// This is a best-effort helper: failures are logged but not propagated,
2444    /// so tools never fail just because a progress event couldn't be sent.
2445    pub async fn emit_progress(&self, tool_name: &str, message: &str) {
2446        let (Some(emitter), Some(ctx), Some(call_id)) =
2447            (&self.event_emitter, &self.event_context, &self.tool_call_id)
2448        else {
2449            return;
2450        };
2451        if let Err(e) = emitter
2452            .emit(EventRequest::new(
2453                self.session_id,
2454                ctx.clone(),
2455                crate::events::ToolProgressData {
2456                    tool_call_id: call_id.clone(),
2457                    tool_name: tool_name.to_string(),
2458                    message: message.to_string(),
2459                    display_name: None,
2460                },
2461            ))
2462            .await
2463        {
2464            tracing::debug!(
2465                tool_call_id = call_id,
2466                tool_name,
2467                error = %e,
2468                "Failed to emit tool.progress event"
2469            );
2470        }
2471    }
2472
2473    /// Emit a `tool.output.delta` event if an event emitter and context are available.
2474    ///
2475    /// Streams incremental output chunks (e.g., stdout/stderr lines) for live
2476    /// rendering in UI and CLI. Best-effort: failures are logged, not propagated.
2477    pub async fn emit_tool_output(&self, tool_name: &str, delta: &str, stream: &str) {
2478        let (Some(emitter), Some(ctx), Some(call_id)) =
2479            (&self.event_emitter, &self.event_context, &self.tool_call_id)
2480        else {
2481            return;
2482        };
2483        if let Err(e) = emitter
2484            .emit(EventRequest::new(
2485                self.session_id,
2486                ctx.clone(),
2487                crate::events::ToolOutputDeltaData {
2488                    tool_call_id: call_id.clone(),
2489                    tool_name: tool_name.to_string(),
2490                    delta: delta.to_string(),
2491                    stream: stream.to_string(),
2492                },
2493            ))
2494            .await
2495        {
2496            tracing::debug!(
2497                tool_call_id = call_id,
2498                tool_name,
2499                error = %e,
2500                "Failed to emit tool.output.delta event"
2501            );
2502        }
2503    }
2504}
2505
2506impl std::fmt::Debug for ToolContext {
2507    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2508        f.debug_struct("ToolContext")
2509            .field("session_id", &self.session_id)
2510            .field("file_store", &self.file_store.is_some())
2511            .field("storage_store", &self.storage_store.is_some())
2512            .field("image_store", &self.image_store.is_some())
2513            .field(
2514                "provider_credential_store",
2515                &self.provider_credential_store.is_some(),
2516            )
2517            .field("utility_llm_service", &self.utility_llm_service.is_some())
2518            .field("egress_service", &self.egress_service.is_some())
2519            .field("sqldb_store", &self.sqldb_store.is_some())
2520            .field("message_retriever", &self.message_retriever.is_some())
2521            .field("session_store", &self.session_store.is_some())
2522            .field("session_mutator", &self.session_mutator.is_some())
2523            .field("agent_store", &self.agent_store.is_some())
2524            .field("connection_resolver", &self.connection_resolver.is_some())
2525            .field("schedule_store", &self.schedule_store.is_some())
2526            .field("platform_store", &self.platform_store.is_some())
2527            .field(
2528                "knowledge_index_search",
2529                &self.knowledge_index_search.is_some(),
2530            )
2531            .field(
2532                "leased_resource_store",
2533                &self.leased_resource_store.is_some(),
2534            )
2535            .field("event_emitter", &self.event_emitter.is_some())
2536            .field("tool_registry", &self.tool_registry.is_some())
2537            .field("payment_authority", &self.payment_authority.is_some())
2538            .field("subagent_spawn_store", &self.subagent_spawn_store.is_some())
2539            .field("subagent_nesting_policy", &self.subagent_nesting_policy)
2540            .field("org_id", &self.org_id)
2541            .finish()
2542    }
2543}
2544
2545// ============================================================================
2546// EventEmitter - For emitting events
2547// ============================================================================
2548
2549use crate::events::{Event, EventRequest};
2550
2551/// Trait for emitting events following the standard event protocol
2552///
2553/// Implementations can:
2554/// - Store events in a database
2555/// - Keep events in memory for testing
2556/// - Stream events via SSE/WebSocket
2557/// - Log events for debugging
2558///
2559/// Events follow a consistent schema: id, type, ts, context, data.
2560/// See specs/events.md for the full event protocol specification.
2561#[async_trait]
2562pub trait EventEmitter: Send + Sync {
2563    /// Emit an event request
2564    ///
2565    /// Takes an EventRequest (without id/sequence) and returns the stored Event
2566    /// with id and sequence assigned by the storage layer.
2567    async fn emit(&self, request: EventRequest) -> Result<Event>;
2568}
2569
2570/// Blanket impl: `Arc<E>` delegates to the inner emitter.
2571#[async_trait]
2572impl<E: EventEmitter + ?Sized> EventEmitter for Arc<E> {
2573    async fn emit(&self, request: EventRequest) -> Result<Event> {
2574        (**self).emit(request).await
2575    }
2576}
2577
2578/// No-op event emitter for when event emission is not needed
2579///
2580/// This is useful for testing or when event observability is disabled.
2581#[derive(Debug, Clone, Default)]
2582pub struct NoopEventEmitter;
2583
2584#[async_trait]
2585impl EventEmitter for NoopEventEmitter {
2586    async fn emit(&self, request: EventRequest) -> Result<Event> {
2587        // Return a dummy event with sequence 0
2588        Ok(request.into_event(crate::typed_id::EventId::new(), 0))
2589    }
2590}
2591
2592// Note: EventListener trait has been moved to event_listeners.rs module.
2593// Use `everruns_core::EventListener` or `everruns_core::event_listeners::EventListener`.
2594
2595// ============================================================================
2596// ImageResolver - For resolving image_file content to actual image data
2597// ============================================================================
2598
2599/// Resolved image data for LLM consumption
2600///
2601/// This struct contains the actual image data in a format suitable for
2602/// sending to LLM providers. Both OpenAI and Anthropic accept base64-encoded
2603/// images with media type information.
2604#[derive(Debug, Clone)]
2605pub struct ResolvedImage {
2606    /// Base64-encoded image data (without data URL prefix)
2607    pub base64: String,
2608    /// MIME type (e.g., "image/png", "image/jpeg")
2609    pub media_type: String,
2610}
2611
2612impl ResolvedImage {
2613    /// Create a new resolved image
2614    pub fn new(base64: impl Into<String>, media_type: impl Into<String>) -> Self {
2615        Self {
2616            base64: base64.into(),
2617            media_type: media_type.into(),
2618        }
2619    }
2620
2621    /// Convert to a data URL suitable for OpenAI Vision API
2622    ///
2623    /// Format: `data:{media_type};base64,{base64_data}`
2624    pub fn to_data_url(&self) -> String {
2625        format!("data:{};base64,{}", self.media_type, self.base64)
2626    }
2627}
2628
2629/// Trait for resolving image_file content parts to actual image data
2630///
2631/// When building LLM messages, `image_file` content parts contain only
2632/// a reference (UUID) to an uploaded image. This trait allows resolving
2633/// those references to actual image data.
2634///
2635/// # Provider-specific formatting
2636///
2637/// The resolved image data is then converted to provider-specific formats:
2638///
2639/// **OpenAI Vision:**
2640/// ```json
2641/// {
2642///   "type": "image_url",
2643///   "image_url": { "url": "data:image/png;base64,..." }
2644/// }
2645/// ```
2646///
2647/// **Anthropic Vision:**
2648/// ```json
2649/// {
2650///   "type": "image",
2651///   "source": { "type": "base64", "media_type": "image/png", "data": "..." }
2652/// }
2653/// ```
2654///
2655/// # Implementation notes
2656///
2657/// Implementations should:
2658/// - Fetch image data from storage (database, S3, etc.)
2659/// - Return base64-encoded data with media type
2660/// - Handle missing images gracefully (return None)
2661#[async_trait]
2662pub trait ImageResolver: Send + Sync {
2663    /// Resolve an image_file reference to actual image data
2664    ///
2665    /// Returns `None` if the image is not found.
2666    async fn resolve_image(&self, image_id: Uuid) -> Result<Option<ResolvedImage>>;
2667}
2668
2669// ============================================================================
2670// SubagentSpawnStore — durable spawn handles for subagent reattach (EVE-535)
2671// ============================================================================
2672
2673/// Result of attempting to claim a subagent spawn slot.
2674#[derive(Debug)]
2675pub enum SpawnClaimResult {
2676    /// First claim — child session does not yet exist.
2677    /// Proceed to create the child, then call `register_child_session`.
2678    Claimed {
2679        spawn_handle_id: uuid::Uuid,
2680        claim_token: uuid::Uuid,
2681    },
2682    /// Row exists but `child_session_id` was never registered (crash between
2683    /// claim and `register_child_session`). Re-create the child and call
2684    /// `register_child_session` — same flow as `Claimed`.
2685    ClaimedPendingChild {
2686        spawn_handle_id: uuid::Uuid,
2687        claim_token: uuid::Uuid,
2688    },
2689    /// Child session was created and is still running.
2690    /// Reattach: wait for the existing child and settle with the stored claim_token.
2691    AlreadyRunning {
2692        child_session_id: crate::typed_id::SessionId,
2693        /// Stored claim token — must be used for `settle_spawn` on this replay.
2694        claim_token: uuid::Uuid,
2695    },
2696    /// Child already finished on a previous execution.
2697    /// Fast-path: return the stored result immediately without waiting.
2698    AlreadySettled {
2699        child_session_id: crate::typed_id::SessionId,
2700        /// The `wait_for_idle` return value from the original execution.
2701        terminal_status: String,
2702        terminal_result: String,
2703    },
2704}
2705
2706/// Durable spawn handle store for subagent idempotency (EVE-535).
2707///
2708/// Maps `(parent_session_id, tool_call_id) → child_session_id` so that when
2709/// a parent's `act` is reclaimed mid-`wait_for_idle`, the tool can reattach
2710/// to the existing child instead of spawning a duplicate.
2711///
2712/// Lifecycle: claim → register_child_session → settle_spawn.
2713#[async_trait]
2714pub trait SubagentSpawnStore: Send + Sync + 'static {
2715    /// Attempt to claim a spawn slot for `(parent_session_id, tool_call_id)`.
2716    ///
2717    /// Does NOT accept `child_session_id` — the child session does not exist yet.
2718    /// Call `register_child_session` with the actual child ID after creating it.
2719    async fn try_claim_spawn(
2720        &self,
2721        parent_session_id: crate::typed_id::SessionId,
2722        tool_call_id: &str,
2723        claim_token: uuid::Uuid,
2724    ) -> Result<SpawnClaimResult>;
2725
2726    /// Register the actual child session ID after it has been created.
2727    ///
2728    /// Must be called after `try_claim_spawn` returns `Claimed` or
2729    /// `ClaimedPendingChild`, before waiting for the child to complete.
2730    async fn register_child_session(
2731        &self,
2732        spawn_handle_id: uuid::Uuid,
2733        claim_token: uuid::Uuid,
2734        child_session_id: crate::typed_id::SessionId,
2735    ) -> Result<()>;
2736
2737    /// Record the terminal result once the child has completed.
2738    ///
2739    /// `claim_token` must match the stored token. `terminal_status` is the
2740    /// `wait_for_idle` return value ("idle", "error", "timeout", etc.) and
2741    /// `terminal_result` is the last agent message.
2742    async fn settle_spawn(
2743        &self,
2744        parent_session_id: crate::typed_id::SessionId,
2745        tool_call_id: &str,
2746        claim_token: uuid::Uuid,
2747        terminal_status: &str,
2748        terminal_result: &str,
2749    ) -> Result<()>;
2750}
2751
2752/// Blanket impl: `Arc<S>` delegates to the inner store.
2753#[async_trait]
2754impl<S: SubagentSpawnStore + ?Sized> SubagentSpawnStore for Arc<S> {
2755    async fn try_claim_spawn(
2756        &self,
2757        parent_session_id: crate::typed_id::SessionId,
2758        tool_call_id: &str,
2759        claim_token: uuid::Uuid,
2760    ) -> Result<SpawnClaimResult> {
2761        (**self)
2762            .try_claim_spawn(parent_session_id, tool_call_id, claim_token)
2763            .await
2764    }
2765
2766    async fn register_child_session(
2767        &self,
2768        spawn_handle_id: uuid::Uuid,
2769        claim_token: uuid::Uuid,
2770        child_session_id: crate::typed_id::SessionId,
2771    ) -> Result<()> {
2772        (**self)
2773            .register_child_session(spawn_handle_id, claim_token, child_session_id)
2774            .await
2775    }
2776
2777    async fn settle_spawn(
2778        &self,
2779        parent_session_id: crate::typed_id::SessionId,
2780        tool_call_id: &str,
2781        claim_token: uuid::Uuid,
2782        terminal_status: &str,
2783        terminal_result: &str,
2784    ) -> Result<()> {
2785        (**self)
2786            .settle_spawn(
2787                parent_session_id,
2788                tool_call_id,
2789                claim_token,
2790                terminal_status,
2791                terminal_result,
2792            )
2793            .await
2794    }
2795}
2796
2797/// No-op spawn store — used when no durable store is configured (dev/test).
2798///
2799/// Always claims (no dedup); settle and register are no-ops.
2800pub struct NoopSubagentSpawnStore;
2801
2802#[async_trait]
2803impl SubagentSpawnStore for NoopSubagentSpawnStore {
2804    async fn try_claim_spawn(
2805        &self,
2806        _parent_session_id: crate::typed_id::SessionId,
2807        _tool_call_id: &str,
2808        claim_token: uuid::Uuid,
2809    ) -> Result<SpawnClaimResult> {
2810        Ok(SpawnClaimResult::Claimed {
2811            spawn_handle_id: uuid::Uuid::new_v4(),
2812            claim_token,
2813        })
2814    }
2815
2816    async fn register_child_session(
2817        &self,
2818        _spawn_handle_id: uuid::Uuid,
2819        _claim_token: uuid::Uuid,
2820        _child_session_id: crate::typed_id::SessionId,
2821    ) -> Result<()> {
2822        Ok(())
2823    }
2824
2825    async fn settle_spawn(
2826        &self,
2827        _parent_session_id: crate::typed_id::SessionId,
2828        _tool_call_id: &str,
2829        _claim_token: uuid::Uuid,
2830        _terminal_status: &str,
2831        _terminal_result: &str,
2832    ) -> Result<()> {
2833        Ok(())
2834    }
2835}
2836
2837// ============================================================================
2838// Tests
2839// ============================================================================
2840
2841#[cfg(test)]
2842mod tests {
2843    use super::*;
2844
2845    #[test]
2846    fn test_resolved_image_new() {
2847        let image = ResolvedImage::new("SGVsbG8=", "image/png");
2848        assert_eq!(image.base64, "SGVsbG8=");
2849        assert_eq!(image.media_type, "image/png");
2850    }
2851
2852    #[test]
2853    fn test_resolved_image_to_data_url() {
2854        let image = ResolvedImage::new("SGVsbG8=", "image/png");
2855        let data_url = image.to_data_url();
2856        assert_eq!(data_url, "data:image/png;base64,SGVsbG8=");
2857    }
2858
2859    #[test]
2860    fn test_resolved_image_jpeg() {
2861        let image = ResolvedImage::new("base64data", "image/jpeg");
2862        let data_url = image.to_data_url();
2863        assert!(data_url.starts_with("data:image/jpeg;base64,"));
2864    }
2865}