A tool call's whole life on the wire - #936
Open
sroussey wants to merge 1 commit into
Open
Conversation
A host drawing a card for one tool call could only reconstruct it by diffing successive copies of the conversation the turn publishes: the ask appears when the assistant message lands, and the outcome when the results land — as one batch, after the last of them, with nothing in between and no way to tell which call is running now. Builder did not hit this because its tools are closures it owns, so the adapter fills the card in from inside the tool. A host whose tools are task types it named has nothing to fill it in from, and neither does one relaying the turn to a protocol. So each call now reports where it has got to: `pending` when the model asks (carrying the arguments), `running` when the loop takes it up, and `completed` or `failed` when it settles, carrying the same string the model is about to read — read back off the result block rather than built a second time, so a card cannot drift from the answer or miss its clamp. Every call passes all three states, including the ones nothing executes for — an unknown tool, arguments its schema rejects, a call nobody approves — so a host draws one lifecycle rather than one per way a call can end. `running` covers waiting on a person for the same reason: an approval is part of making the call. Metadata on the same terms as `phase`: emitted on `stream_chunk`, never accumulated into a port, absent from `finish`, and no status flip. A task is not streaming its output because something it called started. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JEFYAGb9D3mWfyhYkmeAvN
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A host drawing a card for one tool call could only reconstruct it by diffing successive copies of the conversation
AgentTaskpublishes. The ask appears when the assistant message lands, and the outcome when the results land — as one batch, after the last of them, with nothing in between and no way to tell which call is running now.Each call now reports where it has got to, as a
tool-callstream event:pendingrunningcompleted/failedWhy this was not missed earlier
Builder's tools are closures it owns, so its adapter fills the card in from inside the tool. That works exactly as far as the host owning every tool it passes. A host whose tools are task types it named has nothing to fill a card in from, and neither does one relaying a turn to a protocol — where a per-call lifecycle is the shape on the other side of the wire, not an extra.
Three decisions worth a reviewer's attention
The settled text is read back off the result block, not built a second time. What the card says the call produced is then the same string the model is about to read, clamp included, and cannot drift from it. Rebuilding it instead fails three tests.
Every call passes all three states — including the ones nothing executes for. An unknown tool, arguments its schema rejects, a call nobody approves: all of them go
pending → running → failed. A host then draws one lifecycle rather than one per way a call can end.runningcovers waiting on a person for the same reason: an approval is part of making the call, and a host drawing its own approval already knows it asked.Metadata, on
phase's exact terms — emitted onstream_chunk, never accumulated into a port, absent fromfinish, no status flip. A task is not streaming its output because something it called started.StreamEventgains a variant; nothing switches exhaustively over that union, so the change is additive.All of a round's calls are announced before any of them runs, since the model asked for them together and a host can lay out the whole set rather than watching cards appear in an order that is the loop's business rather than the model's.
Verification
bun run build:types— 43/43bun run lint(--type-aware --deny-warnings),bun run format-check— cleanpackages/test/src/test/ai+packages/ai/src— 1,808 passedpackages/test/src/test/graph+packages/task-graph/src— 1,036 passed (this touches core stream types, so that section ran too)AgentTask.test.ts— 30 passed (25 existing, 5 new)Each new test fails when the code it covers is broken, and only those — checked by breaking each in turn: disabling the
StreamProcessorcase fails 5, collapsingfailedintocompletedfails 3, moving thependingemission inside the run loop fails exactly the ordering test, and rebuilding the result text instead of reading the block back fails 3.One trap found on the way, left commented at the site:
Extract<StreamEvent, { status: "completed" }>silently yieldsnever, because the settled member's status is the wider"completed" | "failed"and no narrower constraint matches it. The extraction keys onresultinstead.Related
CLAUDE.mdparagraph that told readers to draw tool cards from themessagessnapshots is corrected; snapshots stay what they were for, mirroring the conversation.🤖 Generated with Claude Code
https://claude.ai/code/session_01JEFYAGb9D3mWfyhYkmeAvN
Generated by Claude Code