Skip to content

Commit 6bd617e

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

File tree

4 files changed

+94
-12
lines changed

4 files changed

+94
-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: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,7 @@ class Tracer:
8181
are sent to the OTLP endpoint.
8282
"""
8383

84-
def __init__(
85-
self,
86-
) -> None:
84+
def __init__(self) -> None:
8785
"""Initialize the tracer."""
8886
self.service_name = __name__
8987
self.tracer_provider: Optional[trace_api.TracerProvider] = None
@@ -92,17 +90,18 @@ def __init__(
9290
ThreadingInstrumentor().instrument()
9391

9492
# Read OTEL_SEMCONV_STABILITY_OPT_IN environment variable
95-
self.use_latest_genai_conventions = self._parse_semconv_opt_in()
93+
opt_in_values = self._parse_semconv_opt_in()
94+
self.use_latest_genai_conventions = "gen_ai_latest_experimental" in opt_in_values
95+
self.include_tool_definitions = "gen_ai_tool_definitions" in opt_in_values
9696

97-
def _parse_semconv_opt_in(self) -> bool:
97+
def _parse_semconv_opt_in(self) -> set[str]:
9898
"""Parse the OTEL_SEMCONV_STABILITY_OPT_IN environment variable.
9999
100100
Returns:
101-
Set of opt-in values from the environment variable
101+
A set of opt-in values from the environment variable.
102102
"""
103103
opt_in_env = os.getenv("OTEL_SEMCONV_STABILITY_OPT_IN", "")
104-
105-
return "gen_ai_latest_experimental" in opt_in_env
104+
return {value.strip() for value in opt_in_env.split(",")}
106105

107106
def _start_span(
108107
self,
@@ -551,6 +550,7 @@ def start_agent_span(
551550
model_id: Optional[str] = None,
552551
tools: Optional[list] = None,
553552
custom_trace_attributes: Optional[Mapping[str, AttributeValue]] = None,
553+
tools_config: Optional[dict] = None,
554554
**kwargs: Any,
555555
) -> Span:
556556
"""Start a new span for an agent invocation.
@@ -561,6 +561,7 @@ def start_agent_span(
561561
model_id: Optional model identifier.
562562
tools: Optional list of tools being used.
563563
custom_trace_attributes: Optional mapping of custom trace attributes to include in the span.
564+
tools_config: Optional dictionary of tool configurations.
564565
**kwargs: Additional attributes to add to the span.
565566
566567
Returns:
@@ -577,8 +578,15 @@ def start_agent_span(
577578
attributes["gen_ai.request.model"] = model_id
578579

579580
if tools:
580-
tools_json = serialize(tools)
581-
attributes["gen_ai.agent.tools"] = tools_json
581+
attributes["gen_ai.agent.tools"] = serialize(tools)
582+
583+
if self.include_tool_definitions and tools_config:
584+
try:
585+
tool_definitions = self._construct_tool_definitions(tools_config)
586+
attributes["gen_ai.tool.definitions"] = serialize(tool_definitions)
587+
except Exception:
588+
# A failure in telemetry should not crash the agent
589+
logger.warning("failed to attach tool metadata to agent span", exc_info=True)
582590

583591
# Add custom trace attributes if provided
584592
if custom_trace_attributes:
@@ -649,6 +657,18 @@ def end_agent_span(
649657

650658
self._end_span(span, attributes, error)
651659

660+
def _construct_tool_definitions(self, tools_config: dict) -> list[dict[str, Any]]:
661+
"""Constructs a list of tool definitions from the provided tools_config."""
662+
return [
663+
{
664+
"name": name,
665+
"description": spec.get("description"),
666+
"inputSchema": spec.get("inputSchema"),
667+
"outputSchema": spec.get("outputSchema"),
668+
}
669+
for name, spec in tools_config.items()
670+
]
671+
652672
def start_multiagent_span(
653673
self,
654674
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)