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
[enhancement] A failed tool round is not resumable — the retry re-fetches what the platform already paid for, and only 120 chars of the result survive #442
Follow-up to #427 item 5: a retry re-runs the tool the platform already paid for
#427's streaming fix (d5c6d1a) and tool-result cap (efe285a) are shipped. Its item 5 — "make the failure resumable: the tool result is already in hand, so a retry should be able to re-run
only the generation rather than re-calling the tool" — was deliberately not attempted, and the
implementer asked for this ticket so #427 can close.
Filing it as asked. Streaming makes this rarer; it does not make it go away — any provider error in
round 2+ (5xx, an overloaded model, a stall, the 180s ceiling) lands in exactly the same place, after
the tool has run.
What it costs, from #427's own production evidence
The same issue, fetched twice, six minutes apart, because the retry had no way to say "you already
have this". Where a round's tools are writes rather than reads, the same retry re-executes side
effects — the cross-round dedup (agent-think.ts:791-800) is scoped to one turn and cannot see
the previous request.
What actually survives today — one correction to #427
#427 says "the user's message and the tool result both survive". The message survives. The tool
result survives only as a 120-character preview:
withPartialToolLog (:1055-1064) attaches that array to the thrown error; agent-do.ts:566-580
writes it as one system message, then an Error: … system message. Everything the next round would
have needed — the full content, the tool_useid, the arguments, the is_error flag, outcomeById — is function-local in runAgentThink and dies with the throw.
So the retry is not merely choosing to re-call the tool; it has no alternative, and the
120-char preview is in the transcript as prose, which is the shape #398 removed from the live path
for good reasons.
What "resumable" needs
Persist the round, structurally. On a provider failure with completed rounds, store what the
next round would have been fed: the assistant tool_use blocks (rawResult.contentBlocks) and
the tool_result blocks keyed by id (outcomeById, already capped by capToolResult). Attach it
to the failed turn — as a message with a non-rendered payload, or a sibling row keyed by the
turn's id. It must be capped and expiring; a stored round is prompt-sized, not log-sized.
A resume entry point.runAgentThink starts at the top of the loop by construction. Resuming
means re-entering with aiMessages already carrying the persisted round, the round counter
advanced, and the dedup map seeded — so the model continues from "here are your results"
instead of "here is the question".
Decide who triggers it. Cheapest: the same retry the user already performs, with the DO
noticing a resumable round on the previous turn. More explicit: a "Continue" action on the error
message. The first is invisible and does the right thing; the second is honest about what it is
doing. I would ship the first and label it in the transcript ("continued from the tool
results already fetched"), because a user who retries is asking for the answer, not for a
decision about caching.
Expire it. A resumable round is only valid while its results are still true. A short TTL
(minutes) and invalidation on any new user message are enough; a resumed github_read_issue from
yesterday is worse than a re-fetch.
Do not return a partial answer.d5c6d1a deliberately never returns a half-streamed message.
A resumed round produces a fresh, whole reply or fails again.
A generic tool-result cache keyed by (tool, args). Bigger and less safe: it would also
short-circuit deliberate re-reads (the mutation-count dedup at :791-800 exists precisely
because "a read is only worth repeating when something CHANGED"), and it says nothing about write
tools. Resuming ONE failed turn is the smaller, exactly-scoped claim.
Persist every round, always. Rejected: prompt-sized writes on the hot path for a case that is
now rare. Write only on failure, and only when at least one tool executed.
Acceptance criteria
A turn that executes a tool and then fails in generation stores that round's tool_use + tool_result blocks against the failed turn, capped and expiring.
Retrying that turn produces an answer without re-invoking the tool — asserted with a tool
double that counts calls.
With no stored round (or an expired one), behaviour is byte-identical to today, including the
partial-tool-log system message.
The transcript says the turn was continued from already-fetched results.
A resumed turn never emits a partial or duplicated reply.
Regression risk
Duplicate side effects are the thing to be careful about, in both directions: resuming must
not re-run a write tool, and it must not skip one whose result was never recorded. Persist the
round only after every tool in it has settled.
The stored blocks are model-visible input. They must go through the same capToolResult seam
(lib/tool-result-cap.ts) rather than a second cap that can drift, and they must never be rendered
to the user as an assistant message.
agent-do.ts message storage is per-instance DO state; a new payload shape on a message needs the
readers (/messages, the console, agent_trace) to ignore what they do not understand.
Files: workers/api/src/agent-think.ts:780-800,873-905,996-1000,1030-1064, workers/api/src/agent-do.ts:560-592, workers/api/src/lib/tool-result-cap.ts, workers/api/src/lib/ai-deadlines.ts, workers/api/src/lib/anthropic-stream.ts.
Related: #427 (items 1-4 shipped; this is its item 5 — #427 can close once this exists), #420 (do-not-lose-the-turn in voice), #398 (why the round must be structured, not prose), #395.
Follow-up to #427 item 5: a retry re-runs the tool the platform already paid for
#427's streaming fix (
d5c6d1a) and tool-result cap (efe285a) are shipped. Its item 5 —"make the failure resumable: the tool result is already in hand, so a retry should be able to re-run
only the generation rather than re-calling the tool" — was deliberately not attempted, and the
implementer asked for this ticket so #427 can close.
Filing it as asked. Streaming makes this rarer; it does not make it go away — any provider error in
round 2+ (5xx, an overloaded model, a stall, the 180s ceiling) lands in exactly the same place, after
the tool has run.
What it costs, from #427's own production evidence
The same issue, fetched twice, six minutes apart, because the retry had no way to say "you already
have this". Where a round's tools are writes rather than reads, the same retry re-executes side
effects — the cross-round dedup (
agent-think.ts:791-800) is scoped to one turn and cannot seethe previous request.
What actually survives today — one correction to #427
#427 says "the user's message and the tool result both survive". The message survives. The tool
result survives only as a 120-character preview:
withPartialToolLog(:1055-1064) attaches that array to the thrown error;agent-do.ts:566-580writes it as one
systemmessage, then anError: …system message. Everything the next round wouldhave needed — the full
content, thetool_useid, the arguments, theis_errorflag,outcomeById— is function-local inrunAgentThinkand dies with the throw.So the retry is not merely choosing to re-call the tool; it has no alternative, and the
120-char preview is in the transcript as prose, which is the shape #398 removed from the live path
for good reasons.
What "resumable" needs
next round would have been fed: the assistant
tool_useblocks (rawResult.contentBlocks) andthe
tool_resultblocks keyed by id (outcomeById, already capped bycapToolResult). Attach itto the failed turn — as a message with a non-rendered payload, or a sibling row keyed by the
turn's id. It must be capped and expiring; a stored round is prompt-sized, not log-sized.
runAgentThinkstarts at the top of the loop by construction. Resumingmeans re-entering with
aiMessagesalready carrying the persisted round, the round counteradvanced, and the dedup map seeded — so the model continues from "here are your results"
instead of "here is the question".
noticing a resumable round on the previous turn. More explicit: a "Continue" action on the error
message. The first is invisible and does the right thing; the second is honest about what it is
doing. I would ship the first and label it in the transcript ("continued from the tool
results already fetched"), because a user who retries is asking for the answer, not for a
decision about caching.
(minutes) and invalidation on any new user message are enough; a resumed
github_read_issuefromyesterday is worse than a re-fetch.
Keep what already works
the completed-tool notice; [bug] Muting while a turn is transcribing deletes the words — the bubble is cleared and the in-flight transcript is then dropped or sent by an unrelated 800ms echo timer #420 is the same property in the voice path. Resumption must be
additive — if resume is unavailable or stale, the turn falls back to exactly today's behaviour.
tool_resultblocks, not the
I called tools:\n…string. Re-entering through the prose shape would undo [bug] Tool results are fed back as the assistant's own prose and the tool_use turn is dropped — the format the model then imitated in #395 #398and re-teach the format [bug] The model writes its own tool RESULTS and the platform believes them — invented GitHub issues and file contents reported as fetched, raw markup left in the message #395's fabrication imitated.
d5c6d1adeliberately never returns a half-streamed message.A resumed round produces a fresh, whole reply or fails again.
Alternatives considered and rejected
wrong: it spends the user's credit twice inside one request with no signal.
short-circuit deliberate re-reads (the mutation-count dedup at
:791-800exists preciselybecause "a read is only worth repeating when something CHANGED"), and it says nothing about write
tools. Resuming ONE failed turn is the smaller, exactly-scoped claim.
now rare. Write only on failure, and only when at least one tool executed.
Acceptance criteria
tool_use+tool_resultblocks against the failed turn, capped and expiring.double that counts calls.
partial-tool-log system message.
Regression risk
not re-run a write tool, and it must not skip one whose result was never recorded. Persist the
round only after every tool in it has settled.
capToolResultseam(
lib/tool-result-cap.ts) rather than a second cap that can drift, and they must never be renderedto the user as an assistant message.
agent-do.tsmessage storage is per-instance DO state; a new payload shape on a message needs thereaders (
/messages, the console,agent_trace) to ignore what they do not understand.Files:
workers/api/src/agent-think.ts:780-800,873-905,996-1000,1030-1064,workers/api/src/agent-do.ts:560-592,workers/api/src/lib/tool-result-cap.ts,workers/api/src/lib/ai-deadlines.ts,workers/api/src/lib/anthropic-stream.ts.Related: #427 (items 1-4 shipped; this is its item 5 — #427 can close once this exists),
#420 (do-not-lose-the-turn in voice), #398 (why the round must be structured, not prose),
#395.