Skip to content

fix(sdk,openai-agents): stop entity names leaking onto sibling and parent spans - #4405

Merged
dvirski merged 4 commits into
mainfrom
dr/fix(sdk)-detach-agent/workflow/entity-path-context-tokens-to-stop-name-leak-across-nested-entities
Aug 9, 2026
Merged

fix(sdk,openai-agents): stop entity names leaking onto sibling and parent spans#4405
dvirski merged 4 commits into
mainfrom
dr/fix(sdk)-detach-agent/workflow/entity-path-context-tokens-to-stop-name-leak-across-nested-entities

Conversation

@dvirski

@dvirski dvirski commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • Bug Fixes

    • Improved tracing context isolation across nested, sibling, asynchronous, workflow, task, and generator-based operations.
    • Prevented agent names, conversation IDs, workflow details, and entity paths from leaking into later spans.
    • Reduced context-related errors when work resumes across threads or tasks.
    • Ensured context cleanup after successful completion, failures, generator exhaustion, and early closure.
  • Documentation

    • Added guidance on context cleanup and preventing context leakage.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds token-based context cleanup for OpenAI agent spans and SDK decorators. It covers synchronous, asynchronous, generator, and async-generator paths, including span failures, task or thread boundaries, and conversation ID scoping.

Changes

Tracing context cleanup

Layer / File(s) Summary
OpenAI agent span token ownership
packages/opentelemetry-instrumentation-openai-agents/..., packages/opentelemetry-instrumentation-openai-agents/tests/test_tracing_processor.py
Agent span creation returns an agent-name detach token. Span completion and failure paths safely detach agent-name and span-context tokens. Tests cover sibling isolation, interleaved spans, and creation failures.
SDK tokenized decorator cleanup
packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py, packages/traceloop-sdk/traceloop/sdk/decorators/base.py, packages/traceloop-sdk/tests/test_agent_name_scope.py
Context setters return detach tokens. Decorator wrappers propagate and detach workflow, agent, span, and entity-path tokens in reverse order across normal, exceptional, generator, and async-generator execution.
Conversation decorator scope
packages/traceloop-sdk/traceloop/sdk/decorators/__init__.py, packages/traceloop-sdk/tests/test_conversation_id.py
The conversation decorator supports generators and async generators. It attaches conversation IDs per invocation and safely detaches them after completion, failure, or iterator abandonment.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DecoratedEntity
  participant DecoratorWrapper
  participant ContextSetters
  participant OpenTelemetrySpan
  DecoratedEntity->>DecoratorWrapper: invoke sync, async, or generator function
  DecoratorWrapper->>ContextSetters: attach scoped context values
  ContextSetters-->>DecoratorWrapper: return detach tokens
  DecoratorWrapper->>OpenTelemetrySpan: create and run span
  OpenTelemetrySpan-->>DecoratorWrapper: complete or close execution
  DecoratorWrapper->>ContextSetters: safely detach tokens
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: preventing entity-name context from leaking to sibling and parent spans.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dr/fix(sdk)-detach-agent/workflow/entity-path-context-tokens-to-stop-name-leak-across-nested-entities

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (5)
packages/traceloop-sdk/tests/test_conversation_id.py (2)

201-219: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The patched detach leaves the token attached.

raising_detach never detaches, so the conv-crash token stays on the context that the coroutine ran in. Today this does not affect test_conversation_id_does_not_leak_to_later_sibling, because pytest-asyncio runs the coroutine in a Task with its own copy of the context. That isolation is incidental. If the test later becomes synchronous, or the runner changes how it creates the context, conv-crash leaks into the following tests and they fail.

Make the cleanup explicit: capture the real detach, raise ValueError, then call the real detach so state is restored.

♻️ Proposed test hardening
+    real_detach = context_api.detach
+
     def raising_detach(token):
+        real_detach(token)
         raise ValueError("was created in a different Context")

