1package tools
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "io"
8 "strings"
9
10 "github.com/bnema/zut/packages/core"
11 "github.com/bnema/zut/packages/provider"
12)
13
14const (
15 UpdateGoalToolName = "update_goal"
16 updateGoalSchema = `{"type":"object","properties":{"status":{"type":"string","enum":["complete","blocked"],"description":"Mark the active autonomous goal complete or blocked."},"reason":{"type":"string","description":"Concise reason when the goal is blocked."}},"required":["status"],"additionalProperties":false}`
17)
18
19// UpdateGoalTool lets the model stop an explicitly user-started autonomous
20// goal. Starting, pausing, and resuming remain user-controlled.
21type UpdateGoalTool struct{}
22
23type updateGoalArgs struct {
24 Status string `json:"status"`
25 Reason string `json:"reason,omitempty"`
26}
27
28// GoalUpdate is trusted metadata returned only by UpdateGoalTool.
29type GoalUpdate struct {
30 Status core.GoalStatus
31 Reason string
32}
33
34func (t *UpdateGoalTool) Name() string { return UpdateGoalToolName }
35
36func (t *UpdateGoalTool) Description() string {
37 return "Mark the active autonomous goal complete or blocked. Only use this for a goal explicitly started by the user with /goal."
38}
39
40func (t *UpdateGoalTool) Schema() json.RawMessage { return json.RawMessage(updateGoalSchema) }
41
42func (t *UpdateGoalTool) Execute(_ context.Context, raw json.RawMessage, _ func(string)) (core.ToolResult, error) {
43 var args updateGoalArgs
44 decoder := json.NewDecoder(bytes.NewReader(raw))
45 decoder.DisallowUnknownFields()
46 if err := decoder.Decode(&args); err != nil {
47 return goalToolError("invalid arguments"), nil
48 }
49 if err := decoder.Decode(&struct{}{}); err != io.EOF {
50 return goalToolError("invalid arguments"), nil
51 }
52 args.Status = strings.TrimSpace(args.Status)
53 args.Reason = strings.TrimSpace(args.Reason)
54
55 var update GoalUpdate
56 switch args.Status {
57 case "complete":
58 update.Status = core.GoalDone
59 case "blocked":
60 if args.Reason == "" {
61 return goalToolError("reason is required when blocking a goal"), nil
62 }
63 update.Status = core.GoalBlocked
64 update.Reason = args.Reason
65 default:
66 return goalToolError("status must be complete or blocked"), nil
67 }
68
69 text := "autonomous goal marked " + string(update.Status)
70 if update.Reason != "" {
71 text += ": " + update.Reason
72 }
73 return core.ToolResult{
74 Content: []provider.Content{provider.TextBlock{Text: text}},
75 Details: update,
76 }, nil
77}
78
79// GoalUpdateFromResult extracts a valid update_goal state transition.
80func GoalUpdateFromResult(result core.ToolResult) (GoalUpdate, bool) {
81 if result.IsError {
82 return GoalUpdate{}, false
83 }
84 update, ok := result.Details.(GoalUpdate)
85 if !ok {
86 return GoalUpdate{}, false
87 }
88 if update.Status != core.GoalDone && update.Status != core.GoalBlocked {
89 return GoalUpdate{}, false
90 }
91 if update.Status == core.GoalBlocked && strings.TrimSpace(update.Reason) == "" {
92 return GoalUpdate{}, false
93 }
94 return update, true
95}
96
97func goalToolError(message string) core.ToolResult {
98 return core.ToolResult{
99 Content: []provider.Content{provider.TextBlock{Text: message}},
100 IsError: true,
101 }
102}