Skip to content

Commit 61d445f

Browse files
committed
fix: close Claude agent span on result message
1 parent cbf9b9c commit 61d445f

2 files changed

Lines changed: 101 additions & 2 deletions

File tree

  • instrumentation-loongsuite/loongsuite-instrumentation-claude-agent-sdk

instrumentation-loongsuite/loongsuite-instrumentation-claude-agent-sdk/src/opentelemetry/instrumentation/claude_agent_sdk/patch.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,12 @@ async def _process_agent_invocation_stream(
848848
session_cwd: Optional[str] = None
849849
agent_closed = False
850850

851+
def close_agent_successfully() -> None:
852+
nonlocal agent_closed
853+
if not agent_closed:
854+
handler.stop_invoke_agent(agent_invocation)
855+
agent_closed = True
856+
851857
try:
852858
async for msg in wrapped_stream:
853859
msg_type = type(msg).__name__
@@ -882,11 +888,11 @@ async def _process_agent_invocation_stream(
882888
)
883889
elif msg_type == "ResultMessage":
884890
_process_result_message(msg, agent_invocation, turn_tracker)
891+
close_agent_successfully()
885892

886893
yield msg
887894

888-
handler.stop_invoke_agent(agent_invocation)
889-
agent_closed = True
895+
close_agent_successfully()
890896

891897
except BaseException as e:
892898
error_msg = str(e)

instrumentation-loongsuite/loongsuite-instrumentation-claude-agent-sdk/tests/test_session_capture.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,22 @@ async def _run_stream(tracer_provider, messages, session_id=None):
150150
pass
151151

152152

153+
async def _read_until_result_without_exhausting(
154+
handler, messages, session_id=None
155+
):
156+
stream = _process_agent_invocation_stream(
157+
wrapped_stream=_stream(messages),
158+
handler=handler,
159+
model="claude-sonnet",
160+
prompt="inspect the project",
161+
session_id=session_id,
162+
)
163+
while True:
164+
message = await stream.__anext__()
165+
if isinstance(message, ResultMessage):
166+
return stream
167+
168+
153169
@pytest.mark.asyncio
154170
async def test_entry_baggage_session_overrides_claude_session(
155171
tracer_provider, span_exporter
@@ -463,6 +479,83 @@ async def wrapped_query(*args, **kwargs):
463479
assert agent_span.attributes[GEN_AI_SESSION_ID] == "parent-session"
464480

465481

482+
@pytest.mark.asyncio
483+
async def test_result_message_closes_agent_before_iterator_is_exhausted(
484+
tracer_provider, span_exporter
485+
):
486+
handler = ExtendedTelemetryHandler(tracer_provider=tracer_provider)
487+
stream = await _read_until_result_without_exhausting(
488+
handler,
489+
[
490+
SystemMessage("sess-terminal"),
491+
AssistantMessage([TextBlock("terminal answer")]),
492+
ResultMessage("sess-terminal"),
493+
],
494+
)
495+
try:
496+
agent_spans = _spans_by_operation(
497+
span_exporter.get_finished_spans(), "invoke_agent"
498+
)
499+
assert len(agent_spans) == 1
500+
assert agent_spans[0].attributes[GEN_AI_SESSION_ID] == (
501+
"sess-terminal"
502+
)
503+
finally:
504+
await stream.aclose()
505+
506+
507+
@pytest.mark.asyncio
508+
async def test_consecutive_result_breaks_keep_outer_parent_context(
509+
tracer_provider, span_exporter
510+
):
511+
handler = ExtendedTelemetryHandler(tracer_provider=tracer_provider)
512+
tracer = tracer_provider.get_tracer(__name__)
513+
streams = []
514+
515+
try:
516+
with tracer.start_as_current_span("caller-operation") as parent_span:
517+
streams.append(
518+
await _read_until_result_without_exhausting(
519+
handler,
520+
[
521+
SystemMessage("sess-first"),
522+
AssistantMessage([TextBlock("first answer")]),
523+
ResultMessage("sess-first"),
524+
],
525+
)
526+
)
527+
streams.append(
528+
await _read_until_result_without_exhausting(
529+
handler,
530+
[
531+
SystemMessage("sess-second"),
532+
AssistantMessage([TextBlock("second answer")]),
533+
ResultMessage("sess-second"),
534+
],
535+
)
536+
)
537+
finally:
538+
for stream in streams:
539+
await stream.aclose()
540+
541+
agent_spans = {
542+
span.attributes[GEN_AI_SESSION_ID]: span
543+
for span in _spans_by_operation(
544+
span_exporter.get_finished_spans(), "invoke_agent"
545+
)
546+
}
547+
548+
first_span = agent_spans["sess-first"]
549+
second_span = agent_spans["sess-second"]
550+
551+
for span in (first_span, second_span):
552+
assert span.parent is not None
553+
assert span.parent.span_id == parent_span.context.span_id
554+
assert span.context.trace_id == parent_span.context.trace_id
555+
556+
assert second_span.parent.span_id != first_span.context.span_id
557+
558+
466559
@pytest.mark.asyncio
467560
async def test_task_subagent_inherits_session_id(
468561
tracer_provider, span_exporter

0 commit comments

Comments
 (0)