feat(agent): Announce-then-stop system block - #357
Conversation
| systemBlocks = append(systemBlocks, "For questions about the current date or time, use an available read-only shell/time command to verify the answer instead of guessing from model memory.") | ||
| systemBlocks = append(systemBlocks, renderToolPolicyBlock()) | ||
| systemBlocks = append(systemBlocks, "For branch decisions or key assumptions requiring user choice, call request_user_input instead of presenting long A/B/C prose menus.") | ||
| systemBlocks = append(systemBlocks, "Execute, don't narrate: never end a turn by announcing further work — do it now, in this turn, with tool calls. A turn ends only when the user explicitly asked for it (e.g. \"stop\", \"summarize\", \"hand off\"), the task is genuinely complete, or you need user input. Otherwise keep going automatically — planned-but-unexecuted next steps are a failure, not a completion.") |
There was a problem hiding this comment.
This block is also injected into agents with an empty tool registry, and the test below explicitly pins it across Agent, Ask, and Plan modes. Requiring the work to be done “with tool calls” therefore conflicts with valid tool-less/read-only executions: a model-only subagent may be told to issue a tool call that does not exist, while Ask/Plan are intentionally allowed to finish without one.\n\nCould we make this conditional, matching the existing premature-end recovery nudge? For example: “If the announced action requires an available tool, issue that tool call now. Otherwise complete the work directly and provide the final answer.” This keeps the announce-then-stop fix without imposing tool use where tools are unavailable or unnecessary.
There was a problem hiding this comment.
Of course, can you design a complete instruction covering all cases and suggest it here?
Can you suggest tests for those tool-less or read-only executions so we can cover all possible cases?
There was a problem hiding this comment.
Better and shorter now, please review if you find more edge cases.
- Execute, don't narrate: never end a turn by announcing further work — do it now, in this turn, with tool calls. A turn ends only when the user explicitly asked for it (e.g. "stop", "summarize", "hand off"), the task is genuinely complete, or you need user input. Otherwise keep going automatically — planned-but-unexecuted next steps are a failure, not a completion.
+ Execute, don't narrate: never end a turn by announcing further work — do it now, in this turn, with tool calls when the work requires an available tool, otherwise finish directly. A turn ends only when the user explicitly asked for it (e.g. "stop", "summarize", "hand off"), the task is genuinely complete, or you need user input.Squash and merge when you didn't find any more improvements.
The new commit is only for reviewability.
There was a problem hiding this comment.
What changed in the guard logic
The premature-end guard previously recovered a turn only when both conditions held: a trailing colon and an imperative verb-prefix on the trailing clause ("fix ", "run ", "then ", …). Inflected lead-ins like "Fixing:" or "Running tests:" failed the prefix match ("fix " ≠ "fixing") and slipped through, ending the turn with zero tool calls.
New predicate: recover = endsWithColon OR action-prefix match.
- A trailing colon now triggers recovery on its own — 3 of 4 known announce-then-stop instances end in
":", including the inflected ones that used to miss. A colon-terminated final answer is near-nonsense anyway (it invites continuation), and the nudge's "if no action remains, provide the complete final answer" escape hatch plus the existing 2-nudge cap bound any false positive to one extra model call. - The verb-prefix scan is kept as the non-colon fallback and now also matches inflected
-inggerund forms (fixing,running,updating,writing,editing,checking,inspecting,verifying,executing,continuing,starting,retrying,rerunning,re-running) by exact enumeration — no stemming, no new false-positive surface beyond the intended inflected shapes. This covers the one known"."-terminated instance ("…then append maintainer reply draft.") and non-colon gerund lead-ins ("Fixing the permission check").
All outer gates unchanged: Agent mode only, SuppressTools off, tools available, end_turn, no tool calls, maxPrematureEndTurnNudges = 2. Strictly more permissive shape recognition; same push policy.
4781347 to
179c8ab
Compare
The model frequently ended turns by writing a status + next-steps ("…then
commit", "…inspect + resolve") followed by end_turn with zero tool calls.
On the fixed binary this is model behavior, not a dropped turn — full-size
completions with no retry signature.
Add one static instruction to buildImmutableSystemBlocksWithTools so it
reaches every entrypoint that shares the turn loop: ACP (whale-acp),
terminal CLI (internal/app runtime), and subagents (internal/tasks) — the
block is appended unconditionally for all agents, so no entrypoint-specific
wiring is needed.
Instruction: execute, don't narrate — never end a turn by announcing further
work; a turn ends only on explicit user request, genuine completion, or a
needed user input; otherwise keep going automatically.
Reasoning-replay half of the feature (upstream PR usewhale#353, prevent stale
response history replay) was already landed in main via the rebase onto
6de6b6b — verified ancestor of this branch; no code change needed here.
Maintainer review: the block is injected into agents with an empty tool
registry (model-only subagents) and is pinned across Agent/Ask/Plan modes,
where finishing without a tool call is legitimate. Requiring the work to be
done "with tool calls" forced impossible tool use there.
Reword to "... with tool calls when the work requires an available tool,
otherwise finish directly." — keeps the announce-then-stop fix while
allowing tool-less and read-only executions.
Also drop the closing "Otherwise keep going automatically — planned-but-
unexecuted next steps are a failure" sentence: the first sentence ("do it
now, in this turn ... otherwise finish directly") plus "A turn ends only
when ..." already cover continuation to completion; the block is new to
this branch (main has none), so nothing regresses. Pinned test fragments
updated accordingly.
Co-authored-by: GPT-5.6 Sol <codex@openai.com>
… inflection
The premature-end guard required BOTH a trailing colon AND a verb-prefix
match on the lowercased trailing clause. Gerund lead-ins ("Fixing:",
"Running tests:") failed the "fix " prefix scan and slipped through:
a session ended at "…contradicts. Fixing:" with 0 tool calls.
Widen the predicate to recover = endsWithColon OR action-prefix match:
- The trailing colon is the empirically reliable signal (3 of 4 known
announce-then-stop instances end in ":"), and a colon-terminated
final answer is near-nonsense; the nudge's escape hatch plus the
existing 2-nudge cap bound any false positive to one extra model call.
- The prefix list stays as the non-colon fallback so the one known
"."-terminated instance ("…then append maintainer reply draft.")
is still caught.
- Outer gates unchanged: Agent mode only, SuppressTools off, tools
available, end_turn, no tool calls, maxPrematureEndTurnNudges = 2.
Tests: add the gerund colon case, then-commit, inspect+resolve, running
tests, dot-terminated fallback, accepted colon false positives (fixture
analysis, headings, user choice prompt), non-end-turn finish reasons,
tool-call-present, trailing-whitespace/empty/bare-colon text edges, and
direct trailingActionClause unit coverage. -race agent suite green.
The non-colon fallback only matched bare imperative prefixes ("fix ",
"run "), so non-colon gerund lead-ins ("Fixing the permission check",
"Running the tests") still slipped through — the same inflection class
the colon rule was added for. Add the -ing forms of the English action
prefixes by exact enumeration (fixing, running, updating, writing,
editing, checking, inspecting, verifying, executing, continuing,
starting, retrying, rerunning, re-running); no stemming, so no new
false-positive surface beyond the intended inflected shapes. Colon rule
and outer gates unchanged.
Tests: non-colon gerund fallback cases (fixing/running/updating/writing/
retrying) plus a non-action gerund negative ("singing"). -race green.
179c8ab to
2e1648f
Compare
Summary
The model frequently ends a turn by writing a status + next-steps ("…then commit", "…inspect + resolve", "…amend tests into commit") followed by
end_turnwith zero tool calls.This is model behavior, not a dropped turn — full-size completions with no retry signature. A static system instruction plus a deterministic turn-loop guard stop it repo-wide.
Changes
buildImmutableSystemBlocksWithTools(internal/agent/system_prompt.go), appended unconditionally alongside the mode contract / tool policy / delegation blocks:internal/agent/premature_end_turn.go: the turn-loop guard that nudges an announce-then-stop shape (Agent mode,end_turn, no tool calls) now recovers on a trailing colon alone. It previously required colon and an imperative verb-prefix on the lowercased trailing clause, which gerund lead-ins such as"Fixing:"/"Running tests:"slipped past. The verb-prefix scan stays as the non-colon fallback and now also matches inflected-inggerund forms (fixing,running,updating,writing), covering the one known"."-terminated shape plus non-colon gerund lead-ins. Outer gates unchanged: Agent mode only, tools available,SuppressToolsoff,end_turn, no tool calls,maxPrematureEndTurnNudges = 2(any false positive costs one bounded extra model call, not a loop).turn_loop.go→buildImmutableSystemBlocksWithTools), so it reaches ACP (cmd/whale-acp), the terminal CLI (internal/appruntime), and subagents (internal/tasks) — no ACP-only wiring, which would have missed the terminal and subagent paths.Why in
internal/agent, notinternal/acpA deep-codemap trace showed the ACP-only placement (
WithExtraSystemBlocksin whale-acp main) would fix only the Zed/ACP path. The terminalwhaleCLI and subagents build their own agents; all three route through the same turn loop, so the static block there is the single correct chokepoint.Validation
go build ./...,go vet, gofmt clean.shouldRecoverPrematureEndTurn: gerund colon lead-ins (Fixing:,Running tests:),then commit:/Inspect + resolve keeping both:, the"."-terminated fallback, mode/tool gates (Plan/Ask,SuppressTools, no tools, non-end_turnfinishes, turns with tool calls), accepted colon false positives (fixture analysis, headings, user-choice prompt), plus directtrailingActionClauseunit coverage.go test -race ./internal/agent -run PrematureEndgreen; full-raceagent suite green.Out of scope
The reasoning-replay half of the source feature (upstream PR #353, "prevent stale response history replay") is already in
main— no code change needed here.User-visible impact
whale-acpsessions,whaleterminals, and subagents alike.Breaking changes
None. Adds one system-prompt block and widens an existing recovery predicate.
Notes for reviewers
system_prompt.gois a leaf (no importers); the block is a plain static append — zero structural change, no shared mutable state, no race surface.whale0.1.65 binaries don't have the block; it reaches terminal users only once a fork-builtwhaleCLI is installed.Developed with carefully directed, manually reviewed AI assistance.