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// ============================================================================
878// DurableToolResultStore — per-tool-call idempotency (EVE-530)
879// ============================================================================
880
881/// Result of a claim attempt on the per-tool-call idempotency store.
882#[derive(Debug)]
883pub enum ToolCallClaimResult {
884    /// First claim for this (turn_id, tool_call_id); caller should execute the tool.
885    /// `claim_token` must be passed to `settle_tool_call` to verify ownership.
886    Claimed { claim_token: uuid::Uuid },
887    /// A prior run already settled this call; replay the stored result.
888    AlreadySettled {
889        result_json: serde_json::Value,
890        args_fingerprint: String,
891    },
892    /// A prior run started but never settled. For `AtMostOnce` tools the
893    /// caller should NOT re-execute; for `Pure`/`Idempotent` tools the caller
894    /// may re-execute and then try to settle (the settle CAS will be a no-op if
895    /// a different claimer wins first).
896    AlreadyRunning { args_fingerprint: String },
897    /// A settled row exists but its `args_fingerprint` does not match the
898    /// current call — this is a determinism violation (workflow replay with
899    /// different inputs). The workflow should be failed loudly.
900    DeterminismViolation {
901        stored_fingerprint: String,
902        current_fingerprint: String,
903    },
904}
905
906/// Durable per-tool-call idempotency store (EVE-530).
907///
908/// Implements the claim/settle CAS that prevents double-execution of
909/// `AtMostOnce` tools on worker reclaim/replay.
910#[async_trait]
911pub trait DurableToolResultStore: Send + Sync + 'static {
912    /// Atomically claim `(turn_id, tool_call_id)` before tool dispatch.
913    ///
914    /// - Inserts a `running` row if none exists → `Claimed`.
915    /// - Finds an existing `settled` row → `AlreadySettled`.
916    /// - Finds an existing `running` row → `AlreadyRunning`.
917    /// - Finds a `settled` row with a mismatched `args_fingerprint`
918    ///   (determinism violation) → `DeterminismViolation`.
919    async fn try_claim_tool_call(
920        &self,
921        turn_id: &str,
922        tool_call_id: &str,
923        tool_name: &str,
924        args_fingerprint: &str,
925    ) -> Result<ToolCallClaimResult>;
926
927    /// Settle a previously claimed tool call with its result.
928    ///
929    /// `claim_token` must match the token returned by `try_claim_tool_call`.
930    /// Returns `Ok(true)` if the row was updated, `Ok(false)` if the claim
931    /// token no longer matches (ownership lost — treat as a warning).
932    async fn settle_tool_call(
933        &self,
934        turn_id: &str,
935        tool_call_id: &str,
936        result_json: serde_json::Value,
937        status: &str,
938        claim_token: uuid::Uuid,
939    ) -> Result<bool>;
940}
941
942/// No-op implementation — used when no durable store is configured (dev/test).
943/// Every call is treated as a fresh first execution; no replay or ownership checks.
944pub struct NoopDurableToolResultStore;
945
946#[async_trait]
947impl DurableToolResultStore for NoopDurableToolResultStore {
948    async fn try_claim_tool_call(
949        &self,
950        _turn_id: &str,
951        _tool_call_id: &str,
952        _tool_name: &str,
953        _args_fingerprint: &str,
954    ) -> Result<ToolCallClaimResult> {
955        Ok(ToolCallClaimResult::Claimed {
956            claim_token: uuid::Uuid::new_v4(),
957        })
958    }
959
960    async fn settle_tool_call(
961        &self,
962        _turn_id: &str,
963        _tool_call_id: &str,
964        _result_json: serde_json::Value,
965        _status: &str,
966        _claim_token: uuid::Uuid,
967    ) -> Result<bool> {
968        Ok(true)
969    }
970}
971
972// ============================================================================
973// StreamHeartbeater — per-stream liveness signal for Reason activity (EVE-531)
974// ============================================================================
975
976/// Progress snapshot carried in each stream heartbeat.
977#[derive(Debug, Clone)]
978pub struct StreamProgress {
979    /// Accumulated text + thinking length (characters) at the time of heartbeat.
980    pub accumulated_len: usize,
981    /// Wall-clock time of the most recent received token (Unix seconds).
982    pub last_delta_at: u64,
983}
984
985/// Heartbeater the Reason streaming loop calls on delta batches and a keepalive
986/// timer, signalling that the provider connection is alive.
987///
988/// Implementations bridge to the durable-execution layer (e.g. gRPC).
989/// The no-op is used in dev/test where no durable store is present.
990#[async_trait]
991pub trait StreamHeartbeater: Send + Sync {
992    /// Signal stream liveness with current progress.
993    ///
994    /// Must be best-effort: errors must not propagate to the caller.
995    /// Cancel-safety is critical — if the worker dies the heartbeat stops
996    /// and the existing task-level reclaim takes over.
997    async fn heartbeat(&self, progress: StreamProgress);
998}
999
1000/// No-op heartbeater — treats every stream as perpetually alive (dev/test).
1001pub struct NoopStreamHeartbeater;
1002
1003#[async_trait]
1004impl StreamHeartbeater for NoopStreamHeartbeater {
1005    async fn heartbeat(&self, _progress: StreamProgress) {}
1006}
1007
1008/// Runtime context provided to tools during execution.
1009///
1010/// This context contains:
1011/// - Session ID for scoping operations
1012/// - Optional stores for tools that need external access
1013///
1014/// Tools that need context-aware execution (like filesystem tools) can use
1015/// the `execute_with_context` method on the Tool trait.
1016#[derive(Clone)]
1017pub struct ToolContext {
1018    /// The session ID for the current execution
1019    pub session_id: SessionId,
1020
1021    /// Optional file store for filesystem operations
1022    pub file_store: Option<Arc<dyn SessionFileSystem>>,
1023
1024    /// Optional storage store for key/value and secret storage
1025    pub storage_store: Option<Arc<dyn SessionStorageStore>>,
1026
1027    /// Optional durable image artifact store for tool-side media persistence.
1028    pub image_store: Option<Arc<dyn ImageArtifactStore>>,
1029
1030    /// Optional provider credential store for tool-side API clients.
1031    pub provider_credential_store: Option<Arc<dyn ProviderCredentialStore>>,
1032
1033    /// Optional system utility LLM service for capability internals.
1034    pub utility_llm_service: Option<Arc<dyn crate::UtilityLlmService>>,
1035
1036    /// Optional outbound egress service for HTTP/API traffic.
1037    pub egress_service: Option<Arc<dyn crate::EgressService>>,
1038
1039    /// Optional session SQL database store
1040    pub sqldb_store: Option<SessionSqlDbStoreRef>,
1041
1042    /// Optional message retriever for tools that need conversation history access
1043    pub message_retriever: Option<Arc<dyn crate::message_retriever::MessageRetriever>>,
1044
1045    /// Optional session store for tools that need session metadata access.
1046    pub session_store: Option<Arc<dyn SessionStore>>,
1047
1048    /// Optional session mutator for tools that need to update session metadata.
1049    pub session_mutator: Option<Arc<dyn SessionMutator>>,
1050
1051    /// Optional agent store for tools that need agent metadata access.
1052    pub agent_store: Option<Arc<dyn AgentStore>>,
1053
1054    /// Optional resolver for user connection tokens (lazy GitHub token lookup, etc.)
1055    pub connection_resolver: Option<Arc<dyn UserConnectionResolver>>,
1056
1057    /// Optional session schedule store for scheduling tools.
1058    pub schedule_store: Option<Arc<dyn SessionScheduleStore>>,
1059
1060    /// Optional platform store for org-level management tools.
1061    pub platform_store: Option<Arc<dyn crate::platform_store::PlatformStore>>,
1062    /// Optional leased resource store for lifecycle-managed provider resources.
1063    pub leased_resource_store: Option<Arc<dyn LeasedResourceStore>>,
1064
1065    /// Optional session resource registry — generic registry of active resources.
1066    pub session_resource_registry: Option<Arc<dyn SessionResourceRegistry>>,
1067
1068    /// Optional event emitter for tools that need to stream progress updates.
1069    /// When set, tools can emit `tool.progress` events during execution.
1070    pub event_emitter: Option<Arc<dyn EventEmitter>>,
1071
1072    /// Event context for correlating progress events with the current tool call.
1073    /// Set by ActAtom when constructing the ToolContext.
1074    pub event_context: Option<crate::events::EventContext>,
1075
1076    /// The tool call ID for the current execution (set by ActAtom).
1077    /// Used by tools to emit correlated progress events.
1078    pub tool_call_id: Option<String>,
1079    /// Optional capability registry for blueprint lookups.
1080    pub capability_registry: Option<crate::capabilities::CapabilityRegistry>,
1081
1082    /// Optional registry of active built-in tools for meta-tools such as
1083    /// `spawn_background` that need to inspect or delegate to sibling tools.
1084    pub tool_registry: Option<Arc<crate::tools::ToolRegistry>>,
1085
1086    /// Optional allowlist of tools visible to the model for this turn.
1087    /// Registry-introspecting tools must filter through this before returning
1088    /// sibling tool metadata, because the execution registry can be a superset.
1089    pub visible_tool_names: Option<Arc<HashSet<String>>>,
1090
1091    /// Optional memory store backend for persistent cross-session memory.
1092    pub memory_store: Option<Arc<dyn crate::memory_store::MemoryStoreBackend>>,
1093
1094    /// Optional org ID for org-scoped operations (memory stores, etc.).
1095    pub org_id: Option<crate::typed_id::OrgId>,
1096
1097    /// Merged network access list (harness ∩ agent ∩ session).
1098    /// When set, tools that make HTTP requests must check URLs against this list.
1099    pub network_access: Option<crate::network_access::NetworkAccessList>,
1100
1101    /// Resolved locale for localized tool behavior (BCP 47, e.g. `uk-UA`).
1102    /// When set, tools that support localization use this to produce
1103    /// locale-appropriate descriptions, error messages, and prompts.
1104    pub locale: Option<String>,
1105
1106    /// Optional budget checker for the check_budget tool.
1107    pub budget_checker: Option<Arc<dyn BudgetChecker>>,
1108
1109    /// Optional internal payment authority for paid capability tools.
1110    pub payment_authority: Option<Arc<dyn PaymentAuthority>>,
1111}
1112
1113impl ToolContext {
1114    /// Create a new tool context with just a session ID
1115    pub fn new(session_id: SessionId) -> Self {
1116        Self {
1117            session_id,
1118            file_store: None,
1119            storage_store: None,
1120            image_store: None,
1121            provider_credential_store: None,
1122            utility_llm_service: None,
1123            egress_service: None,
1124            sqldb_store: None,
1125            message_retriever: None,
1126            session_store: None,
1127            session_mutator: None,
1128            agent_store: None,
1129            connection_resolver: None,
1130            schedule_store: None,
1131            platform_store: None,
1132            leased_resource_store: None,
1133            session_resource_registry: None,
1134            event_emitter: None,
1135            event_context: None,
1136            tool_call_id: None,
1137            capability_registry: None,
1138            tool_registry: None,
1139            visible_tool_names: None,
1140            memory_store: None,
1141            org_id: None,
1142            network_access: None,
1143            locale: None,
1144            budget_checker: None,
1145            payment_authority: None,
1146        }
1147    }
1148
1149    /// Create a context with a file store
1150    pub fn with_file_store(session_id: SessionId, file_store: Arc<dyn SessionFileSystem>) -> Self {
1151        Self {
1152            session_id,
1153            file_store: Some(file_store),
1154            storage_store: None,
1155            image_store: None,
1156            provider_credential_store: None,
1157            utility_llm_service: None,
1158            egress_service: None,
1159            sqldb_store: None,
1160            message_retriever: None,
1161            session_store: None,
1162            session_mutator: None,
1163            agent_store: None,
1164            connection_resolver: None,
1165            schedule_store: None,
1166            platform_store: None,
1167            leased_resource_store: None,
1168            session_resource_registry: None,
1169            event_emitter: None,
1170            event_context: None,
1171            tool_call_id: None,
1172            capability_registry: None,
1173            tool_registry: None,
1174            visible_tool_names: None,
1175            memory_store: None,
1176            org_id: None,
1177            network_access: None,
1178            locale: None,
1179            budget_checker: None,
1180            payment_authority: None,
1181        }
1182    }
1183
1184    /// Create a context with a storage store
1185    pub fn with_storage_store(
1186        session_id: SessionId,
1187        storage_store: Arc<dyn SessionStorageStore>,
1188    ) -> Self {
1189        Self {
1190            session_id,
1191            file_store: None,
1192            storage_store: Some(storage_store),
1193            image_store: None,
1194            provider_credential_store: None,
1195            utility_llm_service: None,
1196            egress_service: None,
1197            sqldb_store: None,
1198            message_retriever: None,
1199            session_store: None,
1200            session_mutator: None,
1201            agent_store: None,
1202            connection_resolver: None,
1203            schedule_store: None,
1204            platform_store: None,
1205            leased_resource_store: None,
1206            session_resource_registry: None,
1207            event_emitter: None,
1208            event_context: None,
1209            tool_call_id: None,
1210            capability_registry: None,
1211            tool_registry: None,
1212            visible_tool_names: None,
1213            memory_store: None,
1214            org_id: None,
1215            network_access: None,
1216            locale: None,
1217            budget_checker: None,
1218            payment_authority: None,
1219        }
1220    }
1221
1222    /// Create a context with both file store and storage store
1223    pub fn with_stores(
1224        session_id: SessionId,
1225        file_store: Arc<dyn SessionFileSystem>,
1226        storage_store: Arc<dyn SessionStorageStore>,
1227    ) -> Self {
1228        Self {
1229            session_id,
1230            file_store: Some(file_store),
1231            storage_store: Some(storage_store),
1232            sqldb_store: None,
1233            image_store: None,
1234            provider_credential_store: None,
1235            utility_llm_service: None,
1236            egress_service: None,
1237            message_retriever: None,
1238            session_store: None,
1239            session_mutator: None,
1240            agent_store: None,
1241            connection_resolver: None,
1242            schedule_store: None,
1243            platform_store: None,
1244            leased_resource_store: None,
1245            session_resource_registry: None,
1246            event_emitter: None,
1247            event_context: None,
1248            tool_call_id: None,
1249            capability_registry: None,
1250            tool_registry: None,
1251            visible_tool_names: None,
1252            memory_store: None,
1253            org_id: None,
1254            network_access: None,
1255            locale: None,
1256            budget_checker: None,
1257            payment_authority: None,
1258        }
1259    }
1260
1261    /// Add a SQL database store to this context
1262    pub fn with_sqldb_store(mut self, sqldb_store: SessionSqlDbStoreRef) -> Self {
1263        self.sqldb_store = Some(sqldb_store);
1264        self
1265    }
1266
1267    /// Add a message retriever to this context
1268    pub fn with_message_retriever(
1269        mut self,
1270        retriever: Arc<dyn crate::message_retriever::MessageRetriever>,
1271    ) -> Self {
1272        self.message_retriever = Some(retriever);
1273        self
1274    }
1275
1276    /// Add a session store to this context.
1277    pub fn with_session_store(mut self, store: Arc<dyn SessionStore>) -> Self {
1278        self.session_store = Some(store);
1279        self
1280    }
1281
1282    /// Add a session mutator to this context.
1283    pub fn with_session_mutator(mut self, mutator: Arc<dyn SessionMutator>) -> Self {
1284        self.session_mutator = Some(mutator);
1285        self
1286    }
1287
1288    /// Add an agent store to this context.
1289    pub fn with_agent_store(mut self, store: Arc<dyn AgentStore>) -> Self {
1290        self.agent_store = Some(store);
1291        self
1292    }
1293
1294    /// Add a connection resolver to this context
1295    pub fn with_connection_resolver(mut self, resolver: Arc<dyn UserConnectionResolver>) -> Self {
1296        self.connection_resolver = Some(resolver);
1297        self
1298    }
1299
1300    /// Create a context with an image artifact store.
1301    pub fn with_image_store(
1302        session_id: SessionId,
1303        image_store: Arc<dyn ImageArtifactStore>,
1304    ) -> Self {
1305        Self {
1306            session_id,
1307            file_store: None,
1308            storage_store: None,
1309            image_store: Some(image_store),
1310            provider_credential_store: None,
1311            utility_llm_service: None,
1312            egress_service: None,
1313            sqldb_store: None,
1314            message_retriever: None,
1315            session_store: None,
1316            session_mutator: None,
1317            agent_store: None,
1318            connection_resolver: None,
1319            schedule_store: None,
1320            platform_store: None,
1321            leased_resource_store: None,
1322            session_resource_registry: None,
1323            event_emitter: None,
1324            event_context: None,
1325            tool_call_id: None,
1326            capability_registry: None,
1327            tool_registry: None,
1328            visible_tool_names: None,
1329            memory_store: None,
1330            org_id: None,
1331            network_access: None,
1332            locale: None,
1333            budget_checker: None,
1334            payment_authority: None,
1335        }
1336    }
1337
1338    /// Set the provider credential store on this context.
1339    pub fn with_provider_credential_store(
1340        mut self,
1341        store: Arc<dyn ProviderCredentialStore>,
1342    ) -> Self {
1343        self.provider_credential_store = Some(store);
1344        self
1345    }
1346
1347    /// Set the utility LLM service on this context.
1348    pub fn with_utility_llm_service(mut self, service: Arc<dyn crate::UtilityLlmService>) -> Self {
1349        self.utility_llm_service = Some(service);
1350        self
1351    }
1352
1353    /// Set the outbound egress service on this context.
1354    pub fn with_egress_service(mut self, service: Arc<dyn crate::EgressService>) -> Self {
1355        self.egress_service = Some(service);
1356        self
1357    }
1358
1359    /// Add a session schedule store to this context.
1360    pub fn with_schedule_store(mut self, store: Arc<dyn SessionScheduleStore>) -> Self {
1361        self.schedule_store = Some(store);
1362        self
1363    }
1364
1365    /// Add a platform store to this context.
1366    pub fn with_platform_store(
1367        mut self,
1368        store: Arc<dyn crate::platform_store::PlatformStore>,
1369    ) -> Self {
1370        self.platform_store = Some(store);
1371        self
1372    }
1373
1374    /// Add a leased resource store to this context.
1375    pub fn with_leased_resource_store(mut self, store: Arc<dyn LeasedResourceStore>) -> Self {
1376        self.leased_resource_store = Some(store);
1377        self
1378    }
1379
1380    /// Add a session resource registry to this context.
1381    pub fn with_session_resource_registry(
1382        mut self,
1383        registry: Arc<dyn SessionResourceRegistry>,
1384    ) -> Self {
1385        self.session_resource_registry = Some(registry);
1386        self
1387    }
1388
1389    /// Add a memory store backend for persistent cross-session memory.
1390    pub fn with_memory_store(
1391        mut self,
1392        store: Arc<dyn crate::memory_store::MemoryStoreBackend>,
1393    ) -> Self {
1394        self.memory_store = Some(store);
1395        self
1396    }
1397
1398    /// Set org ID for org-scoped operations.
1399    pub fn with_org_id(mut self, org_id: crate::typed_id::OrgId) -> Self {
1400        self.org_id = Some(org_id);
1401        self
1402    }
1403
1404    /// Set the active built-in tool registry on this context.
1405    pub fn with_tool_registry(mut self, registry: Arc<crate::tools::ToolRegistry>) -> Self {
1406        self.tool_registry = Some(registry);
1407        self
1408    }
1409
1410    /// Set the tool names visible to the model in this turn.
1411    pub fn with_visible_tool_names(mut self, names: Arc<HashSet<String>>) -> Self {
1412        self.visible_tool_names = Some(names);
1413        self
1414    }
1415
1416    /// Set the merged network access list for URL filtering.
1417    pub fn with_network_access(
1418        mut self,
1419        network_access: Option<crate::network_access::NetworkAccessList>,
1420    ) -> Self {
1421        self.network_access = network_access;
1422        self
1423    }
1424
1425    /// Set the internal payment authority for paid capability operations.
1426    pub fn with_payment_authority(mut self, authority: Arc<dyn PaymentAuthority>) -> Self {
1427        self.payment_authority = Some(authority);
1428        self
1429    }
1430
1431    /// Emit a `tool.progress` event if an event emitter and context are available.
1432    ///
1433    /// This is a best-effort helper: failures are logged but not propagated,
1434    /// so tools never fail just because a progress event couldn't be sent.
1435    pub async fn emit_progress(&self, tool_name: &str, message: &str) {
1436        let (Some(emitter), Some(ctx), Some(call_id)) =
1437            (&self.event_emitter, &self.event_context, &self.tool_call_id)
1438        else {
1439            return;
1440        };
1441        if let Err(e) = emitter
1442            .emit(EventRequest::new(
1443                self.session_id,
1444                ctx.clone(),
1445                crate::events::ToolProgressData {
1446                    tool_call_id: call_id.clone(),
1447                    tool_name: tool_name.to_string(),
1448                    message: message.to_string(),
1449                    display_name: None,
1450                },
1451            ))
1452            .await
1453        {
1454            tracing::debug!(
1455                tool_call_id = call_id,
1456                tool_name,
1457                error = %e,
1458                "Failed to emit tool.progress event"
1459            );
1460        }
1461    }
1462
1463    /// Emit a `tool.output.delta` event if an event emitter and context are available.
1464    ///
1465    /// Streams incremental output chunks (e.g., stdout/stderr lines) for live
1466    /// rendering in UI and CLI. Best-effort: failures are logged, not propagated.
1467    pub async fn emit_tool_output(&self, tool_name: &str, delta: &str, stream: &str) {
1468        let (Some(emitter), Some(ctx), Some(call_id)) =
1469            (&self.event_emitter, &self.event_context, &self.tool_call_id)
1470        else {
1471            return;
1472        };
1473        if let Err(e) = emitter
1474            .emit(EventRequest::new(
1475                self.session_id,
1476                ctx.clone(),
1477                crate::events::ToolOutputDeltaData {
1478                    tool_call_id: call_id.clone(),
1479                    tool_name: tool_name.to_string(),
1480                    delta: delta.to_string(),
1481                    stream: stream.to_string(),
1482                },
1483            ))
1484            .await
1485        {
1486            tracing::debug!(
1487                tool_call_id = call_id,
1488                tool_name,
1489                error = %e,
1490                "Failed to emit tool.output.delta event"
1491            );
1492        }
1493    }
1494}
1495
1496impl std::fmt::Debug for ToolContext {
1497    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1498        f.debug_struct("ToolContext")
1499            .field("session_id", &self.session_id)
1500            .field("file_store", &self.file_store.is_some())
1501            .field("storage_store", &self.storage_store.is_some())
1502            .field("image_store", &self.image_store.is_some())
1503            .field(
1504                "provider_credential_store",
1505                &self.provider_credential_store.is_some(),
1506            )
1507            .field("utility_llm_service", &self.utility_llm_service.is_some())
1508            .field("egress_service", &self.egress_service.is_some())
1509            .field("sqldb_store", &self.sqldb_store.is_some())
1510            .field("message_retriever", &self.message_retriever.is_some())
1511            .field("session_store", &self.session_store.is_some())
1512            .field("session_mutator", &self.session_mutator.is_some())
1513            .field("agent_store", &self.agent_store.is_some())
1514            .field("connection_resolver", &self.connection_resolver.is_some())
1515            .field("schedule_store", &self.schedule_store.is_some())
1516            .field("platform_store", &self.platform_store.is_some())
1517            .field(
1518                "leased_resource_store",
1519                &self.leased_resource_store.is_some(),
1520            )
1521            .field("event_emitter", &self.event_emitter.is_some())
1522            .field("tool_registry", &self.tool_registry.is_some())
1523            .field("memory_store", &self.memory_store.is_some())
1524            .field("payment_authority", &self.payment_authority.is_some())
1525            .field("org_id", &self.org_id)
1526            .finish()
1527    }
1528}
1529
1530// ============================================================================
1531// EventEmitter - For emitting events
1532// ============================================================================
1533
1534use crate::events::{Event, EventRequest};
1535
1536/// Trait for emitting events following the standard event protocol
1537///
1538/// Implementations can:
1539/// - Store events in a database
1540/// - Keep events in memory for testing
1541/// - Stream events via SSE/WebSocket
1542/// - Log events for debugging
1543///
1544/// Events follow a consistent schema: id, type, ts, context, data.
1545/// See specs/events.md for the full event protocol specification.
1546#[async_trait]
1547pub trait EventEmitter: Send + Sync {
1548    /// Emit an event request
1549    ///
1550    /// Takes an EventRequest (without id/sequence) and returns the stored Event
1551    /// with id and sequence assigned by the storage layer.
1552    async fn emit(&self, request: EventRequest) -> Result<Event>;
1553}
1554
1555/// Blanket impl: `Arc<E>` delegates to the inner emitter.
1556#[async_trait]
1557impl<E: EventEmitter + ?Sized> EventEmitter for Arc<E> {
1558    async fn emit(&self, request: EventRequest) -> Result<Event> {
1559        (**self).emit(request).await
1560    }
1561}
1562
1563/// No-op event emitter for when event emission is not needed
1564///
1565/// This is useful for testing or when event observability is disabled.
1566#[derive(Debug, Clone, Default)]
1567pub struct NoopEventEmitter;
1568
1569#[async_trait]
1570impl EventEmitter for NoopEventEmitter {
1571    async fn emit(&self, request: EventRequest) -> Result<Event> {
1572        // Return a dummy event with sequence 0
1573        Ok(request.into_event(crate::typed_id::EventId::new(), 0))
1574    }
1575}
1576
1577// Note: EventListener trait has been moved to event_listeners.rs module.
1578// Use `everruns_core::EventListener` or `everruns_core::event_listeners::EventListener`.
1579
1580// ============================================================================
1581// ImageResolver - For resolving image_file content to actual image data
1582// ============================================================================
1583
1584/// Resolved image data for LLM consumption
1585///
1586/// This struct contains the actual image data in a format suitable for
1587/// sending to LLM providers. Both OpenAI and Anthropic accept base64-encoded
1588/// images with media type information.
1589#[derive(Debug, Clone)]
1590pub struct ResolvedImage {
1591    /// Base64-encoded image data (without data URL prefix)
1592    pub base64: String,
1593    /// MIME type (e.g., "image/png", "image/jpeg")
1594    pub media_type: String,
1595}
1596
1597impl ResolvedImage {
1598    /// Create a new resolved image
1599    pub fn new(base64: impl Into<String>, media_type: impl Into<String>) -> Self {
1600        Self {
1601            base64: base64.into(),
1602            media_type: media_type.into(),
1603        }
1604    }
1605
1606    /// Convert to a data URL suitable for OpenAI Vision API
1607    ///
1608    /// Format: `data:{media_type};base64,{base64_data}`
1609    pub fn to_data_url(&self) -> String {
1610        format!("data:{};base64,{}", self.media_type, self.base64)
1611    }
1612}
1613
1614/// Trait for resolving image_file content parts to actual image data
1615///
1616/// When building LLM messages, `image_file` content parts contain only
1617/// a reference (UUID) to an uploaded image. This trait allows resolving
1618/// those references to actual image data.
1619///
1620/// # Provider-specific formatting
1621///
1622/// The resolved image data is then converted to provider-specific formats:
1623///
1624/// **OpenAI Vision:**
1625/// ```json
1626/// {
1627///   "type": "image_url",
1628///   "image_url": { "url": "data:image/png;base64,..." }
1629/// }
1630/// ```
1631///
1632/// **Anthropic Vision:**
1633/// ```json
1634/// {
1635///   "type": "image",
1636///   "source": { "type": "base64", "media_type": "image/png", "data": "..." }
1637/// }
1638/// ```
1639///
1640/// # Implementation notes
1641///
1642/// Implementations should:
1643/// - Fetch image data from storage (database, S3, etc.)
1644/// - Return base64-encoded data with media type
1645/// - Handle missing images gracefully (return None)
1646#[async_trait]
1647pub trait ImageResolver: Send + Sync {
1648    /// Resolve an image_file reference to actual image data
1649    ///
1650    /// Returns `None` if the image is not found.
1651    async fn resolve_image(&self, image_id: Uuid) -> Result<Option<ResolvedImage>>;
1652}
1653
1654// ============================================================================
1655// Tests
1656// ============================================================================
1657
1658#[cfg(test)]
1659mod tests {
1660    use super::*;
1661
1662    #[test]
1663    fn test_resolved_image_new() {
1664        let image = ResolvedImage::new("SGVsbG8=", "image/png");
1665        assert_eq!(image.base64, "SGVsbG8=");
1666        assert_eq!(image.media_type, "image/png");
1667    }
1668
1669    #[test]
1670    fn test_resolved_image_to_data_url() {
1671        let image = ResolvedImage::new("SGVsbG8=", "image/png");
1672        let data_url = image.to_data_url();
1673        assert_eq!(data_url, "data:image/png;base64,SGVsbG8=");
1674    }
1675
1676    #[test]
1677    fn test_resolved_image_jpeg() {
1678        let image = ResolvedImage::new("base64data", "image/jpeg");
1679        let data_url = image.to_data_url();
1680        assert!(data_url.starts_with("data:image/jpeg;base64,"));
1681    }
1682}