Skip to content

Commit a2c89e2

Browse files
committed
feat(telemetry): Add tool definitions to traces via semconv opt-in
1 parent 89bab98 commit a2c89e2

File tree

4 files changed

+101
-12
lines changed

4 files changed

+101
-12
lines changed

src/strands/agent/agent.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -938,6 +938,7 @@ def _start_agent_trace_span(self, messages: Messages) -> trace_api.Span:
938938
tools=self.tool_names,
939939
system_prompt=self.system_prompt,
940940
custom_trace_attributes=self.trace_attributes,
941+
tools_config=self.tool_registry.get_all_tools_config(),
941942
)
942943

943944
def _end_agent_trace_span(

src/strands/telemetry/tracer.py

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,16 @@ class Tracer:
7979
8080
When the OTEL_EXPORTER_OTLP_ENDPOINT environment variable is set, traces
8181
are sent to the OTLP endpoint.
82+
83+
Attributes:
84+
use_latest_genai_conventions: If True, uses the latest experimental GenAI semantic conventions.
85+
include_tool_definitions: If True, includes detailed tool definitions in the agent trace span.
86+
87+
Both attributes are controlled by including "gen_ai_latest_experimental" or "gen_ai_tool_definitions",
88+
respectively, in the OTEL_SEMCONV_STABILITY_OPT_IN environment variable.
8289
"""
8390

84-
def __init__(
85-
self,
86-
) -> None:
91+
def __init__(self) -> None:
8792
"""Initialize the tracer."""
8893
self.service_name = __name__
8994
self.tracer_provider: Optional[trace_api.TracerProvider] = None
@@ -92,17 +97,18 @@ def __init__(
9297
ThreadingInstrumentor().instrument()
9398

9499
# Read OTEL_SEMCONV_STABILITY_OPT_IN environment variable
95-
self.use_latest_genai_conventions = self._parse_semconv_opt_in()
100+
opt_in_values = self._parse_semconv_opt_in()
101+
self.use_latest_genai_conventions = "gen_ai_latest_experimental" in opt_in_values
102+
self.include_tool_definitions = "gen_ai_tool_definitions" in opt_in_values
96103

97-
def _parse_semconv_opt_in(self) -> bool:
104+
def _parse_semconv_opt_in(self) -> set[str]:
98105
"""Parse the OTEL_SEMCONV_STABILITY_OPT_IN environment variable.
99106
100107
Returns:
101-
Set of opt-in values from the environment variable
108+
A set of opt-in values from the environment variable.
102109
"""
103110
opt_in_env = os.getenv("OTEL_SEMCONV_STABILITY_OPT_IN", "")
104-
105-
return "gen_ai_latest_experimental" in opt_in_env
111+
return {value.strip() for value in opt_in_env.split(",")}
106112

107113
def _start_span(
108114
self,
@@ -551,6 +557,7 @@ def start_agent_span(
551557
model_id: Optional[str] = None,
552558
tools: Optional[list] = None,
553559
custom_trace_attributes: Optional[Mapping[str, AttributeValue]] = None,
560+
tools_config: Optional[dict] = None,
554561
**kwargs: Any,
555562
) -> Span:
556563
"""Start a new span for an agent invocation.
@@ -561,6 +568,7 @@ def start_agent_span(
561568
model_id: Optional model identifier.
562569
tools: Optional list of tools being used.
563570
custom_trace_attributes: Optional mapping of custom trace attributes to include in the span.
571+
tools_config: Optional dictionary of tool configurations.
564572
**kwargs: Additional attributes to add to the span.
565573
566574
Returns:
@@ -577,8 +585,15 @@ def start_agent_span(
577585
attributes["gen_ai.request.model"] = model_id
578586

579587
if tools:
580-
tools_json = serialize(tools)
581-
attributes["gen_ai.agent.tools"] = tools_json
588+
attributes["gen_ai.agent.tools"] = serialize(tools)
589+
590+
if self.include_tool_definitions and tools_config:
591+
try:
592+
tool_definitions = self._construct_tool_definitions(tools_config)
593+
attributes["gen_ai.tool.definitions"] = serialize(tool_definitions)
594+
except Exception:
595+
# A failure in telemetry should not crash the agent
596+
logger.warning("failed to attach tool metadata to agent span", exc_info=True)
582597

583598
# Add custom trace attributes if provided
584599
if custom_trace_attributes:
@@ -649,6 +664,18 @@ def end_agent_span(
649664

650665
self._end_span(span, attributes, error)
651666

667+
def _construct_tool_definitions(self, tools_config: dict) -> list[dict[str, Any]]:
668+
"""Constructs a list of tool definitions from the provided tools_config."""
669+
return [
670+
{
671+
"name": name,
672+
"description": spec.get("description"),
673+
"inputSchema": spec.get("inputSchema"),
674+
"outputSchema": spec.get("outputSchema"),
675+
}
676+
for name, spec in tools_config.items()
677+
]
678+
652679
def start_multiagent_span(
653680
self,
654681
task: str | list[ContentBlock],

tests/strands/agent/test_agent.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1360,6 +1360,7 @@ def test_agent_call_creates_and_ends_span_on_success(mock_get_tracer, mock_model
13601360
tools=agent.tool_names,
13611361
system_prompt=agent.system_prompt,
13621362
custom_trace_attributes=agent.trace_attributes,
1363+
tools_config=unittest.mock.ANY,
13631364
)
13641365

13651366
# Verify span was ended with the result
@@ -1394,6 +1395,7 @@ async def test_event_loop(*args, **kwargs):
13941395
tools=agent.tool_names,
13951396
system_prompt=agent.system_prompt,
13961397
custom_trace_attributes=agent.trace_attributes,
1398+
tools_config=unittest.mock.ANY,
13971399
)
13981400

13991401
expected_response = AgentResult(
@@ -1432,6 +1434,7 @@ def test_agent_call_creates_and_ends_span_on_exception(mock_get_tracer, mock_mod
14321434
tools=agent.tool_names,
14331435
system_prompt=agent.system_prompt,
14341436
custom_trace_attributes=agent.trace_attributes,
1437+
tools_config=unittest.mock.ANY,
14351438
)
14361439

14371440
# Verify span was ended with the exception
@@ -1468,6 +1471,7 @@ async def test_agent_stream_async_creates_and_ends_span_on_exception(mock_get_tr
14681471
tools=agent.tool_names,
14691472
system_prompt=agent.system_prompt,
14701473
custom_trace_attributes=agent.trace_attributes,
1474+
tools_config=unittest.mock.ANY,
14711475
)
14721476

14731477
# Verify span was ended with the exception
@@ -2240,8 +2244,8 @@ def test_agent_backwards_compatibility_single_text_block():
22402244

22412245
# Should extract text for backwards compatibility
22422246
assert agent.system_prompt == text
2243-
2244-
2247+
2248+
22452249
@pytest.mark.parametrize(
22462250
"content, expected",
22472251
[

tests/strands/telemetry/test_tracer.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1324,3 +1324,60 @@ def test_start_event_loop_cycle_span_with_tool_result_message(mock_tracer):
13241324
"gen_ai.tool.message", attributes={"content": json.dumps(messages[0]["content"])}
13251325
)
13261326
assert span is not None
1327+
1328+
1329+
def test_start_agent_span_does_not_include_tool_definitions_by_default():
1330+
"""Verify that start_agent_span does not include tool definitions by default."""
1331+
tracer = Tracer()
1332+
tracer.include_tool_definitions = False
1333+
tracer._start_span = mock.MagicMock()
1334+
1335+
tools_config = {
1336+
"my_tool": {
1337+
"name": "my_tool",
1338+
"description": "A test tool",
1339+
"inputSchema": {"json": {}},
1340+
"outputSchema": {"json": {}},
1341+
}
1342+
}
1343+
1344+
tracer.start_agent_span(messages=[], agent_name="TestAgent", tools_config=tools_config)
1345+
1346+
tracer._start_span.assert_called_once()
1347+
_, call_kwargs = tracer._start_span.call_args
1348+
attributes = call_kwargs.get("attributes", {})
1349+
assert "gen_ai.tool.definitions" not in attributes
1350+
1351+
1352+
def test_start_agent_span_includes_tool_definitions_when_enabled():
1353+
"""Verify that start_agent_span includes tool definitions when enabled."""
1354+
tracer = Tracer()
1355+
tracer.include_tool_definitions = True
1356+
tracer._start_span = mock.MagicMock()
1357+
1358+
tools_config = {
1359+
"my_tool": {
1360+
"name": "my_tool",
1361+
"description": "A test tool",
1362+
"inputSchema": {"json": {"type": "object", "properties": {}}},
1363+
"outputSchema": {"json": {"type": "object", "properties": {}}},
1364+
}
1365+
}
1366+
1367+
tracer.start_agent_span(messages=[], agent_name="TestAgent", tools_config=tools_config)
1368+
1369+
tracer._start_span.assert_called_once()
1370+
_, call_kwargs = tracer._start_span.call_args
1371+
attributes = call_kwargs.get("attributes", {})
1372+
1373+
assert "gen_ai.tool.definitions" in attributes
1374+
expected_tool_details = [
1375+
{
1376+
"name": "my_tool",
1377+
"description": "A test tool",
1378+
"inputSchema": {"json": {"type": "object", "properties": {}}},
1379+
"outputSchema": {"json": {"type": "object", "properties": {}}},
1380+
}
1381+
]
1382+
expected_json = serialize(expected_tool_details)
1383+
assert attributes["gen_ai.tool.definitions"] == expected_json

0 commit comments

Comments
 (0)