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
36pub async fn evaluate_task_progress(
38 ctx: &TaskLoopContext,
39 session: &Session,
40 llm: Arc<dyn LLMProvider>,
41 frame: &TaskEvaluationFrame<'_>,
42) -> Result<TaskEvaluationResult, AgentError> {
43 evaluate_task_progress_with_dispatch(ctx, session, llm, frame, std::future::ready(()), || {})
44 .await
45}
46
47pub(crate) async fn evaluate_task_progress_with_dispatch<G, Fut, F>(
52 ctx: &TaskLoopContext,
53 session: &Session,
54 llm: Arc<dyn LLMProvider>,
55 frame: &TaskEvaluationFrame<'_>,
56 acquire_dispatch_guard: Fut,
57 on_dispatch: F,
58) -> Result<TaskEvaluationResult, AgentError>
59where
60 Fut: std::future::Future<Output = G>,
61 F: FnOnce(),
62{
63 use crate::runtime::stream::handler::{
64 await_stream_bootstrap, consume_llm_stream_silent_with_context,
65 };
66
67 let event_tx = frame.event_tx;
68 let session_id = frame.session_id;
69 let model = frame.model;
70 let reasoning_effort = frame.reasoning_effort;
71
72 let in_progress_count = ctx
73 .items
74 .iter()
75 .filter(|item| matches!(item.status, TaskItemStatus::InProgress))
76 .count();
77
78 if in_progress_count == 0 {
79 return Ok(skipped_evaluation("No in-progress tasks to evaluate"));
80 }
81
82 tracing::info!(
86 "[{}] Evaluating {} in-progress task items",
87 session_id,
88 in_progress_count
89 );
90
91 let _ = event_tx
92 .send(AgentEvent::TaskEvaluationStarted {
93 session_id: session_id.to_string(),
94 items_count: in_progress_count,
95 generation: Some(ctx.version),
96 })
97 .await;
98
99 let messages = build_task_evaluation_messages(ctx, session);
100 let prompt_tokens = estimate_prompt_tokens(&messages);
101 let tools = get_task_evaluation_tools();
102
103 tracing::debug!("[{}] Task evaluation using model: {}", session_id, model);
105
106 let request_options = LLMRequestOptions {
107 session_id: Some(session_id.to_string()),
108 reasoning_effort,
109 parallel_tool_calls: None,
110 required_tool: None,
111 responses: None,
112 request_purpose: Some("task_evaluation".to_string()),
113 cache: None,
114 };
115 let cancel_token = tokio_util::sync::CancellationToken::new();
116 let _dispatch_guard = acquire_dispatch_guard.await;
117 let timeout_context = frame.timeout_context.clone().begin_request();
118 on_dispatch();
119 let stream = match await_stream_bootstrap(
120 llm.chat_stream_with_options(&messages, &tools, Some(8192), model, Some(&request_options)),
121 &cancel_token,
122 session_id,
123 &timeout_context,
124 )
125 .await?
126 {
127 Ok(stream) => stream,
128 Err(error) => {
129 tracing::warn!("[{}] Task evaluation failed: {}", session_id, error);
130 return Ok(skipped_evaluation(&format!("Evaluation failed: {error}")));
131 }
132 };
133 let stream_output =
134 consume_llm_stream_silent_with_context(stream, &cancel_token, session_id, &timeout_context)
135 .await?;
136
137 Ok(outcomes::build_success_result(
138 stream_output,
139 event_tx,
140 session_id,
141 prompt_tokens,
142 ctx.version,
143 )
144 .await)
145}