Skip to content

fix(core): resolve race condition with concurrent tool spans#1034

Merged
omeraplak merged 4 commits into
VoltAgent:mainfrom
klakpin:fix/concurrent-tool-spans
Feb 9, 2026
Merged

fix(core): resolve race condition with concurrent tool spans#1034
omeraplak merged 4 commits into
VoltAgent:mainfrom
klakpin:fix/concurrent-tool-spans

Conversation

@klakpin

@klakpin klakpin commented Feb 9, 2026

Copy link
Copy Markdown
Contributor

#1033

Fixed a race condition where tools running in parallel would overwrite each other's parentToolSpan in the shared systemContext. The fix passes parentToolSpan through execution options instead of systemContext, ensuring each tool receives its unique span. Backward compatibility is maintained by checking both options and systemContext.

PR Checklist

Please check if your PR fulfills the following requirements:

Bugs / Features

What is the current behavior?

When multiple tools are called the spans are propagated incorrectly - the underlying actions inside tool (for example subagent invocation) have parent span of the last invoked tool.

What is the new behavior?

Each tool correctly traces the parent span.


Summary by cubic

Fixes a race condition where parallel tools overwrote each other’s parent spans during tracing. Parent spans now flow through per-call execution options (with systemContext fallback) and propagate correctly to subagents and other tools.

  • Bug Fixes
    • Set parentToolSpan on executionOptions for each tool call; prefer options over systemContext; stop writing parentToolSpan to systemContext in Agent.
    • Correct delegate_task propagation: read parentToolSpan from executeOptions with fallback to creation-time value or systemContext; avoid stale spans; fix name shadowing.
    • Update plan-agent, planning toolkit, retriever tools, and tool routing to read options.parentToolSpan and pass the right parentSpan; add tests for parallel tools and delegate_task to ensure unique spans and correct span names.

Written for commit 84b10cb. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed a race condition for concurrent tool executions: tracing spans are now isolated per execution, including across delegated/sub-agent calls, while preserving backward compatibility.
  • Tests

    • Added end-to-end tests validating distinct tracing spans during parallel tool and subagent executions.
  • Documentation

    • Added a changelog entry documenting the concurrent tool spans fix.

Fixed a race condition where tools running in parallel would overwrite
each other's parentToolSpan in the shared systemContext. The fix passes
parentToolSpan through execution options instead of systemContext, ensuring
each tool receives its unique span. Backward compatibility is maintained
by checking both options and systemContext.
@changeset-bot

changeset-bot Bot commented Feb 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 84b10cb

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@voltagent/core Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Feb 9, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Parent tool span propagation is moved from the shared systemContext to per-execution options (executionOptions.parentToolSpan), with fallbacks to systemContext for compatibility; subagent delegation and planning paths were updated and tests added to ensure distinct spans during concurrent tool/subagent execution.

Changes

Cohort / File(s) Summary
Changelog
.changeset/fix-concurrent-tool-spans.md
Adds changelog entry documenting the fix for concurrent tool span propagation.
Core agent logic
packages/core/src/agent/agent.ts
Set executionOptions.parentToolSpan per call instead of writing to systemContext; read parent span from options with a systemContext fallback.
Concurrent execution tests
packages/core/src/agent/concurrent-tool-spans.spec.ts
New tests asserting parallel tools and delegated subagents receive distinct parent spans and correctly named execution spans.
Sub-agent delegation
packages/core/src/agent/subagent/index.ts
Added parentToolSpan?: Span to SubAgentManager.createDelegateTool; delegate paths accept executeOptions and resolve parent span from executeOptions, local, then systemContext.
Plan agent & planning tools
packages/core/src/planagent/plan-agent.ts, packages/core/src/planagent/planning/index.ts
Tool execution now derives toolSpan from executeOptions.parentToolSpan with fallback to operationContext.systemContext.get("parentToolSpan") and passes toolSpan into handoffs.
Retriever tools
packages/core/src/retriever/tools/index.ts
Prioritize options.parentToolSpan (or options?.systemContext?.get("parentToolSpan") fallback) when extracting toolSpan for retriever tool execution.

Sequence Diagram

sequenceDiagram
    participant Agent as Agent
    participant ToolA as Tool A
    participant ToolB as Tool B
    participant OTel as OpenTelemetry

    Agent->>Agent: create per-call spans\nset executionOptions.parentToolSpan
    Agent->>ToolA: call with executionOptions.parentToolSpan
    Agent->>ToolB: call with executionOptions.parentToolSpan

    par Concurrent Execution
        ToolA->>OTel: start span "tool.execution:toolA"\n(parent = executionOptions.parentToolSpan)
        ToolB->>OTel: start span "tool.execution:toolB"\n(parent = executionOptions.parentToolSpan)
        ToolA->>ToolA: async work / await
        ToolB->>ToolB: async work / await
    end

    ToolA->>Agent: return result
    ToolB->>Agent: return result
    Agent->>OTel: spans are isolated and correlated
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰
I hopped through spans with careful paws,
Gave each tool its own applause,
No tangled traces in the night,
Concurrent hops now each take flight,
Little rabbit traces—clean and bright!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main fix: resolving a race condition with concurrent tool spans, which is the core objective of the changeset.
Description check ✅ Passed The description adequately covers the issue, behavior change, related checklist items, and includes both manual description and auto-generated summary. All required sections are present and substantive.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
packages/core/src/agent/concurrent-tool-spans.spec.ts (1)

