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