Skip to content

Commit 92cb4ba

Browse files
authored
feat(strands): add agents integration (#346)
Trace Strands agent, event loop, model, and tool lifecycle spans by mirroring the framework's native tracer hooks without requiring users to enable OTEL. Add Strands version matrix coverage, auto-instrumentation wiring, and VCR-backed tests for latest and 1.20.0. Store Strands token usage under metadata.strands_usage to avoid double-counting provider token metrics. resolves #337
1 parent 84d98ff commit 92cb4ba

12 files changed

Lines changed: 1526 additions & 828 deletions

py/noxfile.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,18 @@ def test_agno(session, version):
247247
_run_tests(session, f"{INTEGRATION_DIR}/agno/test_workflow.py", version=version)
248248

249249

250+
STRANDS_VERSIONS = _get_matrix_versions("strands-agents")
251+
252+
253+
@nox.session()
254+
@nox.parametrize("version", STRANDS_VERSIONS, ids=STRANDS_VERSIONS)
255+
def test_strands(session, version):
256+
_install_test_deps(session)
257+
_install_matrix_dep(session, "strands-agents", version)
258+
_install_group_locked(session, "test-strands")
259+
_run_tests(session, f"{INTEGRATION_DIR}/strands/test_strands.py", version=version)
260+
261+
250262
AGENTSCOPE_VERSIONS = _get_matrix_versions("agentscope")
251263

252264

py/pyproject.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ test-agentscope = [
133133
"openai==2.31.0",
134134
]
135135

136+
test-strands = [
137+
{include-group = "test"},
138+
"openai==2.32.0",
139+
]
140+
136141
test-pydantic-ai-logfire = [
137142
{include-group = "test"},
138143
"logfire==4.32.1",
@@ -237,6 +242,7 @@ conflicts = [
237242
{group = "test-crewai"},
238243
{group = "test-agno"},
239244
{group = "test-agentscope"},
245+
{group = "test-strands"},
240246
{group = "test-langchain"},
241247
{group = "lint"},
242248
],
@@ -305,6 +311,10 @@ latest = "autogen-agentchat==0.7.5"
305311
latest = "autogen-ext[openai]==0.7.5"
306312
"0.7.0" = "autogen-ext[openai]==0.7.0"
307313

314+
[tool.braintrust.matrix.strands-agents]
315+
latest = "strands-agents==1.37.0"
316+
"1.20.0" = "strands-agents==1.20.0"
317+
308318
[tool.braintrust.matrix.pydantic-ai-integration]
309319
latest = "pydantic-ai==1.86.1"
310320
"1.10.0" = "pydantic-ai==1.10.0"
@@ -396,6 +406,7 @@ openai = ["openai"]
396406
openai_agents = ["openai-agents"]
397407
openrouter = ["openrouter"]
398408
pydantic_ai = ["pydantic-ai-integration", "pydantic-ai-wrap-openai"]
409+
strands = ["strands-agents"]
399410

400411
[tool.braintrust.vendor-packages]
401412
agno = "agno"
@@ -415,4 +426,5 @@ mistralai = "mistralai"
415426
openai = "openai"
416427
openai-agents = "agents"
417428
openrouter = "openrouter"
429+
strands-agents = "strands"
418430
temporalio = "temporalio"

py/src/braintrust/auto.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
OpenAIIntegration,
2626
OpenRouterIntegration,
2727
PydanticAIIntegration,
28+
StrandsIntegration,
2829
)
2930
from braintrust.integrations.base import BaseIntegration
3031

@@ -64,6 +65,7 @@ def auto_instrument(
6465
cohere: bool = True,
6566
autogen: bool = True,
6667
crewai: bool = True,
68+
strands: bool = True,
6769
) -> dict[str, bool]:
6870
"""
6971
Auto-instrument supported AI/ML libraries for Braintrust tracing.
@@ -92,6 +94,7 @@ def auto_instrument(
9294
cohere: Enable Cohere instrumentation (default: True)
9395
autogen: Enable AutoGen instrumentation (default: True)
9496
crewai: Enable CrewAI instrumentation (default: True)
97+
strands: Enable Strands Agents instrumentation (default: True)
9598
9699
Returns:
97100
Dict mapping integration name to whether it was successfully instrumented.
@@ -173,6 +176,8 @@ def auto_instrument(
173176
results["autogen"] = _instrument_integration(AutoGenIntegration)
174177
if crewai:
175178
results["crewai"] = _instrument_integration(CrewAIIntegration)
179+
if strands:
180+
results["strands"] = _instrument_integration(StrandsIntegration)
176181

177182
return results
178183

py/src/braintrust/integrations/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from .openai_agents import OpenAIAgentsIntegration
1616
from .openrouter import OpenRouterIntegration
1717
from .pydantic_ai import PydanticAIIntegration
18+
from .strands import StrandsIntegration
1819

1920

2021
__all__ = [
@@ -35,4 +36,5 @@
3536
"OpenAIAgentsIntegration",
3637
"OpenRouterIntegration",
3738
"PydanticAIIntegration",
39+
"StrandsIntegration",
3840
]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""Braintrust integration for Strands Agents."""
2+
3+
from braintrust.logger import NOOP_SPAN, current_span, init_logger
4+
5+
from .integration import StrandsIntegration
6+
from .patchers import wrap_strands_tracer
7+
8+
9+
__all__ = ["StrandsIntegration", "setup_strands", "wrap_strands_tracer"]
10+
11+
12+
def setup_strands(
13+
api_key: str | None = None,
14+
project_id: str | None = None,
15+
project_name: str | None = None,
16+
) -> bool:
17+
"""Set up Braintrust tracing for Strands Agents."""
18+
span = current_span()
19+
if span == NOOP_SPAN:
20+
init_logger(project=project_name, api_key=api_key, project_id=project_id)
21+
22+
return StrandsIntegration.setup()
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
interactions:
2+
- request:
3+
body: '{"messages":[{"role":"system","content":"Answer with one short sentence."},{"role":"user","content":[{"text":"What
4+
is 2 + 2?","type":"text"}]}],"model":"gpt-4o-mini","max_tokens":16,"stream":true,"stream_options":{"include_usage":true},"temperature":0,"tools":[]}'
5+
headers:
6+
Accept:
7+
- application/json
8+
Accept-Encoding:
9+
- gzip, deflate
10+
Connection:
11+
- keep-alive
12+
Content-Length:
13+
- '263'
14+
Content-Type:
15+
- application/json
16+
Host:
17+
- api.openai.com
18+
User-Agent:
19+
- AsyncOpenAI/Python 2.32.0
20+
X-Stainless-Arch:
21+
- arm64
22+
X-Stainless-Async:
23+
- async:asyncio
24+
X-Stainless-Lang:
25+
- python
26+
X-Stainless-OS:
27+
- MacOS
28+
X-Stainless-Package-Version:
29+
- 2.32.0
30+
X-Stainless-Runtime:
31+
- CPython
32+
X-Stainless-Runtime-Version:
33+
- 3.12.12
34+
x-stainless-read-timeout:
35+
- '600'
36+
x-stainless-retry-count:
37+
- '0'
38+
method: POST
39+
uri: https://api.openai.com/v1/chat/completions
40+
response:
41+
body:
42+
string: 'data: {"id":"chatcmpl-DYGoLJoFHNxxTp5Dlc6rejRQywFLq","object":"chat.completion.chunk","created":1777060145,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_918210d279","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"pk97oOD3G"}
43+
44+
45+
data: {"id":"chatcmpl-DYGoLJoFHNxxTp5Dlc6rejRQywFLq","object":"chat.completion.chunk","created":1777060145,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_918210d279","choices":[{"index":0,"delta":{"content":"2"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"FWoXAmqq5w"}
46+
47+
48+
data: {"id":"chatcmpl-DYGoLJoFHNxxTp5Dlc6rejRQywFLq","object":"chat.completion.chunk","created":1777060145,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_918210d279","choices":[{"index":0,"delta":{"content":"
49+
+"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"lhqvL5rG8"}
50+
51+
52+
data: {"id":"chatcmpl-DYGoLJoFHNxxTp5Dlc6rejRQywFLq","object":"chat.completion.chunk","created":1777060145,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_918210d279","choices":[{"index":0,"delta":{"content":"
53+
"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"XR35oXmsEL"}
54+
55+
56+
data: {"id":"chatcmpl-DYGoLJoFHNxxTp5Dlc6rejRQywFLq","object":"chat.completion.chunk","created":1777060145,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_918210d279","choices":[{"index":0,"delta":{"content":"2"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"jgY9365oSi"}
57+
58+
59+
data: {"id":"chatcmpl-DYGoLJoFHNxxTp5Dlc6rejRQywFLq","object":"chat.completion.chunk","created":1777060145,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_918210d279","choices":[{"index":0,"delta":{"content":"
60+
equals"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"xGU2"}
61+
62+
63+
data: {"id":"chatcmpl-DYGoLJoFHNxxTp5Dlc6rejRQywFLq","object":"chat.completion.chunk","created":1777060145,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_918210d279","choices":[{"index":0,"delta":{"content":"
64+
"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"r0m78JOjNz"}
65+
66+
67+
data: {"id":"chatcmpl-DYGoLJoFHNxxTp5Dlc6rejRQywFLq","object":"chat.completion.chunk","created":1777060145,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_918210d279","choices":[{"index":0,"delta":{"content":"4"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"J2Zqjb6BCe"}
68+
69+
70+
data: {"id":"chatcmpl-DYGoLJoFHNxxTp5Dlc6rejRQywFLq","object":"chat.completion.chunk","created":1777060145,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_918210d279","choices":[{"index":0,"delta":{"content":"."},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"IPWvGvSjYe"}
71+
72+
73+
data: {"id":"chatcmpl-DYGoLJoFHNxxTp5Dlc6rejRQywFLq","object":"chat.completion.chunk","created":1777060145,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_918210d279","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null,"obfuscation":"oRbJv"}
74+
75+
76+
data: {"id":"chatcmpl-DYGoLJoFHNxxTp5Dlc6rejRQywFLq","object":"chat.completion.chunk","created":1777060145,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_918210d279","choices":[],"usage":{"prompt_tokens":37,"completion_tokens":9,"total_tokens":46,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"avqwL2vQhJB"}
77+
78+
79+
data: [DONE]
80+
81+
82+
'
83+
headers:
84+
CF-Cache-Status:
85+
- DYNAMIC
86+
CF-Ray:
87+
- 9f17a112c997175c-YYZ
88+
Connection:
89+
- keep-alive
90+
Content-Type:
91+
- text/event-stream; charset=utf-8
92+
Date:
93+
- Fri, 24 Apr 2026 19:49:06 GMT
94+
Server:
95+
- cloudflare
96+
Strict-Transport-Security:
97+
- max-age=31536000; includeSubDomains; preload
98+
Transfer-Encoding:
99+
- chunked
100+
X-Content-Type-Options:
101+
- nosniff
102+
access-control-expose-headers:
103+
- X-Request-ID
104+
alt-svc:
105+
- h3=":443"; ma=86400
106+
openai-organization:
107+
- braintrust-data
108+
openai-processing-ms:
109+
- '1121'
110+
openai-project:
111+
- proj_vsCSXafhhByzWOThMrJcZiw9
112+
openai-version:
113+
- '2020-10-01'
114+
set-cookie:
115+
- __cf_bm=CRLtmVWDCbvpssTphfkvEbY8e9EYpEgUvoYSE1e3sX8-1777060145.090192-1.0.1.1-9bJCZxU874KR6eEDb6WL48mIvH3drpQUnSaYDjVfPVmXXr43ttzr3vlIV_26.Ad.fnRRJQ21PoVv47xzxY0UczM56WXgcDc1TvsIRWAA_3axHYFsDOUVM087u8AxC.VJ;
116+
HttpOnly; Secure; Path=/; Domain=api.openai.com; Expires=Fri, 24 Apr 2026
117+
20:19:06 GMT
118+
x-openai-proxy-wasm:
119+
- v0.1
120+
x-ratelimit-limit-requests:
121+
- '30000'
122+
x-ratelimit-limit-tokens:
123+
- '150000000'
124+
x-ratelimit-remaining-requests:
125+
- '29999'
126+
x-ratelimit-remaining-tokens:
127+
- '149999985'
128+
x-ratelimit-reset-requests:
129+
- 2ms
130+
x-ratelimit-reset-tokens:
131+
- 0s
132+
x-request-id:
133+
- req_91fceca254ef423881441fdc4636c6aa
134+
status:
135+
code: 200
136+
message: OK
137+
version: 1

0 commit comments

Comments
 (0)