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::llm_models::LlmProviderType;
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};
14use async_trait::async_trait;
15use chrono::{DateTime, Utc};
16use std::any::{Any, TypeId};
17use std::collections::HashMap;
18use std::sync::Arc;
19use uuid::Uuid;
20
21/// Build a map of tool names to definitions for efficient lookup
22fn build_tool_map(tool_defs: &[ToolDefinition]) -> HashMap<&str, &ToolDefinition> {
23    tool_defs.iter().map(|def| (def.name(), def)).collect()
24}
25
26use crate::error::Result;
27
28// ============================================================================
29// AgentStore - For retrieving agent configurations
30// ============================================================================
31
32/// Trait for retrieving agent configurations
33///
34/// Implementations can:
35/// - Load agents from a database
36/// - Keep agents in memory for testing
37/// - Load agents from a configuration file
38#[async_trait]
39pub trait AgentStore: Send + Sync {
40    /// Get an agent by ID
41    async fn get_agent(&self, agent_id: AgentId) -> Result<Option<Agent>>;
42}
43
44#[async_trait]
45impl<T: AgentStore + ?Sized> AgentStore for std::sync::Arc<T> {
46    async fn get_agent(&self, agent_id: AgentId) -> Result<Option<Agent>> {
47        (**self).get_agent(agent_id).await
48    }
49}
50
51// ============================================================================
52// HarnessStore - For retrieving harness configurations
53// ============================================================================
54
55/// Trait for retrieving harness configurations
56///
57/// Implementations can:
58/// - Load harnesses from a database
59/// - Keep harnesses in memory for testing
60///
61/// Returns the harness inheritance chain (root-to-leaf) so the caller
62/// can fold each harness as an `AgentConfigOverlay`. DB-backed stores
63/// return the raw chain; gRPC-backed stores may return a single
64/// pre-merged harness (functionally equivalent when folded).
65#[async_trait]
66pub trait HarnessStore: Send + Sync {
67    /// Get the harness inheritance chain, root-to-leaf.
68    ///
69    /// Returns `Ok(vec![])` if the harness does not exist.
70    /// A harness with no parent returns a single-element vec.
71    async fn get_harness_chain(&self, harness_id: HarnessId) -> Result<Vec<Harness>>;
72}
73
74#[async_trait]
75impl<T: HarnessStore + ?Sized> HarnessStore for std::sync::Arc<T> {
76    async fn get_harness_chain(&self, harness_id: HarnessId) -> Result<Vec<Harness>> {
77        (**self).get_harness_chain(harness_id).await
78    }
79}
80
81// ============================================================================
82// SessionStore - For retrieving session information
83// ============================================================================
84
85use crate::leased_resource::{LeasedResource, UpsertLeasedResource};
86use crate::session::Session;
87
88/// Trait for retrieving session configurations
89///
90/// Implementations can:
91/// - Load sessions from a database
92/// - Keep sessions in memory for testing
93#[async_trait]
94pub trait SessionStore: Send + Sync {
95    /// Get a session by ID
96    async fn get_session(&self, session_id: SessionId) -> Result<Option<Session>>;
97}
98
99#[async_trait]
100impl<T: SessionStore + ?Sized> SessionStore for std::sync::Arc<T> {
101    async fn get_session(&self, session_id: SessionId) -> Result<Option<Session>> {
102        (**self).get_session(session_id).await
103    }
104}
105
106/// Trait for updating mutable session metadata.
107#[async_trait]
108pub trait SessionMutator: Send + Sync {
109    /// Update a session's human-readable title.
110    async fn update_session_title(&self, session_id: SessionId, title: String) -> Result<Session>;
111}
112
113#[async_trait]
114impl<T: SessionMutator + ?Sized> SessionMutator for std::sync::Arc<T> {
115    async fn update_session_title(&self, session_id: SessionId, title: String) -> Result<Session> {
116        (**self).update_session_title(session_id, title).await
117    }
118}
119
120// ============================================================================
121// LlmProviderStore - For retrieving LLM provider configurations
122// ============================================================================
123
124/// Model information with provider details needed for LLM calls
125#[derive(Debug, Clone)]
126pub struct ModelWithProvider {
127    /// The model ID string to pass to the LLM API (e.g., "gpt-4o", "claude-3-opus")
128    pub model: String,
129    /// Provider type for factory selection
130    pub provider_type: LlmProviderType,
131    /// Decrypted API key (if configured)
132    pub api_key: Option<String>,
133    /// Optional base URL override
134    pub base_url: Option<String>,
135}
136
137/// Trait for retrieving LLM provider and model configurations
138///
139/// This trait abstracts the database lookup and API key decryption needed
140/// to create LLM providers at runtime.
141///
142/// Implementations can:
143/// - Load from a database with encrypted API keys
144/// - Use in-memory configurations for testing
145/// - Load from environment variables for development
146#[async_trait]
147pub trait LlmProviderStore: Send + Sync {
148    /// Get model with provider info by model ID
149    ///
150    /// Returns the model string ID, provider type, decrypted API key, and base URL
151    /// needed to create an LLM provider via the factory.
152    async fn get_model_with_provider(&self, model_id: ModelId)
153    -> Result<Option<ModelWithProvider>>;
154
155    /// Get the default model with provider info
156    ///
157    /// Returns the system default model when an agent has no default_model_id set.
158    async fn get_default_model(&self) -> Result<Option<ModelWithProvider>>;
159}
160
161#[async_trait]
162impl<T: LlmProviderStore + ?Sized> LlmProviderStore for std::sync::Arc<T> {
163    async fn get_model_with_provider(
164        &self,
165        model_id: ModelId,
166    ) -> Result<Option<ModelWithProvider>> {
167        (**self).get_model_with_provider(model_id).await
168    }
169
170    async fn get_default_model(&self) -> Result<Option<ModelWithProvider>> {
171        (**self).get_default_model().await
172    }
173}
174
175// ============================================================================
176// ImageArtifactStore - For durable image persistence from tools
177// ============================================================================
178
179/// Metadata for a stored image artifact.
180#[derive(Debug, Clone)]
181pub struct StoredImageInfo {
182    pub id: ImageId,
183    pub filename: String,
184    pub content_type: String,
185    pub size_bytes: i64,
186    pub metadata: serde_json::Value,
187    pub created_at: DateTime<Utc>,
188}
189
190/// Stored image artifact with binary data.
191#[derive(Debug, Clone)]
192pub struct StoredImage {
193    pub info: StoredImageInfo,
194    pub data: Vec<u8>,
195}
196
197/// Input for creating a stored image artifact.
198#[derive(Debug, Clone)]
199pub struct CreateStoredImage {
200    pub filename: String,
201    pub content_type: String,
202    pub data: Vec<u8>,
203    pub metadata: serde_json::Value,
204}
205
206#[async_trait]
207pub trait ImageArtifactStore: Send + Sync {
208    /// Persist an image artifact and return its durable metadata.
209    async fn create_image(&self, input: CreateStoredImage) -> Result<StoredImageInfo>;
210
211    /// Load a stored image artifact including bytes.
212    async fn get_image(&self, image_id: ImageId) -> Result<Option<StoredImage>>;
213
214    /// Load stored image metadata without binary data.
215    async fn get_image_info(&self, image_id: ImageId) -> Result<Option<StoredImageInfo>>;
216}
217
218// ============================================================================
219// ProviderCredentialStore - For tool-side provider credential resolution
220// ============================================================================
221
222/// Provider credentials resolved for tool-side API clients.
223#[derive(Debug, Clone)]
224pub struct ProviderCredentials {
225    pub api_key: String,
226    pub base_url: Option<String>,
227}
228
229#[async_trait]
230pub trait ProviderCredentialStore: Send + Sync {
231    /// Resolve default credentials for a provider type (for example `openai`).
232    ///
233    /// Implementations may apply environment fallbacks internally, but tools
234    /// should never read provider env vars directly.
235    async fn get_default_provider_credentials(
236        &self,
237        provider_type: &str,
238    ) -> Result<Option<ProviderCredentials>>;
239}
240
241// ============================================================================
242// ToolExecutor - For executing tool calls
243// ============================================================================
244
245/// Trait for executing tool calls
246///
247/// Implementations handle the actual tool execution:
248/// - Webhook calls
249/// - Built-in function execution
250/// - Mock execution for testing
251#[async_trait]
252pub trait ToolExecutor: Send + Sync {
253    /// Execute a single tool call (without context)
254    ///
255    /// This is the legacy method that doesn't provide context to tools.
256    /// Use `execute_with_context` when context is available.
257    async fn execute(&self, tool_call: &ToolCall, tool_def: &ToolDefinition) -> Result<ToolResult>;
258
259    /// Execute a single tool call with context
260    ///
261    /// This method provides runtime context to tools that need it (like filesystem tools).
262    /// The default implementation delegates to `execute()`.
263    async fn execute_with_context(
264        &self,
265        tool_call: &ToolCall,
266        tool_def: &ToolDefinition,
267        _context: &ToolContext,
268    ) -> Result<ToolResult> {
269        // Default: delegate to execute(), ignoring context
270        self.execute(tool_call, tool_def).await
271    }
272
273    /// Execute multiple tool calls (default: sequential)
274    async fn execute_batch(
275        &self,
276        tool_calls: &[ToolCall],
277        tool_defs: &[ToolDefinition],
278    ) -> Result<Vec<ToolResult>> {
279        let mut results = Vec::with_capacity(tool_calls.len());
280
281        let tool_map = build_tool_map(tool_defs);
282
283        for tool_call in tool_calls {
284            let tool_def = tool_map.get(tool_call.name.as_str()).ok_or_else(|| {
285                crate::error::AgentLoopError::tool(format!(
286                    "Tool definition not found: {}",
287                    tool_call.name
288                ))
289            })?;
290
291            results.push(self.execute(tool_call, tool_def).await?);
292        }
293
294        Ok(results)
295    }
296
297    /// Execute multiple tool calls in parallel
298    async fn execute_parallel(
299        &self,
300        tool_calls: &[ToolCall],
301        tool_defs: &[ToolDefinition],
302    ) -> Result<Vec<ToolResult>>
303    where
304        Self: Sized,
305    {
306        use futures::future::join_all;
307
308        let tool_map = build_tool_map(tool_defs);
309
310        let futures: Vec<_> = tool_calls
311            .iter()
312            .map(|tool_call| async {
313                let tool_def = tool_map.get(tool_call.name.as_str()).ok_or_else(|| {
314                    crate::error::AgentLoopError::tool(format!(
315                        "Tool definition not found: {}",
316                        tool_call.name
317                    ))
318                })?;
319                self.execute(tool_call, tool_def).await
320            })
321            .collect();
322
323        let results = join_all(futures).await;
324        results.into_iter().collect()
325    }
326}
327
328// ============================================================================
329// SessionFileSystem - For session filesystem operations
330// ============================================================================
331
332/// Trait for session filesystem operations
333///
334/// This trait abstracts the session filesystem contract for tools and hosts.
335/// Implementations can:
336/// - Store files in a database (production)
337/// - Use an in-memory filesystem for testing
338/// - Project files onto real disk or object storage
339#[async_trait]
340pub trait SessionFileSystem: Send + Sync {
341    /// Read a file by path
342    async fn read_file(&self, session_id: SessionId, path: &str) -> Result<Option<SessionFile>>;
343
344    /// Write/create a file
345    async fn write_file(
346        &self,
347        session_id: SessionId,
348        path: &str,
349        content: &str,
350        encoding: &str,
351    ) -> Result<SessionFile>;
352
353    /// Write a file only if its current content snapshot still matches.
354    ///
355    /// Implementations backed by transactional storage should override this
356    /// with an atomic compare-and-set update.
357    async fn write_file_if_content_matches(
358        &self,
359        session_id: SessionId,
360        path: &str,
361        expected_content: &str,
362        expected_encoding: &str,
363        content: &str,
364        encoding: &str,
365    ) -> Result<Option<SessionFile>> {
366        let Some(existing) = self.read_file(session_id, path).await? else {
367            return Ok(None);
368        };
369
370        if existing.is_directory {
371            return Ok(None);
372        }
373
374        let current_content = existing.content.unwrap_or_default();
375        if current_content != expected_content || existing.encoding != expected_encoding {
376            return Ok(None);
377        }
378
379        self.write_file(session_id, path, content, encoding)
380            .await
381            .map(Some)
382    }
383
384    /// Delete a file or directory
385    async fn delete_file(&self, session_id: SessionId, path: &str, recursive: bool)
386    -> Result<bool>;
387
388    /// List files in a directory
389    async fn list_directory(&self, session_id: SessionId, path: &str) -> Result<Vec<FileInfo>>;
390
391    /// Get file metadata
392    async fn stat_file(&self, session_id: SessionId, path: &str) -> Result<Option<FileStat>>;
393
394    /// Search files by pattern (grep)
395    async fn grep_files(
396        &self,
397        session_id: SessionId,
398        pattern: &str,
399        path_pattern: Option<&str>,
400    ) -> Result<Vec<GrepMatch>>;
401
402    /// Create a directory
403    async fn create_directory(&self, session_id: SessionId, path: &str) -> Result<FileInfo>;
404
405    /// Seed a starter file into a session workspace.
406    async fn seed_initial_file(&self, session_id: SessionId, file: &InitialFile) -> Result<()> {
407        if file.is_readonly {
408            return Err(crate::error::AgentLoopError::store(
409                "read-only initial files require a SessionFileSystem-specific seed implementation",
410            ));
411        }
412        self.write_file(session_id, &file.path, &file.content, &file.encoding)
413            .await?;
414        Ok(())
415    }
416}
417
418#[async_trait]
419impl<T: SessionFileSystem + ?Sized> SessionFileSystem for std::sync::Arc<T> {
420    async fn read_file(&self, session_id: SessionId, path: &str) -> Result<Option<SessionFile>> {
421        (**self).read_file(session_id, path).await
422    }
423
424    async fn write_file(
425        &self,
426        session_id: SessionId,
427        path: &str,
428        content: &str,
429        encoding: &str,
430    ) -> Result<SessionFile> {
431        (**self)
432            .write_file(session_id, path, content, encoding)
433            .await
434    }
435
436    async fn write_file_if_content_matches(
437        &self,
438        session_id: SessionId,
439        path: &str,
440        expected_content: &str,
441        expected_encoding: &str,
442        content: &str,
443        encoding: &str,
444    ) -> Result<Option<SessionFile>> {
445        (**self)
446            .write_file_if_content_matches(
447                session_id,
448                path,
449                expected_content,
450                expected_encoding,
451                content,
452                encoding,
453            )
454            .await
455    }
456
457    async fn delete_file(
458        &self,
459        session_id: SessionId,
460        path: &str,
461        recursive: bool,
462    ) -> Result<bool> {
463        (**self).delete_file(session_id, path, recursive).await
464    }
465
466    async fn list_directory(&self, session_id: SessionId, path: &str) -> Result<Vec<FileInfo>> {
467        (**self).list_directory(session_id, path).await
468    }
469
470    async fn stat_file(&self, session_id: SessionId, path: &str) -> Result<Option<FileStat>> {
471        (**self).stat_file(session_id, path).await
472    }
473
474    async fn grep_files(
475        &self,
476        session_id: SessionId,
477        pattern: &str,
478        path_pattern: Option<&str>,
479    ) -> Result<Vec<GrepMatch>> {
480        (**self).grep_files(session_id, pattern, path_pattern).await
481    }
482
483    async fn create_directory(&self, session_id: SessionId, path: &str) -> Result<FileInfo> {
484        (**self).create_directory(session_id, path).await
485    }
486
487    async fn seed_initial_file(&self, session_id: SessionId, file: &InitialFile) -> Result<()> {
488        (**self).seed_initial_file(session_id, file).await
489    }
490}
491
492/// Backward-compatible alias for the old session filesystem trait name.
493pub use SessionFileSystem as SessionFileStore;
494
495/// Host-supplied values used by platform file-system factories.
496///
497/// The context is intentionally type-erased so `everruns-core` can own the
498/// platform contract without depending on server-only types such as
499/// `StorageBackend` or future object-storage clients.
500#[derive(Clone, Default)]
501pub struct SessionFileSystemFactoryContext {
502    values: Arc<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>,
503}
504
505impl SessionFileSystemFactoryContext {
506    pub fn new() -> Self {
507        Self::default()
508    }
509
510    pub fn with<T: Any + Send + Sync>(mut self, value: Arc<T>) -> Self {
511        let values = Arc::make_mut(&mut self.values);
512        values.insert(TypeId::of::<T>(), value);
513        self
514    }
515
516    pub fn get<T: Any + Send + Sync>(&self) -> Option<Arc<T>> {
517        self.values
518            .get(&TypeId::of::<T>())
519            .and_then(|value| value.clone().downcast::<T>().ok())
520    }
521}
522
523/// Factory for deployment-selected session filesystem implementations.
524#[async_trait]
525pub trait SessionFileSystemFactory: Send + Sync {
526    /// Human-readable factory name for diagnostics.
527    fn name(&self) -> &'static str {
528        "SessionFileSystemFactory"
529    }
530
531    /// Whether this factory intentionally leaves filesystem selection to the
532    /// runtime default.
533    fn is_disabled(&self) -> bool {
534        false
535    }
536
537    /// Resolve a live filesystem from host-provided dependencies.
538    async fn create_session_file_system(
539        &self,
540        context: SessionFileSystemFactoryContext,
541    ) -> Result<Arc<dyn SessionFileSystem>>;
542}
543
544/// Default factory used when a platform does not configure session files.
545#[derive(Debug, Clone, Default)]
546pub struct DisabledSessionFileSystemFactory;
547
548#[async_trait]
549impl SessionFileSystemFactory for DisabledSessionFileSystemFactory {
550    fn name(&self) -> &'static str {
551        "DisabledSessionFileSystemFactory"
552    }
553
554    fn is_disabled(&self) -> bool {
555        true
556    }
557
558    async fn create_session_file_system(
559        &self,
560        _context: SessionFileSystemFactoryContext,
561    ) -> Result<Arc<dyn SessionFileSystem>> {
562        Err(crate::error::AgentLoopError::config(
563            "session filesystem is disabled",
564        ))
565    }
566}
567
568// ============================================================================
569// SessionStorageStore - For session key/value and secret storage
570// ============================================================================
571
572/// Info about a stored key (without its value)
573#[derive(Debug, Clone)]
574pub struct KeyInfo {
575    pub key: String,
576    pub created_at: chrono::DateTime<chrono::Utc>,
577    pub updated_at: chrono::DateTime<chrono::Utc>,
578}
579
580/// Info about a stored secret (without its value)
581#[derive(Debug, Clone)]
582pub struct SecretInfo {
583    pub name: String,
584    pub created_at: chrono::DateTime<chrono::Utc>,
585    pub updated_at: chrono::DateTime<chrono::Utc>,
586}
587
588/// Trait for session key/value and secret storage operations
589///
590/// This trait abstracts storage operations for tools that need to persist
591/// data within a session. Implementations can:
592/// - Store data in a database (production)
593/// - Use in-memory storage for testing
594///
595/// Key/value storage is for general data that doesn't need encryption.
596/// Secret storage is for sensitive data that is encrypted at rest.
597#[async_trait]
598pub trait SessionStorageStore: Send + Sync {
599    // Key/Value operations (plain text)
600
601    /// Set a key/value pair (creates or updates)
602    async fn set_value(&self, session_id: SessionId, key: &str, value: &str) -> Result<()>;
603
604    /// Get a value by key
605    async fn get_value(&self, session_id: SessionId, key: &str) -> Result<Option<String>>;
606
607    /// Delete a key/value pair
608    async fn delete_value(&self, session_id: SessionId, key: &str) -> Result<bool>;
609
610    /// List all keys in a session
611    async fn list_keys(&self, session_id: SessionId) -> Result<Vec<KeyInfo>>;
612
613    // Secret operations (encrypted)
614
615    /// Set a secret (creates or updates, value is encrypted before storage)
616    async fn set_secret(&self, session_id: SessionId, name: &str, value: &str) -> Result<()>;
617
618    /// Get a secret by name (value is decrypted before returning)
619    async fn get_secret(&self, session_id: SessionId, name: &str) -> Result<Option<String>>;
620
621    /// Delete a secret
622    async fn delete_secret(&self, session_id: SessionId, name: &str) -> Result<bool>;
623
624    /// List all secret names in a session (without values)
625    async fn list_secrets(&self, session_id: SessionId) -> Result<Vec<SecretInfo>>;
626}
627
628// ============================================================================
629// SessionScheduleStore - For session-scoped schedule operations
630// ============================================================================
631
632use crate::session_schedule::SessionSchedule;
633use crate::typed_id::ScheduleId;
634
635/// Trait for session schedule CRUD operations.
636///
637/// Used by scheduling tools to create, cancel, and list schedules.
638#[async_trait]
639pub trait SessionScheduleStore: Send + Sync {
640    /// Create a new schedule for a session.
641    async fn create_schedule(
642        &self,
643        session_id: SessionId,
644        description: String,
645        cron_expression: Option<String>,
646        scheduled_at: Option<chrono::DateTime<chrono::Utc>>,
647        timezone: String,
648    ) -> Result<SessionSchedule>;
649
650    /// Cancel (disable) a schedule.
651    async fn cancel_schedule(
652        &self,
653        session_id: SessionId,
654        schedule_id: ScheduleId,
655    ) -> Result<SessionSchedule>;
656
657    /// List schedules for a session.
658    async fn list_schedules(&self, session_id: SessionId) -> Result<Vec<SessionSchedule>>;
659
660    /// Count active (enabled) schedules for a session.
661    async fn count_active_schedules(&self, session_id: SessionId) -> Result<u32>;
662}
663
664// ============================================================================
665// SessionResourceRegistry - Generic session-scoped resource registry
666// ============================================================================
667
668/// Generic registry of resources active alongside a session.
669///
670/// Capabilities register resources here (sandboxes, subagents, browser sessions).
671/// Agents query it ("what's running?"), infrastructure scans it for cleanup.
672/// See `specs/session-resources.md`.
673#[async_trait]
674pub trait SessionResourceRegistry: Send + Sync {
675    /// Register a resource (or update if resource_id already exists for this session).
676    async fn register(
677        &self,
678        entry: crate::session_resource::RegisterSessionResource,
679    ) -> Result<crate::session_resource::SessionResourceEntry>;
680
681    /// Update the status of a registered resource.
682    async fn update_status(
683        &self,
684        session_id: SessionId,
685        resource_id: &str,
686        status: crate::session_resource::SessionResourceStatus,
687    ) -> Result<Option<crate::session_resource::SessionResourceEntry>>;
688
689    /// Get a specific resource by ID.
690    async fn get(
691        &self,
692        session_id: SessionId,
693        resource_id: &str,
694    ) -> Result<Option<crate::session_resource::SessionResourceEntry>>;
695
696    /// List resources for a session, optionally filtered.
697    async fn list(
698        &self,
699        session_id: SessionId,
700        filter: Option<&crate::session_resource::SessionResourceFilter>,
701    ) -> Result<Vec<crate::session_resource::SessionResourceEntry>>;
702
703    /// Remove a resource from the registry.
704    async fn deregister(&self, session_id: SessionId, resource_id: &str) -> Result<bool>;
705}
706
707// ============================================================================
708// LeasedResourceStore - For lifecycle-managed external resources
709// ============================================================================
710
711/// Trait for session-scoped leased resource operations.
712///
713/// Tools use this store to register or refresh leases when they create or use
714/// external provider resources. Cleanup workers operate through control-plane
715/// storage APIs directly so they can claim work across organizations.
716#[async_trait]
717pub trait LeasedResourceStore: Send + Sync {
718    /// Create or refresh a leased resource for a session.
719    ///
720    /// Implementations must treat this as an idempotent upsert keyed by the
721    /// provider-specific resource identity so repeated tool usage extends the
722    /// same lease instead of creating duplicate rows.
723    async fn upsert_resource(&self, input: UpsertLeasedResource) -> Result<LeasedResource>;
724
725    /// Mark a leased resource as explicitly released.
726    ///
727    /// This is the fast path for explicit user intent such as "close browser"
728    /// or "delete sandbox". It should transition the resource to `released`
729    /// without waiting for the durable cleanup worker to observe lease expiry.
730    async fn release_resource(
731        &self,
732        session_id: SessionId,
733        provider: &str,
734        resource_type: &str,
735        external_id: &str,
736    ) -> Result<Option<LeasedResource>>;
737
738    /// List leased resources currently associated with a session.
739    ///
740    /// Session surfaces use this for visibility. Released resources remain
741    /// visible so operators can inspect cleanup outcomes and failure history.
742    async fn list_resources(&self, session_id: SessionId) -> Result<Vec<LeasedResource>>;
743}
744
745// ============================================================================
746// ToolContext - Runtime context for tool execution
747// ============================================================================
748
749/// Type alias for the session SQL DB store trait object.
750pub type SessionSqlDbStoreRef = Arc<dyn crate::session_sqldb::SessionSqlDbStore>;
751
752/// Resolves user connection tokens (e.g. GitHub) lazily at tool execution time.
753///
754/// Instead of eagerly injecting tokens at session creation, tools call this
755/// resolver when they need a token. If the user hasn't connected, returns None.
756#[async_trait]
757pub trait UserConnectionResolver: Send + Sync {
758    /// Get a decrypted connection token for the given provider.
759    /// Returns None if the user has no connection for this provider.
760    async fn get_connection_token(
761        &self,
762        session_id: SessionId,
763        provider: &str,
764    ) -> Result<Option<String>>;
765
766    /// Resolve the user ID of the connection used for a session/provider pair.
767    ///
768    /// This is used by leased resources to bind cleanup to the same provider
769    /// identity that created the remote resource.
770    async fn get_connection_user(
771        &self,
772        _session_id: SessionId,
773        _provider: &str,
774    ) -> Result<Option<Uuid>> {
775        Ok(None)
776    }
777
778    /// Resolve a provider token for a specific user.
779    ///
780    /// Cleanup workers use this to avoid "first org member wins" behavior when
781    /// cleaning resources created by a specific provider connection owner.
782    async fn get_connection_token_for_user(
783        &self,
784        _user_id: Uuid,
785        _provider: &str,
786    ) -> Result<Option<String>> {
787        Ok(None)
788    }
789
790    /// Get provider-specific metadata stored alongside the connection.
791    /// Returns None if no metadata is stored or no connection exists.
792    async fn get_connection_metadata(
793        &self,
794        _session_id: SessionId,
795        _provider: &str,
796    ) -> Result<Option<serde_json::Value>> {
797        Ok(None)
798    }
799}
800
801// ============================================================================
802// BudgetChecker - For querying budget status from tools
803// ============================================================================
804
805/// Trait for checking budget status from within tool execution.
806///
807/// Implemented by gRPC adapters (worker → server) and direct adapters (in-process).
808/// Used by the `check_budget` tool to return real budget data to agents.
809/// The org_id is captured at construction time by the implementing adapter.
810#[async_trait]
811pub trait BudgetChecker: Send + Sync {
812    /// Check all budgets for a session and return a tool-friendly response.
813    async fn check_budgets(&self, session_id: &str) -> Result<crate::budget::BudgetToolResponse>;
814}
815
816// ============================================================================
817// PaymentAuthority - For capability-internal machine payments
818// ============================================================================
819
820/// Internal authority for paid capability operations.
821///
822/// Capabilities call this with fixed, typed requests. The model never receives a
823/// generic paid HTTP tool, wallet credentials, or payment payloads.
824#[async_trait]
825pub trait PaymentAuthority: Send + Sync {
826    async fn execute_machine_payment(
827        &self,
828        session_id: SessionId,
829        request: crate::payment::MachinePaymentRequest,
830    ) -> Result<crate::payment::MachinePaymentResponse>;
831}
832
833/// Runtime context provided to tools during execution.
834///
835/// This context contains:
836/// - Session ID for scoping operations
837/// - Optional stores for tools that need external access
838///
839/// Tools that need context-aware execution (like filesystem tools) can use
840/// the `execute_with_context` method on the Tool trait.
841#[derive(Clone)]
842pub struct ToolContext {
843    /// The session ID for the current execution
844    pub session_id: SessionId,
845
846    /// Optional file store for filesystem operations
847    pub file_store: Option<Arc<dyn SessionFileSystem>>,
848
849    /// Optional storage store for key/value and secret storage
850    pub storage_store: Option<Arc<dyn SessionStorageStore>>,
851
852    /// Optional durable image artifact store for tool-side media persistence.
853    pub image_store: Option<Arc<dyn ImageArtifactStore>>,
854
855    /// Optional provider credential store for tool-side API clients.
856    pub provider_credential_store: Option<Arc<dyn ProviderCredentialStore>>,
857
858    /// Optional system utility LLM service for capability internals.
859    pub utility_llm_service: Option<Arc<dyn crate::UtilityLlmService>>,
860
861    /// Optional session SQL database store
862    pub sqldb_store: Option<SessionSqlDbStoreRef>,
863
864    /// Optional message retriever for tools that need conversation history access
865    pub message_retriever: Option<Arc<dyn crate::message_retriever::MessageRetriever>>,
866
867    /// Optional session store for tools that need session metadata access.
868    pub session_store: Option<Arc<dyn SessionStore>>,
869
870    /// Optional session mutator for tools that need to update session metadata.
871    pub session_mutator: Option<Arc<dyn SessionMutator>>,
872
873    /// Optional agent store for tools that need agent metadata access.
874    pub agent_store: Option<Arc<dyn AgentStore>>,
875
876    /// Optional resolver for user connection tokens (lazy GitHub token lookup, etc.)
877    pub connection_resolver: Option<Arc<dyn UserConnectionResolver>>,
878
879    /// Optional session schedule store for scheduling tools.
880    pub schedule_store: Option<Arc<dyn SessionScheduleStore>>,
881
882    /// Optional platform store for org-level management tools.
883    pub platform_store: Option<Arc<dyn crate::platform_store::PlatformStore>>,
884    /// Optional leased resource store for lifecycle-managed provider resources.
885    pub leased_resource_store: Option<Arc<dyn LeasedResourceStore>>,
886
887    /// Optional session resource registry — generic registry of active resources.
888    pub session_resource_registry: Option<Arc<dyn SessionResourceRegistry>>,
889
890    /// Optional event emitter for tools that need to stream progress updates.
891    /// When set, tools can emit `tool.progress` events during execution.
892    pub event_emitter: Option<Arc<dyn EventEmitter>>,
893
894    /// Event context for correlating progress events with the current tool call.
895    /// Set by ActAtom when constructing the ToolContext.
896    pub event_context: Option<crate::events::EventContext>,
897
898    /// The tool call ID for the current execution (set by ActAtom).
899    /// Used by tools to emit correlated progress events.
900    pub tool_call_id: Option<String>,
901    /// Optional capability registry for blueprint lookups.
902    pub capability_registry: Option<crate::capabilities::CapabilityRegistry>,
903
904    /// Optional registry of active built-in tools for meta-tools such as
905    /// `spawn_background` that need to inspect or delegate to sibling tools.
906    pub tool_registry: Option<Arc<crate::tools::ToolRegistry>>,
907
908    /// Optional memory store backend for persistent cross-session memory.
909    pub memory_store: Option<Arc<dyn crate::memory_store::MemoryStoreBackend>>,
910
911    /// Optional org ID for org-scoped operations (memory stores, etc.).
912    pub org_id: Option<crate::typed_id::OrgId>,
913
914    /// Merged network access list (harness ∩ agent ∩ session).
915    /// When set, tools that make HTTP requests must check URLs against this list.
916    pub network_access: Option<crate::network_access::NetworkAccessList>,
917
918    /// Resolved locale for localized tool behavior (BCP 47, e.g. `uk-UA`).
919    /// When set, tools that support localization use this to produce
920    /// locale-appropriate descriptions, error messages, and prompts.
921    pub locale: Option<String>,
922
923    /// Optional budget checker for the check_budget tool.
924    pub budget_checker: Option<Arc<dyn BudgetChecker>>,
925
926    /// Optional internal payment authority for paid capability tools.
927    pub payment_authority: Option<Arc<dyn PaymentAuthority>>,
928}
929
930impl ToolContext {
931    /// Create a new tool context with just a session ID
932    pub fn new(session_id: SessionId) -> Self {
933        Self {
934            session_id,
935            file_store: None,
936            storage_store: None,
937            image_store: None,
938            provider_credential_store: None,
939            utility_llm_service: None,
940            sqldb_store: None,
941            message_retriever: None,
942            session_store: None,
943            session_mutator: None,
944            agent_store: None,
945            connection_resolver: None,
946            schedule_store: None,
947            platform_store: None,
948            leased_resource_store: None,
949            session_resource_registry: None,
950            event_emitter: None,
951            event_context: None,
952            tool_call_id: None,
953            capability_registry: None,
954            tool_registry: None,
955            memory_store: None,
956            org_id: None,
957            network_access: None,
958            locale: None,
959            budget_checker: None,
960            payment_authority: None,
961        }
962    }
963
964    /// Create a context with a file store
965    pub fn with_file_store(session_id: SessionId, file_store: Arc<dyn SessionFileSystem>) -> Self {
966        Self {
967            session_id,
968            file_store: Some(file_store),
969            storage_store: None,
970            image_store: None,
971            provider_credential_store: None,
972            utility_llm_service: None,
973            sqldb_store: None,
974            message_retriever: None,
975            session_store: None,
976            session_mutator: None,
977            agent_store: None,
978            connection_resolver: None,
979            schedule_store: None,
980            platform_store: None,
981            leased_resource_store: None,
982            session_resource_registry: None,
983            event_emitter: None,
984            event_context: None,
985            tool_call_id: None,
986            capability_registry: None,
987            tool_registry: None,
988            memory_store: None,
989            org_id: None,
990            network_access: None,
991            locale: None,
992            budget_checker: None,
993            payment_authority: None,
994        }
995    }
996
997    /// Create a context with a storage store
998    pub fn with_storage_store(
999        session_id: SessionId,
1000        storage_store: Arc<dyn SessionStorageStore>,
1001    ) -> Self {
1002        Self {
1003            session_id,
1004            file_store: None,
1005            storage_store: Some(storage_store),
1006            image_store: None,
1007            provider_credential_store: None,
1008            utility_llm_service: None,
1009            sqldb_store: None,
1010            message_retriever: None,
1011            session_store: None,
1012            session_mutator: None,
1013            agent_store: None,
1014            connection_resolver: None,
1015            schedule_store: None,
1016            platform_store: None,
1017            leased_resource_store: None,
1018            session_resource_registry: None,
1019            event_emitter: None,
1020            event_context: None,
1021            tool_call_id: None,
1022            capability_registry: None,
1023            tool_registry: None,
1024            memory_store: None,
1025            org_id: None,
1026            network_access: None,
1027            locale: None,
1028            budget_checker: None,
1029            payment_authority: None,
1030        }
1031    }
1032
1033    /// Create a context with both file store and storage store
1034    pub fn with_stores(
1035        session_id: SessionId,
1036        file_store: Arc<dyn SessionFileSystem>,
1037        storage_store: Arc<dyn SessionStorageStore>,
1038    ) -> Self {
1039        Self {
1040            session_id,
1041            file_store: Some(file_store),
1042            storage_store: Some(storage_store),
1043            sqldb_store: None,
1044            image_store: None,
1045            provider_credential_store: None,
1046            utility_llm_service: None,
1047            message_retriever: None,
1048            session_store: None,
1049            session_mutator: None,
1050            agent_store: None,
1051            connection_resolver: None,
1052            schedule_store: None,
1053            platform_store: None,
1054            leased_resource_store: None,
1055            session_resource_registry: None,
1056            event_emitter: None,
1057            event_context: None,
1058            tool_call_id: None,
1059            capability_registry: None,
1060            tool_registry: None,
1061            memory_store: None,
1062            org_id: None,
1063            network_access: None,
1064            locale: None,
1065            budget_checker: None,
1066            payment_authority: None,
1067        }
1068    }
1069
1070    /// Add a SQL database store to this context
1071    pub fn with_sqldb_store(mut self, sqldb_store: SessionSqlDbStoreRef) -> Self {
1072        self.sqldb_store = Some(sqldb_store);
1073        self
1074    }
1075
1076    /// Add a message retriever to this context
1077    pub fn with_message_retriever(
1078        mut self,
1079        retriever: Arc<dyn crate::message_retriever::MessageRetriever>,
1080    ) -> Self {
1081        self.message_retriever = Some(retriever);
1082        self
1083    }
1084
1085    /// Add a session store to this context.
1086    pub fn with_session_store(mut self, store: Arc<dyn SessionStore>) -> Self {
1087        self.session_store = Some(store);
1088        self
1089    }
1090
1091    /// Add a session mutator to this context.
1092    pub fn with_session_mutator(mut self, mutator: Arc<dyn SessionMutator>) -> Self {
1093        self.session_mutator = Some(mutator);
1094        self
1095    }
1096
1097    /// Add an agent store to this context.
1098    pub fn with_agent_store(mut self, store: Arc<dyn AgentStore>) -> Self {
1099        self.agent_store = Some(store);
1100        self
1101    }
1102
1103    /// Add a connection resolver to this context
1104    pub fn with_connection_resolver(mut self, resolver: Arc<dyn UserConnectionResolver>) -> Self {
1105        self.connection_resolver = Some(resolver);
1106        self
1107    }
1108
1109    /// Create a context with an image artifact store.
1110    pub fn with_image_store(
1111        session_id: SessionId,
1112        image_store: Arc<dyn ImageArtifactStore>,
1113    ) -> Self {
1114        Self {
1115            session_id,
1116            file_store: None,
1117            storage_store: None,
1118            image_store: Some(image_store),
1119            provider_credential_store: None,
1120            utility_llm_service: None,
1121            sqldb_store: None,
1122            message_retriever: None,
1123            session_store: None,
1124            session_mutator: None,
1125            agent_store: None,
1126            connection_resolver: None,
1127            schedule_store: None,
1128            platform_store: None,
1129            leased_resource_store: None,
1130            session_resource_registry: None,
1131            event_emitter: None,
1132            event_context: None,
1133            tool_call_id: None,
1134            capability_registry: None,
1135            tool_registry: None,
1136            memory_store: None,
1137            org_id: None,
1138            network_access: None,
1139            locale: None,
1140            budget_checker: None,
1141            payment_authority: None,
1142        }
1143    }
1144
1145    /// Set the provider credential store on this context.
1146    pub fn with_provider_credential_store(
1147        mut self,
1148        store: Arc<dyn ProviderCredentialStore>,
1149    ) -> Self {
1150        self.provider_credential_store = Some(store);
1151        self
1152    }
1153
1154    /// Set the utility LLM service on this context.
1155    pub fn with_utility_llm_service(mut self, service: Arc<dyn crate::UtilityLlmService>) -> Self {
1156        self.utility_llm_service = Some(service);
1157        self
1158    }
1159
1160    /// Add a session schedule store to this context.
1161    pub fn with_schedule_store(mut self, store: Arc<dyn SessionScheduleStore>) -> Self {
1162        self.schedule_store = Some(store);
1163        self
1164    }
1165
1166    /// Add a platform store to this context.
1167    pub fn with_platform_store(
1168        mut self,
1169        store: Arc<dyn crate::platform_store::PlatformStore>,
1170    ) -> Self {
1171        self.platform_store = Some(store);
1172        self
1173    }
1174
1175    /// Add a leased resource store to this context.
1176    pub fn with_leased_resource_store(mut self, store: Arc<dyn LeasedResourceStore>) -> Self {
1177        self.leased_resource_store = Some(store);
1178        self
1179    }
1180
1181    /// Add a session resource registry to this context.
1182    pub fn with_session_resource_registry(
1183        mut self,
1184        registry: Arc<dyn SessionResourceRegistry>,
1185    ) -> Self {
1186        self.session_resource_registry = Some(registry);
1187        self
1188    }
1189
1190    /// Add a memory store backend for persistent cross-session memory.
1191    pub fn with_memory_store(
1192        mut self,
1193        store: Arc<dyn crate::memory_store::MemoryStoreBackend>,
1194    ) -> Self {
1195        self.memory_store = Some(store);
1196        self
1197    }
1198
1199    /// Set org ID for org-scoped operations.
1200    pub fn with_org_id(mut self, org_id: crate::typed_id::OrgId) -> Self {
1201        self.org_id = Some(org_id);
1202        self
1203    }
1204
1205    /// Set the active built-in tool registry on this context.
1206    pub fn with_tool_registry(mut self, registry: Arc<crate::tools::ToolRegistry>) -> Self {
1207        self.tool_registry = Some(registry);
1208        self
1209    }
1210
1211    /// Set the merged network access list for URL filtering.
1212    pub fn with_network_access(
1213        mut self,
1214        network_access: Option<crate::network_access::NetworkAccessList>,
1215    ) -> Self {
1216        self.network_access = network_access;
1217        self
1218    }
1219
1220    /// Set the internal payment authority for paid capability operations.
1221    pub fn with_payment_authority(mut self, authority: Arc<dyn PaymentAuthority>) -> Self {
1222        self.payment_authority = Some(authority);
1223        self
1224    }
1225
1226    /// Emit a `tool.progress` event if an event emitter and context are available.
1227    ///
1228    /// This is a best-effort helper: failures are logged but not propagated,
1229    /// so tools never fail just because a progress event couldn't be sent.
1230    pub async fn emit_progress(&self, tool_name: &str, message: &str) {
1231        let (Some(emitter), Some(ctx), Some(call_id)) =
1232            (&self.event_emitter, &self.event_context, &self.tool_call_id)
1233        else {
1234            return;
1235        };
1236        if let Err(e) = emitter
1237            .emit(EventRequest::new(
1238                self.session_id,
1239                ctx.clone(),
1240                crate::events::ToolProgressData {
1241                    tool_call_id: call_id.clone(),
1242                    tool_name: tool_name.to_string(),
1243                    message: message.to_string(),
1244                    display_name: None,
1245                },
1246            ))
1247            .await
1248        {
1249            tracing::debug!(
1250                tool_call_id = call_id,
1251                tool_name,
1252                error = %e,
1253                "Failed to emit tool.progress event"
1254            );
1255        }
1256    }
1257
1258    /// Emit a `tool.output.delta` event if an event emitter and context are available.
1259    ///
1260    /// Streams incremental output chunks (e.g., stdout/stderr lines) for live
1261    /// rendering in UI and CLI. Best-effort: failures are logged, not propagated.
1262    pub async fn emit_tool_output(&self, tool_name: &str, delta: &str, stream: &str) {
1263        let (Some(emitter), Some(ctx), Some(call_id)) =
1264            (&self.event_emitter, &self.event_context, &self.tool_call_id)
1265        else {
1266            return;
1267        };
1268        if let Err(e) = emitter
1269            .emit(EventRequest::new(
1270                self.session_id,
1271                ctx.clone(),
1272                crate::events::ToolOutputDeltaData {
1273                    tool_call_id: call_id.clone(),
1274                    tool_name: tool_name.to_string(),
1275                    delta: delta.to_string(),
1276                    stream: stream.to_string(),
1277                },
1278            ))
1279            .await
1280        {
1281            tracing::debug!(
1282                tool_call_id = call_id,
1283                tool_name,
1284                error = %e,
1285                "Failed to emit tool.output.delta event"
1286            );
1287        }
1288    }
1289}
1290
1291impl std::fmt::Debug for ToolContext {
1292    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1293        f.debug_struct("ToolContext")
1294            .field("session_id", &self.session_id)
1295            .field("file_store", &self.file_store.is_some())
1296            .field("storage_store", &self.storage_store.is_some())
1297            .field("image_store", &self.image_store.is_some())
1298            .field(
1299                "provider_credential_store",
1300                &self.provider_credential_store.is_some(),
1301            )
1302            .field("utility_llm_service", &self.utility_llm_service.is_some())
1303            .field("sqldb_store", &self.sqldb_store.is_some())
1304            .field("message_retriever", &self.message_retriever.is_some())
1305            .field("session_store", &self.session_store.is_some())
1306            .field("session_mutator", &self.session_mutator.is_some())
1307            .field("agent_store", &self.agent_store.is_some())
1308            .field("connection_resolver", &self.connection_resolver.is_some())
1309            .field("schedule_store", &self.schedule_store.is_some())
1310            .field("platform_store", &self.platform_store.is_some())
1311            .field(
1312                "leased_resource_store",
1313                &self.leased_resource_store.is_some(),
1314            )
1315            .field("event_emitter", &self.event_emitter.is_some())
1316            .field("tool_registry", &self.tool_registry.is_some())
1317            .field("memory_store", &self.memory_store.is_some())
1318            .field("payment_authority", &self.payment_authority.is_some())
1319            .field("org_id", &self.org_id)
1320            .finish()
1321    }
1322}
1323
1324// ============================================================================
1325// EventEmitter - For emitting events
1326// ============================================================================
1327
1328use crate::events::{Event, EventRequest};
1329
1330/// Trait for emitting events following the standard event protocol
1331///
1332/// Implementations can:
1333/// - Store events in a database
1334/// - Keep events in memory for testing
1335/// - Stream events via SSE/WebSocket
1336/// - Log events for debugging
1337///
1338/// Events follow a consistent schema: id, type, ts, context, data.
1339/// See specs/events.md for the full event protocol specification.
1340#[async_trait]
1341pub trait EventEmitter: Send + Sync {
1342    /// Emit an event request
1343    ///
1344    /// Takes an EventRequest (without id/sequence) and returns the stored Event
1345    /// with id and sequence assigned by the storage layer.
1346    async fn emit(&self, request: EventRequest) -> Result<Event>;
1347}
1348
1349/// Blanket impl: `Arc<E>` delegates to the inner emitter.
1350#[async_trait]
1351impl<E: EventEmitter + ?Sized> EventEmitter for Arc<E> {
1352    async fn emit(&self, request: EventRequest) -> Result<Event> {
1353        (**self).emit(request).await
1354    }
1355}
1356
1357/// No-op event emitter for when event emission is not needed
1358///
1359/// This is useful for testing or when event observability is disabled.
1360#[derive(Debug, Clone, Default)]
1361pub struct NoopEventEmitter;
1362
1363#[async_trait]
1364impl EventEmitter for NoopEventEmitter {
1365    async fn emit(&self, request: EventRequest) -> Result<Event> {
1366        // Return a dummy event with sequence 0
1367        Ok(request.into_event(crate::typed_id::EventId::new(), 0))
1368    }
1369}
1370
1371// Note: EventListener trait has been moved to event_listeners.rs module.
1372// Use `everruns_core::EventListener` or `everruns_core::event_listeners::EventListener`.
1373
1374// ============================================================================
1375// ImageResolver - For resolving image_file content to actual image data
1376// ============================================================================
1377
1378/// Resolved image data for LLM consumption
1379///
1380/// This struct contains the actual image data in a format suitable for
1381/// sending to LLM providers. Both OpenAI and Anthropic accept base64-encoded
1382/// images with media type information.
1383#[derive(Debug, Clone)]
1384pub struct ResolvedImage {
1385    /// Base64-encoded image data (without data URL prefix)
1386    pub base64: String,
1387    /// MIME type (e.g., "image/png", "image/jpeg")
1388    pub media_type: String,
1389}
1390
1391impl ResolvedImage {
1392    /// Create a new resolved image
1393    pub fn new(base64: impl Into<String>, media_type: impl Into<String>) -> Self {
1394        Self {
1395            base64: base64.into(),
1396            media_type: media_type.into(),
1397        }
1398    }
1399
1400    /// Convert to a data URL suitable for OpenAI Vision API
1401    ///
1402    /// Format: `data:{media_type};base64,{base64_data}`
1403    pub fn to_data_url(&self) -> String {
1404        format!("data:{};base64,{}", self.media_type, self.base64)
1405    }
1406}
1407
1408/// Trait for resolving image_file content parts to actual image data
1409///
1410/// When building LLM messages, `image_file` content parts contain only
1411/// a reference (UUID) to an uploaded image. This trait allows resolving
1412/// those references to actual image data.
1413///
1414/// # Provider-specific formatting
1415///
1416/// The resolved image data is then converted to provider-specific formats:
1417///
1418/// **OpenAI Vision:**
1419/// ```json
1420/// {
1421///   "type": "image_url",
1422///   "image_url": { "url": "data:image/png;base64,..." }
1423/// }
1424/// ```
1425///
1426/// **Anthropic Vision:**
1427/// ```json
1428/// {
1429///   "type": "image",
1430///   "source": { "type": "base64", "media_type": "image/png", "data": "..." }
1431/// }
1432/// ```
1433///
1434/// # Implementation notes
1435///
1436/// Implementations should:
1437/// - Fetch image data from storage (database, S3, etc.)
1438/// - Return base64-encoded data with media type
1439/// - Handle missing images gracefully (return None)
1440#[async_trait]
1441pub trait ImageResolver: Send + Sync {
1442    /// Resolve an image_file reference to actual image data
1443    ///
1444    /// Returns `None` if the image is not found.
1445    async fn resolve_image(&self, image_id: Uuid) -> Result<Option<ResolvedImage>>;
1446}
1447
1448// ============================================================================
1449// Tests
1450// ============================================================================
1451
1452#[cfg(test)]
1453mod tests {
1454    use super::*;
1455
1456    #[test]
1457    fn test_resolved_image_new() {
1458        let image = ResolvedImage::new("SGVsbG8=", "image/png");
1459        assert_eq!(image.base64, "SGVsbG8=");
1460        assert_eq!(image.media_type, "image/png");
1461    }
1462
1463    #[test]
1464    fn test_resolved_image_to_data_url() {
1465        let image = ResolvedImage::new("SGVsbG8=", "image/png");
1466        let data_url = image.to_data_url();
1467        assert_eq!(data_url, "data:image/png;base64,SGVsbG8=");
1468    }
1469
1470    #[test]
1471    fn test_resolved_image_jpeg() {
1472        let image = ResolvedImage::new("base64data", "image/jpeg");
1473        let data_url = image.to_data_url();
1474        assert!(data_url.starts_with("data:image/jpeg;base64,"));
1475    }
1476}