1use std::collections::{BTreeSet, HashMap};
7use std::future::Future;
8use std::pin::Pin;
9use std::sync::Arc;
10
11use tokio::sync::{mpsc, RwLock};
12use tokio_util::sync::CancellationToken;
13use tracing::Instrument;
14
15use bamboo_agent_core::tools::ToolExecutor;
16use bamboo_agent_core::{AgentError, AgentEvent, Session};
17use bamboo_domain::ReasoningEffort;
18use bamboo_llm::LLMProvider;
19
20use crate::runtime::config::{
21 AuxiliaryModelConfig, BashCompletionSink, BashResumeHook, GoldConfig, GuardianConfig,
22 GuardianSpawner, ImageFallbackConfig,
23};
24use crate::runtime::execution::runner_lifecycle::finalize_runner;
25use crate::runtime::execution::runner_state::AgentRunner;
26use crate::runtime::model_roster::ModelRoster;
27use crate::runtime::Agent;
28use crate::runtime::{ExecuteRequest, ExecuteRequestBuilder};
29
30pub type SessionCache = std::sync::Arc<
38 dashmap::DashMap<String, std::sync::Arc<parking_lot::RwLock<bamboo_agent_core::Session>>>,
39>;
40
41pub fn read_cached_session(cache: &SessionCache, id: &str) -> Option<bamboo_agent_core::Session> {
49 cache
50 .get(id)
51 .map(|e| e.value().clone())
52 .map(|a| a.read().clone())
53}
54
55const SKILL_CONTEXT_START_MARKER: &str = "<!-- BAMBOO_SKILL_CONTEXT_START -->";
56const TOOL_GUIDE_START_MARKER: &str = "<!-- BAMBOO_TOOL_GUIDE_START -->";
57const EXTERNAL_MEMORY_START_MARKER: &str = "<!-- BAMBOO_EXTERNAL_MEMORY_START -->";
58const TASK_LIST_START_MARKER: &str = "<!-- BAMBOO_TASK_LIST_START -->";
59
60pub struct SessionExecutionOutcome {
67 pub success: bool,
69 pub cancelled: bool,
71 pub error: Option<String>,
73}
74
75impl SessionExecutionOutcome {
76 fn from_result(result: &Result<(), AgentError>) -> Self {
77 match result {
78 Ok(()) => Self {
79 success: true,
80 cancelled: false,
81 error: None,
82 },
83 Err(error) => Self {
84 success: false,
85 cancelled: error.is_cancelled(),
86 error: Some(error.to_string()),
87 },
88 }
89 }
90}
91
92pub type SessionCompletionHook = Box<
102 dyn for<'a> FnOnce(
103 SessionExecutionOutcome,
104 &'a mut Session,
105 ) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>>
106 + Send,
107>;
108
109pub struct SessionExecutionArgs {
115 pub agent: Arc<Agent>,
117 pub session_id: String,
118 pub session: Session,
119
120 pub tools_override: Option<Arc<dyn ToolExecutor>>,
122 pub provider_override: Option<Arc<dyn LLMProvider>>,
123 pub model_roster: ModelRoster,
127 pub reasoning_effort: Option<ReasoningEffort>,
128 pub reasoning_effort_source: String,
129 pub auxiliary_model_resolver:
130 Option<Arc<dyn Fn() -> crate::runtime::config::AuxiliaryModelConfig + Send + Sync>>,
131 pub disabled_filter_resolver:
135 Option<Arc<dyn Fn() -> (BTreeSet<String>, BTreeSet<String>) + Send + Sync>>,
136 pub disabled_tools: Option<BTreeSet<String>>,
137 pub disabled_skill_ids: Option<BTreeSet<String>>,
138 pub selected_skill_ids: Option<Vec<String>>,
139 pub selected_skill_mode: Option<String>,
140 pub cancel_token: CancellationToken,
141 pub mpsc_tx: mpsc::Sender<AgentEvent>,
142 pub image_fallback: Option<ImageFallbackConfig>,
143 pub gold_config: Option<GoldConfig>,
144 pub guardian_config: Option<GuardianConfig>,
146 pub guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
149 pub bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
151 pub bash_completion_sink: Option<Arc<dyn BashCompletionSink>>,
153 pub app_data_dir: Option<std::path::PathBuf>,
154
155 pub runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
157 pub sessions_cache: SessionCache,
158
159 pub on_complete: Option<SessionCompletionHook>,
162}
163
164struct ExecuteRequestParams {
174 tools: Option<Arc<dyn ToolExecutor>>,
175 provider_override: Option<Arc<dyn LLMProvider>>,
176 model_roster: ModelRoster,
177 reasoning_effort: Option<ReasoningEffort>,
178 auxiliary_model_resolver: Option<Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>>,
179 disabled_filter_resolver:
180 Option<Arc<dyn Fn() -> (BTreeSet<String>, BTreeSet<String>) + Send + Sync>>,
181 disabled_tools: Option<BTreeSet<String>>,
182 disabled_skill_ids: Option<BTreeSet<String>>,
183 selected_skill_ids: Option<Vec<String>>,
184 selected_skill_mode: Option<String>,
185 image_fallback: Option<ImageFallbackConfig>,
186 gold_config: Option<GoldConfig>,
187 guardian_config: Option<GuardianConfig>,
188 guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
189 bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
190 bash_completion_sink: Option<Arc<dyn BashCompletionSink>>,
191 app_data_dir: Option<std::path::PathBuf>,
192}
193
194fn build_execute_request(
201 initial_message: String,
202 event_tx: mpsc::Sender<AgentEvent>,
203 cancel_token: CancellationToken,
204 params: ExecuteRequestParams,
205) -> ExecuteRequest {
206 let ExecuteRequestParams {
207 tools,
208 provider_override,
209 model_roster,
210 reasoning_effort,
211 auxiliary_model_resolver,
212 disabled_filter_resolver,
213 disabled_tools,
214 disabled_skill_ids,
215 selected_skill_ids,
216 selected_skill_mode,
217 image_fallback,
218 gold_config,
219 guardian_config,
220 guardian_spawner,
221 bash_resume_hook,
222 bash_completion_sink,
223 app_data_dir,
224 } = params;
225
226 let mut builder = ExecuteRequestBuilder::new(initial_message, event_tx, cancel_token)
227 .model_roster(model_roster)
228 .gold_config(gold_config)
229 .guardian_config(guardian_config)
230 .guardian_spawner(guardian_spawner)
231 .bash_resume_hook(bash_resume_hook)
232 .bash_completion_sink(bash_completion_sink);
233
234 if let Some(tools) = tools {
235 builder = builder.tools(tools);
236 }
237 if let Some(provider_override) = provider_override {
238 builder = builder.provider_override(provider_override);
239 }
240 if let Some(reasoning_effort) = reasoning_effort {
241 builder = builder.reasoning_effort(reasoning_effort);
242 }
243 if let Some(disabled_filter_resolver) = disabled_filter_resolver {
244 builder = builder.disabled_filter_resolver(disabled_filter_resolver);
245 }
246 if let Some(auxiliary_model_resolver) = auxiliary_model_resolver {
247 builder = builder.auxiliary_model_resolver(auxiliary_model_resolver);
248 }
249 if let Some(disabled_tools) = disabled_tools {
250 builder = builder.disabled_tools(disabled_tools);
251 }
252 if let Some(disabled_skill_ids) = disabled_skill_ids {
253 builder = builder.disabled_skill_ids(disabled_skill_ids);
254 }
255 if let Some(selected_skill_ids) = selected_skill_ids {
256 builder = builder.selected_skill_ids(selected_skill_ids);
257 }
258 if let Some(selected_skill_mode) = selected_skill_mode {
259 builder = builder.selected_skill_mode(selected_skill_mode);
260 }
261 if let Some(image_fallback) = image_fallback {
262 builder = builder.image_fallback(image_fallback);
263 }
264 if let Some(app_data_dir) = app_data_dir {
265 builder = builder.app_data_dir(app_data_dir);
266 }
267
268 builder.build()
269}
270
271pub fn spawn_session_execution(args: SessionExecutionArgs) {
280 let span_session_id = args.session_id.clone();
281 let session_span = tracing::info_span!("agent_execution", session_id = %span_session_id);
282
283 tokio::spawn(
284 async move {
285 let SessionExecutionArgs {
286 agent,
287 session_id,
288 mut session,
289 tools_override,
290 provider_override,
291 model_roster,
292 reasoning_effort,
293 reasoning_effort_source,
294 auxiliary_model_resolver,
295 disabled_filter_resolver,
296 disabled_tools,
297 disabled_skill_ids,
298 selected_skill_ids,
299 selected_skill_mode,
300 cancel_token,
301 mpsc_tx,
302 image_fallback,
303 gold_config,
304 guardian_config,
305 guardian_spawner,
306 bash_resume_hook,
307 bash_completion_sink,
308 app_data_dir,
309 runners,
310 sessions_cache,
311 on_complete,
312 } = args;
313
314 let model = model_roster.model.clone().unwrap_or_default();
318
319 let initial_message = initial_user_message_for_session(&session);
320 let selected_skill_ids =
321 selected_skill_ids.or_else(|| selected_skill_ids_for_session(&session));
322 let selected_skill_mode =
323 selected_skill_mode.or_else(|| selected_skill_mode_for_session(&session));
324
325 tracing::info!(
326 "[{}] Using resolved session model: {}, reasoning_effort={}, reasoning_source={}",
327 session_id,
328 model,
329 reasoning_effort
330 .map(ReasoningEffort::as_str)
331 .unwrap_or("none"),
332 reasoning_effort_source
333 );
334
335 crate::session_app::execution_prep::prepare_session_for_execution(
342 &mut session,
343 None,
344 Some(&model),
345 );
346
347 let system_prompt = system_prompt_for_session(&session);
348 if let Some(prompt) = system_prompt.as_ref() {
349 log_base_system_prompt_snapshot(&session_id, prompt);
350 }
351
352 let execute_request = build_execute_request(
353 initial_message,
354 mpsc_tx.clone(),
355 cancel_token,
356 ExecuteRequestParams {
357 tools: tools_override,
358 provider_override,
359 model_roster,
360 reasoning_effort,
361 auxiliary_model_resolver,
362 disabled_filter_resolver,
363 disabled_tools,
364 disabled_skill_ids,
365 selected_skill_ids,
366 selected_skill_mode,
367 image_fallback,
368 gold_config,
369 guardian_config,
370 guardian_spawner,
371 bash_resume_hook,
372 bash_completion_sink,
373 app_data_dir,
374 },
375 );
376
377 let result = agent.execute(&mut session, execute_request).await;
378
379 if let Some(error_event) = terminal_error_event_for_result(&result) {
381 let _ = mpsc_tx.send(error_event).await;
382 }
383
384 let suspended_non_terminal = result.is_ok()
401 && session
402 .metadata
403 .get("runtime.suspend_reason")
404 .is_some_and(|reason| !reason.trim().is_empty());
405 match &result {
406 Ok(()) if suspended_non_terminal => {
407 session.set_last_run_status("suspended");
408 session.clear_last_run_error();
409 }
410 Ok(()) => {
411 session.set_last_run_status("completed");
412 session.clear_last_run_error();
413 }
414 Err(error) if error.is_cancelled() => {
415 session.set_last_run_status("cancelled");
416 session.set_last_run_error(error.to_string());
417 }
418 Err(error) => {
419 session.set_last_run_status("error");
420 session.set_last_run_error(error.to_string());
421 }
422 }
423
424 if let Some(on_complete) = on_complete {
428 on_complete(SessionExecutionOutcome::from_result(&result), &mut session).await;
429 }
430
431 if let Err(error) = agent.persistence().save_runtime_session(&mut session).await {
435 tracing::warn!("[{}] Failed to save session: {}", session_id, error);
436 }
437
438 finalize_runner(&runners, &session_id, &result).await;
444
445 sessions_cache.insert(
447 session_id.clone(),
448 Arc::new(parking_lot::RwLock::new(session)),
449 );
450
451 tracing::info!("[{}] Agent execution completed", session_id);
452 }
453 .instrument(session_span),
454 );
455}
456
457pub fn log_base_system_prompt_snapshot(session_id: &str, prompt: &str) {
459 tracing::info!(
460 "[{}] Base system prompt snapshot: len={} chars, has_skill={}, has_tool_guide={}, has_external_memory={}, has_task_list={}",
461 session_id,
462 prompt.len(),
463 prompt.contains(SKILL_CONTEXT_START_MARKER),
464 prompt.contains(TOOL_GUIDE_START_MARKER),
465 prompt.contains(EXTERNAL_MEMORY_START_MARKER),
466 prompt.contains(TASK_LIST_START_MARKER),
467 );
468
469 tracing::debug!(
470 "[{}] ========== BASE SYSTEM PROMPT SNAPSHOT ==========",
471 session_id
472 );
473 tracing::debug!("[{}] Snapshot length: {} chars", session_id, prompt.len());
474 tracing::debug!("[{}] -----------------------------------", session_id);
475 tracing::debug!("[{}] {}", session_id, prompt);
476 tracing::debug!(
477 "[{}] ========== END BASE SYSTEM PROMPT SNAPSHOT ==========",
478 session_id
479 );
480}
481
482pub fn terminal_error_event_for_result(result: &Result<(), AgentError>) -> Option<AgentEvent> {
484 match result {
485 Ok(_) => None,
486 Err(error) if error.is_cancelled() => Some(AgentEvent::Error {
487 message: "Agent execution cancelled by user".to_string(),
488 }),
489 Err(error) => Some(AgentEvent::Error {
490 message: error.to_string(),
491 }),
492 }
493}
494
495fn system_prompt_for_session(session: &Session) -> Option<String> {
498 session
499 .messages
500 .iter()
501 .find(|message| matches!(message.role, bamboo_agent_core::Role::System))
502 .map(|message| message.content.clone())
503}
504
505fn initial_user_message_for_session(session: &Session) -> String {
506 session
507 .messages
508 .last()
509 .filter(|message| matches!(message.role, bamboo_agent_core::Role::User))
510 .map(|message| message.content.clone())
511 .unwrap_or_default()
512}
513
514fn selected_skill_ids_for_session(session: &Session) -> Option<Vec<String>> {
515 session
516 .metadata
517 .get("selected_skill_ids")
518 .and_then(|raw| bamboo_skills::selection::parse_selected_skill_ids_metadata(raw))
519}
520
521fn selected_skill_mode_for_session(session: &Session) -> Option<String> {
522 let value = session
523 .metadata
524 .get("skill_mode")
525 .or_else(|| session.metadata.get("mode"))?;
526 let trimmed = value.trim();
527 if trimmed.is_empty() {
528 None
529 } else {
530 Some(trimmed.to_string())
531 }
532}