返回 DeepSeek-Reasonix
updategoal.go
根目录 / internal / tool / builtin / updategoal.go
1 package builtin
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "strings"
8
9 goaldomain "reasonix/internal/goal"
10 "reasonix/internal/tool"
11 )
12
13 func init() { tool.RegisterBuiltin(updateGoal{}) }
14
15 type updateGoal struct{}
16
17 type updateGoalInput struct {
18 GoalID string `json:"goal_id"`
19 Revision uint64 `json:"revision"`
20 Action tool.GoalAction `json:"action"`
21 Objective *string `json:"objective"`
22 Limit optionalRoundLimit `json:"max_goal_rounds"`
23 BlockedReason *string `json:"blocked_reason"`
24 }
25
26 func (updateGoal) Name() string { return "update_goal" }
27 func (updateGoal) Description() string {
28 return "Update the exact current goal revision. edit, pause, and resume require current direct-human authority; complete and blocked are also allowed during the exact autonomous goal round. There is no continue action: leaving an active goal unchanged continues it automatically. Only edit consumes objective/max_goal_rounds; only blocked consumes blocked_reason. Omit unused fields; supplied unused fields are ignored."
29 }
30 func (updateGoal) Schema() json.RawMessage {
31 return json.RawMessage(`{"type":"object","additionalProperties":false,"properties":{"goal_id":{"type":"string","minLength":1},"revision":{"type":"integer","minimum":1},"action":{"type":"string","enum":["edit","pause","resume","complete","blocked"]},"objective":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Replacement non-empty objective for edit; omit or null to keep it. Ignored for other actions."},"max_goal_rounds":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"description":"Replacement limit for edit; omit to keep it, null removes the limit. Ignored for other actions."},"blocked_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Non-empty concrete blocker required for blocked. Ignored for other actions."}},"required":["goal_id","revision","action"]}`)
32 }
33 func (updateGoal) ReadOnly() bool { return false }
34 func (updateGoal) ProviderVisible(ctx context.Context) bool {
35 _, ok := tool.GoalLifecycleFromContext(ctx)
36 return ok
37 }
38 func (updateGoal) Execute(ctx context.Context, args json.RawMessage) (string, error) {
39 if strings.Contains(string(args), `"status"`) && !strings.Contains(string(args), `"action"`) {
40 return "", fmt.Errorf("legacy update_goal protocol is unsupported; call get_goal, then use goal_id, revision, and action; leaving an active goal unchanged continues automatically")
41 }
42 var input updateGoalInput
43 if err := decodeGoalArgs(args, &input, "update_goal"); err != nil {
44 return "", err
45 }
46 input.GoalID = strings.TrimSpace(input.GoalID)
47 if input.GoalID == "" || input.Revision == 0 {
48 return "", fmt.Errorf("goal_id and a positive revision are required")
49 }
50 request := tool.GoalUpdateRequest{Ref: goaldomain.Ref{ID: input.GoalID, Revision: input.Revision}, Action: input.Action}
51 // The action selects the mutation payload. Models may echo the entire goal
52 // snapshot or fill unused optional fields; none may become implicit edits.
53 switch input.Action {
54 case tool.GoalActionEdit:
55 if input.Objective == nil && !input.Limit.Present {
56 return "", fmt.Errorf("edit requires objective and/or max_goal_rounds")
57 }
58 if input.Objective != nil {
59 value, err := trimmedRequired(*input.Objective, "objective")
60 if err != nil {
61 return "", err
62 }
63 request.Objective = &value
64 }
65 if input.Limit.Present {
66 limit, err := parseRoundLimit(input.Limit.Raw)
67 if err != nil {
68 return "", err
69 }
70 request.MaxGoalRounds = goaldomain.RoundLimitChange{Set: true, Value: limit}
71 }
72 case tool.GoalActionBlocked:
73 if input.BlockedReason == nil {
74 return "", fmt.Errorf("blocked requires blocked_reason")
75 }
76 message, err := trimmedRequired(*input.BlockedReason, "blocked_reason")
77 if err != nil {
78 return "", err
79 }
80 request.BlockedReason = &goaldomain.BlockReason{Code: "model-blocked", Message: message}
81 case tool.GoalActionPause, tool.GoalActionResume, tool.GoalActionComplete:
82 default:
83 return "", fmt.Errorf("action must be one of edit|pause|resume|complete|blocked")
84 }
85 binding, err := goalBinding(ctx)
86 if err != nil {
87 return "", err
88 }
89 view, err := binding.Owner.UpdateGoal(ctx, request, binding.Authority)
90 if err != nil {
91 return "", goalToolError("update_goal", err)
92 }
93 instruction := ""
94 if view.Phase == goaldomain.PhaseComplete || view.Phase == goaldomain.PhaseBlocked {
95 instruction = "Finish the current turn with an accurate final summary for the user; no further automatic goal round will be admitted."
96 }
97 if ignored := input.ignoredFields(); len(ignored) > 0 {
98 notice := fmt.Sprintf("Fields not applied: %s. Action %q ignores these fields; the returned goal contains the effective state.", strings.Join(ignored, ", "), input.Action)
99 instruction = strings.TrimSpace(instruction + " " + notice)
100 }
101 return goalToolResultWithInstruction(&view, instruction)
102 }
103
104 func (p updateGoalInput) ignoredFields() []string {
105 var fields []string
106 if p.Action != tool.GoalActionEdit {
107 if p.Objective != nil {
108 fields = append(fields, "objective")
109 }
110 if p.Limit.Present {
111 fields = append(fields, "max_goal_rounds")
112 }
113 }
114 if p.Action != tool.GoalActionBlocked && p.BlockedReason != nil {
115 fields = append(fields, "blocked_reason")
116 }
117 return fields
118 }
119
119 lines GO