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::{FileInfo, FileStat, GrepMatch, InitialFile, SessionFile};
12use crate::tool_types::{ToolCall, ToolDefinition, ToolResult};
13use crate::typed_id::{AgentId, HarnessId, ImageId, ModelId, SessionId, WorkspaceId};
14use async_trait::async_trait;
15use chrono::{DateTime, Utc};
16use std::any::{Any, TypeId};
17use std::collections::{HashMap, HashSet};
18use std::sync::Arc;
19use uuid::Uuid;
20
21fn workspace_display_path(path: &str) -> String {
22    if path == "/" {
23        "/workspace".to_string()
24    } else if path.starts_with('/') {
25        format!("/workspace{path}")
26    } else {
27        format!("/workspace/{path}")
28    }
29}
30
31/// Build a map of tool names to definitions for efficient lookup
32fn build_tool_map(tool_defs: &[ToolDefinition]) -> HashMap<&str, &ToolDefinition> {
33    tool_defs.iter().map(|def| (def.name(), def)).collect()
34}
35
36use crate::error::Result;
37
38// ============================================================================
39// AgentStore - For retrieving agent configurations
40// ============================================================================
41
42/// Trait for retrieving agent configurations
43///
44/// Implementations can:
45/// - Load agents from a database
46/// - Keep agents in memory for testing
47/// - Load agents from a configuration file
48#[async_trait]
49pub trait AgentStore: Send + Sync {
50    /// Get an agent by ID
51    async fn get_agent(&self, agent_id: AgentId) -> Result<Option<Agent>>;
52}
53
54#[async_trait]
55impl<T: AgentStore + ?Sized> AgentStore for std::sync::Arc<T> {
56    async fn get_agent(&self, agent_id: AgentId) -> Result<Option<Agent>> {
57        (**self).get_agent(agent_id).await
58    }
59}
60
61// ============================================================================
62// HarnessStore - For retrieving harness configurations
63// ============================================================================
64
65/// Trait for retrieving harness configurations
66///
67/// Implementations can:
68/// - Load harnesses from a database
69/// - Keep harnesses in memory for testing
70///
71/// Returns the harness inheritance chain (root-to-leaf) so the caller
72/// can fold each harness as an `AgentConfigOverlay`. DB-backed stores
73/// return the raw chain; gRPC-backed stores may return a single
74/// pre-merged harness (functionally equivalent when folded).
75#[async_trait]
76pub trait HarnessStore: Send + Sync {
77    /// Get the harness inheritance chain, root-to-leaf.
78    ///
79    /// Returns `Ok(vec![])` if the harness does not exist.
80    /// A harness with no parent returns a single-element vec.
81    async fn get_harness_chain(&self, harness_id: HarnessId) -> Result<Vec<Harness>>;
82}
83
84#[async_trait]
85impl<T: HarnessStore + ?Sized> HarnessStore for std::sync::Arc<T> {
86    async fn get_harness_chain(&self, harness_id: HarnessId) -> Result<Vec<Harness>> {
87        (**self).get_harness_chain(harness_id).await
88    }
89}
90
91// ============================================================================
92// SessionStore - For retrieving session information
93// ============================================================================
94
95use crate::leased_resource::{LeasedResource, UpsertLeasedResource};
96use crate::session::Session;
97
98/// Trait for retrieving session configurations
99///
100/// Implementations can:
101/// - Load sessions from a database
102/// - Keep sessions in memory for testing
103#[async_trait]
104pub trait SessionStore: Send + Sync {
105    /// Get a session by ID
106    async fn get_session(&self, session_id: SessionId) -> Result<Option<Session>>;
107}
108
109#[async_trait]
110impl<T: SessionStore + ?Sized> SessionStore for std::sync::Arc<T> {
111    async fn get_session(&self, session_id: SessionId) -> Result<Option<Session>> {
112        (**self).get_session(session_id).await
113    }
114}
115
116/// Trait for updating mutable session metadata.
117#[async_trait]
118pub trait SessionMutator: Send + Sync {
119    /// Update a session's human-readable title.
120    async fn update_session_title(&self, session_id: SessionId, title: String) -> Result<Session>;
121}
122
123#[async_trait]
124impl<T: SessionMutator + ?Sized> SessionMutator for std::sync::Arc<T> {
125    async fn update_session_title(&self, session_id: SessionId, title: String) -> Result<Session> {
126        (**self).update_session_title(session_id, title).await
127    }
128}
129
130// ============================================================================
131// ProviderStore - For retrieving LLM provider configurations
132// ============================================================================
133
134/// Model information with provider details needed for LLM calls
135#[derive(Debug, Clone)]
136pub struct ResolvedModel {
137    /// The model ID string to pass to the LLM API (e.g., "gpt-4o", "claude-3-opus")
138    pub model: String,
139    /// Provider type for factory selection
140    pub provider_type: DriverId,
141    /// Decrypted API key (if configured)
142    pub api_key: Option<String>,
143    /// Optional base URL override
144    pub base_url: Option<String>,
145    /// Extra provider-specific metadata (OAuth tokens, account ids, etc.).
146    /// Used by embedder-defined providers that authenticate without an API key.
147    pub provider_metadata: Option<crate::driver_registry::ProviderMetadata>,
148}
149
150/// Trait for retrieving LLM provider and model configurations
151///
152/// This trait abstracts the database lookup and API key decryption needed
153/// to create LLM providers at runtime.
154///
155/// Implementations can:
156/// - Load from a database with encrypted API keys
157/// - Use in-memory configurations for testing
158/// - Load from environment variables for development
159#[async_trait]
160pub trait ProviderStore: Send + Sync {
161    /// Get model with provider info by model ID
162    ///
163    /// Returns the model string ID, provider type, decrypted API key, and base URL
164    /// needed to create an LLM provider via the factory.
165    async fn get_resolved_model(&self, model_id: ModelId) -> Result<Option<ResolvedModel>>;
166
167    /// Get the default model with provider info
168    ///
169    /// Returns the system default model when an agent has no default_model_id set.
170    async fn get_default_model(&self) -> Result<Option<ResolvedModel>>;
171}
172
173#[async_trait]
174impl<T: ProviderStore + ?Sized> ProviderStore for std::sync::Arc<T> {
175    async fn get_resolved_model(&self, model_id: ModelId) -> Result<Option<ResolvedModel>> {
176        (**self).get_resolved_model(model_id).await
177    }
178
179    async fn get_default_model(&self) -> Result<Option<ResolvedModel>> {
180        (**self).get_default_model().await
181    }
182}
183
184// ============================================================================
185// ImageArtifactStore - For durable image persistence from tools
186// ============================================================================
187
188/// Metadata for a stored image artifact.
189#[derive(Debug, Clone)]
190pub struct StoredImageInfo {
191    pub id: ImageId,
192    pub filename: String,
193    pub content_type: String,
194    pub size_bytes: i64,
195    pub metadata: serde_json::Value,
196    pub created_at: DateTime<Utc>,
197}
198
199/// Stored image artifact with binary data.
200#[derive(Debug, Clone)]
201pub struct StoredImage {
202    pub info: StoredImageInfo,
203    pub data: Vec<u8>,
204}
205
206/// Input for creating a stored image artifact.
207#[derive(Debug, Clone)]
208pub struct CreateStoredImage {
209    pub filename: String,
210    pub content_type: String,
211    pub data: Vec<u8>,
212    pub metadata: serde_json::Value,
213}
214
215#[async_trait]
216pub trait ImageArtifactStore: Send + Sync {
217    /// Persist an image artifact and return its durable metadata.
218    async fn create_image(&self, input: CreateStoredImage) -> Result<StoredImageInfo>;
219
220    /// Load a stored image artifact including bytes.
221    async fn get_image(&self, image_id: ImageId) -> Result<Option<StoredImage>>;
222
223    /// Load stored image metadata without binary data.
224    async fn get_image_info(&self, image_id: ImageId) -> Result<Option<StoredImageInfo>>;
225}
226
227// ============================================================================
228// ProviderCredentialStore - For tool-side provider credential resolution
229// ============================================================================
230
231/// Provider credentials resolved for tool-side API clients.
232#[derive(Debug, Clone)]
233pub struct ProviderCredentials {
234    pub api_key: String,
235    pub base_url: Option<String>,
236}
237
238#[async_trait]
239pub trait ProviderCredentialStore: Send + Sync {
240    /// Resolve default credentials for a provider type (for example `openai`).
241    ///
242    /// Implementations may apply environment fallbacks internally, but tools
243    /// should never read provider env vars directly.
244    async fn get_default_provider_credentials(
245        &self,
246        provider_type: &str,
247    ) -> Result<Option<ProviderCredentials>>;
248}
249
250// ============================================================================
251// ToolExecutor - For executing tool calls
252// ============================================================================
253
254/// Trait for executing tool calls
255///
256/// Implementations handle the actual tool execution:
257/// - Webhook calls
258/// - Built-in function execution
259/// - Mock execution for testing
260#[async_trait]
261pub trait ToolExecutor: Send + Sync {
262    /// Execute a single tool call (without context)
263    ///
264    /// This is the legacy method that doesn't provide context to tools.
265    /// Use `execute_with_context` when context is available.
266    async fn execute(&self, tool_call: &ToolCall, tool_def: &ToolDefinition) -> Result<ToolResult>;
267
268    /// Execute a single tool call with context
269    ///
270    /// This method provides runtime context to tools that need it (like filesystem tools).
271    /// The default implementation delegates to `execute()`.
272    async fn execute_with_context(
273        &self,
274        tool_call: &ToolCall,
275        tool_def: &ToolDefinition,
276        _context: &ToolContext,
277    ) -> Result<ToolResult> {
278        // Default: delegate to execute(), ignoring context
279        self.execute(tool_call, tool_def).await
280    }
281
282    /// Execute multiple tool calls (default: sequential)
283    async fn execute_batch(
284        &self,
285        tool_calls: &[ToolCall],
286        tool_defs: &[ToolDefinition],
287    ) -> Result<Vec<ToolResult>> {
288        let mut results = Vec::with_capacity(tool_calls.len());
289
290        let tool_map = build_tool_map(tool_defs);
291
292        for tool_call in tool_calls {
293            let tool_def = tool_map.get(tool_call.name.as_str()).ok_or_else(|| {
294                crate::error::AgentLoopError::tool(format!(
295                    "Tool definition not found: {}",
296                    tool_call.name
297                ))
298            })?;
299
300            results.push(self.execute(tool_call, tool_def).await?);
301        }
302
303        Ok(results)
304    }
305
306    /// Execute multiple tool calls in parallel
307    async fn execute_parallel(
308        &self,
309        tool_calls: &[ToolCall],
310        tool_defs: &[ToolDefinition],
311    ) -> Result<Vec<ToolResult>>
312    where
313        Self: Sized,
314    {
315        use futures::future::join_all;
316
317        let tool_map = build_tool_map(tool_defs);
318
319        let futures: Vec<_> = tool_calls
320            .iter()
321            .map(|tool_call| async {
322                let tool_def = tool_map.get(tool_call.name.as_str()).ok_or_else(|| {
323                    crate::error::AgentLoopError::tool(format!(
324                        "Tool definition not found: {}",
325                        tool_call.name
326                    ))
327                })?;
328                self.execute(tool_call, tool_def).await
329            })
330            .collect();
331
332        let results = join_all(futures).await;
333        results.into_iter().collect()
334    }
335}
336
337/// Delegating impl so callers can hold a `ToolExecutor` as a trait object
338/// (e.g. to choose between a plain registry and an MCP-routing composite at
339/// runtime without monomorphizing the consumer).
340#[async_trait]
341impl ToolExecutor for std::sync::Arc<dyn ToolExecutor> {
342    async fn execute(&self, tool_call: &ToolCall, tool_def: &ToolDefinition) -> Result<ToolResult> {
343        (**self).execute(tool_call, tool_def).await
344    }
345
346    async fn execute_with_context(
347        &self,
348        tool_call: &ToolCall,
349        tool_def: &ToolDefinition,
350        context: &ToolContext,
351    ) -> Result<ToolResult> {
352        (**self)
353            .execute_with_context(tool_call, tool_def, context)
354            .await
355    }
356
357    async fn execute_batch(
358        &self,
359        tool_calls: &[ToolCall],
360        tool_defs: &[ToolDefinition],
361    ) -> Result<Vec<ToolResult>> {
362        (**self).execute_batch(tool_calls, tool_defs).await
363    }
364}
365
366// ============================================================================
367// SessionFileSystem - For session filesystem operations
368// ============================================================================
369
370/// Trait for session filesystem operations
371///
372/// This trait abstracts the session filesystem contract for tools and hosts.
373/// Implementations can:
374/// - Store files in a database (production)
375/// - Use an in-memory filesystem for testing
376/// - Project files onto real disk or object storage
377#[async_trait]
378pub trait SessionFileSystem: Send + Sync {
379    /// Human-facing root path for this filesystem.
380    ///
381    /// `/workspace` remains the stable agent namespace, but embedded runtimes
382    /// backed by a host directory can expose the real root here so shared
383    /// capabilities can avoid misleading users about where files live.
384    fn display_root(&self) -> String {
385        "/workspace".to_string()
386    }
387
388    /// Convert a canonical session path into a human-facing path.
389    fn display_path(&self, path: &str) -> String {
390        workspace_display_path(path)
391    }
392
393    /// Read a file by path
394    async fn read_file(&self, session_id: SessionId, path: &str) -> Result<Option<SessionFile>>;
395
396    /// Write/create a file
397    async fn write_file(
398        &self,
399        session_id: SessionId,
400        path: &str,
401        content: &str,
402        encoding: &str,
403    ) -> Result<SessionFile>;
404
405    /// Write a file only if its current content snapshot still matches.
406    ///
407    /// Implementations backed by transactional storage should override this
408    /// with an atomic compare-and-set update.
409    async fn write_file_if_content_matches(
410        &self,
411        session_id: SessionId,
412        path: &str,
413        expected_content: &str,
414        expected_encoding: &str,
415        content: &str,
416        encoding: &str,
417    ) -> Result<Option<SessionFile>> {
418        let Some(existing) = self.read_file(session_id, path).await? else {
419            return Ok(None);
420        };
421
422        if existing.is_directory {
423            return Ok(None);
424        }
425
426        let current_content = existing.content.unwrap_or_default();
427        if current_content != expected_content || existing.encoding != expected_encoding {
428            return Ok(None);
429        }
430
431        self.write_file(session_id, path, content, encoding)
432            .await
433            .map(Some)
434    }
435
436    /// Delete a file or directory
437    async fn delete_file(&self, session_id: SessionId, path: &str, recursive: bool)
438    -> Result<bool>;
439
440    /// List files in a directory
441    async fn list_directory(&self, session_id: SessionId, path: &str) -> Result<Vec<FileInfo>>;
442
443    /// Get file metadata
444    async fn stat_file(&self, session_id: SessionId, path: &str) -> Result<Option<FileStat>>;
445
446    /// Search files by pattern (grep)
447    async fn grep_files(
448        &self,
449        session_id: SessionId,
450        pattern: &str,
451        path_pattern: Option<&str>,
452    ) -> Result<Vec<GrepMatch>>;
453
454    /// Create a directory
455    async fn create_directory(&self, session_id: SessionId, path: &str) -> Result<FileInfo>;
456
457    /// Seed a starter file into a session workspace.
458    async fn seed_initial_file(&self, session_id: SessionId, file: &InitialFile) -> Result<()> {
459        if file.is_readonly {
460            return Err(crate::error::AgentLoopError::store(
461                "read-only initial files require a SessionFileSystem-specific seed implementation",
462            ));
463        }
464        self.write_file(session_id, &file.path, &file.content, &file.encoding)
465            .await?;
466        Ok(())
467    }
468}
469
470/// A [`SessionFileSystem`] decorator that pins every operation to a fixed
471/// workspace key, ignoring the per-call `session_id`.
472///
473/// Used to re-key file I/O for a session attached to a shared workspace (where
474/// `workspace.id != session.id`): wrap the session's file store once with the
475/// session's `workspace_id`, and all downstream capability/tool access then
476/// addresses the attached workspace rather than the session's own keyspace. For
477/// the default 1:1 session the key equals the session id, so the wrapper is a
478/// transparent pass-through. See `specs/workspace.md`.
479pub struct WorkspaceScopedFileSystem {
480    inner: Arc<dyn SessionFileSystem>,
481    key: SessionId,
482}
483
484impl WorkspaceScopedFileSystem {
485    /// Wrap `inner`, pinning all operations to `workspace_id`'s key.
486    pub fn wrap(
487        inner: Arc<dyn SessionFileSystem>,
488        workspace_id: WorkspaceId,
489    ) -> Arc<dyn SessionFileSystem> {
490        Arc::new(Self {
491            inner,
492            key: SessionId::from_uuid(workspace_id.uuid()),
493        })
494    }
495}
496
497#[async_trait]
498impl SessionFileSystem for WorkspaceScopedFileSystem {
499    async fn read_file(&self, _session_id: SessionId, path: &str) -> Result<Option<SessionFile>> {
500        self.inner.read_file(self.key, path).await
501    }
502    async fn write_file(
503        &self,
504        _session_id: SessionId,
505        path: &str,
506        content: &str,
507        encoding: &str,
508    ) -> Result<SessionFile> {
509        self.inner
510            .write_file(self.key, path, content, encoding)
511            .await
512    }
513    async fn write_file_if_content_matches(
514        &self,
515        _session_id: SessionId,
516        path: &str,
517        expected_content: &str,
518        expected_encoding: &str,
519        content: &str,
520        encoding: &str,
521    ) -> Result<Option<SessionFile>> {
522        self.inner
523            .write_file_if_content_matches(
524                self.key,
525                path,
526                expected_content,
527                expected_encoding,
528                content,
529                encoding,
530            )
531            .await
532    }
533    async fn delete_file(
534        &self,
535        _session_id: SessionId,
536        path: &str,
537        recursive: bool,
538    ) -> Result<bool> {
539        self.inner.delete_file(self.key, path, recursive).await
540    }
541    async fn list_directory(&self, _session_id: SessionId, path: &str) -> Result<Vec<FileInfo>> {
542        self.inner.list_directory(self.key, path).await
543    }
544    async fn stat_file(&self, _session_id: SessionId, path: &str) -> Result<Option<FileStat>> {
545        self.inner.stat_file(self.key, path).await
546    }
547    async fn grep_files(
548        &self,
549        _session_id: SessionId,
550        pattern: &str,
551        path_pattern: Option<&str>,
552    ) -> Result<Vec<GrepMatch>> {
553        self.inner.grep_files(self.key, pattern, path_pattern).await
554    }
555    async fn create_directory(&self, _session_id: SessionId, path: &str) -> Result<FileInfo> {
556        self.inner.create_directory(self.key, path).await
557    }
558    async fn seed_initial_file(&self, _session_id: SessionId, file: &InitialFile) -> Result<()> {
559        self.inner.seed_initial_file(self.key, file).await
560    }
561
562    fn display_root(&self) -> String {
563        self.inner.display_root()
564    }
565
566    fn display_path(&self, path: &str) -> String {
567        self.inner.display_path(path)
568    }
569}
570
571#[async_trait]
572impl<T: SessionFileSystem + ?Sized> SessionFileSystem for std::sync::Arc<T> {
573    fn display_root(&self) -> String {
574        (**self).display_root()
575    }
576
577    fn display_path(&self, path: &str) -> String {
578        (**self).display_path(path)
579    }
580
581    async fn read_file(&self, session_id: SessionId, path: &str) -> Result<Option<SessionFile>> {
582        (**self).read_file(session_id, path).await
583    }
584
585    async fn write_file(
586        &self,
587        session_id: SessionId,
588        path: &str,
589        content: &str,
590        encoding: &str,
591    ) -> Result<SessionFile> {
592        (**self)
593            .write_file(session_id, path, content, encoding)
594            .await
595    }
596
597    async fn write_file_if_content_matches(
598        &self,
599        session_id: SessionId,
600        path: &str,
601        expected_content: &str,
602        expected_encoding: &str,
603        content: &str,
604        encoding: &str,
605    ) -> Result<Option<SessionFile>> {
606        (**self)
607            .write_file_if_content_matches(
608                session_id,
609                path,
610                expected_content,
611                expected_encoding,
612                content,
613                encoding,
614            )
615            .await
616    }
617
618    async fn delete_file(
619        &self,
620        session_id: SessionId,
621        path: &str,
622        recursive: bool,
623    ) -> Result<bool> {
624        (**self).delete_file(session_id, path, recursive).await
625    }
626
627    async fn list_directory(&self, session_id: SessionId, path: &str) -> Result<Vec<FileInfo>> {
628        (**self).list_directory(session_id, path).await
629    }
630
631    async fn stat_file(&self, session_id: SessionId, path: &str) -> Result<Option<FileStat>> {
632        (**self).stat_file(session_id, path).await
633    }
634
635    async fn grep_files(
636        &self,
637        session_id: SessionId,
638        pattern: &str,
639        path_pattern: Option<&str>,
640    ) -> Result<Vec<GrepMatch>> {
641        (**self).grep_files(session_id, pattern, path_pattern).await
642    }
643
644    async fn create_directory(&self, session_id: SessionId, path: &str) -> Result<FileInfo> {
645        (**self).create_directory(session_id, path).await
646    }
647
648    async fn seed_initial_file(&self, session_id: SessionId, file: &InitialFile) -> Result<()> {
649        (**self).seed_initial_file(session_id, file).await
650    }
651}
652
653/// Backward-compatible alias for the old session filesystem trait name.
654pub use SessionFileSystem as SessionFileStore;
655
656/// Host-supplied values used by platform file-system factories.
657///
658/// The context is intentionally type-erased so `everruns-core` can own the
659/// platform contract without depending on server-only types such as
660/// `StorageBackend` or future object-storage clients.
661#[derive(Clone, Default)]
662pub struct SessionFileSystemFactoryContext {
663    values: Arc<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>,
664}
665
666impl SessionFileSystemFactoryContext {
667    pub fn new() -> Self {
668        Self::default()
669    }
670
671    pub fn with<T: Any + Send + Sync>(mut self, value: Arc<T>) -> Self {
672        let values = Arc::make_mut(&mut self.values);
673        values.insert(TypeId::of::<T>(), value);
674        self
675    }
676
677    pub fn get<T: Any + Send + Sync>(&self) -> Option<Arc<T>> {
678        self.values
679            .get(&TypeId::of::<T>())
680            .and_then(|value| value.clone().downcast::<T>().ok())
681    }
682}
683
684/// Factory for deployment-selected session filesystem implementations.
685#[async_trait]
686pub trait SessionFileSystemFactory: Send + Sync {
687    /// Human-readable factory name for diagnostics.
688    fn name(&self) -> &'static str {
689        "SessionFileSystemFactory"
690    }
691
692    /// Whether this factory intentionally leaves filesystem selection to the
693    /// runtime default.
694    fn is_disabled(&self) -> bool {
695        false
696    }
697
698    /// Resolve a live filesystem from host-provided dependencies.
699    async fn create_session_file_system(
700        &self,
701        context: SessionFileSystemFactoryContext,
702    ) -> Result<Arc<dyn SessionFileSystem>>;
703}
704
705/// Default factory used when a platform does not configure session files.
706#[derive(Debug, Clone, Default)]
707pub struct DisabledSessionFileSystemFactory;
708
709#[async_trait]
710impl SessionFileSystemFactory for DisabledSessionFileSystemFactory {
711    fn name(&self) -> &'static str {
712        "DisabledSessionFileSystemFactory"
713    }
714
715    fn is_disabled(&self) -> bool {
716        true
717    }
718
719    async fn create_session_file_system(
720        &self,
721        _context: SessionFileSystemFactoryContext,
722    ) -> Result<Arc<dyn SessionFileSystem>> {
723        Err(crate::error::AgentLoopError::config(
724            "session filesystem is disabled",
725        ))
726    }
727}
728
729// ============================================================================
730// SessionStorageStore - For session key/value and secret storage
731// ============================================================================
732
733/// Info about a stored key (without its value)
734#[derive(Debug, Clone)]
735pub struct KeyInfo {
736    pub key: String,
737    pub created_at: chrono::DateTime<chrono::Utc>,
738    pub updated_at: chrono::DateTime<chrono::Utc>,
739}
740
741/// Info about a stored secret (without its value)
742#[derive(Debug, Clone)]
743pub struct SecretInfo {
744    pub name: String,
745    pub created_at: chrono::DateTime<chrono::Utc>,
746    pub updated_at: chrono::DateTime<chrono::Utc>,
747}
748
749/// Trait for session key/value and secret storage operations
750///
751/// This trait abstracts storage operations for tools that need to persist
752/// data within a session. Implementations can:
753/// - Store data in a database (production)
754/// - Use in-memory storage for testing
755///
756/// Key/value storage is for general data that doesn't need encryption.
757/// Secret storage is for sensitive data that is encrypted at rest.
758#[async_trait]
759pub trait SessionStorageStore: Send + Sync {
760    // Key/Value operations (plain text)
761
762    /// Set a key/value pair (creates or updates)
763    async fn set_value(&self, session_id: SessionId, key: &str, value: &str) -> Result<()>;
764
765    /// Get a value by key
766    async fn get_value(&self, session_id: SessionId, key: &str) -> Result<Option<String>>;
767
768    /// Delete a key/value pair
769    async fn delete_value(&self, session_id: SessionId, key: &str) -> Result<bool>;
770
771    /// List all keys in a session
772    async fn list_keys(&self, session_id: SessionId) -> Result<Vec<KeyInfo>>;
773
774    // Secret operations (encrypted)
775
776    /// Set a secret (creates or updates, value is encrypted before storage)
777    async fn set_secret(&self, session_id: SessionId, name: &str, value: &str) -> Result<()>;
778
779    /// Get a secret by name (value is decrypted before returning)
780    async fn get_secret(&self, session_id: SessionId, name: &str) -> Result<Option<String>>;
781
782    /// Delete a secret
783    async fn delete_secret(&self, session_id: SessionId, name: &str) -> Result<bool>;
784
785    /// List all secret names in a session (without values)
786    async fn list_secrets(&self, session_id: SessionId) -> Result<Vec<SecretInfo>>;
787}
788
789// ============================================================================
790// SessionScheduleStore - For session-scoped schedule operations
791// ============================================================================
792
793use crate::session_schedule::SessionSchedule;
794use crate::typed_id::ScheduleId;
795
796/// Trait for session schedule CRUD operations.
797///
798/// Used by scheduling tools to create, cancel, and list schedules.
799#[async_trait]
800pub trait SessionScheduleStore: Send + Sync {
801    /// Create a new schedule for a session.
802    async fn create_schedule(
803        &self,
804        session_id: SessionId,
805        description: String,
806        cron_expression: Option<String>,
807        scheduled_at: Option<chrono::DateTime<chrono::Utc>>,
808        timezone: String,
809    ) -> Result<SessionSchedule>;
810
811    /// Cancel (disable) a schedule.
812    async fn cancel_schedule(
813        &self,
814        session_id: SessionId,
815        schedule_id: ScheduleId,
816    ) -> Result<SessionSchedule>;
817
818    /// List schedules for a session.
819    async fn list_schedules(&self, session_id: SessionId) -> Result<Vec<SessionSchedule>>;
820
821    /// Count active (enabled) schedules for a session.
822    async fn count_active_schedules(&self, session_id: SessionId) -> Result<u32>;
823}
824
825// ============================================================================
826// SessionResourceRegistry - Generic session-scoped resource registry
827// ============================================================================
828
829/// Generic registry of resources active alongside a session.
830///
831/// Capabilities register resources here (sandboxes, subagents, browser sessions).
832/// Agents query it ("what's running?"), infrastructure scans it for cleanup.
833/// See `specs/session-resources.md`.
834#[async_trait]
835pub trait SessionResourceRegistry: Send + Sync {
836    /// Register a resource (or update if resource_id already exists for this session).
837    async fn register(
838        &self,
839        entry: crate::session_resource::RegisterSessionResource,
840    ) -> Result<crate::session_resource::SessionResourceEntry>;
841
842    /// Update the status of a registered resource.
843    async fn update_status(
844        &self,
845        session_id: SessionId,
846        resource_id: &str,
847        status: crate::session_resource::SessionResourceStatus,
848    ) -> Result<Option<crate::session_resource::SessionResourceEntry>>;
849
850    /// Get a specific resource by ID.
851    async fn get(
852        &self,
853        session_id: SessionId,
854        resource_id: &str,
855    ) -> Result<Option<crate::session_resource::SessionResourceEntry>>;
856
857    /// List resources for a session, optionally filtered.
858    async fn list(
859        &self,
860        session_id: SessionId,
861        filter: Option<&crate::session_resource::SessionResourceFilter>,
862    ) -> Result<Vec<crate::session_resource::SessionResourceEntry>>;
863
864    /// Remove a resource from the registry.
865    async fn deregister(&self, session_id: SessionId, resource_id: &str) -> Result<bool>;
866}
867
868// ============================================================================
869// LeasedResourceStore - For lifecycle-managed external resources
870// ============================================================================
871
872/// Trait for session-scoped leased resource operations.
873///
874/// Tools use this store to register or refresh leases when they create or use
875/// external provider resources. Cleanup workers operate through control-plane
876/// storage APIs directly so they can claim work across organizations.
877#[async_trait]
878pub trait LeasedResourceStore: Send + Sync {
879    /// Create or refresh a leased resource for a session.
880    ///
881    /// Implementations must treat this as an idempotent upsert keyed by the
882    /// provider-specific resource identity so repeated tool usage extends the
883    /// same lease instead of creating duplicate rows.
884    async fn upsert_resource(&self, input: UpsertLeasedResource) -> Result<LeasedResource>;
885
886    /// Mark a leased resource as explicitly released.
887    ///
888    /// This is the fast path for explicit user intent such as "close browser"
889    /// or "delete sandbox". It should transition the resource to `released`
890    /// without waiting for the durable cleanup worker to observe lease expiry.
891    async fn release_resource(
892        &self,
893        session_id: SessionId,
894        provider: &str,
895        resource_type: &str,
896        external_id: &str,
897    ) -> Result<Option<LeasedResource>>;
898
899    /// List leased resources currently associated with a session.
900    ///
901    /// Session surfaces use this for visibility. Released resources remain
902    /// visible so operators can inspect cleanup outcomes and failure history.
903    async fn list_resources(&self, session_id: SessionId) -> Result<Vec<LeasedResource>>;
904}
905
906// ============================================================================
907// ToolContext - Runtime context for tool execution
908// ============================================================================
909
910/// Type alias for the session SQL DB store trait object.
911pub type SessionSqlDbStoreRef = Arc<dyn crate::session_sqldb::SessionSqlDbStore>;
912
913/// Resolves user connection tokens (e.g. GitHub) lazily at tool execution time.
914///
915/// Instead of eagerly injecting tokens at session creation, tools call this
916/// resolver when they need a token. If the user hasn't connected, returns None.
917#[async_trait]
918pub trait UserConnectionResolver: Send + Sync {
919    /// Get a decrypted connection token for the given provider.
920    /// Returns None if the user has no connection for this provider.
921    async fn get_connection_token(
922        &self,
923        session_id: SessionId,
924        provider: &str,
925    ) -> Result<Option<String>>;
926
927    /// Resolve the user ID of the connection used for a session/provider pair.
928    ///
929    /// This is used by leased resources to bind cleanup to the same provider
930    /// identity that created the remote resource.
931    async fn get_connection_user(
932        &self,
933        _session_id: SessionId,
934        _provider: &str,
935    ) -> Result<Option<Uuid>> {
936        Ok(None)
937    }
938
939    /// Resolve a provider token for a specific user.
940    ///
941    /// Cleanup workers use this to avoid "first org member wins" behavior when
942    /// cleaning resources created by a specific provider connection owner.
943    async fn get_connection_token_for_user(
944        &self,
945        _user_id: Uuid,
946        _provider: &str,
947    ) -> Result<Option<String>> {
948        Ok(None)
949    }
950
951    /// Get provider-specific metadata stored alongside the connection.
952    /// Returns None if no metadata is stored or no connection exists.
953    async fn get_connection_metadata(
954        &self,
955        _session_id: SessionId,
956        _provider: &str,
957    ) -> Result<Option<serde_json::Value>> {
958        Ok(None)
959    }
960}
961
962// ============================================================================
963// BudgetChecker - For querying budget status from tools
964// ============================================================================
965
966/// Trait for checking budget status from within tool execution.
967///
968/// Implemented by gRPC adapters (worker → server) and direct adapters (in-process).
969/// Used by the `check_budget` tool to return real budget data to agents.
970/// The org_id is captured at construction time by the implementing adapter.
971#[async_trait]
972pub trait BudgetChecker: Send + Sync {
973    /// Check all budgets for a session and return a tool-friendly response.
974    async fn check_budgets(&self, session_id: &str) -> Result<crate::budget::BudgetToolResponse>;
975}
976
977// ============================================================================
978// PaymentAuthority - For capability-internal machine payments
979// ============================================================================
980
981/// Internal authority for paid capability operations.
982///
983/// Capabilities call this with fixed, typed requests. The model never receives a
984/// generic paid HTTP tool, wallet credentials, or payment payloads.
985#[async_trait]
986pub trait PaymentAuthority: Send + Sync {
987    async fn execute_machine_payment(
988        &self,
989        session_id: SessionId,
990        request: crate::payment::MachinePaymentRequest,
991    ) -> Result<crate::payment::MachinePaymentResponse>;
992}
993
994// OutboundToolRateLimiter - Per-org outbound tool-call rate limiting (TM-TOOL-009)
995// ============================================================================
996
997/// Per-org gate on outbound tool execution.
998///
999/// Returns `true` if the call is within the per-org budget, `false` if the
1000/// org has exceeded its outbound tool rate limit for this window.
1001/// Implementations must be fail-open: Valkey/backend errors should return `true`
1002/// rather than blocking legitimate tool calls.
1003#[async_trait]
1004pub trait OutboundToolRateLimiter: Send + Sync {
1005    /// Key by the public org UUID (keyed string representation).
1006    async fn check_org(&self, org_id: &crate::typed_id::OrgId) -> bool;
1007}
1008
1009// ============================================================================
1010// DurableToolResultStore — per-tool-call idempotency (EVE-530)
1011// ============================================================================
1012
1013/// Result of a claim attempt on the per-tool-call idempotency store.
1014#[derive(Debug)]
1015pub enum ToolCallClaimResult {
1016    /// First claim for this (turn_id, tool_call_id); caller should execute the tool.
1017    /// `claim_token` must be passed to `settle_tool_call` to verify ownership.
1018    Claimed { claim_token: uuid::Uuid },
1019    /// A prior run already settled this call; replay the stored result.
1020    AlreadySettled {
1021        result_json: serde_json::Value,
1022        args_fingerprint: String,
1023    },
1024    /// A prior run started but never settled. For `AtMostOnce` tools the
1025    /// caller should NOT re-execute; for `Pure`/`Idempotent` tools the caller
1026    /// may re-execute and then try to settle (the settle CAS will be a no-op if
1027    /// a different claimer wins first).
1028    AlreadyRunning { args_fingerprint: String },
1029    /// A settled row exists but its `args_fingerprint` does not match the
1030    /// current call — this is a determinism violation (workflow replay with
1031    /// different inputs). The workflow should be failed loudly.
1032    DeterminismViolation {
1033        stored_fingerprint: String,
1034        current_fingerprint: String,
1035    },
1036}
1037
1038/// Read-only status of a tool call in durable storage (EVE-533).
1039#[derive(Debug, Clone)]
1040pub enum DurableToolCallStatus {
1041    /// Tool completed successfully or with an error; result is stored.
1042    Settled { result_json: serde_json::Value },
1043    /// Tool was settled with `interrupted` status; result may contain error details.
1044    Interrupted {
1045        result_json: Option<serde_json::Value>,
1046    },
1047    /// A claim exists but the tool never finished.
1048    Running,
1049}
1050
1051/// Durable per-tool-call idempotency store (EVE-530).
1052///
1053/// Implements the claim/settle CAS that prevents double-execution of
1054/// `AtMostOnce` tools on worker reclaim/replay.
1055#[async_trait]
1056pub trait DurableToolResultStore: Send + Sync + 'static {
1057    /// Atomically claim `(turn_id, tool_call_id)` before tool dispatch.
1058    ///
1059    /// - Inserts a `running` row if none exists → `Claimed`.
1060    /// - Finds an existing `settled` row → `AlreadySettled`.
1061    /// - Finds an existing `running` row → `AlreadyRunning`.
1062    /// - Finds a `settled` row with a mismatched `args_fingerprint`
1063    ///   (determinism violation) → `DeterminismViolation`.
1064    async fn try_claim_tool_call(
1065        &self,
1066        turn_id: &str,
1067        tool_call_id: &str,
1068        tool_name: &str,
1069        args_fingerprint: &str,
1070    ) -> Result<ToolCallClaimResult>;
1071
1072    /// Settle a previously claimed tool call with its result.
1073    ///
1074    /// `claim_token` must match the token returned by `try_claim_tool_call`.
1075    /// Returns `Ok(true)` if the row was updated, `Ok(false)` if the claim
1076    /// token no longer matches (ownership lost — treat as a warning).
1077    async fn settle_tool_call(
1078        &self,
1079        turn_id: &str,
1080        tool_call_id: &str,
1081        result_json: serde_json::Value,
1082        status: &str,
1083        claim_token: uuid::Uuid,
1084    ) -> Result<bool>;
1085
1086    /// Read-only lookup of a tool call's current status in durable storage (EVE-533).
1087    ///
1088    /// Used by transcript repair to decide whether to replay a stored result or
1089    /// synthesize an interrupted placeholder. Returns `None` if no row exists.
1090    async fn get_tool_call_status(
1091        &self,
1092        turn_id: &str,
1093        tool_call_id: &str,
1094    ) -> Result<Option<DurableToolCallStatus>>;
1095}
1096
1097/// No-op implementation — used when no durable store is configured (dev/test).
1098/// Every call is treated as a fresh first execution; no replay or ownership checks.
1099pub struct NoopDurableToolResultStore;
1100
1101#[async_trait]
1102impl DurableToolResultStore for NoopDurableToolResultStore {
1103    async fn try_claim_tool_call(
1104        &self,
1105        _turn_id: &str,
1106        _tool_call_id: &str,
1107        _tool_name: &str,
1108        _args_fingerprint: &str,
1109    ) -> Result<ToolCallClaimResult> {
1110        Ok(ToolCallClaimResult::Claimed {
1111            claim_token: uuid::Uuid::new_v4(),
1112        })
1113    }
1114
1115    async fn settle_tool_call(
1116        &self,
1117        _turn_id: &str,
1118        _tool_call_id: &str,
1119        _result_json: serde_json::Value,
1120        _status: &str,
1121        _claim_token: uuid::Uuid,
1122    ) -> Result<bool> {
1123        Ok(true)
1124    }
1125
1126    async fn get_tool_call_status(
1127        &self,
1128        _turn_id: &str,
1129        _tool_call_id: &str,
1130    ) -> Result<Option<DurableToolCallStatus>> {
1131        Ok(None)
1132    }
1133}
1134
1135// ============================================================================
1136// StreamHeartbeater — per-stream liveness signal for Reason activity (EVE-531)
1137// ============================================================================
1138
1139/// Progress snapshot carried in each stream heartbeat.
1140#[derive(Debug, Clone)]
1141pub struct StreamProgress {
1142    /// Accumulated text + thinking length (characters) at the time of heartbeat.
1143    pub accumulated_len: usize,
1144    /// Wall-clock time of the most recent received token (Unix seconds).
1145    pub last_delta_at: u64,
1146}
1147
1148/// Heartbeater the Reason streaming loop calls on delta batches and a keepalive
1149/// timer, signalling that the provider connection is alive.
1150///
1151/// Implementations bridge to the durable-execution layer (e.g. gRPC).
1152/// The no-op is used in dev/test where no durable store is present.
1153#[async_trait]
1154pub trait StreamHeartbeater: Send + Sync {
1155    /// Signal stream liveness with current progress.
1156    ///
1157    /// Must be best-effort: errors must not propagate to the caller.
1158    /// Cancel-safety is critical — if the worker dies the heartbeat stops
1159    /// and the existing task-level reclaim takes over.
1160    async fn heartbeat(&self, progress: StreamProgress);
1161}
1162
1163/// No-op heartbeater — treats every stream as perpetually alive (dev/test).
1164pub struct NoopStreamHeartbeater;
1165
1166#[async_trait]
1167impl StreamHeartbeater for NoopStreamHeartbeater {
1168    async fn heartbeat(&self, _progress: StreamProgress) {}
1169}
1170
1171// ============================================================================
1172// PartialStreamStore — partial-stream recovery for Reason activity (EVE-532)
1173// ============================================================================
1174
1175/// State of a partially-streamed assistant message detected in the event log.
1176#[derive(Debug, Clone)]
1177pub struct PartialStreamState {
1178    /// Accumulated text from the last `output.message.delta` for the turn.
1179    /// Empty when `output.message.started` was emitted but no delta arrived.
1180    pub accumulated: String,
1181}
1182
1183/// Consults the persisted event log to detect whether a `reason` activity
1184/// was interrupted after `output.message.started` but before
1185/// `output.message.completed` or `output.message.replaced`.
1186///
1187/// Used by `ReasonAtom` on re-entry to apply the ContinuePartial recovery
1188/// policy (EVE-532): finalize the partial text without a second provider call,
1189/// or restart clean if the partial is unusable.
1190#[async_trait]
1191pub trait PartialStreamStore: Send + Sync {
1192    /// Return the partial-stream state for `(session_id, turn_id)` if an
1193    /// in-flight assistant message exists (started but not completed).
1194    async fn get_partial_stream(
1195        &self,
1196        session_id: SessionId,
1197        turn_id: &str,
1198    ) -> Result<Option<PartialStreamState>>;
1199}
1200
1201/// No-op — always reports no partial stream (dev/test / in-memory mode).
1202pub struct NoopPartialStreamStore;
1203
1204#[async_trait]
1205impl PartialStreamStore for NoopPartialStreamStore {
1206    async fn get_partial_stream(
1207        &self,
1208        _session_id: SessionId,
1209        _turn_id: &str,
1210    ) -> Result<Option<PartialStreamState>> {
1211        Ok(None)
1212    }
1213}
1214
1215/// Runtime context provided to tools during execution.
1216///
1217/// This context contains:
1218/// - Session ID for scoping operations
1219/// - Optional stores for tools that need external access
1220///
1221/// Tools that need context-aware execution (like filesystem tools) can use
1222/// the `execute_with_context` method on the Tool trait.
1223#[derive(Clone)]
1224pub struct ToolContext {
1225    /// The session ID for the current execution
1226    pub session_id: SessionId,
1227    /// The workspace this session is attached to — the key for the virtual
1228    /// file store. For the default 1:1 session this equals
1229    /// `WorkspaceId::from_uuid(session_id.uuid())`; for a shared workspace it
1230    /// differs. File-system tools MUST key by this (via `workspace_fs_key`)
1231    /// rather than `session_id` so shared-workspace sessions read/write the
1232    /// attached workspace's files. See specs/workspace.md.
1233    pub workspace_id: WorkspaceId,
1234
1235    /// Optional file store for filesystem operations
1236    pub file_store: Option<Arc<dyn SessionFileSystem>>,
1237
1238    /// Optional storage store for key/value and secret storage
1239    pub storage_store: Option<Arc<dyn SessionStorageStore>>,
1240
1241    /// Optional durable image artifact store for tool-side media persistence.
1242    pub image_store: Option<Arc<dyn ImageArtifactStore>>,
1243
1244    /// Optional provider credential store for tool-side API clients.
1245    pub provider_credential_store: Option<Arc<dyn ProviderCredentialStore>>,
1246
1247    /// Optional system utility LLM service for capability internals.
1248    pub utility_llm_service: Option<Arc<dyn crate::UtilityLlmService>>,
1249
1250    /// Optional outbound egress service for HTTP/API traffic.
1251    pub egress_service: Option<Arc<dyn crate::EgressService>>,
1252
1253    /// Optional session SQL database store
1254    pub sqldb_store: Option<SessionSqlDbStoreRef>,
1255
1256    /// Optional message retriever for tools that need conversation history access
1257    pub message_retriever: Option<Arc<dyn crate::message_retriever::MessageRetriever>>,
1258
1259    /// Optional session store for tools that need session metadata access.
1260    pub session_store: Option<Arc<dyn SessionStore>>,
1261
1262    /// Optional session mutator for tools that need to update session metadata.
1263    pub session_mutator: Option<Arc<dyn SessionMutator>>,
1264
1265    /// Optional agent store for tools that need agent metadata access.
1266    pub agent_store: Option<Arc<dyn AgentStore>>,
1267
1268    /// Optional resolver for user connection tokens (lazy GitHub token lookup, etc.)
1269    pub connection_resolver: Option<Arc<dyn UserConnectionResolver>>,
1270
1271    /// Optional session schedule store for scheduling tools.
1272    pub schedule_store: Option<Arc<dyn SessionScheduleStore>>,
1273
1274    /// Optional platform store for org-level management tools.
1275    pub platform_store: Option<Arc<dyn crate::platform_store::PlatformStore>>,
1276    /// Optional leased resource store for lifecycle-managed provider resources.
1277    pub leased_resource_store: Option<Arc<dyn LeasedResourceStore>>,
1278
1279    /// Optional session resource registry — generic registry of active resources.
1280    pub session_resource_registry: Option<Arc<dyn SessionResourceRegistry>>,
1281
1282    /// Optional session task registry — background work owned by the session
1283    /// (specs/session-tasks.md).
1284    pub session_task_registry: Option<Arc<dyn crate::session_task::SessionTaskRegistry>>,
1285
1286    /// Optional event emitter for tools that need to stream progress updates.
1287    /// When set, tools can emit `tool.progress` events during execution.
1288    pub event_emitter: Option<Arc<dyn EventEmitter>>,
1289
1290    /// Event context for correlating progress events with the current tool call.
1291    /// Set by ActAtom when constructing the ToolContext.
1292    pub event_context: Option<crate::events::EventContext>,
1293
1294    /// The tool call ID for the current execution (set by ActAtom).
1295    /// Used by tools to emit correlated progress events.
1296    pub tool_call_id: Option<String>,
1297    /// Optional capability registry for blueprint lookups.
1298    pub capability_registry: Option<crate::capabilities::CapabilityRegistry>,
1299
1300    /// Optional registry of active built-in tools for meta-tools such as
1301    /// `spawn_background` that need to inspect or delegate to sibling tools.
1302    pub tool_registry: Option<Arc<crate::tools::ToolRegistry>>,
1303
1304    /// Optional allowlist of tools visible to the model for this turn.
1305    /// Registry-introspecting tools must filter through this before returning
1306    /// sibling tool metadata, because the execution registry can be a superset.
1307    pub visible_tool_names: Option<Arc<HashSet<String>>>,
1308
1309    /// Optional org ID for org-scoped operations.
1310    pub org_id: Option<crate::typed_id::OrgId>,
1311
1312    /// Merged network access list (harness ∩ agent ∩ session).
1313    /// When set, tools that make HTTP requests must check URLs against this list.
1314    pub network_access: Option<crate::network_access::NetworkAccessList>,
1315
1316    /// Resolved locale for localized tool behavior (BCP 47, e.g. `uk-UA`).
1317    /// When set, tools that support localization use this to produce
1318    /// locale-appropriate descriptions, error messages, and prompts.
1319    pub locale: Option<String>,
1320
1321    /// Optional budget checker for the check_budget tool.
1322    pub budget_checker: Option<Arc<dyn BudgetChecker>>,
1323
1324    /// Optional internal payment authority for paid capability tools.
1325    pub payment_authority: Option<Arc<dyn PaymentAuthority>>,
1326
1327    /// Optional durable spawn handle store for subagent reattach (EVE-535).
1328    /// When set, `spawn_subagent` uses claim/settle to prevent duplicate spawning
1329    /// on parent worker reclaim.
1330    pub subagent_spawn_store: Option<Arc<dyn SubagentSpawnStore>>,
1331}
1332
1333impl ToolContext {
1334    /// The virtual-file-store key for this execution, derived from the attached
1335    /// workspace. Carried through the `SessionFileSystem` trait's `SessionId`
1336    /// parameter (the store keys by `.uuid()`), so a shared-workspace session
1337    /// addresses the workspace's files rather than its own session-id keyspace.
1338    pub fn workspace_fs_key(&self) -> SessionId {
1339        SessionId::from_uuid(self.workspace_id.uuid())
1340    }
1341
1342    /// Override the attached workspace (default is the 1:1 session-derived id).
1343    pub fn with_workspace_id(mut self, workspace_id: WorkspaceId) -> Self {
1344        self.workspace_id = workspace_id;
1345        self
1346    }
1347
1348    /// Create a new tool context with just a session ID
1349    pub fn new(session_id: SessionId) -> Self {
1350        Self {
1351            session_id,
1352            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
1353            file_store: None,
1354            storage_store: None,
1355            image_store: None,
1356            provider_credential_store: None,
1357            utility_llm_service: None,
1358            egress_service: None,
1359            sqldb_store: None,
1360            message_retriever: None,
1361            session_store: None,
1362            session_mutator: None,
1363            agent_store: None,
1364            connection_resolver: None,
1365            schedule_store: None,
1366            platform_store: None,
1367            leased_resource_store: None,
1368            session_resource_registry: None,
1369            session_task_registry: None,
1370            event_emitter: None,
1371            event_context: None,
1372            tool_call_id: None,
1373            capability_registry: None,
1374            tool_registry: None,
1375            visible_tool_names: None,
1376            org_id: None,
1377            network_access: None,
1378            locale: None,
1379            budget_checker: None,
1380            payment_authority: None,
1381            subagent_spawn_store: None,
1382        }
1383    }
1384
1385    /// Create a context with a file store
1386    pub fn with_file_store(session_id: SessionId, file_store: Arc<dyn SessionFileSystem>) -> Self {
1387        Self {
1388            session_id,
1389            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
1390            file_store: Some(file_store),
1391            storage_store: None,
1392            image_store: None,
1393            provider_credential_store: None,
1394            utility_llm_service: None,
1395            egress_service: None,
1396            sqldb_store: None,
1397            message_retriever: None,
1398            session_store: None,
1399            session_mutator: None,
1400            agent_store: None,
1401            connection_resolver: None,
1402            schedule_store: None,
1403            platform_store: None,
1404            leased_resource_store: None,
1405            session_resource_registry: None,
1406            session_task_registry: None,
1407            event_emitter: None,
1408            event_context: None,
1409            tool_call_id: None,
1410            capability_registry: None,
1411            tool_registry: None,
1412            visible_tool_names: None,
1413            org_id: None,
1414            network_access: None,
1415            locale: None,
1416            budget_checker: None,
1417            payment_authority: None,
1418            subagent_spawn_store: None,
1419        }
1420    }
1421
1422    /// Create a context with a storage store
1423    pub fn with_storage_store(
1424        session_id: SessionId,
1425        storage_store: Arc<dyn SessionStorageStore>,
1426    ) -> Self {
1427        Self {
1428            session_id,
1429            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
1430            file_store: None,
1431            storage_store: Some(storage_store),
1432            image_store: None,
1433            provider_credential_store: None,
1434            utility_llm_service: None,
1435            egress_service: None,
1436            sqldb_store: None,
1437            message_retriever: None,
1438            session_store: None,
1439            session_mutator: None,
1440            agent_store: None,
1441            connection_resolver: None,
1442            schedule_store: None,
1443            platform_store: None,
1444            leased_resource_store: None,
1445            session_resource_registry: None,
1446            session_task_registry: None,
1447            event_emitter: None,
1448            event_context: None,
1449            tool_call_id: None,
1450            capability_registry: None,
1451            tool_registry: None,
1452            visible_tool_names: None,
1453            org_id: None,
1454            network_access: None,
1455            locale: None,
1456            budget_checker: None,
1457            payment_authority: None,
1458            subagent_spawn_store: None,
1459        }
1460    }
1461
1462    /// Create a context with both file store and storage store
1463    pub fn with_stores(
1464        session_id: SessionId,
1465        file_store: Arc<dyn SessionFileSystem>,
1466        storage_store: Arc<dyn SessionStorageStore>,
1467    ) -> Self {
1468        Self {
1469            session_id,
1470            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
1471            file_store: Some(file_store),
1472            storage_store: Some(storage_store),
1473            sqldb_store: None,
1474            image_store: None,
1475            provider_credential_store: None,
1476            utility_llm_service: None,
1477            egress_service: None,
1478            message_retriever: None,
1479            session_store: None,
1480            session_mutator: None,
1481            agent_store: None,
1482            connection_resolver: None,
1483            schedule_store: None,
1484            platform_store: None,
1485            leased_resource_store: None,
1486            session_resource_registry: None,
1487            session_task_registry: None,
1488            event_emitter: None,
1489            event_context: None,
1490            tool_call_id: None,
1491            capability_registry: None,
1492            tool_registry: None,
1493            visible_tool_names: None,
1494            org_id: None,
1495            network_access: None,
1496            locale: None,
1497            budget_checker: None,
1498            payment_authority: None,
1499            subagent_spawn_store: None,
1500        }
1501    }
1502
1503    /// Add a SQL database store to this context
1504    pub fn with_sqldb_store(mut self, sqldb_store: SessionSqlDbStoreRef) -> Self {
1505        self.sqldb_store = Some(sqldb_store);
1506        self
1507    }
1508
1509    /// Add a message retriever to this context
1510    pub fn with_message_retriever(
1511        mut self,
1512        retriever: Arc<dyn crate::message_retriever::MessageRetriever>,
1513    ) -> Self {
1514        self.message_retriever = Some(retriever);
1515        self
1516    }
1517
1518    /// Add a session store to this context.
1519    pub fn with_session_store(mut self, store: Arc<dyn SessionStore>) -> Self {
1520        self.session_store = Some(store);
1521        self
1522    }
1523
1524    /// Add a session mutator to this context.
1525    pub fn with_session_mutator(mut self, mutator: Arc<dyn SessionMutator>) -> Self {
1526        self.session_mutator = Some(mutator);
1527        self
1528    }
1529
1530    /// Add an agent store to this context.
1531    pub fn with_agent_store(mut self, store: Arc<dyn AgentStore>) -> Self {
1532        self.agent_store = Some(store);
1533        self
1534    }
1535
1536    /// Add a connection resolver to this context
1537    pub fn with_connection_resolver(mut self, resolver: Arc<dyn UserConnectionResolver>) -> Self {
1538        self.connection_resolver = Some(resolver);
1539        self
1540    }
1541
1542    /// Create a context with an image artifact store.
1543    pub fn with_image_store(
1544        session_id: SessionId,
1545        image_store: Arc<dyn ImageArtifactStore>,
1546    ) -> Self {
1547        Self {
1548            session_id,
1549            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
1550            file_store: None,
1551            storage_store: None,
1552            image_store: Some(image_store),
1553            provider_credential_store: None,
1554            utility_llm_service: None,
1555            egress_service: None,
1556            sqldb_store: None,
1557            message_retriever: None,
1558            session_store: None,
1559            session_mutator: None,
1560            agent_store: None,
1561            connection_resolver: None,
1562            schedule_store: None,
1563            platform_store: None,
1564            leased_resource_store: None,
1565            session_resource_registry: None,
1566            session_task_registry: None,
1567            event_emitter: None,
1568            event_context: None,
1569            tool_call_id: None,
1570            capability_registry: None,
1571            tool_registry: None,
1572            visible_tool_names: None,
1573            org_id: None,
1574            network_access: None,
1575            locale: None,
1576            budget_checker: None,
1577            payment_authority: None,
1578            subagent_spawn_store: None,
1579        }
1580    }
1581
1582    /// Set the provider credential store on this context.
1583    pub fn with_provider_credential_store(
1584        mut self,
1585        store: Arc<dyn ProviderCredentialStore>,
1586    ) -> Self {
1587        self.provider_credential_store = Some(store);
1588        self
1589    }
1590
1591    /// Set the utility LLM service on this context.
1592    pub fn with_utility_llm_service(mut self, service: Arc<dyn crate::UtilityLlmService>) -> Self {
1593        self.utility_llm_service = Some(service);
1594        self
1595    }
1596
1597    /// Set the outbound egress service on this context.
1598    pub fn with_egress_service(mut self, service: Arc<dyn crate::EgressService>) -> Self {
1599        self.egress_service = Some(service);
1600        self
1601    }
1602
1603    /// Set the outbound egress service on this context when available.
1604    /// Preserves any already-set service when `service` is `None`.
1605    pub fn with_egress_service_opt(
1606        mut self,
1607        service: Option<Arc<dyn crate::EgressService>>,
1608    ) -> Self {
1609        if let Some(service) = service {
1610            self.egress_service = Some(service);
1611        }
1612        self
1613    }
1614
1615    /// Set the session storage store on this context (builder method).
1616    pub fn with_storage_store_arc(mut self, store: Arc<dyn SessionStorageStore>) -> Self {
1617        self.storage_store = Some(store);
1618        self
1619    }
1620
1621    /// Add a session schedule store to this context.
1622    pub fn with_schedule_store(mut self, store: Arc<dyn SessionScheduleStore>) -> Self {
1623        self.schedule_store = Some(store);
1624        self
1625    }
1626
1627    /// Add a platform store to this context.
1628    pub fn with_platform_store(
1629        mut self,
1630        store: Arc<dyn crate::platform_store::PlatformStore>,
1631    ) -> Self {
1632        self.platform_store = Some(store);
1633        self
1634    }
1635
1636    /// Add a leased resource store to this context.
1637    pub fn with_leased_resource_store(mut self, store: Arc<dyn LeasedResourceStore>) -> Self {
1638        self.leased_resource_store = Some(store);
1639        self
1640    }
1641
1642    /// Add a session resource registry to this context.
1643    pub fn with_session_resource_registry(
1644        mut self,
1645        registry: Arc<dyn SessionResourceRegistry>,
1646    ) -> Self {
1647        self.session_resource_registry = Some(registry);
1648        self
1649    }
1650
1651    /// Add a session task registry to this context.
1652    pub fn with_session_task_registry(
1653        mut self,
1654        registry: Arc<dyn crate::session_task::SessionTaskRegistry>,
1655    ) -> Self {
1656        self.session_task_registry = Some(registry);
1657        self
1658    }
1659
1660    /// Set org ID for org-scoped operations.
1661    pub fn with_org_id(mut self, org_id: crate::typed_id::OrgId) -> Self {
1662        self.org_id = Some(org_id);
1663        self
1664    }
1665
1666    /// Set the active built-in tool registry on this context.
1667    pub fn with_tool_registry(mut self, registry: Arc<crate::tools::ToolRegistry>) -> Self {
1668        self.tool_registry = Some(registry);
1669        self
1670    }
1671
1672    /// Set the tool names visible to the model in this turn.
1673    pub fn with_visible_tool_names(mut self, names: Arc<HashSet<String>>) -> Self {
1674        self.visible_tool_names = Some(names);
1675        self
1676    }
1677
1678    /// Set the merged network access list for URL filtering.
1679    pub fn with_network_access(
1680        mut self,
1681        network_access: Option<crate::network_access::NetworkAccessList>,
1682    ) -> Self {
1683        self.network_access = network_access;
1684        self
1685    }
1686
1687    /// Set the internal payment authority for paid capability operations.
1688    pub fn with_payment_authority(mut self, authority: Arc<dyn PaymentAuthority>) -> Self {
1689        self.payment_authority = Some(authority);
1690        self
1691    }
1692
1693    /// Set the durable subagent spawn handle store (EVE-535).
1694    pub fn with_subagent_spawn_store(mut self, store: Arc<dyn SubagentSpawnStore>) -> Self {
1695        self.subagent_spawn_store = Some(store);
1696        self
1697    }
1698
1699    /// Emit a `tool.progress` event if an event emitter and context are available.
1700    ///
1701    /// This is a best-effort helper: failures are logged but not propagated,
1702    /// so tools never fail just because a progress event couldn't be sent.
1703    pub async fn emit_progress(&self, tool_name: &str, message: &str) {
1704        let (Some(emitter), Some(ctx), Some(call_id)) =
1705            (&self.event_emitter, &self.event_context, &self.tool_call_id)
1706        else {
1707            return;
1708        };
1709        if let Err(e) = emitter
1710            .emit(EventRequest::new(
1711                self.session_id,
1712                ctx.clone(),
1713                crate::events::ToolProgressData {
1714                    tool_call_id: call_id.clone(),
1715                    tool_name: tool_name.to_string(),
1716                    message: message.to_string(),
1717                    display_name: None,
1718                },
1719            ))
1720            .await
1721        {
1722            tracing::debug!(
1723                tool_call_id = call_id,
1724                tool_name,
1725                error = %e,
1726                "Failed to emit tool.progress event"
1727            );
1728        }
1729    }
1730
1731    /// Emit a `tool.output.delta` event if an event emitter and context are available.
1732    ///
1733    /// Streams incremental output chunks (e.g., stdout/stderr lines) for live
1734    /// rendering in UI and CLI. Best-effort: failures are logged, not propagated.
1735    pub async fn emit_tool_output(&self, tool_name: &str, delta: &str, stream: &str) {
1736        let (Some(emitter), Some(ctx), Some(call_id)) =
1737            (&self.event_emitter, &self.event_context, &self.tool_call_id)
1738        else {
1739            return;
1740        };
1741        if let Err(e) = emitter
1742            .emit(EventRequest::new(
1743                self.session_id,
1744                ctx.clone(),
1745                crate::events::ToolOutputDeltaData {
1746                    tool_call_id: call_id.clone(),
1747                    tool_name: tool_name.to_string(),
1748                    delta: delta.to_string(),
1749                    stream: stream.to_string(),
1750                },
1751            ))
1752            .await
1753        {
1754            tracing::debug!(
1755                tool_call_id = call_id,
1756                tool_name,
1757                error = %e,
1758                "Failed to emit tool.output.delta event"
1759            );
1760        }
1761    }
1762}
1763
1764impl std::fmt::Debug for ToolContext {
1765    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1766        f.debug_struct("ToolContext")
1767            .field("session_id", &self.session_id)
1768            .field("file_store", &self.file_store.is_some())
1769            .field("storage_store", &self.storage_store.is_some())
1770            .field("image_store", &self.image_store.is_some())
1771            .field(
1772                "provider_credential_store",
1773                &self.provider_credential_store.is_some(),
1774            )
1775            .field("utility_llm_service", &self.utility_llm_service.is_some())
1776            .field("egress_service", &self.egress_service.is_some())
1777            .field("sqldb_store", &self.sqldb_store.is_some())
1778            .field("message_retriever", &self.message_retriever.is_some())
1779            .field("session_store", &self.session_store.is_some())
1780            .field("session_mutator", &self.session_mutator.is_some())
1781            .field("agent_store", &self.agent_store.is_some())
1782            .field("connection_resolver", &self.connection_resolver.is_some())
1783            .field("schedule_store", &self.schedule_store.is_some())
1784            .field("platform_store", &self.platform_store.is_some())
1785            .field(
1786                "leased_resource_store",
1787                &self.leased_resource_store.is_some(),
1788            )
1789            .field("event_emitter", &self.event_emitter.is_some())
1790            .field("tool_registry", &self.tool_registry.is_some())
1791            .field("payment_authority", &self.payment_authority.is_some())
1792            .field("subagent_spawn_store", &self.subagent_spawn_store.is_some())
1793            .field("org_id", &self.org_id)
1794            .finish()
1795    }
1796}
1797
1798// ============================================================================
1799// EventEmitter - For emitting events
1800// ============================================================================
1801
1802use crate::events::{Event, EventRequest};
1803
1804/// Trait for emitting events following the standard event protocol
1805///
1806/// Implementations can:
1807/// - Store events in a database
1808/// - Keep events in memory for testing
1809/// - Stream events via SSE/WebSocket
1810/// - Log events for debugging
1811///
1812/// Events follow a consistent schema: id, type, ts, context, data.
1813/// See specs/events.md for the full event protocol specification.
1814#[async_trait]
1815pub trait EventEmitter: Send + Sync {
1816    /// Emit an event request
1817    ///
1818    /// Takes an EventRequest (without id/sequence) and returns the stored Event
1819    /// with id and sequence assigned by the storage layer.
1820    async fn emit(&self, request: EventRequest) -> Result<Event>;
1821}
1822
1823/// Blanket impl: `Arc<E>` delegates to the inner emitter.
1824#[async_trait]
1825impl<E: EventEmitter + ?Sized> EventEmitter for Arc<E> {
1826    async fn emit(&self, request: EventRequest) -> Result<Event> {
1827        (**self).emit(request).await
1828    }
1829}
1830
1831/// No-op event emitter for when event emission is not needed
1832///
1833/// This is useful for testing or when event observability is disabled.
1834#[derive(Debug, Clone, Default)]
1835pub struct NoopEventEmitter;
1836
1837#[async_trait]
1838impl EventEmitter for NoopEventEmitter {
1839    async fn emit(&self, request: EventRequest) -> Result<Event> {
1840        // Return a dummy event with sequence 0
1841        Ok(request.into_event(crate::typed_id::EventId::new(), 0))
1842    }
1843}
1844
1845// Note: EventListener trait has been moved to event_listeners.rs module.
1846// Use `everruns_core::EventListener` or `everruns_core::event_listeners::EventListener`.
1847
1848// ============================================================================
1849// ImageResolver - For resolving image_file content to actual image data
1850// ============================================================================
1851
1852/// Resolved image data for LLM consumption
1853///
1854/// This struct contains the actual image data in a format suitable for
1855/// sending to LLM providers. Both OpenAI and Anthropic accept base64-encoded
1856/// images with media type information.
1857#[derive(Debug, Clone)]
1858pub struct ResolvedImage {
1859    /// Base64-encoded image data (without data URL prefix)
1860    pub base64: String,
1861    /// MIME type (e.g., "image/png", "image/jpeg")
1862    pub media_type: String,
1863}
1864
1865impl ResolvedImage {
1866    /// Create a new resolved image
1867    pub fn new(base64: impl Into<String>, media_type: impl Into<String>) -> Self {
1868        Self {
1869            base64: base64.into(),
1870            media_type: media_type.into(),
1871        }
1872    }
1873
1874    /// Convert to a data URL suitable for OpenAI Vision API
1875    ///
1876    /// Format: `data:{media_type};base64,{base64_data}`
1877    pub fn to_data_url(&self) -> String {
1878        format!("data:{};base64,{}", self.media_type, self.base64)
1879    }
1880}
1881
1882/// Trait for resolving image_file content parts to actual image data
1883///
1884/// When building LLM messages, `image_file` content parts contain only
1885/// a reference (UUID) to an uploaded image. This trait allows resolving
1886/// those references to actual image data.
1887///
1888/// # Provider-specific formatting
1889///
1890/// The resolved image data is then converted to provider-specific formats:
1891///
1892/// **OpenAI Vision:**
1893/// ```json
1894/// {
1895///   "type": "image_url",
1896///   "image_url": { "url": "data:image/png;base64,..." }
1897/// }
1898/// ```
1899///
1900/// **Anthropic Vision:**
1901/// ```json
1902/// {
1903///   "type": "image",
1904///   "source": { "type": "base64", "media_type": "image/png", "data": "..." }
1905/// }
1906/// ```
1907///
1908/// # Implementation notes
1909///
1910/// Implementations should:
1911/// - Fetch image data from storage (database, S3, etc.)
1912/// - Return base64-encoded data with media type
1913/// - Handle missing images gracefully (return None)
1914#[async_trait]
1915pub trait ImageResolver: Send + Sync {
1916    /// Resolve an image_file reference to actual image data
1917    ///
1918    /// Returns `None` if the image is not found.
1919    async fn resolve_image(&self, image_id: Uuid) -> Result<Option<ResolvedImage>>;
1920}
1921
1922// ============================================================================
1923// SubagentSpawnStore — durable spawn handles for subagent reattach (EVE-535)
1924// ============================================================================
1925
1926/// Result of attempting to claim a subagent spawn slot.
1927#[derive(Debug)]
1928pub enum SpawnClaimResult {
1929    /// First claim — child session does not yet exist.
1930    /// Proceed to create the child, then call `register_child_session`.
1931    Claimed {
1932        spawn_handle_id: uuid::Uuid,
1933        claim_token: uuid::Uuid,
1934    },
1935    /// Row exists but `child_session_id` was never registered (crash between
1936    /// claim and `register_child_session`). Re-create the child and call
1937    /// `register_child_session` — same flow as `Claimed`.
1938    ClaimedPendingChild {
1939        spawn_handle_id: uuid::Uuid,
1940        claim_token: uuid::Uuid,
1941    },
1942    /// Child session was created and is still running.
1943    /// Reattach: wait for the existing child and settle with the stored claim_token.
1944    AlreadyRunning {
1945        child_session_id: crate::typed_id::SessionId,
1946        /// Stored claim token — must be used for `settle_spawn` on this replay.
1947        claim_token: uuid::Uuid,
1948    },
1949    /// Child already finished on a previous execution.
1950    /// Fast-path: return the stored result immediately without waiting.
1951    AlreadySettled {
1952        child_session_id: crate::typed_id::SessionId,
1953        /// The `wait_for_idle` return value from the original execution.
1954        terminal_status: String,
1955        terminal_result: String,
1956    },
1957}
1958
1959/// Durable spawn handle store for subagent idempotency (EVE-535).
1960///
1961/// Maps `(parent_session_id, tool_call_id) → child_session_id` so that when
1962/// a parent's `act` is reclaimed mid-`wait_for_idle`, the tool can reattach
1963/// to the existing child instead of spawning a duplicate.
1964///
1965/// Lifecycle: claim → register_child_session → settle_spawn.
1966#[async_trait]
1967pub trait SubagentSpawnStore: Send + Sync + 'static {
1968    /// Attempt to claim a spawn slot for `(parent_session_id, tool_call_id)`.
1969    ///
1970    /// Does NOT accept `child_session_id` — the child session does not exist yet.
1971    /// Call `register_child_session` with the actual child ID after creating it.
1972    async fn try_claim_spawn(
1973        &self,
1974        parent_session_id: crate::typed_id::SessionId,
1975        tool_call_id: &str,
1976        claim_token: uuid::Uuid,
1977    ) -> Result<SpawnClaimResult>;
1978
1979    /// Register the actual child session ID after it has been created.
1980    ///
1981    /// Must be called after `try_claim_spawn` returns `Claimed` or
1982    /// `ClaimedPendingChild`, before waiting for the child to complete.
1983    async fn register_child_session(
1984        &self,
1985        spawn_handle_id: uuid::Uuid,
1986        claim_token: uuid::Uuid,
1987        child_session_id: crate::typed_id::SessionId,
1988    ) -> Result<()>;
1989
1990    /// Record the terminal result once the child has completed.
1991    ///
1992    /// `claim_token` must match the stored token. `terminal_status` is the
1993    /// `wait_for_idle` return value ("idle", "error", "timeout", etc.) and
1994    /// `terminal_result` is the last agent message.
1995    async fn settle_spawn(
1996        &self,
1997        parent_session_id: crate::typed_id::SessionId,
1998        tool_call_id: &str,
1999        claim_token: uuid::Uuid,
2000        terminal_status: &str,
2001        terminal_result: &str,
2002    ) -> Result<()>;
2003}
2004
2005/// Blanket impl: `Arc<S>` delegates to the inner store.
2006#[async_trait]
2007impl<S: SubagentSpawnStore + ?Sized> SubagentSpawnStore for Arc<S> {
2008    async fn try_claim_spawn(
2009        &self,
2010        parent_session_id: crate::typed_id::SessionId,
2011        tool_call_id: &str,
2012        claim_token: uuid::Uuid,
2013    ) -> Result<SpawnClaimResult> {
2014        (**self)
2015            .try_claim_spawn(parent_session_id, tool_call_id, claim_token)
2016            .await
2017    }
2018
2019    async fn register_child_session(
2020        &self,
2021        spawn_handle_id: uuid::Uuid,
2022        claim_token: uuid::Uuid,
2023        child_session_id: crate::typed_id::SessionId,
2024    ) -> Result<()> {
2025        (**self)
2026            .register_child_session(spawn_handle_id, claim_token, child_session_id)
2027            .await
2028    }
2029
2030    async fn settle_spawn(
2031        &self,
2032        parent_session_id: crate::typed_id::SessionId,
2033        tool_call_id: &str,
2034        claim_token: uuid::Uuid,
2035        terminal_status: &str,
2036        terminal_result: &str,
2037    ) -> Result<()> {
2038        (**self)
2039            .settle_spawn(
2040                parent_session_id,
2041                tool_call_id,
2042                claim_token,
2043                terminal_status,
2044                terminal_result,
2045            )
2046            .await
2047    }
2048}
2049
2050/// No-op spawn store — used when no durable store is configured (dev/test).
2051///
2052/// Always claims (no dedup); settle and register are no-ops.
2053pub struct NoopSubagentSpawnStore;
2054
2055#[async_trait]
2056impl SubagentSpawnStore for NoopSubagentSpawnStore {
2057    async fn try_claim_spawn(
2058        &self,
2059        _parent_session_id: crate::typed_id::SessionId,
2060        _tool_call_id: &str,
2061        claim_token: uuid::Uuid,
2062    ) -> Result<SpawnClaimResult> {
2063        Ok(SpawnClaimResult::Claimed {
2064            spawn_handle_id: uuid::Uuid::new_v4(),
2065            claim_token,
2066        })
2067    }
2068
2069    async fn register_child_session(
2070        &self,
2071        _spawn_handle_id: uuid::Uuid,
2072        _claim_token: uuid::Uuid,
2073        _child_session_id: crate::typed_id::SessionId,
2074    ) -> Result<()> {
2075        Ok(())
2076    }
2077
2078    async fn settle_spawn(
2079        &self,
2080        _parent_session_id: crate::typed_id::SessionId,
2081        _tool_call_id: &str,
2082        _claim_token: uuid::Uuid,
2083        _terminal_status: &str,
2084        _terminal_result: &str,
2085    ) -> Result<()> {
2086        Ok(())
2087    }
2088}
2089
2090// ============================================================================
2091// Tests
2092// ============================================================================
2093
2094#[cfg(test)]
2095mod tests {
2096    use super::*;
2097
2098    #[test]
2099    fn test_resolved_image_new() {
2100        let image = ResolvedImage::new("SGVsbG8=", "image/png");
2101        assert_eq!(image.base64, "SGVsbG8=");
2102        assert_eq!(image.media_type, "image/png");
2103    }
2104
2105    #[test]
2106    fn test_resolved_image_to_data_url() {
2107        let image = ResolvedImage::new("SGVsbG8=", "image/png");
2108        let data_url = image.to_data_url();
2109        assert_eq!(data_url, "data:image/png;base64,SGVsbG8=");
2110    }
2111
2112    #[test]
2113    fn test_resolved_image_jpeg() {
2114        let image = ResolvedImage::new("base64data", "image/jpeg");
2115        let data_url = image.to_data_url();
2116        assert!(data_url.starts_with("data:image/jpeg;base64,"));
2117    }
2118}