From a3ddcccee9f920807f59acf71414ae8af670adbe Mon Sep 17 00:00:00 2001 From: unohee Date: Sun, 20 Sep 2026 10:09:21 +0900 Subject: [PATCH] fix: preserve reviewer verdict on salvage --- src/adapters/agenticLoop.test.ts | 22 ++++++++++++++++++++++ src/adapters/agenticLoop.ts | 5 ++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/adapters/agenticLoop.test.ts b/src/adapters/agenticLoop.test.ts index 69b62921..7a18b30a 100644 --- a/src/adapters/agenticLoop.test.ts +++ b/src/adapters/agenticLoop.test.ts @@ -465,6 +465,28 @@ describe('runAgenticLoop final-answer recovery (INT-2879)', () => { expect(logs).toContain('↻ Final answer was empty — retrying once (no tools)'); }); + it('requires an explicit review verdict from a tool-exhausted reviewer (AGT-4484)', async () => { + let salvagePrompt = ''; + const result = await runAgenticLoop({ + prompt: 'Review this change and return a verdict.', + cwd: process.cwd(), + model: 'test', + webTools: false, + maxTurns: 1, + callApi: async (messages, tools) => { + if (tools.length > 0) return toolCallResp('read', 'read_file', { path: 'package.json' }); + salvagePrompt = String(messages.at(-1)?.content ?? ''); + // This is the conclusion the historical reviewer omitted after using + // its full tool budget: a verdict, not a worker-style status report. + return finalResp('Decision: approve\nNo actionable issues found.'); + }, + }); + + expect(salvagePrompt).toContain('Decision: approve'); + expect(salvagePrompt).toContain('worker-status summary'); + expect(result.text).toContain('Decision: approve'); + }); + it('fails explicitly when the retry is also reasoning-only/empty', async () => { let calls = 0; let finalAnswerCalls = 0; diff --git a/src/adapters/agenticLoop.ts b/src/adapters/agenticLoop.ts index f3edfd63..c3cf127b 100644 --- a/src/adapters/agenticLoop.ts +++ b/src/adapters/agenticLoop.ts @@ -983,7 +983,10 @@ async function runAgenticLoopInner( const stepLimitPrompt = "You've reached this turn's step limit, so stop calling tools now. Using everything " + 'above, write a non-empty final answer now. Follow the output format requested in the ' + - 'original task exactly. Do not mention step/tool limits or "budget" to the user.'; + 'original task exactly. If the original task requests a review or verdict, begin with ' + + '`Decision: approve`, `Decision: revise`, or `Decision: reject` and give concrete ' + + 'reasoning; do not return a worker-status summary. Do not mention step/tool limits or ' + + '"budget" to the user.'; messages.push({ role: 'user', content: stepLimitPrompt }); // Salvage is where a cut-short run gets the answer that becomes its verdict, // and it used to leave no trace at all: the transcript ended on a tool