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":["active","complete","blocked"],"description":"Set the next active goal, or mark the active goal complete or blocked."},"objective":{"type":"string","description":"Concrete objective for status active."},"mission_id":{"type":"string","description":"Mission identifier supplied in the active-goal context when setting a next goal."},"reason":{"type":"string","description":"Concise reason when the goal is blocked."}},"required":["status"],"additionalProperties":false}`
17)
18
19// UpdateGoalTool lets the manager set the next concrete active goal or settle
20// the current goal. Host-side persistence validates that a transition remains
21// within the session's active mission.
22type UpdateGoalTool struct{}
23
24type updateGoalArgs struct {
25 Status string `json:"status"`
26 Objective string `json:"objective,omitempty"`
27 MissionID string `json:"mission_id,omitempty"`
28 Reason string `json:"reason,omitempty"`
29}
30
31// GoalUpdate is trusted metadata returned only by UpdateGoalTool.
32type GoalUpdate struct {
33 Status core.GoalStatus
34 Objective string
35 MissionID string
36 Reason string
37}
38
39func (t *UpdateGoalTool) Name() string { return UpdateGoalToolName }
40
41func (t *UpdateGoalTool) Description() string {
42 return "Set the next concrete goal needed for the active user mission, or mark the active goal complete or blocked. Do not broaden the user's mission."
43}
44
45func (t *UpdateGoalTool) Schema() json.RawMessage { return json.RawMessage(updateGoalSchema) }
46
47func (t *UpdateGoalTool) Execute(_ context.Context, raw json.RawMessage, _ func(string)) (core.ToolResult, error) {
48 var args updateGoalArgs
49 decoder := json.NewDecoder(bytes.NewReader(raw))
50 decoder.DisallowUnknownFields()
51 if err := decoder.Decode(&args); err != nil {
52 return goalToolError("invalid arguments"), nil
53 }
54 if err := decoder.Decode(&struct{}{}); err != io.EOF {
55 return goalToolError("invalid arguments"), nil
56 }
57 args.Status = strings.TrimSpace(args.Status)
58 args.Objective = strings.TrimSpace(args.Objective)
59 args.MissionID = strings.TrimSpace(args.MissionID)
60 args.Reason = strings.TrimSpace(args.Reason)
61
62 var update GoalUpdate
63 switch args.Status {
64 case "active":
65 if args.Objective == "" {
66 return goalToolError("objective is required when setting a goal"), nil
67 }
68 update.Status = core.GoalActive
69 update.Objective = args.Objective
70 update.MissionID = args.MissionID
71 case "complete":
72 update.Status = core.GoalDone
73 case "blocked":
74 if args.Reason == "" {
75 return goalToolError("reason is required when blocking a goal"), nil
76 }
77 update.Status = core.GoalBlocked
78 update.Reason = args.Reason
79 default:
80 return goalToolError("status must be active, complete, or blocked"), nil
81 }
82
83 text := "autonomous goal marked " + string(update.Status)
84 if update.Reason != "" {
85 text += ": " + update.Reason
86 }
87 return core.ToolResult{
88 Content: []provider.Content{provider.TextBlock{Text: text}},
89 Details: update,
90 }, nil
91}
92
93// GoalUpdateFromResult extracts a valid update_goal state transition.
94func GoalUpdateFromResult(result core.ToolResult) (GoalUpdate, bool) {
95 if result.IsError {
96 return GoalUpdate{}, false
97 }
98 update, ok := result.Details.(GoalUpdate)
99 if !ok {
100 return GoalUpdate{}, false
101 }
102 if update.Status != core.GoalActive && update.Status != core.GoalDone && update.Status != core.GoalBlocked {
103 return GoalUpdate{}, false
104 }
105 if update.Status == core.GoalActive && strings.TrimSpace(update.Objective) == "" {
106 return GoalUpdate{}, false
107 }
108 if update.Status == core.GoalBlocked && strings.TrimSpace(update.Reason) == "" {
109 return GoalUpdate{}, false
110 }
111 return update, true
112}
113
114func goalToolError(message string) core.ToolResult {
115 return core.ToolResult{
116 Content: []provider.Content{provider.TextBlock{Text: message}},
117 IsError: true,
118 }
119}