Skip to content
This repository was archived by the owner on Jun 3, 2026. It is now read-only.

refactor: address concurrent tools follow ups - #953

Open
notowen333 wants to merge 1 commit into
strands-agents:mainfrom
notowen333:worktree-concurrent-tools-followup
Open

refactor: address concurrent tools follow ups#953
notowen333 wants to merge 1 commit into
strands-agents:mainfrom
notowen333:worktree-concurrent-tools-followup

Conversation

@notowen333

Copy link
Copy Markdown
Contributor

Description

Follow-up to #854 addressing review comments left open at merge time.

Centralize BeforeToolsEvent / AfterToolsEvent emission

Before, each tool executor (_executeToolsSequential, _executeToolsConcurrent) and the pre-launch cancel path each emitted their own AfterToolsEvent, and BeforeToolsEvent was emitted even when the model returned zero tool-use blocks. The invariant-violation branch (model claimed toolUse stop reason but produced no tool use blocks) had to paper over this by emitting a synthetic empty AfterToolsEvent before throwing, just to keep the bracket contract intact.

executeTools now filters tool use blocks first — the invariant error throws cleanly with no events emitted — and wraps the per-executor delegation in a single try/finally that emits both the BeforeToolsEvent and the terminal AfterToolsEvent centrally. Executors write their assembled Message into a shared ToolResultMessageRef so the finally always has the latest partial result, including when the consumer breaks out of the stream via .return() mid-execution.

Test robustness

  • The tracer overlap test for concurrent mode used 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.
  • Added a concurrent-mode test asserting mid-tool ToolStreamUpdateEvents are surfaced through agent.stream().
  • Added a concurrent-mode test for per-tool BeforeToolCallEvent.cancel — one tool's hook cancel does not disturb its siblings.
  • Removed a duplicate consumer-break test; the surviving test strictly subsumes what the deleted one asserted.

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?

  • I ran npm run check

Checklist

  • I have read the CONTRIBUTING document
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@notowen333
notowen333 requested a review from pgrayy April 28, 2026 18:25
@github-actions github-actions Bot added the strands-running <strands-managed> Whether or not an agent is currently running label Apr 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

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:

  • BeforeInvocationEvent.cancel / BeforeModelCallEvent.cancel (new cancellation contracts)
  • projectedInputTokens / _estimateInputTokens / projectedContextSize (token estimation pipeline)
  • Native countTokens overrides for Anthropic, Bedrock, and Google
  • AfterToolCallEvent.result mutability (readonly → mutable)
  • MODEL_DEFAULTS centralization, warnOnce utility, ProviderTokenCountError
  • ToolExecutorStrategy export + toolExecutor config option

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.

@github-actions

Copy link
Copy Markdown
Contributor

Issue: This PR introduces multiple new public APIs that customers will interact with (cancel hooks, projectedInputTokens, ToolExecutorStrategy, mutable AfterToolCallEvent.result, AgentResult.projectedContextSize). Per the API Bar Raising guidelines, these warrant the needs-api-review label and documented use cases, example code, and complete API signatures in the PR description.

Suggestion: Add the needs-api-review label and update the PR description with:

  • Expected use cases for each new API surface
  • Example code snippets showing BeforeInvocationEvent.cancel, BeforeModelCallEvent.cancel, projectedInputTokens usage
  • Complete API signatures with defaults
  • Module exports listing

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'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-actions

Copy link
Copy Markdown
Contributor

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
  • PR Scope & Description: The diff includes at least four independent feature areas (concurrent tools refactor, cancel hooks, token estimation pipeline, model defaults centralization) but the description only covers the first. This makes review difficult and the PR should be split or the description comprehensively updated.
  • API Bar Raising: Multiple new public APIs (BeforeInvocationEvent.cancel, BeforeModelCallEvent.cancel/projectedInputTokens, mutable AfterToolCallEvent.result, ToolExecutorStrategy, projectedContextSize) warrant the needs-api-review label with documented use cases and signatures.
  • Behavioral Compatibility: The cancel message casing changed from lowercase to capitalized — a subtle but potentially breaking change that should be documented if intentional.
  • Robustness: The sequential executor's mutation-through-shared-reference pattern for ref.message is clever but fragile. The _estimateInputTokens baseline assumption may not hold after conversation trimming. The BeforeModelCallEvent.cancel retry path lacks recursion bounds.

The concurrent tools centralization and test improvements are solid contributions — the cooperative signaling pattern in tests is a great improvement over wall-clock sleeps.

@github-actions github-actions Bot removed the strands-running <strands-managed> Whether or not an agent is currently running label Apr 28, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants