use actix_web::{web, HttpResponse};
use std::collections::BTreeSet;
use tokio::sync::mpsc;
use super::build_sync_info_from_session;
use super::response::{already_running_response, internal_server_error_response, started_response};
use crate::app_state::AppState;
use crate::handlers::agent::execute::runtime::{
reserve_runner, spawn_agent_execution, spawn_event_forwarder, RunnerReservation,
SpawnAgentExecution,
};
use bamboo_engine::model_areas::resolve_global_area_models;
use bamboo_engine::model_config_helper::{resolve_gold_config, GOLD_CONFIG_METADATA_KEY};
use bamboo_engine::session_app::provider_model::session_effective_model_ref;
use bamboo_engine::session_app::types::ExecutionConfigSnapshot;
use bamboo_engine::ImageFallbackConfig;
use bamboo_llm::Config;
pub(super) struct ReadyExecution {
pub session: bamboo_agent_core::Session,
pub effective_model: String,
pub effective_reasoning_effort: Option<bamboo_domain::reasoning::ReasoningEffort>,
pub model_source: &'static str,
pub reasoning_source: &'static str,
pub is_child_session: bool,
pub no_human_approver: bool,
}
pub(super) async fn handle_execute_ready(
state: &web::Data<AppState>,
session_id: &str,
ready: ReadyExecution,
config: &ExecutionConfigSnapshot,
config_snapshot: &Config,
image_fallback: Option<ImageFallbackConfig>,
disabled_tools_vec: Vec<String>,
disabled_skill_ids_vec: Vec<String>,
) -> HttpResponse {
let mut session = ready.session;
apply_no_human_approver(&mut session, ready.no_human_approver);
let session_tx = state.get_session_event_sender(session_id).await;
let (cancel_token, run_id) =
match reserve_runner(state.get_ref(), session_id, &session_tx).await {
RunnerReservation::Started(token, rid) => {
tracing::debug!(
"[{}] Execute Ready -> runner reserved & STARTING (run_id={})",
session_id,
rid
);
(token, rid)
}
RunnerReservation::AlreadyRunning(rid) => {
tracing::debug!(
"[{}] Execute Ready -> runner AlreadyRunning (run_id={}); not spawning",
session_id,
rid
);
let sync_info = build_sync_info_from_session(&session);
return already_running_response(session_id, sync_info, Some(rid));
}
};
if let Err(error) = state.persistence.merge_save_runtime(&mut session).await {
return internal_server_error_response(format!(
"Failed to persist session config before execute: {}",
error
));
}
state.sessions.insert(
session_id.to_string(),
std::sync::Arc::new(parking_lot::RwLock::new(session.clone())),
);
if crate::title_gen::is_untitled(&session.title)
&& session
.messages
.iter()
.any(|m| matches!(m.role, bamboo_agent_core::Role::User))
{
crate::title_gen::spawn_title_generation(
state.clone().into_inner(),
session_id.to_string(),
);
}
let disabled_tools: BTreeSet<String> = disabled_tools_vec.into_iter().collect();
let disabled_skill_ids: BTreeSet<String> = disabled_skill_ids_vec.into_iter().collect();
let resolved_provider_name = session_effective_model_ref(&session)
.map(|model_ref| model_ref.provider)
.unwrap_or_else(|| config.provider_name.clone());
let resolved_provider_type = bamboo_engine::model_config_helper::resolve_provider_type(
config_snapshot,
&resolved_provider_name,
&state.provider_registry,
);
let areas = resolve_global_area_models(
config_snapshot,
&resolved_provider_name,
&state.provider_registry,
);
let sync_info = build_sync_info_from_session(&session);
tracing::info!(
"[{}] Starting agent execution with provider={}, model={}, model_source={}, reasoning_effort={}, reasoning_source={}",
session_id,
resolved_provider_name,
ready.effective_model,
ready.model_source,
ready
.effective_reasoning_effort
.map(bamboo_domain::reasoning::ReasoningEffort::as_str)
.unwrap_or("none"),
ready.reasoning_source
);
let gold_config = resolve_gold_config(
config_snapshot,
session
.metadata
.get(GOLD_CONFIG_METADATA_KEY)
.map(String::as_str),
);
let (mpsc_tx, mpsc_rx) = mpsc::channel::<bamboo_agent_core::AgentEvent>(100);
spawn_event_forwarder(
state.clone(),
session_id.to_string(),
mpsc_rx,
session_tx.clone(),
gold_config.clone(),
);
let model_roster = bamboo_engine::ModelRoster::from_areas(
Some(ready.effective_model),
Some(resolved_provider_name.clone()),
resolved_provider_type,
areas,
);
spawn_agent_execution(SpawnAgentExecution {
state: state.clone(),
session_id: session_id.to_string(),
session,
is_child_session: ready.is_child_session,
provider_name: resolved_provider_name,
provider_override: None,
model_roster,
reasoning_effort: ready.effective_reasoning_effort,
reasoning_effort_source: ready.reasoning_source.to_string(),
disabled_tools,
disabled_skill_ids,
cancel_token,
mpsc_tx,
image_fallback,
gold_config,
app_data_dir: Some(state.app_data_dir.clone()),
});
started_response(session_id, sync_info, run_id)
}
fn apply_no_human_approver(session: &mut bamboo_agent_core::Session, no_human_approver: bool) {
session
.agent_runtime_state
.get_or_insert_with(bamboo_domain::AgentRuntimeState::default)
.no_human_approver = no_human_approver;
}
#[cfg(test)]
mod ready_tests {
use super::apply_no_human_approver;
use bamboo_agent_core::Session;
use bamboo_domain::AgentRuntimeState;
#[test]
fn interactive_execute_resets_persisted_no_human_approver_to_false() {
let mut session = Session::new("sess-cross-mode", "test-model");
let mut prev = AgentRuntimeState::new("run-prev");
prev.no_human_approver = true;
session.agent_runtime_state = Some(prev);
apply_no_human_approver(&mut session, false);
assert!(
!session
.agent_runtime_state
.as_ref()
.unwrap()
.no_human_approver,
"interactive execute (false) must reset a persisted true"
);
}
#[test]
fn headless_execute_yields_no_human_approver_true() {
let mut session = Session::new("sess-headless", "test-model");
assert!(session.agent_runtime_state.is_none());
apply_no_human_approver(&mut session, true);
assert!(
session
.agent_runtime_state
.as_ref()
.unwrap()
.no_human_approver,
"headless execute (true) must set the posture"
);
}
#[test]
fn execute_overwrites_regardless_of_prior_value() {
for (prev_flag, req_flag) in [(false, false), (true, true), (false, true), (true, false)] {
let mut session = Session::new("sess", "test-model");
let mut prev = AgentRuntimeState::new("run-prev");
prev.no_human_approver = prev_flag;
session.agent_runtime_state = Some(prev);
apply_no_human_approver(&mut session, req_flag);
assert_eq!(
session
.agent_runtime_state
.as_ref()
.unwrap()
.no_human_approver,
req_flag,
"request value must be authoritative (prev={prev_flag}, req={req_flag})"
);
}
}
}