Skip to content

Commit 87d6ccb

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

File tree

3 files changed

+106
-12
lines changed

3 files changed

+106
-12
lines changed

src/strands/agent/agent.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -660,7 +660,10 @@ async def stream_async(
660660
# Process input and get message to add (if any)
661661
messages = self._convert_prompt_to_messages(prompt)
662662

663-
self.trace_span = self._start_agent_trace_span(messages)
663+
self.trace_span = self._start_agent_trace_span(
664+
messages,
665+
tools_config=self.tool_registry.get_all_tools_config(),
666+
)
664667

665668
with trace_api.use_span(self.trace_span):
666669
try:
@@ -924,14 +927,15 @@ def _record_tool_execution(
924927
self._append_message(tool_result_msg)
925928
self._append_message(assistant_msg)
926929

927-
def _start_agent_trace_span(self, messages: Messages) -> trace_api.Span:
930+
def _start_agent_trace_span(self, messages: Messages, tools_config: Optional[dict] = None) -> trace_api.Span:
928931
"""Starts a trace span for the agent.
929932
930933
Args:
931-
messages: The input messages.
934+
messages: The input messages.
935+
tools_config: Optional dictionary of tool configurations.
932936
"""
933937
model_id = self.model.config.get("model_id") if hasattr(self.model, "config") else None
934-
return self.tracer.start_agent_span(
938+
span = self.tracer.start_agent_span(
935939
messages=messages,
936940
agent_name=self.name,
937941
model_id=model_id,
@@ -940,6 +944,11 @@ def _start_agent_trace_span(self, messages: Messages) -> trace_api.Span:
940944
custom_trace_attributes=self.trace_attributes,
941945
)
942946

947+
if tools_config:
948+
self.tracer.add_tool_definitions_to_span(span, tools_config)
949+
950+
return span
951+
943952
def _end_agent_trace_span(
944953
self,
945954
response: Optional[AgentResult] = None,

src/strands/telemetry/tracer.py

Lines changed: 31 additions & 8 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,
@@ -649,6 +648,30 @@ def end_agent_span(
649648

650649
self._end_span(span, attributes, error)
651650

651+
def add_tool_definitions_to_span(self, span: Span, tools_config: dict) -> None:
652+
"""Adds tool definitions to a span.
653+
654+
Args:
655+
span: The span to add the definitions to.
656+
tools_config: The tool configurations.
657+
"""
658+
if self.include_tool_definitions:
659+
try:
660+
tool_details = [
661+
{
662+
"name": name,
663+
"description": spec.get("description"),
664+
"inputSchema": spec.get("inputSchema"),
665+
"outputSchema": spec.get("outputSchema"),
666+
}
667+
for name, spec in tools_config.items()
668+
]
669+
serialized_tools = serialize(tool_details)
670+
span.set_attribute("gen_ai.tool.definitions", serialized_tools)
671+
except Exception:
672+
# A failure in telemetry should not crash the agent
673+
logger.exception("failed to attach tool metadata to agent span")
674+
652675
def start_multiagent_span(
653676
self,
654677
task: str | list[ContentBlock],

tests/strands/agent/test_agent.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2242,6 +2242,68 @@ def test_agent_backwards_compatibility_single_text_block():
22422242
assert agent.system_prompt == text
22432243

22442244

2245+
def test_agent_does_not_include_tools_in_trace_by_default(tool_decorated, monkeypatch):
2246+
"""Verify that by default, the agent does not add tool specs to the trace."""
2247+
monkeypatch.setenv("OTEL_SEMCONV_STABILITY_OPT_IN", "")
2248+
with unittest.mock.patch("strands.agent.agent.get_tracer") as mock_get_tracer:
2249+
# We need to re-import the tracer to pick up the new env var
2250+
import importlib
2251+
2252+
from strands.telemetry import tracer
2253+
2254+
importlib.reload(tracer)
2255+
2256+
mock_tracer_instance = tracer.Tracer()
2257+
mock_span = unittest.mock.MagicMock()
2258+
mock_tracer_instance.start_agent_span = unittest.mock.MagicMock(return_value=mock_span)
2259+
mock_get_tracer.return_value = mock_tracer_instance
2260+
2261+
mock_model = MockedModelProvider([{"role": "assistant", "content": [{"text": "hello!"}]}])
2262+
2263+
agent = Agent(tools=[tool_decorated], model=mock_model)
2264+
agent("test prompt")
2265+
2266+
# Check that set_attribute was not called for our specific key
2267+
called_attributes = [call.args[0] for call in mock_span.set_attribute.call_args_list]
2268+
assert "gen_ai.tool.definitions" not in called_attributes
2269+
2270+
2271+
def test_agent_includes_tools_in_trace_when_enabled(tool_decorated, monkeypatch):
2272+
"""Verify that the agent adds tool specs to the trace when the flag is enabled."""
2273+
monkeypatch.setenv("OTEL_SEMCONV_STABILITY_OPT_IN", "gen_ai_tool_definitions")
2274+
with unittest.mock.patch("strands.agent.agent.get_tracer") as mock_get_tracer:
2275+
# We need to re-import the tracer to pick up the new env var
2276+
import importlib
2277+
2278+
from strands.telemetry import tracer
2279+
2280+
importlib.reload(tracer)
2281+
2282+
mock_tracer_instance = tracer.Tracer()
2283+
mock_span = unittest.mock.MagicMock()
2284+
mock_tracer_instance.start_agent_span = unittest.mock.MagicMock(return_value=mock_span)
2285+
mock_get_tracer.return_value = mock_tracer_instance
2286+
2287+
mock_model = MockedModelProvider([{"role": "assistant", "content": [{"text": "hello!"}]}])
2288+
2289+
agent = Agent(tools=[tool_decorated], model=mock_model)
2290+
agent("test prompt")
2291+
2292+
# Verify the correct data is serialized and set as an attribute
2293+
tool_spec = tool_decorated.tool_spec
2294+
expected_tool_details = [
2295+
{
2296+
"name": tool_spec.get("name"),
2297+
"description": tool_spec.get("description"),
2298+
"inputSchema": tool_spec.get("inputSchema"),
2299+
"outputSchema": tool_spec.get("outputSchema"),
2300+
}
2301+
]
2302+
expected_json = serialize(expected_tool_details)
2303+
2304+
mock_span.set_attribute.assert_any_call("gen_ai.tool.definitions", expected_json)
2305+
2306+
22452307
@pytest.mark.parametrize(
22462308
"content, expected",
22472309
[

0 commit comments

Comments
 (0)