1use std::collections::BTreeSet;
10use std::sync::Arc;
11
12use tokio::sync::{mpsc, RwLock};
13use tokio_util::sync::CancellationToken;
14
15use bamboo_agent_core::storage::{AttachmentReader, Storage};
16use bamboo_agent_core::tools::ToolExecutor;
17use bamboo_agent_core::{AgentEvent, Role, Session};
18use bamboo_config::PermissionMode;
19use bamboo_domain::ReasoningEffort;
20use bamboo_llm::Config;
21use bamboo_llm::LLMProvider;
22use bamboo_metrics::MetricsCollector;
23use bamboo_skills::SkillManager;
24
25use crate::runtime::config::{
26 AgentLoopConfig, AuxiliaryModelConfig, BashCompletionSink, BashResumeHook,
27 DisabledFilterResolver, GoldConfig, GuardianConfig, GuardianSpawner, ImageFallbackConfig,
28 PromptMemoryFlags,
29};
30use crate::runtime::hooks::HookRunner;
31use crate::runtime::model_roster::{ModelRoster, RoleModel};
32use crate::runtime::runner::run_agent_loop_with_config;
33use bamboo_domain::{RuntimeSessionPersistence, SessionInboxPort};
34
35use crate::session_activation::SessionActivationRouter;
36use crate::session_messaging::SessionMessenger;
37
38#[derive(Clone)]
47pub struct AgentRuntime {
48 pub storage: Arc<dyn Storage>,
49 pub persistence: Arc<dyn RuntimeSessionPersistence>,
50 pub session_inbox: Option<Arc<dyn SessionInboxPort>>,
51 pub activation_router: Option<Arc<SessionActivationRouter>>,
52 pub session_messenger: Option<Arc<SessionMessenger>>,
53 pub attachment_reader: Arc<dyn AttachmentReader>,
54 pub skill_manager: Arc<SkillManager>,
55 pub project_context_resolver: Option<Arc<crate::project_context::ProjectContextResolver>>,
56 pub metrics_collector: MetricsCollector,
57 pub config: Arc<RwLock<Config>>,
58 pub permission_config: Option<Arc<bamboo_tools::permission::PermissionConfig>>,
60 pub permission_mode: PermissionMode,
62
63 pub provider: Arc<dyn LLMProvider>,
65
66 pub default_tools: Arc<dyn ToolExecutor>,
70
71 pub hook_runner: Arc<HookRunner>,
74}
75
76pub struct AgentRuntimeBuilder {
91 storage: Option<Arc<dyn Storage>>,
92 persistence: Option<Arc<dyn RuntimeSessionPersistence>>,
93 session_inbox: Option<Arc<dyn SessionInboxPort>>,
94 activation_router: Option<Arc<SessionActivationRouter>>,
95 session_messenger: Option<Arc<SessionMessenger>>,
96 attachment_reader: Option<Arc<dyn AttachmentReader>>,
97 skill_manager: Option<Arc<SkillManager>>,
98 project_context_resolver: Option<Arc<crate::project_context::ProjectContextResolver>>,
99 metrics_collector: Option<MetricsCollector>,
100 config: Option<Arc<RwLock<Config>>>,
101 permission_config: Option<Arc<bamboo_tools::permission::PermissionConfig>>,
102 permission_mode: PermissionMode,
103 provider: Option<Arc<dyn LLMProvider>>,
104 default_tools: Option<Arc<dyn ToolExecutor>>,
105 hook_runner: Arc<HookRunner>,
106}
107
108impl AgentRuntimeBuilder {
109 pub fn new() -> Self {
110 Self {
111 storage: None,
112 persistence: None,
113 session_inbox: None,
114 activation_router: None,
115 session_messenger: None,
116 attachment_reader: None,
117 skill_manager: None,
118 project_context_resolver: None,
119 metrics_collector: None,
120 config: None,
121 permission_config: None,
122 permission_mode: PermissionMode::Default,
123 provider: None,
124 default_tools: None,
125 hook_runner: Arc::new(HookRunner::new()),
126 }
127 }
128
129 pub fn storage(mut self, v: Arc<dyn Storage>) -> Self {
130 self.storage = Some(v);
131 self
132 }
133
134 pub fn persistence(mut self, v: Arc<dyn RuntimeSessionPersistence>) -> Self {
135 self.persistence = Some(v);
136 self
137 }
138
139 pub fn session_inbox(mut self, v: Arc<dyn SessionInboxPort>) -> Self {
140 self.session_inbox = Some(v);
141 self
142 }
143
144 pub fn activation_router(mut self, v: Arc<SessionActivationRouter>) -> Self {
145 self.activation_router = Some(v);
146 self
147 }
148
149 pub fn session_messenger(mut self, v: Arc<SessionMessenger>) -> Self {
150 self.session_messenger = Some(v);
151 self
152 }
153
154 pub fn attachment_reader(mut self, v: Arc<dyn AttachmentReader>) -> Self {
155 self.attachment_reader = Some(v);
156 self
157 }
158
159 pub fn skill_manager(mut self, v: Arc<SkillManager>) -> Self {
160 self.skill_manager = Some(v);
161 self
162 }
163
164 pub fn project_context_resolver(
165 mut self,
166 v: Arc<crate::project_context::ProjectContextResolver>,
167 ) -> Self {
168 self.project_context_resolver = Some(v);
169 self
170 }
171
172 pub fn metrics_collector(mut self, v: MetricsCollector) -> Self {
173 self.metrics_collector = Some(v);
174 self
175 }
176
177 pub fn config(mut self, v: Arc<RwLock<Config>>) -> Self {
178 self.config = Some(v);
179 self
180 }
181
182 pub fn permission_config(mut self, v: Arc<bamboo_tools::permission::PermissionConfig>) -> Self {
183 self.permission_config = Some(v);
184 self
185 }
186
187 pub fn permission_mode(mut self, v: PermissionMode) -> Self {
188 self.permission_mode = v;
189 self
190 }
191
192 pub fn provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
193 self.provider = Some(v);
194 self
195 }
196
197 pub fn default_tools(mut self, v: Arc<dyn ToolExecutor>) -> Self {
198 self.default_tools = Some(v);
199 self
200 }
201
202 pub fn hook_runner(mut self, v: Arc<HookRunner>) -> Self {
204 self.hook_runner = v;
205 self
206 }
207
208 pub fn build(self) -> Result<AgentRuntime, &'static str> {
209 if let (Some(router), Some(inbox)) = (&self.activation_router, &self.session_inbox) {
210 router.set_inbox(inbox.clone());
211 }
212 Ok(AgentRuntime {
213 storage: self.storage.ok_or_else(|| format_missing("storage"))?,
214 persistence: self
215 .persistence
216 .ok_or_else(|| format_missing("persistence"))?,
217 session_inbox: self.session_inbox,
218 activation_router: self.activation_router,
219 session_messenger: self.session_messenger,
220 attachment_reader: self
221 .attachment_reader
222 .ok_or_else(|| format_missing("attachment_reader"))?,
223 skill_manager: self
224 .skill_manager
225 .ok_or_else(|| format_missing("skill_manager"))?,
226 project_context_resolver: self.project_context_resolver,
227 metrics_collector: self
228 .metrics_collector
229 .ok_or_else(|| format_missing("metrics_collector"))?,
230 config: self.config.ok_or_else(|| format_missing("config"))?,
231 permission_config: self.permission_config,
232 permission_mode: self.permission_mode,
233 provider: self.provider.ok_or_else(|| format_missing("provider"))?,
234 default_tools: self
235 .default_tools
236 .ok_or_else(|| format_missing("default_tools"))?,
237 hook_runner: self.hook_runner,
238 })
239 }
240}
241
242fn format_missing(field: &str) -> &'static str {
243 match field {
246 "storage" => "AgentRuntimeBuilder: missing storage",
247 "persistence" => "AgentRuntimeBuilder: missing persistence",
248 "attachment_reader" => "AgentRuntimeBuilder: missing attachment_reader",
249 "skill_manager" => "AgentRuntimeBuilder: missing skill_manager",
250 "metrics_collector" => "AgentRuntimeBuilder: missing metrics_collector",
251 "config" => "AgentRuntimeBuilder: missing config",
252 "provider" => "AgentRuntimeBuilder: missing provider",
253 "default_tools" => "AgentRuntimeBuilder: missing default_tools",
254 _ => "AgentRuntimeBuilder: missing required field",
255 }
256}
257
258impl Default for AgentRuntimeBuilder {
259 fn default() -> Self {
260 Self::new()
261 }
262}
263
264pub struct ExecuteRequest {
274 pub initial_message: String,
276 pub event_tx: mpsc::Sender<AgentEvent>,
277 pub cancel_token: CancellationToken,
278
279 pub tools: Option<Arc<dyn ToolExecutor>>,
283 pub provider_override: Option<Arc<dyn LLMProvider>>,
286
287 pub model_roster: ModelRoster,
294 pub reasoning_effort: Option<ReasoningEffort>,
295 pub auxiliary_model_resolver: Option<Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>>,
298 pub disabled_filter_resolver: Option<DisabledFilterResolver>,
301 pub disabled_tools: Option<BTreeSet<String>>,
303 pub disabled_skill_ids: Option<BTreeSet<String>>,
305 pub selected_skill_ids: Option<Vec<String>>,
306 pub selected_skill_mode: Option<String>,
307 pub image_fallback: Option<ImageFallbackConfig>,
308 pub gold_config: Option<GoldConfig>,
309 pub guardian_config: Option<GuardianConfig>,
311 pub guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
314 pub bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
317 pub bash_completion_sink: Option<Arc<dyn BashCompletionSink>>,
320 pub app_data_dir: Option<std::path::PathBuf>,
322 pub run_budget: Option<bamboo_config::RunBudgetConfig>,
330}
331
332pub struct ExecuteRequestBuilder {
348 initial_message: String,
349 event_tx: mpsc::Sender<AgentEvent>,
350 cancel_token: CancellationToken,
351
352 tools: Option<Arc<dyn ToolExecutor>>,
353 provider_override: Option<Arc<dyn LLMProvider>>,
354 model: Option<String>,
358 provider_name: Option<String>,
359 provider_type: Option<String>,
360 fast_model: Option<String>,
361 fast_model_provider: Option<Arc<dyn LLMProvider>>,
362 background_model: Option<String>,
363 background_model_provider: Option<Arc<dyn LLMProvider>>,
364 summarization_model: Option<String>,
365 summarization_model_provider: Option<Arc<dyn LLMProvider>>,
366 reasoning_effort: Option<ReasoningEffort>,
367 auxiliary_model_resolver: Option<Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>>,
368 disabled_filter_resolver: Option<DisabledFilterResolver>,
369 disabled_tools: Option<BTreeSet<String>>,
370 disabled_skill_ids: Option<BTreeSet<String>>,
371 selected_skill_ids: Option<Vec<String>>,
372 selected_skill_mode: Option<String>,
373 image_fallback: Option<ImageFallbackConfig>,
374 gold_config: Option<GoldConfig>,
375 guardian_config: Option<GuardianConfig>,
376 guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
377 bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
378 bash_completion_sink: Option<Arc<dyn BashCompletionSink>>,
379 app_data_dir: Option<std::path::PathBuf>,
380 run_budget: Option<bamboo_config::RunBudgetConfig>,
381}
382
383impl ExecuteRequestBuilder {
384 pub fn new(
387 initial_message: impl Into<String>,
388 event_tx: mpsc::Sender<AgentEvent>,
389 cancel_token: CancellationToken,
390 ) -> Self {
391 Self {
392 initial_message: initial_message.into(),
393 event_tx,
394 cancel_token,
395 tools: None,
396 provider_override: None,
397 model: None,
398 provider_name: None,
399 provider_type: None,
400 fast_model: None,
401 fast_model_provider: None,
402 background_model: None,
403 background_model_provider: None,
404 summarization_model: None,
405 summarization_model_provider: None,
406 reasoning_effort: None,
407 auxiliary_model_resolver: None,
408 disabled_filter_resolver: None,
409 disabled_tools: None,
410 disabled_skill_ids: None,
411 selected_skill_ids: None,
412 selected_skill_mode: None,
413 image_fallback: None,
414 gold_config: None,
415 guardian_config: None,
416 guardian_spawner: None,
417 bash_resume_hook: None,
418 bash_completion_sink: None,
419 app_data_dir: None,
420 run_budget: None,
421 }
422 }
423
424 pub fn tools(mut self, v: Arc<dyn ToolExecutor>) -> Self {
426 self.tools = Some(v);
427 self
428 }
429
430 pub fn provider_override(mut self, v: Arc<dyn LLMProvider>) -> Self {
432 self.provider_override = Some(v);
433 self
434 }
435
436 pub fn model_roster(mut self, roster: ModelRoster) -> Self {
442 self.fast_model = roster.fast_model();
443 self.fast_model_provider = roster.fast_model_provider();
444 self.background_model = roster.background_model();
445 self.background_model_provider = roster.background_model_provider();
446 self.summarization_model = roster.summarization_model();
447 self.summarization_model_provider = roster.summarization_model_provider();
448 self.model = roster.model;
449 self.provider_name = roster.provider_name;
450 self.provider_type = roster.provider_type;
451 self
452 }
453
454 pub fn model(mut self, v: impl Into<String>) -> Self {
456 self.model = Some(v.into());
457 self
458 }
459
460 pub fn provider_name(mut self, v: impl Into<String>) -> Self {
462 self.provider_name = Some(v.into());
463 self
464 }
465
466 pub fn provider_type(mut self, v: impl Into<String>) -> Self {
468 self.provider_type = Some(v.into());
469 self
470 }
471
472 pub fn fast_model(mut self, v: impl Into<String>) -> Self {
474 self.fast_model = Some(v.into());
475 self
476 }
477
478 pub fn fast_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
480 self.fast_model_provider = Some(v);
481 self
482 }
483
484 pub fn background_model(mut self, v: impl Into<String>) -> Self {
486 self.background_model = Some(v.into());
487 self
488 }
489
490 pub fn background_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
492 self.background_model_provider = Some(v);
493 self
494 }
495
496 pub fn summarization_model(mut self, v: impl Into<String>) -> Self {
498 self.summarization_model = Some(v.into());
499 self
500 }
501
502 pub fn summarization_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
504 self.summarization_model_provider = Some(v);
505 self
506 }
507
508 pub fn reasoning_effort(mut self, v: ReasoningEffort) -> Self {
510 self.reasoning_effort = Some(v);
511 self
512 }
513
514 pub fn auxiliary_model_resolver(
516 mut self,
517 v: Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>,
518 ) -> Self {
519 self.auxiliary_model_resolver = Some(v);
520 self
521 }
522
523 pub fn disabled_filter_resolver(mut self, v: DisabledFilterResolver) -> Self {
525 self.disabled_filter_resolver = Some(v);
526 self
527 }
528
529 pub fn disabled_tools(mut self, v: BTreeSet<String>) -> Self {
531 self.disabled_tools = Some(v);
532 self
533 }
534
535 pub fn disabled_skill_ids(mut self, v: BTreeSet<String>) -> Self {
537 self.disabled_skill_ids = Some(v);
538 self
539 }
540
541 pub fn selected_skill_ids(mut self, v: Vec<String>) -> Self {
543 self.selected_skill_ids = Some(v);
544 self
545 }
546
547 pub fn selected_skill_mode(mut self, v: impl Into<String>) -> Self {
549 self.selected_skill_mode = Some(v.into());
550 self
551 }
552
553 pub fn image_fallback(mut self, v: ImageFallbackConfig) -> Self {
555 self.image_fallback = Some(v);
556 self
557 }
558
559 pub(crate) fn gold_config(mut self, v: Option<GoldConfig>) -> Self {
566 self.gold_config = v;
567 self
568 }
569
570 pub(crate) fn guardian_config(mut self, v: Option<GuardianConfig>) -> Self {
573 self.guardian_config = v;
574 self
575 }
576
577 pub(crate) fn guardian_spawner(mut self, v: Option<Arc<dyn GuardianSpawner>>) -> Self {
580 self.guardian_spawner = v;
581 self
582 }
583
584 pub(crate) fn bash_resume_hook(mut self, v: Option<Arc<dyn BashResumeHook>>) -> Self {
587 self.bash_resume_hook = v;
588 self
589 }
590
591 pub(crate) fn bash_completion_sink(mut self, v: Option<Arc<dyn BashCompletionSink>>) -> Self {
594 self.bash_completion_sink = v;
595 self
596 }
597
598 pub fn app_data_dir(mut self, v: std::path::PathBuf) -> Self {
600 self.app_data_dir = Some(v);
601 self
602 }
603
604 pub fn run_budget(mut self, v: bamboo_config::RunBudgetConfig) -> Self {
609 self.run_budget = Some(v);
610 self
611 }
612
613 pub fn build(self) -> ExecuteRequest {
618 let model_roster = ModelRoster {
619 model: self.model,
620 provider_name: self.provider_name,
621 provider_type: self.provider_type,
622 fast: RoleModel::from_parts(self.fast_model, self.fast_model_provider),
623 background: RoleModel::from_parts(
624 self.background_model,
625 self.background_model_provider,
626 ),
627 summarization: RoleModel::from_parts(
628 self.summarization_model,
629 self.summarization_model_provider,
630 ),
631 };
632 ExecuteRequest {
633 initial_message: self.initial_message,
634 event_tx: self.event_tx,
635 cancel_token: self.cancel_token,
636 tools: self.tools,
637 provider_override: self.provider_override,
638 model_roster,
639 reasoning_effort: self.reasoning_effort,
640 auxiliary_model_resolver: self.auxiliary_model_resolver,
641 disabled_filter_resolver: self.disabled_filter_resolver,
642 disabled_tools: self.disabled_tools,
643 disabled_skill_ids: self.disabled_skill_ids,
644 selected_skill_ids: self.selected_skill_ids,
645 selected_skill_mode: self.selected_skill_mode,
646 image_fallback: self.image_fallback,
647 gold_config: self.gold_config,
648 guardian_config: self.guardian_config,
649 guardian_spawner: self.guardian_spawner,
650 bash_resume_hook: self.bash_resume_hook,
651 bash_completion_sink: self.bash_completion_sink,
652 app_data_dir: self.app_data_dir,
653 run_budget: self.run_budget,
654 }
655 }
656}
657
658fn extract_system_prompt(session: &Session) -> Option<String> {
664 session
665 .messages
666 .iter()
667 .find(|m| matches!(m.role, Role::System))
668 .map(|m| m.content.clone())
669}
670
671impl AgentRuntime {
676 pub async fn execute(
681 &self,
682 session: &mut Session,
683 req: ExecuteRequest,
684 ) -> crate::runtime::runner::Result<()> {
685 let session_activation_notifications = match self.activation_router.as_ref() {
686 Some(router) => Some(Arc::new(parking_lot::Mutex::new(
687 router.subscribe(&session.id).await,
688 ))),
689 None => None,
690 };
691 let system_prompt = extract_system_prompt(session);
692 let config = self.config.read().await;
693 let ExecuteRequest {
694 initial_message,
695 event_tx,
696 cancel_token,
697 tools,
698 provider_override,
699 model_roster,
700 reasoning_effort,
701 auxiliary_model_resolver,
702 disabled_filter_resolver,
703 disabled_tools,
704 disabled_skill_ids,
705 selected_skill_ids,
706 selected_skill_mode,
707 image_fallback,
708 gold_config,
709 guardian_config,
710 guardian_spawner,
711 bash_resume_hook,
712 bash_completion_sink,
713 app_data_dir,
714 run_budget,
715 } = req;
716 let tools = tools.unwrap_or_else(|| self.default_tools.clone());
717 let llm = provider_override.unwrap_or_else(|| self.provider.clone());
718 let configured_permission_mode = self
719 .permission_config
720 .as_ref()
721 .map(|config| config.mode())
722 .unwrap_or(self.permission_mode);
723 let active_plan_gate = session
724 .agent_runtime_state
725 .as_ref()
726 .is_some_and(|state| state.plan_mode.is_some());
727
728 let fast_model = model_roster.fast_model();
733 let fast_model_provider = model_roster.fast_model_provider();
734 let background_model = model_roster.background_model();
735 let background_model_provider = model_roster.background_model_provider();
736 let summarization_model = model_roster.summarization_model();
737 let summarization_model_provider = model_roster.summarization_model_provider();
738 let ModelRoster {
739 model,
740 provider_name,
741 provider_type,
742 ..
743 } = model_roster;
744 let hook_runner = Arc::new(
745 self.hook_runner
746 .with_lifecycle_config(&config.lifecycle_hooks, app_data_dir.clone()),
747 );
748
749 let loop_config = AgentLoopConfig {
750 max_rounds: 200,
751 system_prompt,
752 legacy_model_limits: config.extra.get("model_limits").cloned(),
755 disabled_skill_ids: disabled_skill_ids.unwrap_or_else(|| config.disabled_skill_ids()),
756 selected_skill_ids,
757 selected_skill_mode,
758 skill_manager: Some(self.skill_manager.clone()),
759 project_context_resolver: self.project_context_resolver.clone(),
760 skip_initial_user_message: true,
761 storage: Some(self.storage.clone()),
762 persistence: Some(self.persistence.clone()),
763 session_inbox: self.session_inbox.clone(),
764 session_activation_notifications,
765 attachment_reader: Some(self.attachment_reader.clone()),
766 metrics_collector: Some(self.metrics_collector.clone()),
767 model_name: model,
768 fast_model_name: fast_model.or_else(|| config.get_fast_model()),
769 fast_model_provider,
770 background_model_name: background_model
771 .or_else(|| config.get_memory_background_model()),
772 planning_model_name: config
773 .defaults
774 .as_ref()
775 .and_then(|d| d.planning.as_ref())
776 .map(|r| r.model.clone()),
777 search_model_name: config
778 .defaults
779 .as_ref()
780 .and_then(|d| d.search.as_ref().or(d.fast.as_ref()))
781 .map(|r| r.model.clone()),
782 compression_instructions: None,
783 summary_target_ratio: config
784 .memory()
785 .as_ref()
786 .map(|memory| memory.summary_target_ratio)
787 .unwrap_or(0.20),
788 summary_safe_window_percent: config
789 .memory()
790 .as_ref()
791 .map(|memory| memory.summary_safe_window_percent)
792 .unwrap_or(80),
793 summarization_model_name: summarization_model
794 .or_else(|| config.get_task_summary_model()),
795 background_model_provider,
796 summarization_model_provider,
797 provider_name: Some(provider_name.unwrap_or_else(|| config.provider.clone())),
798 provider_type,
799 reasoning_effort,
800 auxiliary_model_resolver,
801 disabled_filter_resolver,
802 disabled_tools: {
803 let mut merged = config.disabled_tool_names();
804 if let Some(dt) = disabled_tools {
805 merged.extend(dt);
806 }
807 merged
808 },
809 image_fallback,
810 app_data_dir,
811 prompt_memory_flags: config
812 .memory()
813 .as_ref()
814 .map(PromptMemoryFlags::from)
815 .unwrap_or_default(),
816 features_dynamic_model_routing: config.features.dynamic_model_routing,
817 permission_mode: Some(if active_plan_gate {
818 PermissionMode::Plan
819 } else {
820 configured_permission_mode
821 }),
822 gold_config,
823 guardian_config,
824 guardian_spawner,
825 bash_resume_hook,
826 bash_completion_sink,
827 hook_runner,
828 mcp_tool_guidance: tools.tool_guidance(),
832 run_budget: config.run_budget.merged_with_override(run_budget.as_ref()),
838 stream_timeout: config.stream_timeout,
839 ..Default::default()
840 };
841
842 drop(config);
843
844 let session_end_runner = loop_config.hook_runner.clone();
845 let session_end_event_tx = event_tx.clone();
846 let result = run_agent_loop_with_config(
847 session,
848 initial_message,
849 event_tx,
850 llm,
851 tools,
852 cancel_token,
853 loop_config,
854 )
855 .await;
856
857 crate::runtime::hooks::run_session_end_hooks(
858 &session_end_runner,
859 &result,
860 session,
861 &session_end_event_tx,
862 )
863 .await;
864
865 if let Err(checkpoint_error) = self.persistence.checkpoint_runtime_session(session).await {
877 match &result {
878 Ok(()) => tracing::warn!(
879 session_id = %session.id,
880 error = %checkpoint_error,
881 "failed to checkpoint session transcript after successful execution"
882 ),
883 Err(execution_error) => tracing::warn!(
884 session_id = %session.id,
885 error = %checkpoint_error,
886 execution_error = %execution_error,
887 "failed to checkpoint session transcript after execution error"
888 ),
889 }
890 }
891
892 result
893 }
894}