Skip to content

Commit 768a791

Browse files
committed
feat(agent): Add opt-in flag to include tool specs
1 parent 89bab98 commit 768a791

File tree

3 files changed

+112
-13
lines changed

3 files changed

+112
-13
lines changed

src/strands/agent/agent.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -928,7 +928,7 @@ def _start_agent_trace_span(self, messages: Messages) -> trace_api.Span:
928928
"""Starts a trace span for the agent.
929929
930930
Args:
931-
messages: The input messages.
931+
messages: The input messages.
932932
"""
933933
model_id = self.model.config.get("model_id") if hasattr(self.model, "config") else None
934934
return self.tracer.start_agent_span(
@@ -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: 80 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,82 @@ 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+
2249+
def test_agent_does_not_include_tools_in_trace_by_default(tool_decorated, monkeypatch):
2250+
"""Verify that by default, the agent does not add tool specs to the trace."""
2251+
monkeypatch.setenv("OTEL_SEMCONV_STABILITY_OPT_IN", "")
2252+
with unittest.mock.patch("strands.agent.agent.get_tracer") as mock_get_tracer:
2253+
# We need to re-import the tracer to pick up the new env var
2254+
import importlib
2255+
2256+
from strands.telemetry import tracer
2257+
2258+
importlib.reload(tracer)
2259+
2260+
mock_tracer_instance = tracer.Tracer()
2261+
mock_span = unittest.mock.MagicMock()
2262+
mock_tracer_instance.start_agent_span = unittest.mock.MagicMock(return_value=mock_span)
2263+
mock_get_tracer.return_value = mock_tracer_instance
2264+
2265+
mock_model = MockedModelProvider([{"role": "assistant", "content": [{"text": "hello!"}]}])
2266+
2267+
agent = Agent(tools=[tool_decorated], model=mock_model)
2268+
agent("test prompt")
2269+
2270+
# Check that set_attribute was not called for our specific key
2271+
called_attributes = [call.args[0] for call in mock_span.set_attribute.call_args_list]
2272+
assert "gen_ai.tool.definitions" not in called_attributes
2273+
2274+
2275+
def test_agent_includes_tools_in_trace_when_enabled(tool_decorated, monkeypatch):
2276+
"""Verify that the agent adds tool specs to the trace when the flag is enabled."""
2277+
monkeypatch.setenv("OTEL_SEMCONV_STABILITY_OPT_IN", "gen_ai_tool_definitions")
2278+
with unittest.mock.patch("strands.agent.agent.get_tracer") as mock_get_tracer:
2279+
# We need to re-import the tracer to pick up the new env var
2280+
import importlib
2281+
2282+
from strands.telemetry import tracer
2283+
2284+
importlib.reload(tracer)
2285+
2286+
# Create a REAL tracer instance so its logic runs
2287+
mock_tracer_instance = tracer.Tracer()
2288+
mock_span = unittest.mock.MagicMock()
2289+
2290+
# Mock the lower-level _start_span method, NOT start_agent_span
2291+
mock_tracer_instance._start_span = unittest.mock.MagicMock(return_value=mock_span)
2292+
mock_get_tracer.return_value = mock_tracer_instance
2293+
2294+
mock_model = MockedModelProvider([{"role": "assistant", "content": [{"text": "hello!"}]}])
2295+
2296+
agent = Agent(tools=[tool_decorated], model=mock_model)
2297+
agent("test prompt")
2298+
2299+
# Now, we assert that _start_span was called, and we inspect the
2300+
# attributes that were passed to it.
2301+
mock_tracer_instance._start_span.assert_called_once()
2302+
call_args, call_kwargs = mock_tracer_instance._start_span.call_args
2303+
passed_attributes = call_kwargs.get("attributes", {})
2304+
2305+
# Construct the data we expect to find in the attributes
2306+
tool_spec = tool_decorated.tool_spec
2307+
expected_tool_details = [
2308+
{
2309+
"name": tool_spec.get("name"),
2310+
"description": tool_spec.get("description"),
2311+
"inputSchema": tool_spec.get("inputSchema"),
2312+
"outputSchema": tool_spec.get("outputSchema"),
2313+
}
2314+
]
2315+
# The final code serializes the data to pass mypy checks, so the test must expect a string.
2316+
expected_json = serialize(expected_tool_details)
2317+
2318+
# Verify that our tool definitions are present in the attributes passed to the span
2319+
assert "gen_ai.tool.definitions" in passed_attributes
2320+
assert passed_attributes["gen_ai.tool.definitions"] == expected_json
2321+
2322+
22452323
@pytest.mark.parametrize(
22462324
"content, expected",
22472325
[

0 commit comments

Comments
 (0)