refactor: address concurrent tools follow ups - #953
Conversation
|
Issue: PR scope significantly exceeds what the description covers. The description documents concurrent tools follow-ups (~300 lines across 3 files), but the actual diff is 32 files, +2235/-110 lines introducing several new features:
Suggestion: Consider splitting this into separate PRs for (1) the concurrent tools refactor described in the PR description, (2) the cancel hooks feature, (3) the token estimation/counting pipeline, and (4) the model defaults centralization. At minimum, the PR description should be updated to document all changes, with use cases and API signatures for each new public API surface — this is important for the API bar-raising process. |
|
Issue: This PR introduces multiple new public APIs that customers will interact with (cancel hooks, Suggestion: Add the
|
| const ref: ToolResultMessageRef = { message: new Message({ role: 'user', content: [] }) } | ||
| try { | ||
| if (beforeToolsEvent.cancel) { | ||
| const message = typeof beforeToolsEvent.cancel === 'string' ? beforeToolsEvent.cancel : 'Tool cancelled by hook' |
There was a problem hiding this comment.
Issue: The default cancel message changed from 'tool cancelled by hook' (lowercase) to 'Tool cancelled by hook' (capitalized). This is a behavioral change from the previous cancelToolMessage() helper — any downstream code or tests comparing against the old string will break.
Suggestion: If the capitalization change is intentional, call it out in the PR description. If not, revert to 'tool cancelled by hook' for backward compatibility.
| ): AsyncGenerator<AgentStreamEvent, void, undefined> { | ||
| const toolResultBlocks: ToolResultBlock[] = [] | ||
| let toolResultMessage: Message | ||
| ref.message = new Message({ role: 'user', content: toolResultBlocks }) |
There was a problem hiding this comment.
Issue: This relies on Message.content storing the exact array reference passed to the constructor, so push() on the local toolResultBlocks mutates the message's content in-place through the readonly type annotation. While this works today, it's fragile — if Message's constructor ever copies the array, partial results will silently stop propagating.
Suggestion: Consider documenting this invariant with a comment, or adopt the same pattern as _executeToolsConcurrent where ref.message is reassigned in the finally block from a complete snapshot.
|
Assessment: Request Changes This PR contains well-structured code with thoughtful concurrency patterns and good test quality (cooperative signaling replacing sleep timers is excellent). However, the scope is significantly larger than the description suggests — 32 files, +2235 lines introducing multiple new public APIs beyond the described concurrent tools refactor. Review Categories
The concurrent tools centralization and test improvements are solid contributions — the cooperative signaling pattern in tests is a great improvement over wall-clock sleeps. |
Description
Follow-up to #854 addressing review comments left open at merge time.
Centralize
BeforeToolsEvent/AfterToolsEventemissionBefore, each tool executor (
_executeToolsSequential,_executeToolsConcurrent) and the pre-launch cancel path each emitted their ownAfterToolsEvent, andBeforeToolsEventwas emitted even when the model returned zero tool-use blocks. The invariant-violation branch (model claimedtoolUsestop reason but produced no tool use blocks) had to paper over this by emitting a synthetic emptyAfterToolsEventbefore throwing, just to keep the bracket contract intact.executeToolsnow filters tool use blocks first — the invariant error throws cleanly with no events emitted — and wraps the per-executor delegation in a singletry/finallythat emits both theBeforeToolsEventand the terminalAfterToolsEventcentrally. Executors write their assembledMessageinto a sharedToolResultMessageRefso thefinallyalways has the latest partial result, including when the consumer breaks out of the stream via.return()mid-execution.Test robustness
sleep(20)to give the executor time to launch both tools before either resolved. Replaced with cooperative signaling: each tool waits on a promise that its peer resolves at entry, so the overlap is guaranteed by construction rather than by wall-clock race.ToolStreamUpdateEvents are surfaced throughagent.stream().BeforeToolCallEvent.cancel— one tool's hook cancel does not disturb its siblings.Related Issues
Follow-up to #854.
Documentation PR
No documentation changes.
Type of Change
Other (refactor + test quality)
Testing
How have you tested the change?
npm run checkChecklist
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.