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