You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: warn before the tool-call budget runs out, and disclose when it did
The 20-iteration cap is what actually truncated the provisioning session
analysed in 8318f7f, and it explains the ending better than the earlier
read did: the model was mid-thought about running a real end-to-end test
when the budget ran out, and the forced synthesis turned that into a
confident sign-off with a tick beside a step that never executed.
Three things were wrong, none of them the cap value itself.
No warning before the cliff. maybeInjectSoftDeadlineWarning has told the
model about the wall-clock budget since it shipped; the tool-call budget
- the one that actually fires - told it nothing. A turn that plans five
steps and is guillotined after four has no way to prioritise, and the
step it loses is always the last one, which is verification. Adds the
iteration analogue, firing with 30% of the budget left (6 calls of 20):
enough to finish and verify, not enough to start exploring again.
The forced-synthesis nudge optimised for the wrong thing. "Do not
apologize without delivering content" plus "the most complete
deliverable you can" reads as an instruction to sound finished. It now
also requires saying the budget ran out, naming the steps that did not
run, and reporting only what tool results actually showed - being cut
off before a check is not the check passing.
The truncation notice existed only as metadata. iterationCapReached has
exactly one consumer, web/src/components/chat-screen.tsx. WeChat,
Discord, Feishu, LINE and API callers drop it silently, so a truncated
turn arrives on those channels looking like a finished answer with no
indication otherwise. Disclosure that lives in UI chrome does not exist
off that UI; it is now also in the message text for channels that cannot
render the badge, on both the streaming and non-streaming paths (emitted
as a trailing chunk too, since a streaming client renders what it
received, not what was persisted).
The default stays at 20. The session overran it partly on work that no
longer happens - the anti-detect-browser detour, the filesystem-wide
find, a re-read of an already-loaded skill - and provisioning now costs
about five native tool calls instead of a dozen shelled-out CLI
invocations. maxToolIterations remains per-agent configurable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
finalContent=fmt.Sprintf("I've reached the maximum number of tool iterations (%d) and couldn't synthesize a final response. The work above represents what I gathered before hitting the limit.", a.maxToolIterations)
// Streaming endpoint failed — persist+emit a fallback line
3241
3292
// with the badge so the user still gets the signal.
3242
-
fallback:=fmt.Sprintf("I've reached the maximum number of tool iterations (%d) and couldn't synthesize a final response. The work above represents what I gathered before hitting the limit.", a.maxToolIterations)
3293
+
fallback:=fmt.Sprintf("I've reached the maximum number of tool iterations (%d) and couldn't synthesize a final response. The work above represents what I gathered before hitting the limit.", a.maxToolIterations) +
content=fmt.Sprintf("I've reached the maximum number of tool iterations (%d) and couldn't synthesize a final response. The work above represents what I gathered before hitting the limit.", a.maxToolIterations)
3289
3341
}
3342
+
// Emit the truncation notice as a trailing chunk too, not just in
3343
+
// the persisted message: a streaming consumer renders what it
3344
+
// received, so a notice added only to the stored copy would never
"You've used all %d tool-call iterations available for this turn. Tools are now disabled for this final response — do not attempt to call any. Synthesize what you've already gathered into the most complete deliverable you can: if the user asked for a structured artifact (table, list, ICP summary, email drafts, etc.), produce it now from the existing tool results. For any fields you couldn't resolve, mark them as 'unknown' / 'not found' / 'partial' rather than dropping rows or skipping the structure — give the user something usable plus an honest note about what's missing. Do not apologize without delivering content.",
3412
+
"You've used all %d tool-call iterations available for this turn. Tools are now disabled for this final response — do not attempt to call any. "+
3413
+
"Synthesize what you've already gathered into the most complete deliverable you can: if the user asked for a structured artifact (table, list, ICP summary, email drafts, etc.), produce it now from the existing tool results. "+
3414
+
"For any fields you couldn't resolve, mark them as 'unknown' / 'not found' / 'partial' rather than dropping rows or skipping the structure — give the user something usable plus an honest note about what's missing. Do not apologize without delivering content.\n\n"+
3415
+
"Two things you MUST get right, because this turn ended early and the user cannot see that unless you say it:\n"+
3416
+
"- State up front that you ran out of tool budget and the work is incomplete. Name the steps you planned but did not run.\n"+
3417
+
"- Report ONLY what tool results actually showed. Do not mark a step done, tick it off, or call something verified unless a tool result in this conversation confirms it — being cut off before a check is not the same as the check passing. "+
3418
+
"If your last plan step was verification and it never ran, the honest report is 'configured but not verified', never a completion claim.",
3349
3419
maxIterations,
3350
3420
),
3351
3421
}
3352
3422
}
3353
3423
3424
+
// iterationCapNotice is the in-band truncation marker for channels that
3425
+
// cannot render the iterationCapReached badge.
3426
+
//
3427
+
// That badge is read by exactly one consumer — the web chat UI. Every
3428
+
// other surface (WeChat, Discord, Feishu, LINE, plain API consumers)
3429
+
// drops the metadata silently, so a turn that was guillotined mid-plan
3430
+
// arrives looking like a finished, confident answer. The disclosure that
3431
+
// the work is partial cannot live only in UI chrome; on those channels
3432
+
// it has to be in the text or it does not exist.
3433
+
//
3434
+
// Returns "" for channels that do render the badge, so the web UI
"\n\n---\n⚠️ This turn hit its %d tool-call limit and was cut short — the answer above is built from partial results, and any step described as done was not necessarily verified. Reply to continue.",
3442
+
maxIterations)
3443
+
}
3444
+
3354
3445
// iterationCapMetadata is the assistant-side metadata stamped on the
3355
3446
// forced final-delivery message so the UI can badge the bubble. Kept
3356
3447
// as a constructor so the key name stays canonical across the streaming
0 commit comments