Skip to content

Commit 531d526

Browse files
authored
fix(otel): record tool_trace on interrupted tool calls (strands-agents#3031)
1 parent c17b211 commit 531d526

3 files changed

Lines changed: 29 additions & 3 deletions

File tree

strands-py/src/strands/telemetry/metrics.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ def add_tool_usage(
298298
duration: float,
299299
tool_trace: Trace,
300300
success: bool,
301-
message: Message,
301+
message: Message | None = None,
302302
) -> None:
303303
"""Record metrics for a tool invocation.
304304
@@ -307,7 +307,8 @@ def add_tool_usage(
307307
duration: How long the tool call took in seconds.
308308
tool_trace: The trace object for this tool call.
309309
success: Whether the tool call was successful.
310-
message: The message associated with the tool call.
310+
message: The message associated with the tool call, if any. Pass ``None``
311+
when the call ended without producing a tool result (e.g. on interrupt).
311312
"""
312313
tool_name = tool.get("name", "unknown_tool")
313314
tool_use_id = tool.get("toolUseId", "unknown")
@@ -319,7 +320,8 @@ def add_tool_usage(
319320
}
320321
)
321322
tool_trace.raw_name = f"{tool_name} - {tool_use_id}"
322-
tool_trace.add_message(message)
323+
if message is not None:
324+
tool_trace.add_message(message)
323325

324326
self.tool_metrics.setdefault(tool_name, ToolMetrics(tool)).add_call(
325327
tool,

strands-py/src/strands/tools/executors/_executor.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,10 @@ async def _stream_with_trace(
331331
yield event
332332

333333
if isinstance(event, ToolInterruptEvent):
334+
tool_duration = time.time() - tool_start_time
335+
if ToolExecutor._is_agent(agent):
336+
agent.event_loop_metrics.add_tool_usage(tool_use, tool_duration, tool_trace, False)
337+
cycle_trace.add_child(tool_trace)
334338
tracer.end_tool_call_span(tool_call_span, tool_result=None)
335339
return
336340

strands-py/tests/strands/tools/executors/test_executor.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,26 @@ async def test_executor_stream_with_trace(
224224
assert isinstance(cycle_trace.add_child.call_args[0][0], Trace)
225225

226226

227+
@pytest.mark.asyncio
228+
async def test_executor_stream_with_trace_records_metrics_on_interrupt(
229+
executor, agent, tool_results, cycle_trace, cycle_span, invocation_state, alist
230+
):
231+
"""Interrupted tool calls are recorded in metrics and cycle_trace with no message attached (issue #1063)."""
232+
tool_use: ToolUse = {"name": "interrupt_tool", "toolUseId": "test_tool_id", "input": {}}
233+
stream = executor._stream_with_trace(agent, tool_use, tool_results, cycle_trace, cycle_span, invocation_state)
234+
235+
await alist(stream)
236+
237+
agent.event_loop_metrics.add_tool_usage.assert_called_once()
238+
call_args = agent.event_loop_metrics.add_tool_usage.call_args
239+
tool_arg, _duration, trace_arg, success_arg, *rest = call_args.args
240+
assert tool_arg == tool_use
241+
assert success_arg is False
242+
assert rest == [] and "message" not in call_args.kwargs
243+
assert isinstance(trace_arg, Trace)
244+
cycle_trace.add_child.assert_called_once_with(trace_arg)
245+
246+
227247
@pytest.mark.parametrize(
228248
("cancel_tool", "cancel_message"),
229249
[(True, "tool cancelled by user"), ("user cancel message", "user cancel message")],

0 commit comments

Comments
 (0)