| 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 | func (updateGoal) Name() string { return "update_goal" } |
| 18 | func (updateGoal) Description() string { |
| 19 | 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." |
| 20 | } |
| 21 | func (updateGoal) Schema() json.RawMessage { |
| 22 | 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":{"type":"string","minLength":1,"description":"Replacement objective; valid only for edit."},"max_goal_rounds":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"description":"Replacement limit for edit; null removes the limit."},"blocked_reason":{"type":"string","minLength":1,"description":"Concrete blocker; required only for blocked."}},"required":["goal_id","revision","action"]}`) |
| 23 | } |
| 24 | func (updateGoal) ReadOnly() bool { return false } |
| 25 | func (updateGoal) ProviderVisible(ctx context.Context) bool { |
| 26 | _, ok := tool.GoalLifecycleFromContext(ctx) |
| 27 | return ok |
| 28 | } |
| 29 | func (updateGoal) Execute(ctx context.Context, args json.RawMessage) (string, error) { |
| 30 | if strings.Contains(string(args), `"status"`) && !strings.Contains(string(args), `"action"`) { |
| 31 | 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") |
| 32 | } |
| 33 | var input struct { |
| 34 | GoalID string `json:"goal_id"` |
| 35 | Revision uint64 `json:"revision"` |
| 36 | Action tool.GoalAction `json:"action"` |
| 37 | Objective *string `json:"objective"` |
| 38 | Limit optionalRoundLimit `json:"max_goal_rounds"` |
| 39 | BlockedReason *string `json:"blocked_reason"` |
| 40 | } |
| 41 | if err := decodeGoalArgs(args, &input, "update_goal"); err != nil { |
| 42 | return "", err |
| 43 | } |
| 44 | input.GoalID = strings.TrimSpace(input.GoalID) |
| 45 | if input.GoalID == "" || input.Revision == 0 { |
| 46 | return "", fmt.Errorf("goal_id and a positive revision are required") |
| 47 | } |
| 48 | request := tool.GoalUpdateRequest{Ref: goaldomain.Ref{ID: input.GoalID, Revision: input.Revision}, Action: input.Action} |
| 49 | switch input.Action { |
| 50 | case tool.GoalActionEdit: |
| 51 | if input.BlockedReason != nil || (input.Objective == nil && !input.Limit.Present) { |
| 52 | return "", fmt.Errorf("edit requires objective and/or max_goal_rounds and does not accept blocked_reason") |
| 53 | } |
| 54 | if input.Objective != nil { |
| 55 | value, err := trimmedRequired(*input.Objective, "objective") |
| 56 | if err != nil { |
| 57 | return "", err |
| 58 | } |
| 59 | request.Objective = &value |
| 60 | } |
| 61 | if input.Limit.Present { |
| 62 | limit, err := parseRoundLimit(input.Limit.Raw) |
| 63 | if err != nil { |
| 64 | return "", err |
| 65 | } |
| 66 | request.MaxGoalRounds = goaldomain.RoundLimitChange{Set: true, Value: limit} |
| 67 | } |
| 68 | case tool.GoalActionBlocked: |
| 69 | if input.Objective != nil || input.Limit.Present || input.BlockedReason == nil { |
| 70 | return "", fmt.Errorf("blocked requires blocked_reason and does not accept edit fields") |
| 71 | } |
| 72 | message, err := trimmedRequired(*input.BlockedReason, "blocked_reason") |
| 73 | if err != nil { |
| 74 | return "", err |
| 75 | } |
| 76 | request.BlockedReason = &goaldomain.BlockReason{Code: "model-blocked", Message: message} |
| 77 | case tool.GoalActionPause, tool.GoalActionResume, tool.GoalActionComplete: |
| 78 | if input.Objective != nil || input.Limit.Present || input.BlockedReason != nil { |
| 79 | return "", fmt.Errorf("%s does not accept objective, max_goal_rounds, or blocked_reason", input.Action) |
| 80 | } |
| 81 | default: |
| 82 | return "", fmt.Errorf("action must be one of edit|pause|resume|complete|blocked") |
| 83 | } |
| 84 | binding, err := goalBinding(ctx) |
| 85 | if err != nil { |
| 86 | return "", err |
| 87 | } |
| 88 | view, err := binding.Owner.UpdateGoal(ctx, request, binding.Authority) |
| 89 | if err != nil { |
| 90 | return "", goalToolError("update_goal", err) |
| 91 | } |
| 92 | instruction := "" |
| 93 | if view.Phase == goaldomain.PhaseComplete || view.Phase == goaldomain.PhaseBlocked { |
| 94 | instruction = "Finish the current turn with an accurate final summary for the user; no further automatic goal round will be admitted." |
| 95 | } |
| 96 | return goalToolResultWithInstruction(&view, instruction) |
| 97 | } |
| 98 |