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 workspace_display_path(path: &str) -> String {
22 if path == "/" {
23 "/workspace".to_string()
24 } else if path.starts_with('/') {
25 format!("/workspace{path}")
26 } else {
27 format!("/workspace/{path}")
28 }
29}
30
31fn build_tool_map(tool_defs: &[ToolDefinition]) -> HashMap<&str, &ToolDefinition> {
33 tool_defs.iter().map(|def| (def.name(), def)).collect()
34}
35
36use crate::error::Result;
37
38#[async_trait]
49pub trait AgentStore: Send + Sync {
50 async fn get_agent(&self, agent_id: AgentId) -> Result<Option<Agent>>;
52}
53
54#[async_trait]
55impl<T: AgentStore + ?Sized> AgentStore for std::sync::Arc<T> {
56 async fn get_agent(&self, agent_id: AgentId) -> Result<Option<Agent>> {
57 (**self).get_agent(agent_id).await
58 }
59}
60
61#[async_trait]
76pub trait HarnessStore: Send + Sync {
77 async fn get_harness_chain(&self, harness_id: HarnessId) -> Result<Vec<Harness>>;
82}
83
84#[async_trait]
85impl<T: HarnessStore + ?Sized> HarnessStore for std::sync::Arc<T> {
86 async fn get_harness_chain(&self, harness_id: HarnessId) -> Result<Vec<Harness>> {
87 (**self).get_harness_chain(harness_id).await
88 }
89}
90
91use crate::leased_resource::{LeasedResource, UpsertLeasedResource};
96use crate::session::Session;
97
98#[async_trait]
104pub trait SessionStore: Send + Sync {
105 async fn get_session(&self, session_id: SessionId) -> Result<Option<Session>>;
107}
108
109#[async_trait]
110impl<T: SessionStore + ?Sized> SessionStore for std::sync::Arc<T> {
111 async fn get_session(&self, session_id: SessionId) -> Result<Option<Session>> {
112 (**self).get_session(session_id).await
113 }
114}
115
116#[async_trait]
118pub trait SessionMutator: Send + Sync {
119 async fn update_session_title(&self, session_id: SessionId, title: String) -> Result<Session>;
121}
122
123#[async_trait]
124impl<T: SessionMutator + ?Sized> SessionMutator for std::sync::Arc<T> {
125 async fn update_session_title(&self, session_id: SessionId, title: String) -> Result<Session> {
126 (**self).update_session_title(session_id, title).await
127 }
128}
129
130#[derive(Debug, Clone)]
136pub struct ResolvedModel {
137 pub model: String,
139 pub provider_type: DriverId,
141 pub api_key: Option<String>,
143 pub base_url: Option<String>,
145 pub provider_metadata: Option<crate::driver_registry::ProviderMetadata>,
148}
149
150#[async_trait]
160pub trait ProviderStore: Send + Sync {
161 async fn get_resolved_model(&self, model_id: ModelId) -> Result<Option<ResolvedModel>>;
166
167 async fn get_default_model(&self) -> Result<Option<ResolvedModel>>;
171}
172
173#[async_trait]
174impl<T: ProviderStore + ?Sized> ProviderStore for std::sync::Arc<T> {
175 async fn get_resolved_model(&self, model_id: ModelId) -> Result<Option<ResolvedModel>> {
176 (**self).get_resolved_model(model_id).await
177 }
178
179 async fn get_default_model(&self) -> Result<Option<ResolvedModel>> {
180 (**self).get_default_model().await
181 }
182}
183
184#[derive(Debug, Clone)]
190pub struct StoredImageInfo {
191 pub id: ImageId,
192 pub filename: String,
193 pub content_type: String,
194 pub size_bytes: i64,
195 pub metadata: serde_json::Value,
196 pub created_at: DateTime<Utc>,
197}
198
199#[derive(Debug, Clone)]
201pub struct StoredImage {
202 pub info: StoredImageInfo,
203 pub data: Vec<u8>,
204}
205
206#[derive(Debug, Clone)]
208pub struct CreateStoredImage {
209 pub filename: String,
210 pub content_type: String,
211 pub data: Vec<u8>,
212 pub metadata: serde_json::Value,
213}
214
215#[async_trait]
216pub trait ImageArtifactStore: Send + Sync {
217 async fn create_image(&self, input: CreateStoredImage) -> Result<StoredImageInfo>;
219
220 async fn get_image(&self, image_id: ImageId) -> Result<Option<StoredImage>>;
222
223 async fn get_image_info(&self, image_id: ImageId) -> Result<Option<StoredImageInfo>>;
225}
226
227#[derive(Debug, Clone)]
233pub struct ProviderCredentials {
234 pub api_key: String,
235 pub base_url: Option<String>,
236}
237
238#[async_trait]
239pub trait ProviderCredentialStore: Send + Sync {
240 async fn get_default_provider_credentials(
245 &self,
246 provider_type: &str,
247 ) -> Result<Option<ProviderCredentials>>;
248}
249
250#[async_trait]
261pub trait ToolExecutor: Send + Sync {
262 async fn execute(&self, tool_call: &ToolCall, tool_def: &ToolDefinition) -> Result<ToolResult>;
267
268 async fn execute_with_context(
273 &self,
274 tool_call: &ToolCall,
275 tool_def: &ToolDefinition,
276 _context: &ToolContext,
277 ) -> Result<ToolResult> {
278 self.execute(tool_call, tool_def).await
280 }
281
282 async fn execute_batch(
284 &self,
285 tool_calls: &[ToolCall],
286 tool_defs: &[ToolDefinition],
287 ) -> Result<Vec<ToolResult>> {
288 let mut results = Vec::with_capacity(tool_calls.len());
289
290 let tool_map = build_tool_map(tool_defs);
291
292 for tool_call in tool_calls {
293 let tool_def = tool_map.get(tool_call.name.as_str()).ok_or_else(|| {
294 crate::error::AgentLoopError::tool(format!(
295 "Tool definition not found: {}",
296 tool_call.name
297 ))
298 })?;
299
300 results.push(self.execute(tool_call, tool_def).await?);
301 }
302
303 Ok(results)
304 }
305
306 async fn execute_parallel(
308 &self,
309 tool_calls: &[ToolCall],
310 tool_defs: &[ToolDefinition],
311 ) -> Result<Vec<ToolResult>>
312 where
313 Self: Sized,
314 {
315 use futures::future::join_all;
316
317 let tool_map = build_tool_map(tool_defs);
318
319 let futures: Vec<_> = tool_calls
320 .iter()
321 .map(|tool_call| async {
322 let tool_def = tool_map.get(tool_call.name.as_str()).ok_or_else(|| {
323 crate::error::AgentLoopError::tool(format!(
324 "Tool definition not found: {}",
325 tool_call.name
326 ))
327 })?;
328 self.execute(tool_call, tool_def).await
329 })
330 .collect();
331
332 let results = join_all(futures).await;
333 results.into_iter().collect()
334 }
335}
336
337#[async_trait]
341impl ToolExecutor for std::sync::Arc<dyn ToolExecutor> {
342 async fn execute(&self, tool_call: &ToolCall, tool_def: &ToolDefinition) -> Result<ToolResult> {
343 (**self).execute(tool_call, tool_def).await
344 }
345
346 async fn execute_with_context(
347 &self,
348 tool_call: &ToolCall,
349 tool_def: &ToolDefinition,
350 context: &ToolContext,
351 ) -> Result<ToolResult> {
352 (**self)
353 .execute_with_context(tool_call, tool_def, context)
354 .await
355 }
356
357 async fn execute_batch(
358 &self,
359 tool_calls: &[ToolCall],
360 tool_defs: &[ToolDefinition],
361 ) -> Result<Vec<ToolResult>> {
362 (**self).execute_batch(tool_calls, tool_defs).await
363 }
364}
365
366#[async_trait]
378pub trait SessionFileSystem: Send + Sync {
379 fn display_root(&self) -> String {
385 "/workspace".to_string()
386 }
387
388 fn display_path(&self, path: &str) -> String {
390 workspace_display_path(path)
391 }
392
393 async fn read_file(&self, session_id: SessionId, path: &str) -> Result<Option<SessionFile>>;
395
396 async fn write_file(
398 &self,
399 session_id: SessionId,
400 path: &str,
401 content: &str,
402 encoding: &str,
403 ) -> Result<SessionFile>;
404
405 async fn write_file_if_content_matches(
410 &self,
411 session_id: SessionId,
412 path: &str,
413 expected_content: &str,
414 expected_encoding: &str,
415 content: &str,
416 encoding: &str,
417 ) -> Result<Option<SessionFile>> {
418 let Some(existing) = self.read_file(session_id, path).await? else {
419 return Ok(None);
420 };
421
422 if existing.is_directory {
423 return Ok(None);
424 }
425
426 let current_content = existing.content.unwrap_or_default();
427 if current_content != expected_content || existing.encoding != expected_encoding {
428 return Ok(None);
429 }
430
431 self.write_file(session_id, path, content, encoding)
432 .await
433 .map(Some)
434 }
435
436 async fn delete_file(&self, session_id: SessionId, path: &str, recursive: bool)
438 -> Result<bool>;
439
440 async fn list_directory(&self, session_id: SessionId, path: &str) -> Result<Vec<FileInfo>>;
442
443 async fn stat_file(&self, session_id: SessionId, path: &str) -> Result<Option<FileStat>>;
445
446 async fn grep_files(
448 &self,
449 session_id: SessionId,
450 pattern: &str,
451 path_pattern: Option<&str>,
452 ) -> Result<Vec<GrepMatch>>;
453
454 async fn create_directory(&self, session_id: SessionId, path: &str) -> Result<FileInfo>;
456
457 async fn seed_initial_file(&self, session_id: SessionId, file: &InitialFile) -> Result<()> {
459 if file.is_readonly {
460 return Err(crate::error::AgentLoopError::store(
461 "read-only initial files require a SessionFileSystem-specific seed implementation",
462 ));
463 }
464 self.write_file(session_id, &file.path, &file.content, &file.encoding)
465 .await?;
466 Ok(())
467 }
468}
469
470pub struct WorkspaceScopedFileSystem {
480 inner: Arc<dyn SessionFileSystem>,
481 key: SessionId,
482}
483
484impl WorkspaceScopedFileSystem {
485 pub fn wrap(
487 inner: Arc<dyn SessionFileSystem>,
488 workspace_id: WorkspaceId,
489 ) -> Arc<dyn SessionFileSystem> {
490 Arc::new(Self {
491 inner,
492 key: SessionId::from_uuid(workspace_id.uuid()),
493 })
494 }
495}
496
497#[async_trait]
498impl SessionFileSystem for WorkspaceScopedFileSystem {
499 async fn read_file(&self, _session_id: SessionId, path: &str) -> Result<Option<SessionFile>> {
500 self.inner.read_file(self.key, path).await
501 }
502 async fn write_file(
503 &self,
504 _session_id: SessionId,
505 path: &str,
506 content: &str,
507 encoding: &str,
508 ) -> Result<SessionFile> {
509 self.inner
510 .write_file(self.key, path, content, encoding)
511 .await
512 }
513 async fn write_file_if_content_matches(
514 &self,
515 _session_id: SessionId,
516 path: &str,
517 expected_content: &str,
518 expected_encoding: &str,
519 content: &str,
520 encoding: &str,
521 ) -> Result<Option<SessionFile>> {
522 self.inner
523 .write_file_if_content_matches(
524 self.key,
525 path,
526 expected_content,
527 expected_encoding,
528 content,
529 encoding,
530 )
531 .await
532 }
533 async fn delete_file(
534 &self,
535 _session_id: SessionId,
536 path: &str,
537 recursive: bool,
538 ) -> Result<bool> {
539 self.inner.delete_file(self.key, path, recursive).await
540 }
541 async fn list_directory(&self, _session_id: SessionId, path: &str) -> Result<Vec<FileInfo>> {
542 self.inner.list_directory(self.key, path).await
543 }
544 async fn stat_file(&self, _session_id: SessionId, path: &str) -> Result<Option<FileStat>> {
545 self.inner.stat_file(self.key, path).await
546 }
547 async fn grep_files(
548 &self,
549 _session_id: SessionId,
550 pattern: &str,
551 path_pattern: Option<&str>,
552 ) -> Result<Vec<GrepMatch>> {
553 self.inner.grep_files(self.key, pattern, path_pattern).await
554 }
555 async fn create_directory(&self, _session_id: SessionId, path: &str) -> Result<FileInfo> {
556 self.inner.create_directory(self.key, path).await
557 }
558 async fn seed_initial_file(&self, _session_id: SessionId, file: &InitialFile) -> Result<()> {
559 self.inner.seed_initial_file(self.key, file).await
560 }
561
562 fn display_root(&self) -> String {
563 self.inner.display_root()
564 }
565
566 fn display_path(&self, path: &str) -> String {
567 self.inner.display_path(path)
568 }
569}
570
571#[async_trait]
572impl<T: SessionFileSystem + ?Sized> SessionFileSystem for std::sync::Arc<T> {
573 fn display_root(&self) -> String {
574 (**self).display_root()
575 }
576
577 fn display_path(&self, path: &str) -> String {
578 (**self).display_path(path)
579 }
580
581 async fn read_file(&self, session_id: SessionId, path: &str) -> Result<Option<SessionFile>> {
582 (**self).read_file(session_id, path).await
583 }
584
585 async fn write_file(
586 &self,
587 session_id: SessionId,
588 path: &str,
589 content: &str,
590 encoding: &str,
591 ) -> Result<SessionFile> {
592 (**self)
593 .write_file(session_id, path, content, encoding)
594 .await
595 }
596
597 async fn write_file_if_content_matches(
598 &self,
599 session_id: SessionId,
600 path: &str,
601 expected_content: &str,
602 expected_encoding: &str,
603 content: &str,
604 encoding: &str,
605 ) -> Result<Option<SessionFile>> {
606 (**self)
607 .write_file_if_content_matches(
608 session_id,
609 path,
610 expected_content,
611 expected_encoding,
612 content,
613 encoding,
614 )
615 .await
616 }
617
618 async fn delete_file(
619 &self,
620 session_id: SessionId,
621 path: &str,
622 recursive: bool,
623 ) -> Result<bool> {
624 (**self).delete_file(session_id, path, recursive).await
625 }
626
627 async fn list_directory(&self, session_id: SessionId, path: &str) -> Result<Vec<FileInfo>> {
628 (**self).list_directory(session_id, path).await
629 }
630
631 async fn stat_file(&self, session_id: SessionId, path: &str) -> Result<Option<FileStat>> {
632 (**self).stat_file(session_id, path).await
633 }
634
635 async fn grep_files(
636 &self,
637 session_id: SessionId,
638 pattern: &str,
639 path_pattern: Option<&str>,
640 ) -> Result<Vec<GrepMatch>> {
641 (**self).grep_files(session_id, pattern, path_pattern).await
642 }
643
644 async fn create_directory(&self, session_id: SessionId, path: &str) -> Result<FileInfo> {
645 (**self).create_directory(session_id, path).await
646 }
647
648 async fn seed_initial_file(&self, session_id: SessionId, file: &InitialFile) -> Result<()> {
649 (**self).seed_initial_file(session_id, file).await
650 }
651}
652
653pub use SessionFileSystem as SessionFileStore;
655
656#[derive(Clone, Default)]
662pub struct SessionFileSystemFactoryContext {
663 values: Arc<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>,
664}
665
666impl SessionFileSystemFactoryContext {
667 pub fn new() -> Self {
668 Self::default()
669 }
670
671 pub fn with<T: Any + Send + Sync>(mut self, value: Arc<T>) -> Self {
672 let values = Arc::make_mut(&mut self.values);
673 values.insert(TypeId::of::<T>(), value);
674 self
675 }
676
677 pub fn get<T: Any + Send + Sync>(&self) -> Option<Arc<T>> {
678 self.values
679 .get(&TypeId::of::<T>())
680 .and_then(|value| value.clone().downcast::<T>().ok())
681 }
682}
683
684#[async_trait]
686pub trait SessionFileSystemFactory: Send + Sync {
687 fn name(&self) -> &'static str {
689 "SessionFileSystemFactory"
690 }
691
692 fn is_disabled(&self) -> bool {
695 false
696 }
697
698 async fn create_session_file_system(
700 &self,
701 context: SessionFileSystemFactoryContext,
702 ) -> Result<Arc<dyn SessionFileSystem>>;
703}
704
705#[derive(Debug, Clone, Default)]
707pub struct DisabledSessionFileSystemFactory;
708
709#[async_trait]
710impl SessionFileSystemFactory for DisabledSessionFileSystemFactory {
711 fn name(&self) -> &'static str {
712 "DisabledSessionFileSystemFactory"
713 }
714
715 fn is_disabled(&self) -> bool {
716 true
717 }
718
719 async fn create_session_file_system(
720 &self,
721 _context: SessionFileSystemFactoryContext,
722 ) -> Result<Arc<dyn SessionFileSystem>> {
723 Err(crate::error::AgentLoopError::config(
724 "session filesystem is disabled",
725 ))
726 }
727}
728
729#[derive(Debug, Clone)]
735pub struct KeyInfo {
736 pub key: String,
737 pub created_at: chrono::DateTime<chrono::Utc>,
738 pub updated_at: chrono::DateTime<chrono::Utc>,
739}
740
741#[derive(Debug, Clone)]
743pub struct SecretInfo {
744 pub name: String,
745 pub created_at: chrono::DateTime<chrono::Utc>,
746 pub updated_at: chrono::DateTime<chrono::Utc>,
747}
748
749#[async_trait]
759pub trait SessionStorageStore: Send + Sync {
760 async fn set_value(&self, session_id: SessionId, key: &str, value: &str) -> Result<()>;
764
765 async fn get_value(&self, session_id: SessionId, key: &str) -> Result<Option<String>>;
767
768 async fn delete_value(&self, session_id: SessionId, key: &str) -> Result<bool>;
770
771 async fn list_keys(&self, session_id: SessionId) -> Result<Vec<KeyInfo>>;
773
774 async fn set_secret(&self, session_id: SessionId, name: &str, value: &str) -> Result<()>;
778
779 async fn get_secret(&self, session_id: SessionId, name: &str) -> Result<Option<String>>;
781
782 async fn delete_secret(&self, session_id: SessionId, name: &str) -> Result<bool>;
784
785 async fn list_secrets(&self, session_id: SessionId) -> Result<Vec<SecretInfo>>;
787}
788
789use crate::session_schedule::SessionSchedule;
794use crate::typed_id::ScheduleId;
795
796#[async_trait]
800pub trait SessionScheduleStore: Send + Sync {
801 async fn create_schedule(
803 &self,
804 session_id: SessionId,
805 description: String,
806 cron_expression: Option<String>,
807 scheduled_at: Option<chrono::DateTime<chrono::Utc>>,
808 timezone: String,
809 ) -> Result<SessionSchedule>;
810
811 async fn cancel_schedule(
813 &self,
814 session_id: SessionId,
815 schedule_id: ScheduleId,
816 ) -> Result<SessionSchedule>;
817
818 async fn list_schedules(&self, session_id: SessionId) -> Result<Vec<SessionSchedule>>;
820
821 async fn count_active_schedules(&self, session_id: SessionId) -> Result<u32>;
823}
824
825#[async_trait]
835pub trait SessionResourceRegistry: Send + Sync {
836 async fn register(
838 &self,
839 entry: crate::session_resource::RegisterSessionResource,
840 ) -> Result<crate::session_resource::SessionResourceEntry>;
841
842 async fn update_status(
844 &self,
845 session_id: SessionId,
846 resource_id: &str,
847 status: crate::session_resource::SessionResourceStatus,
848 ) -> Result<Option<crate::session_resource::SessionResourceEntry>>;
849
850 async fn get(
852 &self,
853 session_id: SessionId,
854 resource_id: &str,
855 ) -> Result<Option<crate::session_resource::SessionResourceEntry>>;
856
857 async fn list(
859 &self,
860 session_id: SessionId,
861 filter: Option<&crate::session_resource::SessionResourceFilter>,
862 ) -> Result<Vec<crate::session_resource::SessionResourceEntry>>;
863
864 async fn deregister(&self, session_id: SessionId, resource_id: &str) -> Result<bool>;
866}
867
868#[async_trait]
878pub trait LeasedResourceStore: Send + Sync {
879 async fn upsert_resource(&self, input: UpsertLeasedResource) -> Result<LeasedResource>;
885
886 async fn release_resource(
892 &self,
893 session_id: SessionId,
894 provider: &str,
895 resource_type: &str,
896 external_id: &str,
897 ) -> Result<Option<LeasedResource>>;
898
899 async fn list_resources(&self, session_id: SessionId) -> Result<Vec<LeasedResource>>;
904}
905
906pub type SessionSqlDbStoreRef = Arc<dyn crate::session_sqldb::SessionSqlDbStore>;
912
913#[async_trait]
918pub trait UserConnectionResolver: Send + Sync {
919 async fn get_connection_token(
922 &self,
923 session_id: SessionId,
924 provider: &str,
925 ) -> Result<Option<String>>;
926
927 async fn get_connection_user(
932 &self,
933 _session_id: SessionId,
934 _provider: &str,
935 ) -> Result<Option<Uuid>> {
936 Ok(None)
937 }
938
939 async fn get_connection_token_for_user(
944 &self,
945 _user_id: Uuid,
946 _provider: &str,
947 ) -> Result<Option<String>> {
948 Ok(None)
949 }
950
951 async fn get_connection_metadata(
954 &self,
955 _session_id: SessionId,
956 _provider: &str,
957 ) -> Result<Option<serde_json::Value>> {
958 Ok(None)
959 }
960}
961
962#[async_trait]
972pub trait BudgetChecker: Send + Sync {
973 async fn check_budgets(&self, session_id: &str) -> Result<crate::budget::BudgetToolResponse>;
975}
976
977#[async_trait]
986pub trait PaymentAuthority: Send + Sync {
987 async fn execute_machine_payment(
988 &self,
989 session_id: SessionId,
990 request: crate::payment::MachinePaymentRequest,
991 ) -> Result<crate::payment::MachinePaymentResponse>;
992}
993
994#[async_trait]
1004pub trait OutboundToolRateLimiter: Send + Sync {
1005 async fn check_org(&self, org_id: &crate::typed_id::OrgId) -> bool;
1007}
1008
1009#[derive(Debug)]
1015pub enum ToolCallClaimResult {
1016 Claimed { claim_token: uuid::Uuid },
1019 AlreadySettled {
1021 result_json: serde_json::Value,
1022 args_fingerprint: String,
1023 },
1024 AlreadyRunning { args_fingerprint: String },
1029 DeterminismViolation {
1033 stored_fingerprint: String,
1034 current_fingerprint: String,
1035 },
1036}
1037
1038#[derive(Debug, Clone)]
1040pub enum DurableToolCallStatus {
1041 Settled { result_json: serde_json::Value },
1043 Interrupted {
1045 result_json: Option<serde_json::Value>,
1046 },
1047 Running,
1049}
1050
1051#[async_trait]
1056pub trait DurableToolResultStore: Send + Sync + 'static {
1057 async fn try_claim_tool_call(
1065 &self,
1066 turn_id: &str,
1067 tool_call_id: &str,
1068 tool_name: &str,
1069 args_fingerprint: &str,
1070 ) -> Result<ToolCallClaimResult>;
1071
1072 async fn settle_tool_call(
1078 &self,
1079 turn_id: &str,
1080 tool_call_id: &str,
1081 result_json: serde_json::Value,
1082 status: &str,
1083 claim_token: uuid::Uuid,
1084 ) -> Result<bool>;
1085
1086 async fn get_tool_call_status(
1091 &self,
1092 turn_id: &str,
1093 tool_call_id: &str,
1094 ) -> Result<Option<DurableToolCallStatus>>;
1095}
1096
1097pub struct NoopDurableToolResultStore;
1100
1101#[async_trait]
1102impl DurableToolResultStore for NoopDurableToolResultStore {
1103 async fn try_claim_tool_call(
1104 &self,
1105 _turn_id: &str,
1106 _tool_call_id: &str,
1107 _tool_name: &str,
1108 _args_fingerprint: &str,
1109 ) -> Result<ToolCallClaimResult> {
1110 Ok(ToolCallClaimResult::Claimed {
1111 claim_token: uuid::Uuid::new_v4(),
1112 })
1113 }
1114
1115 async fn settle_tool_call(
1116 &self,
1117 _turn_id: &str,
1118 _tool_call_id: &str,
1119 _result_json: serde_json::Value,
1120 _status: &str,
1121 _claim_token: uuid::Uuid,
1122 ) -> Result<bool> {
1123 Ok(true)
1124 }
1125
1126 async fn get_tool_call_status(
1127 &self,
1128 _turn_id: &str,
1129 _tool_call_id: &str,
1130 ) -> Result<Option<DurableToolCallStatus>> {
1131 Ok(None)
1132 }
1133}
1134
1135#[derive(Debug, Clone)]
1141pub struct StreamProgress {
1142 pub accumulated_len: usize,
1144 pub last_delta_at: u64,
1146}
1147
1148#[async_trait]
1154pub trait StreamHeartbeater: Send + Sync {
1155 async fn heartbeat(&self, progress: StreamProgress);
1161}
1162
1163pub struct NoopStreamHeartbeater;
1165
1166#[async_trait]
1167impl StreamHeartbeater for NoopStreamHeartbeater {
1168 async fn heartbeat(&self, _progress: StreamProgress) {}
1169}
1170
1171#[derive(Debug, Clone)]
1177pub struct PartialStreamState {
1178 pub accumulated: String,
1181}
1182
1183#[async_trait]
1191pub trait PartialStreamStore: Send + Sync {
1192 async fn get_partial_stream(
1195 &self,
1196 session_id: SessionId,
1197 turn_id: &str,
1198 ) -> Result<Option<PartialStreamState>>;
1199}
1200
1201pub struct NoopPartialStreamStore;
1203
1204#[async_trait]
1205impl PartialStreamStore for NoopPartialStreamStore {
1206 async fn get_partial_stream(
1207 &self,
1208 _session_id: SessionId,
1209 _turn_id: &str,
1210 ) -> Result<Option<PartialStreamState>> {
1211 Ok(None)
1212 }
1213}
1214
1215#[derive(Clone)]
1224pub struct ToolContext {
1225 pub session_id: SessionId,
1227 pub workspace_id: WorkspaceId,
1234
1235 pub file_store: Option<Arc<dyn SessionFileSystem>>,
1237
1238 pub storage_store: Option<Arc<dyn SessionStorageStore>>,
1240
1241 pub image_store: Option<Arc<dyn ImageArtifactStore>>,
1243
1244 pub provider_credential_store: Option<Arc<dyn ProviderCredentialStore>>,
1246
1247 pub utility_llm_service: Option<Arc<dyn crate::UtilityLlmService>>,
1249
1250 pub egress_service: Option<Arc<dyn crate::EgressService>>,
1252
1253 pub sqldb_store: Option<SessionSqlDbStoreRef>,
1255
1256 pub message_retriever: Option<Arc<dyn crate::message_retriever::MessageRetriever>>,
1258
1259 pub session_store: Option<Arc<dyn SessionStore>>,
1261
1262 pub session_mutator: Option<Arc<dyn SessionMutator>>,
1264
1265 pub agent_store: Option<Arc<dyn AgentStore>>,
1267
1268 pub connection_resolver: Option<Arc<dyn UserConnectionResolver>>,
1270
1271 pub schedule_store: Option<Arc<dyn SessionScheduleStore>>,
1273
1274 pub platform_store: Option<Arc<dyn crate::platform_store::PlatformStore>>,
1276 pub leased_resource_store: Option<Arc<dyn LeasedResourceStore>>,
1278
1279 pub session_resource_registry: Option<Arc<dyn SessionResourceRegistry>>,
1281
1282 pub session_task_registry: Option<Arc<dyn crate::session_task::SessionTaskRegistry>>,
1285
1286 pub event_emitter: Option<Arc<dyn EventEmitter>>,
1289
1290 pub event_context: Option<crate::events::EventContext>,
1293
1294 pub tool_call_id: Option<String>,
1297 pub capability_registry: Option<crate::capabilities::CapabilityRegistry>,
1299
1300 pub tool_registry: Option<Arc<crate::tools::ToolRegistry>>,
1303
1304 pub visible_tool_names: Option<Arc<HashSet<String>>>,
1308
1309 pub org_id: Option<crate::typed_id::OrgId>,
1311
1312 pub network_access: Option<crate::network_access::NetworkAccessList>,
1315
1316 pub locale: Option<String>,
1320
1321 pub budget_checker: Option<Arc<dyn BudgetChecker>>,
1323
1324 pub payment_authority: Option<Arc<dyn PaymentAuthority>>,
1326
1327 pub subagent_spawn_store: Option<Arc<dyn SubagentSpawnStore>>,
1331}
1332
1333impl ToolContext {
1334 pub fn workspace_fs_key(&self) -> SessionId {
1339 SessionId::from_uuid(self.workspace_id.uuid())
1340 }
1341
1342 pub fn with_workspace_id(mut self, workspace_id: WorkspaceId) -> Self {
1344 self.workspace_id = workspace_id;
1345 self
1346 }
1347
1348 pub fn new(session_id: SessionId) -> Self {
1350 Self {
1351 session_id,
1352 workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
1353 file_store: None,
1354 storage_store: None,
1355 image_store: None,
1356 provider_credential_store: None,
1357 utility_llm_service: None,
1358 egress_service: None,
1359 sqldb_store: None,
1360 message_retriever: None,
1361 session_store: None,
1362 session_mutator: None,
1363 agent_store: None,
1364 connection_resolver: None,
1365 schedule_store: None,
1366 platform_store: None,
1367 leased_resource_store: None,
1368 session_resource_registry: None,
1369 session_task_registry: None,
1370 event_emitter: None,
1371 event_context: None,
1372 tool_call_id: None,
1373 capability_registry: None,
1374 tool_registry: None,
1375 visible_tool_names: None,
1376 org_id: None,
1377 network_access: None,
1378 locale: None,
1379 budget_checker: None,
1380 payment_authority: None,
1381 subagent_spawn_store: None,
1382 }
1383 }
1384
1385 pub fn with_file_store(session_id: SessionId, file_store: Arc<dyn SessionFileSystem>) -> Self {
1387 Self {
1388 session_id,
1389 workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
1390 file_store: Some(file_store),
1391 storage_store: None,
1392 image_store: None,
1393 provider_credential_store: None,
1394 utility_llm_service: None,
1395 egress_service: None,
1396 sqldb_store: None,
1397 message_retriever: None,
1398 session_store: None,
1399 session_mutator: None,
1400 agent_store: None,
1401 connection_resolver: None,
1402 schedule_store: None,
1403 platform_store: None,
1404 leased_resource_store: None,
1405 session_resource_registry: None,
1406 session_task_registry: None,
1407 event_emitter: None,
1408 event_context: None,
1409 tool_call_id: None,
1410 capability_registry: None,
1411 tool_registry: None,
1412 visible_tool_names: None,
1413 org_id: None,
1414 network_access: None,
1415 locale: None,
1416 budget_checker: None,
1417 payment_authority: None,
1418 subagent_spawn_store: None,
1419 }
1420 }
1421
1422 pub fn with_storage_store(
1424 session_id: SessionId,
1425 storage_store: Arc<dyn SessionStorageStore>,
1426 ) -> Self {
1427 Self {
1428 session_id,
1429 workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
1430 file_store: None,
1431 storage_store: Some(storage_store),
1432 image_store: None,
1433 provider_credential_store: None,
1434 utility_llm_service: None,
1435 egress_service: None,
1436 sqldb_store: None,
1437 message_retriever: None,
1438 session_store: None,
1439 session_mutator: None,
1440 agent_store: None,
1441 connection_resolver: None,
1442 schedule_store: None,
1443 platform_store: None,
1444 leased_resource_store: None,
1445 session_resource_registry: None,
1446 session_task_registry: None,
1447 event_emitter: None,
1448 event_context: None,
1449 tool_call_id: None,
1450 capability_registry: None,
1451 tool_registry: None,
1452 visible_tool_names: None,
1453 org_id: None,
1454 network_access: None,
1455 locale: None,
1456 budget_checker: None,
1457 payment_authority: None,
1458 subagent_spawn_store: None,
1459 }
1460 }
1461
1462 pub fn with_stores(
1464 session_id: SessionId,
1465 file_store: Arc<dyn SessionFileSystem>,
1466 storage_store: Arc<dyn SessionStorageStore>,
1467 ) -> Self {
1468 Self {
1469 session_id,
1470 workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
1471 file_store: Some(file_store),
1472 storage_store: Some(storage_store),
1473 sqldb_store: None,
1474 image_store: None,
1475 provider_credential_store: None,
1476 utility_llm_service: None,
1477 egress_service: None,
1478 message_retriever: None,
1479 session_store: None,
1480 session_mutator: None,
1481 agent_store: None,
1482 connection_resolver: None,
1483 schedule_store: None,
1484 platform_store: None,
1485 leased_resource_store: None,
1486 session_resource_registry: None,
1487 session_task_registry: None,
1488 event_emitter: None,
1489 event_context: None,
1490 tool_call_id: None,
1491 capability_registry: None,
1492 tool_registry: None,
1493 visible_tool_names: None,
1494 org_id: None,
1495 network_access: None,
1496 locale: None,
1497 budget_checker: None,
1498 payment_authority: None,
1499 subagent_spawn_store: None,
1500 }
1501 }
1502
1503 pub fn with_sqldb_store(mut self, sqldb_store: SessionSqlDbStoreRef) -> Self {
1505 self.sqldb_store = Some(sqldb_store);
1506 self
1507 }
1508
1509 pub fn with_message_retriever(
1511 mut self,
1512 retriever: Arc<dyn crate::message_retriever::MessageRetriever>,
1513 ) -> Self {
1514 self.message_retriever = Some(retriever);
1515 self
1516 }
1517
1518 pub fn with_session_store(mut self, store: Arc<dyn SessionStore>) -> Self {
1520 self.session_store = Some(store);
1521 self
1522 }
1523
1524 pub fn with_session_mutator(mut self, mutator: Arc<dyn SessionMutator>) -> Self {
1526 self.session_mutator = Some(mutator);
1527 self
1528 }
1529
1530 pub fn with_agent_store(mut self, store: Arc<dyn AgentStore>) -> Self {
1532 self.agent_store = Some(store);
1533 self
1534 }
1535
1536 pub fn with_connection_resolver(mut self, resolver: Arc<dyn UserConnectionResolver>) -> Self {
1538 self.connection_resolver = Some(resolver);
1539 self
1540 }
1541
1542 pub fn with_image_store(
1544 session_id: SessionId,
1545 image_store: Arc<dyn ImageArtifactStore>,
1546 ) -> Self {
1547 Self {
1548 session_id,
1549 workspace_id: WorkspaceId::from_uuid(session_id.uuid()),
1550 file_store: None,
1551 storage_store: None,
1552 image_store: Some(image_store),
1553 provider_credential_store: None,
1554 utility_llm_service: None,
1555 egress_service: None,
1556 sqldb_store: None,
1557 message_retriever: None,
1558 session_store: None,
1559 session_mutator: None,
1560 agent_store: None,
1561 connection_resolver: None,
1562 schedule_store: None,
1563 platform_store: None,
1564 leased_resource_store: None,
1565 session_resource_registry: None,
1566 session_task_registry: None,
1567 event_emitter: None,
1568 event_context: None,
1569 tool_call_id: None,
1570 capability_registry: None,
1571 tool_registry: None,
1572 visible_tool_names: None,
1573 org_id: None,
1574 network_access: None,
1575 locale: None,
1576 budget_checker: None,
1577 payment_authority: None,
1578 subagent_spawn_store: None,
1579 }
1580 }
1581
1582 pub fn with_provider_credential_store(
1584 mut self,
1585 store: Arc<dyn ProviderCredentialStore>,
1586 ) -> Self {
1587 self.provider_credential_store = Some(store);
1588 self
1589 }
1590
1591 pub fn with_utility_llm_service(mut self, service: Arc<dyn crate::UtilityLlmService>) -> Self {
1593 self.utility_llm_service = Some(service);
1594 self
1595 }
1596
1597 pub fn with_egress_service(mut self, service: Arc<dyn crate::EgressService>) -> Self {
1599 self.egress_service = Some(service);
1600 self
1601 }
1602
1603 pub fn with_egress_service_opt(
1606 mut self,
1607 service: Option<Arc<dyn crate::EgressService>>,
1608 ) -> Self {
1609 if let Some(service) = service {
1610 self.egress_service = Some(service);
1611 }
1612 self
1613 }
1614
1615 pub fn with_storage_store_arc(mut self, store: Arc<dyn SessionStorageStore>) -> Self {
1617 self.storage_store = Some(store);
1618 self
1619 }
1620
1621 pub fn with_schedule_store(mut self, store: Arc<dyn SessionScheduleStore>) -> Self {
1623 self.schedule_store = Some(store);
1624 self
1625 }
1626
1627 pub fn with_platform_store(
1629 mut self,
1630 store: Arc<dyn crate::platform_store::PlatformStore>,
1631 ) -> Self {
1632 self.platform_store = Some(store);
1633 self
1634 }
1635
1636 pub fn with_leased_resource_store(mut self, store: Arc<dyn LeasedResourceStore>) -> Self {
1638 self.leased_resource_store = Some(store);
1639 self
1640 }
1641
1642 pub fn with_session_resource_registry(
1644 mut self,
1645 registry: Arc<dyn SessionResourceRegistry>,
1646 ) -> Self {
1647 self.session_resource_registry = Some(registry);
1648 self
1649 }
1650
1651 pub fn with_session_task_registry(
1653 mut self,
1654 registry: Arc<dyn crate::session_task::SessionTaskRegistry>,
1655 ) -> Self {
1656 self.session_task_registry = Some(registry);
1657 self
1658 }
1659
1660 pub fn with_org_id(mut self, org_id: crate::typed_id::OrgId) -> Self {
1662 self.org_id = Some(org_id);
1663 self
1664 }
1665
1666 pub fn with_tool_registry(mut self, registry: Arc<crate::tools::ToolRegistry>) -> Self {
1668 self.tool_registry = Some(registry);
1669 self
1670 }
1671
1672 pub fn with_visible_tool_names(mut self, names: Arc<HashSet<String>>) -> Self {
1674 self.visible_tool_names = Some(names);
1675 self
1676 }
1677
1678 pub fn with_network_access(
1680 mut self,
1681 network_access: Option<crate::network_access::NetworkAccessList>,
1682 ) -> Self {
1683 self.network_access = network_access;
1684 self
1685 }
1686
1687 pub fn with_payment_authority(mut self, authority: Arc<dyn PaymentAuthority>) -> Self {
1689 self.payment_authority = Some(authority);
1690 self
1691 }
1692
1693 pub fn with_subagent_spawn_store(mut self, store: Arc<dyn SubagentSpawnStore>) -> Self {
1695 self.subagent_spawn_store = Some(store);
1696 self
1697 }
1698
1699 pub async fn emit_progress(&self, tool_name: &str, message: &str) {
1704 let (Some(emitter), Some(ctx), Some(call_id)) =
1705 (&self.event_emitter, &self.event_context, &self.tool_call_id)
1706 else {
1707 return;
1708 };
1709 if let Err(e) = emitter
1710 .emit(EventRequest::new(
1711 self.session_id,
1712 ctx.clone(),
1713 crate::events::ToolProgressData {
1714 tool_call_id: call_id.clone(),
1715 tool_name: tool_name.to_string(),
1716 message: message.to_string(),
1717 display_name: None,
1718 },
1719 ))
1720 .await
1721 {
1722 tracing::debug!(
1723 tool_call_id = call_id,
1724 tool_name,
1725 error = %e,
1726 "Failed to emit tool.progress event"
1727 );
1728 }
1729 }
1730
1731 pub async fn emit_tool_output(&self, tool_name: &str, delta: &str, stream: &str) {
1736 let (Some(emitter), Some(ctx), Some(call_id)) =
1737 (&self.event_emitter, &self.event_context, &self.tool_call_id)
1738 else {
1739 return;
1740 };
1741 if let Err(e) = emitter
1742 .emit(EventRequest::new(
1743 self.session_id,
1744 ctx.clone(),
1745 crate::events::ToolOutputDeltaData {
1746 tool_call_id: call_id.clone(),
1747 tool_name: tool_name.to_string(),
1748 delta: delta.to_string(),
1749 stream: stream.to_string(),
1750 },
1751 ))
1752 .await
1753 {
1754 tracing::debug!(
1755 tool_call_id = call_id,
1756 tool_name,
1757 error = %e,
1758 "Failed to emit tool.output.delta event"
1759 );
1760 }
1761 }
1762}
1763
1764impl std::fmt::Debug for ToolContext {
1765 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1766 f.debug_struct("ToolContext")
1767 .field("session_id", &self.session_id)
1768 .field("file_store", &self.file_store.is_some())
1769 .field("storage_store", &self.storage_store.is_some())
1770 .field("image_store", &self.image_store.is_some())
1771 .field(
1772 "provider_credential_store",
1773 &self.provider_credential_store.is_some(),
1774 )
1775 .field("utility_llm_service", &self.utility_llm_service.is_some())
1776 .field("egress_service", &self.egress_service.is_some())
1777 .field("sqldb_store", &self.sqldb_store.is_some())
1778 .field("message_retriever", &self.message_retriever.is_some())
1779 .field("session_store", &self.session_store.is_some())
1780 .field("session_mutator", &self.session_mutator.is_some())
1781 .field("agent_store", &self.agent_store.is_some())
1782 .field("connection_resolver", &self.connection_resolver.is_some())
1783 .field("schedule_store", &self.schedule_store.is_some())
1784 .field("platform_store", &self.platform_store.is_some())
1785 .field(
1786 "leased_resource_store",
1787 &self.leased_resource_store.is_some(),
1788 )
1789 .field("event_emitter", &self.event_emitter.is_some())
1790 .field("tool_registry", &self.tool_registry.is_some())
1791 .field("payment_authority", &self.payment_authority.is_some())
1792 .field("subagent_spawn_store", &self.subagent_spawn_store.is_some())
1793 .field("org_id", &self.org_id)
1794 .finish()
1795 }
1796}
1797
1798use crate::events::{Event, EventRequest};
1803
1804#[async_trait]
1815pub trait EventEmitter: Send + Sync {
1816 async fn emit(&self, request: EventRequest) -> Result<Event>;
1821}
1822
1823#[async_trait]
1825impl<E: EventEmitter + ?Sized> EventEmitter for Arc<E> {
1826 async fn emit(&self, request: EventRequest) -> Result<Event> {
1827 (**self).emit(request).await
1828 }
1829}
1830
1831#[derive(Debug, Clone, Default)]
1835pub struct NoopEventEmitter;
1836
1837#[async_trait]
1838impl EventEmitter for NoopEventEmitter {
1839 async fn emit(&self, request: EventRequest) -> Result<Event> {
1840 Ok(request.into_event(crate::typed_id::EventId::new(), 0))
1842 }
1843}
1844
1845#[derive(Debug, Clone)]
1858pub struct ResolvedImage {
1859 pub base64: String,
1861 pub media_type: String,
1863}
1864
1865impl ResolvedImage {
1866 pub fn new(base64: impl Into<String>, media_type: impl Into<String>) -> Self {
1868 Self {
1869 base64: base64.into(),
1870 media_type: media_type.into(),
1871 }
1872 }
1873
1874 pub fn to_data_url(&self) -> String {
1878 format!("data:{};base64,{}", self.media_type, self.base64)
1879 }
1880}
1881
1882#[async_trait]
1915pub trait ImageResolver: Send + Sync {
1916 async fn resolve_image(&self, image_id: Uuid) -> Result<Option<ResolvedImage>>;
1920}
1921
1922#[derive(Debug)]
1928pub enum SpawnClaimResult {
1929 Claimed {
1932 spawn_handle_id: uuid::Uuid,
1933 claim_token: uuid::Uuid,
1934 },
1935 ClaimedPendingChild {
1939 spawn_handle_id: uuid::Uuid,
1940 claim_token: uuid::Uuid,
1941 },
1942 AlreadyRunning {
1945 child_session_id: crate::typed_id::SessionId,
1946 claim_token: uuid::Uuid,
1948 },
1949 AlreadySettled {
1952 child_session_id: crate::typed_id::SessionId,
1953 terminal_status: String,
1955 terminal_result: String,
1956 },
1957}
1958
1959#[async_trait]
1967pub trait SubagentSpawnStore: Send + Sync + 'static {
1968 async fn try_claim_spawn(
1973 &self,
1974 parent_session_id: crate::typed_id::SessionId,
1975 tool_call_id: &str,
1976 claim_token: uuid::Uuid,
1977 ) -> Result<SpawnClaimResult>;
1978
1979 async fn register_child_session(
1984 &self,
1985 spawn_handle_id: uuid::Uuid,
1986 claim_token: uuid::Uuid,
1987 child_session_id: crate::typed_id::SessionId,
1988 ) -> Result<()>;
1989
1990 async fn settle_spawn(
1996 &self,
1997 parent_session_id: crate::typed_id::SessionId,
1998 tool_call_id: &str,
1999 claim_token: uuid::Uuid,
2000 terminal_status: &str,
2001 terminal_result: &str,
2002 ) -> Result<()>;
2003}
2004
2005#[async_trait]
2007impl<S: SubagentSpawnStore + ?Sized> SubagentSpawnStore for Arc<S> {
2008 async fn try_claim_spawn(
2009 &self,
2010 parent_session_id: crate::typed_id::SessionId,
2011 tool_call_id: &str,
2012 claim_token: uuid::Uuid,
2013 ) -> Result<SpawnClaimResult> {
2014 (**self)
2015 .try_claim_spawn(parent_session_id, tool_call_id, claim_token)
2016 .await
2017 }
2018
2019 async fn register_child_session(
2020 &self,
2021 spawn_handle_id: uuid::Uuid,
2022 claim_token: uuid::Uuid,
2023 child_session_id: crate::typed_id::SessionId,
2024 ) -> Result<()> {
2025 (**self)
2026 .register_child_session(spawn_handle_id, claim_token, child_session_id)
2027 .await
2028 }
2029
2030 async fn settle_spawn(
2031 &self,
2032 parent_session_id: crate::typed_id::SessionId,
2033 tool_call_id: &str,
2034 claim_token: uuid::Uuid,
2035 terminal_status: &str,
2036 terminal_result: &str,
2037 ) -> Result<()> {
2038 (**self)
2039 .settle_spawn(
2040 parent_session_id,
2041 tool_call_id,
2042 claim_token,
2043 terminal_status,
2044 terminal_result,
2045 )
2046 .await
2047 }
2048}
2049
2050pub struct NoopSubagentSpawnStore;
2054
2055#[async_trait]
2056impl SubagentSpawnStore for NoopSubagentSpawnStore {
2057 async fn try_claim_spawn(
2058 &self,
2059 _parent_session_id: crate::typed_id::SessionId,
2060 _tool_call_id: &str,
2061 claim_token: uuid::Uuid,
2062 ) -> Result<SpawnClaimResult> {
2063 Ok(SpawnClaimResult::Claimed {
2064 spawn_handle_id: uuid::Uuid::new_v4(),
2065 claim_token,
2066 })
2067 }
2068
2069 async fn register_child_session(
2070 &self,
2071 _spawn_handle_id: uuid::Uuid,
2072 _claim_token: uuid::Uuid,
2073 _child_session_id: crate::typed_id::SessionId,
2074 ) -> Result<()> {
2075 Ok(())
2076 }
2077
2078 async fn settle_spawn(
2079 &self,
2080 _parent_session_id: crate::typed_id::SessionId,
2081 _tool_call_id: &str,
2082 _claim_token: uuid::Uuid,
2083 _terminal_status: &str,
2084 _terminal_result: &str,
2085 ) -> Result<()> {
2086 Ok(())
2087 }
2088}
2089
2090#[cfg(test)]
2095mod tests {
2096 use super::*;
2097
2098 #[test]
2099 fn test_resolved_image_new() {
2100 let image = ResolvedImage::new("SGVsbG8=", "image/png");
2101 assert_eq!(image.base64, "SGVsbG8=");
2102 assert_eq!(image.media_type, "image/png");
2103 }
2104
2105 #[test]
2106 fn test_resolved_image_to_data_url() {
2107 let image = ResolvedImage::new("SGVsbG8=", "image/png");
2108 let data_url = image.to_data_url();
2109 assert_eq!(data_url, "data:image/png;base64,SGVsbG8=");
2110 }
2111
2112 #[test]
2113 fn test_resolved_image_jpeg() {
2114 let image = ResolvedImage::new("base64data", "image/jpeg");
2115 let data_url = image.to_data_url();
2116 assert!(data_url.starts_with("data:image/jpeg;base64,"));
2117 }
2118}