73-91: The mock calls execute({}) without simulating tool call metadata — verify this exercises the actual span-creation path.

On Line 80, toolAWrapper.execute({}) is called with an empty object and no second argument. This relies on Agent's internal tool wrapper to inject the parentToolSpan. If the Agent's wrapper expects certain context (like toolCallId) to create a properly named span, the test might silently skip span creation and pass vacuously.

Consider verifying that the captured spans are actually non-trivial (e.g., check they have valid span context or trace IDs) beyond just toBeDefined().


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

No issues found across 7 files

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@packages/core/src/agent/subagent/index.ts`:
- Around line 764-766: The code currently casts options to any to extract
parentToolSpan; instead add parentToolSpan to the declared type for the options
parameter so you can read (options.parentToolSpan) without casting. Update the
options interface/type used by the function that calls createTool (the options
parameter in index.ts where parentToolSpan is read) to include parentToolSpan?:
Span | undefined, then remove the (options as any) cast and use const
parentToolSpan = options.parentToolSpan before calling createTool so the call
site remains fully typed.

In `@packages/core/src/planagent/plan-agent.ts`:
- Around line 773-775: The parentSpan assignment duplicates the same resolution
logic already stored in toolSpan (declared earlier on line ~748); replace the
inline re-derived expression ((executeOptions as any).parentToolSpan ...
operationContext.systemContext.get("parentToolSpan")) with the existing toolSpan
variable when setting parentSpan to avoid divergence and duplication, keeping
the type as Span | undefined (cast if necessary) and ensuring parentSpan uses
toolSpan as its source of truth.
🧹 Nitpick comments (4)
packages/core/src/planagent/plan-agent.ts (1)

748-750: Same as any cast issue as flagged in planning/index.ts.

See the comment on packages/core/src/planagent/planning/index.ts lines 151-153 regarding the as any cast and the suggestion to extract a shared helper.

packages/core/src/agent/concurrent-tool-spans.spec.ts (2)

100-106: Strengthen span assertions to validate actual Span objects.

expect(spanA).toBeDefined() passes for any truthy value. Since this test's purpose is to validate that distinct OpenTelemetry spans are created per tool, consider asserting on a known Span interface method (e.g., spanA.spanContext() returns a valid SpanContext) rather than relying on an internal .name property that may not exist on all Span implementations.

Suggested improvement
     expect(spanA).toBeDefined();
     expect(spanB).toBeDefined();
 
     // Spans shouldn't be the same
     expect(spanA).not.toBe(spanB);
-    expect((spanA as any).name).toContain("tool.execution:toolA");
-    expect((spanB as any).name).toContain("tool.execution:toolB");
+    // Verify these are real Span objects with distinct span contexts
+    const ctxA = spanA!.spanContext();
+    const ctxB = spanB!.spanContext();
+    expect(ctxA.spanId).toBeTruthy();
+    expect(ctxB.spanId).toBeTruthy();
+    expect(ctxA.spanId).not.toBe(ctxB.spanId);

27-30: Consider restoring mocks after each test.

Adding afterEach(() => vi.restoreAllMocks()) prevents mock state from leaking if more tests are added to this file later.

packages/core/src/planagent/planning/index.ts (1)

151-153: as any cast obscures runtime-added properties; add parentToolSpan to the type definition for clarity.

The parentToolSpan property is dynamically set at runtime (see agent.ts:5057) and accessed in multiple places, but it's not declared in ToolExecuteOptions. While the [key: string]: any index signature allows this, the as any cast obscures the intent. Adding parentToolSpan?: Span to ToolExecuteOptions would make the type explicit and eliminate the cast.

This pattern appears in at least six locations (planning/index.ts, plan-agent.ts ×2, retriever/tools/index.ts, agent.ts, subagent/index.ts), making it a good candidate for extraction into a shared helper function to avoid duplication.

Comment thread packages/core/src/agent/subagent/index.ts Outdated
Comment thread packages/core/src/planagent/plan-agent.ts Outdated
@omeraplak

Copy link
Copy Markdown
Member

Hey @klakpin ,
Thank you so much 🔥 Would you check the code reviews?

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/core/src/agent/subagent/index.ts">

<violation number="1" location="packages/core/src/agent/subagent/index.ts:764">
P2: delegate_task uses the tool-creation parentToolSpan instead of the per-call execute options, so concurrent calls reuse a stale/undefined span and fall back to shared systemContext, reintroducing the race the PR aims to fix.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/core/src/agent/subagent/index.ts
@omeraplak
omeraplak merged commit b65b342 into VoltAgent:main Feb 9, 2026
3 checks passed
@omeraplak omeraplak linked an issue Feb 9, 2026 that may be closed by this pull request
@klakpin

klakpin commented Feb 9, 2026

Copy link
Copy Markdown
Contributor Author

@omeraplak thanks for the quick feedback!

When all jobs failed for the first time due to the GitHub issue initially I was really impressed by how small change could destroy everything.. I am never trusting GitHub workflows again

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] incorrent parent span propagation in opentelemetry

2 participants