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::provider::DriverId;
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, WorkspaceId};
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
21fn workspace_display_path(path: &str) -> String {
22    if path == "/" {
23        "/workspace".to_string()
24    } else if path.starts_with('/') {
25        format!("/workspace{path}")
26    } else {
27        format!("/workspace/{path}")
28    }
29}
30
31/// Build a map of tool names to definitions for efficient lookup
32fn build_tool_map(tool_defs: &[ToolDefinition]) -> HashMap<&str, &ToolDefinition> {
33    tool_defs.iter().map(|def| (def.name(), def)).collect()
34}
35
36use crate::error::Result;
37
38// ============================================================================
39// ReasoningEffortHandle - live, turn-scoped reasoning-effort override
40// ============================================================================
41
42/// A live, shared handle to the reasoning effort for the current turn (EVE-595).
43///
44/// Each internal LLM step within a single `run_turn` re-reads this handle when
45/// building its provider request. A tool (or any in-turn actor) can call
46/// [`ReasoningEffortHandle::set`] mid-turn so that *subsequent* LLM steps in the
47/// same turn use the new effort, without waiting for the next turn.
48///
49/// When the handle holds `None`, the [`crate::atoms::ReasonAtom`] falls back to
50/// the effort resolved from the latest user message's `controls` — so callers
51/// that never set an override see no behavior change.
52#[derive(Clone, Default)]
53pub struct ReasoningEffortHandle {
54    inner: Arc<std::sync::RwLock<Option<String>>>,
55}
56
57impl ReasoningEffortHandle {
58    /// Create an empty handle (no override).
59    pub fn new() -> Self {
60        Self::default()
61    }
62
63    /// Create a handle pre-seeded with an effort override.
64    pub fn with_effort(effort: impl Into<String>) -> Self {
65        Self {
66            inner: Arc::new(std::sync::RwLock::new(Some(effort.into()))),
67        }
68    }
69
70    /// Set (or replace) the override effort. Subsequent LLM steps in the same
71    /// turn pick this up. Pass `None` to clear the override and fall back to the
72    /// message-derived effort.
73    pub fn set(&self, effort: Option<String>) {
74        // Recover from a poisoned lock so a panic elsewhere never silently
75        // disables mid-turn overrides; the stored value is a plain Option<String>
76        // with no broken invariant to worry about.
77        let mut guard = self.inner.write().unwrap_or_else(|e| e.into_inner());
78        *guard = effort;
79    }
80
81    /// Read the current override effort, if any.
82    pub fn get(&self) -> Option<String> {
83        // Recover from a poisoned lock rather than silently reverting to the
84        // message-derived effort mid-turn (see `set`).
85        let guard = self.inner.read().unwrap_or_else(|e| e.into_inner());
86        guard.clone()
87    }
88}
89
90impl std::fmt::Debug for ReasoningEffortHandle {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        f.debug_struct("ReasoningEffortHandle")
93            .field("effort", &self.get())
94            .finish()
95    }
96}
97
98// ============================================================================
99// AgentStore - For retrieving agent configurations
100// ============================================================================
101
102/// Trait for retrieving agent configurations
103///
104/// Implementations can:
105/// - Load agents from a database
106/// - Keep agents in memory for testing
107/// - Load agents from a configuration file
108#[async_trait]
109pub trait AgentStore: Send + Sync {
110    /// Get an agent by ID
111    async fn get_agent(&self, agent_id: AgentId) -> Result<Option<Agent>>;
112}
113
114#[async_trait]
115impl<T: AgentStore + ?Sized> AgentStore for std::sync::Arc<T> {
116    async fn get_agent(&self, agent_id: AgentId) -> Result<Option<Agent>> {
117        (**self).get_agent(agent_id).await
118    }
119}
120
121// ============================================================================
122// HarnessStore - For retrieving harness configurations
123// ============================================================================
124
125/// Trait for retrieving harness configurations
126///
127/// Implementations can:
128/// - Load harnesses from a database
129/// - Keep harnesses in memory for testing
130///
131/// Returns the harness inheritance chain (root-to-leaf) so the caller
132/// can fold each harness as an `AgentConfigOverlay`. DB-backed stores
133/// return the raw chain; gRPC-backed stores may return a single
134/// pre-merged harness (functionally equivalent when folded).
135#[async_trait]
136pub trait HarnessStore: Send + Sync {
137    /// Get the harness inheritance chain, root-to-leaf.
138    ///
139    /// Returns `Ok(vec![])` if the harness does not exist.
140    /// A harness with no parent returns a single-element vec.
141    async fn get_harness_chain(&self, harness_id: HarnessId) -> Result<Vec<Harness>>;
142}
143
144#[async_trait]
145impl<T: HarnessStore + ?Sized> HarnessStore for std::sync::Arc<T> {
146    async fn get_harness_chain(&self, harness_id: HarnessId) -> Result<Vec<Harness>> {
147        (**self).get_harness_chain(harness_id).await
148    }
149}
150
151// ============================================================================
152// SessionStore - For retrieving session information
153// ============================================================================
154
155use crate::leased_resource::{LeasedResource, UpsertLeasedResource};
156use crate::session::Session;
157
158/// Trait for retrieving session configurations
159///
160/// Implementations can:
161/// - Load sessions from a database
162/// - Keep sessions in memory for testing
163#[async_trait]
164pub trait SessionStore: Send + Sync {
165    /// Get a session by ID
166    async fn get_session(&self, session_id: SessionId) -> Result<Option<Session>>;
167}
168
169#[async_trait]
170impl<T: SessionStore + ?Sized> SessionStore for std::sync::Arc<T> {
171    async fn get_session(&self, session_id: SessionId) -> Result<Option<Session>> {
172        (**self).get_session(session_id).await
173    }
174}
175
176/// Trait for updating mutable session metadata.
177#[async_trait]
178pub trait SessionMutator: Send + Sync {
179    /// Update a session's human-readable title.
180    async fn update_session_title(&self, session_id: SessionId, title: String) -> Result<Session>;
181}
182
183#[async_trait]
184impl<T: SessionMutator + ?Sized> SessionMutator for std::sync::Arc<T> {
185    async fn update_session_title(&self, session_id: SessionId, title: String) -> Result<Session> {
186        (**self).update_session_title(session_id, title).await
187    }
188}
189
190// ============================================================================
191// ProviderStore - For retrieving LLM provider configurations
192// ============================================================================
193
194/// Model information with provider details needed for LLM calls
195#[derive(Debug, Clone)]
196pub struct ResolvedModel {
197    /// The model ID string to pass to the LLM API (e.g., "gpt-4o", "claude-3-opus")
198    pub model: String,
199    /// Provider type for factory selection
200    pub provider_type: DriverId,
201    /// Decrypted API key (if configured)
202    pub api_key: Option<String>,
203    /// Optional base URL override
204    pub base_url: Option<String>,
205    /// Extra provider-specific metadata (OAuth tokens, account ids, etc.).
206    /// Used by embedder-defined providers that authenticate without an API key.
207    pub provider_metadata: Option<crate::driver_registry::ProviderMetadata>,
208}
209
210/// Trait for retrieving LLM provider and model configurations
211///
212/// This trait abstracts the database lookup and API key decryption needed
213/// to create LLM providers at runtime.
214///
215/// Implementations can:
216/// - Load from a database with encrypted API keys
217/// - Use in-memory configurations for testing
218/// - Load from environment variables for development
219#[async_trait]
220pub trait ProviderStore: Send + Sync {
221    /// Get model with provider info by model ID
222    ///
223    /// Returns the model string ID, provider type, decrypted API key, and base URL
224    /// needed to create an LLM provider via the factory.
225    async fn get_resolved_model(&self, model_id: ModelId) -> Result<Option<ResolvedModel>>;
226
227    /// Get the default model with provider info
228    ///
229    /// Returns the system default model when an agent has no default_model_id set.
230    async fn get_default_model(&self) -> Result<Option<ResolvedModel>>;
231}
232
233#[async_trait]
234impl<T: ProviderStore + ?Sized> ProviderStore for std::sync::Arc<T> {
235    async fn get_resolved_model(&self, model_id: ModelId) -> Result<Option<ResolvedModel>> {
236        (**self).get_resolved_model(model_id).await
237    }
238
239    async fn get_default_model(&self) -> Result<Option<ResolvedModel>> {
240        (**self).get_default_model().await
241    }
242}
243
244// ============================================================================
245// ImageArtifactStore - For durable image persistence from tools
246// ============================================================================
247
248/// Metadata for a stored image artifact.
249#[derive(Debug, Clone)]
250pub struct StoredImageInfo {
251    pub id: ImageId,
252    pub filename: String,
253    pub content_type: String,
254    pub size_bytes: i64,
255    pub metadata: serde_json::Value,
256    pub created_at: DateTime<Utc>,
257}
258
259/// Stored image artifact with binary data.
260#[derive(Debug, Clone)]
261pub struct StoredImage {
262    pub info: StoredImageInfo,
263    pub data: Vec<u8>,
264}
265
266/// Input for creating a stored image artifact.
267#[derive(Debug, Clone)]
268pub struct CreateStoredImage {
269    pub filename: String,
270    pub content_type: String,
271    pub data: Vec<u8>,
272    pub metadata: serde_json::Value,
273}
274
275#[async_trait]
276pub trait ImageArtifactStore: Send + Sync {
277    /// Persist an image artifact and return its durable metadata.
278    async fn create_image(&self, input: CreateStoredImage) -> Result<StoredImageInfo>;
279
280    /// Load a stored image artifact including bytes.
281    async fn get_image(&self, image_id: ImageId) -> Result<Option<StoredImage>>;
282
283    /// Load stored image metadata without binary data.
284    async fn get_image_info(&self, image_id: ImageId) -> Result<Option<StoredImageInfo>>;
285}
286
287// ============================================================================
288// ProviderCredentialStore - For tool-side provider credential resolution
289// ============================================================================
290
291/// Provider credentials resolved for tool-side API clients.
292#[derive(Debug, Clone)]
293pub struct ProviderCredentials {
294    pub api_key: String,
295    pub base_url: Option<String>,
296}
297
298#[async_trait]
299pub trait ProviderCredentialStore: Send + Sync {
300    /// Resolve default credentials for a provider type (for example `openai`).
301    ///
302    /// Implementations may apply environment fallbacks internally, but tools
303    /// should never read provider env vars directly.
304    async fn get_default_provider_credentials(
305        &self,
306        provider_type: &str,
307    ) -> Result<Option<ProviderCredentials>>;
308}
309
310// ============================================================================
311// ToolExecutor - For executing tool calls
312// ============================================================================
313
314/// Trait for executing tool calls
315///
316/// Implementations handle the actual tool execution:
317/// - Webhook calls
318/// - Built-in function execution
319/// - Mock execution for testing
320#[async_trait]
321pub trait ToolExecutor: Send + Sync {
322    /// Execute a single tool call (without context)
323    ///
324    /// This is the legacy method that doesn't provide context to tools.
325    /// Use `execute_with_context` when context is available.
326    async fn execute(&self, tool_call: &ToolCall, tool_def: &ToolDefinition) -> Result<ToolResult>;
327
328    /// Execute a single tool call with context
329    ///
330    /// This method provides runtime context to tools that need it (like filesystem tools).
331    /// The default implementation delegates to `execute()`.
332    async fn execute_with_context(
333        &self,
334        tool_call: &ToolCall,
335        tool_def: &ToolDefinition,
336        _context: &ToolContext,
337    ) -> Result<ToolResult> {
338        // Default: delegate to execute(), ignoring context
339        self.execute(tool_call, tool_def).await
340    }
341
342    /// Execute multiple tool calls (default: sequential)
343    async fn execute_batch(
344        &self,
345        tool_calls: &[ToolCall],
346        tool_defs: &[ToolDefinition],
347    ) -> Result<Vec<ToolResult>> {
348        let mut results = Vec::with_capacity(tool_calls.len());
349
350        let tool_map = build_tool_map(tool_defs);
351
352        for tool_call in tool_calls {
353            let tool_def = tool_map.get(tool_call.name.as_str()).ok_or_else(|| {
354                crate::error::AgentLoopError::tool(format!(
355                    "Tool definition not found: {}",
356                    tool_call.name
357                ))
358            })?;
359
360            results.push(self.execute(tool_call, tool_def).await?);
361        }
362
363        Ok(results)
364    }
365
366    /// Execute multiple tool calls in parallel
367    async fn execute_parallel(
368        &self,
369        tool_calls: &[ToolCall],
370        tool_defs: &[ToolDefinition],
371    ) -> Result<Vec<ToolResult>>
372    where
373        Self: Sized,
374    {
375        use futures::future::join_all;
376
377        let tool_map = build_tool_map(tool_defs);
378
379        let futures: Vec<_> = tool_calls
380            .iter()
381            .map(|tool_call| async {
382                let tool_def = tool_map.get(tool_call.name.as_str()).ok_or_else(|| {
383                    crate::error::AgentLoopError::tool(format!(
384                        "Tool definition not found: {}",
385                        tool_call.name
386                    ))
387                })?;
388                self.execute(tool_call, tool_def).await
389            })
390            .collect();
391
392        let results = join_all(futures).await;
393        results.into_iter().collect()
394    }
395}
396
397/// Delegating impl so callers can hold a `ToolExecutor` as a trait object
398/// (e.g. to choose between a plain registry and an MCP-routing composite at
399/// runtime without monomorphizing the consumer).
400#[async_trait]
401impl ToolExecutor for std::sync::Arc<dyn ToolExecutor> {
402    async fn execute(&self, tool_call: &ToolCall, tool_def: &ToolDefinition) -> Result<ToolResult> {
403        (**self).execute(tool_call, tool_def).await
404    }
405
406    async fn execute_with_context(
407        &self,
408        tool_call: &ToolCall,
409        tool_def: &ToolDefinition,
410        context: &ToolContext,
411    ) -> Result<ToolResult> {
412        (**self)
413            .execute_with_context(tool_call, tool_def, context)
414            .await
415    }
416
417    async fn execute_batch(
418        &self,
419        tool_calls: &[ToolCall],
420        tool_defs: &[ToolDefinition],
421    ) -> Result<Vec<ToolResult>> {
422        (**self).execute_batch(tool_calls, tool_defs).await
423    }
424}
425
426// ============================================================================
427// SessionFileSystem - For session filesystem operations
428// ============================================================================
429
430/// Trait for session filesystem operations
431///
432/// This trait abstracts the session filesystem contract for tools and hosts.
433/// Implementations can:
434/// - Store files in a database (production)
435/// - Use an in-memory filesystem for testing
436/// - Project files onto real disk or object storage
437#[async_trait]
438pub trait SessionFileSystem: Send + Sync {
439    /// Human-facing root path for this filesystem.
440    ///
441    /// `/workspace` remains the stable agent namespace, but embedded runtimes
442    /// backed by a host directory can expose the real root here so shared
443    /// capabilities can avoid misleading users about where files live.
444    fn display_root(&self) -> String {
445        "/workspace".to_string()
446    }
447
448    /// Convert a canonical session path into a human-facing path.
449    fn display_path(&self, path: &str) -> String {
450        workspace_display_path(path)
451    }
452
453    /// Read a file by path
454    async fn read_file(&self, session_id: SessionId, path: &str) -> Result<Option<SessionFile>>;
455
456    /// Write/create a file
457    async fn write_file(
458        &self,
459        session_id: SessionId,
460        path: &str,
461        content: &str,
462        encoding: &str,
463    ) -> Result<SessionFile>;
464
465    /// Write a file only if its current content snapshot still matches.
466    ///
467    /// Implementations backed by transactional storage should override this
468    /// with an atomic compare-and-set update.
469    async fn write_file_if_content_matches(
470        &self,
471        session_id: SessionId,
472        path: &str,
473        expected_content: &str,
474        expected_encoding: &str,
475        content: &str,
476        encoding: &str,
477    ) -> Result<Option<SessionFile>> {
478        let Some(existing) = self.read_file(session_id, path).await? else {
479            return Ok(None);
480        };
481
482        if existing.is_directory {
483            return Ok(None);
484        }
485
486        let current_content = existing.content.unwrap_or_default();
487        if current_content != expected_content || existing.encoding != expected_encoding {
488            return Ok(None);
489        }
490
491        self.write_file(session_id, path, content, encoding)
492            .await
493            .map(Some)
494    }
495
496    /// Delete a file or directory
497    async fn delete_file(&self, session_id: SessionId, path: &str, recursive: bool)
498    -> Result<bool>;
499
500    /// List files in a directory
501    async fn list_directory(&self, session_id: SessionId, path: &str) -> Result<Vec<FileInfo>>;
502
503    /// Get file metadata
504    async fn stat_file(&self, session_id: SessionId, path: &str) -> Result<Option<FileStat>>;
505
506    /// Search files by pattern (grep)
507    async fn grep_files(
508        &self,
509        session_id: SessionId,
510        pattern: &str,
511        path_pattern: Option<&str>,
512    ) -> Result<Vec<GrepMatch>>;
513
514    /// Create a directory
515    async fn create_directory(&self, session_id: SessionId, path: &str) -> Result<FileInfo>;
516
517    /// Seed a starter file into a session workspace.
518    async fn seed_initial_file(&self, session_id: SessionId, file: &InitialFile) -> Result<()> {
519        if file.is_readonly {
520            return Err(crate::error::AgentLoopError::store(
521                "read-only initial files require a SessionFileSystem-specific seed implementation",
522            ));
523        }
524        self.write_file(session_id, &file.path, &file.content, &file.encoding)
525            .await?;
526        Ok(())
527    }
528}
529
530/// A [`SessionFileSystem`] decorator that pins every operation to a fixed
531/// workspace key, ignoring the per-call `session_id`.
532///
533/// Used to re-key file I/O for a session attached to a shared workspace (where
534/// `workspace.id != session.id`): wrap the session's file store once with the
535/// session's `workspace_id`, and all downstream capability/tool access then
536/// addresses the attached workspace rather than the session's own keyspace. For
537/// the default 1:1 session the key equals the session id, so the wrapper is a
538/// transparent pass-through. See `specs/workspace.md`.
539pub struct WorkspaceScopedFileSystem {
540    inner: Arc<dyn SessionFileSystem>,
541    key: SessionId,
542}
543
544impl WorkspaceScopedFileSystem {
545    /// Wrap `inner`, pinning all operations to `workspace_id`'s key.
546    pub fn wrap(
547        inner: Arc<dyn SessionFileSystem>,
548        workspace_id: WorkspaceId,
549    ) -> Arc<dyn SessionFileSystem> {
550        Arc::new(Self {
551            inner,
552            key: SessionId::from_uuid(workspace_id.uuid()),
553        })
554    }
555}
556
557#[async_trait]
558impl SessionFileSystem for WorkspaceScopedFileSystem {
559    async fn read_file(&self, _session_id: SessionId, path: &str) -> Result<Option<SessionFile>> {
560        self.inner.read_file(self.key, path).await
561    }
562    async fn write_file(
563        &self,
564        _session_id: SessionId,
565        path: &str,
566        content: &str,
567        encoding: &str,
568    ) -> Result<SessionFile> {
569        self.inner
570            .write_file(self.key, path, content, encoding)
571            .await
572    }
573    async fn write_file_if_content_matches(
574        &self,
575        _session_id: SessionId,
576        path: &str,
577        expected_content: &str,
578        expected_encoding: &str,
579        content: &str,
580        encoding: &str,
581    ) -> Result<Option<SessionFile>> {
582        self.inner
583            .write_file_if_content_matches(
584                self.key,
585                path,
586                expected_content,
587                expected_encoding,
588                content,
589                encoding,
590            )
591            .await
592    }
593    async fn delete_file(
594        &self,
595        _session_id: SessionId,
596        path: &str,
597        recursive: bool,
598    ) -> Result<bool> {
599        self.inner.delete_file(self.key, path, recursive).await
600    }
601    async fn list_directory(&self, _session_id: SessionId, path: &str) -> Result<Vec<FileInfo>> {
602        self.inner.list_directory(self.key, path).await
603    }
604    async fn stat_file(&self, _session_id: SessionId, path: &str) -> Result<Option<FileStat>> {
605        self.inner.stat_file(self.key, path).await
606    }
607    async fn grep_files(
608        &self,
609        _session_id: SessionId,
610        pattern: &str,
611        path_pattern: Option<&str>,
612    ) -> Result<Vec<GrepMatch>> {
613        self.inner.grep_files(self.key, pattern, path_pattern).await
614    }
615    async fn create_directory(&self, _session_id: SessionId, path: &str) -> Result<FileInfo> {
616        self.inner.create_directory(self.key, path).await
617    }
618    async fn seed_initial_file(&self, _session_id: SessionId, file: &InitialFile) -> Result<()> {
619        self.inner.seed_initial_file(self.key, file).await
620    }
621
622    fn display_root(&self) -> String {
623        self.inner.display_root()
624    }
625
626    fn display_path(&self, path: &str) -> String {
627        self.inner.display_path(path)
628    }
629}
630
631#[async_trait]
632impl<T: SessionFileSystem + ?Sized> SessionFileSystem for std::sync::Arc<T> {
633    fn display_root(&self) -> String {
634        (**self).display_root()
635    }
636
637    fn display_path(&self, path: &str) -> String {
638        (**self).display_path(path)
639    }
640
641    async fn read_file(&self, session_id: SessionId, path: &str) -> Result<Option<SessionFile>> {
642        (**self).read_file(session_id, path).await
643    }
644
645    async fn write_file(
646        &self,
647        session_id: SessionId,
648        path: &str,
649        content: &str,
650        encoding: &str,
651    ) -> Result<SessionFile> {
652        (**self)
653            .write_file(session_id, path, content, encoding)
654            .await
655    }
656
657    async fn write_file_if_content_matches(
658        &self,
659        session_id: SessionId,
660        path: &str,
661        expected_content: &str,
662        expected_encoding: &str,
663        content: &str,
664        encoding: &str,
665    ) -> Result<Option<SessionFile>> {
666        (**self)
667            .write_file_if_content_matches(
668                session_id,
669                path,
670                expected_content,
671                expected_encoding,
672                content,
673                encoding,
674            )
675            .await
676    }
677
678    async fn delete_file(
679        &self,
680        session_id: SessionId,
681        path: &str,
682        recursive: bool,
683    ) -> Result<bool> {
684        (**self).delete_file(session_id, path, recursive).await
685    }
686
687    async fn list_directory(&self, session_id: SessionId, path: &str) -> Result<Vec<FileInfo>> {
688        (**self).list_directory(session_id, path).await
689    }
690
691    async fn stat_file(&self, session_id: SessionId, path: &str) -> Result<Option<FileStat>> {
692        (**self).stat_file(session_id, path).await
693    }
694
695    async fn grep_files(
696        &self,
697        session_id: SessionId,
698        pattern: &str,
699        path_pattern: Option<&str>,
700    ) -> Result<Vec<GrepMatch>> {
701        (**self).grep_files(session_id, pattern, path_pattern).await
702    }
703
704    async fn create_directory(&self, session_id: SessionId, path: &str) -> Result<FileInfo> {
705        (**self).create_directory(session_id, path).await
706    }
707
708    async fn seed_initial_file(&self, session_id: SessionId, file: &InitialFile) -> Result<()> {
709        (**self).seed_initial_file(session_id, file).await
710    }
711}
712
713/// Backward-compatible alias for the old session filesystem trait name.
714pub use SessionFileSystem as SessionFileStore;
715
716/// Host-supplied values used by platform file-system factories.
717///
718/// The context is intentionally type-erased so `everruns-core` can own the
719/// platform contract without depending on server-only types such as
720/// `StorageBackend` or future object-storage clients.
721#[derive(Clone, Default)]
722pub struct SessionFileSystemFactoryContext {
723    values: Arc<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>,
724}
725
726impl SessionFileSystemFactoryContext {
727    pub fn new() -> Self {
728        Self::default()
729    }
730
731    pub fn with<T: Any + Send + Sync>(mut self, value: Arc<T>) -> Self {
732        let values = Arc::make_mut(&mut self.values);
733        values.insert(TypeId::of::<T>(), value);
734        self
735    }
736
737    pub fn get<T: Any + Send + Sync>(&self) -> Option<Arc<T>> {
738        self.values
739            .get(&TypeId::of::<T>())
740            .and_then(|value| value.clone().downcast::<T>().ok())
741    }
742}
743
744/// Factory for deployment-selected session filesystem implementations.
745#[async_trait]
746pub trait SessionFileSystemFactory: Send + Sync {
747    /// Human-readable factory name for diagnostics.
748    fn name(&self) -> &'static str {
749        "SessionFileSystemFactory"
750    }
751
752    /// Whether this factory intentionally leaves filesystem selection to the
753    /// runtime default.
754    fn is_disabled(&self) -> bool {
755        false
756    }
757
758    /// Resolve a live filesystem from host-provided dependencies.
759    async fn create_session_file_system(
760        &self,
761        context: SessionFileSystemFactoryContext,
762    ) -> Result<Arc<dyn SessionFileSystem>>;
763}
764
765/// Default factory used when a platform does not configure session files.
766#[derive(Debug, Clone, Default)]
767pub struct DisabledSessionFileSystemFactory;
768
769#[async_trait]
770impl SessionFileSystemFactory for DisabledSessionFileSystemFactory {
771    fn name(&self) -> &'static str {
772        "DisabledSessionFileSystemFactory"
773    }
774
775    fn is_disabled(&self) -> bool {
776        true
777    }
778
779    async fn create_session_file_system(
780        &self,
781        _context: SessionFileSystemFactoryContext,
782    ) -> Result<Arc<dyn SessionFileSystem>> {
783        Err(crate::error::AgentLoopError::config(
784            "session filesystem is disabled",
785        ))
786    }
787}
788
789// ============================================================================
790// SessionStorageStore - For session key/value and secret storage
791// ============================================================================
792
793/// Info about a stored key (without its value)
794#[derive(Debug, Clone)]
795pub struct KeyInfo {
796    pub key: String,
797    pub created_at: chrono::DateTime<chrono::Utc>,
798    pub updated_at: chrono::DateTime<chrono::Utc>,
799}
800
801/// Info about a stored secret (without its value)
802#[derive(Debug, Clone)]
803pub struct SecretInfo {
804    pub name: String,
805    pub created_at: chrono::DateTime<chrono::Utc>,
806    pub updated_at: chrono::DateTime<chrono::Utc>,
807}
808
809/// Trait for session key/value and secret storage operations
810///
811/// This trait abstracts storage operations for tools that need to persist
812/// data within a session. Implementations can:
813/// - Store data in a database (production)
814/// - Use in-memory storage for testing
815///
816/// A single ranked hit from a Knowledge Base search. Public-id surface only;
817/// no internal UUIDs. See specs/knowledge-bases.md and specs/okf-adoption.md.
818#[derive(Debug, Clone, serde::Serialize)]
819pub struct KnowledgeSearchHit {
820    /// Entry public id (`kbe_…`).
821    pub id: String,
822    /// Owning Knowledge Base public id (`kb_…`).
823    pub kb_id: String,
824    pub title: String,
825    pub kind: String,
826    pub tags: Vec<String>,
827    /// Short body excerpt for the LLM.
828    pub snippet: String,
829    /// Optional OKF resource URI, when set on the entry.
830    pub resource: Option<String>,
831}
832
833/// Org-scoped search over curated Knowledge Bases, backing the agent-facing
834/// `search_knowledge` tool. Implementations MUST scope to `org_id` and silently
835/// ignore KB ids not owned by that org (no existence leak across tenants).
836#[async_trait]
837pub trait KnowledgeStore: Send + Sync {
838    async fn search_knowledge(
839        &self,
840        org_id: crate::typed_id::OrgId,
841        kb_public_ids: &[String],
842        query: &str,
843        kind: Option<&str>,
844        tags: &[String],
845        limit: usize,
846    ) -> Result<Vec<KnowledgeSearchHit>>;
847}
848
849/// Storage for session-scoped key/value pairs and secrets.
850///
851/// Key/value storage is for general data that doesn't need encryption.
852/// Secret storage is for sensitive data that is encrypted at rest.
853#[async_trait]
854pub trait SessionStorageStore: Send + Sync {
855    // Key/Value operations (plain text)
856
857    /// Set a key/value pair (creates or updates)
858    async fn set_value(&self, session_id: SessionId, key: &str, value: &str) -> Result<()>;
859
860    /// Get a value by key
861    async fn get_value(&self, session_id: SessionId, key: &str) -> Result<Option<String>>;
862
863    /// Delete a key/value pair
864    async fn delete_value(&self, session_id: SessionId, key: &str) -> Result<bool>;
865
866    /// List all keys in a session
867    async fn list_keys(&self, session_id: SessionId) -> Result<Vec<KeyInfo>>;
868
869    // Secret operations (encrypted)
870
871    /// Set a secret (creates or updates, value is encrypted before storage)
872    async fn set_secret(&self, session_id: SessionId, name: &str, value: &str) -> Result<()>;
873
874    /// Get a secret by name (value is decrypted before returning)
875    async fn get_secret(&self, session_id: SessionId, name: &str) -> Result<Option<String>>;
876
877    /// Delete a secret
878    async fn delete_secret(&self, session_id: SessionId, name: &str) -> Result<bool>;
879
880    /// List all secret names in a session (without values)
881    async fn list_secrets(&self, session_id: SessionId) -> Result<Vec<SecretInfo>>;
882}
883
884// ============================================================================
885// SessionScheduleStore - For session-scoped schedule operations
886// ============================================================================
887
888use crate::session_schedule::SessionSchedule;
889use crate::typed_id::ScheduleId;
890
891/// Trait for session schedule CRUD operations.
892///
893/// Used by scheduling tools to create, cancel, and list schedules.
894#[async_trait]
895pub trait SessionScheduleStore: Send + Sync {
896    /// Create a new schedule for a session.
897    async fn create_schedule(
898        &self,
899        session_id: SessionId,
900        description: String,
901        cron_expression: Option<String>,
902        scheduled_at: Option<chrono::DateTime<chrono::Utc>>,
903        timezone: String,
904    ) -> Result<SessionSchedule>;
905
906    /// Cancel (disable) a schedule.
907    async fn cancel_schedule(
908        &self,
909        session_id: SessionId,
910        schedule_id: ScheduleId,
911    ) -> Result<SessionSchedule>;
912
913    /// List schedules for a session.
914    async fn list_schedules(&self, session_id: SessionId) -> Result<Vec<SessionSchedule>>;
915
916    /// Count active (enabled) schedules for a session.
917    async fn count_active_schedules(&self, session_id: SessionId) -> Result<u32>;
918
919    /// Count active (enabled) schedules across the whole org this store is
920    /// scoped to. Used to enforce a per-org cap independent of session count:
921    /// `count_active_schedules` only bounds one session, so unlimited sessions
922    /// would otherwise imply unlimited active schedules per org.
923    async fn count_active_org_schedules(&self) -> Result<u32>;
924}
925
926// ============================================================================
927// SessionResourceRegistry - Generic session-scoped resource registry
928// ============================================================================
929
930/// Generic registry of resources active alongside a session.
931///
932/// Capabilities register resources here (sandboxes, subagents, browser sessions).
933/// Agents query it ("what's running?"), infrastructure scans it for cleanup.
934/// See `specs/session-resources.md`.
935#[async_trait]
936pub trait SessionResourceRegistry: Send + Sync {
937    /// Register a resource (or update if resource_id already exists for this session).
938    async fn register(
939        &self,
940        entry: crate::session_resource::RegisterSessionResource,
941    ) -> Result<crate::session_resource::SessionResourceEntry>;
942
943    /// Update the status of a registered resource.
944    async fn update_status(
945        &self,
946        session_id: SessionId,
947        resource_id: &str,
948        status: crate::session_resource::SessionResourceStatus,
949    ) -> Result<Option<crate::session_resource::SessionResourceEntry>>;
950
951    /// Get a specific resource by ID.
952    async fn get(
953        &self,
954        session_id: SessionId,
955        resource_id: &str,
956    ) -> Result<Option<crate::session_resource::SessionResourceEntry>>;
957
958    /// List resources for a session, optionally filtered.
959    async fn list(
960        &self,
961        session_id: SessionId,
962        filter: Option<&crate::session_resource::SessionResourceFilter>,
963    ) -> Result<Vec<crate::session_resource::SessionResourceEntry>>;
964
965    /// Remove a resource from the registry.
966    async fn deregister(&self, session_id: SessionId, resource_id: &str) -> Result<bool>;
967}
968
969// ============================================================================
970// LeasedResourceStore - For lifecycle-managed external resources
971// ============================================================================
972
973/// Trait for session-scoped leased resource operations.
974///
975/// Tools use this store to register or refresh leases when they create or use
976/// external provider resources. Cleanup workers operate through control-plane
977/// storage APIs directly so they can claim work across organizations.
978#[async_trait]
979pub trait LeasedResourceStore: Send + Sync {
980    /// Create or refresh a leased resource for a session.
981    ///
982    /// Implementations must treat this as an idempotent upsert keyed by the
983    /// provider-specific resource identity so repeated tool usage extends the
984    /// same lease instead of creating duplicate rows.
985    async fn upsert_resource(&self, input: UpsertLeasedResource) -> Result<LeasedResource>;
986
987    /// Mark a leased resource as explicitly released.
988    ///
989    /// This is the fast path for explicit user intent such as "close browser"
990    /// or "delete sandbox". It should transition the resource to `released`
991    /// without waiting for the durable cleanup worker to observe lease expiry.
992    async fn release_resource(
993        &self,
994        session_id: SessionId,
995        provider: &str,
996        resource_type: &str,
997        external_id: &str,
998    ) -> Result<Option<LeasedResource>>;
999
1000    /// List leased resources currently associated with a session.
1001    ///
1002    /// Session surfaces use this for visibility. Released resources remain
1003    /// visible so operators can inspect cleanup outcomes and failure history.
1004    async fn list_resources(&self, session_id: SessionId) -> Result<Vec<LeasedResource>>;
1005}
1006
1007// ============================================================================
1008// ToolContext - Runtime context for tool execution
1009// ============================================================================
1010
1011/// Type alias for the session SQL DB store trait object.
1012pub type SessionSqlDbStoreRef = Arc<dyn crate::session_sqldb::SessionSqlDbStore>;
1013
1014/// Resolves user connection tokens (e.g. GitHub) lazily at tool execution time.
1015///
1016/// Instead of eagerly injecting tokens at session creation, tools call this
1017/// resolver when they need a token. If the user hasn't connected, returns None.
1018#[async_trait]
1019pub trait UserConnectionResolver: Send + Sync {
1020    /// Get a decrypted connection token for the given provider.
1021    /// Returns None if the user has no connection for this provider.
1022    async fn get_connection_token(
1023        &self,
1024        session_id: SessionId,
1025        provider: &str,
1026    ) -> Result<Option<String>>;
1027
1028    /// Resolve the user ID of the connection used for a session/provider pair.
1029    ///
1030    /// This is used by leased resources to bind cleanup to the same provider
1031    /// identity that created the remote resource.
1032    async fn get_connection_user(
1033        &self,
1034        _session_id: SessionId,
1035        _provider: &str,
1036    ) -> Result<Option<Uuid>> {
1037        Ok(None)
1038    }
1039
1040    /// Resolve a provider token for a specific user.
1041    ///
1042    /// Cleanup workers use this to avoid "first org member wins" behavior when
1043    /// cleaning resources created by a specific provider connection owner.
1044    async fn get_connection_token_for_user(
1045        &self,
1046        _user_id: Uuid,
1047        _provider: &str,
1048    ) -> Result<Option<String>> {
1049        Ok(None)
1050    }
1051
1052    /// Get provider-specific metadata stored alongside the connection.
1053    /// Returns None if no metadata is stored or no connection exists.
1054    async fn get_connection_metadata(
1055        &self,
1056        _session_id: SessionId,
1057        _provider: &str,
1058    ) -> Result<Option<serde_json::Value>> {
1059        Ok(None)
1060    }
1061}
1062
1063// ============================================================================
1064// BudgetChecker - For querying budget status from tools
1065// ============================================================================
1066
1067/// Trait for checking budget status from within tool execution.
1068///
1069/// Implemented by gRPC adapters (worker → server) and direct adapters (in-process).
1070/// Used by the `check_budget` tool to return real budget data to agents.
1071/// The org_id is captured at construction time by the implementing adapter.
1072#[async_trait]
1073pub trait BudgetChecker: Send + Sync {
1074    /// Check all budgets for a session and return a tool-friendly response.
1075    async fn check_budgets(&self, session_id: &str) -> Result<crate::budget::BudgetToolResponse>;
1076}
1077
1078// ============================================================================
1079// PaymentAuthority - For capability-internal machine payments
1080// ============================================================================
1081
1082/// Internal authority for paid capability operations.
1083///
1084/// Capabilities call this with fixed, typed requests. The model never receives a
1085/// generic paid HTTP tool, wallet credentials, or payment payloads.
1086#[async_trait]
1087pub trait PaymentAuthority: Send + Sync {
1088    async fn execute_machine_payment(
1089        &self,
1090        session_id: SessionId,
1091        request: crate::payment::MachinePaymentRequest,
1092    ) -> Result<crate::payment::MachinePaymentResponse>;
1093}
1094
1095// OutboundToolRateLimiter - Per-org outbound tool-call rate limiting (TM-TOOL-009)
1096// ============================================================================
1097
1098/// Per-org gate on outbound tool execution.
1099///
1100/// Returns `true` if the call is within the per-org budget, `false` if the
1101/// org has exceeded its outbound tool rate limit for this window.
1102/// Implementations must be fail-open: Valkey/backend errors should return `true`
1103/// rather than blocking legitimate tool calls.
1104#[async_trait]
1105pub trait OutboundToolRateLimiter: Send + Sync {
1106    /// Key by the public org UUID (keyed string representation).
1107    async fn check_org(&self, org_id: &crate::typed_id::OrgId) -> bool;
1108}
1109
1110// ============================================================================
1111// DurableToolResultStore — per-tool-call idempotency (EVE-530)
1112// ============================================================================
1113
1114/// Result of a claim attempt on the per-tool-call idempotency store.
1115#[derive(Debug)]
1116pub enum ToolCallClaimResult {
1117    /// First claim for this (turn_id, tool_call_id); caller should execute the tool.
1118    /// `claim_token` must be passed to `settle_tool_call` to verify ownership.
1119    Claimed { claim_token: uuid::Uuid },
1120    /// A prior run already settled this call; replay the stored result.
1121    AlreadySettled {
1122        result_json: serde_json::Value,
1123        args_fingerprint: String,
1124    },
1125    /// A prior run started but never settled. For `AtMostOnce` tools the
1126    /// caller should NOT re-execute; for `Pure`/`Idempotent` tools the caller
1127    /// may re-execute and then try to settle (the settle CAS will be a no-op if
1128    /// a different claimer wins first).
1129    AlreadyRunning { args_fingerprint: String },
1130    /// A settled row exists but its `args_fingerprint` does not match the
1131    /// current call — this is a determinism violation (workflow replay with
1132    /// different inputs). The workflow should be failed loudly.
1133    DeterminismViolation {
1134        stored_fingerprint: String,
1135        current_fingerprint: String,
1136    },
1137}
1138
1139/// Read-only status of a tool call in durable storage (EVE-533).
1140#[derive(Debug, Clone)]
1141pub enum DurableToolCallStatus {
1142    /// Tool completed successfully or with an error; result is stored.
1143    Settled { result_json: serde_json::Value },
1144    /// Tool was settled with `interrupted` status; result may contain error details.
1145    Interrupted {
1146        result_json: Option<serde_json::Value>,
1147    },
1148    /// A claim exists but the tool never finished.
1149    Running,
1150}
1151
1152/// Durable per-tool-call idempotency store (EVE-530).
1153///
1154/// Implements the claim/settle CAS that prevents double-execution of
1155/// `AtMostOnce` tools on worker reclaim/replay.
1156#[async_trait]
1157pub trait DurableToolResultStore: Send + Sync + 'static {
1158    /// Atomically claim `(turn_id, tool_call_id)` before tool dispatch.
1159    ///
1160    /// - Inserts a `running` row if none exists → `Claimed`.
1161    /// - Finds an existing `settled` row → `AlreadySettled`.
1162    /// - Finds an existing `running` row → `AlreadyRunning`.
1163    /// - Finds a `settled` row with a mismatched `args_fingerprint`
1164    ///   (determinism violation) → `DeterminismViolation`.
1165    async fn try_claim_tool_call(
1166        &self,
1167        turn_id: &str,
1168        tool_call_id: &str,
1169        tool_name: &str,
1170        args_fingerprint: &str,
1171    ) -> Result<ToolCallClaimResult>;
1172
1173    /// Settle a previously claimed tool call with its result.
1174    ///
1175    /// `claim_token` must match the token returned by `try_claim_tool_call`.
1176    /// Returns `Ok(true)` if the row was updated, `Ok(false)` if the claim
1177    /// token no longer matches (ownership lost — treat as a warning).
1178    async fn settle_tool_call(
1179        &self,
1180        turn_id: &str,
1181        tool_call_id: &str,
1182        result_json: serde_json::Value,
1183        status: &str,
1184        claim_token: uuid::Uuid,
1185    ) -> Result<bool>;
1186
1187    /// Read-only lookup of a tool call's current status in durable storage (EVE-533).
1188    ///
1189    /// Used by transcript repair to decide whether to replay a stored result or
1190    /// synthesize an interrupted placeholder. Returns `None` if no row exists.
1191    async fn get_tool_call_status(
1192        &self,
1193        turn_id: &str,
1194        tool_call_id: &str,
1195    ) -> Result<Option<DurableToolCallStatus>>;
1196}
1197
1198/// No-op implementation — used when no durable store is configured (dev/test).
1199/// Every call is treated as a fresh first execution; no replay or ownership checks.
1200pub struct NoopDurableToolResultStore;
1201
1202#[async_trait]
1203impl DurableToolResultStore for NoopDurableToolResultStore {
1204    async fn try_claim_tool_call(
1205        &self,
1206        _turn_id: &str,
1207        _tool_call_id: &str,
1208        _tool_name: &str,
1209        _args_fingerprint: &str,
1210    ) -> Result<ToolCallClaimResult> {
1211        Ok(ToolCallClaimResult::Claimed {
1212            claim_token: uuid::Uuid::new_v4(),
1213        })
1214    }
1215
1216    async fn settle_tool_call(
1217        &self,
1218        _turn_id: &str,
1219        _tool_call_id: &str,
1220        _result_json: serde_json::Value,
1221        _status: &str,
1222        _claim_token: uuid::Uuid,
1223    ) -> Result<bool> {
1224        Ok(true)
1225    }
1226
1227    async fn get_tool_call_status(
1228        &self,
1229        _turn_id: &str,
1230        _tool_call_id: &str,
1231    ) -> Result<Option<DurableToolCallStatus>> {
1232        Ok(None)
1233    }
1234}
1235
1236// ============================================================================
1237// StreamHeartbeater — per-stream liveness signal for Reason activity (EVE-531)
1238// ============================================================================
1239
1240/// Progress snapshot carried in each stream heartbeat.
1241#[derive(Debug, Clone)]
1242pub struct StreamProgress {
1243    /// Accumulated text + thinking length (characters) at the time of heartbeat.
1244    pub accumulated_len: usize,
1245    /// Wall-clock time of the most recent received token (Unix seconds).
1246    pub last_delta_at: u64,
1247}
1248
1249/// Heartbeater the Reason streaming loop calls on delta batches and a keepalive
1250/// timer, signalling that the provider connection is alive.
1251///
1252/// Implementations bridge to the durable-execution layer (e.g. gRPC).
1253/// The no-op is used in dev/test where no durable store is present.
1254#[async_trait]
1255pub trait StreamHeartbeater: Send + Sync {
1256    /// Signal stream liveness with current progress.
1257    ///
1258    /// Must be best-effort: errors must not propagate to the caller.
1259    /// Cancel-safety is critical — if the worker dies the heartbeat stops
1260    /// and the existing task-level reclaim takes over.
1261    async fn heartbeat(&self, progress: StreamProgress);
1262}
1263
1264/// No-op heartbeater — treats every stream as perpetually alive (dev/test).
1265pub struct NoopStreamHeartbeater;
1266
1267#[async_trait]
1268impl StreamHeartbeater for NoopStreamHeartbeater {
1269    async fn heartbeat(&self, _progress: StreamProgress) {}
1270}
1271
1272// ============================================================================
1273// PartialStreamStore — partial-stream recovery for Reason activity (EVE-532)
1274// ============================================================================
1275
1276/// State of a partially-streamed assistant message detected in the event log.
1277#[derive(Debug, Clone)]
1278pub struct PartialStreamState {
1279    /// Accumulated text from the last `output.message.delta` for the turn.
1280    /// Empty when `output.message.started` was emitted but no delta arrived.
1281    pub accumulated: String,
1282}
1283
1284/// Consults the persisted event log to detect whether a `reason` activity
1285/// was interrupted after `output.message.started` but before
1286/// `output.message.completed` or `output.message.replaced`.
1287///
1288/// Used by `ReasonAtom` on re-entry to apply the ContinuePartial recovery
1289/// policy (EVE-532): finalize the partial text without a second provider call,
1290/// or restart clean if the partial is unusable.
1291#[async_trait]
1292pub trait PartialStreamStore: Send + Sync {
1293    /// Return the partial-stream state for `(session_id, turn_id)` if an
1294    /// in-flight assistant message exists (started but not completed).
1295    async fn get_partial_stream(
1296        &self,
1297        session_id: SessionId,
1298        turn_id: &str,
1299    ) -> Result<Option<PartialStreamState>>;
1300}
1301
1302/// No-op — always reports no partial stream (dev/test / in-memory mode).
1303pub struct NoopPartialStreamStore;
1304
1305#[async_trait]
1306impl PartialStreamStore for NoopPartialStreamStore {
1307    async fn get_partial_stream(
1308        &self,
1309        _session_id: SessionId,
1310        _turn_id: &str,
1311    ) -> Result<Option<PartialStreamState>> {
1312        Ok(None)
1313    }
1314}
1315
1316/// Runtime context provided to tools during execution.
1317///
1318/// This context contains:
1319/// - Session ID for scoping operations
1320/// - Optional stores for tools that need external access
1321///
1322/// Tools that need context-aware execution (like filesystem tools) can use
1323/// the `execute_with_context` method on the Tool trait.
1324#[derive(Clone)]
1325pub struct ToolContext {
1326    /// The session ID for the current execution
1327    pub session_id: SessionId,
1328    /// The workspace this session is attached to — the key for the virtual
1329    /// file store. For the default 1:1 session this equals
1330    /// `WorkspaceId::from_uuid(session_id.uuid())`; for a shared workspace it
1331    /// differs. File-system tools MUST key by this (via `workspace_fs_key`)
1332    /// rather than `session_id` so shared-workspace sessions read/write the
1333    /// attached workspace's files. See specs/workspace.md.
1334    pub workspace_id: WorkspaceId,
1335
1336    /// Optional file store for filesystem operations
1337    pub file_store: Option<Arc<dyn SessionFileSystem>>,
1338
1339    /// Optional storage store for key/value and secret storage
1340    pub storage_store: Option<Arc<dyn SessionStorageStore>>,
1341
1342    /// Optional durable image artifact store for tool-side media persistence.
1343    pub image_store: Option<Arc<dyn ImageArtifactStore>>,
1344
1345    /// Optional provider credential store for tool-side API clients.
1346    pub provider_credential_store: Option<Arc<dyn ProviderCredentialStore>>,
1347
1348    /// Optional system utility LLM service for capability internals.
1349    pub utility_llm_service: Option<Arc<dyn crate::UtilityLlmService>>,
1350
1351    /// Optional scoped-MCP tool invoker for capability internals that need to
1352    /// call an MCP server out-of-band (e.g. the guardrails `mcp` check
1353    /// delegating a decision to an external guardrail endpoint). The invoker
1354    /// resolves connections and credentials per the current session/org, so
1355    /// tenant scoping is enforced by the host that supplies it.
1356    pub mcp_invoker: Option<Arc<dyn crate::McpToolInvoker>>,
1357
1358    /// Optional outbound egress service for HTTP/API traffic.
1359    pub egress_service: Option<Arc<dyn crate::EgressService>>,
1360
1361    /// Optional session SQL database store
1362    pub sqldb_store: Option<SessionSqlDbStoreRef>,
1363
1364    /// Optional message retriever for tools that need conversation history access
1365    pub message_retriever: Option<Arc<dyn crate::message_retriever::MessageRetriever>>,
1366
1367    /// Optional session store for tools that need session metadata access.
1368    pub session_store: Option<Arc<dyn SessionStore>>,
1369
1370    /// Optional session mutator for tools that need to update session metadata.
1371    pub session_mutator: Option<Arc<dyn SessionMutator>>,
1372
1373    /// Optional agent store for tools that need agent metadata access.
1374    pub agent_store: Option<Arc<dyn AgentStore>>,
1375
1376    /// Optional resolver for user connection tokens (lazy GitHub token lookup, etc.)
1377    pub connection_resolver: Option<Arc<dyn UserConnectionResolver>>,
1378
1379    /// Optional session schedule store for scheduling tools.
1380    pub schedule_store: Option<Arc<dyn SessionScheduleStore>>,
1381
1382    /// Optional platform store for org-level management tools.
1383    pub platform_store: Option<Arc<dyn crate::platform_store::PlatformStore>>,
1384    /// Optional knowledge store backing the `search_knowledge` tool.
1385    pub knowledge_store: Option<Arc<dyn KnowledgeStore>>,
1386
1387    /// Optional hybrid retrieval over bound Knowledge Indexes for the
1388    /// `search_index` tool. Server-implemented; populated only on the server
1389    /// act path alongside `platform_store` / `connection_resolver`.
1390    pub knowledge_index_search: Option<Arc<dyn crate::vector_store::KnowledgeIndexSearch>>,
1391
1392    /// Optional leased resource store for lifecycle-managed provider resources.
1393    pub leased_resource_store: Option<Arc<dyn LeasedResourceStore>>,
1394
1395    /// Optional session resource registry — generic registry of active resources.
1396    pub session_resource_registry: Option<Arc<dyn SessionResourceRegistry>>,
1397
1398    /// Optional session task registry — background work owned by the session
1399    /// (specs/session-tasks.md).
1400    pub session_task_registry: Option<Arc<dyn crate::session_task::SessionTaskRegistry>>,
1401
1402    /// Optional event emitter for tools that need to stream progress updates.
1403    /// When set, tools can emit `tool.progress` events during execution.
1404    pub event_emitter: Option<Arc<dyn EventEmitter>>,
1405
1406    /// Event context for correlating progress events with the current tool call.
1407    /// Set by ActAtom when constructing the ToolContext.
1408    pub event_context: Option<crate::events::EventContext>,
1409
1410    /// The tool call ID for the current execution (set by ActAtom).
1411    /// Used by tools to emit correlated progress events.
1412    pub tool_call_id: Option<String>,
1413    /// Optional capability registry for blueprint lookups.
1414    pub capability_registry: Option<crate::capabilities::CapabilityRegistry>,
1415
1416    /// Optional registry of active built-in tools for meta-tools such as
1417    /// `spawn_background` that need to inspect or delegate to sibling tools.
1418    pub tool_registry: Option<Arc<crate::tools::ToolRegistry>>,
1419
1420    /// Optional allowlist of tools visible to the model for this turn.
1421    /// Registry-introspecting tools must filter through this before returning
1422    /// sibling tool metadata, because the execution registry can be a superset.
1423    pub visible_tool_names: Option<Arc<HashSet<String>>>,
1424
1425    /// Optional org ID for org-scoped operations.
1426    pub org_id: Option<crate::typed_id::OrgId>,
1427
1428    /// Merged network access list (harness ∩ agent ∩ session).
1429    /// When set, tools that make HTTP requests must check URLs against this list.
1430    pub network_access: Option<crate::network_access::NetworkAccessList>,
1431
1432    /// Resolved locale for localized tool behavior (BCP 47, e.g. `uk-UA`).
1433    /// When set, tools that support localization use this to produce
1434    /// locale-appropriate descriptions, error messages, and prompts.
1435    pub locale: Option<String>,
1436
1437    /// Optional budget checker for the check_budget tool.
1438    pub budget_checker: Option<Arc<dyn BudgetChecker>>,
1439
1440    /// Optional internal payment authority for paid capability tools.
1441    pub payment_authority: Option<Arc<dyn PaymentAuthority>>,
1442
1443    /// Optional durable spawn handle store for subagent reattach (EVE-535).
1444    /// When set, `spawn_subagent` uses claim/settle to prevent duplicate spawning
1445    /// on parent worker reclaim.
1446    pub subagent_spawn_store: Option<Arc<dyn SubagentSpawnStore>>,
1447
1448    /// Optional live reasoning-effort handle (EVE-595). When set, a tool can
1449    /// change the reasoning effort mid-turn; subsequent LLM steps in the same
1450    /// `run_turn` re-read it and use the new effort.
1451    pub reasoning_effort_handle: Option<ReasoningEffortHandle>,
1452}
1453
1454impl ToolContext {
1455    /// The virtual-file-store key for this execution, derived from the attached
1456    /// workspace. Carried through the `SessionFileSystem` trait's `SessionId`
1457    /// parameter (the store keys by `.uuid()`), so a shared-workspace session
1458    /// addresses the workspace's files rather than its own session-id keyspace.
1459    pub fn workspace_fs_key(&self) -> SessionId {
1460        SessionId::from_uuid(self.workspace_id.uuid())
1461    }
1462
1463    /// Override the attached workspace (default is the 1:1 session-derived id).
1464    pub fn with_workspace_id(mut self, workspace_id: WorkspaceId) -> Self {
1465        self.workspace_id = workspace_id;
1466        self
1467    }
1468
1469    /// Create a new tool context with just a session ID
1470    pub fn new(session_id: SessionId) -> Self {
1471        Self {
1472            session_id,
1473            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
1474            file_store: None,
1475            storage_store: None,
1476            image_store: None,
1477            provider_credential_store: None,
1478            utility_llm_service: None,
1479            mcp_invoker: None,
1480            egress_service: None,
1481            sqldb_store: None,
1482            message_retriever: None,
1483            session_store: None,
1484            session_mutator: None,
1485            agent_store: None,
1486            connection_resolver: None,
1487            schedule_store: None,
1488            platform_store: None,
1489            knowledge_store: None,
1490            knowledge_index_search: None,
1491            leased_resource_store: None,
1492            session_resource_registry: None,
1493            session_task_registry: None,
1494            event_emitter: None,
1495            event_context: None,
1496            tool_call_id: None,
1497            capability_registry: None,
1498            tool_registry: None,
1499            visible_tool_names: None,
1500            org_id: None,
1501            network_access: None,
1502            locale: None,
1503            budget_checker: None,
1504            payment_authority: None,
1505            subagent_spawn_store: None,
1506            reasoning_effort_handle: None,
1507        }
1508    }
1509
1510    /// Create a context with a file store
1511    pub fn with_file_store(session_id: SessionId, file_store: Arc<dyn SessionFileSystem>) -> Self {
1512        Self {
1513            session_id,
1514            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
1515            file_store: Some(file_store),
1516            storage_store: None,
1517            image_store: None,
1518            provider_credential_store: None,
1519            utility_llm_service: None,
1520            mcp_invoker: None,
1521            egress_service: None,
1522            sqldb_store: None,
1523            message_retriever: None,
1524            session_store: None,
1525            session_mutator: None,
1526            agent_store: None,
1527            connection_resolver: None,
1528            schedule_store: None,
1529            platform_store: None,
1530            knowledge_store: None,
1531            knowledge_index_search: None,
1532            leased_resource_store: None,
1533            session_resource_registry: None,
1534            session_task_registry: None,
1535            event_emitter: None,
1536            event_context: None,
1537            tool_call_id: None,
1538            capability_registry: None,
1539            tool_registry: None,
1540            visible_tool_names: None,
1541            org_id: None,
1542            network_access: None,
1543            locale: None,
1544            budget_checker: None,
1545            payment_authority: None,
1546            subagent_spawn_store: None,
1547            reasoning_effort_handle: None,
1548        }
1549    }
1550
1551    /// Create a context with a storage store
1552    pub fn with_storage_store(
1553        session_id: SessionId,
1554        storage_store: Arc<dyn SessionStorageStore>,
1555    ) -> Self {
1556        Self {
1557            session_id,
1558            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
1559            file_store: None,
1560            storage_store: Some(storage_store),
1561            image_store: None,
1562            provider_credential_store: None,
1563            utility_llm_service: None,
1564            mcp_invoker: None,
1565            egress_service: None,
1566            sqldb_store: None,
1567            message_retriever: None,
1568            session_store: None,
1569            session_mutator: None,
1570            agent_store: None,
1571            connection_resolver: None,
1572            schedule_store: None,
1573            platform_store: None,
1574            knowledge_store: None,
1575            knowledge_index_search: None,
1576            leased_resource_store: None,
1577            session_resource_registry: None,
1578            session_task_registry: None,
1579            event_emitter: None,
1580            event_context: None,
1581            tool_call_id: None,
1582            capability_registry: None,
1583            tool_registry: None,
1584            visible_tool_names: None,
1585            org_id: None,
1586            network_access: None,
1587            locale: None,
1588            budget_checker: None,
1589            payment_authority: None,
1590            subagent_spawn_store: None,
1591            reasoning_effort_handle: None,
1592        }
1593    }
1594
1595    /// Create a context with both file store and storage store
1596    pub fn with_stores(
1597        session_id: SessionId,
1598        file_store: Arc<dyn SessionFileSystem>,
1599        storage_store: Arc<dyn SessionStorageStore>,
1600    ) -> Self {
1601        Self {
1602            session_id,
1603            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
1604            file_store: Some(file_store),
1605            storage_store: Some(storage_store),
1606            sqldb_store: None,
1607            image_store: None,
1608            provider_credential_store: None,
1609            utility_llm_service: None,
1610            mcp_invoker: None,
1611            egress_service: None,
1612            message_retriever: None,
1613            session_store: None,
1614            session_mutator: None,
1615            agent_store: None,
1616            connection_resolver: None,
1617            schedule_store: None,
1618            platform_store: None,
1619            knowledge_store: None,
1620            knowledge_index_search: None,
1621            leased_resource_store: None,
1622            session_resource_registry: None,
1623            session_task_registry: None,
1624            event_emitter: None,
1625            event_context: None,
1626            tool_call_id: None,
1627            capability_registry: None,
1628            tool_registry: None,
1629            visible_tool_names: None,
1630            org_id: None,
1631            network_access: None,
1632            locale: None,
1633            budget_checker: None,
1634            payment_authority: None,
1635            subagent_spawn_store: None,
1636            reasoning_effort_handle: None,
1637        }
1638    }
1639
1640    /// Add a SQL database store to this context
1641    pub fn with_sqldb_store(mut self, sqldb_store: SessionSqlDbStoreRef) -> Self {
1642        self.sqldb_store = Some(sqldb_store);
1643        self
1644    }
1645
1646    /// Add a message retriever to this context
1647    pub fn with_message_retriever(
1648        mut self,
1649        retriever: Arc<dyn crate::message_retriever::MessageRetriever>,
1650    ) -> Self {
1651        self.message_retriever = Some(retriever);
1652        self
1653    }
1654
1655    /// Add a session store to this context.
1656    pub fn with_session_store(mut self, store: Arc<dyn SessionStore>) -> Self {
1657        self.session_store = Some(store);
1658        self
1659    }
1660
1661    /// Add a session mutator to this context.
1662    pub fn with_session_mutator(mut self, mutator: Arc<dyn SessionMutator>) -> Self {
1663        self.session_mutator = Some(mutator);
1664        self
1665    }
1666
1667    /// Add a live reasoning-effort handle (EVE-595). Tools can call
1668    /// [`ReasoningEffortHandle::set`] on it to change the effort used by
1669    /// subsequent LLM steps within the same turn.
1670    pub fn with_reasoning_effort_handle(mut self, handle: ReasoningEffortHandle) -> Self {
1671        self.reasoning_effort_handle = Some(handle);
1672        self
1673    }
1674
1675    /// Add an agent store to this context.
1676    pub fn with_agent_store(mut self, store: Arc<dyn AgentStore>) -> Self {
1677        self.agent_store = Some(store);
1678        self
1679    }
1680
1681    /// Add a connection resolver to this context
1682    pub fn with_connection_resolver(mut self, resolver: Arc<dyn UserConnectionResolver>) -> Self {
1683        self.connection_resolver = Some(resolver);
1684        self
1685    }
1686
1687    /// Create a context with an image artifact store.
1688    pub fn with_image_store(
1689        session_id: SessionId,
1690        image_store: Arc<dyn ImageArtifactStore>,
1691    ) -> Self {
1692        Self {
1693            session_id,
1694            workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
1695            file_store: None,
1696            storage_store: None,
1697            image_store: Some(image_store),
1698            provider_credential_store: None,
1699            utility_llm_service: None,
1700            mcp_invoker: None,
1701            egress_service: None,
1702            sqldb_store: None,
1703            message_retriever: None,
1704            session_store: None,
1705            session_mutator: None,
1706            agent_store: None,
1707            connection_resolver: None,
1708            schedule_store: None,
1709            platform_store: None,
1710            knowledge_store: None,
1711            knowledge_index_search: None,
1712            leased_resource_store: None,
1713            session_resource_registry: None,
1714            session_task_registry: None,
1715            event_emitter: None,
1716            event_context: None,
1717            tool_call_id: None,
1718            capability_registry: None,
1719            tool_registry: None,
1720            visible_tool_names: None,
1721            org_id: None,
1722            network_access: None,
1723            locale: None,
1724            budget_checker: None,
1725            payment_authority: None,
1726            subagent_spawn_store: None,
1727            reasoning_effort_handle: None,
1728        }
1729    }
1730
1731    /// Set the provider credential store on this context.
1732    pub fn with_provider_credential_store(
1733        mut self,
1734        store: Arc<dyn ProviderCredentialStore>,
1735    ) -> Self {
1736        self.provider_credential_store = Some(store);
1737        self
1738    }
1739
1740    /// Set the utility LLM service on this context.
1741    pub fn with_utility_llm_service(mut self, service: Arc<dyn crate::UtilityLlmService>) -> Self {
1742        self.utility_llm_service = Some(service);
1743        self
1744    }
1745
1746    /// Set the scoped-MCP tool invoker on this context.
1747    pub fn with_mcp_invoker(mut self, invoker: Arc<dyn crate::McpToolInvoker>) -> Self {
1748        self.mcp_invoker = Some(invoker);
1749        self
1750    }
1751
1752    /// Set the outbound egress service on this context.
1753    pub fn with_egress_service(mut self, service: Arc<dyn crate::EgressService>) -> Self {
1754        self.egress_service = Some(service);
1755        self
1756    }
1757
1758    /// Set the outbound egress service on this context when available.
1759    /// Preserves any already-set service when `service` is `None`.
1760    pub fn with_egress_service_opt(
1761        mut self,
1762        service: Option<Arc<dyn crate::EgressService>>,
1763    ) -> Self {
1764        if let Some(service) = service {
1765            self.egress_service = Some(service);
1766        }
1767        self
1768    }
1769
1770    /// Set the session storage store on this context (builder method).
1771    pub fn with_storage_store_arc(mut self, store: Arc<dyn SessionStorageStore>) -> Self {
1772        self.storage_store = Some(store);
1773        self
1774    }
1775
1776    /// Add a session schedule store to this context.
1777    pub fn with_schedule_store(mut self, store: Arc<dyn SessionScheduleStore>) -> Self {
1778        self.schedule_store = Some(store);
1779        self
1780    }
1781
1782    /// Add a platform store to this context.
1783    pub fn with_platform_store(
1784        mut self,
1785        store: Arc<dyn crate::platform_store::PlatformStore>,
1786    ) -> Self {
1787        self.platform_store = Some(store);
1788        self
1789    }
1790
1791    /// Add a Knowledge Index search service to this context (for `search_index`).
1792    pub fn with_knowledge_index_search(
1793        mut self,
1794        search: Arc<dyn crate::vector_store::KnowledgeIndexSearch>,
1795    ) -> Self {
1796        self.knowledge_index_search = Some(search);
1797        self
1798    }
1799
1800    /// Add a leased resource store to this context.
1801    pub fn with_leased_resource_store(mut self, store: Arc<dyn LeasedResourceStore>) -> Self {
1802        self.leased_resource_store = Some(store);
1803        self
1804    }
1805
1806    /// Add a session resource registry to this context.
1807    pub fn with_session_resource_registry(
1808        mut self,
1809        registry: Arc<dyn SessionResourceRegistry>,
1810    ) -> Self {
1811        self.session_resource_registry = Some(registry);
1812        self
1813    }
1814
1815    /// Add a session task registry to this context.
1816    pub fn with_session_task_registry(
1817        mut self,
1818        registry: Arc<dyn crate::session_task::SessionTaskRegistry>,
1819    ) -> Self {
1820        self.session_task_registry = Some(registry);
1821        self
1822    }
1823
1824    /// Set org ID for org-scoped operations.
1825    pub fn with_org_id(mut self, org_id: crate::typed_id::OrgId) -> Self {
1826        self.org_id = Some(org_id);
1827        self
1828    }
1829
1830    /// Set the active built-in tool registry on this context.
1831    pub fn with_tool_registry(mut self, registry: Arc<crate::tools::ToolRegistry>) -> Self {
1832        self.tool_registry = Some(registry);
1833        self
1834    }
1835
1836    /// Set the tool names visible to the model in this turn.
1837    pub fn with_visible_tool_names(mut self, names: Arc<HashSet<String>>) -> Self {
1838        self.visible_tool_names = Some(names);
1839        self
1840    }
1841
1842    /// Set the merged network access list for URL filtering.
1843    pub fn with_network_access(
1844        mut self,
1845        network_access: Option<crate::network_access::NetworkAccessList>,
1846    ) -> Self {
1847        self.network_access = network_access;
1848        self
1849    }
1850
1851    /// Set the internal payment authority for paid capability operations.
1852    pub fn with_payment_authority(mut self, authority: Arc<dyn PaymentAuthority>) -> Self {
1853        self.payment_authority = Some(authority);
1854        self
1855    }
1856
1857    /// Set the durable subagent spawn handle store (EVE-535).
1858    pub fn with_subagent_spawn_store(mut self, store: Arc<dyn SubagentSpawnStore>) -> Self {
1859        self.subagent_spawn_store = Some(store);
1860        self
1861    }
1862
1863    /// Emit a `tool.progress` event if an event emitter and context are available.
1864    ///
1865    /// This is a best-effort helper: failures are logged but not propagated,
1866    /// so tools never fail just because a progress event couldn't be sent.
1867    pub async fn emit_progress(&self, tool_name: &str, message: &str) {
1868        let (Some(emitter), Some(ctx), Some(call_id)) =
1869            (&self.event_emitter, &self.event_context, &self.tool_call_id)
1870        else {
1871            return;
1872        };
1873        if let Err(e) = emitter
1874            .emit(EventRequest::new(
1875                self.session_id,
1876                ctx.clone(),
1877                crate::events::ToolProgressData {
1878                    tool_call_id: call_id.clone(),
1879                    tool_name: tool_name.to_string(),
1880                    message: message.to_string(),
1881                    display_name: None,
1882                },
1883            ))
1884            .await
1885        {
1886            tracing::debug!(
1887                tool_call_id = call_id,
1888                tool_name,
1889                error = %e,
1890                "Failed to emit tool.progress event"
1891            );
1892        }
1893    }
1894
1895    /// Emit a `tool.output.delta` event if an event emitter and context are available.
1896    ///
1897    /// Streams incremental output chunks (e.g., stdout/stderr lines) for live
1898    /// rendering in UI and CLI. Best-effort: failures are logged, not propagated.
1899    pub async fn emit_tool_output(&self, tool_name: &str, delta: &str, stream: &str) {
1900        let (Some(emitter), Some(ctx), Some(call_id)) =
1901            (&self.event_emitter, &self.event_context, &self.tool_call_id)
1902        else {
1903            return;
1904        };
1905        if let Err(e) = emitter
1906            .emit(EventRequest::new(
1907                self.session_id,
1908                ctx.clone(),
1909                crate::events::ToolOutputDeltaData {
1910                    tool_call_id: call_id.clone(),
1911                    tool_name: tool_name.to_string(),
1912                    delta: delta.to_string(),
1913                    stream: stream.to_string(),
1914                },
1915            ))
1916            .await
1917        {
1918            tracing::debug!(
1919                tool_call_id = call_id,
1920                tool_name,
1921                error = %e,
1922                "Failed to emit tool.output.delta event"
1923            );
1924        }
1925    }
1926}
1927
1928impl std::fmt::Debug for ToolContext {
1929    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1930        f.debug_struct("ToolContext")
1931            .field("session_id", &self.session_id)
1932            .field("file_store", &self.file_store.is_some())
1933            .field("storage_store", &self.storage_store.is_some())
1934            .field("image_store", &self.image_store.is_some())
1935            .field(
1936                "provider_credential_store",
1937                &self.provider_credential_store.is_some(),
1938            )
1939            .field("utility_llm_service", &self.utility_llm_service.is_some())
1940            .field("egress_service", &self.egress_service.is_some())
1941            .field("sqldb_store", &self.sqldb_store.is_some())
1942            .field("message_retriever", &self.message_retriever.is_some())
1943            .field("session_store", &self.session_store.is_some())
1944            .field("session_mutator", &self.session_mutator.is_some())
1945            .field("agent_store", &self.agent_store.is_some())
1946            .field("connection_resolver", &self.connection_resolver.is_some())
1947            .field("schedule_store", &self.schedule_store.is_some())
1948            .field("platform_store", &self.platform_store.is_some())
1949            .field(
1950                "knowledge_index_search",
1951                &self.knowledge_index_search.is_some(),
1952            )
1953            .field(
1954                "leased_resource_store",
1955                &self.leased_resource_store.is_some(),
1956            )
1957            .field("event_emitter", &self.event_emitter.is_some())
1958            .field("tool_registry", &self.tool_registry.is_some())
1959            .field("payment_authority", &self.payment_authority.is_some())
1960            .field("subagent_spawn_store", &self.subagent_spawn_store.is_some())
1961            .field("org_id", &self.org_id)
1962            .finish()
1963    }
1964}
1965
1966// ============================================================================
1967// EventEmitter - For emitting events
1968// ============================================================================
1969
1970use crate::events::{Event, EventRequest};
1971
1972/// Trait for emitting events following the standard event protocol
1973///
1974/// Implementations can:
1975/// - Store events in a database
1976/// - Keep events in memory for testing
1977/// - Stream events via SSE/WebSocket
1978/// - Log events for debugging
1979///
1980/// Events follow a consistent schema: id, type, ts, context, data.
1981/// See specs/events.md for the full event protocol specification.
1982#[async_trait]
1983pub trait EventEmitter: Send + Sync {
1984    /// Emit an event request
1985    ///
1986    /// Takes an EventRequest (without id/sequence) and returns the stored Event
1987    /// with id and sequence assigned by the storage layer.
1988    async fn emit(&self, request: EventRequest) -> Result<Event>;
1989}
1990
1991/// Blanket impl: `Arc<E>` delegates to the inner emitter.
1992#[async_trait]
1993impl<E: EventEmitter + ?Sized> EventEmitter for Arc<E> {
1994    async fn emit(&self, request: EventRequest) -> Result<Event> {
1995        (**self).emit(request).await
1996    }
1997}
1998
1999/// No-op event emitter for when event emission is not needed
2000///
2001/// This is useful for testing or when event observability is disabled.
2002#[derive(Debug, Clone, Default)]
2003pub struct NoopEventEmitter;
2004
2005#[async_trait]
2006impl EventEmitter for NoopEventEmitter {
2007    async fn emit(&self, request: EventRequest) -> Result<Event> {
2008        // Return a dummy event with sequence 0
2009        Ok(request.into_event(crate::typed_id::EventId::new(), 0))
2010    }
2011}
2012
2013// Note: EventListener trait has been moved to event_listeners.rs module.
2014// Use `everruns_core::EventListener` or `everruns_core::event_listeners::EventListener`.
2015
2016// ============================================================================
2017// ImageResolver - For resolving image_file content to actual image data
2018// ============================================================================
2019
2020/// Resolved image data for LLM consumption
2021///
2022/// This struct contains the actual image data in a format suitable for
2023/// sending to LLM providers. Both OpenAI and Anthropic accept base64-encoded
2024/// images with media type information.
2025#[derive(Debug, Clone)]
2026pub struct ResolvedImage {
2027    /// Base64-encoded image data (without data URL prefix)
2028    pub base64: String,
2029    /// MIME type (e.g., "image/png", "image/jpeg")
2030    pub media_type: String,
2031}
2032
2033impl ResolvedImage {
2034    /// Create a new resolved image
2035    pub fn new(base64: impl Into<String>, media_type: impl Into<String>) -> Self {
2036        Self {
2037            base64: base64.into(),
2038            media_type: media_type.into(),
2039        }
2040    }
2041
2042    /// Convert to a data URL suitable for OpenAI Vision API
2043    ///
2044    /// Format: `data:{media_type};base64,{base64_data}`
2045    pub fn to_data_url(&self) -> String {
2046        format!("data:{};base64,{}", self.media_type, self.base64)
2047    }
2048}
2049
2050/// Trait for resolving image_file content parts to actual image data
2051///
2052/// When building LLM messages, `image_file` content parts contain only
2053/// a reference (UUID) to an uploaded image. This trait allows resolving
2054/// those references to actual image data.
2055///
2056/// # Provider-specific formatting
2057///
2058/// The resolved image data is then converted to provider-specific formats:
2059///
2060/// **OpenAI Vision:**
2061/// ```json
2062/// {
2063///   "type": "image_url",
2064///   "image_url": { "url": "data:image/png;base64,..." }
2065/// }
2066/// ```
2067///
2068/// **Anthropic Vision:**
2069/// ```json
2070/// {
2071///   "type": "image",
2072///   "source": { "type": "base64", "media_type": "image/png", "data": "..." }
2073/// }
2074/// ```
2075///
2076/// # Implementation notes
2077///
2078/// Implementations should:
2079/// - Fetch image data from storage (database, S3, etc.)
2080/// - Return base64-encoded data with media type
2081/// - Handle missing images gracefully (return None)
2082#[async_trait]
2083pub trait ImageResolver: Send + Sync {
2084    /// Resolve an image_file reference to actual image data
2085    ///
2086    /// Returns `None` if the image is not found.
2087    async fn resolve_image(&self, image_id: Uuid) -> Result<Option<ResolvedImage>>;
2088}
2089
2090// ============================================================================
2091// SubagentSpawnStore — durable spawn handles for subagent reattach (EVE-535)
2092// ============================================================================
2093
2094/// Result of attempting to claim a subagent spawn slot.
2095#[derive(Debug)]
2096pub enum SpawnClaimResult {
2097    /// First claim — child session does not yet exist.
2098    /// Proceed to create the child, then call `register_child_session`.
2099    Claimed {
2100        spawn_handle_id: uuid::Uuid,
2101        claim_token: uuid::Uuid,
2102    },
2103    /// Row exists but `child_session_id` was never registered (crash between
2104    /// claim and `register_child_session`). Re-create the child and call
2105    /// `register_child_session` — same flow as `Claimed`.
2106    ClaimedPendingChild {
2107        spawn_handle_id: uuid::Uuid,
2108        claim_token: uuid::Uuid,
2109    },
2110    /// Child session was created and is still running.
2111    /// Reattach: wait for the existing child and settle with the stored claim_token.
2112    AlreadyRunning {
2113        child_session_id: crate::typed_id::SessionId,
2114        /// Stored claim token — must be used for `settle_spawn` on this replay.
2115        claim_token: uuid::Uuid,
2116    },
2117    /// Child already finished on a previous execution.
2118    /// Fast-path: return the stored result immediately without waiting.
2119    AlreadySettled {
2120        child_session_id: crate::typed_id::SessionId,
2121        /// The `wait_for_idle` return value from the original execution.
2122        terminal_status: String,
2123        terminal_result: String,
2124    },
2125}
2126
2127/// Durable spawn handle store for subagent idempotency (EVE-535).
2128///
2129/// Maps `(parent_session_id, tool_call_id) → child_session_id` so that when
2130/// a parent's `act` is reclaimed mid-`wait_for_idle`, the tool can reattach
2131/// to the existing child instead of spawning a duplicate.
2132///
2133/// Lifecycle: claim → register_child_session → settle_spawn.
2134#[async_trait]
2135pub trait SubagentSpawnStore: Send + Sync + 'static {
2136    /// Attempt to claim a spawn slot for `(parent_session_id, tool_call_id)`.
2137    ///
2138    /// Does NOT accept `child_session_id` — the child session does not exist yet.
2139    /// Call `register_child_session` with the actual child ID after creating it.
2140    async fn try_claim_spawn(
2141        &self,
2142        parent_session_id: crate::typed_id::SessionId,
2143        tool_call_id: &str,
2144        claim_token: uuid::Uuid,
2145    ) -> Result<SpawnClaimResult>;
2146
2147    /// Register the actual child session ID after it has been created.
2148    ///
2149    /// Must be called after `try_claim_spawn` returns `Claimed` or
2150    /// `ClaimedPendingChild`, before waiting for the child to complete.
2151    async fn register_child_session(
2152        &self,
2153        spawn_handle_id: uuid::Uuid,
2154        claim_token: uuid::Uuid,
2155        child_session_id: crate::typed_id::SessionId,
2156    ) -> Result<()>;
2157
2158    /// Record the terminal result once the child has completed.
2159    ///
2160    /// `claim_token` must match the stored token. `terminal_status` is the
2161    /// `wait_for_idle` return value ("idle", "error", "timeout", etc.) and
2162    /// `terminal_result` is the last agent message.
2163    async fn settle_spawn(
2164        &self,
2165        parent_session_id: crate::typed_id::SessionId,
2166        tool_call_id: &str,
2167        claim_token: uuid::Uuid,
2168        terminal_status: &str,
2169        terminal_result: &str,
2170    ) -> Result<()>;
2171}
2172
2173/// Blanket impl: `Arc<S>` delegates to the inner store.
2174#[async_trait]
2175impl<S: SubagentSpawnStore + ?Sized> SubagentSpawnStore for Arc<S> {
2176    async fn try_claim_spawn(
2177        &self,
2178        parent_session_id: crate::typed_id::SessionId,
2179        tool_call_id: &str,
2180        claim_token: uuid::Uuid,
2181    ) -> Result<SpawnClaimResult> {
2182        (**self)
2183            .try_claim_spawn(parent_session_id, tool_call_id, claim_token)
2184            .await
2185    }
2186
2187    async fn register_child_session(
2188        &self,
2189        spawn_handle_id: uuid::Uuid,
2190        claim_token: uuid::Uuid,
2191        child_session_id: crate::typed_id::SessionId,
2192    ) -> Result<()> {
2193        (**self)
2194            .register_child_session(spawn_handle_id, claim_token, child_session_id)
2195            .await
2196    }
2197
2198    async fn settle_spawn(
2199        &self,
2200        parent_session_id: crate::typed_id::SessionId,
2201        tool_call_id: &str,
2202        claim_token: uuid::Uuid,
2203        terminal_status: &str,
2204        terminal_result: &str,
2205    ) -> Result<()> {
2206        (**self)
2207            .settle_spawn(
2208                parent_session_id,
2209                tool_call_id,
2210                claim_token,
2211                terminal_status,
2212                terminal_result,
2213            )
2214            .await
2215    }
2216}
2217
2218/// No-op spawn store — used when no durable store is configured (dev/test).
2219///
2220/// Always claims (no dedup); settle and register are no-ops.
2221pub struct NoopSubagentSpawnStore;
2222
2223#[async_trait]
2224impl SubagentSpawnStore for NoopSubagentSpawnStore {
2225    async fn try_claim_spawn(
2226        &self,
2227        _parent_session_id: crate::typed_id::SessionId,
2228        _tool_call_id: &str,
2229        claim_token: uuid::Uuid,
2230    ) -> Result<SpawnClaimResult> {
2231        Ok(SpawnClaimResult::Claimed {
2232            spawn_handle_id: uuid::Uuid::new_v4(),
2233            claim_token,
2234        })
2235    }
2236
2237    async fn register_child_session(
2238        &self,
2239        _spawn_handle_id: uuid::Uuid,
2240        _claim_token: uuid::Uuid,
2241        _child_session_id: crate::typed_id::SessionId,
2242    ) -> Result<()> {
2243        Ok(())
2244    }
2245
2246    async fn settle_spawn(
2247        &self,
2248        _parent_session_id: crate::typed_id::SessionId,
2249        _tool_call_id: &str,
2250        _claim_token: uuid::Uuid,
2251        _terminal_status: &str,
2252        _terminal_result: &str,
2253    ) -> Result<()> {
2254        Ok(())
2255    }
2256}
2257
2258// ============================================================================
2259// Tests
2260// ============================================================================
2261
2262#[cfg(test)]
2263mod tests {
2264    use super::*;
2265
2266    #[test]
2267    fn test_resolved_image_new() {
2268        let image = ResolvedImage::new("SGVsbG8=", "image/png");
2269        assert_eq!(image.base64, "SGVsbG8=");
2270        assert_eq!(image.media_type, "image/png");
2271    }
2272
2273    #[test]
2274    fn test_resolved_image_to_data_url() {
2275        let image = ResolvedImage::new("SGVsbG8=", "image/png");
2276        let data_url = image.to_data_url();
2277        assert_eq!(data_url, "data:image/png;base64,SGVsbG8=");
2278    }
2279
2280    #[test]
2281    fn test_resolved_image_jpeg() {
2282        let image = ResolvedImage::new("base64data", "image/jpeg");
2283        let data_url = image.to_data_url();
2284        assert!(data_url.starts_with("data:image/jpeg;base64,"));
2285    }
2286}