bamboo_server/handlers/agent/execute/handler/
mod.rs1use actix_web::{web, HttpResponse};
2
3use super::image_fallback::resolve_image_fallback;
4use super::{ExecuteRequest, ExecuteSyncInfo, ExecuteSyncReason};
5use crate::app_state::AppState;
6use bamboo_engine::model_areas::resolve_global_area_models;
7use bamboo_engine::model_config_helper::{
8 get_default_model_for_provider, get_reasoning_effort_for_provider, resolve_gold_config,
9 resolve_provider_type,
10};
11
12use self::response::{
13 already_running_response, bad_request_error_response, completed_response,
14 internal_server_error_response,
15};
16
17mod ready;
18mod response;
19#[cfg(test)]
20mod tests;
21mod validation;
22
23pub async fn handler(
25 state: web::Data<AppState>,
26 path: web::Path<String>,
27 req: web::Json<ExecuteRequest>,
28) -> HttpResponse {
29 let session_id = path.into_inner();
30 tracing::debug!(
31 "[{}] Execute requested: model={:?}, model_ref={:?}, reasoning_effort={:?}, has_client_sync={}",
32 session_id,
33 req.model,
34 req.model_ref
35 .as_ref()
36 .map(|m| format!("{}/{}", m.provider, m.model)),
37 req.reasoning_effort,
38 req.client_sync.is_some(),
39 );
40
41 let config_snapshot = state.config.read().await.clone();
43 let image_fallback = match resolve_image_fallback(&config_snapshot) {
44 Ok(value) => value,
45 Err(error) => return internal_server_error_response(error),
46 };
47
48 let disabled_tools_vec: Vec<String> =
49 config_snapshot.disabled_tool_names().into_iter().collect();
50 let disabled_skill_ids_vec: Vec<String> =
51 config_snapshot.disabled_skill_ids().into_iter().collect();
52 let requested_provider = req
53 .model_ref
54 .as_ref()
55 .map(|model_ref| model_ref.provider.as_str())
56 .or(req.provider.as_deref())
57 .unwrap_or(config_snapshot.provider.as_str());
58
59 let requested_provider_type = resolve_provider_type(
60 &config_snapshot,
61 requested_provider,
62 &state.provider_registry,
63 );
64 let areas = resolve_global_area_models(
68 &config_snapshot,
69 requested_provider,
70 &state.provider_registry,
71 );
72
73 let config = bamboo_engine::session_app::types::ExecutionConfigSnapshot {
74 default_model: get_default_model_for_provider(&config_snapshot, requested_provider).ok(),
75 default_model_ref: config_snapshot.defaults.as_ref().map(|d| d.chat.clone()),
76 default_reasoning_effort: get_reasoning_effort_for_provider(
77 &config_snapshot,
78 requested_provider,
79 ),
80 disabled_tools: disabled_tools_vec.clone(),
81 disabled_skill_ids: disabled_skill_ids_vec.clone(),
82 provider_name: requested_provider.to_string(),
83 provider_type: requested_provider_type.clone(),
84 fast_model: areas.fast.as_ref().map(|m| m.model_name.clone()),
85 fast_model_ref: areas.fast_ref.clone(),
86 background_model: areas.background.as_ref().map(|m| m.model_name.clone()),
87 background_model_ref: areas.background_ref.clone(),
88 summarization_model: areas.summarization.as_ref().map(|m| m.model_name.clone()),
89 summarization_model_ref: areas.summarization_ref.clone(),
90 image_fallback: image_fallback.clone(),
91 gold_config: resolve_gold_config(&config_snapshot, None),
92 provider_model_ref_enabled: config_snapshot.features.provider_model_ref,
93 };
94
95 let input = bamboo_engine::session_app::types::ExecuteInput {
96 session_id: session_id.clone(),
97 request_model: req.model.clone(),
98 request_model_ref: req.model_ref.clone(),
99 request_provider: req.provider.clone(),
100 request_reasoning_effort: req.reasoning_effort,
101 request_skill_mode: req.skill_mode.clone(),
102 client_sync: req.client_sync.as_ref().map(|cs| {
103 bamboo_engine::session_app::types::ExecuteClientSync {
104 client_message_count: cs.client_message_count,
105 client_last_message_id: cs.client_last_message_id.clone(),
106 client_has_pending_question: cs.client_has_pending_question,
107 client_pending_question_tool_call_id: cs
108 .client_pending_question_tool_call_id
109 .clone(),
110 }
111 }),
112 };
113
114 let outcome = match bamboo_engine::session_app::execute::prepare_execute(
115 state.as_ref(),
116 config.clone(),
117 input,
118 )
119 .await
120 {
121 Ok(outcome) => outcome,
122 Err(error) => {
123 return match error {
124 bamboo_engine::session_app::errors::ExecutePreparationError::NotFound(_) => {
125 tracing::warn!("[{session_id}] Execute session not found");
126 HttpResponse::NotFound().json(serde_json::json!({
127 "error": crate::error::error_value("Session not found"),
128 "session_id": session_id
129 }))
130 }
131 bamboo_engine::session_app::errors::ExecutePreparationError::LoadFailed(
132 load_err,
133 ) => {
134 let err_msg = load_err.to_string();
135 tracing::error!("[{session_id}] Execute session load error: {err_msg}");
136 HttpResponse::InternalServerError().json(serde_json::json!({
137 "error": crate::error::error_value(format!(
138 "Failed to load session: {err_msg}"
139 ))
140 }))
141 }
142 _ => internal_server_error_response(format!("Execute preparation failed: {error}")),
143 };
144 }
145 };
146
147 match outcome {
148 bamboo_engine::session_app::types::ExecutePreparationOutcome::Ready {
149 session,
150 effective_model,
151 effective_reasoning_effort,
152 model_source,
153 reasoning_source,
154 is_child_session,
155 } => {
156 ready::handle_execute_ready(
157 &state,
158 &session_id,
159 ready::ReadyExecution {
160 session: *session,
161 effective_model,
162 effective_reasoning_effort,
163 model_source,
164 reasoning_source,
165 is_child_session,
166 no_human_approver: req.no_human_approver,
167 run_budget: req.run_budget,
168 },
169 &config,
170 &config_snapshot,
171 image_fallback,
172 disabled_tools_vec,
173 disabled_skill_ids_vec,
174 )
175 .await
176 }
177
178 bamboo_engine::session_app::types::ExecutePreparationOutcome::AlreadyRunning {
179 server_snapshot,
180 } => {
181 let sync_info = server_snapshot_to_sync_info(&server_snapshot, None);
182 already_running_response(&session_id, sync_info, None)
183 }
184
185 bamboo_engine::session_app::types::ExecutePreparationOutcome::NoPendingMessage {
186 server_snapshot,
187 } => {
188 tracing::debug!(
189 "[{}] No pending user message, returning completed status",
190 session_id
191 );
192 let sync_info = server_snapshot_to_sync_info(&server_snapshot, None);
193 completed_response(&session_id, sync_info)
194 }
195
196 bamboo_engine::session_app::types::ExecutePreparationOutcome::SyncMismatch {
197 reason,
198 server_snapshot,
199 } => {
200 tracing::debug!(
201 "[{}] Execute SyncMismatch (reason={:?}); returning completed/need_sync without starting a runner",
202 session_id,
203 reason
204 );
205 state
206 .metrics_service
207 .collector()
208 .execute_sync_mismatch(reason.as_str(), chrono::Utc::now());
209 let sync_info = server_snapshot_to_sync_info(&server_snapshot, Some(reason));
210 completed_response(&session_id, sync_info)
211 }
212
213 bamboo_engine::session_app::types::ExecutePreparationOutcome::ModelRequired => {
214 bad_request_error_response("no model configured for session or provider")
215 }
216
217 bamboo_engine::session_app::types::ExecutePreparationOutcome::ImageFallbackError(error) => {
218 bad_request_error_response(error)
219 }
220 }
221}
222
223fn crate_sync_reason_to_handler(
225 reason: bamboo_engine::session_app::types::ExecuteSyncReason,
226) -> ExecuteSyncReason {
227 match reason {
228 bamboo_engine::session_app::types::ExecuteSyncReason::PendingQuestionMismatch => {
229 ExecuteSyncReason::PendingQuestionMismatch
230 }
231 bamboo_engine::session_app::types::ExecuteSyncReason::MessageCountMismatch => {
232 ExecuteSyncReason::MessageCountMismatch
233 }
234 bamboo_engine::session_app::types::ExecuteSyncReason::LastMessageIdMismatch => {
235 ExecuteSyncReason::LastMessageIdMismatch
236 }
237 }
238}
239
240fn server_snapshot_to_sync_info(
241 server_snapshot: &bamboo_engine::session_app::types::ServerExecuteSnapshot,
242 reason: Option<bamboo_engine::session_app::types::ExecuteSyncReason>,
243) -> ExecuteSyncInfo {
244 ExecuteSyncInfo {
245 need_sync: reason.is_some(),
246 reason: reason.map(crate_sync_reason_to_handler),
247 server_message_count: server_snapshot.message_count,
248 server_last_message_id: server_snapshot.last_message_id.clone(),
249 has_pending_question: server_snapshot.has_pending_question,
250 pending_question_tool_call_id: server_snapshot.pending_question_tool_call_id.clone(),
251 has_pending_user_message: server_snapshot.has_pending_user_message,
252 }
253}
254
255fn build_sync_info_from_session(session: &bamboo_agent_core::Session) -> ExecuteSyncInfo {
256 let snapshot = bamboo_engine::session_app::types::ServerExecuteSnapshot::from_session(session);
262 let mut info = server_snapshot_to_sync_info(&snapshot, None);
263 info.has_pending_user_message = false;
264 info
265}