Skip to content

Commit c6be54b

Browse files
committed
Stop treating a wrap-up envelope as a finished critique without reads
Review/critique leaves now require at least one read or search in readCounts before evaluateSubAgentStop can return complete, even when the four-heading report is present.
1 parent ef4a7a8 commit c6be54b

4 files changed

Lines changed: 70 additions & 1 deletion

File tree

‎src/subagent/index.test.ts‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,48 @@ describe("sub-agent stop helpers", () => {
277277
).toBe("complete");
278278
});
279279

280+
test("evaluateSubAgentStop does not complete a review/critique with empty readCounts even with a full envelope", () => {
281+
const thrashState = {
282+
totalToolCalls: 1,
283+
readCounts: new Map(),
284+
editedPaths: new Set<string>(),
285+
};
286+
expect(
287+
evaluateSubAgentStop({
288+
hasToolCalls: false,
289+
everHadToolCalls: true,
290+
turnsCompleted: 2,
291+
maxTurns: 10,
292+
consecutiveIdentical: 0,
293+
repeatLimit: 2,
294+
lastAssistantText: FULL_REPORT_ENVELOPE,
295+
thrashState,
296+
requireEvidence: true,
297+
}),
298+
).not.toBe("complete");
299+
});
300+
301+
test("evaluateSubAgentStop completes a review when readCounts has file evidence", () => {
302+
const thrashState = {
303+
totalToolCalls: 1,
304+
readCounts: new Map([["src/gate.ts", 1]]),
305+
editedPaths: new Set<string>(),
306+
};
307+
expect(
308+
evaluateSubAgentStop({
309+
hasToolCalls: false,
310+
everHadToolCalls: true,
311+
turnsCompleted: 2,
312+
maxTurns: 10,
313+
consecutiveIdentical: 0,
314+
repeatLimit: 2,
315+
lastAssistantText: FULL_REPORT_ENVELOPE,
316+
thrashState,
317+
requireEvidence: true,
318+
}),
319+
).toBe("complete");
320+
});
321+
280322
test("evaluateSubAgentStop returns never-acted when the run never used tools", () => {
281323
expect(
282324
evaluateSubAgentStop({

‎src/subagent/nudge-director.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ export class SubAgentDirector extends DefaultDirector {
9393
private readonly repeatLimit: number;
9494
/** When true (intent=implement), tool-less finish without edits salvages as never-edited. */
9595
private readonly requireEdit: boolean;
96+
/** When true (intent=review / critique), empty readCounts is not a successful complete. */
97+
private readonly requireEvidence: boolean;
9698
private turnsCompleted = 0;
9799
private everHadToolCalls = false;
98100
private streak: ToolCallStreak = {
@@ -148,6 +150,7 @@ export class SubAgentDirector extends DefaultDirector {
148150
stallTimeoutMs?: number,
149151
now: () => number = Date.now,
150152
requireEdit: boolean = false,
153+
requireEvidence: boolean = false,
151154
) {
152155
super(systemPrompt, toolDefinitions, {});
153156
this.compaction = createCompactionGovernor(requestContinuation, systemPrompt, toolDefinitions);
@@ -157,6 +160,7 @@ export class SubAgentDirector extends DefaultDirector {
157160
this.now = now;
158161
this.lastActivityAt = now();
159162
this.requireEdit = requireEdit;
163+
this.requireEvidence = requireEvidence;
160164
}
161165

162166
override async decide(
@@ -217,6 +221,7 @@ export class SubAgentDirector extends DefaultDirector {
217221
repeatLimit: this.repeatLimit,
218222
thrashState: this.thrashState,
219223
requireEdit: this.requireEdit,
224+
requireEvidence: this.requireEvidence,
220225
lastAssistantText: this.lastAssistantText,
221226
incompleteReportNudgeFired: this.incompleteReportNudgeFired,
222227
});

‎src/subagent/run.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,9 @@ export async function runSubAgent(params: RunSubAgentParams): Promise<string> {
417417
modelFamilyPolicy.subAgentStallTimeoutMs,
418418
Date.now,
419419
params.intent === "implement",
420+
params.intent === "review" ||
421+
(typeof params.systemPromptRole === "string" &&
422+
params.systemPromptRole.includes("CritiqueDirector")),
420423
),
421424
});
422425

‎src/subagent/stop-policy.ts‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,9 @@ export type SubAgentStopReason =
275275
* (Summary, Findings, Blockers, Paths). Omitting `lastAssistantText`
276276
* still completes (back-compat). Missing envelope nudges
277277
* once (`incomplete-report`) then salvages (`incomplete-report-stop`).
278+
* When `requireEvidence` is set (review/critique), an empty `readCounts`
279+
* is not complete even with all four headings — same incomplete-report
280+
* nudge then salvage, so a wrap-up envelope cannot fake a real review.
278281
*/
279282
export function evaluateSubAgentStop(input: {
280283
hasToolCalls: boolean;
@@ -293,6 +296,13 @@ export function evaluateSubAgentStop(input: {
293296
* does not treat a pure-explore "plan" as shipped work.
294297
*/
295298
requireEdit?: boolean;
299+
/**
300+
* When true (intent=review / critique leaf), a tool-using run that never
301+
* read or searched a file is not a successful complete — even a four-heading
302+
* envelope is incomplete-report so the parent does not treat a wrap-up
303+
* narration as a finished review.
304+
*/
305+
requireEvidence?: boolean;
296306
/**
297307
* Final assistant text of this turn. When omitted, a tool-less turn after
298308
* tools still completes (back-compat for existing unit tests). When provided,
@@ -307,7 +317,8 @@ export function evaluateSubAgentStop(input: {
307317
// read/searched (no edit_file/write_file/delete_file) is never-edited —
308318
// both hard-block identical re-dispatch. After those, a tool-less turn
309319
// following tools is complete only with a report envelope (or when
310-
// lastAssistantText is omitted).
320+
// lastAssistantText is omitted). Review/critique additionally requires
321+
// at least one read/search in thrashState.readCounts.
311322
if (!input.hasToolCalls) {
312323
if (!input.everHadToolCalls) return "never-acted";
313324
if (
@@ -324,6 +335,14 @@ export function evaluateSubAgentStop(input: {
324335
? "incomplete-report-stop"
325336
: "incomplete-report";
326337
}
338+
if (
339+
input.requireEvidence === true &&
340+
(input.thrashState === undefined || input.thrashState.readCounts.size === 0)
341+
) {
342+
return input.incompleteReportNudgeFired === true
343+
? "incomplete-report-stop"
344+
: "incomplete-report";
345+
}
327346
return "complete";
328347
}
329348
// No-progress is more specific than thrash or the turn budget when both could apply.

0 commit comments

Comments
 (0)