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