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, HashSet};
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 allowlist of tools visible to the model for this turn.
956    /// Registry-introspecting tools must filter through this before returning
957    /// sibling tool metadata, because the execution registry can be a superset.
958    pub visible_tool_names: Option<Arc<HashSet<String>>>,
959
960    /// Optional memory store backend for persistent cross-session memory.
961    pub memory_store: Option<Arc<dyn crate::memory_store::MemoryStoreBackend>>,
962
963    /// Optional org ID for org-scoped operations (memory stores, etc.).
964    pub org_id: Option<crate::typed_id::OrgId>,
965
966    /// Merged network access list (harness ∩ agent ∩ session).
967    /// When set, tools that make HTTP requests must check URLs against this list.
968    pub network_access: Option<crate::network_access::NetworkAccessList>,
969
970    /// Resolved locale for localized tool behavior (BCP 47, e.g. `uk-UA`).
971    /// When set, tools that support localization use this to produce
972    /// locale-appropriate descriptions, error messages, and prompts.
973    pub locale: Option<String>,
974
975    /// Optional budget checker for the check_budget tool.
976    pub budget_checker: Option<Arc<dyn BudgetChecker>>,
977
978    /// Optional internal payment authority for paid capability tools.
979    pub payment_authority: Option<Arc<dyn PaymentAuthority>>,
980}
981
982impl ToolContext {
983    /// Create a new tool context with just a session ID
984    pub fn new(session_id: SessionId) -> Self {
985        Self {
986            session_id,
987            file_store: None,
988            storage_store: None,
989            image_store: None,
990            provider_credential_store: None,
991            utility_llm_service: None,
992            egress_service: None,
993            sqldb_store: None,
994            message_retriever: None,
995            session_store: None,
996            session_mutator: None,
997            agent_store: None,
998            connection_resolver: None,
999            schedule_store: None,
1000            platform_store: None,
1001            leased_resource_store: None,
1002            session_resource_registry: None,
1003            event_emitter: None,
1004            event_context: None,
1005            tool_call_id: None,
1006            capability_registry: None,
1007            tool_registry: None,
1008            visible_tool_names: None,
1009            memory_store: None,
1010            org_id: None,
1011            network_access: None,
1012            locale: None,
1013            budget_checker: None,
1014            payment_authority: None,
1015        }
1016    }
1017
1018    /// Create a context with a file store
1019    pub fn with_file_store(session_id: SessionId, file_store: Arc<dyn SessionFileSystem>) -> Self {
1020        Self {
1021            session_id,
1022            file_store: Some(file_store),
1023            storage_store: None,
1024            image_store: None,
1025            provider_credential_store: None,
1026            utility_llm_service: None,
1027            egress_service: None,
1028            sqldb_store: None,
1029            message_retriever: None,
1030            session_store: None,
1031            session_mutator: None,
1032            agent_store: None,
1033            connection_resolver: None,
1034            schedule_store: None,
1035            platform_store: None,
1036            leased_resource_store: None,
1037            session_resource_registry: None,
1038            event_emitter: None,
1039            event_context: None,
1040            tool_call_id: None,
1041            capability_registry: None,
1042            tool_registry: None,
1043            visible_tool_names: None,
1044            memory_store: None,
1045            org_id: None,
1046            network_access: None,
1047            locale: None,
1048            budget_checker: None,
1049            payment_authority: None,
1050        }
1051    }
1052
1053    /// Create a context with a storage store
1054    pub fn with_storage_store(
1055        session_id: SessionId,
1056        storage_store: Arc<dyn SessionStorageStore>,
1057    ) -> Self {
1058        Self {
1059            session_id,
1060            file_store: None,
1061            storage_store: Some(storage_store),
1062            image_store: None,
1063            provider_credential_store: None,
1064            utility_llm_service: None,
1065            egress_service: None,
1066            sqldb_store: None,
1067            message_retriever: None,
1068            session_store: None,
1069            session_mutator: None,
1070            agent_store: None,
1071            connection_resolver: None,
1072            schedule_store: None,
1073            platform_store: None,
1074            leased_resource_store: None,
1075            session_resource_registry: None,
1076            event_emitter: None,
1077            event_context: None,
1078            tool_call_id: None,
1079            capability_registry: None,
1080            tool_registry: None,
1081            visible_tool_names: None,
1082            memory_store: None,
1083            org_id: None,
1084            network_access: None,
1085            locale: None,
1086            budget_checker: None,
1087            payment_authority: None,
1088        }
1089    }
1090
1091    /// Create a context with both file store and storage store
1092    pub fn with_stores(
1093        session_id: SessionId,
1094        file_store: Arc<dyn SessionFileSystem>,
1095        storage_store: Arc<dyn SessionStorageStore>,
1096    ) -> Self {
1097        Self {
1098            session_id,
1099            file_store: Some(file_store),
1100            storage_store: Some(storage_store),
1101            sqldb_store: None,
1102            image_store: None,
1103            provider_credential_store: None,
1104            utility_llm_service: None,
1105            egress_service: None,
1106            message_retriever: None,
1107            session_store: None,
1108            session_mutator: None,
1109            agent_store: None,
1110            connection_resolver: None,
1111            schedule_store: None,
1112            platform_store: None,
1113            leased_resource_store: None,
1114            session_resource_registry: None,
1115            event_emitter: None,
1116            event_context: None,
1117            tool_call_id: None,
1118            capability_registry: None,
1119            tool_registry: None,
1120            visible_tool_names: None,
1121            memory_store: None,
1122            org_id: None,
1123            network_access: None,
1124            locale: None,
1125            budget_checker: None,
1126            payment_authority: None,
1127        }
1128    }
1129
1130    /// Add a SQL database store to this context
1131    pub fn with_sqldb_store(mut self, sqldb_store: SessionSqlDbStoreRef) -> Self {
1132        self.sqldb_store = Some(sqldb_store);
1133        self
1134    }
1135
1136    /// Add a message retriever to this context
1137    pub fn with_message_retriever(
1138        mut self,
1139        retriever: Arc<dyn crate::message_retriever::MessageRetriever>,
1140    ) -> Self {
1141        self.message_retriever = Some(retriever);
1142        self
1143    }
1144
1145    /// Add a session store to this context.
1146    pub fn with_session_store(mut self, store: Arc<dyn SessionStore>) -> Self {
1147        self.session_store = Some(store);
1148        self
1149    }
1150
1151    /// Add a session mutator to this context.
1152    pub fn with_session_mutator(mut self, mutator: Arc<dyn SessionMutator>) -> Self {
1153        self.session_mutator = Some(mutator);
1154        self
1155    }
1156
1157    /// Add an agent store to this context.
1158    pub fn with_agent_store(mut self, store: Arc<dyn AgentStore>) -> Self {
1159        self.agent_store = Some(store);
1160        self
1161    }
1162
1163    /// Add a connection resolver to this context
1164    pub fn with_connection_resolver(mut self, resolver: Arc<dyn UserConnectionResolver>) -> Self {
1165        self.connection_resolver = Some(resolver);
1166        self
1167    }
1168
1169    /// Create a context with an image artifact store.
1170    pub fn with_image_store(
1171        session_id: SessionId,
1172        image_store: Arc<dyn ImageArtifactStore>,
1173    ) -> Self {
1174        Self {
1175            session_id,
1176            file_store: None,
1177            storage_store: None,
1178            image_store: Some(image_store),
1179            provider_credential_store: None,
1180            utility_llm_service: None,
1181            egress_service: None,
1182            sqldb_store: None,
1183            message_retriever: None,
1184            session_store: None,
1185            session_mutator: None,
1186            agent_store: None,
1187            connection_resolver: None,
1188            schedule_store: None,
1189            platform_store: None,
1190            leased_resource_store: None,
1191            session_resource_registry: None,
1192            event_emitter: None,
1193            event_context: None,
1194            tool_call_id: None,
1195            capability_registry: None,
1196            tool_registry: None,
1197            visible_tool_names: None,
1198            memory_store: None,
1199            org_id: None,
1200            network_access: None,
1201            locale: None,
1202            budget_checker: None,
1203            payment_authority: None,
1204        }
1205    }
1206
1207    /// Set the provider credential store on this context.
1208    pub fn with_provider_credential_store(
1209        mut self,
1210        store: Arc<dyn ProviderCredentialStore>,
1211    ) -> Self {
1212        self.provider_credential_store = Some(store);
1213        self
1214    }
1215
1216    /// Set the utility LLM service on this context.
1217    pub fn with_utility_llm_service(mut self, service: Arc<dyn crate::UtilityLlmService>) -> Self {
1218        self.utility_llm_service = Some(service);
1219        self
1220    }
1221
1222    /// Set the outbound egress service on this context.
1223    pub fn with_egress_service(mut self, service: Arc<dyn crate::EgressService>) -> Self {
1224        self.egress_service = Some(service);
1225        self
1226    }
1227
1228    /// Add a session schedule store to this context.
1229    pub fn with_schedule_store(mut self, store: Arc<dyn SessionScheduleStore>) -> Self {
1230        self.schedule_store = Some(store);
1231        self
1232    }
1233
1234    /// Add a platform store to this context.
1235    pub fn with_platform_store(
1236        mut self,
1237        store: Arc<dyn crate::platform_store::PlatformStore>,
1238    ) -> Self {
1239        self.platform_store = Some(store);
1240        self
1241    }
1242
1243    /// Add a leased resource store to this context.
1244    pub fn with_leased_resource_store(mut self, store: Arc<dyn LeasedResourceStore>) -> Self {
1245        self.leased_resource_store = Some(store);
1246        self
1247    }
1248
1249    /// Add a session resource registry to this context.
1250    pub fn with_session_resource_registry(
1251        mut self,
1252        registry: Arc<dyn SessionResourceRegistry>,
1253    ) -> Self {
1254        self.session_resource_registry = Some(registry);
1255        self
1256    }
1257
1258    /// Add a memory store backend for persistent cross-session memory.
1259    pub fn with_memory_store(
1260        mut self,
1261        store: Arc<dyn crate::memory_store::MemoryStoreBackend>,
1262    ) -> Self {
1263        self.memory_store = Some(store);
1264        self
1265    }
1266
1267    /// Set org ID for org-scoped operations.
1268    pub fn with_org_id(mut self, org_id: crate::typed_id::OrgId) -> Self {
1269        self.org_id = Some(org_id);
1270        self
1271    }
1272
1273    /// Set the active built-in tool registry on this context.
1274    pub fn with_tool_registry(mut self, registry: Arc<crate::tools::ToolRegistry>) -> Self {
1275        self.tool_registry = Some(registry);
1276        self
1277    }
1278
1279    /// Set the tool names visible to the model in this turn.
1280    pub fn with_visible_tool_names(mut self, names: Arc<HashSet<String>>) -> Self {
1281        self.visible_tool_names = Some(names);
1282        self
1283    }
1284
1285    /// Set the merged network access list for URL filtering.
1286    pub fn with_network_access(
1287        mut self,
1288        network_access: Option<crate::network_access::NetworkAccessList>,
1289    ) -> Self {
1290        self.network_access = network_access;
1291        self
1292    }
1293
1294    /// Set the internal payment authority for paid capability operations.
1295    pub fn with_payment_authority(mut self, authority: Arc<dyn PaymentAuthority>) -> Self {
1296        self.payment_authority = Some(authority);
1297        self
1298    }
1299
1300    /// Emit a `tool.progress` event if an event emitter and context are available.
1301    ///
1302    /// This is a best-effort helper: failures are logged but not propagated,
1303    /// so tools never fail just because a progress event couldn't be sent.
1304    pub async fn emit_progress(&self, tool_name: &str, message: &str) {
1305        let (Some(emitter), Some(ctx), Some(call_id)) =
1306            (&self.event_emitter, &self.event_context, &self.tool_call_id)
1307        else {
1308            return;
1309        };
1310        if let Err(e) = emitter
1311            .emit(EventRequest::new(
1312                self.session_id,
1313                ctx.clone(),
1314                crate::events::ToolProgressData {
1315                    tool_call_id: call_id.clone(),
1316                    tool_name: tool_name.to_string(),
1317                    message: message.to_string(),
1318                    display_name: None,
1319                },
1320            ))
1321            .await
1322        {
1323            tracing::debug!(
1324                tool_call_id = call_id,
1325                tool_name,
1326                error = %e,
1327                "Failed to emit tool.progress event"
1328            );
1329        }
1330    }
1331
1332    /// Emit a `tool.output.delta` event if an event emitter and context are available.
1333    ///
1334    /// Streams incremental output chunks (e.g., stdout/stderr lines) for live
1335    /// rendering in UI and CLI. Best-effort: failures are logged, not propagated.
1336    pub async fn emit_tool_output(&self, tool_name: &str, delta: &str, stream: &str) {
1337        let (Some(emitter), Some(ctx), Some(call_id)) =
1338            (&self.event_emitter, &self.event_context, &self.tool_call_id)
1339        else {
1340            return;
1341        };
1342        if let Err(e) = emitter
1343            .emit(EventRequest::new(
1344                self.session_id,
1345                ctx.clone(),
1346                crate::events::ToolOutputDeltaData {
1347                    tool_call_id: call_id.clone(),
1348                    tool_name: tool_name.to_string(),
1349                    delta: delta.to_string(),
1350                    stream: stream.to_string(),
1351                },
1352            ))
1353            .await
1354        {
1355            tracing::debug!(
1356                tool_call_id = call_id,
1357                tool_name,
1358                error = %e,
1359                "Failed to emit tool.output.delta event"
1360            );
1361        }
1362    }
1363}
1364
1365impl std::fmt::Debug for ToolContext {
1366    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1367        f.debug_struct("ToolContext")
1368            .field("session_id", &self.session_id)
1369            .field("file_store", &self.file_store.is_some())
1370            .field("storage_store", &self.storage_store.is_some())
1371            .field("image_store", &self.image_store.is_some())
1372            .field(
1373                "provider_credential_store",
1374                &self.provider_credential_store.is_some(),
1375            )
1376            .field("utility_llm_service", &self.utility_llm_service.is_some())
1377            .field("egress_service", &self.egress_service.is_some())
1378            .field("sqldb_store", &self.sqldb_store.is_some())
1379            .field("message_retriever", &self.message_retriever.is_some())
1380            .field("session_store", &self.session_store.is_some())
1381            .field("session_mutator", &self.session_mutator.is_some())
1382            .field("agent_store", &self.agent_store.is_some())
1383            .field("connection_resolver", &self.connection_resolver.is_some())
1384            .field("schedule_store", &self.schedule_store.is_some())
1385            .field("platform_store", &self.platform_store.is_some())
1386            .field(
1387                "leased_resource_store",
1388                &self.leased_resource_store.is_some(),
1389            )
1390            .field("event_emitter", &self.event_emitter.is_some())
1391            .field("tool_registry", &self.tool_registry.is_some())
1392            .field("memory_store", &self.memory_store.is_some())
1393            .field("payment_authority", &self.payment_authority.is_some())
1394            .field("org_id", &self.org_id)
1395            .finish()
1396    }
1397}
1398
1399// ============================================================================
1400// EventEmitter - For emitting events
1401// ============================================================================
1402
1403use crate::events::{Event, EventRequest};
1404
1405/// Trait for emitting events following the standard event protocol
1406///
1407/// Implementations can:
1408/// - Store events in a database
1409/// - Keep events in memory for testing
1410/// - Stream events via SSE/WebSocket
1411/// - Log events for debugging
1412///
1413/// Events follow a consistent schema: id, type, ts, context, data.
1414/// See specs/events.md for the full event protocol specification.
1415#[async_trait]
1416pub trait EventEmitter: Send + Sync {
1417    /// Emit an event request
1418    ///
1419    /// Takes an EventRequest (without id/sequence) and returns the stored Event
1420    /// with id and sequence assigned by the storage layer.
1421    async fn emit(&self, request: EventRequest) -> Result<Event>;
1422}
1423
1424/// Blanket impl: `Arc<E>` delegates to the inner emitter.
1425#[async_trait]
1426impl<E: EventEmitter + ?Sized> EventEmitter for Arc<E> {
1427    async fn emit(&self, request: EventRequest) -> Result<Event> {
1428        (**self).emit(request).await
1429    }
1430}
1431
1432/// No-op event emitter for when event emission is not needed
1433///
1434/// This is useful for testing or when event observability is disabled.
1435#[derive(Debug, Clone, Default)]
1436pub struct NoopEventEmitter;
1437
1438#[async_trait]
1439impl EventEmitter for NoopEventEmitter {
1440    async fn emit(&self, request: EventRequest) -> Result<Event> {
1441        // Return a dummy event with sequence 0
1442        Ok(request.into_event(crate::typed_id::EventId::new(), 0))
1443    }
1444}
1445
1446// Note: EventListener trait has been moved to event_listeners.rs module.
1447// Use `everruns_core::EventListener` or `everruns_core::event_listeners::EventListener`.
1448
1449// ============================================================================
1450// ImageResolver - For resolving image_file content to actual image data
1451// ============================================================================
1452
1453/// Resolved image data for LLM consumption
1454///
1455/// This struct contains the actual image data in a format suitable for
1456/// sending to LLM providers. Both OpenAI and Anthropic accept base64-encoded
1457/// images with media type information.
1458#[derive(Debug, Clone)]
1459pub struct ResolvedImage {
1460    /// Base64-encoded image data (without data URL prefix)
1461    pub base64: String,
1462    /// MIME type (e.g., "image/png", "image/jpeg")
1463    pub media_type: String,
1464}
1465
1466impl ResolvedImage {
1467    /// Create a new resolved image
1468    pub fn new(base64: impl Into<String>, media_type: impl Into<String>) -> Self {
1469        Self {
1470            base64: base64.into(),
1471            media_type: media_type.into(),
1472        }
1473    }
1474
1475    /// Convert to a data URL suitable for OpenAI Vision API
1476    ///
1477    /// Format: `data:{media_type};base64,{base64_data}`
1478    pub fn to_data_url(&self) -> String {
1479        format!("data:{};base64,{}", self.media_type, self.base64)
1480    }
1481}
1482
1483/// Trait for resolving image_file content parts to actual image data
1484///
1485/// When building LLM messages, `image_file` content parts contain only
1486/// a reference (UUID) to an uploaded image. This trait allows resolving
1487/// those references to actual image data.
1488///
1489/// # Provider-specific formatting
1490///
1491/// The resolved image data is then converted to provider-specific formats:
1492///
1493/// **OpenAI Vision:**
1494/// ```json
1495/// {
1496///   "type": "image_url",
1497///   "image_url": { "url": "data:image/png;base64,..." }
1498/// }
1499/// ```
1500///
1501/// **Anthropic Vision:**
1502/// ```json
1503/// {
1504///   "type": "image",
1505///   "source": { "type": "base64", "media_type": "image/png", "data": "..." }
1506/// }
1507/// ```
1508///
1509/// # Implementation notes
1510///
1511/// Implementations should:
1512/// - Fetch image data from storage (database, S3, etc.)
1513/// - Return base64-encoded data with media type
1514/// - Handle missing images gracefully (return None)
1515#[async_trait]
1516pub trait ImageResolver: Send + Sync {
1517    /// Resolve an image_file reference to actual image data
1518    ///
1519    /// Returns `None` if the image is not found.
1520    async fn resolve_image(&self, image_id: Uuid) -> Result<Option<ResolvedImage>>;
1521}
1522
1523// ============================================================================
1524// Tests
1525// ============================================================================
1526
1527#[cfg(test)]
1528mod tests {
1529    use super::*;
1530
1531    #[test]
1532    fn test_resolved_image_new() {
1533        let image = ResolvedImage::new("SGVsbG8=", "image/png");
1534        assert_eq!(image.base64, "SGVsbG8=");
1535        assert_eq!(image.media_type, "image/png");
1536    }
1537
1538    #[test]
1539    fn test_resolved_image_to_data_url() {
1540        let image = ResolvedImage::new("SGVsbG8=", "image/png");
1541        let data_url = image.to_data_url();
1542        assert_eq!(data_url, "data:image/png;base64,SGVsbG8=");
1543    }
1544
1545    #[test]
1546    fn test_resolved_image_jpeg() {
1547        let image = ResolvedImage::new("base64data", "image/jpeg");
1548        let data_url = image.to_data_url();
1549        assert!(data_url.starts_with("data:image/jpeg;base64,"));
1550    }
1551}