返回 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 "reasonix/internal/tool"
10 )
11
12 func init() { tool.RegisterBuiltin(updateGoal{}) }
13
14 // updateGoal records the model's structured per-turn goal disposition for the
15 // active goal turn. Like complete_step it has no host side effects: the call
16 // only records candidate state, and the real FSM transition happens after the
17 // turn ends, once Delivery readiness and budget checks pass. It is a host
18 // workflow operation — it never requires write approval and grants no
19 // permissions. Outside an active goal turn it fails closed without changing
20 // any state, so plain chat cannot be hijacked into goal machinery.
21 type updateGoal struct{}
22
23 func (updateGoal) Name() string { return "update_goal" }
24
25 func (updateGoal) Description() string {
26 return "Report this turn's disposition for the active goal. Call it at the end of every goal turn instead of using prose markers: `continue` (work is ongoing — give a concrete next_action), `complete` (the request is fully done, output format and constraints satisfied, and verification was attempted or reported unavailable), or `blocked` (only the user can unblock: missing user-only information, an irreversible/externally visible operation, or changed scope). The host validates your claim against Delivery acceptance criteria and decides whether to continue automatically. Fields: `status` (required, one of continue|complete|blocked), `reason` (required for continue and blocked, optional for complete), `next_action` (optional concrete next step; recommended for continue)."
27 }
28
29 func (updateGoal) Schema() json.RawMessage {
30 return json.RawMessage(`{
31 "type":"object",
32 "properties":{
33 "status":{"type":"string","enum":["continue","complete","blocked"],"description":"continue = keep working autonomously; complete = the goal is fully done and verified; blocked = only the user can unblock."},
34 "reason":{"type":"string","description":"Short explanation. REQUIRED for continue and blocked; optional for complete."},
35 "next_action":{"type":"string","description":"Optional concrete next step. Recommended for continue so the host can guide the next turn."}
36 },
37 "required":["status"]
38 }`)
39 }
40
41 // ReadOnly is true: update_goal only records a claim; the host performs the
42 // state transition after the turn. It never needs approval and cannot expand
43 // tool permissions or bypass sandbox policy.
44 func (updateGoal) ReadOnly() bool { return true }
45
46 // PlanModeSafe reports true: the tool is read-only host bookkeeping, and
47 // outside an active goal turn its Execute fails closed anyway.
48 func (updateGoal) PlanModeSafe() bool { return true }
49
50 func (updateGoal) Execute(ctx context.Context, args json.RawMessage) (string, error) {
51 var p struct {
52 Status string `json:"status"`
53 Reason string `json:"reason"`
54 NextAction string `json:"next_action"`
55 }
56 if err := json.Unmarshal(args, &p); err != nil {
57 return "", fmt.Errorf("invalid update_goal args: %w", err)
58 }
59 p.Status = strings.ToLower(strings.TrimSpace(p.Status))
60 switch p.Status {
61 case "continue", "complete", "blocked":
62 default:
63 return "", fmt.Errorf("update_goal: status must be one of continue|complete|blocked, got %q — no goal state was changed", p.Status)
64 }
65 if (p.Status == "continue" || p.Status == "blocked") && strings.TrimSpace(p.Reason) == "" {
66 return "", fmt.Errorf("update_goal: reason is required for %s — no goal state was changed", p.Status)
67 }
68 recorder, ok := tool.GoalTurnRecorderFromContext(ctx)
69 if !ok {
70 return "", fmt.Errorf("update_goal is only available while an active goal turn is running — no goal state was changed")
71 }
72 return recorder.RecordGoalReport(tool.GoalReport{
73 Status: p.Status,
74 Reason: strings.TrimSpace(p.Reason),
75 NextAction: strings.TrimSpace(p.NextAction),
76 })
77 }
78
78 lines GO