Skip to content

Commit bb3be5a

Browse files
Remove back-compat branch in evaluateSubAgentStop's stop decision (#616)
lastAssistantText is now required so every call path gets the incomplete-report nudge instead of letting omitted text complete a tool-less turn unconditionally.
1 parent 8741be3 commit bb3be5a

3 files changed

Lines changed: 30 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ matching `## [X.Y.Z]` section (plus install instructions). Do not maintain
1111
parallel copies under `docs/` or `scripts/notes/`. At cut time: rename
1212
`## [Unreleased]` to `## [X.Y.Z] - YYYY-MM-DD`, then run the release script.
1313

14+
## [Unreleased]
15+
16+
### Agent
17+
18+
- `evaluateSubAgentStop` now always requires the final assistant text; the
19+
omitted-text branch that unconditionally completed a tool-less turn is
20+
removed, so every call path gets the `incomplete-report` nudge and salvage
21+
when a tool-using run ends in envelope-less narration.
22+
1423
## [0.2.109] - 2026-08-24
1524

1625
### Agent

src/subagent/index.test.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,14 +148,15 @@ describe("sub-agent stop helpers", () => {
148148
expect(subAgentTurnLimitExceeded(1_000_000, Infinity)).toBe(false);
149149
});
150150

151-
test("evaluateSubAgentStop returns complete when tools were used and the final turn has none", () => {
151+
test("evaluateSubAgentStop returns incomplete-report when the final turn has no tool calls and no envelope", () => {
152152
expect(
153153
evaluateSubAgentStop({
154154
hasToolCalls: false,
155155
turnsCompleted: 2,
156156
maxTurns: 10,
157+
lastAssistantText: "",
157158
}),
158-
).toBe("complete");
159+
).toBe("incomplete-report");
159160
});
160161

161162
const SUMMARY_ONLY_NARRATION = [
@@ -293,6 +294,7 @@ describe("sub-agent stop helpers", () => {
293294
expect(
294295
evaluateSubAgentStop({
295296
hasToolCalls: true,
297+
lastAssistantText: "",
296298
turnsCompleted: 40,
297299
maxTurns: 60,
298300
thrashState: thrash,
@@ -304,6 +306,7 @@ describe("sub-agent stop helpers", () => {
304306
expect(
305307
evaluateSubAgentStop({
306308
hasToolCalls: true,
309+
lastAssistantText: "",
307310
turnsCompleted: 10,
308311
maxTurns: 10,
309312
}),
@@ -314,6 +317,7 @@ describe("sub-agent stop helpers", () => {
314317
expect(
315318
evaluateSubAgentStop({
316319
hasToolCalls: true,
320+
lastAssistantText: "",
317321
turnsCompleted: 5,
318322
maxTurns: 10,
319323
}),
@@ -333,6 +337,7 @@ describe("sub-agent stop helpers", () => {
333337
expect(
334338
evaluateSubAgentStop({
335339
hasToolCalls: true,
340+
lastAssistantText: "",
336341
turnsCompleted: 10,
337342
maxTurns: 10,
338343
thrashState: thrash,
@@ -344,6 +349,7 @@ describe("sub-agent stop helpers", () => {
344349
expect(
345350
evaluateSubAgentStop({
346351
hasToolCalls: true,
352+
lastAssistantText: "",
347353
turnsCompleted: 8,
348354
maxTurns: 10,
349355
thrashState: EMPTY_THRASH_STATE,
@@ -353,6 +359,7 @@ describe("sub-agent stop helpers", () => {
353359
expect(
354360
evaluateSubAgentStop({
355361
hasToolCalls: true,
362+
lastAssistantText: "",
356363
turnsCompleted: 9,
357364
maxTurns: 10,
358365
thrashState: EMPTY_THRASH_STATE,
@@ -361,6 +368,7 @@ describe("sub-agent stop helpers", () => {
361368
expect(
362369
evaluateSubAgentStop({
363370
hasToolCalls: true,
371+
lastAssistantText: "",
364372
turnsCompleted: 10,
365373
maxTurns: 10,
366374
thrashState: EMPTY_THRASH_STATE,
@@ -378,6 +386,7 @@ describe("sub-agent stop helpers", () => {
378386
expect(
379387
evaluateSubAgentStop({
380388
hasToolCalls: true,
389+
lastAssistantText: "",
381390
turnsCompleted: 5,
382391
maxTurns: 20,
383392
thrashState: thrash,
@@ -649,6 +658,7 @@ describe("thrash edge cases", () => {
649658
const stop = (turnsCompleted: number, maxTurns: number, thrashState = EMPTY_THRASH_STATE) =>
650659
evaluateSubAgentStop({
651660
hasToolCalls: true,
661+
lastAssistantText: "",
652662
turnsCompleted,
653663
maxTurns,
654664
thrashState,

src/subagent/stop-policy.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,9 @@ export type SubAgentStopReason =
8383
* caller to inject a wrap-up / redirect nudge and keep running; turn-budget
8484
* remains reachable afterward. A tool-less turn (including one that never
8585
* called a tool at all) completes only when the assistant text has a
86-
* four-heading envelope (Summary, Findings, Blockers, Paths). Omitting
87-
* `lastAssistantText` still completes (back-compat). Missing envelope nudges
88-
* once (`incomplete-report`) then salvages (`incomplete-report-stop`).
86+
* four-heading envelope (Summary, Findings, Blockers, Paths). Missing
87+
* envelope nudges once (`incomplete-report`) then salvages
88+
* (`incomplete-report-stop`).
8989
* When `requireEvidence` is set (CritiqueDirector), an empty `readCounts`
9090
* is not complete even with all four headings — same incomplete-report
9191
* nudge then salvage, so a wrap-up envelope cannot fake a real review.
@@ -105,20 +105,17 @@ export function evaluateSubAgentStop(input: {
105105
*/
106106
requireEvidence?: boolean;
107107
/**
108-
* Final assistant text of this turn. When omitted, a tool-less turn after
109-
* tools still completes (back-compat for existing unit tests). When provided,
110-
* a missing four-heading envelope (Summary/Findings/Blockers/Paths) nudges
111-
* once then salvages.
108+
* Final assistant text of this turn. A missing four-heading envelope
109+
* (Summary/Findings/Blockers/Paths) nudges once then salvages.
112110
*/
113-
lastAssistantText?: string;
111+
lastAssistantText: string;
114112
/** True after the one-shot incomplete-report wrap-up nudge has been injected. */
115113
incompleteReportNudgeFired?: boolean;
116114
}): SubAgentStopReason | null {
117-
// A tool-less turn is complete only with a report envelope (or when
118-
// lastAssistantText is omitted). CritiqueDirector additionally requires at
119-
// least one read/search in thrashState.readCounts.
115+
// A tool-less turn is complete only with a report envelope. CritiqueDirector
116+
// additionally requires at least one read/search in thrashState.readCounts.
120117
if (!input.hasToolCalls) {
121-
if (input.lastAssistantText !== undefined && !hasReportEnvelope(input.lastAssistantText)) {
118+
if (!hasReportEnvelope(input.lastAssistantText)) {
122119
return input.incompleteReportNudgeFired === true
123120
? "incomplete-report-stop"
124121
: "incomplete-report";

0 commit comments

Comments
 (0)