bamboo_tools/tools/
goal.rs1use async_trait::async_trait;
16use bamboo_agent_core::{Tool, ToolError, ToolResult};
17use serde::Deserialize;
18use serde_json::json;
19
20pub const UPDATE_GOAL_TOOL_NAME: &str = "update_goal";
22
23#[derive(Debug, Deserialize)]
24struct UpdateGoalArgs {
25 status: String,
26}
27
28pub struct UpdateGoalTool;
30
31impl UpdateGoalTool {
32 pub fn new() -> Self {
33 Self
34 }
35}
36
37impl Default for UpdateGoalTool {
38 fn default() -> Self {
39 Self::new()
40 }
41}
42
43pub fn parse_update_goal_status(arguments: &str) -> Result<String, String> {
47 let parsed: UpdateGoalArgs = serde_json::from_str(arguments)
48 .map_err(|e| format!("invalid update_goal arguments: {e}"))?;
49 let status = parsed.status.trim().to_ascii_lowercase();
50 match status.as_str() {
51 "complete" | "blocked" => Ok(status),
52 other => Err(format!(
53 "update_goal.status must be 'complete' or 'blocked', got '{other}'"
54 )),
55 }
56}
57
58#[async_trait]
59impl Tool for UpdateGoalTool {
60 fn name(&self) -> &str {
61 UPDATE_GOAL_TOOL_NAME
62 }
63
64 fn description(&self) -> &str {
65 "Update the active session goal. Use this ONLY to mark the goal `complete` or `blocked`.\n\
66 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\
67 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."
68 }
69
70 fn mutability(&self) -> crate::ToolMutability {
74 crate::ToolMutability::ReadOnly
75 }
76
77 fn parameters_schema(&self) -> serde_json::Value {
78 json!({
79 "type": "object",
80 "properties": {
81 "status": {
82 "type": "string",
83 "enum": ["complete", "blocked"],
84 "description": "`complete` when the full objective is achieved and verified; `blocked` only after the strict blocked audit (same blocker ≥3 consecutive goal turns)."
85 }
86 },
87 "required": ["status"],
88 "additionalProperties": false
89 })
90 }
91
92 async fn execute(&self, args: serde_json::Value) -> Result<ToolResult, ToolError> {
93 let arguments = args.to_string();
94 let status = parse_update_goal_status(&arguments).map_err(ToolError::InvalidArguments)?;
95
96 let message = match status.as_str() {
97 "complete" => {
98 "Recorded goal status: complete. Before the run ends the runtime will verify the \
99 objective against the current state; if anything remains unproven you will be \
100 asked to keep working."
101 }
102 _ => "Recorded goal status: blocked.",
104 };
105
106 Ok(ToolResult::text(true, message))
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use serde_json::json;
114
115 #[tokio::test]
116 async fn complete_status_is_recorded() {
117 let tool = UpdateGoalTool::new();
118 let result = tool
119 .execute(json!({"status": "complete"}))
120 .await
121 .expect("complete accepted");
122 assert!(result.success);
123 assert!(result.result.to_lowercase().contains("complete"));
124 }
125
126 #[tokio::test]
127 async fn blocked_status_is_recorded() {
128 let tool = UpdateGoalTool::new();
129 let result = tool
130 .execute(json!({"status": "BLOCKED"}))
131 .await
132 .expect("blocked accepted (case-insensitive)");
133 assert!(result.success);
134 assert!(result.result.to_lowercase().contains("blocked"));
135 }
136
137 #[tokio::test]
138 async fn rejects_unknown_status() {
139 let tool = UpdateGoalTool::new();
140 let result = tool.execute(json!({"status": "paused"})).await;
141 assert!(matches!(result, Err(ToolError::InvalidArguments(_))));
142 }
143
144 #[tokio::test]
145 async fn rejects_missing_status() {
146 let tool = UpdateGoalTool::new();
147 assert!(tool.execute(json!({})).await.is_err());
148 }
149
150 #[test]
151 fn parse_helper_normalizes_case() {
152 assert_eq!(
153 parse_update_goal_status(r#"{"status":"Complete"}"#).unwrap(),
154 "complete"
155 );
156 assert!(parse_update_goal_status(r#"{"status":"nope"}"#).is_err());
157 }
158}