fix(goals): let main agent start missions
- commit
- a1cd53cc19acb70f0d5683a07a19212ed67bcb2d
- author
- brice · · 2026-08-14 11:32 UTC
- parent
- 0ee5de5
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +16 -26
M README.md +2 -2
| @@ -254,7 +254,7 @@ Print-mode stats contain `provider`, `model`, `prompt_tokens`, `reasoning_tokens | |||
| 254 | 254 | - `create_worktree`: create a new branch from the current `HEAD` and check it out persistently. A repository with no configured root and no `.worktrees` directory first returns bootstrap guidance without making changes; the agent asks whether to use the repository default or an external root, then retries with `bootstrap_root`. The choice is saved privately in local Git config. Choosing `.worktrees` creates `<repository-root>/.worktrees/<branch>` and adds `/.worktrees/` to the root `.gitignore`; an absolute external root leaves tracked repository files and the root `.gitignore` unchanged. The tool never copies uncommitted or ignored files, and it refuses an existing branch or worktree path. | |
| 255 | 255 | - `lsp`: query configured language servers and linters for diagnostics, definitions, references, hover information, symbols, renames, code actions, capabilities, and raw protocol requests. LSP servers use stdio JSON-RPC; project `lsp.json` files can add or override servers and CLI linters. Diagnostics are bounded, sorted, deduplicated by path/severity/code/start position, and repeated issues are grouped before they reach the model. See [docs/lsp.md](docs/lsp.md). | |
| 256 | 256 | - `web_search`: search the public web through DuckDuckGo HTML and return bounded source titles, URLs, and snippets. Enabled by default for normal CLI sessions; it is unavailable to bot, default SDK, and packaged-agent runs. See [docs/web-search.md](docs/web-search.md). | |
| 257 | - | - `update_goal`: set the manager's next bounded goal within the active mission, or mark the active goal `done` or `blocked`. Starting, pausing, resuming, and clearing missions remain user-controlled through `/goal`. | |
| 257 | + | - `update_goal`: let the main agent start its own mission when none is active, set the next bounded goal within an active mission, or mark the active goal `done` or `blocked`. Pausing, resuming, and clearing missions remain user-controlled through `/goal`. | |
| 258 | 258 | ||
| 259 | 259 | When the sandbox is on (see `/jail`), filesystem tools, `grep`, and LSP workspace edits refuse paths outside the session cwd. `create_worktree` also requires its repository root, configured worktree destination, optional `.gitignore`, and Git metadata to remain inside the jail. Jail does not sandbox web-search network egress. | |
| 260 | 260 | ||
| @@ -335,7 +335,7 @@ Extension-registered commands appear under a divider at the bottom of the popup, | |||
| 335 | 335 | ||
| 336 | 336 | ### `/goal` | |
| 337 | 337 | ||
| 338 | - | In interactive mode, the first user request establishes a durable user-owned mission. `/goal <objective>` explicitly starts or replaces its current user-owned goal and begins work immediately. While a goal is active, zut starts one leased hidden follow-up whenever the interactive thread becomes idle; queued user input always runs first. The manager can use `update_goal` to settle the current goal or set a bounded next goal within the same mission. The current state appears in the status bar and prior transitions are available through `/goal history`; both persist with the session. | |
| 338 | + | In interactive mode, the first user request establishes a durable user-owned mission. `/goal <objective>` explicitly starts or replaces its current user-owned goal and begins work immediately. While a goal is active, zut starts one leased hidden follow-up whenever the interactive thread becomes idle; queued user input always runs first. When no mission is active, the main agent can use `update_goal` to start one of its own; within an active mission, it can settle the current goal or set a bounded next goal. Subagents cannot start or update missions. The current state appears in the status bar and prior transitions are available through `/goal history`; both persist with the session. | |
| 339 | 339 | ||
| 340 | 340 | Goals are unlimited by default. Zut records token use for each goal but only enforces a budget when it is explicitly configured in `config.json`: | |
| 341 | 341 | ||
| @@ -254,7 +254,7 @@ Print-mode stats contain `provider`, `model`, `prompt_tokens`, `reasoning_tokens | |||||
| 254 | - `create_worktree`: create a new branch from the current `HEAD` and check it out persistently. A repository with no configured root and no `.worktrees` directory first returns bootstrap guidance without making changes; the agent asks whether to use the repository default or an external root, then retries with `bootstrap_root`. The choice is saved privately in local Git config. Choosing `.worktrees` creates `<repository-root>/.worktrees/<branch>` and adds `/.worktrees/` to the root `.gitignore`; an absolute external root leaves tracked repository files and the root `.gitignore` unchanged. The tool never copies uncommitted or ignored files, and it refuses an existing branch or worktree path. | 254 | - `create_worktree`: create a new branch from the current `HEAD` and check it out persistently. A repository with no configured root and no `.worktrees` directory first returns bootstrap guidance without making changes; the agent asks whether to use the repository default or an external root, then retries with `bootstrap_root`. The choice is saved privately in local Git config. Choosing `.worktrees` creates `<repository-root>/.worktrees/<branch>` and adds `/.worktrees/` to the root `.gitignore`; an absolute external root leaves tracked repository files and the root `.gitignore` unchanged. The tool never copies uncommitted or ignored files, and it refuses an existing branch or worktree path. | ||
| 255 | - `lsp`: query configured language servers and linters for diagnostics, definitions, references, hover information, symbols, renames, code actions, capabilities, and raw protocol requests. LSP servers use stdio JSON-RPC; project `lsp.json` files can add or override servers and CLI linters. Diagnostics are bounded, sorted, deduplicated by path/severity/code/start position, and repeated issues are grouped before they reach the model. See [docs/lsp.md](docs/lsp.md). | 255 | - `lsp`: query configured language servers and linters for diagnostics, definitions, references, hover information, symbols, renames, code actions, capabilities, and raw protocol requests. LSP servers use stdio JSON-RPC; project `lsp.json` files can add or override servers and CLI linters. Diagnostics are bounded, sorted, deduplicated by path/severity/code/start position, and repeated issues are grouped before they reach the model. See [docs/lsp.md](docs/lsp.md). | ||
| 256 | - `web_search`: search the public web through DuckDuckGo HTML and return bounded source titles, URLs, and snippets. Enabled by default for normal CLI sessions; it is unavailable to bot, default SDK, and packaged-agent runs. See [docs/web-search.md](docs/web-search.md). | 256 | - `web_search`: search the public web through DuckDuckGo HTML and return bounded source titles, URLs, and snippets. Enabled by default for normal CLI sessions; it is unavailable to bot, default SDK, and packaged-agent runs. See [docs/web-search.md](docs/web-search.md). | ||
| 257 | - | - `update_goal`: set the manager's next bounded goal within the active mission, or mark the active goal `done` or `blocked`. Starting, pausing, resuming, and clearing missions remain user-controlled through `/goal`. | 257 | + | - `update_goal`: let the main agent start its own mission when none is active, set the next bounded goal within an active mission, or mark the active goal `done` or `blocked`. Pausing, resuming, and clearing missions remain user-controlled through `/goal`. |
| 258 | 258 | ||||
| 259 | When the sandbox is on (see `/jail`), filesystem tools, `grep`, and LSP workspace edits refuse paths outside the session cwd. `create_worktree` also requires its repository root, configured worktree destination, optional `.gitignore`, and Git metadata to remain inside the jail. Jail does not sandbox web-search network egress. | 259 | When the sandbox is on (see `/jail`), filesystem tools, `grep`, and LSP workspace edits refuse paths outside the session cwd. `create_worktree` also requires its repository root, configured worktree destination, optional `.gitignore`, and Git metadata to remain inside the jail. Jail does not sandbox web-search network egress. | ||
| 260 | 260 | ||||
| @@ -335,7 +335,7 @@ Extension-registered commands appear under a divider at the bottom of the popup, | |||||
| 335 | 335 | ||||
| 336 | ### `/goal` | 336 | ### `/goal` | ||
| 337 | 337 | ||||
| 338 | - | In interactive mode, the first user request establishes a durable user-owned mission. `/goal <objective>` explicitly starts or replaces its current user-owned goal and begins work immediately. While a goal is active, zut starts one leased hidden follow-up whenever the interactive thread becomes idle; queued user input always runs first. The manager can use `update_goal` to settle the current goal or set a bounded next goal within the same mission. The current state appears in the status bar and prior transitions are available through `/goal history`; both persist with the session. | 338 | + | In interactive mode, the first user request establishes a durable user-owned mission. `/goal <objective>` explicitly starts or replaces its current user-owned goal and begins work immediately. While a goal is active, zut starts one leased hidden follow-up whenever the interactive thread becomes idle; queued user input always runs first. When no mission is active, the main agent can use `update_goal` to start one of its own; within an active mission, it can settle the current goal or set a bounded next goal. Subagents cannot start or update missions. The current state appears in the status bar and prior transitions are available through `/goal history`; both persist with the session. |
| 339 | 339 | ||||
| 340 | Goals are unlimited by default. Zut records token use for each goal but only enforces a budget when it is explicitly configured in `config.json`: | 340 | Goals are unlimited by default. Zut records token use for each goal but only enforces a budget when it is explicitly configured in `config.json`: | ||
| 341 | 341 | ||||
M packages/agent/cli.go +1 -7
| @@ -695,7 +695,6 @@ func copyExtensionStates(states map[string]json.RawMessage) map[string]json.RawM | |||
| 695 | 695 | ||
| 696 | 696 | var ( | |
| 697 | 697 | errGoalActiveReplacement = errors.New("active goal replacement") | |
| 698 | - | errGoalMissionMissing = errors.New("goal mission missing") | |
| 699 | 698 | errGoalMissionMismatch = errors.New("goal mission mismatch") | |
| 700 | 699 | errGoalStorageUnavailable = errors.New("goal storage unavailable") | |
| 701 | 700 | errGoalStateWrite = errors.New("goal state write") | |
| @@ -730,8 +729,6 @@ func safeGoalCommitMessage(err error) string { | |||
| 730 | 729 | switch { | |
| 731 | 730 | case errors.Is(err, errGoalActiveReplacement): | |
| 732 | 731 | return "goal transition rejected: the current goal is still active" | |
| 733 | - | case errors.Is(err, errGoalMissionMissing): | |
| 734 | - | return "goal transition rejected: no active mission" | |
| 735 | 732 | case errors.Is(err, errGoalMissionMismatch): | |
| 736 | 733 | return "goal transition rejected: it does not belong to the active mission" | |
| 737 | 734 | case errors.Is(err, errGoalStorageUnavailable): | |
| @@ -755,10 +752,7 @@ func persistGoalToolResult(sess *core.Session, result core.ToolResult, goalMaxTo | |||
| 755 | 752 | if sess.Meta.Goal != nil && sess.Meta.Goal.Status == core.GoalActive { | |
| 756 | 753 | return errGoalActiveReplacement | |
| 757 | 754 | } | |
| 758 | - | if sess.Meta.Mission == nil { | |
| 759 | - | return errGoalMissionMissing | |
| 760 | - | } | |
| 761 | - | if update.MissionID == "" || update.MissionID != sess.Meta.Mission.ID { | |
| 755 | + | if sess.Meta.Mission != nil && update.MissionID != sess.Meta.Mission.ID { | |
| 762 | 756 | return errGoalMissionMismatch | |
| 763 | 757 | } | |
| 764 | 758 | goal := &core.SessionGoal{Objective: update.Objective, Status: core.GoalActive, Owner: core.GoalOwnerManager} | |
| @@ -695,7 +695,6 @@ func copyExtensionStates(states map[string]json.RawMessage) map[string]json.RawM | |||||
| 695 | 695 | ||||
| 696 | var ( | 696 | var ( | ||
| 697 | errGoalActiveReplacement = errors.New("active goal replacement") | 697 | errGoalActiveReplacement = errors.New("active goal replacement") | ||
| 698 | - | errGoalMissionMissing = errors.New("goal mission missing") | |||
| 699 | errGoalMissionMismatch = errors.New("goal mission mismatch") | 698 | errGoalMissionMismatch = errors.New("goal mission mismatch") | ||
| 700 | errGoalStorageUnavailable = errors.New("goal storage unavailable") | 699 | errGoalStorageUnavailable = errors.New("goal storage unavailable") | ||
| 701 | errGoalStateWrite = errors.New("goal state write") | 700 | errGoalStateWrite = errors.New("goal state write") | ||
| @@ -730,8 +729,6 @@ func safeGoalCommitMessage(err error) string { | |||||
| 730 | switch { | 729 | switch { | ||
| 731 | case errors.Is(err, errGoalActiveReplacement): | 730 | case errors.Is(err, errGoalActiveReplacement): | ||
| 732 | return "goal transition rejected: the current goal is still active" | 731 | return "goal transition rejected: the current goal is still active" | ||
| 733 | - | case errors.Is(err, errGoalMissionMissing): | |||
| 734 | - | return "goal transition rejected: no active mission" | |||
| 735 | case errors.Is(err, errGoalMissionMismatch): | 732 | case errors.Is(err, errGoalMissionMismatch): | ||
| 736 | return "goal transition rejected: it does not belong to the active mission" | 733 | return "goal transition rejected: it does not belong to the active mission" | ||
| 737 | case errors.Is(err, errGoalStorageUnavailable): | 734 | case errors.Is(err, errGoalStorageUnavailable): | ||
| @@ -755,10 +752,7 @@ func persistGoalToolResult(sess *core.Session, result core.ToolResult, goalMaxTo | |||||
| 755 | if sess.Meta.Goal != nil && sess.Meta.Goal.Status == core.GoalActive { | 752 | if sess.Meta.Goal != nil && sess.Meta.Goal.Status == core.GoalActive { | ||
| 756 | return errGoalActiveReplacement | 753 | return errGoalActiveReplacement | ||
| 757 | } | 754 | } | ||
| 758 | - | if sess.Meta.Mission == nil { | 755 | + | if sess.Meta.Mission != nil && update.MissionID != sess.Meta.Mission.ID { |
| 759 | - | return errGoalMissionMissing | |||
| 760 | - | } | |||
| 761 | - | if update.MissionID == "" || update.MissionID != sess.Meta.Mission.ID { | |||
| 762 | return errGoalMissionMismatch | 756 | return errGoalMissionMismatch | ||
| 763 | } | 757 | } | ||
| 764 | goal := &core.SessionGoal{Objective: update.Objective, Status: core.GoalActive, Owner: core.GoalOwnerManager} | 758 | goal := &core.SessionGoal{Objective: update.Objective, Status: core.GoalActive, Owner: core.GoalOwnerManager} | ||
M packages/agent/cli_session_test.go +8 -12
| @@ -150,12 +150,6 @@ func TestPersistToolResultStateUsesSafeGoalPersistenceCategories(t *testing.T) { | |||
| 150 | 150 | update: tools.GoalUpdate{Status: core.GoalActive, Objective: privateGoal}, | |
| 151 | 151 | message: "goal transition rejected: the current goal is still active", | |
| 152 | 152 | }, | |
| 153 | - | { | |
| 154 | - | name: "missing mission", | |
| 155 | - | setup: newSession, | |
| 156 | - | update: tools.GoalUpdate{Status: core.GoalActive, Objective: privateGoal, MissionID: privateMission}, | |
| 157 | - | message: "goal transition rejected: no active mission", | |
| 158 | - | }, | |
| 159 | 153 | { | |
| 160 | 154 | name: "mismatched mission", | |
| 161 | 155 | setup: func(t *testing.T) *core.Session { | |
| @@ -227,23 +221,25 @@ func TestPersistToolResultStateUsesSafeGoalPersistenceCategories(t *testing.T) { | |||
| 227 | 221 | } | |
| 228 | 222 | } | |
| 229 | 223 | ||
| 230 | - | func TestPersistGoalToolResultCreatesActiveGoal(t *testing.T) { | |
| 224 | + | func TestPersistGoalToolResultStartsManagerMission(t *testing.T) { | |
| 231 | 225 | sess, err := core.NewSession(t.TempDir(), t.TempDir(), "provider", "model", "version") | |
| 232 | 226 | if err != nil { | |
| 233 | 227 | t.Fatal(err) | |
| 234 | 228 | } | |
| 235 | 229 | defer sess.Close() | |
| 236 | - | if err := sess.EnsureMission("reproduce the reported failure", core.MissionSourceUser); err != nil { | |
| 237 | - | t.Fatal(err) | |
| 238 | - | } | |
| 239 | 230 | ||
| 240 | - | result := core.ToolResult{Details: tools.GoalUpdate{Status: core.GoalActive, Objective: "reproduce the reported failure", MissionID: sess.Meta.Mission.ID}} | |
| 231 | + | result := core.ToolResult{Details: tools.GoalUpdate{Status: core.GoalActive, Objective: "reproduce the reported failure"}} | |
| 241 | 232 | if err := persistGoalToolResult(sess, result); err != nil { | |
| 242 | 233 | t.Fatal(err) | |
| 243 | 234 | } | |
| 244 | - | if goal := sess.Meta.Goal; goal == nil || goal.Status != core.GoalActive || goal.Objective != "reproduce the reported failure" || goal.Owner != core.GoalOwnerManager { | |
| 235 | + | goal := sess.Meta.Goal | |
| 236 | + | if goal == nil || goal.Status != core.GoalActive || goal.Objective != "reproduce the reported failure" || goal.Owner != core.GoalOwnerManager { | |
| 245 | 237 | t.Fatalf("persisted goal = %#v", goal) | |
| 246 | 238 | } | |
| 239 | + | mission := sess.Meta.Mission | |
| 240 | + | if mission == nil || mission.Source != core.MissionSourceManager || mission.Objective != goal.Objective || mission.ID != goal.MissionID || mission.ActiveGoalID != goal.ID { | |
| 241 | + | t.Fatalf("persisted mission = %#v, goal = %#v", mission, goal) | |
| 242 | + | } | |
| 247 | 243 | } | |
| 248 | 244 | ||
| 249 | 245 | func TestPersistGoalToolResultAppliesConfiguredBudget(t *testing.T) { | |
| @@ -150,12 +150,6 @@ func TestPersistToolResultStateUsesSafeGoalPersistenceCategories(t *testing.T) { | |||||
| 150 | update: tools.GoalUpdate{Status: core.GoalActive, Objective: privateGoal}, | 150 | update: tools.GoalUpdate{Status: core.GoalActive, Objective: privateGoal}, | ||
| 151 | message: "goal transition rejected: the current goal is still active", | 151 | message: "goal transition rejected: the current goal is still active", | ||
| 152 | }, | 152 | }, | ||
| 153 | - | { | |||
| 154 | - | name: "missing mission", | |||
| 155 | - | setup: newSession, | |||
| 156 | - | update: tools.GoalUpdate{Status: core.GoalActive, Objective: privateGoal, MissionID: privateMission}, | |||
| 157 | - | message: "goal transition rejected: no active mission", | |||
| 158 | - | }, | |||
| 159 | { | 153 | { | ||
| 160 | name: "mismatched mission", | 154 | name: "mismatched mission", | ||
| 161 | setup: func(t *testing.T) *core.Session { | 155 | setup: func(t *testing.T) *core.Session { | ||
| @@ -227,23 +221,25 @@ func TestPersistToolResultStateUsesSafeGoalPersistenceCategories(t *testing.T) { | |||||
| 227 | } | 221 | } | ||
| 228 | } | 222 | } | ||
| 229 | 223 | ||||
| 230 | - | func TestPersistGoalToolResultCreatesActiveGoal(t *testing.T) { | 224 | + | func TestPersistGoalToolResultStartsManagerMission(t *testing.T) { |
| 231 | sess, err := core.NewSession(t.TempDir(), t.TempDir(), "provider", "model", "version") | 225 | sess, err := core.NewSession(t.TempDir(), t.TempDir(), "provider", "model", "version") | ||
| 232 | if err != nil { | 226 | if err != nil { | ||
| 233 | t.Fatal(err) | 227 | t.Fatal(err) | ||
| 234 | } | 228 | } | ||
| 235 | defer sess.Close() | 229 | defer sess.Close() | ||
| 236 | - | if err := sess.EnsureMission("reproduce the reported failure", core.MissionSourceUser); err != nil { | |||
| 237 | - | t.Fatal(err) | |||
| 238 | - | } | |||
| 239 | 230 | ||||
| 240 | - | result := core.ToolResult{Details: tools.GoalUpdate{Status: core.GoalActive, Objective: "reproduce the reported failure", MissionID: sess.Meta.Mission.ID}} | 231 | + | result := core.ToolResult{Details: tools.GoalUpdate{Status: core.GoalActive, Objective: "reproduce the reported failure"}} |
| 241 | if err := persistGoalToolResult(sess, result); err != nil { | 232 | if err := persistGoalToolResult(sess, result); err != nil { | ||
| 242 | t.Fatal(err) | 233 | t.Fatal(err) | ||
| 243 | } | 234 | } | ||
| 244 | - | if goal := sess.Meta.Goal; goal == nil || goal.Status != core.GoalActive || goal.Objective != "reproduce the reported failure" || goal.Owner != core.GoalOwnerManager { | 235 | + | goal := sess.Meta.Goal |
| 236 | + | if goal == nil || goal.Status != core.GoalActive || goal.Objective != "reproduce the reported failure" || goal.Owner != core.GoalOwnerManager { | |||
| 245 | t.Fatalf("persisted goal = %#v", goal) | 237 | t.Fatalf("persisted goal = %#v", goal) | ||
| 246 | } | 238 | } | ||
| 239 | + | mission := sess.Meta.Mission | |||
| 240 | + | if mission == nil || mission.Source != core.MissionSourceManager || mission.Objective != goal.Objective || mission.ID != goal.MissionID || mission.ActiveGoalID != goal.ID { | |||
| 241 | + | t.Fatalf("persisted mission = %#v, goal = %#v", mission, goal) | |||
| 242 | + | } | |||
| 247 | } | 243 | } | ||
| 248 | 244 | ||||
| 249 | func TestPersistGoalToolResultAppliesConfiguredBudget(t *testing.T) { | 245 | func TestPersistGoalToolResultAppliesConfiguredBudget(t *testing.T) { | ||
M packages/agent/tools/update_goal.go +5 -5
| @@ -13,12 +13,12 @@ import ( | |||
| 13 | 13 | ||
| 14 | 14 | const ( | |
| 15 | 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}` | |
| 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":"Required identifier from the active-goal context when setting a next goal in an existing mission. Omit it when starting a mission."},"reason":{"type":"string","description":"Concise reason when the goal is blocked."}},"required":["status"],"additionalProperties":false}` | |
| 17 | 17 | ) | |
| 18 | 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. | |
| 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. | |
| 22 | 22 | type UpdateGoalTool struct{} | |
| 23 | 23 | ||
| 24 | 24 | type updateGoalArgs struct { | |
| @@ -39,7 +39,7 @@ type GoalUpdate struct { | |||
| 39 | 39 | func (t *UpdateGoalTool) Name() string { return UpdateGoalToolName } | |
| 40 | 40 | ||
| 41 | 41 | func (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." | |
| 42 | + | 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." | |
| 43 | 43 | } | |
| 44 | 44 | ||
| 45 | 45 | func (t *UpdateGoalTool) Schema() json.RawMessage { return json.RawMessage(updateGoalSchema) } | |
| @@ -13,12 +13,12 @@ import ( | |||||
| 13 | 13 | ||||
| 14 | const ( | 14 | const ( | ||
| 15 | UpdateGoalToolName = "update_goal" | 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}` | 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":"Required identifier from the active-goal context when setting a next goal in an existing mission. Omit it when starting a mission."},"reason":{"type":"string","description":"Concise reason when the goal is blocked."}},"required":["status"],"additionalProperties":false}` |
| 17 | ) | 17 | ) | ||
| 18 | 18 | ||||
| 19 | - | // UpdateGoalTool lets the manager set the next concrete active goal or settle | 19 | + | // UpdateGoalTool lets the main agent start a mission, set its next concrete |
| 20 | - | // the current goal. Host-side persistence validates that a transition remains | 20 | + | // active goal, or settle the current goal. Host-side persistence validates |
| 21 | - | // within the session's active mission. | 21 | + | // transitions within an existing session mission. |
| 22 | type UpdateGoalTool struct{} | 22 | type UpdateGoalTool struct{} | ||
| 23 | 23 | ||||
| 24 | type updateGoalArgs struct { | 24 | type updateGoalArgs struct { | ||
| @@ -39,7 +39,7 @@ type GoalUpdate struct { | |||||
| 39 | func (t *UpdateGoalTool) Name() string { return UpdateGoalToolName } | 39 | func (t *UpdateGoalTool) Name() string { return UpdateGoalToolName } | ||
| 40 | 40 | ||||
| 41 | func (t *UpdateGoalTool) Description() string { | 41 | func (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." | 42 | + | 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." |
| 43 | } | 43 | } | ||
| 44 | 44 | ||||
| 45 | func (t *UpdateGoalTool) Schema() json.RawMessage { return json.RawMessage(updateGoalSchema) } | 45 | func (t *UpdateGoalTool) Schema() json.RawMessage { return json.RawMessage(updateGoalSchema) } | ||