feat: add /goal slash command for autonomous goal-driven continuation - #240
Merged
Conversation
Adds the `/goal` slash command, parity with Claude Code (v2.1.139+) and OpenAI Codex CLI. Developers already familiar with this command from other tools can now use it in Amplifier with the same semantics. **Behavior:** - `/goal <condition>` — sets a completion condition and immediately starts a turn. After each turn, a separate evaluator model (no tools) judges "done? yes/no + reason". If not done, another turn starts automatically with the reason as guidance. If done, the goal clears. - `/goal --max-turns N <condition>` — same, with a hard, Python-enforced turn cap. Invalid values (non-integer, 0, negatives) fail loud: error printed, no goal set, no turn runs. - `/goal` (bare) — shows condition, turns used (against cap if set), and evaluator's most recent reason. - `/goal clear` — clears it. Aliases: stop, off, reset, none, cancel. `/clear` also clears an active goal. - Works headless: `amplifier run --mode single "/goal ..."` parses the prefix in `execute_single()`, so the whole loop runs in one invocation. - SIGINT handling added to headless path: first Ctrl-C requests graceful cancellation, second requests immediate. **Implementation details:** - This PR *sets* `session.coordinator.session_state["goal"]` only. The auto-continue loop lives in the orchestrator (microsoft/amplifier-module-loop-streaming#30). This split is deliberate: putting the loop in the orchestrator makes it work headless, in the REPL, and in every other app that mounts it. - `_parse_goal_max_turns()` is a shared parser used by both interactive (CommandProcessor) and headless (execute_single) entry points, ensuring flag syntax and failure behavior cannot drift. - Default is no turn cap, matching Claude Code. The `--max-turns` flag is the opt-in bound — mechanically enforced. - New doc `docs/GOAL_COMMAND.md` covers usage and importantly how to write a condition that converges. **Validation:** - Headless and interactive end-to-end tested (3 auto-continuations → ✓ goal achieved). - Regression: with no goal set, behavior unchanged. - `--max-turns` trips exactly at cap; bad values fail loud with no goal set. - SIGINT mid-run → goal cleared, loop stopped, clean exit. - Independent validation in DTU: stdlib-only URL shortener, 28 tests passing, every endpoint verified. - Another run with "never use import" constraint held (grep count: 0, 39 tests passing). **Known pre-existing issue (flagged separately):** With amplifier-core 1.3.0 as locked, every turn fails with `TypeError: Object of type Decimal is not JSON serializable`, reproducible on a plain prompt with no goal involved. This is unrelated to this feature and deliberately not fixed here. Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
The auto-continue loop (microsoft/amplifier-module-loop-streaming#30) now emits orchestrator:goal_progress events instead of printing directly. Bare print() from an orchestrator corrupts protocol channels for other hosts (amplifier-agent's stdout JSON-RPC, amplifierd/amplifier-chat's journald-only stdout). This hook subscribes to the event and renders to the CLI's rich console, preserving the exact user-visible progress strings. Also adds documentation clarifying what --max-turns does NOT bound: - It bounds goal continuations (number of turns), not work within a turn - An agent that never stops its turn will bypass the cap - /goal is a completion gate, not a runaway guard - Unattended runs need external wall-clock limits too Registered in both interactive_chat and execute_single for consistent goal output across CLI modes. 🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier) Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
"## Summary\n\nAdds the
/goalslash command — parity with Claude Code (v2.1.139+) and OpenAI Codex CLI. Developers already familiar with this command from other tools can now use it in Amplifier with the same semantics.\n\n## What it does\n\n-/goal <condition>— sets a completion condition and immediately starts a turn. After each turn, a separate evaluator model (no tools) judges "done? yes/no + reason". If not done, another turn starts automatically with the reason as guidance. If done, the goal clears.\n-/goal --max-turns N <condition>— same, with a hard, Python-enforced turn cap. Invalid values (non-integer, 0, negatives) fail loud: error printed, no goal set, no turn runs.\n-/goal(bare) — shows condition, turns used (against cap if set), and evaluator's most recent reason.\n-/goal clear— clears it. Aliases:stop,off,reset,none,cancel./clearalso clears an active goal.\n\n## Headless support\n\nWorks in interactive mode and headless:\n\nbash\namplifier run --mode single \"/goal all tests in tests/ pass with pytest exiting 0\"\n\n\nThe entire loop runs in one invocation. SIGINT handling has been added to the headless path: first Ctrl-C requests graceful cancellation, second requests immediate.\n\n## Implementation & orchestrator dependency\n\nThis PR setssession.coordinator.session_state[\"goal\"]only. The auto-continue loop itself lives in the orchestrator — this PR depends on microsoft/amplifier-module-loop-streaming#30. Without that orchestrator change,/goalsets state but no auto-continuation occurs.\n\nThis split is deliberate: putting the loop in the orchestrator makes it work headless, in the REPL, and in every other app that mounts it, rather than needing one implementation per app.\n\n## CLI-side goal-progress renderer (NEW)\n\nProblem: The orchestrator PR (microsoft/amplifier-module-loop-streaming#30) removes allprint()calls from the module, because bare print() from an orchestrator corrupts protocol channels for other hosts —amplifier-agentuses stdout as a JSON-RPC channel,amplifierd/amplifier-chat/amplifier-voiceare daemons/web apps where prints land in journald invisible to the client, andamplifier-resolver-sdkdefensively redirects stdout→stderr to protect the JSON protocol.\n\nSolution: The orchestrator now emitsorchestrator:goal_progressevents instead. This PR adds a new CLI-side hook (goal_progress_hook.py) that subscribes to these events and renders to the CLI's rich console, preserving the exact user-visible progress strings (⟳ goal: turn N/CAP — reason,✓ goal achieved: ...,⚠ goal: hit turn cap (N) — stopping.,✗ goal: cancelled — ...,✗ goal: evaluator failed — ...).\n\nThe hook is registered unconditionally in bothinteractive_chatandexecute_single, following the same pattern asincremental_save.py, so goal output works consistently across all CLI modes.\n\n## Mechanical--max-turnscap\n\nThe--max-turnsflag is the opt-in bound — mechanically enforced by Python, not model-judged. Default is no turn cap, matching Claude Code.\n\n- Bad values (abc,0,-5) fail loud:ValueErroris raised, no goal is set, no turn runs.\n- Good values (1,5,100) are accepted and enforced by the orchestrator loop.\n- The shared parser_parse_goal_max_turns()is used by both interactive (CommandProcessor) and headless (execute_single) entry points, ensuring flag syntax and failure behavior cannot drift between them.\n\n### What the cap does NOT bound\n\n--max-turnsbounds goal continuations — the number of times the goal loop starts a new turn. It does not bound work happening inside a single turn.\n\nAn agent can make an unlimited number of tool calls within one turn; the turn ends only when the model stops requesting tools. If an agent gets stuck polling something that never completes, it never ends its turn, the goal evaluator never runs, and the cap never fires.\n\nThis was verified: an agent given a job-status script that always exited 0 and never reached 100% polled it 59+ times inside a single turn with--max-turns 8set. The cap was never reached because no turn ever ended.\n\n/goalis a completion gate, not a runaway guard. It catches an agent that stops too early. It cannot catch one that never stops. For unattended runs, bound the process externally (a wall-clocktimeout, a CI job limit) in addition to--max-turns.\n\n## New documentation\n\ndocs/GOAL_COMMAND.mdcovers usage, but importantly how to write a condition that converges. A vague or ambiguous condition is the dominant cost driver:\n\n- One run finished the actual work on turn 1 (28 tests passing) but the condition accidentally omitted the literal pytest command, so the evaluator correctly refused to accept it and pushed back twice more.\n- The doc prescribes: a measurable end state, the literal check command, and explicit MUST-NOT constraints.\n\n## Validation (independently verified)\n\nThe feature was proven to do its actual job — catching premature completion:\n\n- Staged-review trap: Agent implemented 1 of 6 required functions and returned. Evaluator caught it — "only theslugifyfunction has been implemented so far; the other 5 functions remain as stubs" — pushed back; agent completed all 6. Reviewer independently verified all 6 present and working.\n- Hidden-remainder trap: Spec required 12 functions plus a per-function changelog policy. Agent did all obvious work and returned. Evaluator caught a genuinely subtle deviation — "the CHANGELOG.md contains all 12 function entries... but they are all under a single dated entry rather than having a distinct dated entry for every one of the 12 functions as required" — pushed back; agent fixed it. Reviewer independently verified 12 functions, 12 distinct dates, all 12 named.\n\nBoth pushback reasons were specific and actionable, and both drove to a verified-correct final state. Zero evaluator failures across every validation run.\n\nPrior validation:\n- Headless and interactive both proven end-to-end (3 auto-continuations →✓ goal achieved).\n- Regression: with no goal set, behavior is unchanged.\n---max-turnstrips exactly at the cap; bad values fail loud with no goal set.\n- SIGINT mid-run → goal cleared, loop stopped, clean exit.\n- In a Digital Twin Universe, an agent built a stdlib-only URL shortener REST API; the reviewer independently ranpytest(28 passed, exit 0) and curled every endpoint (302/404/400/409 correct).\n- A separate "never use any import statement" run held that constraint too (grep -c import→ 0, 39 tests passing).\n\n## Pre-existing issue (flagged for separate PR)\n\nDuring validation, a pre-existing bug was discovered: withamplifier-core1.3.0 as locked, every turn fails withTypeError: Object of type Decimal is not JSON serializable, reproducible on a plain prompt with no goal involved. This is unrelated to this feature and deliberately not fixed here. It should be addressed in a separate issue/PR.\n\n---\n\nGenerated with Amplifier\n"