1use super::{AgentTool, AgentToolResult, ToolContext, ToolError, ToolExecutionMode, ToolTier};
7use async_trait::async_trait;
8use serde_json::{Value, json};
9use std::sync::LazyLock;
10use std::sync::Mutex;
11use std::sync::atomic::{AtomicU64, Ordering};
12use tokio::sync::oneshot;
13
14static NEXT_GOAL_ID: AtomicU64 = AtomicU64::new(1);
16
17#[derive(Clone, Debug)]
19#[allow(dead_code)]
20struct Goal {
21 #[allow(dead_code)]
22 id: String,
23 objective: String,
24 status: String,
25 token_budget: Option<u64>,
26 tokens_used: u64,
27 created_at: u64,
28 updated_at: u64,
29}
30
31static ACTIVE_GOAL: LazyLock<Mutex<Option<Goal>>> = LazyLock::new(|| Mutex::new(None));
33
34pub struct GoalTool;
36
37#[async_trait]
38impl AgentTool for GoalTool {
39 fn name(&self) -> &str {
40 "goal"
41 }
42
43 fn label(&self) -> &str {
44 "Goal"
45 }
46
47 fn description(&self) -> &str {
48 concat!(
49 "Manage investigation goals with token budgets. ",
50 "Operations: create (new goal with objective), ",
51 "get (current goal), complete (mark done), ",
52 "resume (reactivate), drop (abandon)."
53 )
54 }
55
56 fn essential(&self) -> bool {
57 false
58 }
59
60 fn parameters_schema(&self) -> Value {
61 json!({
62 "type": "object",
63 "properties": {
64 "op": {
65 "type": "string",
66 "enum": ["create", "get", "complete", "resume", "drop"],
67 "description": "Goal operation."
68 },
69 "objective": {
70 "type": "string",
71 "description": "Goal objective (required for create)."
72 },
73 "token_budget": {
74 "type": "integer",
75 "description": "Optional token budget limit."
76 }
77 },
78 "required": ["op"]
79 })
80 }
81
82 fn intent(&self) -> Option<&str> {
83 Some("Manage investigation goals")
84 }
85
86 fn execution_mode(&self) -> ToolExecutionMode {
87 ToolExecutionMode::SequentialOnly
88 }
89
90 fn tool_tier(&self) -> ToolTier {
91 ToolTier::Read
92 }
93
94 async fn execute(
95 &self,
96 _tool_call_id: &str,
97 params: Value,
98 _signal: Option<oneshot::Receiver<()>>,
99 _ctx: &ToolContext,
100 ) -> Result<AgentToolResult, ToolError> {
101 let op = params
102 .get("op")
103 .and_then(|v| v.as_str())
104 .ok_or_else(|| "Missing required parameter: op".to_string())?;
105
106 let now = std::time::SystemTime::now()
107 .duration_since(std::time::UNIX_EPOCH)
108 .map(|d| d.as_secs())
109 .unwrap_or(0);
110
111 let mut store = ACTIVE_GOAL
112 .lock()
113 .map_err(|e| format!("Goal lock error: {}", e))?;
114
115 match op {
116 "create" => {
117 let objective = params
118 .get("objective")
119 .and_then(|v| v.as_str())
120 .map(|s| s.to_string())
121 .ok_or_else(|| "Missing 'objective' parameter for create".to_string())?;
122
123 let token_budget = params.get("token_budget").and_then(|v| v.as_u64());
124
125 if store.is_some() {
126 return Err("A goal is already active. Complete or drop it first.".to_string());
127 }
128
129 let id = NEXT_GOAL_ID.fetch_add(1, Ordering::Relaxed);
130 let goal = Goal {
131 id: format!("goal-{}", id),
132 objective: objective.clone(),
133 status: "active".to_string(),
134 token_budget,
135 tokens_used: 0,
136 created_at: now,
137 updated_at: now,
138 };
139
140 *store = Some(goal.clone());
141
142 let mut lines = vec![
143 format!("Goal created: {}", objective),
144 format!("Status: active"),
145 ];
146 if let Some(budget) = token_budget {
147 lines.push(format!("Token budget: {}", budget));
148 }
149
150 Ok(AgentToolResult::success(lines.join("\n")))
151 }
152 "get" => match store.as_ref() {
153 Some(goal) => {
154 let mut lines = vec![
155 format!("Goal: {}", goal.objective),
156 format!("Status: {}", goal.status),
157 format!("Tokens used: {}", goal.tokens_used),
158 ];
159 if let Some(budget) = goal.token_budget {
160 lines.push(format!("Token budget: {}", budget));
161 let remaining = budget.saturating_sub(goal.tokens_used);
162 lines.push(format!("Remaining: {}", remaining));
163 }
164 Ok(AgentToolResult::success(lines.join("\n")))
165 }
166 None => Ok(AgentToolResult::success(
167 "No active goal. Use 'create' to set one.",
168 )),
169 },
170 "complete" => match store.as_mut() {
171 Some(goal) => {
172 goal.status = "complete".to_string();
173 goal.updated_at = now;
174 let goal_clone = goal.clone();
175 *store = None;
176
177 Ok(AgentToolResult::success(format!(
178 "Goal completed: {}\nTokens used: {}",
179 goal_clone.objective, goal_clone.tokens_used
180 )))
181 }
182 None => Err("No active goal to complete.".to_string()),
183 },
184 "resume" => match store.as_mut() {
185 Some(goal)
186 if goal.status == "complete"
187 || goal.status == "paused"
188 || goal.status == "dropped" =>
189 {
190 goal.status = "active".to_string();
191 goal.updated_at = now;
192 Ok(AgentToolResult::success(format!(
193 "Goal resumed: {}",
194 goal.objective
195 )))
196 }
197 Some(goal) => Err(format!(
198 "Goal is currently '{}' and cannot be resumed.",
199 goal.status
200 )),
201 None => Err("No goal to resume. Use 'create' first.".to_string()),
202 },
203 "drop" => match store.take() {
204 Some(goal) => Ok(AgentToolResult::success(format!(
205 "Goal dropped: {}",
206 goal.objective
207 ))),
208 None => Err("No active goal to drop.".to_string()),
209 },
210 _ => Err(format!("Unknown goal operation: {}", op)),
211 }
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[tokio::test]
220 async fn test_goal_create_get_complete() {
221 *ACTIVE_GOAL.lock().unwrap() = None;
223
224 let tool = GoalTool;
225
226 let params = json!({"op": "create", "objective": "Refactor auth module"});
228 let result = tool
229 .execute("id", params, None, &ToolContext::default())
230 .await
231 .unwrap();
232 assert!(result.success);
233 assert!(result.output.contains("Goal created"));
234
235 let params2 = json!({"op": "get"});
237 let result2 = tool
238 .execute("id", params2, None, &ToolContext::default())
239 .await
240 .unwrap();
241 assert!(result2.success);
242 assert!(result2.output.contains("Refactor auth module"));
243
244 let params3 = json!({"op": "create", "objective": "Second goal"});
246 let result3 = tool
247 .execute("id", params3.clone(), None, &ToolContext::default())
248 .await;
249 assert!(result3.is_err());
250
251 let params4 = json!({"op": "complete"});
253 let result4 = tool
254 .execute("id", params4.clone(), None, &ToolContext::default())
255 .await
256 .unwrap();
257 assert!(result4.success);
258 assert!(result4.output.contains("Goal completed"));
259
260 let result5 = tool
262 .execute("id", params4.clone(), None, &ToolContext::default())
263 .await;
264 assert!(result5.is_err());
265 }
266
267 #[tokio::test]
268 async fn test_goal_create_with_budget() {
269 *ACTIVE_GOAL.lock().unwrap() = None;
270
271 let tool = GoalTool;
272 let params = json!({"op": "create", "objective": "Fix bugs", "token_budget": 5000});
273 let result = tool
274 .execute("id", params, None, &ToolContext::default())
275 .await
276 .unwrap();
277 assert!(result.success);
278 assert!(result.output.contains("5000"));
279 }
280}