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