fix(google-genai): propagate context to threaded tool calls (#38) - #271
RichardoMrMu wants to merge 4 commits into
Conversation
Update tool_call_wrapper.py
Update test_tool_call_wrapper.py
Update CHANGELOG.md
…t imports) CI ruff check failed on this package (which cascaded into the precommit, Lint 0, and package-test jobs, since ruff runs first): F401 for an unused 'import contextvars' left over from an earlier approach (the fix uses opentelemetry.context, not contextvars directly), and PLC0415 for function-local imports in the new alibaba#38 regression tests. Remove the dead import and hoist 'concurrent.futures' to the top; the two function-local 'from opentelemetry.trace import get_tracer_provider' were redundant (already imported at module top), so drop them. No behavior change.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Out-of-order context-token detachment leaks the captured trace context into reused workers.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 1
Open (1)
What changed in this PR
Propagates OpenTelemetry context to threaded Google GenAI tool calls.
Changes:
- Captures and reattaches parent context for tool spans.
- Adds thread-pool regression tests.
- Documents the fix.
| File | Description |
|---|---|
tool_call_wrapper.py |
Adds context propagation. |
test_tool_call_wrapper.py |
Tests threaded tool calls. |
CHANGELOG.md |
Records the fix. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| finally: | ||
| if token is not None: | ||
| otel_context.detach(token) |
|
Thanks for working on cross-thread tool-call tracing. We tested the two regression tests added in this PR against current
LoongSuite already includes threading instrumentation in its default bootstrap registry. For manual instrumentation, it can be enabled with: from opentelemetry.instrumentation.threading import ThreadingInstrumentor
ThreadingInstrumentor().instrument()For the tested We also checked child spans created inside a tool. On Could you share a minimal reproduction that still loses context with threading instrumentation enabled, including the package versions and startup command? Based on the current reproduction, enabling the existing threading instrumentation appears sufficient, and we would prefer documenting/verifying that integration over adding another context bridge in the Google tool wrapper. Test environment: Python 3.12.13, OpenTelemetry SDK 1.39.1, threading instrumentation 0.60b1, Google GenAI SDK 2.25.0, and LoongSuite distro/Google instrumentation/GenAI util from source. The Google SDK automatic-tool-call check used a mocked HTTP transport; the thread propagation checks did not require a live model service. |
|
Thanks @sipercai for the thorough investigation — you're right on both counts, and I'm closing this PR. I re-checked against the You also correctly spotted the regression: my change detaches the captured context right after the tool span is created but before the tool body runs, so a child span created inside the tool becomes a sibling of the tool span instead of its child ( Since the existing threading instrumentation is the right layer for this and my wrapper-level bridge both duplicates it and breaks span nesting, closing is the correct call. If it'd be useful, I'm happy to instead contribute a short docs note + a hierarchy-asserting test that verifies the threading-instrumentation integration for the automatic-function-calling path. Thanks again for the careful review. |

What
Fixes #38. Parallel/concurrent tool calls in an agent produce multiple
disconnected traces instead of one trace with multiple spans.
Root cause
tool_call_wrapper.wrapped_tool()wraps each tool while the agent/LLM span isactive (
generate_content._wrapped_config_with_tools). The wrapped tool buildsits
ToolInvocation->start_execute_tool, whose span is parented to whateveris in the OpenTelemetry context at call time. The Google GenAI SDK's
automatic function calling (and agent frameworks) execute those wrapped tools in
a
ThreadPoolExecutor/run_in_executorworker. Worker threads do not inheritcontextvars, so the worker sees an empty context and each tool span starts anew root trace.
This matches the maintainer's confirmed analysis on #38, and is the same class
of issue already worked around per-instrumentation in
bfclv4(
threading_propagation.py) andopenhands(session_context.py).Fix
tool_call_wrappernow captures the active context when a tool is wrapped(only when a span is active) and re-attaches it around invocation creation in
both the sync and async wrappers, then detaches immediately. The tool function
itself runs in its normal context; only the invocation/span creation is
re-parented. When no span is active the capture returns
Noneand behavior isunchanged, so single-threaded execution is unaffected.
Only
tool_call_wrapper.pychanges on the source side;_compat.pyandgenerate_content.pyare untouched.Tests
Added two regression tests in
tests/utils/test_tool_call_wrapper.py:test_parallel_tool_calls_share_parent_traceruns two wrapped tools in aThreadPoolExecutorunder an agent span and asserts bothexecute_toolspansshare the agent's
trace_id.test_run_in_executor_tool_call_shares_parent_tracecovers theasyncio.run_in_executorpath named in the issue.Both fail on the current wrapper (tool spans land on new traces) and pass with
the fix. The existing single-threaded tool-wrapper tests are unchanged and still
pass.