Also add assert context_api.get_value("conversation_id") is None after the call once the real detach runs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/traceloop-sdk/tests/test_conversation_id.py` around lines 201 - 219,
Update test_conversation_decorator_async_detach_does_not_crash to preserve the
original context_api.detach before monkeypatching, have the replacement raise
the ValueError and then invoke the saved real detach with the token, and assert
context_api.get_value("conversation_id") is None after handler() completes.

156-198: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Define inner_task before the generator that calls it.

streaming_chat references inner_task at line 162, and inner_task is defined at line 165. This works, because the generator body runs at iteration time and resolves the closure then. Reordering the definitions makes the dependency clear and removes the reliance on deferred name resolution.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/traceloop-sdk/tests/test_conversation_id.py` around lines 156 - 198,
In both test_conversation_decorator_sync_generator and
test_conversation_decorator_async_generator, move the inner_task definition
before the streaming_chat generator definition that invokes it. Preserve the
existing decorators, yielded values, and assertions unchanged.
packages/traceloop-sdk/traceloop/sdk/decorators/__init__.py (1)

54-55: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider a module-level import for _safe_detach.

_safe_detach and set_conversation_id are imported inside conversation. If the import guards against a circular import, add a short comment stating that. Otherwise move both imports to module level.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/traceloop-sdk/traceloop/sdk/decorators/__init__.py` around lines 54
- 55, Update the imports used by conversation to be module-level for
_safe_detach and set_conversation_id; if _safe_detach must remain inside
conversation to avoid a circular import, keep that placement and add a brief
explanatory comment.
packages/opentelemetry-instrumentation-openai-agents/tests/test_tracing_processor.py (1)

1337-1363: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the "no OTel span created" detach branch.

on_span_start also detaches the name token when _start_agent_span returns a falsy span (the elif name_token is not None branch in _hooks.py). The failure test covers the raise path only. A test that makes start_span return None would cover the remaining branch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/opentelemetry-instrumentation-openai-agents/tests/test_tracing_processor.py`
around lines 1337 - 1363, Extend test_name_detached_when_span_creation_fails to
cover the falsy-span path by configuring processor.tracer.start_span to return
None instead of raising. Invoke processor.on_span_start with the same named
AgentSpanData and assert get_value("agent_name") is None, covering the elif
name_token is not None branch in on_span_start.
packages/traceloop-sdk/traceloop/sdk/decorators/base.py (1)

330-346: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Tokens stay attached after sync_wrap returns a generator.

