1use crate::event::AgentEvent;
2use crate::goal::runtime::GoalRuntimeHandle;
3use crate::goal::types::{GoalStatus, SessionGoal};
4use crate::tool::{
5 Tool, ToolDefinition, ToolInvocation, ToolInvocationContext, ToolKind, ToolMetadata, ToolResult,
6};
7use async_trait::async_trait;
8use serde_json::{Value, json};
9use std::sync::Arc;
10
11fn make_result(invocation_id: &str, ok: bool, output: Value) -> ToolResult {
14 ToolResult {
15 invocation_id: invocation_id.to_string(),
16 ok,
17 output,
18 }
19}
20
21fn goal_with_status(goal: &SessionGoal, status: GoalStatus) -> SessionGoal {
22 let mut new_goal = goal.clone();
23 new_goal.status = status;
24 new_goal.updated_at = crate::session::current_unix_timestamp();
25 new_goal
26}
27
28fn limit_short_description(value: &str) -> String {
29 value.trim().chars().take(40).collect()
30}
31
32fn emit_goal_updated(context: &ToolInvocationContext, goal: &SessionGoal) {
34 if let Some(tx) = &context.event_tx {
35 let _ = tx.send(AgentEvent::GoalUpdated {
36 session_id: goal.session_id.clone(),
37 goal_id: goal.goal_id.as_str().to_string(),
38 objective: goal.objective.clone(),
39 short_description: goal.short_description.clone(),
40 status: goal.status,
41 tokens_used: goal.tokens_used,
42 token_budget: goal.token_budget,
43 });
44 }
45}
46
47fn is_unfinished(status: GoalStatus) -> bool {
48 !status.is_terminal()
49}
50
51pub struct GetGoalTool {
54 runtime: Arc<GoalRuntimeHandle>,
55}
56
57impl GetGoalTool {
58 pub fn new(runtime: Arc<GoalRuntimeHandle>) -> Self {
59 Self { runtime }
60 }
61
62 pub fn definition_static() -> ToolDefinition {
63 ToolDefinition {
64 name: "get_goal".to_string(),
65 description: "Get the current goal for this thread, including status, budgets, token \
66 and elapsed-time usage, and remaining token budget."
67 .to_string(),
68 kind: ToolKind::Read,
69 input_schema: json!({
70 "type": "object",
71 "properties": {},
72 "additionalProperties": false
73 }),
74 metadata: ToolMetadata {
75 tags: vec!["goal".to_string(), "session".to_string()],
76 capabilities: vec!["goal.read".to_string()],
77 exposure: crate::tool::ToolExposure::Direct,
78 ..ToolMetadata::default()
79 },
80 }
81 }
82}
83
84#[async_trait]
85impl Tool for GetGoalTool {
86 fn definition(&self) -> ToolDefinition {
87 Self::definition_static()
88 }
89
90 async fn invoke(&self, invocation: ToolInvocation) -> anyhow::Result<ToolResult> {
91 let result = match self.runtime.get_goal() {
92 Some(goal) => make_result(
93 &invocation.id,
94 true,
95 json!({
96 "goal": {
97 "goal_id": goal.goal_id.as_str(),
98 "objective": goal.objective,
99 "short_description": goal.short_description,
100 "status": goal.status.as_str(),
101 "tokens_used": goal.tokens_used,
102 "token_budget": goal.token_budget,
103 "time_used_seconds": goal.time_used_seconds,
104 "created_at": goal.created_at,
105 "updated_at": goal.updated_at
106 },
107 "remaining_tokens": goal.remaining_budget(),
108 }),
109 ),
110 None => make_result(
111 &invocation.id,
112 true,
113 json!({
114 "goal": null,
115 "message": "No goal is currently set for this session."
116 }),
117 ),
118 };
119 Ok(result)
120 }
121
122 async fn invoke_with_context(
123 &self,
124 invocation: ToolInvocation,
125 _context: ToolInvocationContext,
126 ) -> anyhow::Result<ToolResult> {
127 self.invoke(invocation).await
128 }
129}
130
131pub struct CreateGoalTool {
134 runtime: Arc<GoalRuntimeHandle>,
135}
136
137impl CreateGoalTool {
138 pub fn new(runtime: Arc<GoalRuntimeHandle>) -> Self {
139 Self { runtime }
140 }
141
142 pub fn definition_static() -> ToolDefinition {
143 ToolDefinition {
144 name: "create_goal".to_string(),
145 description: format!(
146 "Create a goal only when explicitly requested by the user or system/developer \
147instructions; do not infer goals from ordinary tasks.\n\
148Set token_budget only when an explicit token budget is requested. Fails if an unfinished goal \
149exists; use `{UPDATE_GOAL}` only for status.",
150 UPDATE_GOAL = "update_goal"
151 ),
152 kind: ToolKind::Write,
153 input_schema: json!({
154 "type": "object",
155 "properties": {
156 "objective": {
157 "type": "string",
158 "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."
159 },
160 "short_description": {
161 "type": "string",
162 "maxLength": 40,
163 "description": "Optional short UI label (max 40 characters)."
164 },
165 "token_budget": {
166 "type": "integer",
167 "description": "Positive token budget for the new goal. Omit unless explicitly requested."
168 }
169 },
170 "required": ["objective"],
171 "additionalProperties": false
172 }),
173 metadata: ToolMetadata {
174 tags: vec!["goal".to_string(), "session".to_string()],
175 capabilities: vec!["goal.create".to_string()],
176 exposure: crate::tool::ToolExposure::Direct,
177 ..ToolMetadata::default()
178 },
179 }
180 }
181}
182
183#[async_trait]
184impl Tool for CreateGoalTool {
185 fn definition(&self) -> ToolDefinition {
186 Self::definition_static()
187 }
188
189 async fn invoke(&self, invocation: ToolInvocation) -> anyhow::Result<ToolResult> {
190 self.invoke_with_context(invocation, ToolInvocationContext::default())
191 .await
192 }
193
194 async fn invoke_with_context(
195 &self,
196 invocation: ToolInvocation,
197 context: ToolInvocationContext,
198 ) -> anyhow::Result<ToolResult> {
199 let args = invocation.input.clone();
200
201 let objective = args
202 .get("objective")
203 .and_then(|v| v.as_str())
204 .map(str::trim)
205 .unwrap_or("")
206 .to_string();
207 if objective.is_empty() {
208 return Ok(make_result(
209 &invocation.id,
210 false,
211 json!({"error": "objective is required"}),
212 ));
213 }
214
215 if let Some(existing) = self.runtime.get_goal() {
216 if is_unfinished(existing.status) {
217 return Ok(make_result(
218 &invocation.id,
219 false,
220 json!({
221 "error": "cannot create a new goal because this thread has an unfinished goal; complete the existing goal first",
222 "existing_goal_id": existing.goal_id.as_str(),
223 "existing_status": existing.status.as_str(),
224 }),
225 ));
226 }
227 }
228
229 let token_budget = args.get("token_budget").and_then(|v| v.as_i64());
230 if let Some(budget) = token_budget {
231 if budget <= 0 {
232 return Ok(make_result(
233 &invocation.id,
234 false,
235 json!({"error": "token_budget must be a positive integer when set"}),
236 ));
237 }
238 }
239
240 let short_description = args
241 .get("short_description")
242 .and_then(|v| v.as_str())
243 .map(limit_short_description);
244
245 let goal = self
247 .runtime
248 .create_new_goal(objective, short_description, token_budget);
249 self.runtime.set_auto_continue(true);
250 emit_goal_updated(&context, &goal);
251
252 Ok(make_result(
253 &invocation.id,
254 true,
255 json!({
256 "goal": {
257 "goal_id": goal.goal_id.as_str(),
258 "objective": goal.objective,
259 "short_description": goal.short_description,
260 "status": goal.status.as_str(),
261 "token_budget": goal.token_budget,
262 "tokens_used": goal.tokens_used,
263 },
264 "message": "Goal created successfully. The agent will auto-continue working toward this goal."
265 }),
266 ))
267 }
268}
269
270pub struct UpdateGoalTool {
273 runtime: Arc<GoalRuntimeHandle>,
274}
275
276impl UpdateGoalTool {
277 pub fn new(runtime: Arc<GoalRuntimeHandle>) -> Self {
278 Self { runtime }
279 }
280
281 pub fn definition_static() -> ToolDefinition {
282 ToolDefinition {
283 name: "update_goal".to_string(),
284 description: r#"Update the existing goal.
285Use this tool only to mark the goal achieved or genuinely blocked.
286Set status to `complete` only when the objective has actually been achieved and no required work remains.
287Set 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.
288If 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.
289Once the blocked threshold is satisfied, do not keep reporting that you are still blocked while leaving the goal active; set status to `blocked`.
290Do not use `blocked` merely because the work is hard, slow, uncertain, incomplete, or would benefit from clarification.
291Do not mark a goal complete merely because its budget is nearly exhausted or because you are stopping work.
292You cannot use this tool to pause, resume, budget-limit, or usage-limit a goal; those status changes are controlled by the user or system.
293When marking a budgeted goal achieved with status `complete`, report the final token usage from the tool result to the user."#
294 .to_string(),
295 kind: ToolKind::Write,
296 input_schema: json!({
297 "type": "object",
298 "properties": {
299 "status": {
300 "type": "string",
301 "enum": ["complete", "blocked"],
302 "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."
303 }
304 },
305 "required": ["status"],
306 "additionalProperties": false
307 }),
308 metadata: ToolMetadata {
309 tags: vec!["goal".to_string(), "session".to_string()],
310 capabilities: vec!["goal.update".to_string()],
311 exposure: crate::tool::ToolExposure::Direct,
312 ..ToolMetadata::default()
313 },
314 }
315 }
316}
317
318#[async_trait]
319impl Tool for UpdateGoalTool {
320 fn definition(&self) -> ToolDefinition {
321 Self::definition_static()
322 }
323
324 async fn invoke(&self, invocation: ToolInvocation) -> anyhow::Result<ToolResult> {
325 self.invoke_with_context(invocation, ToolInvocationContext::default())
326 .await
327 }
328
329 async fn invoke_with_context(
330 &self,
331 invocation: ToolInvocation,
332 context: ToolInvocationContext,
333 ) -> anyhow::Result<ToolResult> {
334 let args = invocation.input.clone();
335 let status = args.get("status").and_then(|v| v.as_str()).unwrap_or("");
336
337 let goal = match self.runtime.get_goal() {
338 Some(g) => g,
339 None => {
340 return Ok(make_result(
341 &invocation.id,
342 false,
343 json!({"error": "cannot update goal because this thread has no goal"}),
344 ));
345 }
346 };
347
348 match status {
349 "complete" => {
350 let updated = goal_with_status(&goal, GoalStatus::Complete);
351 self.runtime.update_goal(updated.clone());
352 self.runtime.set_auto_continue(false);
353 emit_goal_updated(&context, &updated);
354 Ok(make_result(
355 &invocation.id,
356 true,
357 json!({
358 "goal": {
359 "goal_id": updated.goal_id.as_str(),
360 "objective": updated.objective,
361 "status": "complete",
362 "tokens_used": updated.tokens_used,
363 "token_budget": updated.token_budget,
364 "time_used_seconds": updated.time_used_seconds,
365 },
366 "remaining_tokens": updated.remaining_budget(),
367 "completion_budget_report": updated.token_budget.map(|b| {
368 format!(
369 "Goal complete. Tokens used: {} / budget: {}.",
370 updated.tokens_used, b
371 )
372 }),
373 "message": "Goal marked as complete."
374 }),
375 ))
376 }
377 "blocked" => {
378 let updated = goal_with_status(&goal, GoalStatus::Blocked);
379 self.runtime.update_goal(updated.clone());
380 self.runtime.set_auto_continue(false);
381 emit_goal_updated(&context, &updated);
382 Ok(make_result(
383 &invocation.id,
384 true,
385 json!({
386 "goal": {
387 "goal_id": updated.goal_id.as_str(),
388 "objective": updated.objective,
389 "status": "blocked",
390 "tokens_used": updated.tokens_used,
391 "token_budget": updated.token_budget,
392 },
393 "message": "Goal marked as blocked."
394 }),
395 ))
396 }
397 other => Ok(make_result(
398 &invocation.id,
399 false,
400 json!({
401 "error": format!(
402 "update_goal can only mark the existing goal complete or blocked; got `{other}`. Pause, resume, budget-limited, and usage-limited status changes are controlled by the user or system"
403 )
404 }),
405 )),
406 }
407 }
408}
409
410pub struct UpdateGoalChecklistTool {
415 runtime: Arc<GoalRuntimeHandle>,
416}
417
418impl UpdateGoalChecklistTool {
419 pub fn new(runtime: Arc<GoalRuntimeHandle>) -> Self {
420 Self { runtime }
421 }
422
423 pub fn definition_static() -> ToolDefinition {
424 ToolDefinition {
425 name: "update_goal_checklist".to_string(),
426 description: "Host/API checklist management for a session goal (not model-facing)."
427 .to_string(),
428 kind: ToolKind::Write,
429 input_schema: json!({
430 "type": "object",
431 "properties": {
432 "action": {
433 "type": "string",
434 "enum": ["set", "update"]
435 },
436 "tasks": {
437 "type": "array",
438 "items": { "type": "string" }
439 },
440 "task_id": { "type": "integer" },
441 "status": {
442 "type": "string",
443 "enum": ["pending", "in_progress", "done", "verified", "skipped"]
444 },
445 "verification": { "type": "string" }
446 },
447 "required": ["action"],
448 "additionalProperties": false
449 }),
450 metadata: ToolMetadata {
451 tags: vec![
452 "goal".to_string(),
453 "session".to_string(),
454 "host".to_string(),
455 ],
456 capabilities: vec!["goal.update".to_string()],
457 exposure: crate::tool::ToolExposure::Internal,
458 ..ToolMetadata::default()
459 },
460 }
461 }
462}
463
464#[async_trait]
465impl Tool for UpdateGoalChecklistTool {
466 fn definition(&self) -> ToolDefinition {
467 Self::definition_static()
468 }
469
470 async fn invoke(&self, invocation: ToolInvocation) -> anyhow::Result<ToolResult> {
471 self.invoke_with_context(invocation, ToolInvocationContext::default())
472 .await
473 }
474
475 async fn invoke_with_context(
476 &self,
477 invocation: ToolInvocation,
478 context: ToolInvocationContext,
479 ) -> anyhow::Result<ToolResult> {
480 use super::types::{GoalTask, TaskStatus};
481
482 let args = invocation.input.clone();
483 let action = args.get("action").and_then(|v| v.as_str()).unwrap_or("");
484
485 let result = match action {
486 "set" => {
487 let tasks = args.get("tasks").and_then(|v| v.as_array()).cloned();
488 let Some(tasks) = tasks else {
489 return Ok(make_result(
490 &invocation.id,
491 false,
492 json!({"error": "`tasks` array is required for action `set`"}),
493 ));
494 };
495 let goal_tasks: Vec<GoalTask> = tasks
496 .iter()
497 .enumerate()
498 .filter_map(|(i, v)| {
499 v.as_str()
500 .map(|s| s.trim().to_string())
501 .filter(|s| !s.is_empty())
502 .map(|desc| GoalTask::new(i, desc))
503 })
504 .collect();
505 match self.runtime.update_checklist(goal_tasks.clone()) {
506 Some(goal) => {
507 emit_goal_updated(&context, &goal);
508 make_result(
509 &invocation.id,
510 true,
511 json!({
512 "updated": true,
513 "task_count": goal_tasks.len(),
514 "message": "Checklist set."
515 }),
516 )
517 }
518 None => make_result(
519 &invocation.id,
520 false,
521 json!({"error": "No goal is currently set."}),
522 ),
523 }
524 }
525 "update" => {
526 let task_id = args
527 .get("task_id")
528 .and_then(|v| v.as_u64())
529 .map(|v| v as usize);
530 let status_str = args.get("status").and_then(|v| v.as_str());
531 let (Some(task_id), Some(status_str)) = (task_id, status_str) else {
532 return Ok(make_result(
533 &invocation.id,
534 false,
535 json!({"error": "`task_id` and `status` are required for action `update`"}),
536 ));
537 };
538 let status = match status_str {
539 "pending" => TaskStatus::Pending,
540 "in_progress" => TaskStatus::InProgress,
541 "done" => TaskStatus::Done,
542 "verified" => TaskStatus::Verified,
543 "skipped" => TaskStatus::Skipped,
544 other => {
545 return Ok(make_result(
546 &invocation.id,
547 false,
548 json!({"error": format!("Unknown status `{other}`")}),
549 ));
550 }
551 };
552 match self.runtime.update_task_status(task_id, status) {
553 Some(mut goal) => {
554 if let Some(verification) =
555 args.get("verification").and_then(|v| v.as_str())
556 {
557 if let Some(task) = goal.checklist.get_mut(task_id) {
558 task.verification = Some(verification.to_string());
559 task.verified = status == TaskStatus::Verified;
560 }
561 self.runtime.update_goal(goal.clone());
562 }
563 emit_goal_updated(&context, &goal);
564 make_result(
565 &invocation.id,
566 true,
567 json!({
568 "updated": true,
569 "task_id": task_id,
570 "status": status_str,
571 }),
572 )
573 }
574 None => make_result(
575 &invocation.id,
576 false,
577 json!({"error": "No goal is set or task_id is out of range."}),
578 ),
579 }
580 }
581 other => make_result(
582 &invocation.id,
583 false,
584 json!({"error": format!("Unknown action `{other}`. Valid actions: set, update.")}),
585 ),
586 };
587 Ok(result)
588 }
589}
590
591pub fn goal_tool_definitions() -> Vec<ToolDefinition> {
593 vec![
594 GetGoalTool::definition_static(),
595 CreateGoalTool::definition_static(),
596 UpdateGoalTool::definition_static(),
597 ]
598}