1use crate::agent::Agent;
9use crate::harness::Harness;
10use crate::provider::DriverId;
11use crate::session_file::{FileInfo, FileStat, GrepMatch, InitialFile, SessionFile};
12use crate::tool_types::{ToolCall, ToolDefinition, ToolResult};
13use crate::typed_id::{AgentId, HarnessId, ImageId, ModelId, SessionId, WorkspaceId};
14use async_trait::async_trait;
15use chrono::{DateTime, Utc};
16use std::any::{Any, TypeId};
17use std::collections::{HashMap, HashSet};
18use std::sync::Arc;
19use uuid::Uuid;
20
21fn 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#[derive(Clone, Default)]
43pub struct ReasoningEffortHandle {
44 inner: Arc<std::sync::RwLock<Option<String>>>,
45}
46
47impl ReasoningEffortHandle {
48 pub fn new() -> Self {
50 Self::default()
51 }
52
53 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 pub fn set(&self, effort: Option<String>) {
64 let mut guard = self.inner.write().unwrap_or_else(|e| e.into_inner());
68 *guard = effort;
69 }
70
71 pub fn get(&self) -> Option<String> {
73 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#[async_trait]
99pub trait AgentStore: Send + Sync {
100 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#[async_trait]
126pub trait HarnessStore: Send + Sync {
127 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
141use crate::leased_resource::{LeasedResource, UpsertLeasedResource};
146use crate::session::Session;
147
148#[async_trait]
154pub trait SessionStore: Send + Sync {
155 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#[async_trait]
168pub trait SessionMutator: Send + Sync {
169 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#[derive(Debug, Clone)]
186pub struct ResolvedModel {
187 pub model: String,
189 pub provider_type: DriverId,
191 pub api_key: Option<String>,
193 pub base_url: Option<String>,
195 pub provider_metadata: Option<crate::driver_registry::ProviderMetadata>,
198}
199
200#[async_trait]
210pub trait ProviderStore: Send + Sync {
211 async fn get_resolved_model(&self, model_id: ModelId) -> Result<Option<ResolvedModel>>;
216
217 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#[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#[derive(Debug, Clone)]
251pub struct StoredImage {
252 pub info: StoredImageInfo,
253 pub data: Vec<u8>,
254}
255
256#[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 async fn create_image(&self, input: CreateStoredImage) -> Result<StoredImageInfo>;
269
270 async fn get_image(&self, image_id: ImageId) -> Result<Option<StoredImage>>;
272
273 async fn get_image_info(&self, image_id: ImageId) -> Result<Option<StoredImageInfo>>;
275}
276
277#[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 async fn get_default_provider_credentials(
295 &self,
296 provider_type: &str,
297 ) -> Result<Option<ProviderCredentials>>;
298}
299
300#[async_trait]
311pub trait ToolExecutor: Send + Sync {
312 async fn execute(&self, tool_call: &ToolCall, tool_def: &ToolDefinition) -> Result<ToolResult>;
317
318 async fn execute_with_context(
323 &self,
324 tool_call: &ToolCall,
325 tool_def: &ToolDefinition,
326 _context: &ToolContext,
327 ) -> Result<ToolResult> {
328 self.execute(tool_call, tool_def).await
330 }
331
332 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 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#[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#[async_trait]
428pub trait SessionFileSystem: Send + Sync {
429 fn display_root(&self) -> String {
436 crate::session_path::WORKSPACE_PREFIX.to_string()
437 }
438
439 fn display_path(&self, path: &str) -> String {
444 crate::session_path::to_display_path(path)
445 }
446
447 fn resolve_path(&self, input: &str) -> String {
458 crate::session_path::to_session_path(input)
459 }
460
461 async fn read_file(&self, session_id: SessionId, path: &str) -> Result<Option<SessionFile>>;
463
464 async fn write_file(
466 &self,
467 session_id: SessionId,
468 path: &str,
469 content: &str,
470 encoding: &str,
471 ) -> Result<SessionFile>;
472
473 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 async fn delete_file(&self, session_id: SessionId, path: &str, recursive: bool)
506 -> Result<bool>;
507
508 async fn list_directory(&self, session_id: SessionId, path: &str) -> Result<Vec<FileInfo>>;
510
511 async fn stat_file(&self, session_id: SessionId, path: &str) -> Result<Option<FileStat>>;
513
514 async fn grep_files(
516 &self,
517 session_id: SessionId,
518 pattern: &str,
519 path_pattern: Option<&str>,
520 ) -> Result<Vec<GrepMatch>>;
521
522 async fn create_directory(&self, session_id: SessionId, path: &str) -> Result<FileInfo>;
524
525 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
538pub struct WorkspaceScopedFileSystem {
548 inner: Arc<dyn SessionFileSystem>,
549 key: SessionId,
550}
551
552impl WorkspaceScopedFileSystem {
553 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
729pub use SessionFileSystem as SessionFileStore;
731
732#[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#[async_trait]
762pub trait SessionFileSystemFactory: Send + Sync {
763 fn name(&self) -> &'static str {
765 "SessionFileSystemFactory"
766 }
767
768 fn is_disabled(&self) -> bool {
771 false
772 }
773
774 async fn create_session_file_system(
776 &self,
777 context: SessionFileSystemFactoryContext,
778 ) -> Result<Arc<dyn SessionFileSystem>>;
779}
780
781#[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#[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#[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#[derive(Debug, Clone, serde::Serialize)]
835pub struct KnowledgeSearchHit {
836 pub id: String,
838 pub kb_id: String,
840 pub title: String,
841 pub kind: String,
842 pub tags: Vec<String>,
843 pub snippet: String,
845 pub resource: Option<String>,
847}
848
849#[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#[async_trait]
870pub trait SessionStorageStore: Send + Sync {
871 async fn set_value(&self, session_id: SessionId, key: &str, value: &str) -> Result<()>;
875
876 async fn get_value(&self, session_id: SessionId, key: &str) -> Result<Option<String>>;
878
879 async fn delete_value(&self, session_id: SessionId, key: &str) -> Result<bool>;
881
882 async fn list_keys(&self, session_id: SessionId) -> Result<Vec<KeyInfo>>;
884
885 async fn set_secret(&self, session_id: SessionId, name: &str, value: &str) -> Result<()>;
889
890 async fn get_secret(&self, session_id: SessionId, name: &str) -> Result<Option<String>>;
892
893 async fn delete_secret(&self, session_id: SessionId, name: &str) -> Result<bool>;
895
896 async fn list_secrets(&self, session_id: SessionId) -> Result<Vec<SecretInfo>>;
898}
899
900use crate::session_schedule::SessionSchedule;
905use crate::typed_id::ScheduleId;
906
907#[async_trait]
911pub trait SessionScheduleStore: Send + Sync {
912 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 async fn create_schedule_enforcing_limits(
926 &self,
927 session_id: SessionId,
928 description: String,
929 cron_expression: Option<String>,
930 scheduled_at: Option<chrono::DateTime<chrono::Utc>>,
931 timezone: String,
932 ) -> std::result::Result<SessionSchedule, crate::session_schedule::ScheduleLimitError> {
933 let per_session = self
934 .count_active_schedules(session_id)
935 .await
936 .map_err(crate::session_schedule::ScheduleLimitError::Store)?;
937 if per_session >= crate::session_schedule::MAX_ACTIVE_SCHEDULES_PER_SESSION {
938 return Err(crate::session_schedule::ScheduleLimitError::Rejected(
939 format!(
940 "Maximum {} active schedules per session. Cancel an existing schedule first.",
941 crate::session_schedule::MAX_ACTIVE_SCHEDULES_PER_SESSION
942 ),
943 ));
944 }
945
946 let max_per_org = crate::session_schedule::max_active_schedules_per_org();
947 let per_org = self
948 .count_active_org_schedules()
949 .await
950 .map_err(crate::session_schedule::ScheduleLimitError::Store)?;
951 if i64::from(per_org) >= max_per_org {
952 return Err(crate::session_schedule::ScheduleLimitError::Rejected(
953 format!(
954 "Maximum {max_per_org} active schedules per org reached. Cancel an existing schedule first."
955 ),
956 ));
957 }
958
959 if let Some(cron) = cron_expression.as_deref() {
960 crate::session_schedule::validate_cron_min_interval(cron)
961 .map_err(crate::session_schedule::ScheduleLimitError::Rejected)?;
962 }
963
964 self.create_schedule(
965 session_id,
966 description,
967 cron_expression,
968 scheduled_at,
969 timezone,
970 )
971 .await
972 .map_err(crate::session_schedule::ScheduleLimitError::Store)
973 }
974
975 async fn cancel_schedule(
977 &self,
978 session_id: SessionId,
979 schedule_id: ScheduleId,
980 ) -> Result<SessionSchedule>;
981
982 async fn list_schedules(&self, session_id: SessionId) -> Result<Vec<SessionSchedule>>;
984
985 async fn count_active_schedules(&self, session_id: SessionId) -> Result<u32>;
987
988 async fn count_active_org_schedules(&self) -> Result<u32>;
993}
994
995#[async_trait]
1005pub trait SessionResourceRegistry: Send + Sync {
1006 async fn register(
1008 &self,
1009 entry: crate::session_resource::RegisterSessionResource,
1010 ) -> Result<crate::session_resource::SessionResourceEntry>;
1011
1012 async fn update_status(
1014 &self,
1015 session_id: SessionId,
1016 resource_id: &str,
1017 status: crate::session_resource::SessionResourceStatus,
1018 ) -> Result<Option<crate::session_resource::SessionResourceEntry>>;
1019
1020 async fn get(
1022 &self,
1023 session_id: SessionId,
1024 resource_id: &str,
1025 ) -> Result<Option<crate::session_resource::SessionResourceEntry>>;
1026
1027 async fn list(
1029 &self,
1030 session_id: SessionId,
1031 filter: Option<&crate::session_resource::SessionResourceFilter>,
1032 ) -> Result<Vec<crate::session_resource::SessionResourceEntry>>;
1033
1034 async fn deregister(&self, session_id: SessionId, resource_id: &str) -> Result<bool>;
1036}
1037
1038#[async_trait]
1048pub trait LeasedResourceStore: Send + Sync {
1049 async fn upsert_resource(&self, input: UpsertLeasedResource) -> Result<LeasedResource>;
1055
1056 async fn release_resource(
1062 &self,
1063 session_id: SessionId,
1064 provider: &str,
1065 resource_type: &str,
1066 external_id: &str,
1067 ) -> Result<Option<LeasedResource>>;
1068
1069 async fn list_resources(&self, session_id: SessionId) -> Result<Vec<LeasedResource>>;
1074}
1075
1076pub type SessionSqlDbStoreRef = Arc<dyn crate::session_sqldb::SessionSqlDbStore>;
1082
1083#[async_trait]
1088pub trait UserConnectionResolver: Send + Sync {
1089 async fn get_connection_token(
1092 &self,
1093 session_id: SessionId,
1094 provider: &str,
1095 ) -> Result<Option<String>>;
1096
1097 async fn get_connection_user(
1102 &self,
1103 _session_id: SessionId,
1104 _provider: &str,
1105 ) -> Result<Option<Uuid>> {
1106 Ok(None)
1107 }
1108
1109 async fn get_connection_token_for_user(
1114 &self,
1115 _user_id: Uuid,
1116 _provider: &str,
1117 ) -> Result<Option<String>> {
1118 Ok(None)
1119 }
1120
1121 async fn get_connection_metadata(
1124 &self,
1125 _session_id: SessionId,
1126 _provider: &str,
1127 ) -> Result<Option<serde_json::Value>> {
1128 Ok(None)
1129 }
1130}
1131
1132#[async_trait]
1142pub trait BudgetChecker: Send + Sync {
1143 async fn check_budgets(&self, session_id: &str) -> Result<crate::budget::BudgetToolResponse>;
1145}
1146
1147#[async_trait]
1156pub trait PaymentAuthority: Send + Sync {
1157 async fn execute_machine_payment(
1158 &self,
1159 session_id: SessionId,
1160 request: crate::payment::MachinePaymentRequest,
1161 ) -> Result<crate::payment::MachinePaymentResponse>;
1162}
1163
1164#[async_trait]
1174pub trait OutboundToolRateLimiter: Send + Sync {
1175 async fn check_org(&self, org_id: &crate::typed_id::OrgId) -> bool;
1177}
1178
1179#[derive(Debug)]
1185pub enum ToolCallClaimResult {
1186 Claimed { claim_token: uuid::Uuid },
1189 AlreadySettled {
1191 result_json: serde_json::Value,
1192 args_fingerprint: String,
1193 },
1194 AlreadyRunning { args_fingerprint: String },
1199 DeterminismViolation {
1203 stored_fingerprint: String,
1204 current_fingerprint: String,
1205 },
1206}
1207
1208#[derive(Debug, Clone)]
1210pub enum DurableToolCallStatus {
1211 Settled { result_json: serde_json::Value },
1213 Interrupted {
1215 result_json: Option<serde_json::Value>,
1216 },
1217 Running,
1219}
1220
1221#[async_trait]
1226pub trait DurableToolResultStore: Send + Sync + 'static {
1227 async fn try_claim_tool_call(
1235 &self,
1236 turn_id: &str,
1237 tool_call_id: &str,
1238 tool_name: &str,
1239 args_fingerprint: &str,
1240 ) -> Result<ToolCallClaimResult>;
1241
1242 async fn settle_tool_call(
1248 &self,
1249 turn_id: &str,
1250 tool_call_id: &str,
1251 result_json: serde_json::Value,
1252 status: &str,
1253 claim_token: uuid::Uuid,
1254 ) -> Result<bool>;
1255
1256 async fn get_tool_call_status(
1261 &self,
1262 turn_id: &str,
1263 tool_call_id: &str,
1264 ) -> Result<Option<DurableToolCallStatus>>;
1265}
1266
1267pub struct NoopDurableToolResultStore;
1270
1271#[async_trait]
1272impl DurableToolResultStore for NoopDurableToolResultStore {
1273 async fn try_claim_tool_call(
1274 &self,
1275 _turn_id: &str,
1276 _tool_call_id: &str,
1277 _tool_name: &str,
1278 _args_fingerprint: &str,
1279 ) -> Result<ToolCallClaimResult> {
1280 Ok(ToolCallClaimResult::Claimed {
1281 claim_token: uuid::Uuid::new_v4(),
1282 })
1283 }
1284
1285 async fn settle_tool_call(
1286 &self,
1287 _turn_id: &str,
1288 _tool_call_id: &str,
1289 _result_json: serde_json::Value,
1290 _status: &str,
1291 _claim_token: uuid::Uuid,
1292 ) -> Result<bool> {
1293 Ok(true)
1294 }
1295
1296 async fn get_tool_call_status(
1297 &self,
1298 _turn_id: &str,
1299 _tool_call_id: &str,
1300 ) -> Result<Option<DurableToolCallStatus>> {
1301 Ok(None)
1302 }
1303}
1304
1305#[derive(Debug, Clone)]
1311pub struct StreamProgress {
1312 pub accumulated_len: usize,
1314 pub last_delta_at: u64,
1316}
1317
1318#[async_trait]
1324pub trait StreamHeartbeater: Send + Sync {
1325 async fn heartbeat(&self, progress: StreamProgress);
1331}
1332
1333pub struct NoopStreamHeartbeater;
1335
1336#[async_trait]
1337impl StreamHeartbeater for NoopStreamHeartbeater {
1338 async fn heartbeat(&self, _progress: StreamProgress) {}
1339}
1340
1341#[derive(Debug, Clone)]
1347pub struct PartialStreamState {
1348 pub accumulated: String,
1351}
1352
1353#[async_trait]
1361pub trait PartialStreamStore: Send + Sync {
1362 async fn get_partial_stream(
1365 &self,
1366 session_id: SessionId,
1367 turn_id: &str,
1368 ) -> Result<Option<PartialStreamState>>;
1369}
1370
1371pub struct NoopPartialStreamStore;
1373
1374#[async_trait]
1375impl PartialStreamStore for NoopPartialStreamStore {
1376 async fn get_partial_stream(
1377 &self,
1378 _session_id: SessionId,
1379 _turn_id: &str,
1380 ) -> Result<Option<PartialStreamState>> {
1381 Ok(None)
1382 }
1383}
1384
1385#[derive(Clone)]
1394pub struct ToolContext {
1395 pub session_id: SessionId,
1397 pub workspace_id: WorkspaceId,
1404
1405 pub file_store: Option<Arc<dyn SessionFileSystem>>,
1407
1408 pub storage_store: Option<Arc<dyn SessionStorageStore>>,
1410
1411 pub image_store: Option<Arc<dyn ImageArtifactStore>>,
1413
1414 pub provider_credential_store: Option<Arc<dyn ProviderCredentialStore>>,
1416
1417 pub utility_llm_service: Option<Arc<dyn crate::UtilityLlmService>>,
1419
1420 pub mcp_invoker: Option<Arc<dyn crate::McpToolInvoker>>,
1426
1427 pub egress_service: Option<Arc<dyn crate::EgressService>>,
1429
1430 pub sqldb_store: Option<SessionSqlDbStoreRef>,
1432
1433 pub message_retriever: Option<Arc<dyn crate::message_retriever::MessageRetriever>>,
1435
1436 pub session_store: Option<Arc<dyn SessionStore>>,
1438
1439 pub session_mutator: Option<Arc<dyn SessionMutator>>,
1441
1442 pub agent_store: Option<Arc<dyn AgentStore>>,
1444
1445 pub connection_resolver: Option<Arc<dyn UserConnectionResolver>>,
1447
1448 pub schedule_store: Option<Arc<dyn SessionScheduleStore>>,
1450
1451 pub platform_store: Option<Arc<dyn crate::platform_store::PlatformStore>>,
1453 pub knowledge_store: Option<Arc<dyn KnowledgeStore>>,
1455
1456 pub knowledge_index_search: Option<Arc<dyn crate::vector_store::KnowledgeIndexSearch>>,
1460
1461 pub leased_resource_store: Option<Arc<dyn LeasedResourceStore>>,
1463
1464 pub session_resource_registry: Option<Arc<dyn SessionResourceRegistry>>,
1466
1467 pub session_task_registry: Option<Arc<dyn crate::session_task::SessionTaskRegistry>>,
1470
1471 pub event_emitter: Option<Arc<dyn EventEmitter>>,
1474
1475 pub event_context: Option<crate::events::EventContext>,
1478
1479 pub tool_call_id: Option<String>,
1482 pub capability_registry: Option<crate::capabilities::CapabilityRegistry>,
1484
1485 pub tool_registry: Option<Arc<crate::tools::ToolRegistry>>,
1488
1489 pub visible_tool_names: Option<Arc<HashSet<String>>>,
1493
1494 pub org_id: Option<crate::typed_id::OrgId>,
1496
1497 pub network_access: Option<crate::network_access::NetworkAccessList>,
1500
1501 pub locale: Option<String>,
1505
1506 pub budget_checker: Option<Arc<dyn BudgetChecker>>,
1508
1509 pub payment_authority: Option<Arc<dyn PaymentAuthority>>,
1511
1512 pub subagent_spawn_store: Option<Arc<dyn SubagentSpawnStore>>,
1516
1517 pub reasoning_effort_handle: Option<ReasoningEffortHandle>,
1521}
1522
1523impl ToolContext {
1524 pub fn workspace_fs_key(&self) -> SessionId {
1529 SessionId::from_uuid(self.workspace_id.uuid())
1530 }
1531
1532 pub fn with_workspace_id(mut self, workspace_id: WorkspaceId) -> Self {
1534 self.workspace_id = workspace_id;
1535 self
1536 }
1537
1538 pub fn new(session_id: SessionId) -> Self {
1540 Self {
1541 session_id,
1542 workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
1543 file_store: None,
1544 storage_store: None,
1545 image_store: None,
1546 provider_credential_store: None,
1547 utility_llm_service: None,
1548 mcp_invoker: None,
1549 egress_service: None,
1550 sqldb_store: None,
1551 message_retriever: None,
1552 session_store: None,
1553 session_mutator: None,
1554 agent_store: None,
1555 connection_resolver: None,
1556 schedule_store: None,
1557 platform_store: None,
1558 knowledge_store: None,
1559 knowledge_index_search: None,
1560 leased_resource_store: None,
1561 session_resource_registry: None,
1562 session_task_registry: None,
1563 event_emitter: None,
1564 event_context: None,
1565 tool_call_id: None,
1566 capability_registry: None,
1567 tool_registry: None,
1568 visible_tool_names: None,
1569 org_id: None,
1570 network_access: None,
1571 locale: None,
1572 budget_checker: None,
1573 payment_authority: None,
1574 subagent_spawn_store: None,
1575 reasoning_effort_handle: None,
1576 }
1577 }
1578
1579 pub fn with_file_store(session_id: SessionId, file_store: Arc<dyn SessionFileSystem>) -> Self {
1581 Self {
1582 session_id,
1583 workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
1584 file_store: Some(file_store),
1585 storage_store: None,
1586 image_store: None,
1587 provider_credential_store: None,
1588 utility_llm_service: None,
1589 mcp_invoker: None,
1590 egress_service: None,
1591 sqldb_store: None,
1592 message_retriever: None,
1593 session_store: None,
1594 session_mutator: None,
1595 agent_store: None,
1596 connection_resolver: None,
1597 schedule_store: None,
1598 platform_store: None,
1599 knowledge_store: None,
1600 knowledge_index_search: None,
1601 leased_resource_store: None,
1602 session_resource_registry: None,
1603 session_task_registry: None,
1604 event_emitter: None,
1605 event_context: None,
1606 tool_call_id: None,
1607 capability_registry: None,
1608 tool_registry: None,
1609 visible_tool_names: None,
1610 org_id: None,
1611 network_access: None,
1612 locale: None,
1613 budget_checker: None,
1614 payment_authority: None,
1615 subagent_spawn_store: None,
1616 reasoning_effort_handle: None,
1617 }
1618 }
1619
1620 pub fn with_storage_store(
1622 session_id: SessionId,
1623 storage_store: Arc<dyn SessionStorageStore>,
1624 ) -> Self {
1625 Self {
1626 session_id,
1627 workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
1628 file_store: None,
1629 storage_store: Some(storage_store),
1630 image_store: None,
1631 provider_credential_store: None,
1632 utility_llm_service: None,
1633 mcp_invoker: None,
1634 egress_service: None,
1635 sqldb_store: None,
1636 message_retriever: None,
1637 session_store: None,
1638 session_mutator: None,
1639 agent_store: None,
1640 connection_resolver: None,
1641 schedule_store: None,
1642 platform_store: None,
1643 knowledge_store: None,
1644 knowledge_index_search: None,
1645 leased_resource_store: None,
1646 session_resource_registry: None,
1647 session_task_registry: None,
1648 event_emitter: None,
1649 event_context: None,
1650 tool_call_id: None,
1651 capability_registry: None,
1652 tool_registry: None,
1653 visible_tool_names: None,
1654 org_id: None,
1655 network_access: None,
1656 locale: None,
1657 budget_checker: None,
1658 payment_authority: None,
1659 subagent_spawn_store: None,
1660 reasoning_effort_handle: None,
1661 }
1662 }
1663
1664 pub fn with_stores(
1666 session_id: SessionId,
1667 file_store: Arc<dyn SessionFileSystem>,
1668 storage_store: Arc<dyn SessionStorageStore>,
1669 ) -> Self {
1670 Self {
1671 session_id,
1672 workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
1673 file_store: Some(file_store),
1674 storage_store: Some(storage_store),
1675 sqldb_store: None,
1676 image_store: None,
1677 provider_credential_store: None,
1678 utility_llm_service: None,
1679 mcp_invoker: None,
1680 egress_service: None,
1681 message_retriever: None,
1682 session_store: None,
1683 session_mutator: None,
1684 agent_store: None,
1685 connection_resolver: None,
1686 schedule_store: None,
1687 platform_store: None,
1688 knowledge_store: None,
1689 knowledge_index_search: None,
1690 leased_resource_store: None,
1691 session_resource_registry: None,
1692 session_task_registry: None,
1693 event_emitter: None,
1694 event_context: None,
1695 tool_call_id: None,
1696 capability_registry: None,
1697 tool_registry: None,
1698 visible_tool_names: None,
1699 org_id: None,
1700 network_access: None,
1701 locale: None,
1702 budget_checker: None,
1703 payment_authority: None,
1704 subagent_spawn_store: None,
1705 reasoning_effort_handle: None,
1706 }
1707 }
1708
1709 pub fn with_sqldb_store(mut self, sqldb_store: SessionSqlDbStoreRef) -> Self {
1711 self.sqldb_store = Some(sqldb_store);
1712 self
1713 }
1714
1715 pub fn with_message_retriever(
1717 mut self,
1718 retriever: Arc<dyn crate::message_retriever::MessageRetriever>,
1719 ) -> Self {
1720 self.message_retriever = Some(retriever);
1721 self
1722 }
1723
1724 pub fn with_session_store(mut self, store: Arc<dyn SessionStore>) -> Self {
1726 self.session_store = Some(store);
1727 self
1728 }
1729
1730 pub fn with_session_mutator(mut self, mutator: Arc<dyn SessionMutator>) -> Self {
1732 self.session_mutator = Some(mutator);
1733 self
1734 }
1735
1736 pub fn with_reasoning_effort_handle(mut self, handle: ReasoningEffortHandle) -> Self {
1740 self.reasoning_effort_handle = Some(handle);
1741 self
1742 }
1743
1744 pub fn with_agent_store(mut self, store: Arc<dyn AgentStore>) -> Self {
1746 self.agent_store = Some(store);
1747 self
1748 }
1749
1750 pub fn with_connection_resolver(mut self, resolver: Arc<dyn UserConnectionResolver>) -> Self {
1752 self.connection_resolver = Some(resolver);
1753 self
1754 }
1755
1756 pub fn with_image_store(
1758 session_id: SessionId,
1759 image_store: Arc<dyn ImageArtifactStore>,
1760 ) -> Self {
1761 Self {
1762 session_id,
1763 workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
1764 file_store: None,
1765 storage_store: None,
1766 image_store: Some(image_store),
1767 provider_credential_store: None,
1768 utility_llm_service: None,
1769 mcp_invoker: None,
1770 egress_service: None,
1771 sqldb_store: None,
1772 message_retriever: None,
1773 session_store: None,
1774 session_mutator: None,
1775 agent_store: None,
1776 connection_resolver: None,
1777 schedule_store: None,
1778 platform_store: None,
1779 knowledge_store: None,
1780 knowledge_index_search: None,
1781 leased_resource_store: None,
1782 session_resource_registry: None,
1783 session_task_registry: None,
1784 event_emitter: None,
1785 event_context: None,
1786 tool_call_id: None,
1787 capability_registry: None,
1788 tool_registry: None,
1789 visible_tool_names: None,
1790 org_id: None,
1791 network_access: None,
1792 locale: None,
1793 budget_checker: None,
1794 payment_authority: None,
1795 subagent_spawn_store: None,
1796 reasoning_effort_handle: None,
1797 }
1798 }
1799
1800 pub fn with_provider_credential_store(
1802 mut self,
1803 store: Arc<dyn ProviderCredentialStore>,
1804 ) -> Self {
1805 self.provider_credential_store = Some(store);
1806 self
1807 }
1808
1809 pub fn with_utility_llm_service(mut self, service: Arc<dyn crate::UtilityLlmService>) -> Self {
1811 self.utility_llm_service = Some(service);
1812 self
1813 }
1814
1815 pub fn with_mcp_invoker(mut self, invoker: Arc<dyn crate::McpToolInvoker>) -> Self {
1817 self.mcp_invoker = Some(invoker);
1818 self
1819 }
1820
1821 pub fn with_egress_service(mut self, service: Arc<dyn crate::EgressService>) -> Self {
1823 self.egress_service = Some(service);
1824 self
1825 }
1826
1827 pub fn with_egress_service_opt(
1830 mut self,
1831 service: Option<Arc<dyn crate::EgressService>>,
1832 ) -> Self {
1833 if let Some(service) = service {
1834 self.egress_service = Some(service);
1835 }
1836 self
1837 }
1838
1839 pub fn with_storage_store_arc(mut self, store: Arc<dyn SessionStorageStore>) -> Self {
1841 self.storage_store = Some(store);
1842 self
1843 }
1844
1845 pub fn with_schedule_store(mut self, store: Arc<dyn SessionScheduleStore>) -> Self {
1847 self.schedule_store = Some(store);
1848 self
1849 }
1850
1851 pub fn with_platform_store(
1853 mut self,
1854 store: Arc<dyn crate::platform_store::PlatformStore>,
1855 ) -> Self {
1856 self.platform_store = Some(store);
1857 self
1858 }
1859
1860 pub fn with_knowledge_index_search(
1862 mut self,
1863 search: Arc<dyn crate::vector_store::KnowledgeIndexSearch>,
1864 ) -> Self {
1865 self.knowledge_index_search = Some(search);
1866 self
1867 }
1868
1869 pub fn with_leased_resource_store(mut self, store: Arc<dyn LeasedResourceStore>) -> Self {
1871 self.leased_resource_store = Some(store);
1872 self
1873 }
1874
1875 pub fn with_session_resource_registry(
1877 mut self,
1878 registry: Arc<dyn SessionResourceRegistry>,
1879 ) -> Self {
1880 self.session_resource_registry = Some(registry);
1881 self
1882 }
1883
1884 pub fn with_session_task_registry(
1886 mut self,
1887 registry: Arc<dyn crate::session_task::SessionTaskRegistry>,
1888 ) -> Self {
1889 self.session_task_registry = Some(registry);
1890 self
1891 }
1892
1893 pub fn with_org_id(mut self, org_id: crate::typed_id::OrgId) -> Self {
1895 self.org_id = Some(org_id);
1896 self
1897 }
1898
1899 pub fn with_tool_registry(mut self, registry: Arc<crate::tools::ToolRegistry>) -> Self {
1901 self.tool_registry = Some(registry);
1902 self
1903 }
1904
1905 pub fn with_visible_tool_names(mut self, names: Arc<HashSet<String>>) -> Self {
1907 self.visible_tool_names = Some(names);
1908 self
1909 }
1910
1911 pub fn with_network_access(
1913 mut self,
1914 network_access: Option<crate::network_access::NetworkAccessList>,
1915 ) -> Self {
1916 self.network_access = network_access;
1917 self
1918 }
1919
1920 pub fn with_payment_authority(mut self, authority: Arc<dyn PaymentAuthority>) -> Self {
1922 self.payment_authority = Some(authority);
1923 self
1924 }
1925
1926 pub fn with_subagent_spawn_store(mut self, store: Arc<dyn SubagentSpawnStore>) -> Self {
1928 self.subagent_spawn_store = Some(store);
1929 self
1930 }
1931
1932 pub async fn emit_progress(&self, tool_name: &str, message: &str) {
1937 let (Some(emitter), Some(ctx), Some(call_id)) =
1938 (&self.event_emitter, &self.event_context, &self.tool_call_id)
1939 else {
1940 return;
1941 };
1942 if let Err(e) = emitter
1943 .emit(EventRequest::new(
1944 self.session_id,
1945 ctx.clone(),
1946 crate::events::ToolProgressData {
1947 tool_call_id: call_id.clone(),
1948 tool_name: tool_name.to_string(),
1949 message: message.to_string(),
1950 display_name: None,
1951 },
1952 ))
1953 .await
1954 {
1955 tracing::debug!(
1956 tool_call_id = call_id,
1957 tool_name,
1958 error = %e,
1959 "Failed to emit tool.progress event"
1960 );
1961 }
1962 }
1963
1964 pub async fn emit_tool_output(&self, tool_name: &str, delta: &str, stream: &str) {
1969 let (Some(emitter), Some(ctx), Some(call_id)) =
1970 (&self.event_emitter, &self.event_context, &self.tool_call_id)
1971 else {
1972 return;
1973 };
1974 if let Err(e) = emitter
1975 .emit(EventRequest::new(
1976 self.session_id,
1977 ctx.clone(),
1978 crate::events::ToolOutputDeltaData {
1979 tool_call_id: call_id.clone(),
1980 tool_name: tool_name.to_string(),
1981 delta: delta.to_string(),
1982 stream: stream.to_string(),
1983 },
1984 ))
1985 .await
1986 {
1987 tracing::debug!(
1988 tool_call_id = call_id,
1989 tool_name,
1990 error = %e,
1991 "Failed to emit tool.output.delta event"
1992 );
1993 }
1994 }
1995}
1996
1997impl std::fmt::Debug for ToolContext {
1998 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1999 f.debug_struct("ToolContext")
2000 .field("session_id", &self.session_id)
2001 .field("file_store", &self.file_store.is_some())
2002 .field("storage_store", &self.storage_store.is_some())
2003 .field("image_store", &self.image_store.is_some())
2004 .field(
2005 "provider_credential_store",
2006 &self.provider_credential_store.is_some(),
2007 )
2008 .field("utility_llm_service", &self.utility_llm_service.is_some())
2009 .field("egress_service", &self.egress_service.is_some())
2010 .field("sqldb_store", &self.sqldb_store.is_some())
2011 .field("message_retriever", &self.message_retriever.is_some())
2012 .field("session_store", &self.session_store.is_some())
2013 .field("session_mutator", &self.session_mutator.is_some())
2014 .field("agent_store", &self.agent_store.is_some())
2015 .field("connection_resolver", &self.connection_resolver.is_some())
2016 .field("schedule_store", &self.schedule_store.is_some())
2017 .field("platform_store", &self.platform_store.is_some())
2018 .field(
2019 "knowledge_index_search",
2020 &self.knowledge_index_search.is_some(),
2021 )
2022 .field(
2023 "leased_resource_store",
2024 &self.leased_resource_store.is_some(),
2025 )
2026 .field("event_emitter", &self.event_emitter.is_some())
2027 .field("tool_registry", &self.tool_registry.is_some())
2028 .field("payment_authority", &self.payment_authority.is_some())
2029 .field("subagent_spawn_store", &self.subagent_spawn_store.is_some())
2030 .field("org_id", &self.org_id)
2031 .finish()
2032 }
2033}
2034
2035use crate::events::{Event, EventRequest};
2040
2041#[async_trait]
2052pub trait EventEmitter: Send + Sync {
2053 async fn emit(&self, request: EventRequest) -> Result<Event>;
2058}
2059
2060#[async_trait]
2062impl<E: EventEmitter + ?Sized> EventEmitter for Arc<E> {
2063 async fn emit(&self, request: EventRequest) -> Result<Event> {
2064 (**self).emit(request).await
2065 }
2066}
2067
2068#[derive(Debug, Clone, Default)]
2072pub struct NoopEventEmitter;
2073
2074#[async_trait]
2075impl EventEmitter for NoopEventEmitter {
2076 async fn emit(&self, request: EventRequest) -> Result<Event> {
2077 Ok(request.into_event(crate::typed_id::EventId::new(), 0))
2079 }
2080}
2081
2082#[derive(Debug, Clone)]
2095pub struct ResolvedImage {
2096 pub base64: String,
2098 pub media_type: String,
2100}
2101
2102impl ResolvedImage {
2103 pub fn new(base64: impl Into<String>, media_type: impl Into<String>) -> Self {
2105 Self {
2106 base64: base64.into(),
2107 media_type: media_type.into(),
2108 }
2109 }
2110
2111 pub fn to_data_url(&self) -> String {
2115 format!("data:{};base64,{}", self.media_type, self.base64)
2116 }
2117}
2118
2119#[async_trait]
2152pub trait ImageResolver: Send + Sync {
2153 async fn resolve_image(&self, image_id: Uuid) -> Result<Option<ResolvedImage>>;
2157}
2158
2159#[derive(Debug)]
2165pub enum SpawnClaimResult {
2166 Claimed {
2169 spawn_handle_id: uuid::Uuid,
2170 claim_token: uuid::Uuid,
2171 },
2172 ClaimedPendingChild {
2176 spawn_handle_id: uuid::Uuid,
2177 claim_token: uuid::Uuid,
2178 },
2179 AlreadyRunning {
2182 child_session_id: crate::typed_id::SessionId,
2183 claim_token: uuid::Uuid,
2185 },
2186 AlreadySettled {
2189 child_session_id: crate::typed_id::SessionId,
2190 terminal_status: String,
2192 terminal_result: String,
2193 },
2194}
2195
2196#[async_trait]
2204pub trait SubagentSpawnStore: Send + Sync + 'static {
2205 async fn try_claim_spawn(
2210 &self,
2211 parent_session_id: crate::typed_id::SessionId,
2212 tool_call_id: &str,
2213 claim_token: uuid::Uuid,
2214 ) -> Result<SpawnClaimResult>;
2215
2216 async fn register_child_session(
2221 &self,
2222 spawn_handle_id: uuid::Uuid,
2223 claim_token: uuid::Uuid,
2224 child_session_id: crate::typed_id::SessionId,
2225 ) -> Result<()>;
2226
2227 async fn settle_spawn(
2233 &self,
2234 parent_session_id: crate::typed_id::SessionId,
2235 tool_call_id: &str,
2236 claim_token: uuid::Uuid,
2237 terminal_status: &str,
2238 terminal_result: &str,
2239 ) -> Result<()>;
2240}
2241
2242#[async_trait]
2244impl<S: SubagentSpawnStore + ?Sized> SubagentSpawnStore for Arc<S> {
2245 async fn try_claim_spawn(
2246 &self,
2247 parent_session_id: crate::typed_id::SessionId,
2248 tool_call_id: &str,
2249 claim_token: uuid::Uuid,
2250 ) -> Result<SpawnClaimResult> {
2251 (**self)
2252 .try_claim_spawn(parent_session_id, tool_call_id, claim_token)
2253 .await
2254 }
2255
2256 async fn register_child_session(
2257 &self,
2258 spawn_handle_id: uuid::Uuid,
2259 claim_token: uuid::Uuid,
2260 child_session_id: crate::typed_id::SessionId,
2261 ) -> Result<()> {
2262 (**self)
2263 .register_child_session(spawn_handle_id, claim_token, child_session_id)
2264 .await
2265 }
2266
2267 async fn settle_spawn(
2268 &self,
2269 parent_session_id: crate::typed_id::SessionId,
2270 tool_call_id: &str,
2271 claim_token: uuid::Uuid,
2272 terminal_status: &str,
2273 terminal_result: &str,
2274 ) -> Result<()> {
2275 (**self)
2276 .settle_spawn(
2277 parent_session_id,
2278 tool_call_id,
2279 claim_token,
2280 terminal_status,
2281 terminal_result,
2282 )
2283 .await
2284 }
2285}
2286
2287pub struct NoopSubagentSpawnStore;
2291
2292#[async_trait]
2293impl SubagentSpawnStore for NoopSubagentSpawnStore {
2294 async fn try_claim_spawn(
2295 &self,
2296 _parent_session_id: crate::typed_id::SessionId,
2297 _tool_call_id: &str,
2298 claim_token: uuid::Uuid,
2299 ) -> Result<SpawnClaimResult> {
2300 Ok(SpawnClaimResult::Claimed {
2301 spawn_handle_id: uuid::Uuid::new_v4(),
2302 claim_token,
2303 })
2304 }
2305
2306 async fn register_child_session(
2307 &self,
2308 _spawn_handle_id: uuid::Uuid,
2309 _claim_token: uuid::Uuid,
2310 _child_session_id: crate::typed_id::SessionId,
2311 ) -> Result<()> {
2312 Ok(())
2313 }
2314
2315 async fn settle_spawn(
2316 &self,
2317 _parent_session_id: crate::typed_id::SessionId,
2318 _tool_call_id: &str,
2319 _claim_token: uuid::Uuid,
2320 _terminal_status: &str,
2321 _terminal_result: &str,
2322 ) -> Result<()> {
2323 Ok(())
2324 }
2325}
2326
2327#[cfg(test)]
2332mod tests {
2333 use super::*;
2334
2335 #[test]
2336 fn test_resolved_image_new() {
2337 let image = ResolvedImage::new("SGVsbG8=", "image/png");
2338 assert_eq!(image.base64, "SGVsbG8=");
2339 assert_eq!(image.media_type, "image/png");
2340 }
2341
2342 #[test]
2343 fn test_resolved_image_to_data_url() {
2344 let image = ResolvedImage::new("SGVsbG8=", "image/png");
2345 let data_url = image.to_data_url();
2346 assert_eq!(data_url, "data:image/png;base64,SGVsbG8=");
2347 }
2348
2349 #[test]
2350 fn test_resolved_image_jpeg() {
2351 let image = ResolvedImage::new("base64data", "image/jpeg");
2352 let data_url = image.to_data_url();
2353 assert!(data_url.starts_with("data:image/jpeg;base64,"));
2354 }
2355}