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/// Read-only status of a tool call in durable storage (EVE-533).
907#[derive(Debug, Clone)]
908pub enum DurableToolCallStatus {
909    /// Tool completed successfully or with an error; result is stored.
910    Settled { result_json: serde_json::Value },
911    /// Tool was settled with `interrupted` status; result may contain error details.
912    Interrupted {
913        result_json: Option<serde_json::Value>,
914    },
915    /// A claim exists but the tool never finished.
916    Running,
917}
918
919/// Durable per-tool-call idempotency store (EVE-530).
920///
921/// Implements the claim/settle CAS that prevents double-execution of
922/// `AtMostOnce` tools on worker reclaim/replay.
923#[async_trait]
924pub trait DurableToolResultStore: Send + Sync + 'static {
925    /// Atomically claim `(turn_id, tool_call_id)` before tool dispatch.
926    ///
927    /// - Inserts a `running` row if none exists → `Claimed`.
928    /// - Finds an existing `settled` row → `AlreadySettled`.
929    /// - Finds an existing `running` row → `AlreadyRunning`.
930    /// - Finds a `settled` row with a mismatched `args_fingerprint`
931    ///   (determinism violation) → `DeterminismViolation`.
932    async fn try_claim_tool_call(
933        &self,
934        turn_id: &str,
935        tool_call_id: &str,
936        tool_name: &str,
937        args_fingerprint: &str,
938    ) -> Result<ToolCallClaimResult>;
939
940    /// Settle a previously claimed tool call with its result.
941    ///
942    /// `claim_token` must match the token returned by `try_claim_tool_call`.
943    /// Returns `Ok(true)` if the row was updated, `Ok(false)` if the claim
944    /// token no longer matches (ownership lost — treat as a warning).
945    async fn settle_tool_call(
946        &self,
947        turn_id: &str,
948        tool_call_id: &str,
949        result_json: serde_json::Value,
950        status: &str,
951        claim_token: uuid::Uuid,
952    ) -> Result<bool>;
953
954    /// Read-only lookup of a tool call's current status in durable storage (EVE-533).
955    ///
956    /// Used by transcript repair to decide whether to replay a stored result or
957    /// synthesize an interrupted placeholder. Returns `None` if no row exists.
958    async fn get_tool_call_status(
959        &self,
960        turn_id: &str,
961        tool_call_id: &str,
962    ) -> Result<Option<DurableToolCallStatus>>;
963}
964
965/// No-op implementation — used when no durable store is configured (dev/test).
966/// Every call is treated as a fresh first execution; no replay or ownership checks.
967pub struct NoopDurableToolResultStore;
968
969#[async_trait]
970impl DurableToolResultStore for NoopDurableToolResultStore {
971    async fn try_claim_tool_call(
972        &self,
973        _turn_id: &str,
974        _tool_call_id: &str,
975        _tool_name: &str,
976        _args_fingerprint: &str,
977    ) -> Result<ToolCallClaimResult> {
978        Ok(ToolCallClaimResult::Claimed {
979            claim_token: uuid::Uuid::new_v4(),
980        })
981    }
982
983    async fn settle_tool_call(
984        &self,
985        _turn_id: &str,
986        _tool_call_id: &str,
987        _result_json: serde_json::Value,
988        _status: &str,
989        _claim_token: uuid::Uuid,
990    ) -> Result<bool> {
991        Ok(true)
992    }
993
994    async fn get_tool_call_status(
995        &self,
996        _turn_id: &str,
997        _tool_call_id: &str,
998    ) -> Result<Option<DurableToolCallStatus>> {
999        Ok(None)
1000    }
1001}
1002
1003// ============================================================================
1004// StreamHeartbeater — per-stream liveness signal for Reason activity (EVE-531)
1005// ============================================================================
1006
1007/// Progress snapshot carried in each stream heartbeat.
1008#[derive(Debug, Clone)]
1009pub struct StreamProgress {
1010    /// Accumulated text + thinking length (characters) at the time of heartbeat.
1011    pub accumulated_len: usize,
1012    /// Wall-clock time of the most recent received token (Unix seconds).
1013    pub last_delta_at: u64,
1014}
1015
1016/// Heartbeater the Reason streaming loop calls on delta batches and a keepalive
1017/// timer, signalling that the provider connection is alive.
1018///
1019/// Implementations bridge to the durable-execution layer (e.g. gRPC).
1020/// The no-op is used in dev/test where no durable store is present.
1021#[async_trait]
1022pub trait StreamHeartbeater: Send + Sync {
1023    /// Signal stream liveness with current progress.
1024    ///
1025    /// Must be best-effort: errors must not propagate to the caller.
1026    /// Cancel-safety is critical — if the worker dies the heartbeat stops
1027    /// and the existing task-level reclaim takes over.
1028    async fn heartbeat(&self, progress: StreamProgress);
1029}
1030
1031/// No-op heartbeater — treats every stream as perpetually alive (dev/test).
1032pub struct NoopStreamHeartbeater;
1033
1034#[async_trait]
1035impl StreamHeartbeater for NoopStreamHeartbeater {
1036    async fn heartbeat(&self, _progress: StreamProgress) {}
1037}
1038
1039// ============================================================================
1040// PartialStreamStore — partial-stream recovery for Reason activity (EVE-532)
1041// ============================================================================
1042
1043/// State of a partially-streamed assistant message detected in the event log.
1044#[derive(Debug, Clone)]
1045pub struct PartialStreamState {
1046    /// Accumulated text from the last `output.message.delta` for the turn.
1047    /// Empty when `output.message.started` was emitted but no delta arrived.
1048    pub accumulated: String,
1049}
1050
1051/// Consults the persisted event log to detect whether a `reason` activity
1052/// was interrupted after `output.message.started` but before
1053/// `output.message.completed` or `output.message.replaced`.
1054///
1055/// Used by `ReasonAtom` on re-entry to apply the ContinuePartial recovery
1056/// policy (EVE-532): finalize the partial text without a second provider call,
1057/// or restart clean if the partial is unusable.
1058#[async_trait]
1059pub trait PartialStreamStore: Send + Sync {
1060    /// Return the partial-stream state for `(session_id, turn_id)` if an
1061    /// in-flight assistant message exists (started but not completed).
1062    async fn get_partial_stream(
1063        &self,
1064        session_id: SessionId,
1065        turn_id: &str,
1066    ) -> Result<Option<PartialStreamState>>;
1067}
1068
1069/// No-op — always reports no partial stream (dev/test / in-memory mode).
1070pub struct NoopPartialStreamStore;
1071
1072#[async_trait]
1073impl PartialStreamStore for NoopPartialStreamStore {
1074    async fn get_partial_stream(
1075        &self,
1076        _session_id: SessionId,
1077        _turn_id: &str,
1078    ) -> Result<Option<PartialStreamState>> {
1079        Ok(None)
1080    }
1081}
1082
1083/// Runtime context provided to tools during execution.
1084///
1085/// This context contains:
1086/// - Session ID for scoping operations
1087/// - Optional stores for tools that need external access
1088///
1089/// Tools that need context-aware execution (like filesystem tools) can use
1090/// the `execute_with_context` method on the Tool trait.
1091#[derive(Clone)]
1092pub struct ToolContext {
1093    /// The session ID for the current execution
1094    pub session_id: SessionId,
1095
1096    /// Optional file store for filesystem operations
1097    pub file_store: Option<Arc<dyn SessionFileSystem>>,
1098
1099    /// Optional storage store for key/value and secret storage
1100    pub storage_store: Option<Arc<dyn SessionStorageStore>>,
1101
1102    /// Optional durable image artifact store for tool-side media persistence.
1103    pub image_store: Option<Arc<dyn ImageArtifactStore>>,
1104
1105    /// Optional provider credential store for tool-side API clients.
1106    pub provider_credential_store: Option<Arc<dyn ProviderCredentialStore>>,
1107
1108    /// Optional system utility LLM service for capability internals.
1109    pub utility_llm_service: Option<Arc<dyn crate::UtilityLlmService>>,
1110
1111    /// Optional outbound egress service for HTTP/API traffic.
1112    pub egress_service: Option<Arc<dyn crate::EgressService>>,
1113
1114    /// Optional session SQL database store
1115    pub sqldb_store: Option<SessionSqlDbStoreRef>,
1116
1117    /// Optional message retriever for tools that need conversation history access
1118    pub message_retriever: Option<Arc<dyn crate::message_retriever::MessageRetriever>>,
1119
1120    /// Optional session store for tools that need session metadata access.
1121    pub session_store: Option<Arc<dyn SessionStore>>,
1122
1123    /// Optional session mutator for tools that need to update session metadata.
1124    pub session_mutator: Option<Arc<dyn SessionMutator>>,
1125
1126    /// Optional agent store for tools that need agent metadata access.
1127    pub agent_store: Option<Arc<dyn AgentStore>>,
1128
1129    /// Optional resolver for user connection tokens (lazy GitHub token lookup, etc.)
1130    pub connection_resolver: Option<Arc<dyn UserConnectionResolver>>,
1131
1132    /// Optional session schedule store for scheduling tools.
1133    pub schedule_store: Option<Arc<dyn SessionScheduleStore>>,
1134
1135    /// Optional platform store for org-level management tools.
1136    pub platform_store: Option<Arc<dyn crate::platform_store::PlatformStore>>,
1137    /// Optional leased resource store for lifecycle-managed provider resources.
1138    pub leased_resource_store: Option<Arc<dyn LeasedResourceStore>>,
1139
1140    /// Optional session resource registry — generic registry of active resources.
1141    pub session_resource_registry: Option<Arc<dyn SessionResourceRegistry>>,
1142
1143    /// Optional session task registry — background work owned by the session
1144    /// (specs/session-tasks.md).
1145    pub session_task_registry: Option<Arc<dyn crate::session_task::SessionTaskRegistry>>,
1146
1147    /// Optional event emitter for tools that need to stream progress updates.
1148    /// When set, tools can emit `tool.progress` events during execution.
1149    pub event_emitter: Option<Arc<dyn EventEmitter>>,
1150
1151    /// Event context for correlating progress events with the current tool call.
1152    /// Set by ActAtom when constructing the ToolContext.
1153    pub event_context: Option<crate::events::EventContext>,
1154
1155    /// The tool call ID for the current execution (set by ActAtom).
1156    /// Used by tools to emit correlated progress events.
1157    pub tool_call_id: Option<String>,
1158    /// Optional capability registry for blueprint lookups.
1159    pub capability_registry: Option<crate::capabilities::CapabilityRegistry>,
1160
1161    /// Optional registry of active built-in tools for meta-tools such as
1162    /// `spawn_background` that need to inspect or delegate to sibling tools.
1163    pub tool_registry: Option<Arc<crate::tools::ToolRegistry>>,
1164
1165    /// Optional allowlist of tools visible to the model for this turn.
1166    /// Registry-introspecting tools must filter through this before returning
1167    /// sibling tool metadata, because the execution registry can be a superset.
1168    pub visible_tool_names: Option<Arc<HashSet<String>>>,
1169
1170    /// Optional org ID for org-scoped operations.
1171    pub org_id: Option<crate::typed_id::OrgId>,
1172
1173    /// Merged network access list (harness ∩ agent ∩ session).
1174    /// When set, tools that make HTTP requests must check URLs against this list.
1175    pub network_access: Option<crate::network_access::NetworkAccessList>,
1176
1177    /// Resolved locale for localized tool behavior (BCP 47, e.g. `uk-UA`).
1178    /// When set, tools that support localization use this to produce
1179    /// locale-appropriate descriptions, error messages, and prompts.
1180    pub locale: Option<String>,
1181
1182    /// Optional budget checker for the check_budget tool.
1183    pub budget_checker: Option<Arc<dyn BudgetChecker>>,
1184
1185    /// Optional internal payment authority for paid capability tools.
1186    pub payment_authority: Option<Arc<dyn PaymentAuthority>>,
1187
1188    /// Optional durable spawn handle store for subagent reattach (EVE-535).
1189    /// When set, `spawn_subagent` uses claim/settle to prevent duplicate spawning
1190    /// on parent worker reclaim.
1191    pub subagent_spawn_store: Option<Arc<dyn SubagentSpawnStore>>,
1192}
1193
1194impl ToolContext {
1195    /// Create a new tool context with just a session ID
1196    pub fn new(session_id: SessionId) -> Self {
1197        Self {
1198            session_id,
1199            file_store: None,
1200            storage_store: None,
1201            image_store: None,
1202            provider_credential_store: None,
1203            utility_llm_service: None,
1204            egress_service: None,
1205            sqldb_store: None,
1206            message_retriever: None,
1207            session_store: None,
1208            session_mutator: None,
1209            agent_store: None,
1210            connection_resolver: None,
1211            schedule_store: None,
1212            platform_store: None,
1213            leased_resource_store: None,
1214            session_resource_registry: None,
1215            session_task_registry: None,
1216            event_emitter: None,
1217            event_context: None,
1218            tool_call_id: None,
1219            capability_registry: None,
1220            tool_registry: None,
1221            visible_tool_names: None,
1222            org_id: None,
1223            network_access: None,
1224            locale: None,
1225            budget_checker: None,
1226            payment_authority: None,
1227            subagent_spawn_store: None,
1228        }
1229    }
1230
1231    /// Create a context with a file store
1232    pub fn with_file_store(session_id: SessionId, file_store: Arc<dyn SessionFileSystem>) -> Self {
1233        Self {
1234            session_id,
1235            file_store: Some(file_store),
1236            storage_store: None,
1237            image_store: None,
1238            provider_credential_store: None,
1239            utility_llm_service: None,
1240            egress_service: None,
1241            sqldb_store: None,
1242            message_retriever: None,
1243            session_store: None,
1244            session_mutator: None,
1245            agent_store: None,
1246            connection_resolver: None,
1247            schedule_store: None,
1248            platform_store: None,
1249            leased_resource_store: None,
1250            session_resource_registry: None,
1251            session_task_registry: None,
1252            event_emitter: None,
1253            event_context: None,
1254            tool_call_id: None,
1255            capability_registry: None,
1256            tool_registry: None,
1257            visible_tool_names: None,
1258            org_id: None,
1259            network_access: None,
1260            locale: None,
1261            budget_checker: None,
1262            payment_authority: None,
1263            subagent_spawn_store: None,
1264        }
1265    }
1266
1267    /// Create a context with a storage store
1268    pub fn with_storage_store(
1269        session_id: SessionId,
1270        storage_store: Arc<dyn SessionStorageStore>,
1271    ) -> Self {
1272        Self {
1273            session_id,
1274            file_store: None,
1275            storage_store: Some(storage_store),
1276            image_store: None,
1277            provider_credential_store: None,
1278            utility_llm_service: None,
1279            egress_service: None,
1280            sqldb_store: None,
1281            message_retriever: None,
1282            session_store: None,
1283            session_mutator: None,
1284            agent_store: None,
1285            connection_resolver: None,
1286            schedule_store: None,
1287            platform_store: None,
1288            leased_resource_store: None,
1289            session_resource_registry: None,
1290            session_task_registry: None,
1291            event_emitter: None,
1292            event_context: None,
1293            tool_call_id: None,
1294            capability_registry: None,
1295            tool_registry: None,
1296            visible_tool_names: None,
1297            org_id: None,
1298            network_access: None,
1299            locale: None,
1300            budget_checker: None,
1301            payment_authority: None,
1302            subagent_spawn_store: None,
1303        }
1304    }
1305
1306    /// Create a context with both file store and storage store
1307    pub fn with_stores(
1308        session_id: SessionId,
1309        file_store: Arc<dyn SessionFileSystem>,
1310        storage_store: Arc<dyn SessionStorageStore>,
1311    ) -> Self {
1312        Self {
1313            session_id,
1314            file_store: Some(file_store),
1315            storage_store: Some(storage_store),
1316            sqldb_store: None,
1317            image_store: None,
1318            provider_credential_store: None,
1319            utility_llm_service: None,
1320            egress_service: None,
1321            message_retriever: None,
1322            session_store: None,
1323            session_mutator: None,
1324            agent_store: None,
1325            connection_resolver: None,
1326            schedule_store: None,
1327            platform_store: None,
1328            leased_resource_store: None,
1329            session_resource_registry: None,
1330            session_task_registry: None,
1331            event_emitter: None,
1332            event_context: None,
1333            tool_call_id: None,
1334            capability_registry: None,
1335            tool_registry: None,
1336            visible_tool_names: None,
1337            org_id: None,
1338            network_access: None,
1339            locale: None,
1340            budget_checker: None,
1341            payment_authority: None,
1342            subagent_spawn_store: None,
1343        }
1344    }
1345
1346    /// Add a SQL database store to this context
1347    pub fn with_sqldb_store(mut self, sqldb_store: SessionSqlDbStoreRef) -> Self {
1348        self.sqldb_store = Some(sqldb_store);
1349        self
1350    }
1351
1352    /// Add a message retriever to this context
1353    pub fn with_message_retriever(
1354        mut self,
1355        retriever: Arc<dyn crate::message_retriever::MessageRetriever>,
1356    ) -> Self {
1357        self.message_retriever = Some(retriever);
1358        self
1359    }
1360
1361    /// Add a session store to this context.
1362    pub fn with_session_store(mut self, store: Arc<dyn SessionStore>) -> Self {
1363        self.session_store = Some(store);
1364        self
1365    }
1366
1367    /// Add a session mutator to this context.
1368    pub fn with_session_mutator(mut self, mutator: Arc<dyn SessionMutator>) -> Self {
1369        self.session_mutator = Some(mutator);
1370        self
1371    }
1372
1373    /// Add an agent store to this context.
1374    pub fn with_agent_store(mut self, store: Arc<dyn AgentStore>) -> Self {
1375        self.agent_store = Some(store);
1376        self
1377    }
1378
1379    /// Add a connection resolver to this context
1380    pub fn with_connection_resolver(mut self, resolver: Arc<dyn UserConnectionResolver>) -> Self {
1381        self.connection_resolver = Some(resolver);
1382        self
1383    }
1384
1385    /// Create a context with an image artifact store.
1386    pub fn with_image_store(
1387        session_id: SessionId,
1388        image_store: Arc<dyn ImageArtifactStore>,
1389    ) -> Self {
1390        Self {
1391            session_id,
1392            file_store: None,
1393            storage_store: None,
1394            image_store: Some(image_store),
1395            provider_credential_store: None,
1396            utility_llm_service: None,
1397            egress_service: None,
1398            sqldb_store: None,
1399            message_retriever: None,
1400            session_store: None,
1401            session_mutator: None,
1402            agent_store: None,
1403            connection_resolver: None,
1404            schedule_store: None,
1405            platform_store: None,
1406            leased_resource_store: None,
1407            session_resource_registry: None,
1408            session_task_registry: None,
1409            event_emitter: None,
1410            event_context: None,
1411            tool_call_id: None,
1412            capability_registry: None,
1413            tool_registry: None,
1414            visible_tool_names: None,
1415            org_id: None,
1416            network_access: None,
1417            locale: None,
1418            budget_checker: None,
1419            payment_authority: None,
1420            subagent_spawn_store: None,
1421        }
1422    }
1423
1424    /// Set the provider credential store on this context.
1425    pub fn with_provider_credential_store(
1426        mut self,
1427        store: Arc<dyn ProviderCredentialStore>,
1428    ) -> Self {
1429        self.provider_credential_store = Some(store);
1430        self
1431    }
1432
1433    /// Set the utility LLM service on this context.
1434    pub fn with_utility_llm_service(mut self, service: Arc<dyn crate::UtilityLlmService>) -> Self {
1435        self.utility_llm_service = Some(service);
1436        self
1437    }
1438
1439    /// Set the outbound egress service on this context.
1440    pub fn with_egress_service(mut self, service: Arc<dyn crate::EgressService>) -> Self {
1441        self.egress_service = Some(service);
1442        self
1443    }
1444
1445    /// Add a session schedule store to this context.
1446    pub fn with_schedule_store(mut self, store: Arc<dyn SessionScheduleStore>) -> Self {
1447        self.schedule_store = Some(store);
1448        self
1449    }
1450
1451    /// Add a platform store to this context.
1452    pub fn with_platform_store(
1453        mut self,
1454        store: Arc<dyn crate::platform_store::PlatformStore>,
1455    ) -> Self {
1456        self.platform_store = Some(store);
1457        self
1458    }
1459
1460    /// Add a leased resource store to this context.
1461    pub fn with_leased_resource_store(mut self, store: Arc<dyn LeasedResourceStore>) -> Self {
1462        self.leased_resource_store = Some(store);
1463        self
1464    }
1465
1466    /// Add a session resource registry to this context.
1467    pub fn with_session_resource_registry(
1468        mut self,
1469        registry: Arc<dyn SessionResourceRegistry>,
1470    ) -> Self {
1471        self.session_resource_registry = Some(registry);
1472        self
1473    }
1474
1475    /// Add a session task registry to this context.
1476    pub fn with_session_task_registry(
1477        mut self,
1478        registry: Arc<dyn crate::session_task::SessionTaskRegistry>,
1479    ) -> Self {
1480        self.session_task_registry = Some(registry);
1481        self
1482    }
1483
1484    /// Set org ID for org-scoped operations.
1485    pub fn with_org_id(mut self, org_id: crate::typed_id::OrgId) -> Self {
1486        self.org_id = Some(org_id);
1487        self
1488    }
1489
1490    /// Set the active built-in tool registry on this context.
1491    pub fn with_tool_registry(mut self, registry: Arc<crate::tools::ToolRegistry>) -> Self {
1492        self.tool_registry = Some(registry);
1493        self
1494    }
1495
1496    /// Set the tool names visible to the model in this turn.
1497    pub fn with_visible_tool_names(mut self, names: Arc<HashSet<String>>) -> Self {
1498        self.visible_tool_names = Some(names);
1499        self
1500    }
1501
1502    /// Set the merged network access list for URL filtering.
1503    pub fn with_network_access(
1504        mut self,
1505        network_access: Option<crate::network_access::NetworkAccessList>,
1506    ) -> Self {
1507        self.network_access = network_access;
1508        self
1509    }
1510
1511    /// Set the internal payment authority for paid capability operations.
1512    pub fn with_payment_authority(mut self, authority: Arc<dyn PaymentAuthority>) -> Self {
1513        self.payment_authority = Some(authority);
1514        self
1515    }
1516
1517    /// Set the durable subagent spawn handle store (EVE-535).
1518    pub fn with_subagent_spawn_store(mut self, store: Arc<dyn SubagentSpawnStore>) -> Self {
1519        self.subagent_spawn_store = Some(store);
1520        self
1521    }
1522
1523    /// Emit a `tool.progress` event if an event emitter and context are available.
1524    ///
1525    /// This is a best-effort helper: failures are logged but not propagated,
1526    /// so tools never fail just because a progress event couldn't be sent.
1527    pub async fn emit_progress(&self, tool_name: &str, message: &str) {
1528        let (Some(emitter), Some(ctx), Some(call_id)) =
1529            (&self.event_emitter, &self.event_context, &self.tool_call_id)
1530        else {
1531            return;
1532        };
1533        if let Err(e) = emitter
1534            .emit(EventRequest::new(
1535                self.session_id,
1536                ctx.clone(),
1537                crate::events::ToolProgressData {
1538                    tool_call_id: call_id.clone(),
1539                    tool_name: tool_name.to_string(),
1540                    message: message.to_string(),
1541                    display_name: None,
1542                },
1543            ))
1544            .await
1545        {
1546            tracing::debug!(
1547                tool_call_id = call_id,
1548                tool_name,
1549                error = %e,
1550                "Failed to emit tool.progress event"
1551            );
1552        }
1553    }
1554
1555    /// Emit a `tool.output.delta` event if an event emitter and context are available.
1556    ///
1557    /// Streams incremental output chunks (e.g., stdout/stderr lines) for live
1558    /// rendering in UI and CLI. Best-effort: failures are logged, not propagated.
1559    pub async fn emit_tool_output(&self, tool_name: &str, delta: &str, stream: &str) {
1560        let (Some(emitter), Some(ctx), Some(call_id)) =
1561            (&self.event_emitter, &self.event_context, &self.tool_call_id)
1562        else {
1563            return;
1564        };
1565        if let Err(e) = emitter
1566            .emit(EventRequest::new(
1567                self.session_id,
1568                ctx.clone(),
1569                crate::events::ToolOutputDeltaData {
1570                    tool_call_id: call_id.clone(),
1571                    tool_name: tool_name.to_string(),
1572                    delta: delta.to_string(),
1573                    stream: stream.to_string(),
1574                },
1575            ))
1576            .await
1577        {
1578            tracing::debug!(
1579                tool_call_id = call_id,
1580                tool_name,
1581                error = %e,
1582                "Failed to emit tool.output.delta event"
1583            );
1584        }
1585    }
1586}
1587
1588impl std::fmt::Debug for ToolContext {
1589    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1590        f.debug_struct("ToolContext")
1591            .field("session_id", &self.session_id)
1592            .field("file_store", &self.file_store.is_some())
1593            .field("storage_store", &self.storage_store.is_some())
1594            .field("image_store", &self.image_store.is_some())
1595            .field(
1596                "provider_credential_store",
1597                &self.provider_credential_store.is_some(),
1598            )
1599            .field("utility_llm_service", &self.utility_llm_service.is_some())
1600            .field("egress_service", &self.egress_service.is_some())
1601            .field("sqldb_store", &self.sqldb_store.is_some())
1602            .field("message_retriever", &self.message_retriever.is_some())
1603            .field("session_store", &self.session_store.is_some())
1604            .field("session_mutator", &self.session_mutator.is_some())
1605            .field("agent_store", &self.agent_store.is_some())
1606            .field("connection_resolver", &self.connection_resolver.is_some())
1607            .field("schedule_store", &self.schedule_store.is_some())
1608            .field("platform_store", &self.platform_store.is_some())
1609            .field(
1610                "leased_resource_store",
1611                &self.leased_resource_store.is_some(),
1612            )
1613            .field("event_emitter", &self.event_emitter.is_some())
1614            .field("tool_registry", &self.tool_registry.is_some())
1615            .field("payment_authority", &self.payment_authority.is_some())
1616            .field("subagent_spawn_store", &self.subagent_spawn_store.is_some())
1617            .field("org_id", &self.org_id)
1618            .finish()
1619    }
1620}
1621
1622// ============================================================================
1623// EventEmitter - For emitting events
1624// ============================================================================
1625
1626use crate::events::{Event, EventRequest};
1627
1628/// Trait for emitting events following the standard event protocol
1629///
1630/// Implementations can:
1631/// - Store events in a database
1632/// - Keep events in memory for testing
1633/// - Stream events via SSE/WebSocket
1634/// - Log events for debugging
1635///
1636/// Events follow a consistent schema: id, type, ts, context, data.
1637/// See specs/events.md for the full event protocol specification.
1638#[async_trait]
1639pub trait EventEmitter: Send + Sync {
1640    /// Emit an event request
1641    ///
1642    /// Takes an EventRequest (without id/sequence) and returns the stored Event
1643    /// with id and sequence assigned by the storage layer.
1644    async fn emit(&self, request: EventRequest) -> Result<Event>;
1645}
1646
1647/// Blanket impl: `Arc<E>` delegates to the inner emitter.
1648#[async_trait]
1649impl<E: EventEmitter + ?Sized> EventEmitter for Arc<E> {
1650    async fn emit(&self, request: EventRequest) -> Result<Event> {
1651        (**self).emit(request).await
1652    }
1653}
1654
1655/// No-op event emitter for when event emission is not needed
1656///
1657/// This is useful for testing or when event observability is disabled.
1658#[derive(Debug, Clone, Default)]
1659pub struct NoopEventEmitter;
1660
1661#[async_trait]
1662impl EventEmitter for NoopEventEmitter {
1663    async fn emit(&self, request: EventRequest) -> Result<Event> {
1664        // Return a dummy event with sequence 0
1665        Ok(request.into_event(crate::typed_id::EventId::new(), 0))
1666    }
1667}
1668
1669// Note: EventListener trait has been moved to event_listeners.rs module.
1670// Use `everruns_core::EventListener` or `everruns_core::event_listeners::EventListener`.
1671
1672// ============================================================================
1673// ImageResolver - For resolving image_file content to actual image data
1674// ============================================================================
1675
1676/// Resolved image data for LLM consumption
1677///
1678/// This struct contains the actual image data in a format suitable for
1679/// sending to LLM providers. Both OpenAI and Anthropic accept base64-encoded
1680/// images with media type information.
1681#[derive(Debug, Clone)]
1682pub struct ResolvedImage {
1683    /// Base64-encoded image data (without data URL prefix)
1684    pub base64: String,
1685    /// MIME type (e.g., "image/png", "image/jpeg")
1686    pub media_type: String,
1687}
1688
1689impl ResolvedImage {
1690    /// Create a new resolved image
1691    pub fn new(base64: impl Into<String>, media_type: impl Into<String>) -> Self {
1692        Self {
1693            base64: base64.into(),
1694            media_type: media_type.into(),
1695        }
1696    }
1697
1698    /// Convert to a data URL suitable for OpenAI Vision API
1699    ///
1700    /// Format: `data:{media_type};base64,{base64_data}`
1701    pub fn to_data_url(&self) -> String {
1702        format!("data:{};base64,{}", self.media_type, self.base64)
1703    }
1704}
1705
1706/// Trait for resolving image_file content parts to actual image data
1707///
1708/// When building LLM messages, `image_file` content parts contain only
1709/// a reference (UUID) to an uploaded image. This trait allows resolving
1710/// those references to actual image data.
1711///
1712/// # Provider-specific formatting
1713///
1714/// The resolved image data is then converted to provider-specific formats:
1715///
1716/// **OpenAI Vision:**
1717/// ```json
1718/// {
1719///   "type": "image_url",
1720///   "image_url": { "url": "data:image/png;base64,..." }
1721/// }
1722/// ```
1723///
1724/// **Anthropic Vision:**
1725/// ```json
1726/// {
1727///   "type": "image",
1728///   "source": { "type": "base64", "media_type": "image/png", "data": "..." }
1729/// }
1730/// ```
1731///
1732/// # Implementation notes
1733///
1734/// Implementations should:
1735/// - Fetch image data from storage (database, S3, etc.)
1736/// - Return base64-encoded data with media type
1737/// - Handle missing images gracefully (return None)
1738#[async_trait]
1739pub trait ImageResolver: Send + Sync {
1740    /// Resolve an image_file reference to actual image data
1741    ///
1742    /// Returns `None` if the image is not found.
1743    async fn resolve_image(&self, image_id: Uuid) -> Result<Option<ResolvedImage>>;
1744}
1745
1746// ============================================================================
1747// SubagentSpawnStore — durable spawn handles for subagent reattach (EVE-535)
1748// ============================================================================
1749
1750/// Result of attempting to claim a subagent spawn slot.
1751#[derive(Debug)]
1752pub enum SpawnClaimResult {
1753    /// First claim — child session does not yet exist.
1754    /// Proceed to create the child, then call `register_child_session`.
1755    Claimed {
1756        spawn_handle_id: uuid::Uuid,
1757        claim_token: uuid::Uuid,
1758    },
1759    /// Row exists but `child_session_id` was never registered (crash between
1760    /// claim and `register_child_session`). Re-create the child and call
1761    /// `register_child_session` — same flow as `Claimed`.
1762    ClaimedPendingChild {
1763        spawn_handle_id: uuid::Uuid,
1764        claim_token: uuid::Uuid,
1765    },
1766    /// Child session was created and is still running.
1767    /// Reattach: wait for the existing child and settle with the stored claim_token.
1768    AlreadyRunning {
1769        child_session_id: crate::typed_id::SessionId,
1770        /// Stored claim token — must be used for `settle_spawn` on this replay.
1771        claim_token: uuid::Uuid,
1772    },
1773    /// Child already finished on a previous execution.
1774    /// Fast-path: return the stored result immediately without waiting.
1775    AlreadySettled {
1776        child_session_id: crate::typed_id::SessionId,
1777        /// The `wait_for_idle` return value from the original execution.
1778        terminal_status: String,
1779        terminal_result: String,
1780    },
1781}
1782
1783/// Durable spawn handle store for subagent idempotency (EVE-535).
1784///
1785/// Maps `(parent_session_id, tool_call_id) → child_session_id` so that when
1786/// a parent's `act` is reclaimed mid-`wait_for_idle`, the tool can reattach
1787/// to the existing child instead of spawning a duplicate.
1788///
1789/// Lifecycle: claim → register_child_session → settle_spawn.
1790#[async_trait]
1791pub trait SubagentSpawnStore: Send + Sync + 'static {
1792    /// Attempt to claim a spawn slot for `(parent_session_id, tool_call_id)`.
1793    ///
1794    /// Does NOT accept `child_session_id` — the child session does not exist yet.
1795    /// Call `register_child_session` with the actual child ID after creating it.
1796    async fn try_claim_spawn(
1797        &self,
1798        parent_session_id: crate::typed_id::SessionId,
1799        tool_call_id: &str,
1800        subagent_name: &str,
1801        subagent_task: &str,
1802        claim_token: uuid::Uuid,
1803    ) -> Result<SpawnClaimResult>;
1804
1805    /// Register the actual child session ID after it has been created.
1806    ///
1807    /// Must be called after `try_claim_spawn` returns `Claimed` or
1808    /// `ClaimedPendingChild`, before waiting for the child to complete.
1809    async fn register_child_session(
1810        &self,
1811        spawn_handle_id: uuid::Uuid,
1812        claim_token: uuid::Uuid,
1813        child_session_id: crate::typed_id::SessionId,
1814    ) -> Result<()>;
1815
1816    /// Record the terminal result once the child has completed.
1817    ///
1818    /// `claim_token` must match the stored token. `terminal_status` is the
1819    /// `wait_for_idle` return value ("idle", "error", "timeout", etc.) and
1820    /// `terminal_result` is the last agent message.
1821    async fn settle_spawn(
1822        &self,
1823        parent_session_id: crate::typed_id::SessionId,
1824        tool_call_id: &str,
1825        claim_token: uuid::Uuid,
1826        terminal_status: &str,
1827        terminal_result: &str,
1828    ) -> Result<()>;
1829}
1830
1831/// Blanket impl: `Arc<S>` delegates to the inner store.
1832#[async_trait]
1833impl<S: SubagentSpawnStore + ?Sized> SubagentSpawnStore for Arc<S> {
1834    async fn try_claim_spawn(
1835        &self,
1836        parent_session_id: crate::typed_id::SessionId,
1837        tool_call_id: &str,
1838        subagent_name: &str,
1839        subagent_task: &str,
1840        claim_token: uuid::Uuid,
1841    ) -> Result<SpawnClaimResult> {
1842        (**self)
1843            .try_claim_spawn(
1844                parent_session_id,
1845                tool_call_id,
1846                subagent_name,
1847                subagent_task,
1848                claim_token,
1849            )
1850            .await
1851    }
1852
1853    async fn register_child_session(
1854        &self,
1855        spawn_handle_id: uuid::Uuid,
1856        claim_token: uuid::Uuid,
1857        child_session_id: crate::typed_id::SessionId,
1858    ) -> Result<()> {
1859        (**self)
1860            .register_child_session(spawn_handle_id, claim_token, child_session_id)
1861            .await
1862    }
1863
1864    async fn settle_spawn(
1865        &self,
1866        parent_session_id: crate::typed_id::SessionId,
1867        tool_call_id: &str,
1868        claim_token: uuid::Uuid,
1869        terminal_status: &str,
1870        terminal_result: &str,
1871    ) -> Result<()> {
1872        (**self)
1873            .settle_spawn(
1874                parent_session_id,
1875                tool_call_id,
1876                claim_token,
1877                terminal_status,
1878                terminal_result,
1879            )
1880            .await
1881    }
1882}
1883
1884/// No-op spawn store — used when no durable store is configured (dev/test).
1885///
1886/// Always claims (no dedup); settle and register are no-ops.
1887pub struct NoopSubagentSpawnStore;
1888
1889#[async_trait]
1890impl SubagentSpawnStore for NoopSubagentSpawnStore {
1891    async fn try_claim_spawn(
1892        &self,
1893        _parent_session_id: crate::typed_id::SessionId,
1894        _tool_call_id: &str,
1895        _subagent_name: &str,
1896        _subagent_task: &str,
1897        claim_token: uuid::Uuid,
1898    ) -> Result<SpawnClaimResult> {
1899        Ok(SpawnClaimResult::Claimed {
1900            spawn_handle_id: uuid::Uuid::new_v4(),
1901            claim_token,
1902        })
1903    }
1904
1905    async fn register_child_session(
1906        &self,
1907        _spawn_handle_id: uuid::Uuid,
1908        _claim_token: uuid::Uuid,
1909        _child_session_id: crate::typed_id::SessionId,
1910    ) -> Result<()> {
1911        Ok(())
1912    }
1913
1914    async fn settle_spawn(
1915        &self,
1916        _parent_session_id: crate::typed_id::SessionId,
1917        _tool_call_id: &str,
1918        _claim_token: uuid::Uuid,
1919        _terminal_status: &str,
1920        _terminal_result: &str,
1921    ) -> Result<()> {
1922        Ok(())
1923    }
1924}
1925
1926// ============================================================================
1927// Tests
1928// ============================================================================
1929
1930#[cfg(test)]
1931mod tests {
1932    use super::*;
1933
1934    #[test]
1935    fn test_resolved_image_new() {
1936        let image = ResolvedImage::new("SGVsbG8=", "image/png");
1937        assert_eq!(image.base64, "SGVsbG8=");
1938        assert_eq!(image.media_type, "image/png");
1939    }
1940
1941    #[test]
1942    fn test_resolved_image_to_data_url() {
1943        let image = ResolvedImage::new("SGVsbG8=", "image/png");
1944        let data_url = image.to_data_url();
1945        assert_eq!(data_url, "data:image/png;base64,SGVsbG8=");
1946    }
1947
1948    #[test]
1949    fn test_resolved_image_jpeg() {
1950        let image = ResolvedImage::new("base64data", "image/jpeg");
1951        let data_url = image.to_data_url();
1952        assert!(data_url.starts_with("data:image/jpeg;base64,"));
1953    }
1954}