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 finalize_runner(&runners, &session_id, &result).await;
386
387 if let Some(on_complete) = on_complete {
392 on_complete(SessionExecutionOutcome::from_result(&result), &mut session).await;
393 }
394
395 if let Err(error) = agent.persistence().save_runtime_session(&mut session).await {
399 tracing::warn!("[{}] Failed to save session: {}", session_id, error);
400 }
401
402 sessions_cache.insert(
404 session_id.clone(),
405 Arc::new(parking_lot::RwLock::new(session)),
406 );
407
408 tracing::info!("[{}] Agent execution completed", session_id);
409 }
410 .instrument(session_span),
411 );
412}
413
414pub fn log_base_system_prompt_snapshot(session_id: &str, prompt: &str) {
416 tracing::info!(
417 "[{}] Base system prompt snapshot: len={} chars, has_skill={}, has_tool_guide={}, has_external_memory={}, has_task_list={}",
418 session_id,
419 prompt.len(),
420 prompt.contains(SKILL_CONTEXT_START_MARKER),
421 prompt.contains(TOOL_GUIDE_START_MARKER),
422 prompt.contains(EXTERNAL_MEMORY_START_MARKER),
423 prompt.contains(TASK_LIST_START_MARKER),
424 );
425
426 tracing::debug!(
427 "[{}] ========== BASE SYSTEM PROMPT SNAPSHOT ==========",
428 session_id
429 );
430 tracing::debug!("[{}] Snapshot length: {} chars", session_id, prompt.len());
431 tracing::debug!("[{}] -----------------------------------", session_id);
432 tracing::debug!("[{}] {}", session_id, prompt);
433 tracing::debug!(
434 "[{}] ========== END BASE SYSTEM PROMPT SNAPSHOT ==========",
435 session_id
436 );
437}
438
439pub fn terminal_error_event_for_result(result: &Result<(), AgentError>) -> Option<AgentEvent> {
441 match result {
442 Ok(_) => None,
443 Err(error) if error.is_cancelled() => Some(AgentEvent::Error {
444 message: "Agent execution cancelled by user".to_string(),
445 }),
446 Err(error) => Some(AgentEvent::Error {
447 message: error.to_string(),
448 }),
449 }
450}
451
452fn system_prompt_for_session(session: &Session) -> Option<String> {
455 session
456 .messages
457 .iter()
458 .find(|message| matches!(message.role, bamboo_agent_core::Role::System))
459 .map(|message| message.content.clone())
460}
461
462fn initial_user_message_for_session(session: &Session) -> String {
463 session
464 .messages
465 .last()
466 .filter(|message| matches!(message.role, bamboo_agent_core::Role::User))
467 .map(|message| message.content.clone())
468 .unwrap_or_default()
469}
470
471fn selected_skill_ids_for_session(session: &Session) -> Option<Vec<String>> {
472 session
473 .metadata
474 .get("selected_skill_ids")
475 .and_then(|raw| bamboo_skills::selection::parse_selected_skill_ids_metadata(raw))
476}
477
478fn selected_skill_mode_for_session(session: &Session) -> Option<String> {
479 let value = session
480 .metadata
481 .get("skill_mode")
482 .or_else(|| session.metadata.get("mode"))?;
483 let trimmed = value.trim();
484 if trimmed.is_empty() {
485 None
486 } else {
487 Some(trimmed.to_string())
488 }
489}