Skip to main content

everruns_core/
traits.rs

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