bamboo_engine/runtime/task_evaluation/
executor.rs1use std::sync::Arc;
2
3use tokio::sync::mpsc;
4
5use bamboo_agent_core::{AgentError, AgentEvent, Session};
6use bamboo_domain::ReasoningEffort;
7use bamboo_domain::TaskItemStatus;
8use bamboo_llm::{LLMProvider, LLMRequestOptions};
9
10use super::super::task_context::TaskLoopContext;
11use super::message_builder::build_task_evaluation_messages;
12use super::schema::get_task_evaluation_tools;
13use super::token_estimation::estimate_prompt_tokens;
14use super::TaskEvaluationResult;
15
16mod outcomes;
17
18pub struct TaskEvaluationFrame<'a> {
19 pub event_tx: &'a mpsc::Sender<AgentEvent>,
20 pub session_id: &'a str,
21 pub model: &'a str,
22 pub reasoning_effort: Option<ReasoningEffort>,
23 pub timeout_context: &'a crate::runtime::stream::handler::StreamTimeoutContext,
24}
25
26fn skipped_evaluation(reasoning: &str) -> TaskEvaluationResult {
27 TaskEvaluationResult {
28 needs_evaluation: false,
29 updates: Vec::new(),
30 reasoning: reasoning.to_string(),
31 prompt_tokens: 0,
32 completion_tokens: 0,
33 }
34}
35
36fn normalize_lightweight_reasoning_effort(
37 reasoning_effort: Option<ReasoningEffort>,
38) -> Option<ReasoningEffort> {
39 reasoning_effort.map(|effort| match effort {
40 ReasoningEffort::Xhigh | ReasoningEffort::Max => ReasoningEffort::High,
41 other => other,
42 })
43}
44
45pub async fn evaluate_task_progress(
47 ctx: &TaskLoopContext,
48 session: &Session,
49 llm: Arc<dyn LLMProvider>,
50 frame: &TaskEvaluationFrame<'_>,
51) -> Result<TaskEvaluationResult, AgentError> {
52 evaluate_task_progress_with_dispatch(ctx, session, llm, frame, std::future::ready(()), || {})
53 .await
54}
55
56pub(crate) async fn evaluate_task_progress_with_dispatch<G, Fut, F>(
61 ctx: &TaskLoopContext,
62 session: &Session,
63 llm: Arc<dyn LLMProvider>,
64 frame: &TaskEvaluationFrame<'_>,
65 acquire_dispatch_guard: Fut,
66 on_dispatch: F,
67) -> Result<TaskEvaluationResult, AgentError>
68where
69 Fut: std::future::Future<Output = G>,
70 F: FnOnce(),
71{
72 use crate::runtime::stream::handler::{
73 await_stream_bootstrap, consume_llm_stream_silent_with_context,
74 };
75
76 let event_tx = frame.event_tx;
77 let session_id = frame.session_id;
78 let model = frame.model;
79 let reasoning_effort = frame.reasoning_effort;
80
81 let in_progress_count = ctx
82 .items
83 .iter()
84 .filter(|item| matches!(item.status, TaskItemStatus::InProgress))
85 .count();
86
87 if in_progress_count == 0 {
88 return Ok(skipped_evaluation("No in-progress tasks to evaluate"));
89 }
90
91 tracing::info!(
95 "[{}] Evaluating {} in-progress task items",
96 session_id,
97 in_progress_count
98 );
99
100 let _ = event_tx
101 .send(AgentEvent::TaskEvaluationStarted {
102 session_id: session_id.to_string(),
103 items_count: in_progress_count,
104 generation: Some(ctx.version),
105 })
106 .await;
107
108 let messages = build_task_evaluation_messages(ctx, session);
109 let prompt_tokens = estimate_prompt_tokens(&messages);
110 let tools = get_task_evaluation_tools();
111
112 tracing::debug!("[{}] Task evaluation using model: {}", session_id, model);
114
115 let request_reasoning_effort = normalize_lightweight_reasoning_effort(reasoning_effort);
116 if request_reasoning_effort != reasoning_effort {
117 tracing::debug!(
118 "[{}] Task evaluation downgraded reasoning effort from {:?} to {:?} for lightweight request",
119 session_id,
120 reasoning_effort,
121 request_reasoning_effort
122 );
123 }
124
125 let request_options = LLMRequestOptions {
126 session_id: Some(session_id.to_string()),
127 reasoning_effort: request_reasoning_effort,
128 parallel_tool_calls: None,
129 required_tool: None,
130 responses: None,
131 request_purpose: Some("task_evaluation".to_string()),
132 cache: None,
133 };
134 let cancel_token = tokio_util::sync::CancellationToken::new();
135 let _dispatch_guard = acquire_dispatch_guard.await;
136 let timeout_context = frame.timeout_context.clone().begin_request();
137 on_dispatch();
138 let stream = match await_stream_bootstrap(
139 llm.chat_stream_with_options(&messages, &tools, Some(8192), model, Some(&request_options)),
140 &cancel_token,
141 session_id,
142 &timeout_context,
143 )
144 .await?
145 {
146 Ok(stream) => stream,
147 Err(error) => {
148 tracing::warn!("[{}] Task evaluation failed: {}", session_id, error);
149 return Ok(skipped_evaluation(&format!("Evaluation failed: {error}")));
150 }
151 };
152 let stream_output =
153 consume_llm_stream_silent_with_context(stream, &cancel_token, session_id, &timeout_context)
154 .await?;
155
156 Ok(outcomes::build_success_result(
157 stream_output,
158 event_tx,
159 session_id,
160 prompt_tokens,
161 ctx.version,
162 )
163 .await)
164}