1use serde_json::{json, Value};
2
3use crate::goal;
4use crate::goal::GoalStatus;
5use crate::tools::{require_str, ToolResult, ToolRuntime};
6use crate::types::{FunctionDef, ToolDefinition};
7
8pub fn get_goal_definition() -> ToolDefinition {
9 ToolDefinition {
10 def_type: "function".to_string(),
11 function: FunctionDef {
12 name: "get_goal".to_string(),
13 description: "Get the current goal objective, status, and progress. Returns the goal if one is set, or a message indicating no goal exists.".to_string(),
14 parameters: json!({
15 "type": "object",
16 "properties": {},
17 "required": []
18 }),
19 },
20 }
21}
22
23pub fn update_goal_definition() -> ToolDefinition {
24 ToolDefinition {
25 def_type: "function".to_string(),
26 function: FunctionDef {
27 name: "update_goal".to_string(),
28 description: "Update the existing goal.\n\
29 Use this tool only to mark the goal achieved or genuinely blocked.\n\
30 Set status to `complete` only when the objective has actually been achieved and no required work remains.\n\
31 Set status to `blocked` only when the same blocking condition has repeated for at least three consecutive goal turns, counting the original/user-triggered turn and any automatic continuations, and the agent cannot make meaningful progress without user input or an external-state change.\n\
32 If the user resumes a goal that was previously marked `blocked`, treat the resumed run as a fresh blocked audit. If the same blocking condition then repeats for at least three consecutive resumed goal turns, set status to `blocked` again.\n\
33 Once the blocked threshold is satisfied, do not keep reporting that you are still blocked while leaving the goal active; set status to `blocked`.\n\
34 Do not use `blocked` merely because the work is hard, slow, uncertain, incomplete, or would benefit from clarification.\n\
35 Do not mark a goal complete merely because its budget is nearly exhausted or because you are stopping work.\n\
36 You cannot use this tool to pause, resume, budget-limit, or usage-limit a goal; those status changes are controlled by the user or system.\n\
37 When marking a budgeted goal achieved with status `complete`, report the final token usage from the tool result to the user.".to_string(),
38 parameters: json!({
39 "type": "object",
40 "properties": {
41 "status": {
42 "type": "string",
43 "enum": ["complete", "blocked"],
44 "description": "Required. Set to `complete` only when the objective is achieved and no required work remains. Set to `blocked` only after the same blocking condition has recurred for at least three consecutive goal turns and the agent is at an impasse. After a previously blocked goal is resumed, the resumed run starts a fresh blocked audit."
45 }
46 },
47 "required": ["status"]
48 }),
49 },
50 }
51}
52
53pub fn create_goal_definition() -> ToolDefinition {
54 ToolDefinition {
55 def_type: "function".to_string(),
56 function: FunctionDef {
57 name: "create_goal".to_string(),
58 description: "Create a goal only when explicitly requested by the user or system/developer instructions; do not infer goals from ordinary tasks.\n\
59 Set token_budget only when an explicit token budget is requested. Fails if an unfinished goal exists; use update_goal only for status.".to_string(),
60 parameters: json!({
61 "type": "object",
62 "properties": {
63 "objective": {
64 "type": "string",
65 "description": "Required. The concrete objective to start pursuing. This starts a new active goal when no goal exists or replaces the current goal when it is complete."
66 },
67 "token_budget": {
68 "type": "integer",
69 "description": "Positive token budget for the new goal. Omit unless explicitly requested."
70 }
71 },
72 "required": ["objective"]
73 }),
74 },
75 }
76}
77
78pub async fn execute_create_goal(args: Value, runtime: &ToolRuntime) -> ToolResult {
79 let session_id = match require_session(runtime) {
80 Ok(sid) => sid.to_string(),
81 Err(result) => return result,
82 };
83 let store_path = runtime.store_path.clone();
84
85 let objective = match require_str(&args, "objective") {
86 Ok(s) => s.trim().to_string(),
87 Err(result) => return result,
88 };
89
90 if objective.is_empty() {
91 return ToolResult {
92 content: "Error: objective must not be empty".to_string(),
93 is_error: true,
94 };
95 }
96
97 let token_budget: Option<i64> = args
98 .get("token_budget")
99 .and_then(|v| v.as_i64());
100
101 if let Some(budget) = token_budget {
102 if budget <= 0 {
103 return ToolResult {
104 content: "Error: token_budget must be positive when provided".to_string(),
105 is_error: true,
106 };
107 }
108 }
109
110 let result = tokio::task::spawn_blocking(move || {
111 if let Ok(Some(existing)) = goal::load_goal(&store_path, &session_id) {
113 if !existing.status.is_terminal() {
114 anyhow::bail!(
115 "An unfinished goal already exists. Complete or clear the current goal before creating a new one."
116 );
117 }
118 }
119
120 let now = goal::now_utc();
121 let new_goal = goal::GoalState {
122 goal_id: goal::new_goal_id(),
123 objective: objective.clone(),
124 status: GoalStatus::Active,
125 tokens_used: 0,
126 time_used_seconds: 0,
127 token_budget,
128 created_at: now.clone(),
129 updated_at: now,
130 };
131 goal::save_goal(&store_path, &session_id, &new_goal)?;
132 Ok(new_goal)
133 })
134 .await;
135
136 match result {
137 Ok(Ok(g)) => {
138 let mut lines = vec![
139 format!("Goal created successfully."),
140 format!("Objective: {}", g.objective),
141 format!("Status: {}", g.status.label()),
142 ];
143 if let Some(budget) = g.token_budget {
144 lines.push(format!("Token budget: {}", budget));
145 }
146 lines.push(format!("Created: {}", g.created_at));
147 ToolResult {
148 content: lines.join("\n"),
149 is_error: false,
150 }
151 }
152 Ok(Err(e)) => ToolResult {
153 content: format!("Error: {}", e),
154 is_error: true,
155 },
156 Err(e) => ToolResult {
157 content: format!("Error: {}", e),
158 is_error: true,
159 },
160 }
161}
162
163pub async fn execute_get_goal(_args: Value, runtime: &ToolRuntime) -> ToolResult {
164 let session_id = match require_session(runtime) {
165 Ok(sid) => sid.to_string(),
166 Err(result) => return result,
167 };
168 let store_path = runtime.store_path.clone();
169
170 let result = tokio::task::spawn_blocking(move || {
171 goal::load_goal(&store_path, &session_id)
172 })
173 .await;
174
175 match result {
176 Ok(Ok(Some(g))) => {
177 let mut lines = vec![
178 format!("Goal objective: {}", g.objective),
179 format!("Status: {}", g.status.label()),
180 format!("Tokens used: {}", g.tokens_used),
181 format!("Time used: {}s", g.time_used_seconds),
182 ];
183 if let Some(budget) = g.token_budget {
184 let remaining = (budget - g.tokens_used).max(0);
185 lines.push(format!("Token budget: {}", budget));
186 lines.push(format!("Tokens remaining: {}", remaining));
187 }
188 match g.status {
189 goal::GoalStatus::UsageLimited => {
190 lines.push("Note: Goal is paused because the session usage limit was exceeded. The user can resume with /goal resume.".to_string());
191 }
192 goal::GoalStatus::BudgetLimited => {
193 lines.push("Note: Goal is stopped because its token budget has been exhausted. The user must raise the budget and resume.".to_string());
194 }
195 _ => {}
196 }
197 lines.push(format!("Created: {}", g.created_at));
198 lines.push(format!("Updated: {}", g.updated_at));
199 ToolResult {
200 content: lines.join("\n"),
201 is_error: false,
202 }
203 }
204 Ok(Ok(None)) => ToolResult {
205 content: "No goal is currently set.".to_string(),
206 is_error: false,
207 },
208 Ok(Err(e)) => ToolResult {
209 content: format!("Error loading goal: {}", e),
210 is_error: true,
211 },
212 Err(e) => ToolResult {
213 content: format!("Error: {}", e),
214 is_error: true,
215 },
216 }
217}
218
219pub async fn execute_update_goal(args: Value, runtime: &ToolRuntime) -> ToolResult {
220 let session_id = match require_session(runtime) {
221 Ok(sid) => sid.to_string(),
222 Err(result) => return result,
223 };
224 let store_path = runtime.store_path.clone();
225
226 let status_str = match require_str(&args, "status") {
227 Ok(s) => s,
228 Err(result) => return result,
229 };
230
231 let new_status = match goal::GoalStatus::from_str(&status_str) {
232 Some(s) if matches!(s, goal::GoalStatus::Complete | goal::GoalStatus::Blocked) => s,
233 _ => {
234 return ToolResult {
235 content: "Error: status must be 'complete' or 'blocked'".to_string(),
236 is_error: true,
237 }
238 }
239 };
240
241 let status_label = new_status.label().to_string();
242 let result = tokio::task::spawn_blocking(move || {
243 let mut g = match goal::load_goal(&store_path, &session_id)? {
244 Some(g) => g,
245 None => anyhow::bail!("No goal is currently set"),
246 };
247 g.status = new_status;
248 g.updated_at = goal::now_utc();
249 goal::save_goal(&store_path, &session_id, &g)?;
250 Ok(g)
251 })
252 .await;
253
254 match result {
255 Ok(Ok(g)) => {
256 let budget_info = match g.token_budget {
257 Some(budget) => format!(
258 "\nTokens used: {} of {} budget",
259 g.tokens_used, budget
260 ),
261 None => format!("\nTokens used: {}", g.tokens_used),
262 };
263 ToolResult {
264 content: format!(
265 "Goal updated to '{}'. Objective: {}\nTime used: {}s{}",
266 status_label, g.objective, g.time_used_seconds, budget_info
267 ),
268 is_error: false,
269 }
270 }
271 Ok(Err(e)) => ToolResult {
272 content: format!("Error: {}", e),
273 is_error: true,
274 },
275 Err(e) => ToolResult {
276 content: format!("Error: {}", e),
277 is_error: true,
278 },
279 }
280}
281
282fn require_session(runtime: &ToolRuntime) -> Result<&str, ToolResult> {
283 runtime.session_id.as_deref().ok_or_else(|| ToolResult {
284 content: "Error: goal tools require an active session".to_string(),
285 is_error: true,
286 })
287}