use async_trait::async_trait;
use bamboo_agent_core::{Tool, ToolError, ToolResult};
use serde::Deserialize;
use serde_json::json;
pub const UPDATE_GOAL_TOOL_NAME: &str = "update_goal";
#[derive(Debug, Deserialize)]
struct UpdateGoalArgs {
status: String,
}
pub struct UpdateGoalTool;
impl UpdateGoalTool {
pub fn new() -> Self {
Self
}
}
impl Default for UpdateGoalTool {
fn default() -> Self {
Self::new()
}
}
pub fn parse_update_goal_status(arguments: &str) -> Result<String, String> {
let parsed: UpdateGoalArgs = serde_json::from_str(arguments)
.map_err(|e| format!("invalid update_goal arguments: {e}"))?;
let status = parsed.status.trim().to_ascii_lowercase();
match status.as_str() {
"complete" | "blocked" => Ok(status),
other => Err(format!(
"update_goal.status must be 'complete' or 'blocked', got '{other}'"
)),
}
}
#[async_trait]
impl Tool for UpdateGoalTool {
fn name(&self) -> &str {
UPDATE_GOAL_TOOL_NAME
}
fn description(&self) -> &str {
"Update the active session goal. Use this ONLY to mark the goal `complete` or `blocked`.\n\
Set status to `complete` only when the objective has actually been achieved and no required work remains — not because the budget is nearly exhausted or because you are stopping. Completion is reverified against the current state before the run ends, so do not claim it on weak, indirect, or unverified evidence.\n\
Set status to `blocked` only when the same blocking condition has repeated for at least three consecutive goal turns (counting the original turn and any automatic continuations) and you genuinely cannot make progress without user input or an external-state change. Never use `blocked` merely because work is hard, slow, uncertain, or incomplete."
}
fn mutability(&self) -> crate::ToolMutability {
crate::ToolMutability::ReadOnly
}
fn parameters_schema(&self) -> serde_json::Value {
json!({
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": ["complete", "blocked"],
"description": "`complete` when the full objective is achieved and verified; `blocked` only after the strict blocked audit (same blocker ≥3 consecutive goal turns)."
}
},
"required": ["status"],
"additionalProperties": false
})
}
async fn execute(&self, args: serde_json::Value) -> Result<ToolResult, ToolError> {
let arguments = args.to_string();
let status = parse_update_goal_status(&arguments).map_err(ToolError::InvalidArguments)?;
let message = match status.as_str() {
"complete" => {
"Recorded goal status: complete. Before the run ends the runtime will verify the \
objective against the current state; if anything remains unproven you will be \
asked to keep working."
}
_ => "Recorded goal status: blocked.",
};
Ok(ToolResult::text(true, message))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[tokio::test]
async fn complete_status_is_recorded() {
let tool = UpdateGoalTool::new();
let result = tool
.execute(json!({"status": "complete"}))
.await
.expect("complete accepted");
assert!(result.success);
assert!(result.result.to_lowercase().contains("complete"));
}
#[tokio::test]
async fn blocked_status_is_recorded() {
let tool = UpdateGoalTool::new();
let result = tool
.execute(json!({"status": "BLOCKED"}))
.await
.expect("blocked accepted (case-insensitive)");
assert!(result.success);
assert!(result.result.to_lowercase().contains("blocked"));
}
#[tokio::test]
async fn rejects_unknown_status() {
let tool = UpdateGoalTool::new();
let result = tool.execute(json!({"status": "paused"})).await;
assert!(matches!(result, Err(ToolError::InvalidArguments(_))));
}
#[tokio::test]
async fn rejects_missing_status() {
let tool = UpdateGoalTool::new();
assert!(tool.execute(json!({})).await.is_err());
}
#[test]
fn parse_helper_normalizes_case() {
assert_eq!(
parse_update_goal_status(r#"{"status":"Complete"}"#).unwrap(),
"complete"
);
assert!(parse_update_goal_status(r#"{"status":"nope"}"#).is_err());
}
}