If fn returns a generator, sync_wrap returns _handle_generator(...) without detaching. The name, span, and path tokens stay attached on the caller's context until the returned generator runs its finally. If the caller never iterates the generator, the entity name leaks for the rest of the trace. This matches the earlier behavior for the span token, so it is not a regression, but the new name and path tokens widen the leaked surface. Consider documenting this alongside the async_gen_wrap "KNOWN LIMITATION" note.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/traceloop-sdk/traceloop/sdk/decorators/base.py` around lines 330 -
346, Document this known limitation in the synchronous generator path around
sync_wrap and _handle_generator: when fn returns an unconsumed generator, the
entity name, span, and path tokens remain attached until generator cleanup runs,
so they may leak if iteration never begins. Align the documentation with the
existing async_gen_wrap KNOWN LIMITATION note without changing the generator
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/opentelemetry-instrumentation-openai-agents/opentelemetry/instrumentation/openai_agents/_hooks.py`:
- Around line 670-672: Update shutdown() to clear _agent_name_tokens alongside
_otel_spans, _span_contexts, _root_spans, and _reverse_handoffs_dict, ensuring
shutdown releases all tracked agent-name tokens and related references.

In `@packages/traceloop-sdk/traceloop/sdk/decorators/__init__.py`:
- Around line 93-101: Update sync_wrapper in the conversation decorator to
detect when fn returns a generator and keep the conversation token attached
until iteration completes, mirroring base._handle_generator; retain immediate
cleanup for non-generator results and ensure cleanup also occurs if iteration
raises. Add a test covering `@conversation` stacked above `@task` on a generator
function, verifying the conversation ID remains available while yielding.

---

Nitpick comments:
In
`@packages/opentelemetry-instrumentation-openai-agents/tests/test_tracing_processor.py`:
- Around line 1337-1363: Extend test_name_detached_when_span_creation_fails to
cover the falsy-span path by configuring processor.tracer.start_span to return
None instead of raising. Invoke processor.on_span_start with the same named
AgentSpanData and assert get_value("agent_name") is None, covering the elif
name_token is not None branch in on_span_start.

In `@packages/traceloop-sdk/tests/test_conversation_id.py`:
- Around line 201-219: Update
test_conversation_decorator_async_detach_does_not_crash to preserve the original
context_api.detach before monkeypatching, have the replacement raise the
ValueError and then invoke the saved real detach with the token, and assert
context_api.get_value("conversation_id") is None after handler() completes.
- Around line 156-198: In both test_conversation_decorator_sync_generator and
test_conversation_decorator_async_generator, move the inner_task definition
before the streaming_chat generator definition that invokes it. Preserve the
existing decorators, yielded values, and assertions unchanged.

In `@packages/traceloop-sdk/traceloop/sdk/decorators/__init__.py`:
- Around line 54-55: Update the imports used by conversation to be module-level
for _safe_detach and set_conversation_id; if _safe_detach must remain inside
conversation to avoid a circular import, keep that placement and add a brief
explanatory comment.

In `@packages/traceloop-sdk/traceloop/sdk/decorators/base.py`:
- Around line 330-346: Document this known limitation in the synchronous
generator path around sync_wrap and _handle_generator: when fn returns an
unconsumed generator, the entity name, span, and path tokens remain attached
until generator cleanup runs, so they may leak if iteration never begins. Align
the documentation with the existing async_gen_wrap KNOWN LIMITATION note without
changing the generator behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0c8599f6-45be-4f3e-96b2-15ae4357a8f2

📥 Commits

Reviewing files that changed from the base of the PR and between 93429cf and 7f8d0dc.

⛔ Files ignored due to path filters (3)
  • packages/opentelemetry-instrumentation-openai-agents/uv.lock is excluded by !**/*.lock
  • packages/sample-app/uv.lock is excluded by !**/*.lock
  • packages/traceloop-sdk/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • packages/opentelemetry-instrumentation-openai-agents/opentelemetry/instrumentation/openai_agents/_hooks.py
  • packages/opentelemetry-instrumentation-openai-agents/tests/test_tracing_processor.py
  • packages/traceloop-sdk/tests/test_agent_name_scope.py
  • packages/traceloop-sdk/tests/test_conversation_id.py
  • packages/traceloop-sdk/traceloop/sdk/decorators/__init__.py
  • packages/traceloop-sdk/traceloop/sdk/decorators/base.py
  • packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py

Comment thread packages/traceloop-sdk/traceloop/sdk/decorators/__init__.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/traceloop-sdk/traceloop/sdk/decorators/__init__.py`:
- Around line 109-125: Update the generator-return path in the wrapper around fn
and _consume_with_token so the invocation token is always detached before
returning the generator. Have _consume_with_token attach a fresh conversation
token when iteration begins and detach it when consumption finishes, while
preserving conversation propagation during iteration. Add coverage for storing
an unstarted returned generator before unrelated tracing work executes.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 08a6efac-07cc-4e7e-beea-35bf88b2dbc9

📥 Commits

Reviewing files that changed from the base of the PR and between 7f8d0dc and ee7855a.

📒 Files selected for processing (2)
  • packages/traceloop-sdk/tests/test_conversation_id.py
  • packages/traceloop-sdk/traceloop/sdk/decorators/__init__.py

Comment thread packages/traceloop-sdk/traceloop/sdk/decorators/__init__.py
@dvirski
dvirski merged commit e561195 into main Aug 9, 2026
12 checks passed
@dvirski
dvirski deleted the dr/fix(sdk)-detach-agent/workflow/entity-path-context-tokens-to-stop-name-leak-across-nested-entities branch August 9, 2026 08:54
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.

2 participants