Skip to main content

bamboo_engine/gold_auto_answer/
mod.rs

1use crate::config::GoldConfig;
2use bamboo_agent_core::GoldDecision;
3
4use crate::app_context::AgentSessionContext;
5use crate::events::publish_replayable_session_event;
6use crate::model_config_helper::{resolve_gold_config, GOLD_CONFIG_METADATA_KEY};
7use crate::session_app::repository::SessionAccess;
8use crate::session_app::respond::{submit_pending_response_with_source, ResponseSource};
9use crate::session_app::resume::{resume_session_execution, ResumeExecutionPort};
10use crate::session_app::types::{RespondInput, ResumeOutcome};
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
39/// Attempt a Gold auto-answer for a session's pending clarification.
40///
41/// `state` supplies session/provider/event context (and session persistence
42/// via [`SessionAccess`]); `resume_port` is the server-side adapter that knows
43/// how to actually spawn a resumed agent execution.
44pub 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 =
105        match evaluate_gold_state_for_pending_question(state, session_id, &session, &gold_config)
106            .await
107        {
108            Ok(result) => result,
109            Err(error) => {
110                tracing::warn!(
111                    session_id = %session_id,
112                    error = %error,
113                    "Gold auto-answer skipped because Gold state evaluation failed"
114                );
115                return GoldAutoAnswerOutcome::Skipped {
116                    reason: format!("state_evaluation_failed:{error}"),
117                };
118            }
119        };
120
121    if !state_evaluation
122        .confidence
123        .meets(gold_config.min_auto_continue_confidence)
124    {
125        return GoldAutoAnswerOutcome::Skipped {
126            reason: format!(
127                "state_evaluation_confidence_{}",
128                state_evaluation.confidence.as_str()
129            ),
130        };
131    }
132
133    if !matches!(
134        state_evaluation.decision,
135        GoldDecision::Continue | GoldDecision::NeedInput
136    ) {
137        return GoldAutoAnswerOutcome::Skipped {
138            reason: format!(
139                "state_evaluation_decision_{}",
140                state_evaluation.decision.as_str()
141            ),
142        };
143    }
144
145    let answer_decision = match evaluate_gold_auto_answer_question(
146        state,
147        session_id,
148        &session,
149        &gold_config,
150        &state_evaluation,
151    )
152    .await
153    {
154        Ok(result) => result,
155        Err(error) => {
156            tracing::warn!(
157                session_id = %session_id,
158                error = %error,
159                "Gold auto-answer skipped because question evaluation failed"
160            );
161            return GoldAutoAnswerOutcome::Skipped {
162                reason: format!("question_evaluation_failed:{error}"),
163            };
164        }
165    };
166
167    if !answer_decision.apply {
168        return GoldAutoAnswerOutcome::Skipped {
169            reason: format!("question_decision_declined:{}", answer_decision.reasoning),
170        };
171    }
172
173    if !answer_decision
174        .confidence
175        .meets(gold_config.min_auto_continue_confidence)
176    {
177        return GoldAutoAnswerOutcome::Skipped {
178            reason: format!(
179                "question_decision_confidence_{}",
180                answer_decision.confidence.as_str()
181            ),
182        };
183    }
184
185    let Some(raw_answer) = answer_decision.answer.as_deref() else {
186        return GoldAutoAnswerOutcome::Skipped {
187            reason: "question_decision_missing_answer".to_string(),
188        };
189    };
190
191    let Some(answer) = canonicalize_pending_answer(pending_question, raw_answer) else {
192        return GoldAutoAnswerOutcome::Skipped {
193            reason: "question_decision_answer_not_canonical".to_string(),
194        };
195    };
196
197    tracing::info!(
198        session_id = %session_id,
199        tool_name = %pending_question.tool_name,
200        answer = %answer,
201        reasoning = %answer_decision.reasoning,
202        "Applying Gold auto-answer for pending clarification"
203    );
204
205    let respond_input = RespondInput {
206        session_id: session_id.to_string(),
207        user_response: answer.clone(),
208        model: None,
209        model_ref: None,
210        provider: None,
211        reasoning_effort: session.reasoning_effort,
212    };
213
214    let (updated_session, _submitted_answer, plan_mode_transition) =
215        match submit_pending_response_with_source(state, respond_input, ResponseSource::Gold).await
216        {
217            Ok(result) => result,
218            Err(error) => {
219                tracing::warn!(
220                    session_id = %session_id,
221                    error = %error,
222                    "Gold auto-answer skipped because submitting the response failed"
223                );
224                return GoldAutoAnswerOutcome::Skipped {
225                    reason: format!("submit_pending_response_failed:{error}"),
226                };
227            }
228        };
229
230    if let Some(event) = plan_mode_transition_event(session_id, plan_mode_transition.as_ref()) {
231        publish_replayable_session_event(state, session_id, event).await;
232    }
233
234    let resume_config =
235        build_resume_config_snapshot(state, &updated_session, Some(gold_config.clone())).await;
236    let resume_outcome = resume_session_execution(resume_port, session_id, resume_config).await;
237
238    tracing::info!(
239        session_id = %session_id,
240        resume_status = %resume_outcome.status_str(),
241        resume_run_id = %resume_outcome.run_id().map(String::as_str).unwrap_or_default(),
242        "Gold auto-answer completed"
243    );
244
245    GoldAutoAnswerOutcome::Applied {
246        answer,
247        resume_outcome,
248    }
249}