From 98ac44d2ac4088367d1c92146fbd5fb030dd12f1 Mon Sep 17 00:00:00 2001 From: Rene Leonhardt <65483435+reneleonhardt@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:52:16 +0200 Subject: [PATCH 1/4] feat(agent): announce-then-stop system block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 #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. --- internal/agent/system_prompt.go | 1 + internal/agent/system_prompt_test.go | 43 ++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/internal/agent/system_prompt.go b/internal/agent/system_prompt.go index 582029d8..888ffd19 100644 --- a/internal/agent/system_prompt.go +++ b/internal/agent/system_prompt.go @@ -33,6 +33,7 @@ func (a *Agent) buildImmutableSystemBlocksWithTools(_ *core.ToolRegistry, opts . 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.") return systemBlocks } diff --git a/internal/agent/system_prompt_test.go b/internal/agent/system_prompt_test.go index 71ac03f7..ba8525ee 100644 --- a/internal/agent/system_prompt_test.go +++ b/internal/agent/system_prompt_test.go @@ -513,3 +513,46 @@ func TestImmutableSystemPromptToolPolicyDoesNotDependOnToolRegistry(t *testing.T t.Fatalf("immutable system prompt changed with tool registry\nwithout tools:\n%s\n\nwith tools:\n%s", a, b) } } + +// TestImmutableSystemBlocksIncludeAnnounceThenStop verifies the announce- +// then-stop instruction is a static immutable block, so it reaches every +// entrypoint (ACP, terminal CLI, subagents) that shares the turn loop. +func TestImmutableSystemBlocksIncludeAnnounceThenStop(t *testing.T) { + a := NewAgentWithRegistry(nil, nil, core.NewToolRegistry(nil), WithProjectMemory(false, 0, nil, "/repo")) + joined := strings.Join(a.buildImmutableSystemBlocks(), "\n\n") + + for _, want := range []string{ + "Execute, don't narrate", + "never end a turn by announcing further work", + "do it now, in this turn, with tool calls", + `"stop"`, + "planned-but-unexecuted next steps are a failure", + } { + if !strings.Contains(joined, want) { + t.Fatalf("announce-then-stop block missing %q:\n%s", want, joined) + } + } + if got := strings.Count(joined, "Execute, don't narrate"); got != 1 { + t.Fatalf("announce-then-stop block appears %d times, want exactly once:\n%s", got, joined) + } + // The instruction is a contract, not runtime context: it must never depend + // on the tool registry or session mode, and it must not be duplicated into + // the runtime blocks (which are re-rendered per turn). + runtime := strings.Join(a.buildRuntimeSystemBlocks(), "\n\n") + if strings.Contains(runtime, "Execute, don't narrate") { + t.Fatalf("announce-then-stop block leaked into runtime blocks:\n%s", runtime) + } + modes := []session.Mode{session.ModeAgent, session.ModeAsk, session.ModePlan} + for _, m := range modes { + withMode := NewAgentWithRegistry(nil, nil, core.NewToolRegistry(nil), WithSessionMode(m)) + if got := strings.Join(withMode.buildImmutableSystemBlocks(), "\n\n"); !strings.Contains(got, "Execute, don't narrate") { + t.Fatalf("mode %v missing announce-then-stop block", m) + } + } + // Immutable blocks are shared by every entrypoint via the turn loop; the + // tool registry must not change the block (it is appended unconditionally). + withTools := NewAgentWithRegistry(nil, nil, core.NewToolRegistry([]core.Tool{&recordingWorkflowTool{}})) + if got := strings.Join(withTools.buildImmutableSystemBlocks(), "\n\n"); !strings.Contains(got, "Execute, don't narrate") { + t.Fatalf("with-tools agent missing announce-then-stop block") + } +} From f9591081079b7d4cd456b915012218e241e56ecb Mon Sep 17 00:00:00 2001 From: Rene Leonhardt Date: Fri, 7 Aug 2026 09:39:09 +0200 Subject: [PATCH 2/4] fix(agent): Make announce-then-stop conditional on tool availability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- internal/agent/system_prompt.go | 2 +- internal/agent/system_prompt_test.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/agent/system_prompt.go b/internal/agent/system_prompt.go index 888ffd19..ec2354ed 100644 --- a/internal/agent/system_prompt.go +++ b/internal/agent/system_prompt.go @@ -33,7 +33,7 @@ func (a *Agent) buildImmutableSystemBlocksWithTools(_ *core.ToolRegistry, opts . 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.") + systemBlocks = append(systemBlocks, "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.") return systemBlocks } diff --git a/internal/agent/system_prompt_test.go b/internal/agent/system_prompt_test.go index ba8525ee..4898a290 100644 --- a/internal/agent/system_prompt_test.go +++ b/internal/agent/system_prompt_test.go @@ -526,7 +526,6 @@ func TestImmutableSystemBlocksIncludeAnnounceThenStop(t *testing.T) { "never end a turn by announcing further work", "do it now, in this turn, with tool calls", `"stop"`, - "planned-but-unexecuted next steps are a failure", } { if !strings.Contains(joined, want) { t.Fatalf("announce-then-stop block missing %q:\n%s", want, joined) From c3a4fc2e5885bb856c113160c77119ff2bbdfa0a Mon Sep 17 00:00:00 2001 From: Rene Leonhardt Date: Fri, 7 Aug 2026 18:36:32 +0200 Subject: [PATCH 3/4] fix(agent): recover colon-terminated announce-then-stop regardless of inflection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- internal/agent/premature_end_turn.go | 18 +++--- internal/agent/premature_end_turn_test.go | 68 ++++++++++++++++++++++- 2 files changed, 75 insertions(+), 11 deletions(-) diff --git a/internal/agent/premature_end_turn.go b/internal/agent/premature_end_turn.go index 023e5918..b4a3fa95 100644 --- a/internal/agent/premature_end_turn.go +++ b/internal/agent/premature_end_turn.go @@ -74,11 +74,13 @@ var prematureActionPrefixes = []string{ "跑", } -// shouldRecoverPrematureEndTurn recognizes the narrow failure shape observed -// in DeepSeek sessions: an Agent-mode reply stops at an action lead-in ending -// in a colon, yet contains no structured tool call. Requiring both the dangling -// colon and an immediate-action prefix avoids retrying ordinary final answers, -// questions, headings, Plan replies, and tool-suppressed internal requests. +// shouldRecoverPrematureEndTurn catches an Agent-mode reply that stops at an +// action lead-in without issuing a tool call. A trailing colon alone triggers +// recovery: a colon-terminated final answer invites continuation, and the +// nudge's escape hatch plus the 2-nudge cap bound a false positive to one extra +// model call. Non-colon text falls back to the action-prefix scan, covering the +// one known "."-terminated instance. Outer gates stay: Agent mode, tools +// available, SuppressTools off, end_turn, no tool calls. func shouldRecoverPrematureEndTurn(msg core.Message, mode session.Mode, opts RunOptions, toolsAvailable bool) bool { if mode != session.ModeAgent || opts.SuppressTools || !toolsAvailable { return false @@ -87,11 +89,11 @@ func shouldRecoverPrematureEndTurn(msg core.Message, mode session.Mode, opts Run return false } text := strings.TrimSpace(msg.Text) - if !strings.HasSuffix(text, ":") && !strings.HasSuffix(text, ":") { - return false + if strings.HasSuffix(text, ":") || strings.HasSuffix(text, ":") { + return true } - clause := trailingActionClause(strings.TrimSpace(strings.TrimSuffix(strings.TrimSuffix(text, ":"), ":"))) + clause := trailingActionClause(text) clause = strings.ToLower(strings.TrimSpace(clause)) for _, prefix := range prematureActionPrefixes { if strings.HasPrefix(clause, prefix) { diff --git a/internal/agent/premature_end_turn_test.go b/internal/agent/premature_end_turn_test.go index bfb1ab48..abdd04a9 100644 --- a/internal/agent/premature_end_turn_test.go +++ b/internal/agent/premature_end_turn_test.go @@ -122,11 +122,19 @@ func TestShouldRecoverPrematureEndTurn(t *testing.T) { tests := []struct { name string text string + finishReason core.FinishReason + toolCalls []core.ToolCall mode session.Mode opts RunOptions toolsAvailable bool want bool }{ + // Colon-terminated lead-ins: the colon is the primary trigger since the + // guard was widened. 3 of 4 known announce-then-stop instances end in + // ":", including the gerund forms the old verb-prefix scan missed + // ("Fixing:", "Running tests:"). + {name: "gerund lead-in fixing colon", text: "a deliberate asymmetry the comment now contradicts. Fixing:", mode: session.ModeAgent, toolsAvailable: true, want: true}, + {name: "running tests lead-in", text: "Running tests:", mode: session.ModeAgent, toolsAvailable: true, want: true}, {name: "observed chinese verify lead-in", text: "现在验证 workflow 语法:", mode: session.ModeAgent, toolsAvailable: true, want: true}, {name: "observed chinese retry lead-in", text: "API 没改到权限。换端点重试:", mode: session.ModeAgent, toolsAvailable: true, want: true}, {name: "observed chinese patch lead-in", text: "第一个 edit 没改到 description,我写重复了,补上:", mode: session.ModeAgent, toolsAvailable: true, want: true}, @@ -138,22 +146,76 @@ func TestShouldRecoverPrematureEndTurn(t *testing.T) { {name: "chinese next step lead-in", text: "下一步检查 CI:", mode: session.ModeAgent, toolsAvailable: true, want: true}, {name: "english action lead-in", text: "The config is present. Now verify the workflow:", mode: session.ModeAgent, toolsAvailable: true, want: true}, {name: "english final action lead-in", text: "The checks passed. Finally merge the pull request:", mode: session.ModeAgent, toolsAvailable: true, want: true}, + {name: "then commit colon", text: "The diff is staged, then commit:", mode: session.ModeAgent, toolsAvailable: true, want: true}, + {name: "inspect plus resolve colon", text: "Inspect + resolve keeping both:", mode: session.ModeAgent, toolsAvailable: true, want: true}, + // Non-colon fallback: the one known "."-terminated announce-then-stop + // instance is caught by the trailing action-clause prefix scan. + {name: "dot-terminated action clause fallback", text: "Amend tests into commit, then append maintainer reply draft.", mode: session.ModeAgent, toolsAvailable: true, want: true}, + // Accepted colon false positives: bounded by the 2-nudge cap, harmless + // per the nudge's "If no action remains, provide the complete final + // answer" escape hatch. + {name: "fixture analysis colon accepted", text: "The fixture analysis:", mode: session.ModeAgent, toolsAvailable: true, want: true}, + {name: "ordinary heading accepted", text: "Details:", mode: session.ModeAgent, toolsAvailable: true, want: true}, + {name: "ordinary final heading accepted", text: "最后:", mode: session.ModeAgent, toolsAvailable: true, want: true}, + {name: "user choice prompt accepted", text: "请选择:", mode: session.ModeAgent, toolsAvailable: true, want: true}, + // Negatives: no colon and no action-prefix clause stay final answers. {name: "complete answer", text: "The workflow is valid.", mode: session.ModeAgent, toolsAvailable: true}, - {name: "ordinary heading", text: "Details:", mode: session.ModeAgent, toolsAvailable: true}, - {name: "ordinary final heading", text: "最后:", mode: session.ModeAgent, toolsAvailable: true}, - {name: "user choice prompt", text: "请选择:", mode: session.ModeAgent, toolsAvailable: true}, + {name: "complete answer the fix is simple", text: "The fix is simple.", mode: session.ModeAgent, toolsAvailable: true}, + {name: "final answer with future-tense clause", text: "The user will decide the direction.", mode: session.ModeAgent, toolsAvailable: true}, + {name: "colon heading with content", text: "Details: the workflow is valid.", mode: session.ModeAgent, toolsAvailable: true}, {name: "plan reply", text: "接下来执行:", mode: session.ModePlan, toolsAvailable: true}, {name: "ask reply", text: "现在检查:", mode: session.ModeAsk, toolsAvailable: true}, {name: "tools suppressed", text: "Now verify:", mode: session.ModeAgent, opts: RunOptions{SuppressTools: true}, toolsAvailable: true}, {name: "no tools available", text: "Now verify:", mode: session.ModeAgent, toolsAvailable: false}, + // Remaining outer gates: non-end-turn finishes and turns that already + // carried a structured tool call are never recovered. + {name: "tool use finish reason", text: "Now verify:", finishReason: core.FinishReasonToolUse, mode: session.ModeAgent, toolsAvailable: true}, + {name: "cancelled finish reason", text: "Now verify:", finishReason: core.FinishReasonCanceled, mode: session.ModeAgent, toolsAvailable: true}, + {name: "has tool calls", text: "Now verify:", toolCalls: []core.ToolCall{{ID: "tc-1", Name: "echo", Input: "{}"}}, mode: session.ModeAgent, toolsAvailable: true}, + // Text-shape edges around the colon check. + {name: "trailing whitespace after colon", text: "Fixing: \n", mode: session.ModeAgent, toolsAvailable: true, want: true}, + {name: "bare colon", text: ":", mode: session.ModeAgent, toolsAvailable: true, want: true}, + {name: "bare fullwidth colon", text: ":", mode: session.ModeAgent, toolsAvailable: true, want: true}, + {name: "empty text", text: " ", mode: session.ModeAgent, toolsAvailable: true}, + {name: "no colon no prefix bullet", text: "The output is ready.", mode: session.ModeAgent, toolsAvailable: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { msg := base msg.Text = tt.text + if tt.finishReason != "" { + msg.FinishReason = tt.finishReason + } + msg.ToolCalls = tt.toolCalls if got := shouldRecoverPrematureEndTurn(msg, tt.mode, tt.opts, tt.toolsAvailable); got != tt.want { t.Fatalf("shouldRecoverPrematureEndTurn(%q) = %v, want %v", tt.text, got, tt.want) } }) } } + +func TestTrailingActionClause(t *testing.T) { + tests := []struct { + name string + text string + want string + }{ + {name: "sentence boundary", text: "The checks passed. Then merge it", want: "Then merge it"}, + {name: "newline boundary", text: "Step one done\nNext verify", want: "Next verify"}, + {name: "comma clause", text: "commit, then append reply", want: "then append reply"}, + {name: "chinese sentence boundary", text: "第一步完成。然后验证", want: "然后验证"}, + {name: "chinese comma boundary", text: "检查完毕,补上说明", want: "补上说明"}, + {name: "no boundary keeps whole", text: "fix the permission check", want: "fix the permission check"}, + {name: "strips list markers", text: "Amend tests:\n- then run them", want: "then run them"}, + {name: "strips numbered marker", text: "Steps:\n2) then verify", want: "then verify"}, + {name: "strips heading marker", text: "Steps:\n## then verify", want: "then verify"}, + {name: "empty input", text: "", want: ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := trailingActionClause(tt.text); got != tt.want { + t.Fatalf("trailingActionClause(%q) = %q, want %q", tt.text, got, tt.want) + } + }) + } +} From 2e1648f089ab336d714485a217ce1b03f6f63735 Mon Sep 17 00:00:00 2001 From: Rene Leonhardt Date: Fri, 7 Aug 2026 19:07:42 +0200 Subject: [PATCH 4/4] fix(agent): match inflected -ing gerunds in premature-end fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- internal/agent/premature_end_turn.go | 21 ++++++++++++++++++--- internal/agent/premature_end_turn_test.go | 9 +++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/internal/agent/premature_end_turn.go b/internal/agent/premature_end_turn.go index b4a3fa95..543984d9 100644 --- a/internal/agent/premature_end_turn.go +++ b/internal/agent/premature_end_turn.go @@ -28,19 +28,33 @@ var prematureActionPrefixes = []string{ "then ", "finally ", "continue ", + "continuing ", "start ", + "starting ", "retry ", + "retrying ", "rerun ", + "rerunning ", "re-run ", + "re-running ", "check ", + "checking ", "inspect ", + "inspecting ", "verify ", + "verifying ", "run ", + "running ", "execute ", + "executing ", "update ", + "updating ", "edit ", + "editing ", "write ", + "writing ", "fix ", + "fixing ", "我来", "我先", "我现在", @@ -78,9 +92,10 @@ var prematureActionPrefixes = []string{ // action lead-in without issuing a tool call. A trailing colon alone triggers // recovery: a colon-terminated final answer invites continuation, and the // nudge's escape hatch plus the 2-nudge cap bound a false positive to one extra -// model call. Non-colon text falls back to the action-prefix scan, covering the -// one known "."-terminated instance. Outer gates stay: Agent mode, tools -// available, SuppressTools off, end_turn, no tool calls. +// model call. Non-colon text falls back to the action-prefix scan, which also +// matches inflected -ing forms (fixing, running, updating), so the known +// "."-terminated instance and gerund lead-ins are still caught. Outer gates +// stay: Agent mode, tools available, SuppressTools off, end_turn, no tool calls. func shouldRecoverPrematureEndTurn(msg core.Message, mode session.Mode, opts RunOptions, toolsAvailable bool) bool { if mode != session.ModeAgent || opts.SuppressTools || !toolsAvailable { return false diff --git a/internal/agent/premature_end_turn_test.go b/internal/agent/premature_end_turn_test.go index abdd04a9..4172d0ea 100644 --- a/internal/agent/premature_end_turn_test.go +++ b/internal/agent/premature_end_turn_test.go @@ -151,6 +151,14 @@ func TestShouldRecoverPrematureEndTurn(t *testing.T) { // Non-colon fallback: the one known "."-terminated announce-then-stop // instance is caught by the trailing action-clause prefix scan. {name: "dot-terminated action clause fallback", text: "Amend tests into commit, then append maintainer reply draft.", mode: session.ModeAgent, toolsAvailable: true, want: true}, + // Non-colon inflected lead-ins: the prefix scan also matches -ing + // gerund forms, so "fixing"/"running"/"updating"/"writing" without a + // trailing colon are recovered instead of slipping through. + {name: "non-colon gerund fixing", text: "The typo is clear. Fixing the permission check", mode: session.ModeAgent, toolsAvailable: true, want: true}, + {name: "non-colon gerund running", text: "The suite is ready. Running the tests", mode: session.ModeAgent, toolsAvailable: true, want: true}, + {name: "non-colon gerund updating", text: "Rebuild needed. Updating the dependency", mode: session.ModeAgent, toolsAvailable: true, want: true}, + {name: "non-colon gerund writing", text: "Understood. Writing the patch", mode: session.ModeAgent, toolsAvailable: true, want: true}, + {name: "non-colon gerund retrying", text: "Rate limited. Retrying the request", mode: session.ModeAgent, toolsAvailable: true, want: true}, // Accepted colon false positives: bounded by the 2-nudge cap, harmless // per the nudge's "If no action remains, provide the complete final // answer" escape hatch. @@ -178,6 +186,7 @@ func TestShouldRecoverPrematureEndTurn(t *testing.T) { {name: "bare fullwidth colon", text: ":", mode: session.ModeAgent, toolsAvailable: true, want: true}, {name: "empty text", text: " ", mode: session.ModeAgent, toolsAvailable: true}, {name: "no colon no prefix bullet", text: "The output is ready.", mode: session.ModeAgent, toolsAvailable: true}, + {name: "non-action gerund not matched", text: "The response was helpful. Singing the final note is next.", mode: session.ModeAgent, toolsAvailable: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {