bamboo_engine/gold_auto_answer/
mod.rs1use bamboo_agent_core::GoldDecision;
2use crate::config::GoldConfig;
3
4use crate::app_context::AgentSessionContext;
5use crate::events::publish_replayable_session_event;
6use crate::session_app::repository::SessionAccess;
7use crate::session_app::respond::{submit_pending_response_with_source, ResponseSource};
8use crate::session_app::resume::{resume_session_execution, ResumeExecutionPort};
9use crate::session_app::types::{RespondInput, ResumeOutcome};
10use crate::model_config_helper::{resolve_gold_config, GOLD_CONFIG_METADATA_KEY};
11
12mod decision;
13mod evaluation;
14mod prompt;
15mod resume;
16
17#[cfg(test)]
18mod tests;
19
20use decision::{
21 canonicalize_pending_answer, session_is_awaiting_clarification, should_attempt_gold_auto_answer,
22};
23use evaluation::{evaluate_gold_auto_answer_question, evaluate_gold_state_for_pending_question};
24use resume::{build_resume_config_snapshot, plan_mode_transition_event};
25
26const GOLD_AUTO_ANSWER_TOOL_NAME: &str = "report_gold_auto_answer";
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum GoldAutoAnswerOutcome {
30 Skipped {
31 reason: String,
32 },
33 Applied {
34 answer: String,
35 resume_outcome: ResumeOutcome,
36 },
37}
38
39pub async fn maybe_auto_answer_pending_question<S>(
45 state: &S,
46 resume_port: &dyn ResumeExecutionPort,
47 session_id: &str,
48 gold_config_override: Option<GoldConfig>,
49) -> GoldAutoAnswerOutcome
50where
51 S: AgentSessionContext + SessionAccess,
52{
53 let Some(session) = state.load_session_merged(session_id).await else {
54 return GoldAutoAnswerOutcome::Skipped {
55 reason: "session_not_found".to_string(),
56 };
57 };
58
59 let config_snapshot = state.config().read().await.clone();
60 let Some(gold_config) = gold_config_override.or_else(|| {
61 resolve_gold_config(
62 &config_snapshot,
63 session
64 .metadata
65 .get(GOLD_CONFIG_METADATA_KEY)
66 .map(String::as_str),
67 )
68 }) else {
69 return GoldAutoAnswerOutcome::Skipped {
70 reason: "gold_config_unavailable".to_string(),
71 };
72 };
73
74 let Some(pending_question) = session.pending_question.as_ref() else {
75 return GoldAutoAnswerOutcome::Skipped {
76 reason: "no_pending_question".to_string(),
77 };
78 };
79
80 if !gold_config.enabled {
81 return GoldAutoAnswerOutcome::Skipped {
82 reason: "gold_disabled".to_string(),
83 };
84 }
85
86 if !gold_config.auto_answer_enabled {
87 return GoldAutoAnswerOutcome::Skipped {
88 reason: "gold_auto_answer_disabled".to_string(),
89 };
90 }
91
92 if !session_is_awaiting_clarification(&session) {
93 return GoldAutoAnswerOutcome::Skipped {
94 reason: "session_not_awaiting_clarification".to_string(),
95 };
96 }
97
98 if !should_attempt_gold_auto_answer(pending_question) {
99 return GoldAutoAnswerOutcome::Skipped {
100 reason: "pending_question_not_whitelisted".to_string(),
101 };
102 }
103
104 let state_evaluation = match evaluate_gold_state_for_pending_question(
105 state,
106 session_id,
107 &session,
108 &gold_config,
109 )
110 .await
111 {
112 Ok(result) => result,
113 Err(error) => {
114 tracing::warn!(
115 session_id = %session_id,
116 error = %error,
117 "Gold auto-answer skipped because Gold state evaluation failed"
118 );
119 return GoldAutoAnswerOutcome::Skipped {
120 reason: format!("state_evaluation_failed:{error}"),
121 };
122 }
123 };
124
125 if !state_evaluation
126 .confidence
127 .meets(gold_config.min_auto_continue_confidence)
128 {
129 return GoldAutoAnswerOutcome::Skipped {
130 reason: format!(
131 "state_evaluation_confidence_{}",
132 state_evaluation.confidence.as_str()
133 ),
134 };
135 }
136
137 if !matches!(
138 state_evaluation.decision,
139 GoldDecision::Continue | GoldDecision::NeedInput
140 ) {
141 return GoldAutoAnswerOutcome::Skipped {
142 reason: format!(
143 "state_evaluation_decision_{}",
144 state_evaluation.decision.as_str()
145 ),
146 };
147 }
148
149 let answer_decision = match evaluate_gold_auto_answer_question(
150 state,
151 session_id,
152 &session,
153 &gold_config,
154 &state_evaluation,
155 )
156 .await
157 {
158 Ok(result) => result,
159 Err(error) => {
160 tracing::warn!(
161 session_id = %session_id,
162 error = %error,
163 "Gold auto-answer skipped because question evaluation failed"
164 );
165 return GoldAutoAnswerOutcome::Skipped {
166 reason: format!("question_evaluation_failed:{error}"),
167 };
168 }
169 };
170
171 if !answer_decision.apply {
172 return GoldAutoAnswerOutcome::Skipped {
173 reason: format!("question_decision_declined:{}", answer_decision.reasoning),
174 };
175 }
176
177 if !answer_decision
178 .confidence
179 .meets(gold_config.min_auto_continue_confidence)
180 {
181 return GoldAutoAnswerOutcome::Skipped {
182 reason: format!(
183 "question_decision_confidence_{}",
184 answer_decision.confidence.as_str()
185 ),
186 };
187 }
188
189 let Some(raw_answer) = answer_decision.answer.as_deref() else {
190 return GoldAutoAnswerOutcome::Skipped {
191 reason: "question_decision_missing_answer".to_string(),
192 };
193 };
194
195 let Some(answer) = canonicalize_pending_answer(pending_question, raw_answer) else {
196 return GoldAutoAnswerOutcome::Skipped {
197 reason: "question_decision_answer_not_canonical".to_string(),
198 };
199 };
200
201 tracing::info!(
202 session_id = %session_id,
203 tool_name = %pending_question.tool_name,
204 answer = %answer,
205 reasoning = %answer_decision.reasoning,
206 "Applying Gold auto-answer for pending clarification"
207 );
208
209 let respond_input = RespondInput {
210 session_id: session_id.to_string(),
211 user_response: answer.clone(),
212 model: None,
213 model_ref: None,
214 provider: None,
215 reasoning_effort: session.reasoning_effort,
216 };
217
218 let (updated_session, _submitted_answer, plan_mode_transition) =
219 match submit_pending_response_with_source(
220 state,
221 respond_input,
222 ResponseSource::Gold,
223 )
224 .await
225 {
226 Ok(result) => result,
227 Err(error) => {
228 tracing::warn!(
229 session_id = %session_id,
230 error = %error,
231 "Gold auto-answer skipped because submitting the response failed"
232 );
233 return GoldAutoAnswerOutcome::Skipped {
234 reason: format!("submit_pending_response_failed:{error}"),
235 };
236 }
237 };
238
239 if let Some(event) = plan_mode_transition_event(session_id, plan_mode_transition.as_ref()) {
240 publish_replayable_session_event(state, session_id, event).await;
241 }
242
243 let resume_config =
244 build_resume_config_snapshot(state, &updated_session, Some(gold_config.clone()))
245 .await;
246 let resume_outcome =
247 resume_session_execution(resume_port, session_id, resume_config)
248 .await;
249
250 tracing::info!(
251 session_id = %session_id,
252 resume_status = %resume_outcome.status_str(),
253 resume_run_id = %resume_outcome.run_id().map(String::as_str).unwrap_or_default(),
254 "Gold auto-answer completed"
255 );
256
257 GoldAutoAnswerOutcome::Applied {
258 answer,
259 resume_outcome,
260 }
261}