Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions tests/test_tracing_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import json
from dataclasses import dataclass

import pytest

from opentelemetry import context as context_api
from opentelemetry.sdk import trace as trace_sdk

Expand Down Expand Up @@ -309,6 +311,39 @@ def test_trace_tool_call_skips_content_when_env_false(monkeypatch):
assert "gen_ai.output" not in span.attributes


class _DeferredResponseTool(_FakeTool):
# Mirrors ADK's task-delegation tool, whose response the framework
# synthesizes after the sub-agent finishes.
_defers_response = True


class _LongRunningTool(_FakeTool):
is_long_running = True


@pytest.mark.parametrize("tool", [_DeferredResponseTool(), _LongRunningTool()])
def test_trace_tool_call_marks_a_deferred_response_as_pending(monkeypatch, tool):
monkeypatch.setenv("OBSERVABILITY_OPENTELEMETRY_TRACE_CONTENT", "true")

with _start_test_span("execute_tool lookup") as span:
telemetry.trace_tool_call(tool, {"request": "check"}, None)

for name in ("gen_ai.tool.output", "cozeloop.output", "gen_ai.output"):
assert json.loads(span.attributes[name]) == {"status": "pending"}


def test_trace_tool_call_keeps_unknown_output_when_a_response_is_missing(
monkeypatch,
):
monkeypatch.setenv("OBSERVABILITY_OPENTELEMETRY_TRACE_CONTENT", "true")

with _start_test_span("execute_tool lookup") as span:
telemetry.trace_tool_call(_FakeTool(), {"query": "q"}, None)

for name in ("gen_ai.tool.output", "cozeloop.output", "gen_ai.output"):
assert span.attributes[name] == "<unknown_tool_output>"


def test_apmplus_tool_metrics_skip_token_usage_when_tool_content_missing():
metric_recorder = object.__new__(PortalMetricRecorder)
metric_recorder.apmplus_span_latency = _FakeMetricRecorder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ def tool_gen_ai_tool_name(params: ToolAttributesParams) -> ExtractorResponse:
return ExtractorResponse(content=params.tool.name or "<unknown_tool_name>")


def _responds_later(tool) -> bool:
"""Whether a tool's response can arrive after its call returns."""
return bool(
getattr(tool, "is_long_running", False)
or getattr(tool, "_defers_response", False)
)


def tool_gen_ai_tool_output(params: ToolAttributesParams) -> ExtractorResponse:
"""Extract tool output data for span attributes.

Expand All @@ -125,10 +133,18 @@ def tool_gen_ai_tool_output(params: ToolAttributesParams) -> ExtractorResponse:
params: Tool execution parameters containing function response event

Returns:
ExtractorResponse: Response containing JSON serialized tool output data
ExtractorResponse: Response containing JSON serialized tool output data,
``{"status": "pending"}`` when a long-running or deferred-response
tool has not responded yet
"""
function_responses = get_event_function_responses(params.function_response_event)
if not function_responses:
if _responds_later(params.tool):
# The call was accepted and its response arrives later (a
# long-running tool, or ADK's task delegation, whose response is
# synthesized once the sub-agent finishes): record that state
# instead of an output that looks missing.
return ExtractorResponse(content=safe_json_serialize({"status": "pending"}))
return ExtractorResponse(content="<unknown_tool_output>")
function_response_obj = function_responses[0]
if hasattr(function_response_obj, "model_dump"):
Expand Down