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/// Delegating impl so callers can hold a `ToolExecutor` as a trait object
329/// (e.g. to choose between a plain registry and an MCP-routing composite at
330/// runtime without monomorphizing the consumer).
331#[async_trait]
332impl ToolExecutor for std::sync::Arc<dyn ToolExecutor> {
333    async fn execute(&self, tool_call: &ToolCall, tool_def: &ToolDefinition) -> Result<ToolResult> {
334        (**self).execute(tool_call, tool_def).await
335    }
336
337    async fn execute_with_context(
338        &self,
339        tool_call: &ToolCall,
340        tool_def: &ToolDefinition,
341        context: &ToolContext,
342    ) -> Result<ToolResult> {
343        (**self)
344            .execute_with_context(tool_call, tool_def, context)
345            .await
346    }
347
348    async fn execute_batch(
349        &self,
350        tool_calls: &[ToolCall],
351        tool_defs: &[ToolDefinition],
352    ) -> Result<Vec<ToolResult>> {
353        (**self).execute_batch(tool_calls, tool_defs).await
354    }
355}
356
357// ============================================================================
358// SessionFileSystem - For session filesystem operations
359// ============================================================================
360
361/// Trait for session filesystem operations
362///
363/// This trait abstracts the session filesystem contract for tools and hosts.
364/// Implementations can:
365/// - Store files in a database (production)
366/// - Use an in-memory filesystem for testing
367/// - Project files onto real disk or object storage
368#[async_trait]
369pub trait SessionFileSystem: Send + Sync {
370    /// Read a file by path
371    async fn read_file(&self, session_id: SessionId, path: &str) -> Result<Option<SessionFile>>;
372
373    /// Write/create a file
374    async fn write_file(
375        &self,
376        session_id: SessionId,
377        path: &str,
378        content: &str,
379        encoding: &str,
380    ) -> Result<SessionFile>;
381
382    /// Write a file only if its current content snapshot still matches.
383    ///
384    /// Implementations backed by transactional storage should override this
385    /// with an atomic compare-and-set update.
386    async fn write_file_if_content_matches(
387        &self,
388        session_id: SessionId,
389        path: &str,
390        expected_content: &str,
391        expected_encoding: &str,
392        content: &str,
393        encoding: &str,
394    ) -> Result<Option<SessionFile>> {
395        let Some(existing) = self.read_file(session_id, path).await? else {
396            return Ok(None);
397        };
398
399        if existing.is_directory {
400            return Ok(None);
401        }
402
403        let current_content = existing.content.unwrap_or_default();
404        if current_content != expected_content || existing.encoding != expected_encoding {
405            return Ok(None);
406        }
407
408        self.write_file(session_id, path, content, encoding)
409            .await
410            .map(Some)
411    }
412
413    /// Delete a file or directory
414    async fn delete_file(&self, session_id: SessionId, path: &str, recursive: bool)
415    -> Result<bool>;
416
417    /// List files in a directory
418    async fn list_directory(&self, session_id: SessionId, path: &str) -> Result<Vec<FileInfo>>;
419
420    /// Get file metadata
421    async fn stat_file(&self, session_id: SessionId, path: &str) -> Result<Option<FileStat>>;
422
423    /// Search files by pattern (grep)
424    async fn grep_files(
425        &self,
426        session_id: SessionId,
427        pattern: &str,
428        path_pattern: Option<&str>,
429    ) -> Result<Vec<GrepMatch>>;
430
431    /// Create a directory
432    async fn create_directory(&self, session_id: SessionId, path: &str) -> Result<FileInfo>;
433
434    /// Seed a starter file into a session workspace.
435    async fn seed_initial_file(&self, session_id: SessionId, file: &InitialFile) -> Result<()> {
436        if file.is_readonly {
437            return Err(crate::error::AgentLoopError::store(
438                "read-only initial files require a SessionFileSystem-specific seed implementation",
439            ));
440        }
441        self.write_file(session_id, &file.path, &file.content, &file.encoding)
442            .await?;
443        Ok(())
444    }
445}
446
447#[async_trait]
448impl<T: SessionFileSystem + ?Sized> SessionFileSystem for std::sync::Arc<T> {
449    async fn read_file(&self, session_id: SessionId, path: &str) -> Result<Option<SessionFile>> {
450        (**self).read_file(session_id, path).await
451    }
452
453    async fn write_file(
454        &self,
455        session_id: SessionId,
456        path: &str,
457        content: &str,
458        encoding: &str,
459    ) -> Result<SessionFile> {
460        (**self)
461            .write_file(session_id, path, content, encoding)
462            .await
463    }
464
465    async fn write_file_if_content_matches(
466        &self,
467        session_id: SessionId,
468        path: &str,
469        expected_content: &str,
470        expected_encoding: &str,
471        content: &str,
472        encoding: &str,
473    ) -> Result<Option<SessionFile>> {
474        (**self)
475            .write_file_if_content_matches(
476                session_id,
477                path,
478                expected_content,
479                expected_encoding,
480                content,
481                encoding,
482            )
483            .await
484    }
485
486    async fn delete_file(
487        &self,
488        session_id: SessionId,
489        path: &str,
490        recursive: bool,
491    ) -> Result<bool> {
492        (**self).delete_file(session_id, path, recursive).await
493    }
494
495    async fn list_directory(&self, session_id: SessionId, path: &str) -> Result<Vec<FileInfo>> {
496        (**self).list_directory(session_id, path).await
497    }
498
499    async fn stat_file(&self, session_id: SessionId, path: &str) -> Result<Option<FileStat>> {
500        (**self).stat_file(session_id, path).await
501    }
502
503    async fn grep_files(
504        &self,
505        session_id: SessionId,
506        pattern: &str,
507        path_pattern: Option<&str>,
508    ) -> Result<Vec<GrepMatch>> {
509        (**self).grep_files(session_id, pattern, path_pattern).await
510    }
511
512    async fn create_directory(&self, session_id: SessionId, path: &str) -> Result<FileInfo> {
513        (**self).create_directory(session_id, path).await
514    }
515
516    async fn seed_initial_file(&self, session_id: SessionId, file: &InitialFile) -> Result<()> {
517        (**self).seed_initial_file(session_id, file).await
518    }
519}
520
521/// Backward-compatible alias for the old session filesystem trait name.
522pub use SessionFileSystem as SessionFileStore;
523
524/// Host-supplied values used by platform file-system factories.
525///
526/// The context is intentionally type-erased so `everruns-core` can own the
527/// platform contract without depending on server-only types such as
528/// `StorageBackend` or future object-storage clients.
529#[derive(Clone, Default)]
530pub struct SessionFileSystemFactoryContext {
531    values: Arc<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>,
532}
533
534impl SessionFileSystemFactoryContext {
535    pub fn new() -> Self {
536        Self::default()
537    }
538
539    pub fn with<T: Any + Send + Sync>(mut self, value: Arc<T>) -> Self {
540        let values = Arc::make_mut(&mut self.values);
541        values.insert(TypeId::of::<T>(), value);
542        self
543    }
544
545    pub fn get<T: Any + Send + Sync>(&self) -> Option<Arc<T>> {
546        self.values
547            .get(&TypeId::of::<T>())
548            .and_then(|value| value.clone().downcast::<T>().ok())
549    }
550}
551
552/// Factory for deployment-selected session filesystem implementations.
553#[async_trait]
554pub trait SessionFileSystemFactory: Send + Sync {
555    /// Human-readable factory name for diagnostics.
556    fn name(&self) -> &'static str {
557        "SessionFileSystemFactory"
558    }
559
560    /// Whether this factory intentionally leaves filesystem selection to the
561    /// runtime default.
562    fn is_disabled(&self) -> bool {
563        false
564    }
565
566    /// Resolve a live filesystem from host-provided dependencies.
567    async fn create_session_file_system(
568        &self,
569        context: SessionFileSystemFactoryContext,
570    ) -> Result<Arc<dyn SessionFileSystem>>;
571}
572
573/// Default factory used when a platform does not configure session files.
574#[derive(Debug, Clone, Default)]
575pub struct DisabledSessionFileSystemFactory;
576
577#[async_trait]
578impl SessionFileSystemFactory for DisabledSessionFileSystemFactory {
579    fn name(&self) -> &'static str {
580        "DisabledSessionFileSystemFactory"
581    }
582
583    fn is_disabled(&self) -> bool {
584        true
585    }
586
587    async fn create_session_file_system(
588        &self,
589        _context: SessionFileSystemFactoryContext,
590    ) -> Result<Arc<dyn SessionFileSystem>> {
591        Err(crate::error::AgentLoopError::config(
592            "session filesystem is disabled",
593        ))
594    }
595}
596
597// ============================================================================
598// SessionStorageStore - For session key/value and secret storage
599// ============================================================================
600
601/// Info about a stored key (without its value)
602#[derive(Debug, Clone)]
603pub struct KeyInfo {
604    pub key: String,
605    pub created_at: chrono::DateTime<chrono::Utc>,
606    pub updated_at: chrono::DateTime<chrono::Utc>,
607}
608
609/// Info about a stored secret (without its value)
610#[derive(Debug, Clone)]
611pub struct SecretInfo {
612    pub name: String,
613    pub created_at: chrono::DateTime<chrono::Utc>,
614    pub updated_at: chrono::DateTime<chrono::Utc>,
615}
616
617/// Trait for session key/value and secret storage operations
618///
619/// This trait abstracts storage operations for tools that need to persist
620/// data within a session. Implementations can:
621/// - Store data in a database (production)
622/// - Use in-memory storage for testing
623///
624/// Key/value storage is for general data that doesn't need encryption.
625/// Secret storage is for sensitive data that is encrypted at rest.
626#[async_trait]
627pub trait SessionStorageStore: Send + Sync {
628    // Key/Value operations (plain text)
629
630    /// Set a key/value pair (creates or updates)
631    async fn set_value(&self, session_id: SessionId, key: &str, value: &str) -> Result<()>;
632
633    /// Get a value by key
634    async fn get_value(&self, session_id: SessionId, key: &str) -> Result<Option<String>>;
635
636    /// Delete a key/value pair
637    async fn delete_value(&self, session_id: SessionId, key: &str) -> Result<bool>;
638
639    /// List all keys in a session
640    async fn list_keys(&self, session_id: SessionId) -> Result<Vec<KeyInfo>>;
641
642    // Secret operations (encrypted)
643
644    /// Set a secret (creates or updates, value is encrypted before storage)
645    async fn set_secret(&self, session_id: SessionId, name: &str, value: &str) -> Result<()>;
646
647    /// Get a secret by name (value is decrypted before returning)
648    async fn get_secret(&self, session_id: SessionId, name: &str) -> Result<Option<String>>;
649
650    /// Delete a secret
651    async fn delete_secret(&self, session_id: SessionId, name: &str) -> Result<bool>;
652
653    /// List all secret names in a session (without values)
654    async fn list_secrets(&self, session_id: SessionId) -> Result<Vec<SecretInfo>>;
655}
656
657// ============================================================================
658// SessionScheduleStore - For session-scoped schedule operations
659// ============================================================================
660
661use crate::session_schedule::SessionSchedule;
662use crate::typed_id::ScheduleId;
663
664/// Trait for session schedule CRUD operations.
665///
666/// Used by scheduling tools to create, cancel, and list schedules.
667#[async_trait]
668pub trait SessionScheduleStore: Send + Sync {
669    /// Create a new schedule for a session.
670    async fn create_schedule(
671        &self,
672        session_id: SessionId,
673        description: String,
674        cron_expression: Option<String>,
675        scheduled_at: Option<chrono::DateTime<chrono::Utc>>,
676        timezone: String,
677    ) -> Result<SessionSchedule>;
678
679    /// Cancel (disable) a schedule.
680    async fn cancel_schedule(
681        &self,
682        session_id: SessionId,
683        schedule_id: ScheduleId,
684    ) -> Result<SessionSchedule>;
685
686    /// List schedules for a session.
687    async fn list_schedules(&self, session_id: SessionId) -> Result<Vec<SessionSchedule>>;
688
689    /// Count active (enabled) schedules for a session.
690    async fn count_active_schedules(&self, session_id: SessionId) -> Result<u32>;
691}
692
693// ============================================================================
694// SessionResourceRegistry - Generic session-scoped resource registry
695// ============================================================================
696
697/// Generic registry of resources active alongside a session.
698///
699/// Capabilities register resources here (sandboxes, subagents, browser sessions).
700/// Agents query it ("what's running?"), infrastructure scans it for cleanup.
701/// See `specs/session-resources.md`.
702#[async_trait]
703pub trait SessionResourceRegistry: Send + Sync {
704    /// Register a resource (or update if resource_id already exists for this session).
705    async fn register(
706        &self,
707        entry: crate::session_resource::RegisterSessionResource,
708    ) -> Result<crate::session_resource::SessionResourceEntry>;
709
710    /// Update the status of a registered resource.
711    async fn update_status(
712        &self,
713        session_id: SessionId,
714        resource_id: &str,
715        status: crate::session_resource::SessionResourceStatus,
716    ) -> Result<Option<crate::session_resource::SessionResourceEntry>>;
717
718    /// Get a specific resource by ID.
719    async fn get(
720        &self,
721        session_id: SessionId,
722        resource_id: &str,
723    ) -> Result<Option<crate::session_resource::SessionResourceEntry>>;
724
725    /// List resources for a session, optionally filtered.
726    async fn list(
727        &self,
728        session_id: SessionId,
729        filter: Option<&crate::session_resource::SessionResourceFilter>,
730    ) -> Result<Vec<crate::session_resource::SessionResourceEntry>>;
731
732    /// Remove a resource from the registry.
733    async fn deregister(&self, session_id: SessionId, resource_id: &str) -> Result<bool>;
734}
735
736// ============================================================================
737// LeasedResourceStore - For lifecycle-managed external resources
738// ============================================================================
739
740/// Trait for session-scoped leased resource operations.
741///
742/// Tools use this store to register or refresh leases when they create or use
743/// external provider resources. Cleanup workers operate through control-plane
744/// storage APIs directly so they can claim work across organizations.
745#[async_trait]
746pub trait LeasedResourceStore: Send + Sync {
747    /// Create or refresh a leased resource for a session.
748    ///
749    /// Implementations must treat this as an idempotent upsert keyed by the
750    /// provider-specific resource identity so repeated tool usage extends the
751    /// same lease instead of creating duplicate rows.
752    async fn upsert_resource(&self, input: UpsertLeasedResource) -> Result<LeasedResource>;
753
754    /// Mark a leased resource as explicitly released.
755    ///
756    /// This is the fast path for explicit user intent such as "close browser"
757    /// or "delete sandbox". It should transition the resource to `released`
758    /// without waiting for the durable cleanup worker to observe lease expiry.
759    async fn release_resource(
760        &self,
761        session_id: SessionId,
762        provider: &str,
763        resource_type: &str,
764        external_id: &str,
765    ) -> Result<Option<LeasedResource>>;
766
767    /// List leased resources currently associated with a session.
768    ///
769    /// Session surfaces use this for visibility. Released resources remain
770    /// visible so operators can inspect cleanup outcomes and failure history.
771    async fn list_resources(&self, session_id: SessionId) -> Result<Vec<LeasedResource>>;
772}
773
774// ============================================================================
775// ToolContext - Runtime context for tool execution
776// ============================================================================
777
778/// Type alias for the session SQL DB store trait object.
779pub type SessionSqlDbStoreRef = Arc<dyn crate::session_sqldb::SessionSqlDbStore>;
780
781/// Resolves user connection tokens (e.g. GitHub) lazily at tool execution time.
782///
783/// Instead of eagerly injecting tokens at session creation, tools call this
784/// resolver when they need a token. If the user hasn't connected, returns None.
785#[async_trait]
786pub trait UserConnectionResolver: Send + Sync {
787    /// Get a decrypted connection token for the given provider.
788    /// Returns None if the user has no connection for this provider.
789    async fn get_connection_token(
790        &self,
791        session_id: SessionId,
792        provider: &str,
793    ) -> Result<Option<String>>;
794
795    /// Resolve the user ID of the connection used for a session/provider pair.
796    ///
797    /// This is used by leased resources to bind cleanup to the same provider
798    /// identity that created the remote resource.
799    async fn get_connection_user(
800        &self,
801        _session_id: SessionId,
802        _provider: &str,
803    ) -> Result<Option<Uuid>> {
804        Ok(None)
805    }
806
807    /// Resolve a provider token for a specific user.
808    ///
809    /// Cleanup workers use this to avoid "first org member wins" behavior when
810    /// cleaning resources created by a specific provider connection owner.
811    async fn get_connection_token_for_user(
812        &self,
813        _user_id: Uuid,
814        _provider: &str,
815    ) -> Result<Option<String>> {
816        Ok(None)
817    }
818
819    /// Get provider-specific metadata stored alongside the connection.
820    /// Returns None if no metadata is stored or no connection exists.
821    async fn get_connection_metadata(
822        &self,
823        _session_id: SessionId,
824        _provider: &str,
825    ) -> Result<Option<serde_json::Value>> {
826        Ok(None)
827    }
828}
829
830// ============================================================================
831// BudgetChecker - For querying budget status from tools
832// ============================================================================
833
834/// Trait for checking budget status from within tool execution.
835///
836/// Implemented by gRPC adapters (worker → server) and direct adapters (in-process).
837/// Used by the `check_budget` tool to return real budget data to agents.
838/// The org_id is captured at construction time by the implementing adapter.
839#[async_trait]
840pub trait BudgetChecker: Send + Sync {
841    /// Check all budgets for a session and return a tool-friendly response.
842    async fn check_budgets(&self, session_id: &str) -> Result<crate::budget::BudgetToolResponse>;
843}
844
845// ============================================================================
846// PaymentAuthority - For capability-internal machine payments
847// ============================================================================
848
849/// Internal authority for paid capability operations.
850///
851/// Capabilities call this with fixed, typed requests. The model never receives a
852/// generic paid HTTP tool, wallet credentials, or payment payloads.
853#[async_trait]
854pub trait PaymentAuthority: Send + Sync {
855    async fn execute_machine_payment(
856        &self,
857        session_id: SessionId,
858        request: crate::payment::MachinePaymentRequest,
859    ) -> Result<crate::payment::MachinePaymentResponse>;
860}
861
862// OutboundToolRateLimiter - Per-org outbound tool-call rate limiting (TM-TOOL-009)
863// ============================================================================
864
865/// Per-org gate on outbound tool execution.
866///
867/// Returns `true` if the call is within the per-org budget, `false` if the
868/// org has exceeded its outbound tool rate limit for this window.
869/// Implementations must be fail-open: Valkey/backend errors should return `true`
870/// rather than blocking legitimate tool calls.
871#[async_trait]
872pub trait OutboundToolRateLimiter: Send + Sync {
873    /// Key by the public org UUID (keyed string representation).
874    async fn check_org(&self, org_id: &crate::typed_id::OrgId) -> bool;
875}
876
877/// Runtime context provided to tools during execution.
878///
879/// This context contains:
880/// - Session ID for scoping operations
881/// - Optional stores for tools that need external access
882///
883/// Tools that need context-aware execution (like filesystem tools) can use
884/// the `execute_with_context` method on the Tool trait.
885#[derive(Clone)]
886pub struct ToolContext {
887    /// The session ID for the current execution
888    pub session_id: SessionId,
889
890    /// Optional file store for filesystem operations
891    pub file_store: Option<Arc<dyn SessionFileSystem>>,
892
893    /// Optional storage store for key/value and secret storage
894    pub storage_store: Option<Arc<dyn SessionStorageStore>>,
895
896    /// Optional durable image artifact store for tool-side media persistence.
897    pub image_store: Option<Arc<dyn ImageArtifactStore>>,
898
899    /// Optional provider credential store for tool-side API clients.
900    pub provider_credential_store: Option<Arc<dyn ProviderCredentialStore>>,
901
902    /// Optional system utility LLM service for capability internals.
903    pub utility_llm_service: Option<Arc<dyn crate::UtilityLlmService>>,
904
905    /// Optional outbound egress service for HTTP/API traffic.
906    pub egress_service: Option<Arc<dyn crate::EgressService>>,
907
908    /// Optional session SQL database store
909    pub sqldb_store: Option<SessionSqlDbStoreRef>,
910
911    /// Optional message retriever for tools that need conversation history access
912    pub message_retriever: Option<Arc<dyn crate::message_retriever::MessageRetriever>>,
913
914    /// Optional session store for tools that need session metadata access.
915    pub session_store: Option<Arc<dyn SessionStore>>,
916
917    /// Optional session mutator for tools that need to update session metadata.
918    pub session_mutator: Option<Arc<dyn SessionMutator>>,
919
920    /// Optional agent store for tools that need agent metadata access.
921    pub agent_store: Option<Arc<dyn AgentStore>>,
922
923    /// Optional resolver for user connection tokens (lazy GitHub token lookup, etc.)
924    pub connection_resolver: Option<Arc<dyn UserConnectionResolver>>,
925
926    /// Optional session schedule store for scheduling tools.
927    pub schedule_store: Option<Arc<dyn SessionScheduleStore>>,
928
929    /// Optional platform store for org-level management tools.
930    pub platform_store: Option<Arc<dyn crate::platform_store::PlatformStore>>,
931    /// Optional leased resource store for lifecycle-managed provider resources.
932    pub leased_resource_store: Option<Arc<dyn LeasedResourceStore>>,
933
934    /// Optional session resource registry — generic registry of active resources.
935    pub session_resource_registry: Option<Arc<dyn SessionResourceRegistry>>,
936
937    /// Optional event emitter for tools that need to stream progress updates.
938    /// When set, tools can emit `tool.progress` events during execution.
939    pub event_emitter: Option<Arc<dyn EventEmitter>>,
940
941    /// Event context for correlating progress events with the current tool call.
942    /// Set by ActAtom when constructing the ToolContext.
943    pub event_context: Option<crate::events::EventContext>,
944
945    /// The tool call ID for the current execution (set by ActAtom).
946    /// Used by tools to emit correlated progress events.
947    pub tool_call_id: Option<String>,
948    /// Optional capability registry for blueprint lookups.
949    pub capability_registry: Option<crate::capabilities::CapabilityRegistry>,
950
951    /// Optional registry of active built-in tools for meta-tools such as
952    /// `spawn_background` that need to inspect or delegate to sibling tools.
953    pub tool_registry: Option<Arc<crate::tools::ToolRegistry>>,
954
955    /// Optional memory store backend for persistent cross-session memory.
956    pub memory_store: Option<Arc<dyn crate::memory_store::MemoryStoreBackend>>,
957
958    /// Optional org ID for org-scoped operations (memory stores, etc.).
959    pub org_id: Option<crate::typed_id::OrgId>,
960
961    /// Merged network access list (harness ∩ agent ∩ session).
962    /// When set, tools that make HTTP requests must check URLs against this list.
963    pub network_access: Option<crate::network_access::NetworkAccessList>,
964
965    /// Resolved locale for localized tool behavior (BCP 47, e.g. `uk-UA`).
966    /// When set, tools that support localization use this to produce
967    /// locale-appropriate descriptions, error messages, and prompts.
968    pub locale: Option<String>,
969
970    /// Optional budget checker for the check_budget tool.
971    pub budget_checker: Option<Arc<dyn BudgetChecker>>,
972
973    /// Optional internal payment authority for paid capability tools.
974    pub payment_authority: Option<Arc<dyn PaymentAuthority>>,
975}
976
977impl ToolContext {
978    /// Create a new tool context with just a session ID
979    pub fn new(session_id: SessionId) -> Self {
980        Self {
981            session_id,
982            file_store: None,
983            storage_store: None,
984            image_store: None,
985            provider_credential_store: None,
986            utility_llm_service: None,
987            egress_service: None,
988            sqldb_store: None,
989            message_retriever: None,
990            session_store: None,
991            session_mutator: None,
992            agent_store: None,
993            connection_resolver: None,
994            schedule_store: None,
995            platform_store: None,
996            leased_resource_store: None,
997            session_resource_registry: None,
998            event_emitter: None,
999            event_context: None,
1000            tool_call_id: None,
1001            capability_registry: None,
1002            tool_registry: None,
1003            memory_store: None,
1004            org_id: None,
1005            network_access: None,
1006            locale: None,
1007            budget_checker: None,
1008            payment_authority: None,
1009        }
1010    }
1011
1012    /// Create a context with a file store
1013    pub fn with_file_store(session_id: SessionId, file_store: Arc<dyn SessionFileSystem>) -> Self {
1014        Self {
1015            session_id,
1016            file_store: Some(file_store),
1017            storage_store: None,
1018            image_store: None,
1019            provider_credential_store: None,
1020            utility_llm_service: None,
1021            egress_service: None,
1022            sqldb_store: None,
1023            message_retriever: None,
1024            session_store: None,
1025            session_mutator: None,
1026            agent_store: None,
1027            connection_resolver: None,
1028            schedule_store: None,
1029            platform_store: None,
1030            leased_resource_store: None,
1031            session_resource_registry: None,
1032            event_emitter: None,
1033            event_context: None,
1034            tool_call_id: None,
1035            capability_registry: None,
1036            tool_registry: None,
1037            memory_store: None,
1038            org_id: None,
1039            network_access: None,
1040            locale: None,
1041            budget_checker: None,
1042            payment_authority: None,
1043        }
1044    }
1045
1046    /// Create a context with a storage store
1047    pub fn with_storage_store(
1048        session_id: SessionId,
1049        storage_store: Arc<dyn SessionStorageStore>,
1050    ) -> Self {
1051        Self {
1052            session_id,
1053            file_store: None,
1054            storage_store: Some(storage_store),
1055            image_store: None,
1056            provider_credential_store: None,
1057            utility_llm_service: None,
1058            egress_service: None,
1059            sqldb_store: None,
1060            message_retriever: None,
1061            session_store: None,
1062            session_mutator: None,
1063            agent_store: None,
1064            connection_resolver: None,
1065            schedule_store: None,
1066            platform_store: None,
1067            leased_resource_store: None,
1068            session_resource_registry: None,
1069            event_emitter: None,
1070            event_context: None,
1071            tool_call_id: None,
1072            capability_registry: None,
1073            tool_registry: None,
1074            memory_store: None,
1075            org_id: None,
1076            network_access: None,
1077            locale: None,
1078            budget_checker: None,
1079            payment_authority: None,
1080        }
1081    }
1082
1083    /// Create a context with both file store and storage store
1084    pub fn with_stores(
1085        session_id: SessionId,
1086        file_store: Arc<dyn SessionFileSystem>,
1087        storage_store: Arc<dyn SessionStorageStore>,
1088    ) -> Self {
1089        Self {
1090            session_id,
1091            file_store: Some(file_store),
1092            storage_store: Some(storage_store),
1093            sqldb_store: None,
1094            image_store: None,
1095            provider_credential_store: None,
1096            utility_llm_service: None,
1097            egress_service: None,
1098            message_retriever: None,
1099            session_store: None,
1100            session_mutator: None,
1101            agent_store: None,
1102            connection_resolver: None,
1103            schedule_store: None,
1104            platform_store: None,
1105            leased_resource_store: None,
1106            session_resource_registry: None,
1107            event_emitter: None,
1108            event_context: None,
1109            tool_call_id: None,
1110            capability_registry: None,
1111            tool_registry: None,
1112            memory_store: None,
1113            org_id: None,
1114            network_access: None,
1115            locale: None,
1116            budget_checker: None,
1117            payment_authority: None,
1118        }
1119    }
1120
1121    /// Add a SQL database store to this context
1122    pub fn with_sqldb_store(mut self, sqldb_store: SessionSqlDbStoreRef) -> Self {
1123        self.sqldb_store = Some(sqldb_store);
1124        self
1125    }
1126
1127    /// Add a message retriever to this context
1128    pub fn with_message_retriever(
1129        mut self,
1130        retriever: Arc<dyn crate::message_retriever::MessageRetriever>,
1131    ) -> Self {
1132        self.message_retriever = Some(retriever);
1133        self
1134    }
1135
1136    /// Add a session store to this context.
1137    pub fn with_session_store(mut self, store: Arc<dyn SessionStore>) -> Self {
1138        self.session_store = Some(store);
1139        self
1140    }
1141
1142    /// Add a session mutator to this context.
1143    pub fn with_session_mutator(mut self, mutator: Arc<dyn SessionMutator>) -> Self {
1144        self.session_mutator = Some(mutator);
1145        self
1146    }
1147
1148    /// Add an agent store to this context.
1149    pub fn with_agent_store(mut self, store: Arc<dyn AgentStore>) -> Self {
1150        self.agent_store = Some(store);
1151        self
1152    }
1153
1154    /// Add a connection resolver to this context
1155    pub fn with_connection_resolver(mut self, resolver: Arc<dyn UserConnectionResolver>) -> Self {
1156        self.connection_resolver = Some(resolver);
1157        self
1158    }
1159
1160    /// Create a context with an image artifact store.
1161    pub fn with_image_store(
1162        session_id: SessionId,
1163        image_store: Arc<dyn ImageArtifactStore>,
1164    ) -> Self {
1165        Self {
1166            session_id,
1167            file_store: None,
1168            storage_store: None,
1169            image_store: Some(image_store),
1170            provider_credential_store: None,
1171            utility_llm_service: None,
1172            egress_service: None,
1173            sqldb_store: None,
1174            message_retriever: None,
1175            session_store: None,
1176            session_mutator: None,
1177            agent_store: None,
1178            connection_resolver: None,
1179            schedule_store: None,
1180            platform_store: None,
1181            leased_resource_store: None,
1182            session_resource_registry: None,
1183            event_emitter: None,
1184            event_context: None,
1185            tool_call_id: None,
1186            capability_registry: None,
1187            tool_registry: None,
1188            memory_store: None,
1189            org_id: None,
1190            network_access: None,
1191            locale: None,
1192            budget_checker: None,
1193            payment_authority: None,
1194        }
1195    }
1196
1197    /// Set the provider credential store on this context.
1198    pub fn with_provider_credential_store(
1199        mut self,
1200        store: Arc<dyn ProviderCredentialStore>,
1201    ) -> Self {
1202        self.provider_credential_store = Some(store);
1203        self
1204    }
1205
1206    /// Set the utility LLM service on this context.
1207    pub fn with_utility_llm_service(mut self, service: Arc<dyn crate::UtilityLlmService>) -> Self {
1208        self.utility_llm_service = Some(service);
1209        self
1210    }
1211
1212    /// Set the outbound egress service on this context.
1213    pub fn with_egress_service(mut self, service: Arc<dyn crate::EgressService>) -> Self {
1214        self.egress_service = Some(service);
1215        self
1216    }
1217
1218    /// Add a session schedule store to this context.
1219    pub fn with_schedule_store(mut self, store: Arc<dyn SessionScheduleStore>) -> Self {
1220        self.schedule_store = Some(store);
1221        self
1222    }
1223
1224    /// Add a platform store to this context.
1225    pub fn with_platform_store(
1226        mut self,
1227        store: Arc<dyn crate::platform_store::PlatformStore>,
1228    ) -> Self {
1229        self.platform_store = Some(store);
1230        self
1231    }
1232
1233    /// Add a leased resource store to this context.
1234    pub fn with_leased_resource_store(mut self, store: Arc<dyn LeasedResourceStore>) -> Self {
1235        self.leased_resource_store = Some(store);
1236        self
1237    }
1238
1239    /// Add a session resource registry to this context.
1240    pub fn with_session_resource_registry(
1241        mut self,
1242        registry: Arc<dyn SessionResourceRegistry>,
1243    ) -> Self {
1244        self.session_resource_registry = Some(registry);
1245        self
1246    }
1247
1248    /// Add a memory store backend for persistent cross-session memory.
1249    pub fn with_memory_store(
1250        mut self,
1251        store: Arc<dyn crate::memory_store::MemoryStoreBackend>,
1252    ) -> Self {
1253        self.memory_store = Some(store);
1254        self
1255    }
1256
1257    /// Set org ID for org-scoped operations.
1258    pub fn with_org_id(mut self, org_id: crate::typed_id::OrgId) -> Self {
1259        self.org_id = Some(org_id);
1260        self
1261    }
1262
1263    /// Set the active built-in tool registry on this context.
1264    pub fn with_tool_registry(mut self, registry: Arc<crate::tools::ToolRegistry>) -> Self {
1265        self.tool_registry = Some(registry);
1266        self
1267    }
1268
1269    /// Set the merged network access list for URL filtering.
1270    pub fn with_network_access(
1271        mut self,
1272        network_access: Option<crate::network_access::NetworkAccessList>,
1273    ) -> Self {
1274        self.network_access = network_access;
1275        self
1276    }
1277
1278    /// Set the internal payment authority for paid capability operations.
1279    pub fn with_payment_authority(mut self, authority: Arc<dyn PaymentAuthority>) -> Self {
1280        self.payment_authority = Some(authority);
1281        self
1282    }
1283
1284    /// Emit a `tool.progress` event if an event emitter and context are available.
1285    ///
1286    /// This is a best-effort helper: failures are logged but not propagated,
1287    /// so tools never fail just because a progress event couldn't be sent.
1288    pub async fn emit_progress(&self, tool_name: &str, message: &str) {
1289        let (Some(emitter), Some(ctx), Some(call_id)) =
1290            (&self.event_emitter, &self.event_context, &self.tool_call_id)
1291        else {
1292            return;
1293        };
1294        if let Err(e) = emitter
1295            .emit(EventRequest::new(
1296                self.session_id,
1297                ctx.clone(),
1298                crate::events::ToolProgressData {
1299                    tool_call_id: call_id.clone(),
1300                    tool_name: tool_name.to_string(),
1301                    message: message.to_string(),
1302                    display_name: None,
1303                },
1304            ))
1305            .await
1306        {
1307            tracing::debug!(
1308                tool_call_id = call_id,
1309                tool_name,
1310                error = %e,
1311                "Failed to emit tool.progress event"
1312            );
1313        }
1314    }
1315
1316    /// Emit a `tool.output.delta` event if an event emitter and context are available.
1317    ///
1318    /// Streams incremental output chunks (e.g., stdout/stderr lines) for live
1319    /// rendering in UI and CLI. Best-effort: failures are logged, not propagated.
1320    pub async fn emit_tool_output(&self, tool_name: &str, delta: &str, stream: &str) {
1321        let (Some(emitter), Some(ctx), Some(call_id)) =
1322            (&self.event_emitter, &self.event_context, &self.tool_call_id)
1323        else {
1324            return;
1325        };
1326        if let Err(e) = emitter
1327            .emit(EventRequest::new(
1328                self.session_id,
1329                ctx.clone(),
1330                crate::events::ToolOutputDeltaData {
1331                    tool_call_id: call_id.clone(),
1332                    tool_name: tool_name.to_string(),
1333                    delta: delta.to_string(),
1334                    stream: stream.to_string(),
1335                },
1336            ))
1337            .await
1338        {
1339            tracing::debug!(
1340                tool_call_id = call_id,
1341                tool_name,
1342                error = %e,
1343                "Failed to emit tool.output.delta event"
1344            );
1345        }
1346    }
1347}
1348
1349impl std::fmt::Debug for ToolContext {
1350    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1351        f.debug_struct("ToolContext")
1352            .field("session_id", &self.session_id)
1353            .field("file_store", &self.file_store.is_some())
1354            .field("storage_store", &self.storage_store.is_some())
1355            .field("image_store", &self.image_store.is_some())
1356            .field(
1357                "provider_credential_store",
1358                &self.provider_credential_store.is_some(),
1359            )
1360            .field("utility_llm_service", &self.utility_llm_service.is_some())
1361            .field("egress_service", &self.egress_service.is_some())
1362            .field("sqldb_store", &self.sqldb_store.is_some())
1363            .field("message_retriever", &self.message_retriever.is_some())
1364            .field("session_store", &self.session_store.is_some())
1365            .field("session_mutator", &self.session_mutator.is_some())
1366            .field("agent_store", &self.agent_store.is_some())
1367            .field("connection_resolver", &self.connection_resolver.is_some())
1368            .field("schedule_store", &self.schedule_store.is_some())
1369            .field("platform_store", &self.platform_store.is_some())
1370            .field(
1371                "leased_resource_store",
1372                &self.leased_resource_store.is_some(),
1373            )
1374            .field("event_emitter", &self.event_emitter.is_some())
1375            .field("tool_registry", &self.tool_registry.is_some())
1376            .field("memory_store", &self.memory_store.is_some())
1377            .field("payment_authority", &self.payment_authority.is_some())
1378            .field("org_id", &self.org_id)
1379            .finish()
1380    }
1381}
1382
1383// ============================================================================
1384// EventEmitter - For emitting events
1385// ============================================================================
1386
1387use crate::events::{Event, EventRequest};
1388
1389/// Trait for emitting events following the standard event protocol
1390///
1391/// Implementations can:
1392/// - Store events in a database
1393/// - Keep events in memory for testing
1394/// - Stream events via SSE/WebSocket
1395/// - Log events for debugging
1396///
1397/// Events follow a consistent schema: id, type, ts, context, data.
1398/// See specs/events.md for the full event protocol specification.
1399#[async_trait]
1400pub trait EventEmitter: Send + Sync {
1401    /// Emit an event request
1402    ///
1403    /// Takes an EventRequest (without id/sequence) and returns the stored Event
1404    /// with id and sequence assigned by the storage layer.
1405    async fn emit(&self, request: EventRequest) -> Result<Event>;
1406}
1407
1408/// Blanket impl: `Arc<E>` delegates to the inner emitter.
1409#[async_trait]
1410impl<E: EventEmitter + ?Sized> EventEmitter for Arc<E> {
1411    async fn emit(&self, request: EventRequest) -> Result<Event> {
1412        (**self).emit(request).await
1413    }
1414}
1415
1416/// No-op event emitter for when event emission is not needed
1417///
1418/// This is useful for testing or when event observability is disabled.
1419#[derive(Debug, Clone, Default)]
1420pub struct NoopEventEmitter;
1421
1422#[async_trait]
1423impl EventEmitter for NoopEventEmitter {
1424    async fn emit(&self, request: EventRequest) -> Result<Event> {
1425        // Return a dummy event with sequence 0
1426        Ok(request.into_event(crate::typed_id::EventId::new(), 0))
1427    }
1428}
1429
1430// Note: EventListener trait has been moved to event_listeners.rs module.
1431// Use `everruns_core::EventListener` or `everruns_core::event_listeners::EventListener`.
1432
1433// ============================================================================
1434// ImageResolver - For resolving image_file content to actual image data
1435// ============================================================================
1436
1437/// Resolved image data for LLM consumption
1438///
1439/// This struct contains the actual image data in a format suitable for
1440/// sending to LLM providers. Both OpenAI and Anthropic accept base64-encoded
1441/// images with media type information.
1442#[derive(Debug, Clone)]
1443pub struct ResolvedImage {
1444    /// Base64-encoded image data (without data URL prefix)
1445    pub base64: String,
1446    /// MIME type (e.g., "image/png", "image/jpeg")
1447    pub media_type: String,
1448}
1449
1450impl ResolvedImage {
1451    /// Create a new resolved image
1452    pub fn new(base64: impl Into<String>, media_type: impl Into<String>) -> Self {
1453        Self {
1454            base64: base64.into(),
1455            media_type: media_type.into(),
1456        }
1457    }
1458
1459    /// Convert to a data URL suitable for OpenAI Vision API
1460    ///
1461    /// Format: `data:{media_type};base64,{base64_data}`
1462    pub fn to_data_url(&self) -> String {
1463        format!("data:{};base64,{}", self.media_type, self.base64)
1464    }
1465}
1466
1467/// Trait for resolving image_file content parts to actual image data
1468///
1469/// When building LLM messages, `image_file` content parts contain only
1470/// a reference (UUID) to an uploaded image. This trait allows resolving
1471/// those references to actual image data.
1472///
1473/// # Provider-specific formatting
1474///
1475/// The resolved image data is then converted to provider-specific formats:
1476///
1477/// **OpenAI Vision:**
1478/// ```json
1479/// {
1480///   "type": "image_url",
1481///   "image_url": { "url": "data:image/png;base64,..." }
1482/// }
1483/// ```
1484///
1485/// **Anthropic Vision:**
1486/// ```json
1487/// {
1488///   "type": "image",
1489///   "source": { "type": "base64", "media_type": "image/png", "data": "..." }
1490/// }
1491/// ```
1492///
1493/// # Implementation notes
1494///
1495/// Implementations should:
1496/// - Fetch image data from storage (database, S3, etc.)
1497/// - Return base64-encoded data with media type
1498/// - Handle missing images gracefully (return None)
1499#[async_trait]
1500pub trait ImageResolver: Send + Sync {
1501    /// Resolve an image_file reference to actual image data
1502    ///
1503    /// Returns `None` if the image is not found.
1504    async fn resolve_image(&self, image_id: Uuid) -> Result<Option<ResolvedImage>>;
1505}
1506
1507// ============================================================================
1508// Tests
1509// ============================================================================
1510
1511#[cfg(test)]
1512mod tests {
1513    use super::*;
1514
1515    #[test]
1516    fn test_resolved_image_new() {
1517        let image = ResolvedImage::new("SGVsbG8=", "image/png");
1518        assert_eq!(image.base64, "SGVsbG8=");
1519        assert_eq!(image.media_type, "image/png");
1520    }
1521
1522    #[test]
1523    fn test_resolved_image_to_data_url() {
1524        let image = ResolvedImage::new("SGVsbG8=", "image/png");
1525        let data_url = image.to_data_url();
1526        assert_eq!(data_url, "data:image/png;base64,SGVsbG8=");
1527    }
1528
1529    #[test]
1530    fn test_resolved_image_jpeg() {
1531        let image = ResolvedImage::new("base64data", "image/jpeg");
1532        let data_url = image.to_data_url();
1533        assert!(data_url.starts_with("data:image/jpeg;base64,"));
1534    }
1535}