Skip to content

Commit e617a71

Browse files
committed
fix: Make sure cross context cleanup doesn't raise an error
1 parent 9548ae3 commit e617a71

3 files changed

Lines changed: 127 additions & 1 deletion

File tree

py/src/braintrust/context.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,10 @@ def set_current_span(self, span_object: Any) -> Any:
103103
def unset_current_span(self, context_token: Any = None) -> None:
104104
"""Unset the current active span."""
105105
if context_token:
106-
self._current_span.reset(context_token)
106+
try:
107+
self._current_span.reset(context_token)
108+
except ValueError:
109+
self._current_span.set(None)
107110
else:
108111
self._current_span.set(None)
109112

py/src/braintrust/test_context.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,27 @@ async def task_work():
753753
)
754754

755755

756+
@pytest.mark.asyncio
757+
async def test_unset_current_span_with_cross_context_token_falls_back_to_clear():
758+
"""Cross-context cleanup should not raise if the token can't be reset."""
759+
from braintrust.context import BraintrustContextManager
760+
761+
context_manager = BraintrustContextManager()
762+
token = context_manager.set_current_span("parent")
763+
result = {}
764+
765+
async def other_task():
766+
try:
767+
context_manager.unset_current_span(token)
768+
result["outcome"] = "ok"
769+
except Exception as e:
770+
result["outcome"] = f"{type(e).__name__}: {e}"
771+
772+
await asyncio.create_task(other_task())
773+
774+
assert result["outcome"] == "ok"
775+
776+
756777
@pytest.mark.asyncio
757778
async def test_async_generator_early_break_context_token(test_logger, with_memory_logger):
758779
"""

py/src/braintrust/wrappers/claude_agent_sdk/test_wrapper.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
the actual Claude Agent SDK.
66
"""
77

8+
import asyncio
9+
import gc
10+
import sys
11+
import types
12+
813
import pytest
914

1015
# Try to import the Claude Agent SDK - skip tests if not available
@@ -19,6 +24,7 @@
1924
from braintrust import logger
2025
from braintrust.span_types import SpanTypeAttribute
2126
from braintrust.test_helpers import init_test_logger
27+
from braintrust.wrappers.claude_agent_sdk import setup_claude_agent_sdk
2228
from braintrust.wrappers.claude_agent_sdk._wrapper import (
2329
_create_client_wrapper_class,
2430
_create_tool_wrapper_class,
@@ -292,3 +298,99 @@ class TestAutoInstrumentClaudeAgentSDK:
292298
def test_auto_instrument_claude_agent_sdk(self):
293299
"""Test auto_instrument patches Claude Agent SDK and creates spans."""
294300
verify_autoinstrument_script("test_auto_claude_agent_sdk.py")
301+
302+
303+
class _FakeClaudeAgentOptions:
304+
def __init__(self, model, permission_mode=None):
305+
self.model = model
306+
self.permission_mode = permission_mode
307+
308+
309+
class _FakeMessage:
310+
def __init__(self, content):
311+
self.content = content
312+
313+
314+
class _FakeResultMessage:
315+
def __init__(self):
316+
self.usage = types.SimpleNamespace(input_tokens=1, output_tokens=1, cache_creation_input_tokens=0)
317+
self.num_turns = 1
318+
self.session_id = "session-123"
319+
320+
321+
class _FakeClaudeSDKClient:
322+
def __init__(self, options):
323+
self.options = options
324+
self._prompt = None
325+
326+
async def __aenter__(self):
327+
return self
328+
329+
async def __aexit__(self, *args):
330+
return None
331+
332+
async def query(self, prompt):
333+
self._prompt = prompt
334+
335+
async def receive_response(self):
336+
yield _FakeMessage("Hello")
337+
await asyncio.sleep(0)
338+
yield _FakeResultMessage()
339+
340+
341+
def _install_fake_claude_sdk(monkeypatch):
342+
fake_module = types.ModuleType("claude_agent_sdk")
343+
fake_module.ClaudeSDKClient = _FakeClaudeSDKClient
344+
fake_module.ClaudeAgentOptions = _FakeClaudeAgentOptions
345+
fake_module.__dict__["SdkMcpTool"] = None
346+
fake_module.__dict__["tool"] = None
347+
monkeypatch.setitem(sys.modules, "claude_agent_sdk", fake_module)
348+
return fake_module
349+
350+
351+
@pytest.mark.asyncio
352+
async def test_setup_claude_agent_sdk_repro_import_before_setup(memory_logger, monkeypatch):
353+
"""Regression test for https://github.com/braintrustdata/braintrust-sdk-python/issues/7."""
354+
assert not memory_logger.pop()
355+
356+
fake_sdk = _install_fake_claude_sdk(monkeypatch)
357+
consumer_module = types.ModuleType("test_issue7_repro_module")
358+
consumer_module.ClaudeSDKClient = fake_sdk.ClaudeSDKClient
359+
consumer_module.ClaudeAgentOptions = fake_sdk.ClaudeAgentOptions
360+
monkeypatch.setitem(sys.modules, consumer_module.__name__, consumer_module)
361+
362+
# Mirror the reported import pattern:
363+
# from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
364+
assert setup_claude_agent_sdk(project=PROJECT_NAME, api_key=logger.TEST_API_KEY)
365+
assert consumer_module.ClaudeSDKClient is not _FakeClaudeSDKClient
366+
367+
loop_errors = []
368+
received_types = []
369+
370+
async def main():
371+
loop = asyncio.get_running_loop()
372+
loop.set_exception_handler(lambda loop, ctx: loop_errors.append(ctx.get("exception") or ctx.get("message")))
373+
374+
options = consumer_module.ClaudeAgentOptions(
375+
model="claude-sonnet-4-20250514",
376+
permission_mode="bypassPermissions",
377+
)
378+
async with consumer_module.ClaudeSDKClient(options=options) as client:
379+
await client.query("Hello")
380+
async for message in client.receive_response():
381+
received_types.append(type(message).__name__)
382+
383+
await asyncio.sleep(0)
384+
gc.collect()
385+
await asyncio.sleep(0.01)
386+
387+
await main()
388+
389+
assert loop_errors == []
390+
assert received_types == ["_FakeMessage", "_FakeResultMessage"]
391+
392+
spans = memory_logger.pop()
393+
task_spans = [s for s in spans if s["span_attributes"]["type"] == SpanTypeAttribute.TASK]
394+
assert len(task_spans) == 1
395+
assert task_spans[0]["span_attributes"]["name"] == "Claude Agent"
396+
assert task_spans[0]["input"] == "Hello"

0 commit comments

Comments
 (0)