diff --git a/.changeset/silly-nights-walk.md b/.changeset/silly-nights-walk.md new file mode 100644 index 000000000..1ae0dd0db --- /dev/null +++ b/.changeset/silly-nights-walk.md @@ -0,0 +1,5 @@ +--- +"braintrust": minor +--- + +feat: Add support for `@strands-agents/sdk` diff --git a/e2e/config/pr-comment-scenarios.json b/e2e/config/pr-comment-scenarios.json index ee98b2fba..2bfa0dfab 100644 --- a/e2e/config/pr-comment-scenarios.json +++ b/e2e/config/pr-comment-scenarios.json @@ -27,6 +27,12 @@ "metadataScenario": "pi-coding-agent-instrumentation", "variants": [{ "variantKey": "pi-coding-agent-v079", "label": "v0.79" }] }, + { + "scenarioDirName": "strands-agent-sdk-instrumentation", + "label": "Strands Agent SDK Instrumentation", + "metadataScenario": "strands-agent-sdk-instrumentation", + "variants": [{ "variantKey": "strands-agent-sdk-v160", "label": "v1.6.0" }] + }, { "scenarioDirName": "openai-agents-instrumentation", "label": "OpenAI Agents Instrumentation", diff --git a/e2e/scenarios/strands-agent-sdk-instrumentation/__cassettes__/strands-agent-sdk-v160.cassette.json b/e2e/scenarios/strands-agent-sdk-instrumentation/__cassettes__/strands-agent-sdk-v160.cassette.json new file mode 100644 index 000000000..729e3aebc --- /dev/null +++ b/e2e/scenarios/strands-agent-sdk-instrumentation/__cassettes__/strands-agent-sdk-v160.cassette.json @@ -0,0 +1,730 @@ +{ + "entries": [ + { + "callIndex": 0, + "id": "f4cfeb0f63426af9", + "matchKey": "POST api.openai.com/v1/chat/completions", + "recordedAt": "2026-06-18T14:19:59.210Z", + "request": { + "body": { + "kind": "json", + "value": { + "max_completion_tokens": 160, + "messages": [ + { + "content": "You are a deterministic test agent. Always call lookup_weather exactly once for Vienna, then answer exactly STRANDS_AGENT_TOOL_OK.", + "role": "system" + }, + { + "content": [ + { + "text": "Use lookup_weather for Vienna, then answer exactly STRANDS_AGENT_TOOL_OK.", + "type": "text" + } + ], + "role": "user" + } + ], + "model": "gpt-4o-mini-2024-07-18", + "stream": true, + "stream_options": { + "include_usage": true + }, + "temperature": 0, + "tools": [ + { + "function": { + "description": "Return a deterministic weather report for one city.", + "name": "lookup_weather", + "parameters": { + "properties": { + "city": { + "type": "string" + } + }, + "required": ["city"], + "type": "object" + } + }, + "type": "function" + } + ] + } + }, + "headers": {}, + "method": "POST", + "url": "https://api.openai.com/v1/chat/completions" + }, + "response": { + "body": { + "chunks": [ + "data: {\"id\":\"chatcmpl-Ds7t0xMACYpkfXid5mwnMs3P59cxm\",\"object\":\"chat.completion.chunk\",\"created\":1781792398,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_a1aad5391f\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"index\":0,\"id\":\"call_mbDgh0RdovSgQqERgsAGv77i\",\"type\":\"function\",\"function\":{\"name\":\"lookup_weather\",\"arguments\":\"\"}}],\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"d\"}", + "data: {\"id\":\"chatcmpl-Ds7t0xMACYpkfXid5mwnMs3P59cxm\",\"object\":\"chat.completion.chunk\",\"created\":1781792398,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_a1aad5391f\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"{\\\"\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"fhceWzbWvJk1Wl\"}", + "data: {\"id\":\"chatcmpl-Ds7t0xMACYpkfXid5mwnMs3P59cxm\",\"object\":\"chat.completion.chunk\",\"created\":1781792398,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_a1aad5391f\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"city\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"41TlwUxKk60NR\"}", + "data: {\"id\":\"chatcmpl-Ds7t0xMACYpkfXid5mwnMs3P59cxm\",\"object\":\"chat.completion.chunk\",\"created\":1781792398,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_a1aad5391f\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\":\\\"\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"y2vJLhN1HrSi\"}", + "data: {\"id\":\"chatcmpl-Ds7t0xMACYpkfXid5mwnMs3P59cxm\",\"object\":\"chat.completion.chunk\",\"created\":1781792398,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_a1aad5391f\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"Vien\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"SdZayNuwo2w2O\"}", + "data: {\"id\":\"chatcmpl-Ds7t0xMACYpkfXid5mwnMs3P59cxm\",\"object\":\"chat.completion.chunk\",\"created\":1781792398,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_a1aad5391f\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"na\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"ohiNThF0hMH83Ow\"}", + "data: {\"id\":\"chatcmpl-Ds7t0xMACYpkfXid5mwnMs3P59cxm\",\"object\":\"chat.completion.chunk\",\"created\":1781792398,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_a1aad5391f\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\"}\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"4tz04kET3JTwEL\"}", + "data: {\"id\":\"chatcmpl-Ds7t0xMACYpkfXid5mwnMs3P59cxm\",\"object\":\"chat.completion.chunk\",\"created\":1781792398,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_a1aad5391f\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"tool_calls\"}],\"usage\":null,\"obfuscation\":\"msNzf5UbMatL9Vh\"}", + "data: {\"id\":\"chatcmpl-Ds7t0xMACYpkfXid5mwnMs3P59cxm\",\"object\":\"chat.completion.chunk\",\"created\":1781792398,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_a1aad5391f\",\"choices\":[],\"usage\":{\"prompt_tokens\":86,\"completion_tokens\":15,\"total_tokens\":101,\"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\":\"Yk2aPOEJd\"}", + "data: [DONE]" + ], + "kind": "sse" + }, + "headers": { + "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", + "alt-svc": "h3=\":443\"; ma=86400", + "cf-cache-status": "DYNAMIC", + "cf-ray": "a0daee9a0d43c24f-VIE", + "connection": "keep-alive", + "content-type": "text/event-stream; charset=utf-8", + "date": "Thu, 18 Jun 2026 14:19:59 GMT", + "openai-organization": "braintrust-data", + "openai-processing-ms": "569", + "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", + "openai-version": "2020-10-01", + "server": "cloudflare", + "set-cookie": "[REDACTED]", + "strict-transport-security": "max-age=31536000; includeSubDomains; preload", + "transfer-encoding": "chunked", + "x-content-type-options": "nosniff", + "x-openai-proxy-wasm": "v0.1", + "x-ratelimit-limit-requests": "30000", + "x-ratelimit-limit-tokens": "150000000", + "x-ratelimit-remaining-requests": "29999", + "x-ratelimit-remaining-tokens": "149999945", + "x-ratelimit-reset-requests": "2ms", + "x-ratelimit-reset-tokens": "0s", + "x-request-id": "req_ac0653ff238e4af29d4358857853f412" + }, + "status": 200, + "statusText": "OK" + } + }, + { + "callIndex": 1, + "id": "7c32ae1e1e08f309", + "matchKey": "POST api.openai.com/v1/chat/completions", + "recordedAt": "2026-06-18T14:19:59.941Z", + "request": { + "body": { + "kind": "json", + "value": { + "max_completion_tokens": 160, + "messages": [ + { + "content": "You are a deterministic test agent. Always call lookup_weather exactly once for Vienna, then answer exactly STRANDS_AGENT_TOOL_OK.", + "role": "system" + }, + { + "content": [ + { + "text": "Use lookup_weather for Vienna, then answer exactly STRANDS_AGENT_TOOL_OK.", + "type": "text" + } + ], + "role": "user" + }, + { + "content": "", + "role": "assistant", + "tool_calls": [ + { + "function": { + "arguments": "{\"city\":\"Vienna\"}", + "name": "lookup_weather" + }, + "id": "call_mbDgh0RdovSgQqERgsAGv77i", + "type": "function" + } + ] + }, + { + "content": "{\"city\":\"Vienna\",\"forecast\":\"sunny\",\"marker\":\"STRANDS_TOOL_OK\"}", + "role": "tool", + "tool_call_id": "call_mbDgh0RdovSgQqERgsAGv77i" + } + ], + "model": "gpt-4o-mini-2024-07-18", + "stream": true, + "stream_options": { + "include_usage": true + }, + "temperature": 0, + "tools": [ + { + "function": { + "description": "Return a deterministic weather report for one city.", + "name": "lookup_weather", + "parameters": { + "properties": { + "city": { + "type": "string" + } + }, + "required": ["city"], + "type": "object" + } + }, + "type": "function" + } + ] + } + }, + "headers": {}, + "method": "POST", + "url": "https://api.openai.com/v1/chat/completions" + }, + "response": { + "body": { + "chunks": [ + "data: {\"id\":\"chatcmpl-Ds7t1OFfsEw81aE4BxobsBfZHfyMN\",\"object\":\"chat.completion.chunk\",\"created\":1781792399,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_a1aad5391f\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"kVrGlr9Yr\"}", + "data: {\"id\":\"chatcmpl-Ds7t1OFfsEw81aE4BxobsBfZHfyMN\",\"object\":\"chat.completion.chunk\",\"created\":1781792399,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_a1aad5391f\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"STR\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"rBjUFZSw\"}", + "data: {\"id\":\"chatcmpl-Ds7t1OFfsEw81aE4BxobsBfZHfyMN\",\"object\":\"chat.completion.chunk\",\"created\":1781792399,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_a1aad5391f\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"ANDS\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"4mcymcO\"}", + "data: {\"id\":\"chatcmpl-Ds7t1OFfsEw81aE4BxobsBfZHfyMN\",\"object\":\"chat.completion.chunk\",\"created\":1781792399,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_a1aad5391f\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"_AGENT\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"p6WTr\"}", + "data: {\"id\":\"chatcmpl-Ds7t1OFfsEw81aE4BxobsBfZHfyMN\",\"object\":\"chat.completion.chunk\",\"created\":1781792399,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_a1aad5391f\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"_TOOL\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"5hvYdn\"}", + "data: {\"id\":\"chatcmpl-Ds7t1OFfsEw81aE4BxobsBfZHfyMN\",\"object\":\"chat.completion.chunk\",\"created\":1781792399,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_a1aad5391f\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"_OK\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"nyoupvL1\"}", + "data: {\"id\":\"chatcmpl-Ds7t1OFfsEw81aE4BxobsBfZHfyMN\",\"object\":\"chat.completion.chunk\",\"created\":1781792399,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_a1aad5391f\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"usage\":null,\"obfuscation\":\"3Xmze\"}", + "data: {\"id\":\"chatcmpl-Ds7t1OFfsEw81aE4BxobsBfZHfyMN\",\"object\":\"chat.completion.chunk\",\"created\":1781792399,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_a1aad5391f\",\"choices\":[],\"usage\":{\"prompt_tokens\":127,\"completion_tokens\":6,\"total_tokens\":133,\"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\":\"XEQVXM5Sk\"}", + "data: [DONE]" + ], + "kind": "sse" + }, + "headers": { + "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", + "alt-svc": "h3=\":443\"; ma=86400", + "cf-cache-status": "DYNAMIC", + "cf-ray": "a0daee9ffb07c24f-VIE", + "connection": "keep-alive", + "content-type": "text/event-stream; charset=utf-8", + "date": "Thu, 18 Jun 2026 14:19:59 GMT", + "openai-organization": "braintrust-data", + "openai-processing-ms": "310", + "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", + "openai-version": "2020-10-01", + "server": "cloudflare", + "set-cookie": "[REDACTED]", + "strict-transport-security": "max-age=31536000; includeSubDomains; preload", + "transfer-encoding": "chunked", + "x-content-type-options": "nosniff", + "x-openai-proxy-wasm": "v0.1", + "x-ratelimit-limit-requests": "30000", + "x-ratelimit-limit-tokens": "150000000", + "x-ratelimit-remaining-requests": "29999", + "x-ratelimit-remaining-tokens": "149999927", + "x-ratelimit-reset-requests": "2ms", + "x-ratelimit-reset-tokens": "0s", + "x-request-id": "req_94b59424c7104cb79041e8fe3bf23997" + }, + "status": 200, + "statusText": "OK" + } + }, + { + "callIndex": 2, + "id": "7f682068be947c96", + "matchKey": "POST api.openai.com/v1/chat/completions", + "recordedAt": "2026-06-18T14:20:00.867Z", + "request": { + "body": { + "kind": "json", + "value": { + "max_completion_tokens": 160, + "messages": [ + { + "content": "Reply exactly STRANDS_AGENT_STREAM_OK.", + "role": "system" + }, + { + "content": [ + { + "text": "Reply exactly STRANDS_AGENT_STREAM_OK.", + "type": "text" + } + ], + "role": "user" + } + ], + "model": "gpt-4o-mini-2024-07-18", + "stream": true, + "stream_options": { + "include_usage": true + }, + "temperature": 0 + } + }, + "headers": {}, + "method": "POST", + "url": "https://api.openai.com/v1/chat/completions" + }, + "response": { + "body": { + "chunks": [ + "data: {\"id\":\"chatcmpl-Ds7t23uscJUyEKe9f1uqu2KfrPBPx\",\"object\":\"chat.completion.chunk\",\"created\":1781792400,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_7396530e6a\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"sGYTuEwid\"}", + "data: {\"id\":\"chatcmpl-Ds7t23uscJUyEKe9f1uqu2KfrPBPx\",\"object\":\"chat.completion.chunk\",\"created\":1781792400,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_7396530e6a\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"STR\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"tTU9TS1L\"}", + "data: {\"id\":\"chatcmpl-Ds7t23uscJUyEKe9f1uqu2KfrPBPx\",\"object\":\"chat.completion.chunk\",\"created\":1781792400,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_7396530e6a\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"ANDS\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"mrRmvZE\"}", + "data: {\"id\":\"chatcmpl-Ds7t23uscJUyEKe9f1uqu2KfrPBPx\",\"object\":\"chat.completion.chunk\",\"created\":1781792400,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_7396530e6a\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"_AGENT\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"q8eX4\"}", + "data: {\"id\":\"chatcmpl-Ds7t23uscJUyEKe9f1uqu2KfrPBPx\",\"object\":\"chat.completion.chunk\",\"created\":1781792400,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_7396530e6a\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"_STREAM\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"I5KH\"}", + "data: {\"id\":\"chatcmpl-Ds7t23uscJUyEKe9f1uqu2KfrPBPx\",\"object\":\"chat.completion.chunk\",\"created\":1781792400,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_7396530e6a\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"_OK\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"S90gyPz5\"}", + "data: {\"id\":\"chatcmpl-Ds7t23uscJUyEKe9f1uqu2KfrPBPx\",\"object\":\"chat.completion.chunk\",\"created\":1781792400,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_7396530e6a\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\".\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"STniGBQr0X\"}", + "data: {\"id\":\"chatcmpl-Ds7t23uscJUyEKe9f1uqu2KfrPBPx\",\"object\":\"chat.completion.chunk\",\"created\":1781792400,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_7396530e6a\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"usage\":null,\"obfuscation\":\"AVoEr\"}", + "data: {\"id\":\"chatcmpl-Ds7t23uscJUyEKe9f1uqu2KfrPBPx\",\"object\":\"chat.completion.chunk\",\"created\":1781792400,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_7396530e6a\",\"choices\":[],\"usage\":{\"prompt_tokens\":27,\"completion_tokens\":6,\"total_tokens\":33,\"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\":\"JcJMXbgYUjG\"}", + "data: [DONE]" + ], + "kind": "sse" + }, + "headers": { + "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", + "alt-svc": "h3=\":443\"; ma=86400", + "cf-cache-status": "DYNAMIC", + "cf-ray": "a0daeea48fdec24f-VIE", + "connection": "keep-alive", + "content-type": "text/event-stream; charset=utf-8", + "date": "Thu, 18 Jun 2026 14:20:00 GMT", + "openai-organization": "braintrust-data", + "openai-processing-ms": "451", + "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", + "openai-version": "2020-10-01", + "server": "cloudflare", + "set-cookie": "[REDACTED]", + "strict-transport-security": "max-age=31536000; includeSubDomains; preload", + "transfer-encoding": "chunked", + "x-content-type-options": "nosniff", + "x-openai-proxy-wasm": "v0.1", + "x-ratelimit-limit-requests": "30000", + "x-ratelimit-limit-tokens": "150000000", + "x-ratelimit-remaining-requests": "29999", + "x-ratelimit-remaining-tokens": "149999977", + "x-ratelimit-reset-requests": "2ms", + "x-ratelimit-reset-tokens": "0s", + "x-request-id": "req_80eb7af70057450e8d6b7874ad1cfbb9" + }, + "status": 200, + "statusText": "OK" + } + }, + { + "callIndex": 3, + "id": "f1affedbdb5d1da0", + "matchKey": "POST api.openai.com/v1/chat/completions", + "recordedAt": "2026-06-18T14:20:01.798Z", + "request": { + "body": { + "kind": "json", + "value": { + "max_completion_tokens": 160, + "messages": [ + { + "content": "Reply exactly GRAPH_RESEARCH_DONE.", + "role": "system" + }, + { + "content": [ + { + "text": "Have the first node finish, then have the writer answer exactly STRANDS_GRAPH_OK.", + "type": "text" + } + ], + "role": "user" + } + ], + "model": "gpt-4o-mini-2024-07-18", + "stream": true, + "stream_options": { + "include_usage": true + }, + "temperature": 0 + } + }, + "headers": {}, + "method": "POST", + "url": "https://api.openai.com/v1/chat/completions" + }, + "response": { + "body": { + "chunks": [ + "data: {\"id\":\"chatcmpl-Ds7t3HDUrqAqy5Xyr6iNo53LIq7GQ\",\"object\":\"chat.completion.chunk\",\"created\":1781792401,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_eee5e0b136\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"MBxO0hTG4\"}", + "data: {\"id\":\"chatcmpl-Ds7t3HDUrqAqy5Xyr6iNo53LIq7GQ\",\"object\":\"chat.completion.chunk\",\"created\":1781792401,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_eee5e0b136\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"GRAPH\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"hVJYgn\"}", + "data: {\"id\":\"chatcmpl-Ds7t3HDUrqAqy5Xyr6iNo53LIq7GQ\",\"object\":\"chat.completion.chunk\",\"created\":1781792401,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_eee5e0b136\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"_RE\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"P2aJfC2Z\"}", + "data: {\"id\":\"chatcmpl-Ds7t3HDUrqAqy5Xyr6iNo53LIq7GQ\",\"object\":\"chat.completion.chunk\",\"created\":1781792401,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_eee5e0b136\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"SEARCH\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"fygZc\"}", + "data: {\"id\":\"chatcmpl-Ds7t3HDUrqAqy5Xyr6iNo53LIq7GQ\",\"object\":\"chat.completion.chunk\",\"created\":1781792401,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_eee5e0b136\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"_DONE\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"Ata5gu\"}", + "data: {\"id\":\"chatcmpl-Ds7t3HDUrqAqy5Xyr6iNo53LIq7GQ\",\"object\":\"chat.completion.chunk\",\"created\":1781792401,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_eee5e0b136\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"usage\":null,\"obfuscation\":\"6MdN4\"}", + "data: {\"id\":\"chatcmpl-Ds7t3HDUrqAqy5Xyr6iNo53LIq7GQ\",\"object\":\"chat.completion.chunk\",\"created\":1781792401,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_eee5e0b136\",\"choices\":[],\"usage\":{\"prompt_tokens\":35,\"completion_tokens\":4,\"total_tokens\":39,\"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\":\"7uMhMC58XRN\"}", + "data: [DONE]" + ], + "kind": "sse" + }, + "headers": { + "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", + "alt-svc": "h3=\":443\"; ma=86400", + "cf-cache-status": "DYNAMIC", + "cf-ray": "a0daeeaa3db1c24f-VIE", + "connection": "keep-alive", + "content-type": "text/event-stream; charset=utf-8", + "date": "Thu, 18 Jun 2026 14:20:01 GMT", + "openai-organization": "braintrust-data", + "openai-processing-ms": "411", + "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", + "openai-version": "2020-10-01", + "server": "cloudflare", + "set-cookie": "[REDACTED]", + "strict-transport-security": "max-age=31536000; includeSubDomains; preload", + "transfer-encoding": "chunked", + "x-content-type-options": "nosniff", + "x-openai-proxy-wasm": "v0.1", + "x-ratelimit-limit-requests": "30000", + "x-ratelimit-limit-tokens": "150000000", + "x-ratelimit-remaining-requests": "29999", + "x-ratelimit-remaining-tokens": "149999970", + "x-ratelimit-reset-requests": "2ms", + "x-ratelimit-reset-tokens": "0s", + "x-request-id": "req_06d536c5ee4e4e74ba18e62bc13edc08" + }, + "status": 200, + "statusText": "OK" + } + }, + { + "callIndex": 4, + "id": "da2ccaeedff7ef8b", + "matchKey": "POST api.openai.com/v1/chat/completions", + "recordedAt": "2026-06-18T14:20:02.321Z", + "request": { + "body": { + "kind": "json", + "value": { + "max_completion_tokens": 160, + "messages": [ + { + "content": "Reply exactly STRANDS_GRAPH_OK.", + "role": "system" + }, + { + "content": [ + { + "text": "Have the first node finish, then have the writer answer exactly STRANDS_GRAPH_OK.", + "type": "text" + }, + { + "text": "[node: graph-researcher]", + "type": "text" + }, + { + "text": "GRAPH_RESEARCH_DONE", + "type": "text" + } + ], + "role": "user" + } + ], + "model": "gpt-4o-mini-2024-07-18", + "stream": true, + "stream_options": { + "include_usage": true + }, + "temperature": 0 + } + }, + "headers": {}, + "method": "POST", + "url": "https://api.openai.com/v1/chat/completions" + }, + "response": { + "body": { + "chunks": [ + "data: {\"id\":\"chatcmpl-Ds7t4bzueUCNI3ncT3VmgLobzJIiR\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_944bbf963c\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"FxdXLCgEG\"}", + "data: {\"id\":\"chatcmpl-Ds7t4bzueUCNI3ncT3VmgLobzJIiR\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_944bbf963c\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"STR\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"4ZrLh7F4\"}", + "data: {\"id\":\"chatcmpl-Ds7t4bzueUCNI3ncT3VmgLobzJIiR\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_944bbf963c\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"ANDS\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"rbZEnTq\"}", + "data: {\"id\":\"chatcmpl-Ds7t4bzueUCNI3ncT3VmgLobzJIiR\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_944bbf963c\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"_GRAPH\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"XvTmC\"}", + "data: {\"id\":\"chatcmpl-Ds7t4bzueUCNI3ncT3VmgLobzJIiR\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_944bbf963c\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"_OK\"},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"njpC1qwi\"}", + "data: {\"id\":\"chatcmpl-Ds7t4bzueUCNI3ncT3VmgLobzJIiR\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_944bbf963c\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"usage\":null,\"obfuscation\":\"3Ncel\"}", + "data: {\"id\":\"chatcmpl-Ds7t4bzueUCNI3ncT3VmgLobzJIiR\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_944bbf963c\",\"choices\":[],\"usage\":{\"prompt_tokens\":46,\"completion_tokens\":4,\"total_tokens\":50,\"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\":\"dtu7yeODzeT\"}", + "data: [DONE]" + ], + "kind": "sse" + }, + "headers": { + "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", + "alt-svc": "h3=\":443\"; ma=86400", + "cf-cache-status": "DYNAMIC", + "cf-ray": "a0daeeb00b23c24f-VIE", + "connection": "keep-alive", + "content-type": "text/event-stream; charset=utf-8", + "date": "Thu, 18 Jun 2026 14:20:02 GMT", + "openai-organization": "braintrust-data", + "openai-processing-ms": "156", + "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", + "openai-version": "2020-10-01", + "server": "cloudflare", + "set-cookie": "[REDACTED]", + "strict-transport-security": "max-age=31536000; includeSubDomains; preload", + "transfer-encoding": "chunked", + "x-content-type-options": "nosniff", + "x-openai-proxy-wasm": "v0.1", + "x-ratelimit-limit-requests": "30000", + "x-ratelimit-limit-tokens": "150000000", + "x-ratelimit-remaining-requests": "29999", + "x-ratelimit-remaining-tokens": "149999957", + "x-ratelimit-reset-requests": "2ms", + "x-ratelimit-reset-tokens": "0s", + "x-request-id": "req_8376ea31b4be41d1b3947e9f0e24a7d8" + }, + "status": 200, + "statusText": "OK" + } + }, + { + "callIndex": 5, + "id": "bb7637a47f479eeb", + "matchKey": "POST api.openai.com/v1/chat/completions", + "recordedAt": "2026-06-18T14:20:03.435Z", + "request": { + "body": { + "kind": "json", + "value": { + "max_completion_tokens": 160, + "messages": [ + { + "content": "Use the structured output schema. Hand off to swarm-finisher with message finish with STRANDS_SWARM_OK.", + "role": "system" + }, + { + "content": [ + { + "text": "Route to the finisher, then finish exactly STRANDS_SWARM_OK.", + "type": "text" + } + ], + "role": "user" + } + ], + "model": "gpt-4o-mini-2024-07-18", + "stream": true, + "stream_options": { + "include_usage": true + }, + "temperature": 0, + "tools": [ + { + "function": { + "description": "This tool MUST only be invoked as the last and final tool before returning the completed result to the caller.\nDecide whether to hand off to another agent or produce a final response.", + "name": "strands_structured_output", + "parameters": { + "additionalProperties": false, + "description": "Decide whether to hand off to another agent or produce a final response.", + "properties": { + "agentId": { + "description": "Target agent to hand off to. Omit to end the conversation.\n\nAvailable agents:\n- swarm-finisher: Finishes the test response.", + "enum": ["swarm-finisher"], + "type": "string" + }, + "context": { + "additionalProperties": {}, + "description": "Structured data to pass to the next agent.", + "propertyNames": { + "type": "string" + }, + "type": "object" + }, + "message": { + "description": "Instructions for the next agent, or the final response if no handoff.", + "type": "string" + } + }, + "required": ["message"], + "type": "object" + } + }, + "type": "function" + } + ] + } + }, + "headers": {}, + "method": "POST", + "url": "https://api.openai.com/v1/chat/completions" + }, + "response": { + "body": { + "chunks": [ + "data: {\"id\":\"chatcmpl-Ds7t4UcdNamY4iQXZeDa5IiJmkUfB\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_f4c4edc0d3\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"index\":0,\"id\":\"call_q0XD2r6QQ5O4VmphUgNKD2lK\",\"type\":\"function\",\"function\":{\"name\":\"strands_structured_output\",\"arguments\":\"\"}}],\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"gpc2Tp\"}", + "data: {\"id\":\"chatcmpl-Ds7t4UcdNamY4iQXZeDa5IiJmkUfB\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_f4c4edc0d3\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"{\\\"\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"YCaS5KhCvDdSs5\"}", + "data: {\"id\":\"chatcmpl-Ds7t4UcdNamY4iQXZeDa5IiJmkUfB\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_f4c4edc0d3\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"agent\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"1SQNycCAQIPr\"}", + "data: {\"id\":\"chatcmpl-Ds7t4UcdNamY4iQXZeDa5IiJmkUfB\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_f4c4edc0d3\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"Id\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"kF5s4OdnSY9QMWK\"}", + "data: {\"id\":\"chatcmpl-Ds7t4UcdNamY4iQXZeDa5IiJmkUfB\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_f4c4edc0d3\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\":\\\"\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"xFPOvTWkaTj6\"}", + "data: {\"id\":\"chatcmpl-Ds7t4UcdNamY4iQXZeDa5IiJmkUfB\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_f4c4edc0d3\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"sw\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"oZAYm5yVYyvz6GR\"}", + "data: {\"id\":\"chatcmpl-Ds7t4UcdNamY4iQXZeDa5IiJmkUfB\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_f4c4edc0d3\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"arm\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"IbmobzpjfcyXrg\"}", + "data: {\"id\":\"chatcmpl-Ds7t4UcdNamY4iQXZeDa5IiJmkUfB\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_f4c4edc0d3\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"-fin\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"HkbTZKKRchqaX\"}", + "data: {\"id\":\"chatcmpl-Ds7t4UcdNamY4iQXZeDa5IiJmkUfB\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_f4c4edc0d3\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"isher\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"j4cpWsZAWqsW\"}", + "data: {\"id\":\"chatcmpl-Ds7t4UcdNamY4iQXZeDa5IiJmkUfB\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_f4c4edc0d3\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\",\\\"\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"YcW990fbz2Li\"}", + "data: {\"id\":\"chatcmpl-Ds7t4UcdNamY4iQXZeDa5IiJmkUfB\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_f4c4edc0d3\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"message\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"kzmtmveIaG\"}", + "data: {\"id\":\"chatcmpl-Ds7t4UcdNamY4iQXZeDa5IiJmkUfB\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_f4c4edc0d3\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\":\\\"\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"8fFMoZ7ETfR3\"}", + "data: {\"id\":\"chatcmpl-Ds7t4UcdNamY4iQXZeDa5IiJmkUfB\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_f4c4edc0d3\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"finish\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"z9cw9cXjRsF\"}", + "data: {\"id\":\"chatcmpl-Ds7t4UcdNamY4iQXZeDa5IiJmkUfB\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_f4c4edc0d3\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\" with\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"Ki1eJkmCG5bo\"}", + "data: {\"id\":\"chatcmpl-Ds7t4UcdNamY4iQXZeDa5IiJmkUfB\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_f4c4edc0d3\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\" STR\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"LbeCuCssJyCnD\"}", + "data: {\"id\":\"chatcmpl-Ds7t4UcdNamY4iQXZeDa5IiJmkUfB\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_f4c4edc0d3\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"ANDS\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"4NYR7UkuwofER\"}", + "data: {\"id\":\"chatcmpl-Ds7t4UcdNamY4iQXZeDa5IiJmkUfB\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_f4c4edc0d3\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"_SW\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"bf4p5vqz5uQaok\"}", + "data: {\"id\":\"chatcmpl-Ds7t4UcdNamY4iQXZeDa5IiJmkUfB\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_f4c4edc0d3\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"ARM\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"HlOTt5rpoFhAJg\"}", + "data: {\"id\":\"chatcmpl-Ds7t4UcdNamY4iQXZeDa5IiJmkUfB\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_f4c4edc0d3\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"_OK\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"3UF348Nkhe1UEu\"}", + "data: {\"id\":\"chatcmpl-Ds7t4UcdNamY4iQXZeDa5IiJmkUfB\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_f4c4edc0d3\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\"}\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"G4A9qqtl8St5ZN\"}", + "data: {\"id\":\"chatcmpl-Ds7t4UcdNamY4iQXZeDa5IiJmkUfB\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_f4c4edc0d3\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"tool_calls\"}],\"usage\":null,\"obfuscation\":\"qfJQ4JZl1XnGCuz\"}", + "data: {\"id\":\"chatcmpl-Ds7t4UcdNamY4iQXZeDa5IiJmkUfB\",\"object\":\"chat.completion.chunk\",\"created\":1781792402,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_f4c4edc0d3\",\"choices\":[],\"usage\":{\"prompt_tokens\":198,\"completion_tokens\":31,\"total_tokens\":229,\"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\":\"PD3WHLUv\"}", + "data: [DONE]" + ], + "kind": "sse" + }, + "headers": { + "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", + "alt-svc": "h3=\":443\"; ma=86400", + "cf-cache-status": "DYNAMIC", + "cf-ray": "a0daeeb36e6ac24f-VIE", + "connection": "keep-alive", + "content-type": "text/event-stream; charset=utf-8", + "date": "Thu, 18 Jun 2026 14:20:03 GMT", + "openai-organization": "braintrust-data", + "openai-processing-ms": "499", + "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", + "openai-version": "2020-10-01", + "server": "cloudflare", + "set-cookie": "[REDACTED]", + "strict-transport-security": "max-age=31536000; includeSubDomains; preload", + "transfer-encoding": "chunked", + "x-content-type-options": "nosniff", + "x-openai-proxy-wasm": "v0.1", + "x-ratelimit-limit-requests": "30000", + "x-ratelimit-limit-tokens": "150000000", + "x-ratelimit-remaining-requests": "29999", + "x-ratelimit-remaining-tokens": "149999955", + "x-ratelimit-reset-requests": "2ms", + "x-ratelimit-reset-tokens": "0s", + "x-request-id": "req_67461b3d77f349cca9fabc366c293bda" + }, + "status": 200, + "statusText": "OK" + } + }, + { + "callIndex": 6, + "id": "85b37d48b99f35cb", + "matchKey": "POST api.openai.com/v1/chat/completions", + "recordedAt": "2026-06-18T14:20:04.202Z", + "request": { + "body": { + "kind": "json", + "value": { + "max_completion_tokens": 160, + "messages": [ + { + "content": "Use the structured output schema. Do not hand off. Set the final message to exactly STRANDS_SWARM_OK.", + "role": "system" + }, + { + "content": [ + { + "text": "finish with STRANDS_SWARM_OK", + "type": "text" + } + ], + "role": "user" + } + ], + "model": "gpt-4o-mini-2024-07-18", + "stream": true, + "stream_options": { + "include_usage": true + }, + "temperature": 0, + "tools": [ + { + "function": { + "description": "This tool MUST only be invoked as the last and final tool before returning the completed result to the caller.\nDecide whether to hand off to another agent or produce a final response.", + "name": "strands_structured_output", + "parameters": { + "additionalProperties": false, + "description": "Decide whether to hand off to another agent or produce a final response.", + "properties": { + "agentId": { + "description": "Target agent to hand off to. Omit to end the conversation.\n\nAvailable agents:\n- swarm-router: Routes work to the finisher agent.", + "enum": ["swarm-router"], + "type": "string" + }, + "context": { + "additionalProperties": {}, + "description": "Structured data to pass to the next agent.", + "propertyNames": { + "type": "string" + }, + "type": "object" + }, + "message": { + "description": "Instructions for the next agent, or the final response if no handoff.", + "type": "string" + } + }, + "required": ["message"], + "type": "object" + } + }, + "type": "function" + } + ] + } + }, + "headers": {}, + "method": "POST", + "url": "https://api.openai.com/v1/chat/completions" + }, + "response": { + "body": { + "chunks": [ + "data: {\"id\":\"chatcmpl-Ds7t56eG4oGPTtWeZhLIcWLGBKbHT\",\"object\":\"chat.completion.chunk\",\"created\":1781792403,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_899b586ef5\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"index\":0,\"id\":\"call_2hbwdCfA3AJO5su7xD5IHmbT\",\"type\":\"function\",\"function\":{\"name\":\"strands_structured_output\",\"arguments\":\"\"}}],\"refusal\":null},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"BiW62l\"}", + "data: {\"id\":\"chatcmpl-Ds7t56eG4oGPTtWeZhLIcWLGBKbHT\",\"object\":\"chat.completion.chunk\",\"created\":1781792403,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_899b586ef5\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"{\\\"\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"o14K0rT6u8peCL\"}", + "data: {\"id\":\"chatcmpl-Ds7t56eG4oGPTtWeZhLIcWLGBKbHT\",\"object\":\"chat.completion.chunk\",\"created\":1781792403,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_899b586ef5\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"message\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"iizawaSWNb\"}", + "data: {\"id\":\"chatcmpl-Ds7t56eG4oGPTtWeZhLIcWLGBKbHT\",\"object\":\"chat.completion.chunk\",\"created\":1781792403,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_899b586ef5\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\":\\\"\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"bfYDroT2n9Gl\"}", + "data: {\"id\":\"chatcmpl-Ds7t56eG4oGPTtWeZhLIcWLGBKbHT\",\"object\":\"chat.completion.chunk\",\"created\":1781792403,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_899b586ef5\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"STR\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"OhGIjlLT0Cc5a6\"}", + "data: {\"id\":\"chatcmpl-Ds7t56eG4oGPTtWeZhLIcWLGBKbHT\",\"object\":\"chat.completion.chunk\",\"created\":1781792403,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_899b586ef5\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"ANDS\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"ChFEkPtiPvbMj\"}", + "data: {\"id\":\"chatcmpl-Ds7t56eG4oGPTtWeZhLIcWLGBKbHT\",\"object\":\"chat.completion.chunk\",\"created\":1781792403,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_899b586ef5\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"_SW\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"0JyNjkKbLxxVan\"}", + "data: {\"id\":\"chatcmpl-Ds7t56eG4oGPTtWeZhLIcWLGBKbHT\",\"object\":\"chat.completion.chunk\",\"created\":1781792403,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_899b586ef5\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"ARM\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"a5HRAPBmVc5Ckl\"}", + "data: {\"id\":\"chatcmpl-Ds7t56eG4oGPTtWeZhLIcWLGBKbHT\",\"object\":\"chat.completion.chunk\",\"created\":1781792403,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_899b586ef5\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"_OK\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"GjKfBH1aESoOFo\"}", + "data: {\"id\":\"chatcmpl-Ds7t56eG4oGPTtWeZhLIcWLGBKbHT\",\"object\":\"chat.completion.chunk\",\"created\":1781792403,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_899b586ef5\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"\\\"}\"}}]},\"logprobs\":null,\"finish_reason\":null}],\"usage\":null,\"obfuscation\":\"wGmvYKD5Nwe6Cr\"}", + "data: {\"id\":\"chatcmpl-Ds7t56eG4oGPTtWeZhLIcWLGBKbHT\",\"object\":\"chat.completion.chunk\",\"created\":1781792403,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_899b586ef5\",\"choices\":[{\"index\":0,\"delta\":{},\"logprobs\":null,\"finish_reason\":\"tool_calls\"}],\"usage\":null,\"obfuscation\":\"SXJfbLraNcwxXTt\"}", + "data: {\"id\":\"chatcmpl-Ds7t56eG4oGPTtWeZhLIcWLGBKbHT\",\"object\":\"chat.completion.chunk\",\"created\":1781792403,\"model\":\"gpt-4o-mini-2024-07-18\",\"service_tier\":\"default\",\"system_fingerprint\":\"fp_899b586ef5\",\"choices\":[],\"usage\":{\"prompt_tokens\":191,\"completion_tokens\":21,\"total_tokens\":212,\"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\":\"SRX6AAUf\"}", + "data: [DONE]" + ], + "kind": "sse" + }, + "headers": { + "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", + "alt-svc": "h3=\":443\"; ma=86400", + "cf-cache-status": "DYNAMIC", + "cf-ray": "a0daeeba4d27c24f-VIE", + "connection": "keep-alive", + "content-type": "text/event-stream; charset=utf-8", + "date": "Thu, 18 Jun 2026 14:20:04 GMT", + "openai-organization": "braintrust-data", + "openai-processing-ms": "349", + "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", + "openai-version": "2020-10-01", + "server": "cloudflare", + "set-cookie": "[REDACTED]", + "strict-transport-security": "max-age=31536000; includeSubDomains; preload", + "transfer-encoding": "chunked", + "x-content-type-options": "nosniff", + "x-openai-proxy-wasm": "v0.1", + "x-ratelimit-limit-requests": "30000", + "x-ratelimit-limit-tokens": "150000000", + "x-ratelimit-remaining-requests": "29999", + "x-ratelimit-remaining-tokens": "149999965", + "x-ratelimit-reset-requests": "2ms", + "x-ratelimit-reset-tokens": "0s", + "x-request-id": "req_21ee960568de454ca2662e264189dff4" + }, + "status": 200, + "statusText": "OK" + } + } + ], + "meta": { + "createdAt": "2026-06-18T14:16:44.922Z" + } +} diff --git a/e2e/scenarios/strands-agent-sdk-instrumentation/__snapshots__/strands-agent-sdk-v160-auto-hook.span-tree.json b/e2e/scenarios/strands-agent-sdk-instrumentation/__snapshots__/strands-agent-sdk-v160-auto-hook.span-tree.json new file mode 100644 index 000000000..f45ed1f21 --- /dev/null +++ b/e2e/scenarios/strands-agent-sdk-instrumentation/__snapshots__/strands-agent-sdk-v160-auto-hook.span-tree.json @@ -0,0 +1,548 @@ +{ + "span_tree": [ + { + "name": "strands-agent-sdk-instrumentation-root", + "type": "task", + "children": [ + { + "name": "strands-agent-invoke-operation", + "children": [ + { + "name": "Agent: weather-agent", + "type": "task", + "children": [ + { + "name": "Strands model: gpt-4o-mini-2024-07-18", + "type": "llm", + "children": [], + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.operation": "model.stream", + "strands.stop_reason": "toolUse" + }, + "metrics": { + "completion_tokens": 15, + "duration": "", + "prompt_tokens": 86, + "tokens": 101 + } + }, + { + "name": "tool: lookup_weather", + "type": "tool", + "children": [], + "input": { + "city": "Vienna" + }, + "output": { + "toolResult": { + "content": [ + { + "json": { + "city": "Vienna", + "forecast": "sunny", + "marker": "STRANDS_TOOL_OK" + } + } + ], + "status": "success", + "toolUseId": "" + } + }, + "metadata": { + "gen_ai.tool.name": "lookup_weather", + "provider": "strands", + "strands.operation": "tool.call", + "strands.tool.name": "lookup_weather" + }, + "metrics": { + "duration": "" + } + }, + { + "name": "Strands model: gpt-4o-mini-2024-07-18", + "type": "llm", + "children": [], + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.operation": "model.stream", + "strands.stop_reason": "endTurn" + }, + "metrics": { + "completion_tokens": 6, + "duration": "", + "prompt_tokens": 127, + "tokens": 133 + } + } + ], + "input": "Use lookup_weather for Vienna, then answer exactly STRANDS_AGENT_TOOL_OK.", + "output": { + "content": [ + { + "text": "STRANDS_AGENT_TOOL_OK" + } + ], + "metadata": { + "usage": { + "inputTokens": 127, + "outputTokens": 6, + "totalTokens": 133 + } + }, + "role": "assistant" + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.agent.id": "weather-agent", + "strands.agent.name": "weather-agent", + "strands.operation": "Agent.stream", + "strands.stop_reason": "endTurn" + }, + "metrics": { + "completion_tokens": 21, + "duration": "", + "prompt_tokens": 213, + "tokens": 234 + } + } + ], + "metadata": { + "operation": "agent-invoke" + } + }, + { + "name": "strands-agent-stream-operation", + "children": [ + { + "name": "Agent: stream-agent", + "type": "task", + "children": [ + { + "name": "Strands model: gpt-4o-mini-2024-07-18", + "type": "llm", + "children": [], + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.operation": "model.stream", + "strands.stop_reason": "endTurn" + }, + "metrics": { + "completion_tokens": 6, + "duration": "", + "prompt_tokens": 27, + "tokens": 33 + } + } + ], + "input": "Reply exactly STRANDS_AGENT_STREAM_OK.", + "output": { + "content": [ + { + "text": "STRANDS_AGENT_STREAM_OK." + } + ], + "metadata": { + "usage": { + "inputTokens": 27, + "outputTokens": 6, + "totalTokens": 33 + } + }, + "role": "assistant" + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.agent.id": "stream-agent", + "strands.agent.name": "stream-agent", + "strands.operation": "Agent.stream", + "strands.stop_reason": "endTurn" + }, + "metrics": { + "completion_tokens": 6, + "duration": "", + "prompt_tokens": 27, + "tokens": 33 + } + } + ], + "metadata": { + "operation": "agent-stream" + } + }, + { + "name": "strands-graph-invoke-operation", + "children": [ + { + "name": "Strands Graph", + "type": "task", + "children": [ + { + "name": "node: graph-researcher", + "type": "task", + "children": [ + { + "name": "Agent: graph-researcher", + "type": "task", + "children": [ + { + "name": "Strands model: gpt-4o-mini-2024-07-18", + "type": "llm", + "children": [], + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.operation": "model.stream", + "strands.stop_reason": "endTurn" + }, + "metrics": { + "completion_tokens": 4, + "duration": "", + "prompt_tokens": 35, + "tokens": 39 + } + } + ], + "input": "Have the first node finish, then have the writer answer exactly STRANDS_GRAPH_OK.", + "output": { + "content": [ + { + "text": "GRAPH_RESEARCH_DONE" + } + ], + "metadata": { + "usage": { + "inputTokens": 35, + "outputTokens": 4, + "totalTokens": 39 + } + }, + "role": "assistant" + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.agent.id": "graph-researcher", + "strands.agent.name": "graph-researcher", + "strands.operation": "Agent.stream", + "strands.stop_reason": "endTurn" + }, + "metrics": { + "completion_tokens": 4, + "duration": "", + "prompt_tokens": 35, + "tokens": 39 + } + } + ], + "output": "GRAPH_RESEARCH_DONE", + "metadata": { + "provider": "strands", + "strands.node.id": "graph-researcher", + "strands.node.status": "COMPLETED", + "strands.node.type": "agentNode", + "strands.operation": "node.call" + }, + "metrics": { + "completion_tokens": 4, + "duration": "", + "prompt_tokens": 35, + "strands.node.duration_ms": "", + "tokens": 39 + } + }, + { + "name": "node: graph-writer", + "type": "task", + "children": [ + { + "name": "Agent: graph-writer", + "type": "task", + "children": [ + { + "name": "Strands model: gpt-4o-mini-2024-07-18", + "type": "llm", + "children": [], + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.operation": "model.stream", + "strands.stop_reason": "endTurn" + }, + "metrics": { + "completion_tokens": 4, + "duration": "", + "prompt_tokens": 46, + "tokens": 50 + } + } + ], + "input": [ + { + "text": "Have the first node finish, then have the writer answer exactly STRANDS_GRAPH_OK." + }, + { + "text": "[node: graph-researcher]" + }, + { + "text": "GRAPH_RESEARCH_DONE" + } + ], + "output": { + "content": [ + { + "text": "STRANDS_GRAPH_OK" + } + ], + "metadata": { + "usage": { + "inputTokens": 46, + "outputTokens": 4, + "totalTokens": 50 + } + }, + "role": "assistant" + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.agent.id": "graph-writer", + "strands.agent.name": "graph-writer", + "strands.operation": "Agent.stream", + "strands.stop_reason": "endTurn" + }, + "metrics": { + "completion_tokens": 4, + "duration": "", + "prompt_tokens": 46, + "tokens": 50 + } + } + ], + "output": "STRANDS_GRAPH_OK", + "metadata": { + "provider": "strands", + "strands.node.id": "graph-writer", + "strands.node.status": "COMPLETED", + "strands.node.type": "agentNode", + "strands.operation": "node.call" + }, + "metrics": { + "completion_tokens": 4, + "duration": "", + "prompt_tokens": 46, + "strands.node.duration_ms": "", + "tokens": 50 + } + } + ], + "input": "Have the first node finish, then have the writer answer exactly STRANDS_GRAPH_OK.", + "output": "STRANDS_GRAPH_OK", + "metadata": { + "provider": "strands", + "strands.handoffs": [ + { + "source": "graph-researcher", + "targets": [ + "graph-writer" + ] + } + ], + "strands.operation": "Graph.stream", + "strands.orchestrator.id": "weather-graph", + "strands.status": "COMPLETED" + }, + "metrics": { + "completion_tokens": 8, + "duration": "", + "prompt_tokens": 81, + "tokens": 89 + } + } + ], + "metadata": { + "operation": "graph-invoke" + } + }, + { + "name": "strands-swarm-invoke-operation", + "children": [ + { + "name": "Strands Swarm", + "type": "task", + "children": [ + { + "name": "node: swarm-router", + "type": "task", + "children": [ + { + "name": "Agent: swarm-router", + "type": "task", + "children": [ + { + "name": "Strands model: gpt-4o-mini-2024-07-18", + "type": "llm", + "children": [], + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.operation": "model.stream", + "strands.stop_reason": "toolUse" + }, + "metrics": { + "completion_tokens": 31, + "duration": "", + "prompt_tokens": 198, + "tokens": 229 + } + } + ], + "input": "Route to the finisher, then finish exactly STRANDS_SWARM_OK.", + "output": { + "agentId": "", + "message": "finish with STRANDS_SWARM_OK" + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.agent.id": "swarm-router", + "strands.agent.name": "swarm-router", + "strands.operation": "Agent.stream", + "strands.stop_reason": "toolUse" + }, + "metrics": { + "completion_tokens": 31, + "duration": "", + "prompt_tokens": 198, + "tokens": 229 + } + } + ], + "output": { + "agentId": "", + "message": "finish with STRANDS_SWARM_OK" + }, + "metadata": { + "provider": "strands", + "strands.node.id": "swarm-router", + "strands.node.status": "COMPLETED", + "strands.node.type": "agentNode", + "strands.operation": "node.call" + }, + "metrics": { + "completion_tokens": 31, + "duration": "", + "prompt_tokens": 198, + "strands.node.duration_ms": "", + "tokens": 229 + } + }, + { + "name": "node: swarm-finisher", + "type": "task", + "children": [ + { + "name": "Agent: swarm-finisher", + "type": "task", + "children": [ + { + "name": "Strands model: gpt-4o-mini-2024-07-18", + "type": "llm", + "children": [], + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.operation": "model.stream", + "strands.stop_reason": "toolUse" + }, + "metrics": { + "completion_tokens": 21, + "duration": "", + "prompt_tokens": 191, + "tokens": 212 + } + } + ], + "input": [ + { + "text": "finish with STRANDS_SWARM_OK" + } + ], + "output": { + "message": "STRANDS_SWARM_OK" + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.agent.id": "swarm-finisher", + "strands.agent.name": "swarm-finisher", + "strands.operation": "Agent.stream", + "strands.stop_reason": "toolUse" + }, + "metrics": { + "completion_tokens": 21, + "duration": "", + "prompt_tokens": 191, + "tokens": 212 + } + } + ], + "output": { + "message": "STRANDS_SWARM_OK" + }, + "metadata": { + "provider": "strands", + "strands.node.id": "swarm-finisher", + "strands.node.status": "COMPLETED", + "strands.node.type": "agentNode", + "strands.operation": "node.call" + }, + "metrics": { + "completion_tokens": 21, + "duration": "", + "prompt_tokens": 191, + "strands.node.duration_ms": "", + "tokens": 212 + } + } + ], + "input": "Route to the finisher, then finish exactly STRANDS_SWARM_OK.", + "output": "STRANDS_SWARM_OK", + "metadata": { + "provider": "strands", + "strands.handoffs": [ + { + "source": "swarm-router", + "targets": [ + "swarm-finisher" + ] + } + ], + "strands.operation": "Swarm.stream", + "strands.orchestrator.id": "weather-swarm", + "strands.status": "COMPLETED" + }, + "metrics": { + "completion_tokens": 52, + "duration": "", + "prompt_tokens": 389, + "tokens": 441 + } + } + ], + "metadata": { + "operation": "swarm-invoke" + } + } + ], + "metadata": { + "scenario": "strands-agent-sdk-instrumentation" + } + } + ] +} diff --git a/e2e/scenarios/strands-agent-sdk-instrumentation/__snapshots__/strands-agent-sdk-v160-auto-hook.span-tree.txt b/e2e/scenarios/strands-agent-sdk-instrumentation/__snapshots__/strands-agent-sdk-v160-auto-hook.span-tree.txt new file mode 100644 index 000000000..e07b7d0a6 --- /dev/null +++ b/e2e/scenarios/strands-agent-sdk-instrumentation/__snapshots__/strands-agent-sdk-v160-auto-hook.span-tree.txt @@ -0,0 +1,432 @@ +span_tree: +└── strands-agent-sdk-instrumentation-root [task] + metadata: { + "scenario": "strands-agent-sdk-instrumentation" + } + ├── strands-agent-invoke-operation + │ metadata: { + │ "operation": "agent-invoke" + │ } + │ └── Agent: weather-agent [task] + │ input: "Use lookup_weather for Vienna, then answer exactly STRANDS_AGENT_TOOL_OK." + │ output: { + │ "content": [ + │ { + │ "text": "STRANDS_AGENT_TOOL_OK" + │ } + │ ], + │ "metadata": { + │ "usage": { + │ "inputTokens": 127, + │ "outputTokens": 6, + │ "totalTokens": 133 + │ } + │ }, + │ "role": "assistant" + │ } + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18", + │ "provider": "openai", + │ "strands.agent.id": "weather-agent", + │ "strands.agent.name": "weather-agent", + │ "strands.operation": "Agent.stream", + │ "strands.stop_reason": "endTurn" + │ } + │ metrics: { + │ "completion_tokens": 21, + │ "duration": "", + │ "prompt_tokens": 213, + │ "tokens": 234 + │ } + │ ├── Strands model: gpt-4o-mini-2024-07-18 [llm] + │ │ metadata: { + │ │ "model": "gpt-4o-mini-2024-07-18", + │ │ "provider": "openai", + │ │ "strands.operation": "model.stream", + │ │ "strands.stop_reason": "toolUse" + │ │ } + │ │ metrics: { + │ │ "completion_tokens": 15, + │ │ "duration": "", + │ │ "prompt_tokens": 86, + │ │ "tokens": 101 + │ │ } + │ ├── tool: lookup_weather [tool] + │ │ input: { + │ │ "city": "Vienna" + │ │ } + │ │ output: { + │ │ "toolResult": { + │ │ "content": [ + │ │ { + │ │ "json": { + │ │ "city": "Vienna", + │ │ "forecast": "sunny", + │ │ "marker": "STRANDS_TOOL_OK" + │ │ } + │ │ } + │ │ ], + │ │ "status": "success", + │ │ "toolUseId": "" + │ │ } + │ │ } + │ │ metadata: { + │ │ "gen_ai.tool.name": "lookup_weather", + │ │ "provider": "strands", + │ │ "strands.operation": "tool.call", + │ │ "strands.tool.name": "lookup_weather" + │ │ } + │ │ metrics: { + │ │ "duration": "" + │ │ } + │ └── Strands model: gpt-4o-mini-2024-07-18 [llm] + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18", + │ "provider": "openai", + │ "strands.operation": "model.stream", + │ "strands.stop_reason": "endTurn" + │ } + │ metrics: { + │ "completion_tokens": 6, + │ "duration": "", + │ "prompt_tokens": 127, + │ "tokens": 133 + │ } + ├── strands-agent-stream-operation + │ metadata: { + │ "operation": "agent-stream" + │ } + │ └── Agent: stream-agent [task] + │ input: "Reply exactly STRANDS_AGENT_STREAM_OK." + │ output: { + │ "content": [ + │ { + │ "text": "STRANDS_AGENT_STREAM_OK." + │ } + │ ], + │ "metadata": { + │ "usage": { + │ "inputTokens": 27, + │ "outputTokens": 6, + │ "totalTokens": 33 + │ } + │ }, + │ "role": "assistant" + │ } + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18", + │ "provider": "openai", + │ "strands.agent.id": "stream-agent", + │ "strands.agent.name": "stream-agent", + │ "strands.operation": "Agent.stream", + │ "strands.stop_reason": "endTurn" + │ } + │ metrics: { + │ "completion_tokens": 6, + │ "duration": "", + │ "prompt_tokens": 27, + │ "tokens": 33 + │ } + │ └── Strands model: gpt-4o-mini-2024-07-18 [llm] + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18", + │ "provider": "openai", + │ "strands.operation": "model.stream", + │ "strands.stop_reason": "endTurn" + │ } + │ metrics: { + │ "completion_tokens": 6, + │ "duration": "", + │ "prompt_tokens": 27, + │ "tokens": 33 + │ } + ├── strands-graph-invoke-operation + │ metadata: { + │ "operation": "graph-invoke" + │ } + │ └── Strands Graph [task] + │ input: "Have the first node finish, then have the writer answer exactly STRANDS_GRAPH_OK." + │ output: "STRANDS_GRAPH_OK" + │ metadata: { + │ "provider": "strands", + │ "strands.handoffs": [ + │ { + │ "source": "graph-researcher", + │ "targets": [ + │ "graph-writer" + │ ] + │ } + │ ], + │ "strands.operation": "Graph.stream", + │ "strands.orchestrator.id": "weather-graph", + │ "strands.status": "COMPLETED" + │ } + │ metrics: { + │ "completion_tokens": 8, + │ "duration": "", + │ "prompt_tokens": 81, + │ "tokens": 89 + │ } + │ ├── node: graph-researcher [task] + │ │ output: "GRAPH_RESEARCH_DONE" + │ │ metadata: { + │ │ "provider": "strands", + │ │ "strands.node.id": "graph-researcher", + │ │ "strands.node.status": "COMPLETED", + │ │ "strands.node.type": "agentNode", + │ │ "strands.operation": "node.call" + │ │ } + │ │ metrics: { + │ │ "completion_tokens": 4, + │ │ "duration": "", + │ │ "prompt_tokens": 35, + │ │ "strands.node.duration_ms": "", + │ │ "tokens": 39 + │ │ } + │ │ └── Agent: graph-researcher [task] + │ │ input: "Have the first node finish, then have the writer answer exactly STRANDS_GRAPH_OK." + │ │ output: { + │ │ "content": [ + │ │ { + │ │ "text": "GRAPH_RESEARCH_DONE" + │ │ } + │ │ ], + │ │ "metadata": { + │ │ "usage": { + │ │ "inputTokens": 35, + │ │ "outputTokens": 4, + │ │ "totalTokens": 39 + │ │ } + │ │ }, + │ │ "role": "assistant" + │ │ } + │ │ metadata: { + │ │ "model": "gpt-4o-mini-2024-07-18", + │ │ "provider": "openai", + │ │ "strands.agent.id": "graph-researcher", + │ │ "strands.agent.name": "graph-researcher", + │ │ "strands.operation": "Agent.stream", + │ │ "strands.stop_reason": "endTurn" + │ │ } + │ │ metrics: { + │ │ "completion_tokens": 4, + │ │ "duration": "", + │ │ "prompt_tokens": 35, + │ │ "tokens": 39 + │ │ } + │ │ └── Strands model: gpt-4o-mini-2024-07-18 [llm] + │ │ metadata: { + │ │ "model": "gpt-4o-mini-2024-07-18", + │ │ "provider": "openai", + │ │ "strands.operation": "model.stream", + │ │ "strands.stop_reason": "endTurn" + │ │ } + │ │ metrics: { + │ │ "completion_tokens": 4, + │ │ "duration": "", + │ │ "prompt_tokens": 35, + │ │ "tokens": 39 + │ │ } + │ └── node: graph-writer [task] + │ output: "STRANDS_GRAPH_OK" + │ metadata: { + │ "provider": "strands", + │ "strands.node.id": "graph-writer", + │ "strands.node.status": "COMPLETED", + │ "strands.node.type": "agentNode", + │ "strands.operation": "node.call" + │ } + │ metrics: { + │ "completion_tokens": 4, + │ "duration": "", + │ "prompt_tokens": 46, + │ "strands.node.duration_ms": "", + │ "tokens": 50 + │ } + │ └── Agent: graph-writer [task] + │ input: [ + │ { + │ "text": "Have the first node finish, then have the writer answer exactly STRANDS_GRAPH_OK." + │ }, + │ { + │ "text": "[node: graph-researcher]" + │ }, + │ { + │ "text": "GRAPH_RESEARCH_DONE" + │ } + │ ] + │ output: { + │ "content": [ + │ { + │ "text": "STRANDS_GRAPH_OK" + │ } + │ ], + │ "metadata": { + │ "usage": { + │ "inputTokens": 46, + │ "outputTokens": 4, + │ "totalTokens": 50 + │ } + │ }, + │ "role": "assistant" + │ } + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18", + │ "provider": "openai", + │ "strands.agent.id": "graph-writer", + │ "strands.agent.name": "graph-writer", + │ "strands.operation": "Agent.stream", + │ "strands.stop_reason": "endTurn" + │ } + │ metrics: { + │ "completion_tokens": 4, + │ "duration": "", + │ "prompt_tokens": 46, + │ "tokens": 50 + │ } + │ └── Strands model: gpt-4o-mini-2024-07-18 [llm] + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18", + │ "provider": "openai", + │ "strands.operation": "model.stream", + │ "strands.stop_reason": "endTurn" + │ } + │ metrics: { + │ "completion_tokens": 4, + │ "duration": "", + │ "prompt_tokens": 46, + │ "tokens": 50 + │ } + └── strands-swarm-invoke-operation + metadata: { + "operation": "swarm-invoke" + } + └── Strands Swarm [task] + input: "Route to the finisher, then finish exactly STRANDS_SWARM_OK." + output: "STRANDS_SWARM_OK" + metadata: { + "provider": "strands", + "strands.handoffs": [ + { + "source": "swarm-router", + "targets": [ + "swarm-finisher" + ] + } + ], + "strands.operation": "Swarm.stream", + "strands.orchestrator.id": "weather-swarm", + "strands.status": "COMPLETED" + } + metrics: { + "completion_tokens": 52, + "duration": "", + "prompt_tokens": 389, + "tokens": 441 + } + ├── node: swarm-router [task] + │ output: { + │ "agentId": "", + │ "message": "finish with STRANDS_SWARM_OK" + │ } + │ metadata: { + │ "provider": "strands", + │ "strands.node.id": "swarm-router", + │ "strands.node.status": "COMPLETED", + │ "strands.node.type": "agentNode", + │ "strands.operation": "node.call" + │ } + │ metrics: { + │ "completion_tokens": 31, + │ "duration": "", + │ "prompt_tokens": 198, + │ "strands.node.duration_ms": "", + │ "tokens": 229 + │ } + │ └── Agent: swarm-router [task] + │ input: "Route to the finisher, then finish exactly STRANDS_SWARM_OK." + │ output: { + │ "agentId": "", + │ "message": "finish with STRANDS_SWARM_OK" + │ } + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18", + │ "provider": "openai", + │ "strands.agent.id": "swarm-router", + │ "strands.agent.name": "swarm-router", + │ "strands.operation": "Agent.stream", + │ "strands.stop_reason": "toolUse" + │ } + │ metrics: { + │ "completion_tokens": 31, + │ "duration": "", + │ "prompt_tokens": 198, + │ "tokens": 229 + │ } + │ └── Strands model: gpt-4o-mini-2024-07-18 [llm] + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18", + │ "provider": "openai", + │ "strands.operation": "model.stream", + │ "strands.stop_reason": "toolUse" + │ } + │ metrics: { + │ "completion_tokens": 31, + │ "duration": "", + │ "prompt_tokens": 198, + │ "tokens": 229 + │ } + └── node: swarm-finisher [task] + output: { + "message": "STRANDS_SWARM_OK" + } + metadata: { + "provider": "strands", + "strands.node.id": "swarm-finisher", + "strands.node.status": "COMPLETED", + "strands.node.type": "agentNode", + "strands.operation": "node.call" + } + metrics: { + "completion_tokens": 21, + "duration": "", + "prompt_tokens": 191, + "strands.node.duration_ms": "", + "tokens": 212 + } + └── Agent: swarm-finisher [task] + input: [ + { + "text": "finish with STRANDS_SWARM_OK" + } + ] + output: { + "message": "STRANDS_SWARM_OK" + } + metadata: { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.agent.id": "swarm-finisher", + "strands.agent.name": "swarm-finisher", + "strands.operation": "Agent.stream", + "strands.stop_reason": "toolUse" + } + metrics: { + "completion_tokens": 21, + "duration": "", + "prompt_tokens": 191, + "tokens": 212 + } + └── Strands model: gpt-4o-mini-2024-07-18 [llm] + metadata: { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.operation": "model.stream", + "strands.stop_reason": "toolUse" + } + metrics: { + "completion_tokens": 21, + "duration": "", + "prompt_tokens": 191, + "tokens": 212 + } diff --git a/e2e/scenarios/strands-agent-sdk-instrumentation/__snapshots__/strands-agent-sdk-v160-wrapped.span-tree.json b/e2e/scenarios/strands-agent-sdk-instrumentation/__snapshots__/strands-agent-sdk-v160-wrapped.span-tree.json new file mode 100644 index 000000000..ef147ae2b --- /dev/null +++ b/e2e/scenarios/strands-agent-sdk-instrumentation/__snapshots__/strands-agent-sdk-v160-wrapped.span-tree.json @@ -0,0 +1,634 @@ +{ + "span_tree": [ + { + "name": "strands-agent-sdk-instrumentation-root", + "type": "task", + "children": [ + { + "name": "strands-agent-invoke-operation", + "children": [ + { + "name": "Agent: weather-agent", + "type": "task", + "children": [ + { + "name": "Strands model: gpt-4o-mini-2024-07-18", + "type": "llm", + "children": [], + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.operation": "model.stream", + "strands.stop_reason": "toolUse" + }, + "metrics": { + "completion_tokens": 15, + "duration": "", + "prompt_tokens": 86, + "tokens": 101 + } + }, + { + "name": "tool: lookup_weather", + "type": "tool", + "children": [], + "input": { + "city": "Vienna" + }, + "output": { + "toolResult": { + "content": [ + { + "json": { + "city": "Vienna", + "forecast": "sunny", + "marker": "STRANDS_TOOL_OK" + } + } + ], + "status": "success", + "toolUseId": "" + } + }, + "metadata": { + "gen_ai.tool.name": "lookup_weather", + "provider": "strands", + "strands.operation": "tool.call", + "strands.tool.name": "lookup_weather" + }, + "metrics": { + "duration": "" + } + }, + { + "name": "Strands model: gpt-4o-mini-2024-07-18", + "type": "llm", + "children": [], + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.operation": "model.stream", + "strands.stop_reason": "endTurn" + }, + "metrics": { + "completion_tokens": 6, + "duration": "", + "prompt_tokens": 127, + "tokens": 133 + } + } + ], + "input": "Use lookup_weather for Vienna, then answer exactly STRANDS_AGENT_TOOL_OK.", + "output": { + "content": [ + { + "text": "STRANDS_AGENT_TOOL_OK" + } + ], + "metadata": { + "usage": { + "inputTokens": 127, + "outputTokens": 6, + "totalTokens": 133 + } + }, + "role": "assistant" + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.agent.id": "weather-agent", + "strands.agent.name": "weather-agent", + "strands.operation": "Agent.stream", + "strands.stop_reason": "endTurn" + }, + "metrics": { + "completion_tokens": 21, + "duration": "", + "prompt_tokens": 213, + "tokens": 234 + } + } + ], + "metadata": { + "operation": "agent-invoke" + } + }, + { + "name": "strands-agent-stream-operation", + "children": [ + { + "name": "Agent: stream-agent", + "type": "task", + "children": [ + { + "name": "Strands model: gpt-4o-mini-2024-07-18", + "type": "llm", + "children": [], + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.operation": "model.stream", + "strands.stop_reason": "endTurn" + }, + "metrics": { + "completion_tokens": 6, + "duration": "", + "prompt_tokens": 27, + "tokens": 33 + } + } + ], + "input": "Reply exactly STRANDS_AGENT_STREAM_OK.", + "output": { + "content": [ + { + "text": "STRANDS_AGENT_STREAM_OK." + } + ], + "metadata": { + "usage": { + "inputTokens": 27, + "outputTokens": 6, + "totalTokens": 33 + } + }, + "role": "assistant" + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.agent.id": "stream-agent", + "strands.agent.name": "stream-agent", + "strands.operation": "Agent.stream", + "strands.stop_reason": "endTurn" + }, + "metrics": { + "completion_tokens": 6, + "duration": "", + "prompt_tokens": 27, + "tokens": 33 + } + } + ], + "metadata": { + "operation": "agent-stream" + } + }, + { + "name": "strands-graph-invoke-operation", + "children": [ + { + "name": "Strands Graph", + "type": "task", + "children": [ + { + "name": "node: graph-researcher", + "type": "task", + "children": [ + { + "name": "Agent: graph-researcher", + "type": "task", + "children": [ + { + "name": "Strands model: gpt-4o-mini-2024-07-18", + "type": "llm", + "children": [], + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.operation": "model.stream", + "strands.stop_reason": "endTurn" + }, + "metrics": { + "completion_tokens": 4, + "duration": "", + "prompt_tokens": 35, + "tokens": 39 + } + } + ], + "input": "Have the first node finish, then have the writer answer exactly STRANDS_GRAPH_OK.", + "output": { + "content": [ + { + "text": "GRAPH_RESEARCH_DONE" + } + ], + "metadata": { + "usage": { + "inputTokens": 35, + "outputTokens": 4, + "totalTokens": 39 + } + }, + "role": "assistant" + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.agent.id": "graph-researcher", + "strands.agent.name": "graph-researcher", + "strands.operation": "Agent.stream", + "strands.stop_reason": "endTurn" + }, + "metrics": { + "completion_tokens": 4, + "duration": "", + "prompt_tokens": 35, + "tokens": 39 + } + } + ], + "output": "GRAPH_RESEARCH_DONE", + "metadata": { + "provider": "strands", + "strands.node.id": "graph-researcher", + "strands.node.status": "COMPLETED", + "strands.node.type": "agentNode", + "strands.operation": "node.call" + }, + "metrics": { + "completion_tokens": 4, + "duration": "", + "prompt_tokens": 35, + "strands.node.duration_ms": "", + "tokens": 39 + } + }, + { + "name": "node: graph-writer", + "type": "task", + "children": [ + { + "name": "Agent: graph-writer", + "type": "task", + "children": [ + { + "name": "Strands model: gpt-4o-mini-2024-07-18", + "type": "llm", + "children": [], + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.operation": "model.stream", + "strands.stop_reason": "endTurn" + }, + "metrics": { + "completion_tokens": 4, + "duration": "", + "prompt_tokens": 46, + "tokens": 50 + } + } + ], + "input": [ + { + "text": "Have the first node finish, then have the writer answer exactly STRANDS_GRAPH_OK." + }, + { + "text": "[node: graph-researcher]" + }, + { + "text": "GRAPH_RESEARCH_DONE" + } + ], + "output": { + "content": [ + { + "text": "STRANDS_GRAPH_OK" + } + ], + "metadata": { + "usage": { + "inputTokens": 46, + "outputTokens": 4, + "totalTokens": 50 + } + }, + "role": "assistant" + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.agent.id": "graph-writer", + "strands.agent.name": "graph-writer", + "strands.operation": "Agent.stream", + "strands.stop_reason": "endTurn" + }, + "metrics": { + "completion_tokens": 4, + "duration": "", + "prompt_tokens": 46, + "tokens": 50 + } + } + ], + "output": "STRANDS_GRAPH_OK", + "metadata": { + "provider": "strands", + "strands.node.id": "graph-writer", + "strands.node.status": "COMPLETED", + "strands.node.type": "agentNode", + "strands.operation": "node.call" + }, + "metrics": { + "completion_tokens": 4, + "duration": "", + "prompt_tokens": 46, + "strands.node.duration_ms": "", + "tokens": 50 + } + } + ], + "input": "Have the first node finish, then have the writer answer exactly STRANDS_GRAPH_OK.", + "output": "STRANDS_GRAPH_OK", + "metadata": { + "provider": "strands", + "strands.handoffs": [ + { + "source": "graph-researcher", + "targets": [ + "graph-writer" + ] + } + ], + "strands.operation": "Graph.stream", + "strands.orchestrator.id": "weather-graph", + "strands.status": "COMPLETED" + }, + "metrics": { + "completion_tokens": 8, + "duration": "", + "prompt_tokens": 81, + "tokens": 89 + } + } + ], + "metadata": { + "operation": "graph-invoke" + } + }, + { + "name": "strands-overlap-parent-operation", + "children": [ + { + "name": "Strands Graph", + "type": "task", + "children": [ + { + "name": "node: overlap-node-a", + "type": "task", + "children": [], + "output": "OVERLAP_NODE_A_OK", + "metadata": { + "provider": "strands", + "strands.node.id": "overlap-node-a", + "strands.node.status": "COMPLETED", + "strands.node.type": "agentNode", + "strands.operation": "node.call" + }, + "metrics": { + "duration": "", + "strands.node.duration_ms": "" + } + }, + { + "name": "node: overlap-node-b", + "type": "task", + "children": [], + "output": "OVERLAP_NODE_B_OK", + "metadata": { + "provider": "strands", + "strands.node.id": "overlap-node-b", + "strands.node.status": "COMPLETED", + "strands.node.type": "agentNode", + "strands.operation": "node.call" + }, + "metrics": { + "duration": "", + "strands.node.duration_ms": "" + } + } + ], + "input": "overlap graph input", + "output": "OVERLAP_GRAPH_OK", + "metadata": { + "provider": "strands", + "strands.operation": "Graph.stream", + "strands.orchestrator.id": "overlap-graph", + "strands.status": "COMPLETED" + }, + "metrics": { + "duration": "" + } + }, + { + "name": "Agent: overlap-agent", + "type": "task", + "children": [], + "input": "overlap-node-a work", + "output": { + "content": [ + { + "text": "OVERLAP_AGENT_OK overlap-node-a work" + } + ], + "role": "assistant" + }, + "metadata": { + "provider": "strands", + "strands.agent.id": "overlap-agent", + "strands.agent.name": "overlap-agent", + "strands.operation": "Agent.stream", + "strands.stop_reason": "endTurn" + }, + "metrics": { + "completion_tokens": 1, + "duration": "", + "prompt_tokens": 1, + "tokens": 2 + } + } + ], + "metadata": { + "operation": "overlap-parent" + } + }, + { + "name": "strands-swarm-invoke-operation", + "children": [ + { + "name": "Strands Swarm", + "type": "task", + "children": [ + { + "name": "node: swarm-router", + "type": "task", + "children": [ + { + "name": "Agent: swarm-router", + "type": "task", + "children": [ + { + "name": "Strands model: gpt-4o-mini-2024-07-18", + "type": "llm", + "children": [], + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.operation": "model.stream", + "strands.stop_reason": "toolUse" + }, + "metrics": { + "completion_tokens": 31, + "duration": "", + "prompt_tokens": 198, + "tokens": 229 + } + } + ], + "input": "Route to the finisher, then finish exactly STRANDS_SWARM_OK.", + "output": { + "agentId": "", + "message": "finish with STRANDS_SWARM_OK" + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.agent.id": "swarm-router", + "strands.agent.name": "swarm-router", + "strands.operation": "Agent.stream", + "strands.stop_reason": "toolUse" + }, + "metrics": { + "completion_tokens": 31, + "duration": "", + "prompt_tokens": 198, + "tokens": 229 + } + } + ], + "output": { + "agentId": "", + "message": "finish with STRANDS_SWARM_OK" + }, + "metadata": { + "provider": "strands", + "strands.node.id": "swarm-router", + "strands.node.status": "COMPLETED", + "strands.node.type": "agentNode", + "strands.operation": "node.call" + }, + "metrics": { + "completion_tokens": 31, + "duration": "", + "prompt_tokens": 198, + "strands.node.duration_ms": "", + "tokens": 229 + } + }, + { + "name": "node: swarm-finisher", + "type": "task", + "children": [ + { + "name": "Agent: swarm-finisher", + "type": "task", + "children": [ + { + "name": "Strands model: gpt-4o-mini-2024-07-18", + "type": "llm", + "children": [], + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.operation": "model.stream", + "strands.stop_reason": "toolUse" + }, + "metrics": { + "completion_tokens": 21, + "duration": "", + "prompt_tokens": 191, + "tokens": 212 + } + } + ], + "input": [ + { + "text": "finish with STRANDS_SWARM_OK" + } + ], + "output": { + "message": "STRANDS_SWARM_OK" + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.agent.id": "swarm-finisher", + "strands.agent.name": "swarm-finisher", + "strands.operation": "Agent.stream", + "strands.stop_reason": "toolUse" + }, + "metrics": { + "completion_tokens": 21, + "duration": "", + "prompt_tokens": 191, + "tokens": 212 + } + } + ], + "output": { + "message": "STRANDS_SWARM_OK" + }, + "metadata": { + "provider": "strands", + "strands.node.id": "swarm-finisher", + "strands.node.status": "COMPLETED", + "strands.node.type": "agentNode", + "strands.operation": "node.call" + }, + "metrics": { + "completion_tokens": 21, + "duration": "", + "prompt_tokens": 191, + "strands.node.duration_ms": "", + "tokens": 212 + } + } + ], + "input": "Route to the finisher, then finish exactly STRANDS_SWARM_OK.", + "output": "STRANDS_SWARM_OK", + "metadata": { + "provider": "strands", + "strands.handoffs": [ + { + "source": "swarm-router", + "targets": [ + "swarm-finisher" + ] + } + ], + "strands.operation": "Swarm.stream", + "strands.orchestrator.id": "weather-swarm", + "strands.status": "COMPLETED" + }, + "metrics": { + "completion_tokens": 52, + "duration": "", + "prompt_tokens": 389, + "tokens": 441 + } + } + ], + "metadata": { + "operation": "swarm-invoke" + } + } + ], + "metadata": { + "scenario": "strands-agent-sdk-instrumentation" + } + } + ] +} diff --git a/e2e/scenarios/strands-agent-sdk-instrumentation/__snapshots__/strands-agent-sdk-v160-wrapped.span-tree.txt b/e2e/scenarios/strands-agent-sdk-instrumentation/__snapshots__/strands-agent-sdk-v160-wrapped.span-tree.txt new file mode 100644 index 000000000..998ea28c2 --- /dev/null +++ b/e2e/scenarios/strands-agent-sdk-instrumentation/__snapshots__/strands-agent-sdk-v160-wrapped.span-tree.txt @@ -0,0 +1,497 @@ +span_tree: +└── strands-agent-sdk-instrumentation-root [task] + metadata: { + "scenario": "strands-agent-sdk-instrumentation" + } + ├── strands-agent-invoke-operation + │ metadata: { + │ "operation": "agent-invoke" + │ } + │ └── Agent: weather-agent [task] + │ input: "Use lookup_weather for Vienna, then answer exactly STRANDS_AGENT_TOOL_OK." + │ output: { + │ "content": [ + │ { + │ "text": "STRANDS_AGENT_TOOL_OK" + │ } + │ ], + │ "metadata": { + │ "usage": { + │ "inputTokens": 127, + │ "outputTokens": 6, + │ "totalTokens": 133 + │ } + │ }, + │ "role": "assistant" + │ } + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18", + │ "provider": "openai", + │ "strands.agent.id": "weather-agent", + │ "strands.agent.name": "weather-agent", + │ "strands.operation": "Agent.stream", + │ "strands.stop_reason": "endTurn" + │ } + │ metrics: { + │ "completion_tokens": 21, + │ "duration": "", + │ "prompt_tokens": 213, + │ "tokens": 234 + │ } + │ ├── Strands model: gpt-4o-mini-2024-07-18 [llm] + │ │ metadata: { + │ │ "model": "gpt-4o-mini-2024-07-18", + │ │ "provider": "openai", + │ │ "strands.operation": "model.stream", + │ │ "strands.stop_reason": "toolUse" + │ │ } + │ │ metrics: { + │ │ "completion_tokens": 15, + │ │ "duration": "", + │ │ "prompt_tokens": 86, + │ │ "tokens": 101 + │ │ } + │ ├── tool: lookup_weather [tool] + │ │ input: { + │ │ "city": "Vienna" + │ │ } + │ │ output: { + │ │ "toolResult": { + │ │ "content": [ + │ │ { + │ │ "json": { + │ │ "city": "Vienna", + │ │ "forecast": "sunny", + │ │ "marker": "STRANDS_TOOL_OK" + │ │ } + │ │ } + │ │ ], + │ │ "status": "success", + │ │ "toolUseId": "" + │ │ } + │ │ } + │ │ metadata: { + │ │ "gen_ai.tool.name": "lookup_weather", + │ │ "provider": "strands", + │ │ "strands.operation": "tool.call", + │ │ "strands.tool.name": "lookup_weather" + │ │ } + │ │ metrics: { + │ │ "duration": "" + │ │ } + │ └── Strands model: gpt-4o-mini-2024-07-18 [llm] + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18", + │ "provider": "openai", + │ "strands.operation": "model.stream", + │ "strands.stop_reason": "endTurn" + │ } + │ metrics: { + │ "completion_tokens": 6, + │ "duration": "", + │ "prompt_tokens": 127, + │ "tokens": 133 + │ } + ├── strands-agent-stream-operation + │ metadata: { + │ "operation": "agent-stream" + │ } + │ └── Agent: stream-agent [task] + │ input: "Reply exactly STRANDS_AGENT_STREAM_OK." + │ output: { + │ "content": [ + │ { + │ "text": "STRANDS_AGENT_STREAM_OK." + │ } + │ ], + │ "metadata": { + │ "usage": { + │ "inputTokens": 27, + │ "outputTokens": 6, + │ "totalTokens": 33 + │ } + │ }, + │ "role": "assistant" + │ } + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18", + │ "provider": "openai", + │ "strands.agent.id": "stream-agent", + │ "strands.agent.name": "stream-agent", + │ "strands.operation": "Agent.stream", + │ "strands.stop_reason": "endTurn" + │ } + │ metrics: { + │ "completion_tokens": 6, + │ "duration": "", + │ "prompt_tokens": 27, + │ "tokens": 33 + │ } + │ └── Strands model: gpt-4o-mini-2024-07-18 [llm] + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18", + │ "provider": "openai", + │ "strands.operation": "model.stream", + │ "strands.stop_reason": "endTurn" + │ } + │ metrics: { + │ "completion_tokens": 6, + │ "duration": "", + │ "prompt_tokens": 27, + │ "tokens": 33 + │ } + ├── strands-graph-invoke-operation + │ metadata: { + │ "operation": "graph-invoke" + │ } + │ └── Strands Graph [task] + │ input: "Have the first node finish, then have the writer answer exactly STRANDS_GRAPH_OK." + │ output: "STRANDS_GRAPH_OK" + │ metadata: { + │ "provider": "strands", + │ "strands.handoffs": [ + │ { + │ "source": "graph-researcher", + │ "targets": [ + │ "graph-writer" + │ ] + │ } + │ ], + │ "strands.operation": "Graph.stream", + │ "strands.orchestrator.id": "weather-graph", + │ "strands.status": "COMPLETED" + │ } + │ metrics: { + │ "completion_tokens": 8, + │ "duration": "", + │ "prompt_tokens": 81, + │ "tokens": 89 + │ } + │ ├── node: graph-researcher [task] + │ │ output: "GRAPH_RESEARCH_DONE" + │ │ metadata: { + │ │ "provider": "strands", + │ │ "strands.node.id": "graph-researcher", + │ │ "strands.node.status": "COMPLETED", + │ │ "strands.node.type": "agentNode", + │ │ "strands.operation": "node.call" + │ │ } + │ │ metrics: { + │ │ "completion_tokens": 4, + │ │ "duration": "", + │ │ "prompt_tokens": 35, + │ │ "strands.node.duration_ms": "", + │ │ "tokens": 39 + │ │ } + │ │ └── Agent: graph-researcher [task] + │ │ input: "Have the first node finish, then have the writer answer exactly STRANDS_GRAPH_OK." + │ │ output: { + │ │ "content": [ + │ │ { + │ │ "text": "GRAPH_RESEARCH_DONE" + │ │ } + │ │ ], + │ │ "metadata": { + │ │ "usage": { + │ │ "inputTokens": 35, + │ │ "outputTokens": 4, + │ │ "totalTokens": 39 + │ │ } + │ │ }, + │ │ "role": "assistant" + │ │ } + │ │ metadata: { + │ │ "model": "gpt-4o-mini-2024-07-18", + │ │ "provider": "openai", + │ │ "strands.agent.id": "graph-researcher", + │ │ "strands.agent.name": "graph-researcher", + │ │ "strands.operation": "Agent.stream", + │ │ "strands.stop_reason": "endTurn" + │ │ } + │ │ metrics: { + │ │ "completion_tokens": 4, + │ │ "duration": "", + │ │ "prompt_tokens": 35, + │ │ "tokens": 39 + │ │ } + │ │ └── Strands model: gpt-4o-mini-2024-07-18 [llm] + │ │ metadata: { + │ │ "model": "gpt-4o-mini-2024-07-18", + │ │ "provider": "openai", + │ │ "strands.operation": "model.stream", + │ │ "strands.stop_reason": "endTurn" + │ │ } + │ │ metrics: { + │ │ "completion_tokens": 4, + │ │ "duration": "", + │ │ "prompt_tokens": 35, + │ │ "tokens": 39 + │ │ } + │ └── node: graph-writer [task] + │ output: "STRANDS_GRAPH_OK" + │ metadata: { + │ "provider": "strands", + │ "strands.node.id": "graph-writer", + │ "strands.node.status": "COMPLETED", + │ "strands.node.type": "agentNode", + │ "strands.operation": "node.call" + │ } + │ metrics: { + │ "completion_tokens": 4, + │ "duration": "", + │ "prompt_tokens": 46, + │ "strands.node.duration_ms": "", + │ "tokens": 50 + │ } + │ └── Agent: graph-writer [task] + │ input: [ + │ { + │ "text": "Have the first node finish, then have the writer answer exactly STRANDS_GRAPH_OK." + │ }, + │ { + │ "text": "[node: graph-researcher]" + │ }, + │ { + │ "text": "GRAPH_RESEARCH_DONE" + │ } + │ ] + │ output: { + │ "content": [ + │ { + │ "text": "STRANDS_GRAPH_OK" + │ } + │ ], + │ "metadata": { + │ "usage": { + │ "inputTokens": 46, + │ "outputTokens": 4, + │ "totalTokens": 50 + │ } + │ }, + │ "role": "assistant" + │ } + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18", + │ "provider": "openai", + │ "strands.agent.id": "graph-writer", + │ "strands.agent.name": "graph-writer", + │ "strands.operation": "Agent.stream", + │ "strands.stop_reason": "endTurn" + │ } + │ metrics: { + │ "completion_tokens": 4, + │ "duration": "", + │ "prompt_tokens": 46, + │ "tokens": 50 + │ } + │ └── Strands model: gpt-4o-mini-2024-07-18 [llm] + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18", + │ "provider": "openai", + │ "strands.operation": "model.stream", + │ "strands.stop_reason": "endTurn" + │ } + │ metrics: { + │ "completion_tokens": 4, + │ "duration": "", + │ "prompt_tokens": 46, + │ "tokens": 50 + │ } + ├── strands-overlap-parent-operation + │ metadata: { + │ "operation": "overlap-parent" + │ } + │ ├── Strands Graph [task] + │ │ input: "overlap graph input" + │ │ output: "OVERLAP_GRAPH_OK" + │ │ metadata: { + │ │ "provider": "strands", + │ │ "strands.operation": "Graph.stream", + │ │ "strands.orchestrator.id": "overlap-graph", + │ │ "strands.status": "COMPLETED" + │ │ } + │ │ metrics: { + │ │ "duration": "" + │ │ } + │ │ ├── node: overlap-node-a [task] + │ │ │ output: "OVERLAP_NODE_A_OK" + │ │ │ metadata: { + │ │ │ "provider": "strands", + │ │ │ "strands.node.id": "overlap-node-a", + │ │ │ "strands.node.status": "COMPLETED", + │ │ │ "strands.node.type": "agentNode", + │ │ │ "strands.operation": "node.call" + │ │ │ } + │ │ │ metrics: { + │ │ │ "duration": "", + │ │ │ "strands.node.duration_ms": "" + │ │ │ } + │ │ └── node: overlap-node-b [task] + │ │ output: "OVERLAP_NODE_B_OK" + │ │ metadata: { + │ │ "provider": "strands", + │ │ "strands.node.id": "overlap-node-b", + │ │ "strands.node.status": "COMPLETED", + │ │ "strands.node.type": "agentNode", + │ │ "strands.operation": "node.call" + │ │ } + │ │ metrics: { + │ │ "duration": "", + │ │ "strands.node.duration_ms": "" + │ │ } + │ └── Agent: overlap-agent [task] + │ input: "overlap-node-a work" + │ output: { + │ "content": [ + │ { + │ "text": "OVERLAP_AGENT_OK overlap-node-a work" + │ } + │ ], + │ "role": "assistant" + │ } + │ metadata: { + │ "provider": "strands", + │ "strands.agent.id": "overlap-agent", + │ "strands.agent.name": "overlap-agent", + │ "strands.operation": "Agent.stream", + │ "strands.stop_reason": "endTurn" + │ } + │ metrics: { + │ "completion_tokens": 1, + │ "duration": "", + │ "prompt_tokens": 1, + │ "tokens": 2 + │ } + └── strands-swarm-invoke-operation + metadata: { + "operation": "swarm-invoke" + } + └── Strands Swarm [task] + input: "Route to the finisher, then finish exactly STRANDS_SWARM_OK." + output: "STRANDS_SWARM_OK" + metadata: { + "provider": "strands", + "strands.handoffs": [ + { + "source": "swarm-router", + "targets": [ + "swarm-finisher" + ] + } + ], + "strands.operation": "Swarm.stream", + "strands.orchestrator.id": "weather-swarm", + "strands.status": "COMPLETED" + } + metrics: { + "completion_tokens": 52, + "duration": "", + "prompt_tokens": 389, + "tokens": 441 + } + ├── node: swarm-router [task] + │ output: { + │ "agentId": "", + │ "message": "finish with STRANDS_SWARM_OK" + │ } + │ metadata: { + │ "provider": "strands", + │ "strands.node.id": "swarm-router", + │ "strands.node.status": "COMPLETED", + │ "strands.node.type": "agentNode", + │ "strands.operation": "node.call" + │ } + │ metrics: { + │ "completion_tokens": 31, + │ "duration": "", + │ "prompt_tokens": 198, + │ "strands.node.duration_ms": "", + │ "tokens": 229 + │ } + │ └── Agent: swarm-router [task] + │ input: "Route to the finisher, then finish exactly STRANDS_SWARM_OK." + │ output: { + │ "agentId": "", + │ "message": "finish with STRANDS_SWARM_OK" + │ } + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18", + │ "provider": "openai", + │ "strands.agent.id": "swarm-router", + │ "strands.agent.name": "swarm-router", + │ "strands.operation": "Agent.stream", + │ "strands.stop_reason": "toolUse" + │ } + │ metrics: { + │ "completion_tokens": 31, + │ "duration": "", + │ "prompt_tokens": 198, + │ "tokens": 229 + │ } + │ └── Strands model: gpt-4o-mini-2024-07-18 [llm] + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18", + │ "provider": "openai", + │ "strands.operation": "model.stream", + │ "strands.stop_reason": "toolUse" + │ } + │ metrics: { + │ "completion_tokens": 31, + │ "duration": "", + │ "prompt_tokens": 198, + │ "tokens": 229 + │ } + └── node: swarm-finisher [task] + output: { + "message": "STRANDS_SWARM_OK" + } + metadata: { + "provider": "strands", + "strands.node.id": "swarm-finisher", + "strands.node.status": "COMPLETED", + "strands.node.type": "agentNode", + "strands.operation": "node.call" + } + metrics: { + "completion_tokens": 21, + "duration": "", + "prompt_tokens": 191, + "strands.node.duration_ms": "", + "tokens": 212 + } + └── Agent: swarm-finisher [task] + input: [ + { + "text": "finish with STRANDS_SWARM_OK" + } + ] + output: { + "message": "STRANDS_SWARM_OK" + } + metadata: { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.agent.id": "swarm-finisher", + "strands.agent.name": "swarm-finisher", + "strands.operation": "Agent.stream", + "strands.stop_reason": "toolUse" + } + metrics: { + "completion_tokens": 21, + "duration": "", + "prompt_tokens": 191, + "tokens": 212 + } + └── Strands model: gpt-4o-mini-2024-07-18 [llm] + metadata: { + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai", + "strands.operation": "model.stream", + "strands.stop_reason": "toolUse" + } + metrics: { + "completion_tokens": 21, + "duration": "", + "prompt_tokens": 191, + "tokens": 212 + } diff --git a/e2e/scenarios/strands-agent-sdk-instrumentation/assertions.ts b/e2e/scenarios/strands-agent-sdk-instrumentation/assertions.ts new file mode 100644 index 000000000..16f4bed37 --- /dev/null +++ b/e2e/scenarios/strands-agent-sdk-instrumentation/assertions.ts @@ -0,0 +1,496 @@ +import { beforeAll, describe, expect, test } from "vitest"; +import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; +import type { Json } from "../../helpers/normalize"; +import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; +import { + effectiveScenarioTimeoutMs, + withScenarioHarness, + type ScenarioRunContext, +} from "../../helpers/scenario-harness"; +import { + matchSpanTreeSnapshot, + spanTreeFields, + type SpanTreeEntry, +} from "../../helpers/span-tree"; +import { findChildSpans, findLatestSpan } from "../../helpers/trace-selectors"; +import { MODEL_NAME, ROOT_NAME, SCENARIO_NAME } from "./scenario.impl.mjs"; + +type RunStrandsAgentSDKScenario = (harness: { + runNodeScenarioDir: (options: { + entry: string; + nodeArgs: string[]; + runContext?: ScenarioRunContext; + scenarioDir: string; + timeoutMs: number; + }) => Promise; + runScenarioDir: (options: { + entry: string; + runContext?: ScenarioRunContext; + scenarioDir: string; + timeoutMs: number; + }) => Promise; +}) => Promise; + +const SNAPSHOT_METADATA_KEYS = [ + "gen_ai.tool.name", + "model", + "operation", + "provider", + "scenario", + "strands.agent.id", + "strands.agent.name", + "strands.handoffs", + "strands.model.api", + "strands.node.id", + "strands.node.status", + "strands.node.type", + "strands.operation", + "strands.orchestrator.id", + "strands.status", + "strands.stop_reason", + "strands.tool.name", +] as const; + +const OPERATION_NAMES = [ + "strands-agent-invoke-operation", + "strands-agent-stream-operation", + "strands-graph-invoke-operation", + "strands-overlap-parent-operation", + "strands-swarm-invoke-operation", +]; + +function pickSnapshotMetadata( + value: unknown, +): Record | undefined { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return undefined; + } + + const metadata = Object.fromEntries( + Object.entries(value).filter(([key]) => + SNAPSHOT_METADATA_KEYS.includes( + key as (typeof SNAPSHOT_METADATA_KEYS)[number], + ), + ), + ) as Record; + return Object.keys(metadata).length > 0 ? metadata : undefined; +} + +function normalizeValue(value: unknown): Json | undefined { + if (value === undefined) { + return undefined; + } + if (typeof value === "string") { + return value.replace(/call_[A-Za-z0-9_-]+/g, ""); + } + if (Array.isArray(value)) { + return value.map((entry) => normalizeValue(entry) ?? null) as Json; + } + if (value && typeof value === "object") { + return Object.fromEntries( + Object.entries(value).map(([key, entry]) => [ + key, + normalizeValue(entry) ?? null, + ]), + ) as Json; + } + return value as Json; +} + +function normalizeSnapshotMetrics( + value: unknown, +): Record | undefined { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return undefined; + } + + const metrics = Object.fromEntries( + Object.entries(value) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, entry]) => { + if (key === "start" || key === "end") { + return [key, ""]; + } + if ( + key === "duration" || + key === "strands.duration_ms" || + key === "strands.latency_ms" || + key === "strands.node.duration_ms" || + key === "strands.time_to_first_byte_ms" + ) { + return [key, ""]; + } + return [key, normalizeValue(entry) ?? null]; + }), + ) as Record; + + return Object.keys(metrics).length > 0 ? metrics : undefined; +} + +function snapshotFields(event: CapturedLogEvent) { + const fields = spanTreeFields(event); + + return { + span_attributes: fields.span_attributes, + input: + event.span.type === "llm" || event.span.type === undefined + ? undefined + : normalizeValue(fields.input), + output: + event.span.type === "llm" || event.span.type === undefined + ? undefined + : normalizeValue(fields.output), + metadata: normalizeValue(pickSnapshotMetadata(fields.metadata)), + metrics: normalizeSnapshotMetrics(fields.metrics), + error: fields.error, + }; +} + +function latestEventsPerSpan(events: CapturedLogEvent[]): CapturedLogEvent[] { + const orderedSpanIds: string[] = []; + const latestBySpanId = new Map(); + + for (const event of events) { + if (!event.span.id) { + continue; + } + if (!latestBySpanId.has(event.span.id)) { + orderedSpanIds.push(event.span.id); + } + latestBySpanId.set(event.span.id, event); + } + + return orderedSpanIds.flatMap((spanId) => { + const event = latestBySpanId.get(spanId); + return event ? [event] : []; + }); +} + +function summarize(events: CapturedLogEvent[]): SpanTreeEntry[] { + const latestEvents = latestEventsPerSpan(events); + const names = new Set([ + ROOT_NAME, + ...OPERATION_NAMES, + "Agent: weather-agent", + "Agent: stream-agent", + "Agent: graph-researcher", + "Agent: graph-writer", + "Agent: overlap-agent", + "Agent: swarm-router", + "Agent: swarm-finisher", + `Strands model: ${MODEL_NAME}`, + "Strands Graph", + "Strands Swarm", + "node: graph-researcher", + "node: graph-writer", + "node: overlap-node-a", + "node: overlap-node-b", + "node: swarm-router", + "node: swarm-finisher", + "tool: lookup_weather", + ]); + + return latestEvents + .filter((event) => event.span.name && names.has(event.span.name)) + .map((event) => ({ + event, + fields: snapshotFields(event), + })); +} + +function findLatestChild( + events: CapturedLogEvent[], + name: string, + parentId: string | undefined, +): CapturedLogEvent | undefined { + return findChildSpans(events, name, parentId).at(-1); +} + +export function defineStrandsAgentSDKInstrumentationAssertions(options: { + name: string; + expectOverlapParentProbe?: boolean; + runScenario: RunStrandsAgentSDKScenario; + snapshotName: string; + testFileUrl: string; + timeoutMs: number; +}): void { + const spanSnapshotPath = resolveFileSnapshotPath( + options.testFileUrl, + `${options.snapshotName}.span-tree.json`, + ); + const timeoutMs = effectiveScenarioTimeoutMs(options.timeoutMs); + const testConfig = { timeout: timeoutMs }; + + describe(options.name, () => { + let events: CapturedLogEvent[] = []; + let setupError: string | undefined; + + beforeAll(async () => { + try { + await withScenarioHarness(async (harness) => { + await options.runScenario(harness); + events = harness.events(); + }); + } catch (error) { + setupError = error instanceof Error ? error.message : String(error); + } + }, timeoutMs); + + test("captures the root trace", testConfig, () => { + expect(setupError).toBeUndefined(); + const root = findLatestSpan(events, ROOT_NAME); + + expect(root).toBeDefined(); + expect(root?.row.metadata).toMatchObject({ scenario: SCENARIO_NAME }); + }); + + test("captures agent invoke and stream spans", testConfig, () => { + expect(setupError).toBeUndefined(); + const invokeOperation = findLatestSpan( + events, + "strands-agent-invoke-operation", + ); + const invokeAgent = findLatestChild( + events, + "Agent: weather-agent", + invokeOperation?.span.id, + ); + const invokeModels = findChildSpans( + events, + `Strands model: ${MODEL_NAME}`, + invokeAgent?.span.id, + ); + const toolSpan = findLatestChild( + events, + "tool: lookup_weather", + invokeAgent?.span.id, + ); + const streamOperation = findLatestSpan( + events, + "strands-agent-stream-operation", + ); + const streamAgent = findLatestChild( + events, + "Agent: stream-agent", + streamOperation?.span.id, + ); + const streamModels = findChildSpans( + events, + `Strands model: ${MODEL_NAME}`, + streamAgent?.span.id, + ); + + expect(invokeAgent?.span.type).toBe("task"); + expect(invokeAgent?.row.metadata).toMatchObject({ + "strands.operation": "Agent.stream", + provider: "openai", + }); + expect(JSON.stringify(invokeAgent?.output)).toContain( + "STRANDS_AGENT_TOOL_OK", + ); + + expect(invokeModels.length).toBeGreaterThanOrEqual(1); + expect(invokeModels[0]?.span.type).toBe("llm"); + expect(invokeModels[0]?.row.metadata).toMatchObject({ + model: MODEL_NAME, + provider: "openai", + "strands.operation": "model.stream", + }); + + expect(toolSpan?.span.type).toBe("tool"); + expect(toolSpan?.input).toMatchObject({ city: "Vienna" }); + expect(toolSpan?.row.metadata).toMatchObject({ + "gen_ai.tool.name": "lookup_weather", + "strands.tool.name": "lookup_weather", + }); + expect(toolSpan?.row.metadata?.["gen_ai.tool.call.id"]).toEqual( + expect.any(String), + ); + expect(JSON.stringify(toolSpan?.output)).toContain("STRANDS_TOOL_OK"); + + expect(streamAgent?.span.type).toBe("task"); + expect(JSON.stringify(streamAgent?.output)).toContain( + "STRANDS_AGENT_STREAM_OK", + ); + expect(streamModels.length).toBeGreaterThanOrEqual(1); + }); + + test("captures graph and swarm orchestration spans", testConfig, () => { + expect(setupError).toBeUndefined(); + const graphOperation = findLatestSpan( + events, + "strands-graph-invoke-operation", + ); + const graph = findLatestChild( + events, + "Strands Graph", + graphOperation?.span.id, + ); + const graphResearcherNode = findLatestChild( + events, + "node: graph-researcher", + graph?.span.id, + ); + const graphWriterNode = findLatestChild( + events, + "node: graph-writer", + graph?.span.id, + ); + const graphWriterAgent = findLatestChild( + events, + "Agent: graph-writer", + graphWriterNode?.span.id, + ); + const swarmOperation = findLatestSpan( + events, + "strands-swarm-invoke-operation", + ); + const swarm = findLatestChild( + events, + "Strands Swarm", + swarmOperation?.span.id, + ); + const swarmRouterNode = findLatestChild( + events, + "node: swarm-router", + swarm?.span.id, + ); + const swarmFinisherNode = findLatestChild( + events, + "node: swarm-finisher", + swarm?.span.id, + ); + const swarmFinisherAgent = findLatestChild( + events, + "Agent: swarm-finisher", + swarmFinisherNode?.span.id, + ); + + expect(graph?.span.type).toBe("task"); + expect(graph?.row.metadata).toMatchObject({ + "strands.operation": "Graph.stream", + "strands.orchestrator.id": "weather-graph", + provider: "strands", + }); + expect(JSON.stringify(graph?.output)).toContain("STRANDS_GRAPH_OK"); + expect(graphResearcherNode?.row.metadata).toMatchObject({ + "strands.node.id": "graph-researcher", + "strands.node.status": "COMPLETED", + }); + expect(graphWriterNode?.row.metadata).toMatchObject({ + "strands.node.id": "graph-writer", + "strands.node.status": "COMPLETED", + }); + expect(graphWriterAgent?.span.parentIds).toContain( + graphWriterNode?.span.id, + ); + expect(JSON.stringify(graphWriterAgent?.output)).toContain( + "STRANDS_GRAPH_OK", + ); + + expect(swarm?.span.type).toBe("task"); + expect(swarm?.row.metadata).toMatchObject({ + "strands.handoffs": [ + { source: "swarm-router", targets: ["swarm-finisher"] }, + ], + "strands.operation": "Swarm.stream", + "strands.orchestrator.id": "weather-swarm", + }); + expect(JSON.stringify(swarm?.output)).toContain("STRANDS_SWARM_OK"); + expect(swarmRouterNode?.row.metadata).toMatchObject({ + "strands.node.id": "swarm-router", + "strands.node.status": "COMPLETED", + }); + expect(swarmFinisherNode?.row.metadata).toMatchObject({ + "strands.node.id": "swarm-finisher", + "strands.node.status": "COMPLETED", + }); + expect(swarmFinisherAgent?.span.parentIds).toContain( + swarmFinisherNode?.span.id, + ); + expect(JSON.stringify(swarmFinisherAgent?.output)).toContain( + "STRANDS_SWARM_OK", + ); + }); + + if (options.expectOverlapParentProbe) { + test( + "does not attach ambiguous child streams to the wrong node", + testConfig, + () => { + expect(setupError).toBeUndefined(); + const operation = findLatestSpan( + events, + "strands-overlap-parent-operation", + ); + const graph = findLatestChild( + events, + "Strands Graph", + operation?.span.id, + ); + const overlapNodeA = findLatestChild( + events, + "node: overlap-node-a", + graph?.span.id, + ); + const overlapNodeB = findLatestChild( + events, + "node: overlap-node-b", + graph?.span.id, + ); + const agentUnderOperation = findLatestChild( + events, + "Agent: overlap-agent", + operation?.span.id, + ); + const agentUnderNodeA = findLatestChild( + events, + "Agent: overlap-agent", + overlapNodeA?.span.id, + ); + const agentUnderNodeB = findLatestChild( + events, + "Agent: overlap-agent", + overlapNodeB?.span.id, + ); + + expect(graph?.span.type).toBe("task"); + expect(overlapNodeA?.row.metadata).toMatchObject({ + "strands.node.id": "overlap-node-a", + "strands.node.status": "COMPLETED", + }); + expect(overlapNodeB?.row.metadata).toMatchObject({ + "strands.node.id": "overlap-node-b", + "strands.node.status": "COMPLETED", + }); + expect(agentUnderOperation?.span.type).toBe("task"); + expect(JSON.stringify(agentUnderOperation?.output)).toContain( + "OVERLAP_AGENT_OK", + ); + expect(agentUnderNodeA).toBeUndefined(); + expect(agentUnderNodeB).toBeUndefined(); + }, + ); + } + + test( + "does not emit duplicate OpenAI provider auto spans", + testConfig, + () => { + expect(setupError).toBeUndefined(); + const duplicateProviderSpans = latestEventsPerSpan(events).filter( + (event) => + event.span.name === "Chat Completion" || + event.span.name?.startsWith("openai."), + ); + + expect(duplicateProviderSpans).toHaveLength(0); + }, + ); + + test("matches the span tree snapshot", testConfig, async () => { + expect(setupError).toBeUndefined(); + await matchSpanTreeSnapshot(summarize(events), spanSnapshotPath); + }); + }); +} diff --git a/e2e/scenarios/strands-agent-sdk-instrumentation/package.json b/e2e/scenarios/strands-agent-sdk-instrumentation/package.json new file mode 100644 index 000000000..31a9471cf --- /dev/null +++ b/e2e/scenarios/strands-agent-sdk-instrumentation/package.json @@ -0,0 +1,15 @@ +{ + "private": true, + "type": "module", + "dependencies": { + "@strands-agents/sdk": "1.6.0", + "openai": "^6.7.0" + }, + "braintrustScenario": { + "canary": { + "dependencies": { + "@strands-agents/sdk": "latest" + } + } + } +} diff --git a/e2e/scenarios/strands-agent-sdk-instrumentation/pnpm-lock.yaml b/e2e/scenarios/strands-agent-sdk-instrumentation/pnpm-lock.yaml new file mode 100644 index 000000000..f620ce017 --- /dev/null +++ b/e2e/scenarios/strands-agent-sdk-instrumentation/pnpm-lock.yaml @@ -0,0 +1,1355 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@strands-agents/sdk': + specifier: 1.6.0 + version: 1.6.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(@opentelemetry/api@1.9.1)(@smithy/types@4.15.0)(express@5.2.1)(openai@6.44.0(zod@4.4.3))(zod@4.4.3) + openai: + specifier: ^6.7.0 + version: 6.44.0(zod@4.4.3) + +packages: + + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-bedrock-runtime@3.1071.0': + resolution: {integrity: sha512-1Sa7UTC98Wy+M+zYbbXEqhcL5WUE3VnAmO1Y2RikmYjWx/8nCHudjSj1bVovk6Obfrrsir9Q1AbrmKbgxwI90g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.974.22': + resolution: {integrity: sha512-YofH63shc6YRdXjz80BJkpJW+Bkn0Cuu2dn4Rv7s9G2Idt58tgtzQEWxrR2xVljlVfIBeUjPuULnSVYLke3sUQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.48': + resolution: {integrity: sha512-h6FEC95fbexUd6zxm4PdgS82bTcI2PRtUb2ZwMipb/Xr8bPwtf0G8rBo2jp7NA24Mbx2JA8/WingiYpA9RCCyw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.50': + resolution: {integrity: sha512-lJO3OLpjvz5m/RSBQmsG/CEUGsvCy5ruxKwPQaOCqxqCMuyYT2BZwQUTDZVVwqQ9LrZKuK24JSa6r31hL/tvkg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.55': + resolution: {integrity: sha512-TBoF4buBGYhXjdZAryayY2TrkQj2B2KfE/msG4V53XCt+w0EhEwM2JRjx8p2grJ2C6gtH5++SAwEvGMRdi0yyw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.54': + resolution: {integrity: sha512-hBWI3wZTdTGiuMfmPts6AWbAjFfRniOQnqx68tc2cQvRKWawFbN9wkLOVPWM1FAOyowZU73mC6Fi+rHSHNyLFw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.57': + resolution: {integrity: sha512-u6dClpzNdWf1HGWz4wwhdXi1wiOofCLniM9S4BQQGlLAN9TW7VB+ld5V533GdKrYMaFeBGFqKnj0JCYvynLqwQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.48': + resolution: {integrity: sha512-w6VZwojPt12WnEkAUy6Nu4K6sWCbBmR7QX390b0nE6vRvkXbrYr9Lq9VySGkfjiMjpUA87op+J4EgvRmtWIDoQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.972.54': + resolution: {integrity: sha512-23uZpIpF2SIFDCa1fcWa202tK4gGeyvX6GIIAjiB8WBsvsVRBMnJ/7dCxHzxf7eZT7GToJg837LDIBnZsl/VUg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.54': + resolution: {integrity: sha512-0Iv5QttS6wcATlodYKgvQj6B9Db51rx7NU9fqu0PoLeS4BIgdYMc/QK4smwLwpm5RFrs02V/eLyEFp3FklvlNQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/eventstream-handler-node@3.972.22': + resolution: {integrity: sha512-tqPJv0dz4+O0hWGm1a6YekcMZyPhDFs/zH73Von7icaVT5n0Jqvm86typ3jRrG+qoUdPhALOnboRLTmnWQTlYQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-eventstream@3.972.18': + resolution: {integrity: sha512-OHpk8YoZi3yexPq8aFt1vN1IxA2zLKvsIR5GpWYylX/ve6kQmY7wxHNSFy/D3t2apMZ16rs76Co4dJWcDyIk3A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-websocket@3.972.30': + resolution: {integrity: sha512-kH6N4f/Fzi9r/dYap8EQ+Zk4NOz8pl4AtWKhzAoG2C1/4YkIHok9APp/e+75woreWQq264n+LkrJsJVZ0Q+M1Q==} + engines: {node: '>= 14.0.0'} + + '@aws-sdk/nested-clients@3.997.22': + resolution: {integrity: sha512-4IwtcYSxEIVw5hcp8ogq0CMbFNZFw7jJUetpfFUhFFeqsa1K8j2Ihg2hnxLyOp3stMZnXda6VzOmPi1AFZQXcg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.35': + resolution: {integrity: sha512-6L/VWs+Wch2stHemCGTmUNqKLMzURxQDK5boNG3Jn3kAOp71meDUuS5sbObpEvFxHDq0uWeSLFDNSYsjNt+Dlg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1071.0': + resolution: {integrity: sha512-4LDW2Qob6LoLFuqYSYZq2AyTE9koSE9+i+n5UZcm10GpmQOK0zRD9L4uYlzItiTKksIWgC/qMFChAi3RvKYtMg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.973.13': + resolution: {integrity: sha512-pEHZqRkAlHfnfAU9tK+WpKv/gBNjGJrHMgA3A0iYRGyswBS2t0pfez+lWlwktb3Bqa0ovh7w/QJTFwp3fDxLNg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.8': + resolution: {integrity: sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.30': + resolution: {integrity: sha512-StElZPEoBquWwNqw1AcfpzEyZqJvFxouG+mpDNYlcH6ZOrqd2CuIryv+8LV8gNHZUOyKyJF3Dq9vxaXEmDR9TQ==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.2.4': + resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} + engines: {node: '>=18.0.0'} + + '@hono/node-server@1.19.14': + resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@nodable/entities@2.2.0': + resolution: {integrity: sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==} + + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@smithy/core@3.25.1': + resolution: {integrity: sha512-zpDbpXBCBsxfLtG2GEUyfgvHvSFrw5CwDZSNzL0v52gx/c3oPlPbm+7W7num8xs6vyiUBn+bvYPHcQDOXZynCQ==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.4.1': + resolution: {integrity: sha512-TSAF5NHgxEsllbErYWbK8aLnl5L601NGc5VYJlSPsKnf3YlkhdoBN+geGcaU00oiw2OK3QO5LA3QNXiiWhCidQ==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.5.1': + resolution: {integrity: sha512-96JrD1q71anokymx9Iblb+zKmNQYNstlV/25A9ZYIJ2A0rp1r7/GZAIm0bDWSmVvz3DpNOCZuabzsiL+w0UHhw==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/node-http-handler@4.8.1': + resolution: {integrity: sha512-emtXvoky671puri18ETf64AFIQUGIEA093F2drXpBgB0OGnBLjcwNR3CA2mYu62IAqNsS56xa5lnTxAgPq7cjw==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.5.1': + resolution: {integrity: sha512-X9rVls3En0z3NtrmguTmpRM0/NqtWUxBjal6fcAkwtsub+gOdLZ6kD+V7xhUgFMGdG14bHbZ7M5QjaRI1+DatQ==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.15.0': + resolution: {integrity: sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@strands-agents/sdk@1.6.0': + resolution: {integrity: sha512-tvmDHkgO7oPe8G0c3bmnTiSetb/Iden4DdSh/maRyLqDZj7uLIeS7whwrk654yM7EOYgo6p0/ewEItMbS+7m+A==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@a2a-js/sdk': ^0.3.10 + '@ai-sdk/provider': ^3.0.0 + '@anthropic-ai/sdk': ^0.92.0 + '@aws-sdk/client-bedrock-agent': ^3.943.0 + '@aws-sdk/client-bedrock-agent-runtime': ^3.943.0 + '@aws-sdk/client-s3': ^3.943.0 + '@aws/bedrock-token-generator': ^1.1.0 + '@cedar-policy/cedar-wasm': ^4.0.0 + '@cedar-policy/mcp-schema-generator-wasm': ^0.6.0 + '@google/genai': ^1.40.0 + '@modelcontextprotocol/sdk': ^1.25.2 + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/exporter-metrics-otlp-http': ^0.214.0 + '@opentelemetry/exporter-trace-otlp-http': ^0.214.0 + '@opentelemetry/resources': ^2.6.1 + '@opentelemetry/sdk-metrics': ^2.6.1 + '@opentelemetry/sdk-trace-base': ^2.6.1 + '@opentelemetry/sdk-trace-node': ^2.6.1 + '@smithy/types': ^4.0.0 + express: ^5.1.0 + openai: ^6.7.0 + zod: ^4.1.12 + peerDependenciesMeta: + '@a2a-js/sdk': + optional: true + '@ai-sdk/provider': + optional: true + '@anthropic-ai/sdk': + optional: true + '@aws-sdk/client-bedrock-agent': + optional: true + '@aws-sdk/client-bedrock-agent-runtime': + optional: true + '@aws-sdk/client-s3': + optional: true + '@aws/bedrock-token-generator': + optional: true + '@cedar-policy/cedar-wasm': + optional: true + '@cedar-policy/mcp-schema-generator-wasm': + optional: true + '@google/genai': + optional: true + '@opentelemetry/exporter-metrics-otlp-http': + optional: true + '@opentelemetry/exporter-trace-otlp-http': + optional: true + '@opentelemetry/resources': + optional: true + '@opentelemetry/sdk-metrics': + optional: true + '@opentelemetry/sdk-trace-base': + optional: true + '@opentelemetry/sdk-trace-node': + optional: true + '@smithy/types': + optional: true + express: + optional: true + openai: + optional: true + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + anynum@1.0.1: + resolution: {integrity: sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==} + + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} + engines: {node: '>=18'} + + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource-parser@3.1.0: + resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + express-rate-limit@8.5.2: + resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + + fast-xml-builder@1.2.0: + resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} + + fast-xml-parser@5.7.3: + resolution: {integrity: sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==} + hasBin: true + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + hono@4.12.26: + resolution: {integrity: sha512-uyZtpnYxM9CmQ7QsQknM4zN8EftNqhON1qYeIKM0Se67CCEe2c44xyGURwB0axX2fBDu1dqHrHAc1hmNT8ITkw==} + engines: {node: '>=16.9.0'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + openai@6.44.0: + resolution: {integrity: sha512-09/gH+8jH0RgUwsgWHAaxsKGRT5zVZ95IaJUnqAWj6XejIBmnFRwq2WUIF37VtDEsmGrtPmvCs5+yBSeZGWvkA==} + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-expression-matcher@1.5.0: + resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} + engines: {node: '>=14.0.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + strnum@2.4.1: + resolution: {integrity: sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + uuid@14.0.0: + resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} + hasBin: true + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + xml-naming@0.1.0: + resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==} + engines: {node: '>=16.0.0'} + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + +snapshots: + + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + tslib: 2.8.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + '@aws-sdk/util-locate-window': 3.965.8 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.13 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-bedrock-runtime@3.1071.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/credential-provider-node': 3.972.57 + '@aws-sdk/eventstream-handler-node': 3.972.22 + '@aws-sdk/middleware-eventstream': 3.972.18 + '@aws-sdk/middleware-websocket': 3.972.30 + '@aws-sdk/token-providers': 3.1071.0 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/core@3.974.22': + dependencies: + '@aws-sdk/types': 3.973.13 + '@aws-sdk/xml-builder': 3.972.30 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/core': 3.25.1 + '@smithy/signature-v4': 5.5.1 + '@smithy/types': 4.15.0 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.48': + dependencies: + '@aws-sdk/core': 3.974.22 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.50': + dependencies: + '@aws-sdk/core': 3.974.22 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.972.55': + dependencies: + '@aws-sdk/core': 3.974.22 + '@aws-sdk/credential-provider-env': 3.972.48 + '@aws-sdk/credential-provider-http': 3.972.50 + '@aws-sdk/credential-provider-login': 3.972.54 + '@aws-sdk/credential-provider-process': 3.972.48 + '@aws-sdk/credential-provider-sso': 3.972.54 + '@aws-sdk/credential-provider-web-identity': 3.972.54 + '@aws-sdk/nested-clients': 3.997.22 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/credential-provider-imds': 4.4.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-login@3.972.54': + dependencies: + '@aws-sdk/core': 3.974.22 + '@aws-sdk/nested-clients': 3.997.22 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.57': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.48 + '@aws-sdk/credential-provider-http': 3.972.50 + '@aws-sdk/credential-provider-ini': 3.972.55 + '@aws-sdk/credential-provider-process': 3.972.48 + '@aws-sdk/credential-provider-sso': 3.972.54 + '@aws-sdk/credential-provider-web-identity': 3.972.54 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/credential-provider-imds': 4.4.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.48': + dependencies: + '@aws-sdk/core': 3.974.22 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.972.54': + dependencies: + '@aws-sdk/core': 3.974.22 + '@aws-sdk/nested-clients': 3.997.22 + '@aws-sdk/token-providers': 3.1071.0 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.54': + dependencies: + '@aws-sdk/core': 3.974.22 + '@aws-sdk/nested-clients': 3.997.22 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/eventstream-handler-node@3.972.22': + dependencies: + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-eventstream@3.972.18': + dependencies: + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-websocket@3.972.30': + dependencies: + '@aws-sdk/core': 3.974.22 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/signature-v4': 5.5.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.22': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/signature-v4-multi-region': 3.996.35 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.35': + dependencies: + '@aws-sdk/types': 3.973.13 + '@smithy/signature-v4': 5.5.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1071.0': + dependencies: + '@aws-sdk/core': 3.974.22 + '@aws-sdk/nested-clients': 3.997.22 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/types@3.973.13': + dependencies: + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.8': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.30': + dependencies: + '@smithy/types': 4.15.0 + fast-xml-parser: 5.7.3 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.4': {} + + '@hono/node-server@1.19.14(hono@4.12.26)': + dependencies: + hono: 4.12.26 + + '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': + dependencies: + '@hono/node-server': 1.19.14(hono@4.12.26) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.1.0 + express: 5.2.1 + express-rate-limit: 8.5.2(express@5.2.1) + hono: 4.12.26 + jose: 6.2.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.4.3 + zod-to-json-schema: 3.25.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + + '@nodable/entities@2.2.0': {} + + '@opentelemetry/api@1.9.1': {} + + '@smithy/core@3.25.1': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.4.1': + dependencies: + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.5.1': + dependencies: + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/node-http-handler@4.8.1': + dependencies: + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/signature-v4@5.5.1': + dependencies: + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/types@4.15.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@strands-agents/sdk@1.6.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(@opentelemetry/api@1.9.1)(@smithy/types@4.15.0)(express@5.2.1)(openai@6.44.0(zod@4.4.3))(zod@4.4.3)': + dependencies: + '@aws-sdk/client-bedrock-runtime': 3.1071.0 + '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) + '@opentelemetry/api': 1.9.1 + '@types/json-schema': 7.0.15 + uuid: 14.0.0 + yaml: 2.9.0 + zod: 4.4.3 + optionalDependencies: + '@smithy/types': 4.15.0 + express: 5.2.1 + openai: 6.44.0(zod@4.4.3) + + '@types/json-schema@7.0.15': {} + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + anynum@1.0.1: {} + + body-parser@2.3.0: + dependencies: + bytes: 3.1.2 + content-type: 2.0.0 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.2 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + + bowser@2.14.1: {} + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + depd@2.0.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + + encodeurl@2.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + escape-html@1.0.3: {} + + etag@1.8.1: {} + + eventsource-parser@3.1.0: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.1.0 + + express-rate-limit@8.5.2(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.2.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.3.0 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.2 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-uri@3.1.2: {} + + fast-xml-builder@1.2.0: + dependencies: + path-expression-matcher: 1.5.0 + xml-naming: 0.1.0 + + fast-xml-parser@5.7.3: + dependencies: + '@nodable/entities': 2.2.0 + fast-xml-builder: 1.2.0 + path-expression-matcher: 1.5.0 + strnum: 2.4.1 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + hono@4.12.26: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + inherits@2.0.4: {} + + ip-address@10.2.0: {} + + ipaddr.js@1.9.1: {} + + is-promise@4.0.0: {} + + isexe@2.0.0: {} + + jose@6.2.3: {} + + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + ms@2.1.3: {} + + negotiator@1.0.0: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + openai@6.44.0(zod@4.4.3): + optionalDependencies: + zod: 4.4.3 + + parseurl@1.3.3: {} + + path-expression-matcher@1.5.0: {} + + path-key@3.1.1: {} + + path-to-regexp@8.4.2: {} + + pkce-challenge@5.0.1: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + qs@6.15.2: + dependencies: + side-channel: 1.1.1 + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + require-from-string@2.0.2: {} + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + safer-buffer@2.1.2: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + statuses@2.0.2: {} + + strnum@2.4.1: + dependencies: + anynum: 1.0.1 + + toidentifier@1.0.1: {} + + tslib@2.8.1: {} + + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.0 + mime-types: 3.0.2 + + unpipe@1.0.0: {} + + uuid@14.0.0: {} + + vary@1.1.2: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrappy@1.0.2: {} + + xml-naming@0.1.0: {} + + yaml@2.9.0: {} + + zod-to-json-schema@3.25.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + + zod@4.4.3: {} diff --git a/e2e/scenarios/strands-agent-sdk-instrumentation/scenario.impl.mjs b/e2e/scenarios/strands-agent-sdk-instrumentation/scenario.impl.mjs new file mode 100644 index 000000000..2d02a1146 --- /dev/null +++ b/e2e/scenarios/strands-agent-sdk-instrumentation/scenario.impl.mjs @@ -0,0 +1,354 @@ +import { wrapStrandsAgentSDK } from "braintrust"; +import { + collectAsync, + runOperation, + runTracedScenario, +} from "../../helpers/provider-runtime.mjs"; + +const MODEL_NAME = "gpt-4o-mini-2024-07-18"; +const ROOT_NAME = "strands-agent-sdk-instrumentation-root"; +const SCENARIO_NAME = "strands-agent-sdk-instrumentation"; + +function createOpenAIModel(OpenAIModel) { + return new OpenAIModel({ + api: "chat", + modelId: MODEL_NAME, + temperature: 0, + maxTokens: 160, + ...(process.env.OPENAI_BASE_URL + ? { clientConfig: { baseURL: process.env.OPENAI_BASE_URL } } + : {}), + }); +} + +function resultText(result) { + return typeof result?.toString === "function" && + result.toString !== Object.prototype.toString + ? result.toString() + : JSON.stringify(result); +} + +function expectContains(value, marker, label) { + const text = resultText(value); + if (!text.includes(marker)) { + throw new Error(`${label} did not include ${marker}: ${text}`); + } +} + +function createOverlappingParentSDK() { + class Agent { + constructor(options = {}) { + Object.assign(this, options); + } + + async *stream(input) { + const result = { + type: "agentResult", + stopReason: "endTurn", + lastMessage: { + role: "assistant", + content: [ + { + text: `OVERLAP_AGENT_OK ${input}`, + }, + ], + }, + metrics: { + accumulatedUsage: { + inputTokens: 1, + outputTokens: 1, + totalTokens: 2, + }, + }, + }; + yield { type: "agentResultEvent", result }; + return result; + } + } + + class Graph { + constructor(options = {}) { + this.id = options.id; + this.nodes = new Map( + (options.nodes ?? []).map((node) => [node.id, node]), + ); + } + + async *stream() { + yield { + type: "beforeNodeCallEvent", + nodeId: "overlap-node-a", + orchestrator: this, + }; + yield { + type: "beforeNodeCallEvent", + nodeId: "overlap-node-b", + orchestrator: this, + }; + yield { + type: "nodeResultEvent", + nodeId: "overlap-node-b", + nodeType: "agentNode", + result: { + nodeId: "overlap-node-b", + status: "COMPLETED", + content: [{ text: "OVERLAP_NODE_B_OK" }], + duration: 1, + }, + }; + yield { + type: "afterNodeCallEvent", + nodeId: "overlap-node-b", + orchestrator: this, + }; + yield { + type: "nodeResultEvent", + nodeId: "overlap-node-a", + nodeType: "agentNode", + result: { + nodeId: "overlap-node-a", + status: "COMPLETED", + content: [{ text: "OVERLAP_NODE_A_OK" }], + duration: 1, + }, + }; + yield { + type: "afterNodeCallEvent", + nodeId: "overlap-node-a", + orchestrator: this, + }; + + const result = { + type: "multiAgentResult", + status: "COMPLETED", + content: [{ text: "OVERLAP_GRAPH_OK" }], + duration: 2, + }; + yield { type: "multiAgentResultEvent", result }; + return result; + } + } + + class Swarm extends Graph {} + + return { Agent, Graph, Swarm }; +} + +async function runOverlappingParentProbe() { + const strands = wrapStrandsAgentSDK(createOverlappingParentSDK()); + const sharedAgent = new strands.Agent({ + id: "overlap-agent", + name: "overlap-agent", + }); + const graph = new strands.Graph({ + id: "overlap-graph", + nodes: [ + { + id: "overlap-node-a", + type: "agentNode", + agent: sharedAgent, + }, + { + id: "overlap-node-b", + type: "agentNode", + agent: sharedAgent, + }, + ], + }); + const graphIterator = graph.stream("overlap graph input"); + const graphEvents = []; + + graphEvents.push((await graphIterator.next()).value); + graphEvents.push((await graphIterator.next()).value); + + const agentEvents = await collectAsync( + sharedAgent.stream("overlap-node-a work"), + ); + + while (true) { + const step = await graphIterator.next(); + graphEvents.push(step.value); + if (step.done) { + break; + } + } + + if (!JSON.stringify(agentEvents).includes("OVERLAP_AGENT_OK")) { + throw new Error("overlap agent stream did not include OVERLAP_AGENT_OK"); + } + if (!JSON.stringify(graphEvents).includes("OVERLAP_GRAPH_OK")) { + throw new Error("overlap graph stream did not include OVERLAP_GRAPH_OK"); + } +} + +async function runStrandsAgentSDKInstrumentationScenario( + sdk, + OpenAIModel, + options = {}, +) { + const strands = options.decorateSDK ? options.decorateSDK(sdk) : sdk; + const { Agent, Graph, Swarm, tool } = strands; + const lookupWeather = tool({ + name: "lookup_weather", + description: "Return a deterministic weather report for one city.", + inputSchema: { + type: "object", + properties: { + city: { type: "string" }, + }, + required: ["city"], + }, + callback: ({ city }) => ({ + city, + forecast: "sunny", + marker: "STRANDS_TOOL_OK", + }), + }); + + await runTracedScenario({ + callback: async () => { + await runOperation( + "strands-agent-invoke-operation", + "agent-invoke", + async () => { + const agent = new Agent({ + id: "weather-agent", + name: "weather-agent", + model: createOpenAIModel(OpenAIModel), + printer: false, + systemPrompt: + "You are a deterministic test agent. Always call lookup_weather exactly once for Vienna, then answer exactly STRANDS_AGENT_TOOL_OK.", + tools: [lookupWeather], + }); + const result = await agent.invoke( + "Use lookup_weather for Vienna, then answer exactly STRANDS_AGENT_TOOL_OK.", + ); + + expectContains(result, "STRANDS_AGENT_TOOL_OK", "agent invoke"); + }, + ); + + await runOperation( + "strands-agent-stream-operation", + "agent-stream", + async () => { + const agent = new Agent({ + id: "stream-agent", + name: "stream-agent", + model: createOpenAIModel(OpenAIModel), + printer: false, + systemPrompt: "Reply exactly STRANDS_AGENT_STREAM_OK.", + }); + const events = await collectAsync( + agent.stream("Reply exactly STRANDS_AGENT_STREAM_OK."), + ); + + if (!JSON.stringify(events).includes("STRANDS_AGENT_STREAM_OK")) { + throw new Error( + "agent stream did not include STRANDS_AGENT_STREAM_OK", + ); + } + }, + ); + + await runOperation( + "strands-graph-invoke-operation", + "graph-invoke", + async () => { + const researcher = new Agent({ + id: "graph-researcher", + name: "graph-researcher", + model: createOpenAIModel(OpenAIModel), + printer: false, + systemPrompt: "Reply exactly GRAPH_RESEARCH_DONE.", + }); + const writer = new Agent({ + id: "graph-writer", + name: "graph-writer", + model: createOpenAIModel(OpenAIModel), + printer: false, + systemPrompt: "Reply exactly STRANDS_GRAPH_OK.", + }); + const graph = new Graph({ + id: "weather-graph", + maxSteps: 3, + nodes: [researcher, writer], + edges: [["graph-researcher", "graph-writer"]], + }); + const result = await graph.invoke( + "Have the first node finish, then have the writer answer exactly STRANDS_GRAPH_OK.", + ); + + expectContains(result, "STRANDS_GRAPH_OK", "graph invoke"); + }, + ); + + if (options.includeOverlapParentProbe) { + await runOperation( + "strands-overlap-parent-operation", + "overlap-parent", + runOverlappingParentProbe, + ); + } + + await runOperation( + "strands-swarm-invoke-operation", + "swarm-invoke", + async () => { + const router = new Agent({ + id: "swarm-router", + name: "swarm-router", + description: "Routes work to the finisher agent.", + model: createOpenAIModel(OpenAIModel), + printer: false, + systemPrompt: + "Use the structured output schema. Hand off to swarm-finisher with message finish with STRANDS_SWARM_OK.", + }); + const finisher = new Agent({ + id: "swarm-finisher", + name: "swarm-finisher", + description: "Finishes the test response.", + model: createOpenAIModel(OpenAIModel), + printer: false, + systemPrompt: + "Use the structured output schema. Do not hand off. Set the final message to exactly STRANDS_SWARM_OK.", + }); + const swarm = new Swarm({ + id: "weather-swarm", + maxSteps: 2, + nodes: [router, finisher], + start: "swarm-router", + }); + const result = await swarm.invoke( + "Route to the finisher, then finish exactly STRANDS_SWARM_OK.", + ); + + expectContains(result, "STRANDS_SWARM_OK", "swarm invoke"); + }, + ); + }, + flushCount: 2, + flushDelayMs: 250, + metadata: { + scenario: SCENARIO_NAME, + }, + projectNameBase: "e2e-strands-agent-sdk-instrumentation", + rootName: ROOT_NAME, + }); +} + +export async function runWrappedStrandsAgentSDKInstrumentation( + sdk, + OpenAIModel, +) { + await runStrandsAgentSDKInstrumentationScenario(sdk, OpenAIModel, { + decorateSDK: wrapStrandsAgentSDK, + includeOverlapParentProbe: true, + }); +} + +export async function runAutoStrandsAgentSDKInstrumentation(sdk, OpenAIModel) { + await runStrandsAgentSDKInstrumentationScenario(sdk, OpenAIModel); +} + +export { MODEL_NAME, ROOT_NAME, SCENARIO_NAME }; diff --git a/e2e/scenarios/strands-agent-sdk-instrumentation/scenario.mjs b/e2e/scenarios/strands-agent-sdk-instrumentation/scenario.mjs new file mode 100644 index 000000000..6cddc7644 --- /dev/null +++ b/e2e/scenarios/strands-agent-sdk-instrumentation/scenario.mjs @@ -0,0 +1,6 @@ +import * as strands from "@strands-agents/sdk"; +import { OpenAIModel } from "@strands-agents/sdk/models/openai"; +import { runMain } from "../../helpers/provider-runtime.mjs"; +import { runAutoStrandsAgentSDKInstrumentation } from "./scenario.impl.mjs"; + +runMain(() => runAutoStrandsAgentSDKInstrumentation(strands, OpenAIModel)); diff --git a/e2e/scenarios/strands-agent-sdk-instrumentation/scenario.test.ts b/e2e/scenarios/strands-agent-sdk-instrumentation/scenario.test.ts new file mode 100644 index 000000000..81bdac2c3 --- /dev/null +++ b/e2e/scenarios/strands-agent-sdk-instrumentation/scenario.test.ts @@ -0,0 +1,66 @@ +import { describe } from "vitest"; +import { + prepareScenarioDir, + readInstalledPackageVersion, + resolveScenarioDir, +} from "../../helpers/scenario-harness"; +import { defineStrandsAgentSDKInstrumentationAssertions } from "./assertions"; + +const originalScenarioDir = resolveScenarioDir(import.meta.url); +const scenarioDir = await prepareScenarioDir({ + scenarioDir: originalScenarioDir, +}); +const TIMEOUT_MS = 240_000; +const strandsAgentSDKScenario = { + autoEntry: "scenario.mjs", + autoSnapshotName: "strands-agent-sdk-v160-auto-hook", + version: await readInstalledPackageVersion( + scenarioDir, + "@strands-agents/sdk", + ), + wrapperEntry: "scenario.ts", + wrapperSnapshotName: "strands-agent-sdk-v160-wrapped", + variantKey: "strands-agent-sdk-v160", +}; + +describe("wrapped instrumentation", () => { + defineStrandsAgentSDKInstrumentationAssertions({ + expectOverlapParentProbe: true, + name: `Strands Agent SDK ${strandsAgentSDKScenario.version}`, + runScenario: async ({ runScenarioDir }) => { + await runScenarioDir({ + entry: strandsAgentSDKScenario.wrapperEntry, + runContext: { + variantKey: strandsAgentSDKScenario.variantKey, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + }, + snapshotName: strandsAgentSDKScenario.wrapperSnapshotName, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); +}); + +describe("auto-hook instrumentation", () => { + defineStrandsAgentSDKInstrumentationAssertions({ + name: `Strands Agent SDK ${strandsAgentSDKScenario.version}`, + runScenario: async ({ runNodeScenarioDir }) => { + await runNodeScenarioDir({ + entry: strandsAgentSDKScenario.autoEntry, + nodeArgs: ["--import", "braintrust/hook.mjs"], + runContext: { + variantKey: strandsAgentSDKScenario.variantKey, + originalScenarioDir, + }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + }, + snapshotName: strandsAgentSDKScenario.autoSnapshotName, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); +}); diff --git a/e2e/scenarios/strands-agent-sdk-instrumentation/scenario.ts b/e2e/scenarios/strands-agent-sdk-instrumentation/scenario.ts new file mode 100644 index 000000000..be2d50c8c --- /dev/null +++ b/e2e/scenarios/strands-agent-sdk-instrumentation/scenario.ts @@ -0,0 +1,6 @@ +import * as strands from "@strands-agents/sdk"; +import { OpenAIModel } from "@strands-agents/sdk/models/openai"; +import { runMain } from "../../helpers/provider-runtime.mjs"; +import { runWrappedStrandsAgentSDKInstrumentation } from "./scenario.impl.mjs"; + +runMain(() => runWrappedStrandsAgentSDKInstrumentation(strands, OpenAIModel)); diff --git a/js/src/auto-instrumentations/configs/all.ts b/js/src/auto-instrumentations/configs/all.ts index ad179de34..da543ee51 100644 --- a/js/src/auto-instrumentations/configs/all.ts +++ b/js/src/auto-instrumentations/configs/all.ts @@ -24,6 +24,7 @@ import { openAICodexConfigs } from "./openai-codex"; import { openRouterConfigs } from "./openrouter"; import { openRouterAgentConfigs } from "./openrouter-agent"; import { piCodingAgentConfigs } from "./pi-coding-agent"; +import { strandsAgentSDKConfigs } from "./strands-agent-sdk"; interface InstrumentationConfigGroup { integrations: readonly (keyof InstrumentationIntegrationsConfig)[]; @@ -81,6 +82,10 @@ const defaultInstrumentationConfigGroups: readonly InstrumentationConfigGroup[] integrations: ["piCodingAgent"], configs: piCodingAgentConfigs, }, + { + integrations: ["strandsAgentSDK"], + configs: strandsAgentSDKConfigs, + }, { integrations: ["flue"], configs: flueConfigs, diff --git a/js/src/auto-instrumentations/configs/strands-agent-sdk.test.ts b/js/src/auto-instrumentations/configs/strands-agent-sdk.test.ts new file mode 100644 index 000000000..58dcff867 --- /dev/null +++ b/js/src/auto-instrumentations/configs/strands-agent-sdk.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from "vitest"; +import { readDisabledInstrumentationEnvConfig } from "../../instrumentation/config"; +import { getDefaultInstrumentationConfigs } from "./all"; +import { strandsAgentSDKConfigs } from "./strands-agent-sdk"; + +const strandsChannelNames = [ + "Agent.stream", + "Graph.stream", + "Swarm.stream", +] as const; + +describe("strandsAgentSDKConfigs", () => { + it("targets the Strands Agent SDK stream entrypoints", () => { + expect(strandsAgentSDKConfigs).toHaveLength(3); + expect(strandsAgentSDKConfigs.map((config) => config.channelName)).toEqual( + strandsChannelNames, + ); + expect(strandsAgentSDKConfigs).toEqual([ + expect.objectContaining({ + module: { + name: "@strands-agents/sdk", + versionRange: ">=1.0.0 <2.0.0", + filePath: "dist/src/agent/agent.js", + }, + functionQuery: { + className: "Agent", + methodName: "stream", + kind: "Sync", + }, + }), + expect.objectContaining({ + module: expect.objectContaining({ + filePath: "dist/src/multiagent/graph.js", + }), + functionQuery: expect.objectContaining({ + className: "Graph", + methodName: "stream", + }), + }), + expect.objectContaining({ + module: expect.objectContaining({ + filePath: "dist/src/multiagent/swarm.js", + }), + functionQuery: expect.objectContaining({ + className: "Swarm", + methodName: "stream", + }), + }), + ]); + }); + + it("is included by default and disabled by Strands Agent SDK integration keys", () => { + expect( + getDefaultInstrumentationConfigs().some((config) => + strandsChannelNames.includes(config.channelName as never), + ), + ).toBe(true); + + for (const alias of [ + "strandsAgentSDK", + "strandsagentsdk", + "strands-agent-sdk", + "@strands-agents/sdk", + ]) { + const disabledConfig = + readDisabledInstrumentationEnvConfig(alias).integrations; + + expect(disabledConfig).toMatchObject({ strandsAgentSDK: false }); + expect( + getDefaultInstrumentationConfigs({ + disabledIntegrationConfig: disabledConfig, + }).some((config) => + strandsChannelNames.includes(config.channelName as never), + ), + ).toBe(false); + } + }); +}); diff --git a/js/src/auto-instrumentations/configs/strands-agent-sdk.ts b/js/src/auto-instrumentations/configs/strands-agent-sdk.ts new file mode 100644 index 000000000..7a7f7a5b9 --- /dev/null +++ b/js/src/auto-instrumentations/configs/strands-agent-sdk.ts @@ -0,0 +1,46 @@ +import type { InstrumentationConfig } from "@apm-js-collab/code-transformer"; +import { strandsAgentSDKChannels } from "../../instrumentation/plugins/strands-agent-sdk-channels"; + +const strandsAgentSDKVersionRange = ">=1.0.0 <2.0.0"; + +export const strandsAgentSDKConfigs: InstrumentationConfig[] = [ + { + channelName: strandsAgentSDKChannels.agentStream.channelName, + module: { + name: "@strands-agents/sdk", + versionRange: strandsAgentSDKVersionRange, + filePath: "dist/src/agent/agent.js", + }, + functionQuery: { + className: "Agent", + methodName: "stream", + kind: "Sync", + }, + }, + { + channelName: strandsAgentSDKChannels.graphStream.channelName, + module: { + name: "@strands-agents/sdk", + versionRange: strandsAgentSDKVersionRange, + filePath: "dist/src/multiagent/graph.js", + }, + functionQuery: { + className: "Graph", + methodName: "stream", + kind: "Sync", + }, + }, + { + channelName: strandsAgentSDKChannels.swarmStream.channelName, + module: { + name: "@strands-agents/sdk", + versionRange: strandsAgentSDKVersionRange, + filePath: "dist/src/multiagent/swarm.js", + }, + functionQuery: { + className: "Swarm", + methodName: "stream", + kind: "Sync", + }, + }, +]; diff --git a/js/src/exports.ts b/js/src/exports.ts index 54cd03e2a..c6c4572fb 100644 --- a/js/src/exports.ts +++ b/js/src/exports.ts @@ -185,6 +185,7 @@ export { wrapClaudeAgentSDK } from "./wrappers/claude-agent-sdk/claude-agent-sdk export { wrapOpenAICodexSDK } from "./wrappers/openai-codex"; export { wrapCursorSDK } from "./wrappers/cursor-sdk"; export { wrapPiCodingAgentSDK } from "./wrappers/pi-coding-agent"; +export { wrapStrandsAgentSDK } from "./wrappers/strands-agent-sdk"; export { wrapGoogleGenAI } from "./wrappers/google-genai"; export { wrapGoogleADK } from "./wrappers/google-adk"; export { wrapGenkit } from "./wrappers/genkit"; diff --git a/js/src/instrumentation/braintrust-plugin.test.ts b/js/src/instrumentation/braintrust-plugin.test.ts index 34ac282af..394a6b0f7 100644 --- a/js/src/instrumentation/braintrust-plugin.test.ts +++ b/js/src/instrumentation/braintrust-plugin.test.ts @@ -16,6 +16,7 @@ import { GroqPlugin } from "./plugins/groq-plugin"; import { GitHubCopilotPlugin } from "./plugins/github-copilot-plugin"; import { LangChainPlugin } from "./plugins/langchain-plugin"; import { PiCodingAgentPlugin } from "./plugins/pi-coding-agent-plugin"; +import { StrandsAgentSDKPlugin } from "./plugins/strands-agent-sdk-plugin"; function createPluginClassMock() { return vi.fn(function MockPlugin(this: { @@ -101,6 +102,10 @@ vi.mock("./plugins/pi-coding-agent-plugin", () => ({ PiCodingAgentPlugin: createPluginClassMock(), })); +vi.mock("./plugins/strands-agent-sdk-plugin", () => ({ + StrandsAgentSDKPlugin: createPluginClassMock(), +})); + describe("BraintrustPlugin", () => { beforeEach(() => { vi.clearAllMocks(); @@ -245,6 +250,16 @@ describe("BraintrustPlugin", () => { expect(mockInstance.enable).toHaveBeenCalledTimes(1); }); + it("should create and enable Strands Agent SDK plugin by default", () => { + const plugin = new BraintrustPlugin(); + plugin.enable(); + + expect(StrandsAgentSDKPlugin).toHaveBeenCalledTimes(1); + const mockInstance = vi.mocked(StrandsAgentSDKPlugin).mock.results[0] + .value; + expect(mockInstance.enable).toHaveBeenCalledTimes(1); + }); + it("should create all plugins when enabled with no config", () => { const plugin = new BraintrustPlugin(); plugin.enable(); @@ -263,6 +278,7 @@ describe("BraintrustPlugin", () => { expect(CoherePlugin).toHaveBeenCalledTimes(1); expect(GroqPlugin).toHaveBeenCalledTimes(1); expect(GitHubCopilotPlugin).toHaveBeenCalledTimes(1); + expect(StrandsAgentSDKPlugin).toHaveBeenCalledTimes(1); expect(LangChainPlugin).toHaveBeenCalledTimes(1); }); @@ -284,6 +300,7 @@ describe("BraintrustPlugin", () => { expect(CoherePlugin).toHaveBeenCalledTimes(1); expect(GroqPlugin).toHaveBeenCalledTimes(1); expect(GitHubCopilotPlugin).toHaveBeenCalledTimes(1); + expect(StrandsAgentSDKPlugin).toHaveBeenCalledTimes(1); expect(LangChainPlugin).toHaveBeenCalledTimes(1); }); @@ -304,6 +321,7 @@ describe("BraintrustPlugin", () => { expect(MistralPlugin).toHaveBeenCalledTimes(1); expect(CoherePlugin).toHaveBeenCalledTimes(1); expect(GroqPlugin).toHaveBeenCalledTimes(1); + expect(StrandsAgentSDKPlugin).toHaveBeenCalledTimes(1); expect(LangChainPlugin).toHaveBeenCalledTimes(1); }); }); @@ -563,6 +581,7 @@ describe("BraintrustPlugin", () => { gitHubCopilot: false, langchain: false, piCodingAgent: false, + strandsAgentSDK: false, }, }); plugin.enable(); @@ -583,6 +602,7 @@ describe("BraintrustPlugin", () => { expect(GitHubCopilotPlugin).not.toHaveBeenCalled(); expect(LangChainPlugin).not.toHaveBeenCalled(); expect(PiCodingAgentPlugin).not.toHaveBeenCalled(); + expect(StrandsAgentSDKPlugin).not.toHaveBeenCalled(); }); it("should not create Pi Coding Agent plugin when piCodingAgent: false", () => { @@ -596,6 +616,16 @@ describe("BraintrustPlugin", () => { expect(AnthropicPlugin).toHaveBeenCalledTimes(1); }); + it("should not create Strands Agent SDK plugin when strandsAgentSDK: false", () => { + const plugin = new BraintrustPlugin({ + integrations: { strandsAgentSDK: false }, + }); + plugin.enable(); + + expect(StrandsAgentSDKPlugin).not.toHaveBeenCalled(); + expect(OpenAIPlugin).toHaveBeenCalledTimes(1); + }); + it("should allow selective enabling of plugins", () => { const plugin = new BraintrustPlugin({ integrations: { @@ -751,6 +781,8 @@ describe("BraintrustPlugin", () => { const groqMock = vi.mocked(GroqPlugin).mock.results[0].value; const piCodingAgentMock = vi.mocked(PiCodingAgentPlugin).mock.results[0].value; + const strandsAgentSDKMock = vi.mocked(StrandsAgentSDKPlugin).mock + .results[0].value; const langChainMock = vi.mocked(LangChainPlugin).mock.results[0].value; expect(openaiMock.enable).toHaveBeenCalledTimes(1); @@ -767,6 +799,7 @@ describe("BraintrustPlugin", () => { expect(cohereMock.enable).toHaveBeenCalledTimes(1); expect(groqMock.enable).toHaveBeenCalledTimes(1); expect(piCodingAgentMock.enable).toHaveBeenCalledTimes(1); + expect(strandsAgentSDKMock.enable).toHaveBeenCalledTimes(1); expect(langChainMock.enable).toHaveBeenCalledTimes(1); }); @@ -795,6 +828,8 @@ describe("BraintrustPlugin", () => { const groqMock = vi.mocked(GroqPlugin).mock.results[0].value; const piCodingAgentMock = vi.mocked(PiCodingAgentPlugin).mock.results[0].value; + const strandsAgentSDKMock = vi.mocked(StrandsAgentSDKPlugin).mock + .results[0].value; const langChainMock = vi.mocked(LangChainPlugin).mock.results[0].value; plugin.disable(); @@ -813,6 +848,7 @@ describe("BraintrustPlugin", () => { expect(cohereMock.disable).toHaveBeenCalledTimes(1); expect(groqMock.disable).toHaveBeenCalledTimes(1); expect(piCodingAgentMock.disable).toHaveBeenCalledTimes(1); + expect(strandsAgentSDKMock.disable).toHaveBeenCalledTimes(1); expect(langChainMock.disable).toHaveBeenCalledTimes(1); }); @@ -861,6 +897,7 @@ describe("BraintrustPlugin", () => { expect(CoherePlugin).not.toHaveBeenCalled(); expect(GroqPlugin).not.toHaveBeenCalled(); expect(PiCodingAgentPlugin).not.toHaveBeenCalled(); + expect(StrandsAgentSDKPlugin).not.toHaveBeenCalled(); }); it("should allow re-enabling after disable", () => { @@ -886,6 +923,7 @@ describe("BraintrustPlugin", () => { expect(CoherePlugin).toHaveBeenCalledTimes(1); expect(GroqPlugin).toHaveBeenCalledTimes(1); expect(PiCodingAgentPlugin).toHaveBeenCalledTimes(1); + expect(StrandsAgentSDKPlugin).toHaveBeenCalledTimes(1); expect(LangChainPlugin).toHaveBeenCalledTimes(1); }); diff --git a/js/src/instrumentation/braintrust-plugin.ts b/js/src/instrumentation/braintrust-plugin.ts index 36e7e83f0..87e8687bf 100644 --- a/js/src/instrumentation/braintrust-plugin.ts +++ b/js/src/instrumentation/braintrust-plugin.ts @@ -19,19 +19,13 @@ import { GitHubCopilotPlugin } from "./plugins/github-copilot-plugin"; import { FluePlugin } from "./plugins/flue-plugin"; import { LangChainPlugin } from "./plugins/langchain-plugin"; import { PiCodingAgentPlugin } from "./plugins/pi-coding-agent-plugin"; +import { StrandsAgentSDKPlugin } from "./plugins/strands-agent-sdk-plugin"; import type { InstrumentationIntegrationsConfig } from "./config"; export interface BraintrustPluginConfig { integrations?: InstrumentationIntegrationsConfig; } -function getIntegrationConfig( - integrations: NonNullable, - key: string, -): boolean | undefined { - return (integrations as Record)[key]; -} - /** * Default Braintrust plugin that manages all AI provider instrumentation plugins. * @@ -71,6 +65,7 @@ export class BraintrustPlugin extends BasePlugin { private fluePlugin: FluePlugin | null = null; private langChainPlugin: LangChainPlugin | null = null; private piCodingAgentPlugin: PiCodingAgentPlugin | null = null; + private strandsAgentSDKPlugin: StrandsAgentSDKPlugin | null = null; constructor(config: BraintrustPluginConfig = {}) { super(); @@ -179,7 +174,12 @@ export class BraintrustPlugin extends BasePlugin { this.piCodingAgentPlugin.enable(); } - if (getIntegrationConfig(integrations, "flue") !== false) { + if (integrations.strandsAgentSDK !== false) { + this.strandsAgentSDKPlugin = new StrandsAgentSDKPlugin(); + this.strandsAgentSDKPlugin.enable(); + } + + if (integrations.flue !== false) { this.fluePlugin = new FluePlugin(); this.fluePlugin.enable(); } @@ -287,6 +287,11 @@ export class BraintrustPlugin extends BasePlugin { this.piCodingAgentPlugin = null; } + if (this.strandsAgentSDKPlugin) { + this.strandsAgentSDKPlugin.disable(); + this.strandsAgentSDKPlugin = null; + } + if (this.fluePlugin) { this.fluePlugin.disable(); this.fluePlugin = null; diff --git a/js/src/instrumentation/config.ts b/js/src/instrumentation/config.ts index 299b2b827..54abe9cf4 100644 --- a/js/src/instrumentation/config.ts +++ b/js/src/instrumentation/config.ts @@ -22,6 +22,7 @@ export interface InstrumentationIntegrationsConfig { gitHubCopilot?: boolean; openaiCodexSDK?: boolean; piCodingAgent?: boolean; + strandsAgentSDK?: boolean; langchain?: boolean; langgraph?: boolean; } @@ -49,6 +50,10 @@ const envIntegrationAliases: Record< picodingagent: "piCodingAgent", picodingagentsdk: "piCodingAgent", "@earendil-works/pi-coding-agent": "piCodingAgent", + strandsAgentSDK: "strandsAgentSDK", + strandsagentsdk: "strandsAgentSDK", + "strands-agent-sdk": "strandsAgentSDK", + "@strands-agents/sdk": "strandsAgentSDK", anthropic: "anthropic", aisdk: "aisdk", "ai-sdk": "aisdk", @@ -120,6 +125,7 @@ export function getDefaultInstrumentationIntegrations(): Record< langchain: true, langgraph: true, piCodingAgent: true, + strandsAgentSDK: true, }; } @@ -130,9 +136,12 @@ export function readDisabledInstrumentationEnvConfig( if (disabledList) { for (const value of disabledList.split(",")) { - const sdk = value.trim().toLowerCase(); + const rawSdk = value.trim(); + const sdk = rawSdk.toLowerCase(); if (sdk.length > 0) { - integrations[envIntegrationAliases[sdk] ?? sdk] = false; + integrations[ + envIntegrationAliases[rawSdk] ?? envIntegrationAliases[sdk] ?? sdk + ] = false; } } } diff --git a/js/src/instrumentation/core/index.ts b/js/src/instrumentation/core/index.ts index c5afe288a..0189111f2 100644 --- a/js/src/instrumentation/core/index.ts +++ b/js/src/instrumentation/core/index.ts @@ -10,6 +10,7 @@ */ export { BasePlugin } from "./plugin"; +export { toLoggedError } from "./logging"; export { createChannelName, parseChannelName, diff --git a/js/src/instrumentation/core/logging.test.ts b/js/src/instrumentation/core/logging.test.ts new file mode 100644 index 000000000..706d9bd50 --- /dev/null +++ b/js/src/instrumentation/core/logging.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "vitest"; +import { toLoggedError } from "./logging"; + +describe("toLoggedError", () => { + it("returns Error messages", () => { + expect(toLoggedError(new Error("failed"))).toBe("failed"); + }); + + it("preserves string throws", () => { + expect(toLoggedError("failed")).toBe("failed"); + }); + + it("serializes non-Error thrown values", () => { + expect(toLoggedError({ code: "bad_request" })).toBe( + '{"code":"bad_request"}', + ); + }); + + it("falls back when non-Error thrown values are not JSON serializable", () => { + const circular: Record = {}; + circular.self = circular; + + expect(toLoggedError(circular)).toBe("[object Object]"); + }); +}); diff --git a/js/src/instrumentation/core/logging.ts b/js/src/instrumentation/core/logging.ts new file mode 100644 index 000000000..00598016b --- /dev/null +++ b/js/src/instrumentation/core/logging.ts @@ -0,0 +1,19 @@ +export function toLoggedError(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + if (typeof error === "string") { + return error; + } + + try { + const serialized = JSON.stringify(error); + return serialized === undefined ? String(error) : serialized; + } catch { + try { + return String(error); + } catch { + return ""; + } + } +} diff --git a/js/src/instrumentation/core/stream-patcher.ts b/js/src/instrumentation/core/stream-patcher.ts index 90f7a5a95..6796f5ccb 100644 --- a/js/src/instrumentation/core/stream-patcher.ts +++ b/js/src/instrumentation/core/stream-patcher.ts @@ -48,6 +48,12 @@ export interface StreamPatchOptions { * Default: collect all chunks. */ shouldCollect?: (chunk: TChunk) => boolean; + + /** + * Optional wrapper around iterator.next(). Used by integrations that need to + * preserve async context while the producer advances. + */ + aroundNext?: (callback: () => PromiseLike) => PromiseLike | T; } type AsyncIteratorLike = AsyncIterable & @@ -155,7 +161,9 @@ export function patchStreamIfNeeded( stream.next = async (...args: [] | [undefined]) => { try { - const result = await originalNext(...args); + const result = await runNextWithWrapper(options, () => + originalNext(...args), + ); if (result.done) { if (!completed) { @@ -276,7 +284,9 @@ export function patchStreamIfNeeded( // Patch the next() method iterator.next = async function (...args: [] | [undefined]) { try { - const result = await originalNext(...args); + const result = await runNextWithWrapper(options, () => + originalNext(...args), + ); if (result.done) { // Stream completed successfully @@ -396,6 +406,13 @@ export function patchStreamIfNeeded( } } +function runNextWithWrapper( + options: StreamPatchOptions, + callback: () => PromiseLike, +): PromiseLike | T { + return options.aroundNext ? options.aroundNext(callback) : callback(); +} + /** * Higher-level helper for common pattern: collect chunks and process on completion. * diff --git a/js/src/instrumentation/plugins/ai-sdk-plugin.ts b/js/src/instrumentation/plugins/ai-sdk-plugin.ts index 749b6fb70..114b06c43 100644 --- a/js/src/instrumentation/plugins/ai-sdk-plugin.ts +++ b/js/src/instrumentation/plugins/ai-sdk-plugin.ts @@ -1,4 +1,4 @@ -import { BasePlugin } from "../core"; +import { BasePlugin, toLoggedError } from "../core"; import { traceAsyncChannel, traceStreamingChannel, @@ -1361,9 +1361,7 @@ function prepareAISDKChildTracing( } span.log({ output: lastValue }); } catch (error) { - span.log({ - error: error instanceof Error ? error.message : String(error), - }); + span.log({ error: toLoggedError(error) }); throw error; } finally { span.end(); diff --git a/js/src/instrumentation/plugins/anthropic-plugin.ts b/js/src/instrumentation/plugins/anthropic-plugin.ts index c16bb50be..a8454f67c 100644 --- a/js/src/instrumentation/plugins/anthropic-plugin.ts +++ b/js/src/instrumentation/plugins/anthropic-plugin.ts @@ -1,4 +1,4 @@ -import { BasePlugin } from "../core"; +import { BasePlugin, toLoggedError } from "../core"; import { traceStreamingChannel, unsubscribeAll } from "../core/channel-tracing"; import { isAsyncIterable, patchStreamIfNeeded } from "../core/stream-patcher"; import { Attachment, startSpan, withCurrent } from "../../logger"; @@ -277,10 +277,6 @@ function extractAnthropicToolNames(tools: unknown[]): string[] { return toolNames; } -function toErrorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - function getAnthropicToolRunnerInput(args: unknown[]): unknown { return args.length > 0 ? args[0] : undefined; } @@ -339,7 +335,7 @@ function wrapAnthropicToolRunnerTool( }; const finalizeError = (error: unknown) => { - span.log({ error: toErrorMessage(error) }); + span.log({ error: toLoggedError(error) }); throw error; }; @@ -555,7 +551,7 @@ function finalizeAnthropicToolRunnerError( } state.finalized = true; state.span.log({ - error: error instanceof Error ? error.message : String(error), + error: toLoggedError(error), }); state.span.end(); } diff --git a/js/src/instrumentation/plugins/claude-agent-sdk-local-tool-spans.ts b/js/src/instrumentation/plugins/claude-agent-sdk-local-tool-spans.ts index 1631cc75f..28026770b 100644 --- a/js/src/instrumentation/plugins/claude-agent-sdk-local-tool-spans.ts +++ b/js/src/instrumentation/plugins/claude-agent-sdk-local-tool-spans.ts @@ -1,5 +1,6 @@ import { startSpan, withCurrent } from "../../logger"; import { SpanTypeAttribute, isPromiseLike } from "../../../util/index"; +import { toLoggedError } from "../core"; import { getClaudeLocalToolParentResolver } from "./claude-agent-sdk-local-tool-context"; export type LocalToolSpanMetadata = { @@ -13,10 +14,6 @@ const LOCAL_TOOL_HANDLER_WRAPPED = Symbol.for( "braintrust.claude_agent_sdk.local_tool_handler_wrapped", ); -function toErrorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - function getToolUseIdFromExtra(extra: unknown): string | undefined { if (!extra || typeof extra !== "object" || !("_meta" in extra)) { return undefined; @@ -85,7 +82,7 @@ export function wrapLocalClaudeToolHandler( return result; }; const finalizeError = (error: unknown) => { - span.log({ error: toErrorMessage(error) }); + span.log({ error: toLoggedError(error) }); span.end(); throw error; }; diff --git a/js/src/instrumentation/plugins/cursor-sdk-plugin.ts b/js/src/instrumentation/plugins/cursor-sdk-plugin.ts index 5f45bf440..b9347d4f6 100644 --- a/js/src/instrumentation/plugins/cursor-sdk-plugin.ts +++ b/js/src/instrumentation/plugins/cursor-sdk-plugin.ts @@ -1,4 +1,4 @@ -import { BasePlugin } from "../core"; +import { BasePlugin, toLoggedError } from "../core"; import type { ChannelMessage } from "../core/channel-definitions"; import type { IsoChannelHandlers } from "../../isomorph"; import { debugLogger } from "../../debug-logger"; @@ -542,7 +542,7 @@ async function handleToolUpdate( })); finishToolSpan(toolState, { - error: toolCall?.status === "error" ? stringifyUnknown(result) : undefined, + error: toolCall?.status === "error" ? toLoggedError(result) : undefined, metadata: { "cursor_sdk.tool.status": toolCall?.status ?? "completed", }, @@ -654,7 +654,7 @@ async function handleToolMessage( })); finishToolSpan(toolState, { error: - message.status === "error" ? stringifyUnknown(message.result) : undefined, + message.status === "error" ? toLoggedError(message.result) : undefined, metadata: { "cursor_sdk.tool.status": message.status, }, @@ -722,9 +722,7 @@ async function handleConversationStep( }); finishToolSpan(toolState, { error: - toolCall?.status === "error" - ? stringifyUnknown(toolCall.result) - : undefined, + toolCall?.status === "error" ? toLoggedError(toolCall.result) : undefined, metadata: { "cursor_sdk.tool.status": toolCall?.status ?? "completed", }, @@ -885,7 +883,7 @@ async function finalizeCursorRun( }; if (error) { safeLog(state.span, { - error: error instanceof Error ? error.message : String(error), + error: toLoggedError(error), metadata, metrics: { ...cleanMetrics(state.metrics), @@ -1142,20 +1140,6 @@ function getString( return typeof value === "string" ? value : undefined; } -function stringifyUnknown(value: unknown): string { - if (value instanceof Error) { - return value.message; - } - if (typeof value === "string") { - return value; - } - try { - return JSON.stringify(value); - } catch { - return String(value); - } -} - function safeLog(span: Span, event: Parameters[0]): void { try { span.log(event); diff --git a/js/src/instrumentation/plugins/flue-plugin.ts b/js/src/instrumentation/plugins/flue-plugin.ts index 408c203c3..68f257506 100644 --- a/js/src/instrumentation/plugins/flue-plugin.ts +++ b/js/src/instrumentation/plugins/flue-plugin.ts @@ -1,4 +1,4 @@ -import { BasePlugin } from "../core"; +import { BasePlugin, toLoggedError } from "../core"; import type { ChannelMessage } from "../core/channel-definitions"; import type { IsoChannelHandlers } from "../../isomorph"; import { @@ -380,7 +380,7 @@ class FlueObserveBridge { if (state) { safeLog(state.span, { - ...(event.isError ? { error: errorToString(event.error) } : {}), + ...(event.isError ? { error: toLoggedError(event.error) } : {}), metadata: { ...state.metadata, ...extractEventMetadata(event), @@ -442,7 +442,7 @@ class FlueObserveBridge { this.finishPendingChildrenForOperation(event, output); safeLog(state.span, { - ...(event.isError ? { error: errorToString(event.error) } : {}), + ...(event.isError ? { error: toLoggedError(event.error) } : {}), metadata, metrics: durationMetrics(event.durationMs), output, @@ -510,7 +510,7 @@ class FlueObserveBridge { }; safeLog(state.span, { - ...(event.isError ? { error: errorToString(event.error) } : {}), + ...(event.isError ? { error: toLoggedError(event.error) } : {}), metadata, metrics: { ...durationMetrics(event.durationMs), @@ -566,7 +566,7 @@ class FlueObserveBridge { }; safeLog(state.span, { - ...(event.isError ? { error: errorToString(event.result) } : {}), + ...(event.isError ? { error: toLoggedError(event.result) } : {}), metadata, metrics: durationMetrics(event.durationMs), output: event.result, @@ -609,7 +609,7 @@ class FlueObserveBridge { } safeLog(state.span, { - ...(event.isError ? { error: errorToString(event.result) } : {}), + ...(event.isError ? { error: toLoggedError(event.result) } : {}), metadata: { ...state.metadata, ...extractEventMetadata(event), @@ -1124,20 +1124,6 @@ function safeEnd(span: Span, endTime: number | undefined): void { } } -function errorToString(error: unknown): string { - if (error instanceof Error) { - return error.message; - } - if (typeof error === "string") { - return error; - } - try { - return JSON.stringify(error); - } catch { - return String(error); - } -} - function logInstrumentationError(label: string, error: unknown): void { // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. console.error(`Error in ${label} instrumentation:`, error); diff --git a/js/src/instrumentation/plugins/genkit-plugin.ts b/js/src/instrumentation/plugins/genkit-plugin.ts index 82f01c6d9..d58a6192b 100644 --- a/js/src/instrumentation/plugins/genkit-plugin.ts +++ b/js/src/instrumentation/plugins/genkit-plugin.ts @@ -1,4 +1,4 @@ -import { BasePlugin } from "../core"; +import { BasePlugin, toLoggedError } from "../core"; import { traceAsyncChannel, traceSyncStreamChannel, @@ -560,7 +560,7 @@ function createDeferredSpanFinalizer( try { await callback(); } catch (error) { - span.log({ error: errorMessage(error) }); + span.log({ error: toLoggedError(error) }); } finally { span.end(); } @@ -759,10 +759,6 @@ function pickNumberMetrics( ); } -function errorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - function stringValue(value: unknown): string | undefined { return typeof value === "string" ? value : undefined; } diff --git a/js/src/instrumentation/plugins/openai-codex-plugin.ts b/js/src/instrumentation/plugins/openai-codex-plugin.ts index 0231c0b8c..402972632 100644 --- a/js/src/instrumentation/plugins/openai-codex-plugin.ts +++ b/js/src/instrumentation/plugins/openai-codex-plugin.ts @@ -1,4 +1,4 @@ -import { BasePlugin } from "../core"; +import { BasePlugin, toLoggedError } from "../core"; import type { ChannelMessage } from "../core/channel-definitions"; import type { IsoChannelHandlers } from "../../isomorph"; import { debugLogger } from "../../debug-logger"; @@ -309,9 +309,7 @@ async function finalizeCodexRun( try { const error = params.error; safeLog(state.span, { - ...(error - ? { error: error instanceof Error ? error.message : String(error) } - : {}), + ...(error ? { error: toLoggedError(error) } : {}), metadata: state.metadata, metrics, output, @@ -464,9 +462,7 @@ async function finishActiveLlmSpan( state.activeLlmSpan = undefined; const output = buildLlmOutput(active); safeLog(active.span, { - ...(error - ? { error: error instanceof Error ? error.message : String(error) } - : {}), + ...(error ? { error: toLoggedError(error) } : {}), metadata: active.metadata, ...(output ? { output } : {}), }); diff --git a/js/src/instrumentation/plugins/openrouter-agent-plugin.ts b/js/src/instrumentation/plugins/openrouter-agent-plugin.ts index 73e61e8fb..facda63c0 100644 --- a/js/src/instrumentation/plugins/openrouter-agent-plugin.ts +++ b/js/src/instrumentation/plugins/openrouter-agent-plugin.ts @@ -1,4 +1,4 @@ -import { BasePlugin } from "../core"; +import { BasePlugin, toLoggedError } from "../core"; import { traceAsyncChannel, traceStreamingChannel, @@ -949,9 +949,7 @@ function patchOpenRouterCallModelResult(args: { return; } ended = true; - span.log({ - error: normalizeError(error).message, - }); + span.log({ error: toLoggedError(error) }); span.end(); }; diff --git a/js/src/instrumentation/plugins/openrouter-plugin.ts b/js/src/instrumentation/plugins/openrouter-plugin.ts index 6268d8ea3..c89b8f9a7 100644 --- a/js/src/instrumentation/plugins/openrouter-plugin.ts +++ b/js/src/instrumentation/plugins/openrouter-plugin.ts @@ -1,4 +1,4 @@ -import { BasePlugin } from "../core"; +import { BasePlugin, toLoggedError } from "../core"; import { traceAsyncChannel, traceStreamingChannel, @@ -1131,9 +1131,7 @@ function patchOpenRouterCallModelResult(args: { return; } ended = true; - span.log({ - error: normalizeError(error).message, - }); + span.log({ error: toLoggedError(error) }); span.end(); }; diff --git a/js/src/instrumentation/plugins/pi-coding-agent-plugin.ts b/js/src/instrumentation/plugins/pi-coding-agent-plugin.ts index b7e755ce9..aa4ed1fe3 100644 --- a/js/src/instrumentation/plugins/pi-coding-agent-plugin.ts +++ b/js/src/instrumentation/plugins/pi-coding-agent-plugin.ts @@ -1,4 +1,4 @@ -import { BasePlugin } from "../core"; +import { BasePlugin, toLoggedError } from "../core"; import type { ChannelMessage } from "../core/channel-definitions"; import iso, { type IsoAsyncLocalStorage, @@ -692,7 +692,7 @@ function finishPiToolSpan( }; try { safeLog(toolState.span, { - ...(event.isError ? { error: stringifyUnknown(event.result) } : {}), + ...(event.isError ? { error: toLoggedError(event.result) } : {}), metadata, output: event.result, }); @@ -731,7 +731,7 @@ async function finalizePiPromptRun( }; try { safeLog(state.span, { - ...(error ? { error: stringifyUnknown(error) } : {}), + ...(error ? { error: toLoggedError(error) } : {}), metadata, metrics: { ...cleanMetrics(state.metrics), @@ -808,7 +808,7 @@ function finishPiLlmSpan( try { safeLog(llmState.span, { ...(error || messageError - ? { error: stringifyUnknown(error ?? messageError) } + ? { error: toLoggedError(error ?? messageError) } : {}), metadata: { ...llmState.metadata, @@ -826,7 +826,7 @@ function finishOpenToolSpans(state: PiPromptState, error?: unknown): void { for (const [, toolState] of state.activeToolSpans) { try { safeLog(toolState.span, { - error: error ? stringifyUnknown(error) : "Pi tool did not complete", + error: error ? toLoggedError(error) : "Pi tool did not complete", }); toolState.span.end(); } finally { diff --git a/js/src/instrumentation/plugins/strands-agent-sdk-channels.ts b/js/src/instrumentation/plugins/strands-agent-sdk-channels.ts new file mode 100644 index 000000000..c8246534e --- /dev/null +++ b/js/src/instrumentation/plugins/strands-agent-sdk-channels.ts @@ -0,0 +1,57 @@ +import { channel, defineChannels } from "../core/channel-definitions"; +import type { + StrandsAgent, + StrandsAgentResult, + StrandsAgentStreamEvent, + StrandsInvokeArgs, + StrandsInvokeOptions, + StrandsMultiAgent, + StrandsMultiAgentInput, + StrandsMultiAgentInvokeOptions, + StrandsMultiAgentResult, + StrandsMultiAgentStreamEvent, +} from "../../vendor-sdk-types/strands-agent-sdk"; + +type StrandsChannelContext = { + self?: unknown; +}; + +export const strandsAgentSDKChannels = defineChannels("@strands-agents/sdk", { + agentStream: channel< + [StrandsInvokeArgs, StrandsInvokeOptions | undefined], + AsyncGenerator, + StrandsChannelContext & { agent?: StrandsAgent }, + StrandsAgentStreamEvent + >({ + channelName: "Agent.stream", + kind: "sync-stream", + }), + + graphStream: channel< + [StrandsMultiAgentInput, StrandsMultiAgentInvokeOptions | undefined], + AsyncGenerator< + StrandsMultiAgentStreamEvent, + StrandsMultiAgentResult, + undefined + >, + StrandsChannelContext & { orchestrator?: StrandsMultiAgent }, + StrandsMultiAgentStreamEvent + >({ + channelName: "Graph.stream", + kind: "sync-stream", + }), + + swarmStream: channel< + [StrandsMultiAgentInput, StrandsMultiAgentInvokeOptions | undefined], + AsyncGenerator< + StrandsMultiAgentStreamEvent, + StrandsMultiAgentResult, + undefined + >, + StrandsChannelContext & { orchestrator?: StrandsMultiAgent }, + StrandsMultiAgentStreamEvent + >({ + channelName: "Swarm.stream", + kind: "sync-stream", + }), +}); diff --git a/js/src/instrumentation/plugins/strands-agent-sdk-plugin.test.ts b/js/src/instrumentation/plugins/strands-agent-sdk-plugin.test.ts new file mode 100644 index 000000000..abc3d1c0e --- /dev/null +++ b/js/src/instrumentation/plugins/strands-agent-sdk-plugin.test.ts @@ -0,0 +1,510 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const { + mockBindStore, + mockWithCurrent, + mockNewAsyncLocalStorage, + mockStartSpan, + mockUnbindStore, +} = vi.hoisted(() => ({ + mockBindStore: vi.fn(), + mockWithCurrent: vi.fn(), + mockNewAsyncLocalStorage: vi.fn(() => { + let current: unknown; + return { + enterWith: vi.fn((store: unknown) => { + current = store; + }), + getStore: vi.fn(() => current), + run: vi.fn((store: unknown, callback: () => unknown) => { + const previous = current; + current = store; + try { + return callback(); + } finally { + current = previous; + } + }), + }; + }), + mockStartSpan: vi.fn(), + mockUnbindStore: vi.fn(), +})); + +vi.mock("../../isomorph", () => ({ + default: { + newAsyncLocalStorage: mockNewAsyncLocalStorage, + newTracingChannel: vi.fn(), + }, +})); + +vi.mock("../../logger", () => ({ + startSpan: (...args: unknown[]) => mockStartSpan(...args), + withCurrent: (...args: unknown[]) => mockWithCurrent(...args), +})); + +import iso from "../../isomorph"; +import { isAutoInstrumentationSuppressed } from "../auto-instrumentation-suppression"; +import { StrandsAgentSDKPlugin } from "./strands-agent-sdk-plugin"; + +const mockNewTracingChannel = iso.newTracingChannel as ReturnType; + +describe("StrandsAgentSDKPlugin", () => { + let handlersByName: Map; + let spans: Array<{ + args: any; + end: ReturnType; + log: ReturnType; + rootSpanId: string; + spanId: string; + }>; + let currentSpan: + | { + rootSpanId: string; + spanId: string; + } + | undefined; + + beforeEach(() => { + handlersByName = new Map(); + spans = []; + mockNewTracingChannel.mockImplementation((name: string) => ({ + start: { + bindStore: mockBindStore, + unbindStore: mockUnbindStore, + }, + subscribe: vi.fn((handlers) => handlersByName.set(name, handlers)), + traceSync: vi.fn((fn) => fn()), + unsubscribe: vi.fn(), + })); + currentSpan = undefined; + mockWithCurrent.mockImplementation((span: any, callback: () => unknown) => { + const previous = currentSpan; + currentSpan = span; + try { + return callback(); + } finally { + currentSpan = previous; + } + }); + mockStartSpan.mockImplementation((args: any) => { + const parentSpanIds = + args.parentSpanIds ?? + (currentSpan + ? { + rootSpanId: currentSpan.rootSpanId, + spanId: currentSpan.spanId, + } + : undefined); + const effectiveArgs = parentSpanIds ? { ...args, parentSpanIds } : args; + const span = { + args: effectiveArgs, + end: vi.fn(), + log: vi.fn(), + rootSpanId: parentSpanIds?.rootSpanId ?? `root-${spans.length}`, + spanId: `span-${spans.length}`, + }; + if (effectiveArgs.event) { + span.log(effectiveArgs.event); + } + spans.push(span); + return span; + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("subscribes to Strands stream channels and binds suppression", () => { + const plugin = new StrandsAgentSDKPlugin(); + plugin.enable(); + + expect( + handlersByName.has("orchestrion:@strands-agents/sdk:Agent.stream"), + ).toBe(true); + expect( + handlersByName.has("orchestrion:@strands-agents/sdk:Graph.stream"), + ).toBe(true); + expect( + handlersByName.has("orchestrion:@strands-agents/sdk:Swarm.stream"), + ).toBe(true); + expect(mockBindStore).toHaveBeenCalledTimes(3); + + plugin.disable(); + + expect(mockUnbindStore).toHaveBeenCalledTimes(3); + }); + + it("records agent model and tool spans from stream events", async () => { + const plugin = new StrandsAgentSDKPlugin(); + plugin.enable(); + + const handlers = handlersByName.get( + "orchestrion:@strands-agents/sdk:Agent.stream", + ); + class OpenAIModel { + modelId = "gpt-4o-mini"; + + getConfig() { + return { api: "responses", modelId: "gpt-4o-mini" }; + } + } + const model = new OpenAIModel(); + const agent = { + id: "agent-1", + messages: [{ role: "user", content: [{ text: "hello" }] }], + model, + name: "helper", + stream: vi.fn(), + }; + const suppressionStates: boolean[] = []; + const stream = makeAgentStream( + [ + { agent, model, projectedInputTokens: 3, type: "beforeModelCallEvent" }, + { + event: { + metrics: { latencyMs: 50, timeToFirstByteMs: 10 }, + type: "modelMetadataEvent", + usage: { inputTokens: 3, outputTokens: 4, totalTokens: 7 }, + }, + type: "modelStreamUpdateEvent", + }, + { + agent, + attemptCount: 1, + model, + stopData: { + message: { role: "assistant", content: [{ text: "call tool" }] }, + stopReason: "tool_use", + }, + type: "afterModelCallEvent", + }, + { + toolUse: { + input: { city: "Vienna" }, + name: "lookup_weather", + toolUseId: "tool-1", + }, + type: "beforeToolCallEvent", + }, + { + result: { + content: [{ text: "sunny" }], + status: "success", + toolUseId: "tool-1", + }, + toolUse: { + input: { city: "Vienna" }, + name: "lookup_weather", + toolUseId: "tool-1", + }, + type: "afterToolCallEvent", + }, + { + result: { + lastMessage: { role: "assistant", content: [{ text: "sunny" }] }, + metrics: { + accumulatedUsage: { + inputTokens: 10, + outputTokens: 5, + totalTokens: 15, + }, + }, + stopReason: "end_turn", + }, + type: "agentResultEvent", + }, + ], + () => suppressionStates.push(isAutoInstrumentationSuppressed()), + ); + const event = { + arguments: ["hello", undefined], + moduleVersion: "1.6.0", + result: stream, + self: agent, + }; + + handlers.start(event); + handlers.end(event); + const chunks = []; + for await (const chunk of stream) { + chunks.push(chunk); + } + + expect(chunks).toHaveLength(6); + expect(suppressionStates).toEqual([true, true, true, true, true, true]); + const rootSpan = spans.find((span) => span.args.name === "Agent: helper"); + const modelSpan = spans.find( + (span) => span.args.name === "Strands model: gpt-4o-mini", + ); + const toolSpan = spans.find( + (span) => span.args.name === "tool: lookup_weather", + ); + + expect(rootSpan?.log).toHaveBeenCalledWith( + expect.objectContaining({ + metadata: expect.objectContaining({ + "strands.agent.id": "agent-1", + "strands.operation": "Agent.stream", + model: "gpt-4o-mini", + provider: "openai", + }), + }), + ); + expect(modelSpan?.args.parentSpanIds).toEqual({ + rootSpanId: rootSpan?.rootSpanId, + spanId: rootSpan?.spanId, + }); + expect(modelSpan?.log).toHaveBeenCalledWith( + expect.objectContaining({ + metrics: expect.objectContaining({ + completion_tokens: 4, + prompt_tokens: 3, + tokens: 7, + }), + }), + ); + expect(toolSpan?.log).toHaveBeenCalledWith( + expect.objectContaining({ + output: expect.objectContaining({ status: "success" }), + }), + ); + expect(rootSpan?.end).toHaveBeenCalledTimes(1); + }); + + it("parents nested agent spans under active graph nodes", async () => { + const plugin = new StrandsAgentSDKPlugin(); + plugin.enable(); + + const graphHandlers = handlersByName.get( + "orchestrion:@strands-agents/sdk:Graph.stream", + ); + const agentHandlers = handlersByName.get( + "orchestrion:@strands-agents/sdk:Agent.stream", + ); + const agent = { + model: { getConfig: () => ({ modelId: "gpt-4o-mini" }) }, + name: "worker", + stream: vi.fn(), + }; + const graph = { + id: "graph-1", + nodes: new Map([["worker", { agent, id: "worker", type: "agentNode" }]]), + stream: vi.fn(), + }; + const graphStream = makeMultiAgentStream([ + { nodeId: "worker", orchestrator: graph, type: "beforeNodeCallEvent" }, + { + nodeId: "worker", + nodeType: "agentNode", + result: { + content: [{ text: "node done" }], + nodeId: "worker", + status: "COMPLETED", + usage: { inputTokens: 1, outputTokens: 2, totalTokens: 3 }, + }, + type: "nodeResultEvent", + }, + { nodeId: "worker", orchestrator: graph, type: "afterNodeCallEvent" }, + { + result: { + content: [{ text: "graph done" }], + status: "COMPLETED", + usage: { inputTokens: 1, outputTokens: 2, totalTokens: 3 }, + }, + type: "multiAgentResultEvent", + }, + ]); + const graphEvent = { + arguments: ["work", undefined], + result: graphStream, + self: graph, + }; + + graphHandlers.start(graphEvent); + graphHandlers.end(graphEvent); + const iterator = graphStream[Symbol.asyncIterator](); + await iterator.next(); + + const agentStream = makeAgentStream([ + { + result: { + lastMessage: { role: "assistant", content: [{ text: "nested" }] }, + stopReason: "end_turn", + }, + type: "agentResultEvent", + }, + ]); + const agentEvent = { + arguments: ["node work", undefined], + result: agentStream, + self: agent, + }; + agentHandlers.start(agentEvent); + agentHandlers.end(agentEvent); + for await (const _chunk of agentStream) { + // consume nested agent stream + } + await iterator.next(); + await iterator.next(); + await iterator.next(); + await iterator.next(); + + const nodeSpan = spans.find((span) => span.args.name === "node: worker"); + const agentSpan = spans.find((span) => span.args.name === "Agent: worker"); + + expect(agentSpan?.args.parentSpanIds).toEqual({ + rootSpanId: nodeSpan?.rootSpanId, + spanId: nodeSpan?.spanId, + }); + expect(nodeSpan?.end).toHaveBeenCalledTimes(1); + }); + + it("parents nested graph spans under active graph nodes", async () => { + const plugin = new StrandsAgentSDKPlugin(); + plugin.enable(); + + const graphHandlers = handlersByName.get( + "orchestrion:@strands-agents/sdk:Graph.stream", + ); + const innerGraph = { + id: "inner-graph", + nodes: new Map(), + stream: vi.fn(), + }; + const outerGraph = { + id: "outer-graph", + nodes: new Map([ + ["inner", { id: "inner", orchestrator: innerGraph, type: "graphNode" }], + ]), + stream: vi.fn(), + }; + const outerStream = makeMultiAgentStream([ + { + nodeId: "inner", + orchestrator: outerGraph, + type: "beforeNodeCallEvent", + }, + { + nodeId: "inner", + nodeType: "graphNode", + result: { + content: [{ text: "inner done" }], + nodeId: "inner", + status: "COMPLETED", + }, + type: "nodeResultEvent", + }, + { nodeId: "inner", orchestrator: outerGraph, type: "afterNodeCallEvent" }, + { + result: { content: [{ text: "outer done" }], status: "COMPLETED" }, + type: "multiAgentResultEvent", + }, + ]); + const outerEvent = { + arguments: ["outer work", undefined], + result: outerStream, + self: outerGraph, + }; + + graphHandlers.start(outerEvent); + graphHandlers.end(outerEvent); + const outerIterator = outerStream[Symbol.asyncIterator](); + await outerIterator.next(); + + const innerStream = makeMultiAgentStream([ + { + result: { content: [{ text: "nested done" }], status: "COMPLETED" }, + type: "multiAgentResultEvent", + }, + ]); + const innerEvent = { + arguments: ["inner work", undefined], + result: innerStream, + self: innerGraph, + }; + + graphHandlers.start(innerEvent); + graphHandlers.end(innerEvent); + for await (const _chunk of innerStream) { + // consume nested graph stream + } + await outerIterator.next(); + await outerIterator.next(); + await outerIterator.next(); + await outerIterator.next(); + + const nodeSpan = spans.find((span) => span.args.name === "node: inner"); + const innerGraphSpan = spans.find( + (span) => + span.args.name === "Strands Graph" && + span.args.event?.metadata?.["strands.orchestrator.id"] === + "inner-graph", + ); + + expect(innerGraphSpan?.args.parentSpanIds).toEqual({ + rootSpanId: nodeSpan?.rootSpanId, + spanId: nodeSpan?.spanId, + }); + expect(nodeSpan?.end).toHaveBeenCalledTimes(1); + }); + + it("ends open child spans when a stream errors", async () => { + const plugin = new StrandsAgentSDKPlugin(); + plugin.enable(); + + const handlers = handlersByName.get( + "orchestrion:@strands-agents/sdk:Agent.stream", + ); + const model = { getConfig: () => ({ modelId: "gpt-4o-mini" }) }; + const stream = (async function* () { + yield { model, type: "beforeModelCallEvent" }; + throw new Error("stream failed"); + })(); + const event = { + arguments: ["hello", undefined], + result: stream, + self: { model, stream: vi.fn() }, + }; + + handlers.start(event); + handlers.end(event); + + await expect(consume(stream)).rejects.toThrow("stream failed"); + + const rootSpan = spans.find((span) => span.args.name === "Strands Agent"); + const modelSpan = spans.find( + (span) => span.args.name === "Strands model: gpt-4o-mini", + ); + expect(modelSpan?.end).toHaveBeenCalledTimes(1); + expect(rootSpan?.log).toHaveBeenCalledWith( + expect.objectContaining({ error: "stream failed" }), + ); + expect(rootSpan?.end).toHaveBeenCalledTimes(1); + }); +}); + +async function* makeAgentStream(events: unknown[], beforeYield?: () => void) { + for (const event of events) { + beforeYield?.(); + yield event as never; + } + return { + lastMessage: { role: "assistant", content: [{ text: "done" }] }, + stopReason: "end_turn", + }; +} + +async function* makeMultiAgentStream(events: unknown[]) { + for (const event of events) { + yield event as never; + } + return { content: [{ text: "done" }], status: "COMPLETED" }; +} + +async function consume(stream: AsyncIterable) { + for await (const _chunk of stream) { + // consume stream + } +} diff --git a/js/src/instrumentation/plugins/strands-agent-sdk-plugin.ts b/js/src/instrumentation/plugins/strands-agent-sdk-plugin.ts new file mode 100644 index 000000000..0907fc972 --- /dev/null +++ b/js/src/instrumentation/plugins/strands-agent-sdk-plugin.ts @@ -0,0 +1,1088 @@ +import { BasePlugin, toLoggedError } from "../core"; +import type { ChannelMessage } from "../core/channel-definitions"; +import { isAsyncIterable, patchStreamIfNeeded } from "../core/stream-patcher"; +import type { IsoChannelHandlers } from "../../isomorph"; +import { debugLogger } from "../../debug-logger"; +import { startSpan, withCurrent } from "../../logger"; +import type { Span } from "../../logger"; +import { getCurrentUnixTimestamp } from "../../util"; +import { SpanTypeAttribute, isObject } from "../../../util/index"; +import { + bindAutoInstrumentationSuppressionToStart, + runWithAutoInstrumentationSuppressed, +} from "../auto-instrumentation-suppression"; +import { strandsAgentSDKChannels } from "./strands-agent-sdk-channels"; +import type { + StrandsAfterModelCallEvent, + StrandsAfterNodeCallEvent, + StrandsAfterToolCallEvent, + StrandsAgent, + StrandsAgentResult, + StrandsAgentStreamEvent, + StrandsBeforeModelCallEvent, + StrandsBeforeNodeCallEvent, + StrandsBeforeToolCallEvent, + StrandsContentBlock, + StrandsModel, + StrandsModelMetrics, + StrandsModelStreamUpdateEvent, + StrandsMultiAgent, + StrandsMultiAgentHandoffEvent, + StrandsMultiAgentResult, + StrandsMultiAgentStreamEvent, + StrandsNode, + StrandsNodeResultEvent, + StrandsToolUse, + StrandsUsage, +} from "../../vendor-sdk-types/strands-agent-sdk"; + +type AgentStreamState = { + activeModel?: ModelSpanState; + activeTools: Map; + finalized: boolean; + metadata: Record; + span: Span; + startTime: number; +}; + +type ModelSpanState = { + metadata: Record; + metrics: Record; + span: Span; + startTime: number; +}; + +type ToolSpanState = { + span: Span; + startTime: number; + toolUse?: StrandsToolUse; +}; + +type MultiAgentStreamState = { + activeNodes: Map; + finalized: boolean; + handoffs: Array<{ source?: string; targets?: string[] }>; + metadata: Record; + operation: "Graph.stream" | "Swarm.stream"; + orchestrator?: StrandsMultiAgent; + span: Span; + startTime: number; +}; + +type NodeSpanState = { + child?: object; + span: Span; + startTime: number; +}; + +type MultiAgentStreamChannel = + | typeof strandsAgentSDKChannels.graphStream + | typeof strandsAgentSDKChannels.swarmStream; + +type ActiveChildParents = WeakMap>; + +export class StrandsAgentSDKPlugin extends BasePlugin { + private readonly activeChildParents: ActiveChildParents = new WeakMap(); + + protected onEnable(): void { + this.subscribeToAgentStream(); + this.subscribeToMultiAgentStream( + strandsAgentSDKChannels.graphStream, + "Graph.stream", + ); + this.subscribeToMultiAgentStream( + strandsAgentSDKChannels.swarmStream, + "Swarm.stream", + ); + } + + protected onDisable(): void { + for (const unsubscribe of this.unsubscribers) { + unsubscribe(); + } + this.unsubscribers = []; + } + + private subscribeToAgentStream(): void { + const channel = strandsAgentSDKChannels.agentStream.tracingChannel(); + const states = new WeakMap(); + const unbindAutoInstrumentationSuppression = + bindAutoInstrumentationSuppressionToStart(channel); + + const handlers: IsoChannelHandlers< + ChannelMessage + > = { + start: (event) => { + const state = startAgentStream(event, this.activeChildParents); + if (state) { + states.set(event, state); + } + }, + end: (event) => { + const state = states.get(event); + if (!state) { + return; + } + + const result = event.result; + if (isAsyncIterable(result)) { + patchStreamIfNeeded(result, { + aroundNext: (callback) => + runWithAutoInstrumentationSuppressed(callback), + onChunk: (chunk) => handleAgentStreamEvent(state, chunk), + onComplete: () => { + finalizeAgentStream(state); + states.delete(event); + }, + onError: (error) => { + finalizeAgentStream(state, error); + states.delete(event); + }, + }); + return; + } + + finalizeAgentStream(state, undefined, result); + states.delete(event); + }, + error: (event) => { + const state = states.get(event); + if (!state || !event.error) { + return; + } + finalizeAgentStream(state, event.error); + states.delete(event); + }, + }; + + channel.subscribe(handlers); + this.unsubscribers.push(() => { + unbindAutoInstrumentationSuppression?.(); + channel.unsubscribe(handlers); + }); + } + + private subscribeToMultiAgentStream( + channel: MultiAgentStreamChannel, + operation: MultiAgentStreamState["operation"], + ): void { + const tracingChannel = channel.tracingChannel(); + const states = new WeakMap(); + const unbindAutoInstrumentationSuppression = + bindAutoInstrumentationSuppressionToStart(tracingChannel); + + const handlers: IsoChannelHandlers> = { + start: (event) => { + const state = startMultiAgentStream( + event, + operation, + this.activeChildParents, + ); + if (state) { + states.set(event, state); + } + }, + end: (event) => { + const state = states.get(event); + if (!state) { + return; + } + + const result = event.result; + if (isAsyncIterable(result)) { + patchStreamIfNeeded(result, { + aroundNext: (callback) => + runWithAutoInstrumentationSuppressed(callback), + onChunk: (chunk) => + handleMultiAgentStreamEvent( + state, + chunk, + this.activeChildParents, + ), + onComplete: () => { + finalizeMultiAgentStream(state, this.activeChildParents); + states.delete(event); + }, + onError: (error) => { + finalizeMultiAgentStream(state, this.activeChildParents, error); + states.delete(event); + }, + }); + return; + } + + finalizeMultiAgentStream( + state, + this.activeChildParents, + undefined, + result, + ); + states.delete(event); + }, + error: (event) => { + const state = states.get(event); + if (!state || !event.error) { + return; + } + finalizeMultiAgentStream(state, this.activeChildParents, event.error); + states.delete(event); + }, + }; + + tracingChannel.subscribe(handlers); + this.unsubscribers.push(() => { + unbindAutoInstrumentationSuppression?.(); + tracingChannel.unsubscribe(handlers); + }); + } +} + +function startAgentStream( + event: ChannelMessage, + activeChildParents: ActiveChildParents, +): AgentStreamState | undefined { + const agent = extractAgent(event); + const model = agent?.model; + const metadata = { + ...extractAgentMetadata(agent), + ...extractModelMetadata(model), + "strands.operation": "Agent.stream", + provider: extractProvider(model), + ...(event.moduleVersion + ? { "strands_agent_sdk.version": event.moduleVersion } + : {}), + }; + const parentSpan = agent + ? getOnlyChildParent(activeChildParents, agent) + : undefined; + const span = parentSpan + ? withCurrent(parentSpan, () => + startSpan({ + event: { + input: event.arguments[0], + metadata, + }, + name: formatAgentSpanName(agent), + spanAttributes: { type: SpanTypeAttribute.TASK }, + }), + ) + : startSpan({ + event: { + input: event.arguments[0], + metadata, + }, + name: formatAgentSpanName(agent), + spanAttributes: { type: SpanTypeAttribute.TASK }, + }); + + return { + activeTools: new Map(), + finalized: false, + metadata, + span, + startTime: getCurrentUnixTimestamp(), + }; +} + +function startMultiAgentStream( + event: ChannelMessage, + operation: MultiAgentStreamState["operation"], + activeChildParents: ActiveChildParents, +): MultiAgentStreamState { + const orchestrator = extractOrchestrator(event); + const metadata = { + "strands.operation": operation, + provider: "strands", + ...(orchestrator?.id ? { "strands.orchestrator.id": orchestrator.id } : {}), + ...(event.moduleVersion + ? { "strands_agent_sdk.version": event.moduleVersion } + : {}), + }; + const parentSpan = orchestrator + ? getOnlyChildParent(activeChildParents, orchestrator) + : undefined; + const span = parentSpan + ? withCurrent(parentSpan, () => + startSpan({ + event: { + input: event.arguments[0], + metadata, + }, + name: + operation === "Graph.stream" ? "Strands Graph" : "Strands Swarm", + spanAttributes: { type: SpanTypeAttribute.TASK }, + }), + ) + : startSpan({ + event: { + input: event.arguments[0], + metadata, + }, + name: operation === "Graph.stream" ? "Strands Graph" : "Strands Swarm", + spanAttributes: { type: SpanTypeAttribute.TASK }, + }); + + return { + activeNodes: new Map(), + finalized: false, + handoffs: [], + metadata, + operation, + orchestrator, + span, + startTime: getCurrentUnixTimestamp(), + }; +} + +function handleAgentStreamEvent( + state: AgentStreamState, + event: StrandsAgentStreamEvent, +): void { + try { + switch (event.type) { + case "beforeModelCallEvent": + startModelSpan(state, event); + break; + case "modelStreamUpdateEvent": + collectModelStreamMetadata(state, event); + break; + case "afterModelCallEvent": + finalizeModelSpan(state, event); + break; + case "beforeToolCallEvent": + startToolSpan(state, event); + break; + case "afterToolCallEvent": + finalizeToolSpan(state, event); + break; + case "toolResultEvent": + finalizeToolSpanFromResult(state, event.result); + break; + case "agentResultEvent": + safeLog(state.span, { + metadata: { + ...state.metadata, + ...extractAgentResultMetadata(event.result), + }, + metrics: { + ...buildDurationMetrics(state.startTime), + ...parseUsage(event.result?.metrics?.accumulatedUsage), + }, + output: extractAgentResultOutput(event.result), + }); + break; + default: + break; + } + } catch (error) { + logInstrumentationError("Strands Agent SDK event", error); + } +} + +function handleMultiAgentStreamEvent( + state: MultiAgentStreamState, + event: StrandsMultiAgentStreamEvent, + activeChildParents: ActiveChildParents, +): void { + try { + switch (event.type) { + case "beforeNodeCallEvent": + startNodeSpan(state, event, activeChildParents); + break; + case "nodeResultEvent": + logNodeResult(state, event); + break; + case "afterNodeCallEvent": + finalizeNodeSpan(state, event, activeChildParents); + break; + case "multiAgentHandoffEvent": + collectHandoff(state, event); + break; + case "multiAgentResultEvent": + safeLog(state.span, { + metadata: { + ...state.metadata, + ...(state.handoffs.length > 0 + ? { "strands.handoffs": state.handoffs } + : {}), + ...extractMultiAgentResultMetadata(event.result), + }, + metrics: { + ...buildDurationMetrics(state.startTime), + ...parseUsage(event.result?.usage), + }, + output: extractMultiAgentResultOutput(event.result), + }); + break; + default: + break; + } + } catch (error) { + logInstrumentationError("Strands Agent SDK multi-agent event", error); + } +} + +function startModelSpan( + state: AgentStreamState, + event: StrandsBeforeModelCallEvent, +): void { + if (state.activeModel) { + finalizeModelSpan(state); + } + + const model = event.model ?? event.agent?.model; + const metadata = { + ...extractModelMetadata(model), + "strands.operation": "model.stream", + ...(typeof event.projectedInputTokens === "number" + ? { "strands.projected_input_tokens": event.projectedInputTokens } + : {}), + provider: extractProvider(model), + }; + const span = withCurrent(state.span, () => + startSpan({ + event: { + input: Array.isArray(event.agent?.messages) + ? event.agent.messages + : undefined, + metadata, + }, + name: formatModelSpanName(model), + spanAttributes: { type: SpanTypeAttribute.LLM }, + }), + ); + + state.activeModel = { + metadata, + metrics: + typeof event.projectedInputTokens === "number" + ? { prompt_tokens: event.projectedInputTokens } + : {}, + span, + startTime: getCurrentUnixTimestamp(), + }; +} + +function collectModelStreamMetadata( + state: AgentStreamState, + event: StrandsModelStreamUpdateEvent, +): void { + if (!state.activeModel || event.event?.type !== "modelMetadataEvent") { + return; + } + Object.assign(state.activeModel.metrics, parseUsage(event.event.usage)); + Object.assign( + state.activeModel.metrics, + parseModelMetrics(event.event.metrics), + ); +} + +function finalizeModelSpan( + state: AgentStreamState, + event?: StrandsAfterModelCallEvent, +): void { + const modelState = state.activeModel; + if (!modelState) { + return; + } + state.activeModel = undefined; + + const metadata = { + ...modelState.metadata, + ...(event?.attemptCount + ? { "strands.model.attempt": event.attemptCount } + : {}), + ...(event?.stopData?.stopReason + ? { "strands.stop_reason": event.stopData.stopReason } + : {}), + }; + const metrics = { + ...buildDurationMetrics(modelState.startTime), + ...modelState.metrics, + }; + + safeLog(modelState.span, { + ...(event?.error ? { error: toLoggedError(event.error) } : {}), + metadata, + metrics: cleanMetrics(metrics), + output: event?.stopData?.message, + }); + modelState.span.end(); +} + +function startToolSpan( + state: AgentStreamState, + event: StrandsBeforeToolCallEvent, +): void { + const toolUse = event.toolUse; + const key = toolKey(toolUse); + if (state.activeTools.has(key)) { + finalizeToolSpanState(state.activeTools.get(key)); + state.activeTools.delete(key); + } + + const name = extractToolName(toolUse, event.tool); + const span = withCurrent(state.span, () => + startSpan({ + event: { + input: toolUse?.input, + metadata: { + "gen_ai.tool.call.id": toolUse?.toolUseId, + "gen_ai.tool.name": name, + "strands.operation": "tool.call", + "strands.tool.name": name, + provider: "strands", + }, + }, + name: `tool: ${name}`, + spanAttributes: { type: SpanTypeAttribute.TOOL }, + }), + ); + + state.activeTools.set(key, { + span, + startTime: getCurrentUnixTimestamp(), + toolUse, + }); +} + +function finalizeToolSpan( + state: AgentStreamState, + event: StrandsAfterToolCallEvent, +): void { + const key = toolKey(event.toolUse); + const toolState = state.activeTools.get(key); + if (!toolState) { + return; + } + state.activeTools.delete(key); + + finalizeToolSpanState(toolState, { + error: event.error, + result: event.result, + toolUse: event.toolUse, + }); +} + +function finalizeToolSpanFromResult( + state: AgentStreamState, + result: unknown, +): void { + if (!isObject(result)) { + return; + } + const toolUseId = result.toolUseId; + if (typeof toolUseId !== "string") { + return; + } + const toolState = state.activeTools.get(toolUseId); + if (!toolState) { + return; + } + state.activeTools.delete(toolUseId); + finalizeToolSpanState(toolState, { result }); +} + +function finalizeToolSpanState( + toolState: ToolSpanState | undefined, + data: { + error?: unknown; + result?: unknown; + toolUse?: StrandsToolUse; + } = {}, +): void { + if (!toolState) { + return; + } + const result = data.result; + safeLog(toolState.span, { + ...(data.error ? { error: toLoggedError(data.error) } : {}), + metadata: { + ...((data.toolUse?.toolUseId ?? toolState.toolUse?.toolUseId) + ? { + "gen_ai.tool.call.id": + data.toolUse?.toolUseId ?? toolState.toolUse?.toolUseId, + } + : {}), + ...(extractToolName(data.toolUse ?? toolState.toolUse) + ? { + "gen_ai.tool.name": extractToolName( + data.toolUse ?? toolState.toolUse, + ), + } + : {}), + ...(isObject(result) && typeof result.status === "string" + ? { "strands.tool.status": result.status } + : {}), + }, + metrics: buildDurationMetrics(toolState.startTime), + output: result, + }); + toolState.span.end(); +} + +function startNodeSpan( + state: MultiAgentStreamState, + event: StrandsBeforeNodeCallEvent, + activeChildParents: ActiveChildParents, +): void { + const nodeId = event.nodeId ?? "unknown"; + const node = findNode(event.orchestrator ?? state.orchestrator, nodeId); + const child = extractNodeChild(node); + const metadata = { + "strands.node.id": nodeId, + ...(node?.type ? { "strands.node.type": node.type } : {}), + "strands.operation": "node.call", + provider: "strands", + }; + const span = withCurrent(state.span, () => + startSpan({ + event: { metadata }, + name: `node: ${nodeId}`, + spanAttributes: { type: SpanTypeAttribute.TASK }, + }), + ); + const nodeState = { + ...(child ? { child } : {}), + span, + startTime: getCurrentUnixTimestamp(), + }; + + state.activeNodes.set(nodeId, nodeState); + if (child) { + pushChildParent(activeChildParents, child, span); + } +} + +function logNodeResult( + state: MultiAgentStreamState, + event: StrandsNodeResultEvent, +): void { + const nodeId = event.nodeId ?? event.result?.nodeId ?? "unknown"; + const nodeState = state.activeNodes.get(nodeId); + if (!nodeState) { + return; + } + + safeLog(nodeState.span, { + ...(event.result?.error + ? { error: toLoggedError(event.result.error) } + : {}), + metadata: { + ...(event.nodeType ? { "strands.node.type": event.nodeType } : {}), + ...(event.result?.status + ? { "strands.node.status": event.result.status } + : {}), + }, + metrics: { + ...buildDurationMetrics(nodeState.startTime), + ...(typeof event.result?.duration === "number" + ? { "strands.node.duration_ms": event.result.duration } + : {}), + ...parseUsage(event.result?.usage), + }, + output: extractNodeResultOutput(event.result), + }); +} + +function finalizeNodeSpan( + state: MultiAgentStreamState, + event: StrandsAfterNodeCallEvent, + activeChildParents: ActiveChildParents, +): void { + const nodeId = event.nodeId ?? "unknown"; + const nodeState = state.activeNodes.get(nodeId); + if (!nodeState) { + return; + } + state.activeNodes.delete(nodeId); + if (event.error) { + safeLog(nodeState.span, { error: toLoggedError(event.error) }); + } + nodeState.span.end(); + if (nodeState.child) { + popChildParent(activeChildParents, nodeState.child, nodeState.span); + } +} + +function collectHandoff( + state: MultiAgentStreamState, + event: StrandsMultiAgentHandoffEvent, +): void { + state.handoffs.push({ + ...(event.source ? { source: event.source } : {}), + ...(Array.isArray(event.targets) ? { targets: event.targets } : {}), + }); +} + +function finalizeAgentStream( + state: AgentStreamState, + error?: unknown, + output?: unknown, +): void { + if (state.finalized) { + return; + } + state.finalized = true; + + finalizeModelSpan(state); + for (const toolState of state.activeTools.values()) { + finalizeToolSpanState(toolState); + } + state.activeTools.clear(); + + safeLog(state.span, { + ...(error ? { error: toLoggedError(error) } : {}), + metrics: buildDurationMetrics(state.startTime), + ...(output !== undefined ? { output } : {}), + }); + state.span.end(); +} + +function finalizeMultiAgentStream( + state: MultiAgentStreamState, + activeChildParents: ActiveChildParents, + error?: unknown, + output?: unknown, +): void { + if (state.finalized) { + return; + } + state.finalized = true; + + for (const nodeState of state.activeNodes.values()) { + if (nodeState.child) { + popChildParent(activeChildParents, nodeState.child, nodeState.span); + } + nodeState.span.end(); + } + state.activeNodes.clear(); + + safeLog(state.span, { + ...(error ? { error: toLoggedError(error) } : {}), + metrics: buildDurationMetrics(state.startTime), + ...(output !== undefined ? { output } : {}), + }); + state.span.end(); +} + +function extractAgent( + event: ChannelMessage, +): StrandsAgent | undefined { + const candidate = event.agent ?? event.self; + return isObject(candidate) && typeof candidate.stream === "function" + ? (candidate as StrandsAgent) + : undefined; +} + +function extractOrchestrator( + event: ChannelMessage, +): StrandsMultiAgent | undefined { + const candidate = event.orchestrator ?? event.self; + return isObject(candidate) && typeof candidate.stream === "function" + ? (candidate as StrandsMultiAgent) + : undefined; +} + +function extractAgentMetadata( + agent: StrandsAgent | undefined, +): Record { + return { + ...(agent?.id ? { "strands.agent.id": agent.id } : {}), + ...(agent?.name ? { "strands.agent.name": agent.name } : {}), + ...(agent?.description + ? { "strands.agent.description": agent.description } + : {}), + }; +} + +function extractModelMetadata( + model: StrandsModel | undefined, +): Record { + const config = getModelConfig(model); + const modelName = extractModelName(model); + return { + ...(modelName ? { model: modelName } : {}), + ...(config?.api ? { "strands.model.api": config.api } : {}), + ...(model?.stateful !== undefined + ? { "strands.model.stateful": model.stateful } + : {}), + }; +} + +function extractAgentResultMetadata( + result: StrandsAgentResult | undefined, +): Record { + return { + ...(result?.stopReason ? { "strands.stop_reason": result.stopReason } : {}), + ...(typeof result?.metrics?.latestContextSize === "number" + ? { "strands.context_size": result.metrics.latestContextSize } + : {}), + ...(typeof result?.metrics?.projectedContextSize === "number" + ? { + "strands.projected_context_size": result.metrics.projectedContextSize, + } + : {}), + }; +} + +function extractMultiAgentResultMetadata( + result: StrandsMultiAgentResult | undefined, +): Record { + return { + ...(result?.status ? { "strands.status": result.status } : {}), + ...(typeof result?.duration === "number" + ? { "strands.duration_ms": result.duration } + : {}), + }; +} + +function extractProvider(model: StrandsModel | undefined): string { + const config = getModelConfig(model); + if (typeof config?.provider === "string") { + return config.provider; + } + const constructorName = getConstructorName(model).toLowerCase(); + if (constructorName.includes("openai")) { + return "openai"; + } + if (constructorName.includes("anthropic")) { + return "anthropic"; + } + if (constructorName.includes("bedrock")) { + return "bedrock"; + } + if (constructorName.includes("google")) { + return "google"; + } + if (constructorName.includes("vercel")) { + return "vercel"; + } + return "strands"; +} + +function extractModelName(model: StrandsModel | undefined): string | undefined { + const config = getModelConfig(model); + if (typeof model?.modelId === "string") { + return model.modelId; + } + if (typeof config?.modelId === "string") { + return config.modelId; + } + if (typeof config?.model === "string") { + return config.model; + } + return undefined; +} + +function getModelConfig(model: StrandsModel | undefined) { + if (!model || typeof model.getConfig !== "function") { + return undefined; + } + try { + return model.getConfig(); + } catch (error) { + logInstrumentationError("Strands Agent SDK model config", error); + return undefined; + } +} + +function formatAgentSpanName(agent: StrandsAgent | undefined): string { + return agent?.name ? `Agent: ${agent.name}` : "Strands Agent"; +} + +function formatModelSpanName(model: StrandsModel | undefined): string { + const modelName = extractModelName(model); + return modelName ? `Strands model: ${modelName}` : "Strands model"; +} + +function extractAgentResultOutput(result: StrandsAgentResult | undefined) { + if (!result) { + return undefined; + } + if (result.structuredOutput !== undefined) { + return result.structuredOutput; + } + if (result.lastMessage) { + return result.lastMessage; + } + return result; +} + +function extractMultiAgentResultOutput( + result: StrandsMultiAgentResult | undefined, +) { + if (!result) { + return undefined; + } + if (Array.isArray(result.content)) { + return normalizeContentBlocks(result.content); + } + return result; +} + +function extractNodeResultOutput(result: StrandsNodeResultEvent["result"]) { + if (!result) { + return undefined; + } + if (result.structuredOutput !== undefined) { + return result.structuredOutput; + } + if (Array.isArray(result.content)) { + return normalizeContentBlocks(result.content); + } + return result; +} + +function normalizeContentBlocks(blocks: StrandsContentBlock[]): unknown { + const text = blocks + .map((block) => (typeof block.text === "string" ? block.text : undefined)) + .filter((part): part is string => Boolean(part)) + .join(""); + return text.length > 0 ? text : blocks; +} + +function parseUsage(usage: StrandsUsage | undefined): Record { + const metrics: Record = {}; + assignMetric(metrics, "prompt_tokens", usage?.inputTokens); + assignMetric(metrics, "completion_tokens", usage?.outputTokens); + assignMetric(metrics, "tokens", usage?.totalTokens); + assignMetric(metrics, "prompt_cached_tokens", usage?.cacheReadInputTokens); + assignMetric( + metrics, + "prompt_cache_creation_tokens", + usage?.cacheWriteInputTokens, + ); + return metrics; +} + +function parseModelMetrics( + metrics: StrandsModelMetrics | undefined, +): Record { + const parsed: Record = {}; + assignMetric(parsed, "strands.latency_ms", metrics?.latencyMs); + assignMetric( + parsed, + "strands.time_to_first_byte_ms", + metrics?.timeToFirstByteMs, + ); + return parsed; +} + +function assignMetric( + metrics: Record, + key: string, + value: unknown, +): void { + if (typeof value === "number" && Number.isFinite(value)) { + metrics[key] = value; + } +} + +function buildDurationMetrics(startTime: number): Record { + const end = getCurrentUnixTimestamp(); + return { + duration: end - startTime, + end, + start: startTime, + }; +} + +function cleanMetrics(metrics: Record): Record { + const cleaned: Record = {}; + for (const [key, value] of Object.entries(metrics)) { + if (Number.isFinite(value)) { + cleaned[key] = value; + } + } + return cleaned; +} + +function extractToolName( + toolUse: StrandsToolUse | undefined, + tool?: { name?: string }, +): string { + return toolUse?.name ?? tool?.name ?? "unknown"; +} + +function toolKey(toolUse: StrandsToolUse | undefined): string { + return toolUse?.toolUseId ?? toolUse?.name ?? "unknown"; +} + +function findNode( + orchestrator: StrandsMultiAgent | undefined, + nodeId: string, +): StrandsNode | undefined { + try { + return orchestrator?.nodes?.get(nodeId); + } catch { + return undefined; + } +} + +function extractNodeChild(node: StrandsNode | undefined): object | undefined { + const child = node?.agent ?? node?.orchestrator; + return isObject(child) ? child : undefined; +} + +function pushChildParent( + activeChildParents: ActiveChildParents, + child: object, + span: Span, +): void { + const parents = activeChildParents.get(child) ?? new Set(); + parents.add(span); + activeChildParents.set(child, parents); +} + +function popChildParent( + activeChildParents: ActiveChildParents, + child: object, + span: Span, +): void { + const parents = activeChildParents.get(child); + if (!parents) { + return; + } + parents.delete(span); + if (parents.size === 0) { + activeChildParents.delete(child); + } +} + +function getOnlyChildParent( + activeChildParents: ActiveChildParents, + child: object, +): Span | undefined { + const parents = activeChildParents.get(child); + if (!parents || parents.size !== 1) { + // Strands does not provide a child stream to node id correlation here. + // When a reused child has multiple active parents, choosing one would be + // a guess and can create a false parent/child relationship. + return undefined; + } + return parents.values().next().value; +} + +function getConstructorName(value: unknown): string { + if (!isObject(value)) { + return ""; + } + const constructor = value.constructor; + return typeof constructor === "function" && + typeof constructor.name === "string" + ? constructor.name + : ""; +} + +function safeLog(span: Span, event: Parameters[0]): void { + try { + span.log(event); + } catch (error) { + logInstrumentationError("Strands Agent SDK span log", error); + } +} + +function logInstrumentationError(context: string, error: unknown): void { + debugLogger.debug(`${context}:`, error); +} diff --git a/js/src/instrumentation/registry.test.ts b/js/src/instrumentation/registry.test.ts index aa82cf62d..f56374051 100644 --- a/js/src/instrumentation/registry.test.ts +++ b/js/src/instrumentation/registry.test.ts @@ -155,6 +155,7 @@ describe("configureInstrumentation API", () => { mistral: false, cohere: false, mastra: false, + strandsAgentSDK: false, }, }); }); diff --git a/js/src/vendor-sdk-types/strands-agent-sdk.ts b/js/src/vendor-sdk-types/strands-agent-sdk.ts new file mode 100644 index 000000000..b4069e2f8 --- /dev/null +++ b/js/src/vendor-sdk-types/strands-agent-sdk.ts @@ -0,0 +1,316 @@ +/** + * Vendored types for @strands-agents/sdk used by Braintrust instrumentation. + * + * Keep this surface intentionally narrow. These types are not exported to SDK + * users and should only cover fields we read, wrap, or log. + */ + +export interface StrandsAgentSDKModule { + Agent?: StrandsAgentConstructor; + Graph?: StrandsMultiAgentConstructor; + Swarm?: StrandsMultiAgentConstructor; + [key: string]: unknown; +} + +export interface StrandsAgentConstructor { + new (...args: unknown[]): StrandsAgent; + [key: string | symbol]: unknown; +} + +export interface StrandsMultiAgentConstructor { + new (...args: unknown[]): StrandsMultiAgent; + [key: string | symbol]: unknown; +} + +export interface StrandsAgent { + readonly id?: string; + readonly name?: string; + readonly description?: string; + readonly model?: StrandsModel; + readonly messages?: StrandsMessage[]; + invoke( + args: StrandsInvokeArgs, + options?: StrandsInvokeOptions, + ): Promise; + stream( + args: StrandsInvokeArgs, + options?: StrandsInvokeOptions, + ): AsyncGenerator; + [key: string | symbol]: unknown; +} + +export interface StrandsMultiAgent { + readonly id?: string; + readonly nodes?: ReadonlyMap; + invoke( + input: StrandsMultiAgentInput, + options?: StrandsMultiAgentInvokeOptions, + ): Promise; + stream( + input: StrandsMultiAgentInput, + options?: StrandsMultiAgentInvokeOptions, + ): AsyncGenerator< + StrandsMultiAgentStreamEvent, + StrandsMultiAgentResult, + undefined + >; + [key: string | symbol]: unknown; +} + +export type StrandsInvokeArgs = unknown; +export type StrandsMultiAgentInput = unknown; + +export interface StrandsInvokeOptions { + invocationState?: Record; + cancelSignal?: AbortSignal; + structuredOutputSchema?: unknown; + [key: string]: unknown; +} + +export interface StrandsMultiAgentInvokeOptions { + invocationState?: Record; + cancelSignal?: AbortSignal; + [key: string]: unknown; +} + +export interface StrandsModel { + readonly modelId?: string; + readonly stateful?: boolean; + getConfig?: () => StrandsModelConfig; + [key: string | symbol]: unknown; +} + +export interface StrandsModelConfig { + modelId?: string; + model?: string; + provider?: string; + api?: string; + [key: string]: unknown; +} + +export interface StrandsMessage { + role?: string; + content?: StrandsContentBlock[]; + metadata?: { + usage?: StrandsUsage; + metrics?: StrandsModelMetrics; + custom?: Record; + }; + toJSON?: () => unknown; + [key: string]: unknown; +} + +export type StrandsContentBlock = { + type?: string; + text?: string; + toolUse?: StrandsToolUse; + toolResult?: StrandsToolResult; + reasoning?: unknown; + citations?: unknown; + toJSON?: () => unknown; + [key: string]: unknown; +}; + +export interface StrandsToolUse { + name: string; + toolUseId: string; + input: unknown; + [key: string]: unknown; +} + +export interface StrandsToolResult { + toolUseId: string; + status: "success" | "error" | string; + content?: unknown[]; + error?: Error; + [key: string]: unknown; +} + +export interface StrandsAgentResult { + type?: "agentResult"; + stopReason?: string; + lastMessage?: StrandsMessage; + structuredOutput?: unknown; + metrics?: { + accumulatedUsage?: StrandsUsage; + latestUsage?: StrandsUsage; + projectedContextSize?: number; + latestContextSize?: number; + [key: string]: unknown; + }; + interrupts?: unknown[]; + toJSON?: () => unknown; + toString?: () => string; + [key: string]: unknown; +} + +export type StrandsAgentStreamEvent = + | StrandsBeforeModelCallEvent + | StrandsAfterModelCallEvent + | StrandsModelStreamUpdateEvent + | StrandsBeforeToolCallEvent + | StrandsAfterToolCallEvent + | StrandsToolStreamUpdateEvent + | StrandsToolResultEvent + | StrandsAgentResultEvent; + +export interface StrandsBeforeModelCallEvent { + type: "beforeModelCallEvent"; + agent?: StrandsAgent; + model?: StrandsModel; + projectedInputTokens?: number; + [key: string]: unknown; +} + +export interface StrandsAfterModelCallEvent { + type: "afterModelCallEvent"; + agent?: StrandsAgent; + model?: StrandsModel; + stopData?: { + message?: StrandsMessage; + stopReason?: string; + redaction?: unknown; + }; + error?: Error; + attemptCount?: number; + [key: string]: unknown; +} + +export interface StrandsModelStreamUpdateEvent { + type: "modelStreamUpdateEvent"; + event?: { + type?: string; + usage?: StrandsUsage; + metrics?: StrandsModelMetrics; + [key: string]: unknown; + }; + [key: string]: unknown; +} + +export interface StrandsBeforeToolCallEvent { + type: "beforeToolCallEvent"; + agent?: StrandsAgent; + toolUse?: StrandsToolUse; + tool?: { name?: string; description?: string; [key: string]: unknown }; + [key: string]: unknown; +} + +export interface StrandsAfterToolCallEvent { + type: "afterToolCallEvent"; + agent?: StrandsAgent; + toolUse?: StrandsToolUse; + tool?: { name?: string; description?: string; [key: string]: unknown }; + result?: StrandsToolResult; + error?: Error; + [key: string]: unknown; +} + +export interface StrandsToolStreamUpdateEvent { + type: "toolStreamUpdateEvent"; + event?: unknown; + [key: string]: unknown; +} + +export interface StrandsToolResultEvent { + type: "toolResultEvent"; + result?: StrandsToolResult; + [key: string]: unknown; +} + +export interface StrandsAgentResultEvent { + type: "agentResultEvent"; + result?: StrandsAgentResult; + [key: string]: unknown; +} + +export interface StrandsUsage { + inputTokens?: number; + outputTokens?: number; + totalTokens?: number; + cacheReadInputTokens?: number; + cacheWriteInputTokens?: number; + [key: string]: unknown; +} + +export interface StrandsModelMetrics { + latencyMs?: number; + timeToFirstByteMs?: number; + [key: string]: unknown; +} + +export interface StrandsNode { + readonly id?: string; + readonly type?: string; + readonly agent?: StrandsAgent; + readonly orchestrator?: StrandsMultiAgent; + [key: string | symbol]: unknown; +} + +export type StrandsMultiAgentStreamEvent = + | StrandsBeforeNodeCallEvent + | StrandsAfterNodeCallEvent + | StrandsNodeResultEvent + | StrandsMultiAgentHandoffEvent + | StrandsMultiAgentResultEvent; + +export interface StrandsBeforeNodeCallEvent { + type: "beforeNodeCallEvent"; + orchestrator?: StrandsMultiAgent; + nodeId?: string; + state?: unknown; + [key: string]: unknown; +} + +export interface StrandsAfterNodeCallEvent { + type: "afterNodeCallEvent"; + orchestrator?: StrandsMultiAgent; + nodeId?: string; + error?: Error; + [key: string]: unknown; +} + +export interface StrandsNodeResultEvent { + type: "nodeResultEvent"; + nodeId?: string; + nodeType?: string; + result?: StrandsNodeResult; + [key: string]: unknown; +} + +export interface StrandsMultiAgentHandoffEvent { + type: "multiAgentHandoffEvent"; + source?: string; + targets?: string[]; + [key: string]: unknown; +} + +export interface StrandsMultiAgentResultEvent { + type: "multiAgentResultEvent"; + result?: StrandsMultiAgentResult; + [key: string]: unknown; +} + +export interface StrandsNodeResult { + nodeId?: string; + status?: string; + duration?: number; + content?: StrandsContentBlock[]; + error?: Error; + structuredOutput?: unknown; + usage?: StrandsUsage; + toJSON?: () => unknown; + [key: string]: unknown; +} + +export interface StrandsMultiAgentResult { + type?: "multiAgentResult"; + status?: string; + results?: StrandsNodeResult[]; + content?: StrandsContentBlock[]; + duration?: number; + error?: Error; + usage?: StrandsUsage; + interrupts?: unknown[]; + toJSON?: () => unknown; + [key: string]: unknown; +} diff --git a/js/src/wrappers/strands-agent-sdk.test.ts b/js/src/wrappers/strands-agent-sdk.test.ts new file mode 100644 index 000000000..57103b46a --- /dev/null +++ b/js/src/wrappers/strands-agent-sdk.test.ts @@ -0,0 +1,193 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +const { traceSync } = vi.hoisted(() => ({ + traceSync: vi.fn((fn: () => unknown) => fn()), +})); + +vi.mock("../isomorph", () => ({ + default: { + newTracingChannel: vi.fn(() => ({ + subscribe: vi.fn(), + traceSync, + unsubscribe: vi.fn(), + })), + }, +})); + +import { wrapStrandsAgentSDK } from "./strands-agent-sdk"; + +describe("wrapStrandsAgentSDK", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("returns invalid modules unchanged", () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const sdk = { AgentSession: class {} }; + + expect(wrapStrandsAgentSDK(sdk)).toBe(sdk); + expect(warnSpy).toHaveBeenCalledWith( + "Unsupported Strands Agent SDK. Not wrapping.", + ); + + warnSpy.mockRestore(); + }); + + it("wraps Agent.stream and Agent.invoke through the same stream channel", async () => { + class Agent { + name = "assistant"; + + async *stream(input: string) { + yield { type: "agentResultEvent", result: { stopReason: "end_turn" } }; + return { + lastMessage: { role: "assistant", content: [{ text: input }] }, + }; + } + + async invoke() { + throw new Error("original invoke should not be called"); + } + } + + const wrapped = wrapStrandsAgentSDK({ Agent }); + const agent = new wrapped.Agent(); + const chunks = []; + for await (const chunk of agent.stream("hello")) { + chunks.push(chunk); + } + const result = await agent.invoke("world"); + + expect(chunks).toHaveLength(1); + expect(result).toMatchObject({ + lastMessage: { role: "assistant", content: [{ text: "world" }] }, + }); + expect(traceSync).toHaveBeenCalledTimes(2); + expect(traceSync.mock.calls[0][1]).toMatchObject({ + arguments: ["hello", undefined], + self: expect.objectContaining({ name: "assistant" }), + }); + expect(traceSync.mock.calls[1][1]).toMatchObject({ + arguments: ["world", undefined], + }); + }); + + it("wraps Graph and Swarm stream/invoke", async () => { + class Graph { + async *stream(input: string) { + yield { type: "multiAgentResultEvent" }; + return { status: "COMPLETED", content: [{ text: input }] }; + } + + async invoke() { + throw new Error("original invoke should not be called"); + } + } + class Swarm { + async *stream(input: string) { + yield { type: "multiAgentResultEvent" }; + return { status: "COMPLETED", content: [{ text: input }] }; + } + + async invoke() { + throw new Error("original invoke should not be called"); + } + } + + const wrapped = wrapStrandsAgentSDK({ Graph, Swarm }); + + await expect(new wrapped.Graph().invoke("graph")).resolves.toMatchObject({ + status: "COMPLETED", + }); + await expect(new wrapped.Swarm().invoke("swarm")).resolves.toMatchObject({ + status: "COMPLETED", + }); + expect(traceSync).toHaveBeenCalledTimes(2); + expect(traceSync.mock.calls[0][1]).toMatchObject({ + arguments: ["graph", undefined], + }); + expect(traceSync.mock.calls[1][1]).toMatchObject({ + arguments: ["swarm", undefined], + }); + }); + + it("does not double-wrap already wrapped classes", async () => { + class Agent { + async *stream() { + return { stopReason: "end_turn" }; + } + + async invoke() { + throw new Error("original invoke should not be called"); + } + } + + const wrapped = wrapStrandsAgentSDK(wrapStrandsAgentSDK({ Agent })); + + await new wrapped.Agent().invoke("hello"); + expect(traceSync).toHaveBeenCalledTimes(1); + }); + + it("preserves private-field-safe method binding", async () => { + class Agent { + #value = "secret"; + + async *stream() { + return { value: this.#value }; + } + + async invoke() { + throw new Error("original invoke should not be called"); + } + + read() { + return this.#value; + } + } + + const wrapped = wrapStrandsAgentSDK({ Agent }); + const agent = new wrapped.Agent(); + + expect(agent.read()).toBe("secret"); + await expect(agent.invoke("hello")).resolves.toEqual({ value: "secret" }); + }); + + it("preserves non-instrumented function and class exports unchanged", () => { + class Agent { + async *stream() { + return { stopReason: "end_turn" }; + } + } + class Message { + static fromMessageData(data: unknown) { + return { data }; + } + } + class InterruptResponseContent { + static fromJSON(json: unknown) { + return { json }; + } + } + function helper() { + return "ok"; + } + + const wrapped = wrapStrandsAgentSDK({ + Agent, + helper, + InterruptResponseContent, + Message, + }); + + expect(wrapped.Message).toBe(Message); + expect(wrapped.Message.fromMessageData({ role: "user" })).toEqual({ + data: { role: "user" }, + }); + expect(wrapped.InterruptResponseContent).toBe(InterruptResponseContent); + expect( + wrapped.InterruptResponseContent.fromJSON({ type: "interrupt" }), + ).toEqual({ + json: { type: "interrupt" }, + }); + expect(wrapped.helper).toBe(helper); + }); +}); diff --git a/js/src/wrappers/strands-agent-sdk.ts b/js/src/wrappers/strands-agent-sdk.ts new file mode 100644 index 000000000..1b0bc1130 --- /dev/null +++ b/js/src/wrappers/strands-agent-sdk.ts @@ -0,0 +1,259 @@ +import { strandsAgentSDKChannels } from "../instrumentation/plugins/strands-agent-sdk-channels"; +import type { + StrandsAgent, + StrandsAgentConstructor, + StrandsAgentResult, + StrandsAgentSDKModule, + StrandsAgentStreamEvent, + StrandsInvokeArgs, + StrandsInvokeOptions, + StrandsMultiAgent, + StrandsMultiAgentConstructor, + StrandsMultiAgentInput, + StrandsMultiAgentInvokeOptions, + StrandsMultiAgentResult, + StrandsMultiAgentStreamEvent, +} from "../vendor-sdk-types/strands-agent-sdk"; + +const WRAPPED_CLASS = Symbol.for("braintrust.strands-agent-sdk.wrapped-class"); +const WRAPPED_INSTANCE = Symbol.for( + "braintrust.strands-agent-sdk.wrapped-instance", +); + +/** + * Wraps the Strands Agent SDK with Braintrust tracing. The wrapper emits + * diagnostics-channel events; the Strands plugin owns span lifecycle. + */ +export function wrapStrandsAgentSDK(sdk: T): T { + if (!sdk || typeof sdk !== "object") { + return sdk; + } + + const maybeSDK = sdk as Record; + if ( + typeof maybeSDK.Agent !== "function" && + typeof maybeSDK.Graph !== "function" && + typeof maybeSDK.Swarm !== "function" + ) { + // eslint-disable-next-line no-restricted-properties -- preserving intentional console usage. + console.warn("Unsupported Strands Agent SDK. Not wrapping."); + return sdk; + } + + const target = isModuleNamespace(sdk) + ? Object.setPrototypeOf({}, sdk) + : (sdk as Record); + + return new Proxy(target, { + get(target, prop, receiver) { + const value = Reflect.get(target, prop, receiver); + + if (prop === "Agent" && typeof value === "function") { + return wrapAgentClass(value as unknown as StrandsAgentConstructor); + } + if (prop === "Graph" && typeof value === "function") { + return wrapMultiAgentClass( + value as unknown as StrandsMultiAgentConstructor, + "graph", + ); + } + if (prop === "Swarm" && typeof value === "function") { + return wrapMultiAgentClass( + value as unknown as StrandsMultiAgentConstructor, + "swarm", + ); + } + + return value; + }, + }) as T & StrandsAgentSDKModule; +} + +function isModuleNamespace(obj: unknown): boolean { + if (!obj || typeof obj !== "object") { + return false; + } + if (obj.constructor?.name === "Module") { + return true; + } + const keys = Object.keys(obj); + if (keys.length === 0) { + return false; + } + const descriptor = Object.getOwnPropertyDescriptor(obj, keys[0]); + return descriptor ? !descriptor.configurable && !descriptor.writable : false; +} + +function wrapAgentClass( + AgentClass: StrandsAgentConstructor, +): StrandsAgentConstructor { + if ((AgentClass as Record)[WRAPPED_CLASS]) { + return AgentClass; + } + + return new Proxy(AgentClass, { + get(target, prop, receiver) { + if (prop === WRAPPED_CLASS) { + return true; + } + const value = Reflect.get(target, prop, receiver); + return typeof value === "function" ? value.bind(target) : value; + }, + construct(target, args, newTarget) { + const instance = Reflect.construct(target, args, newTarget); + return wrapAgentInstance(instance as StrandsAgent); + }, + }); +} + +function wrapMultiAgentClass( + MultiAgentClass: StrandsMultiAgentConstructor, + kind: "graph" | "swarm", +): StrandsMultiAgentConstructor { + if ((MultiAgentClass as Record)[WRAPPED_CLASS]) { + return MultiAgentClass; + } + + return new Proxy(MultiAgentClass, { + get(target, prop, receiver) { + if (prop === WRAPPED_CLASS) { + return true; + } + const value = Reflect.get(target, prop, receiver); + return typeof value === "function" ? value.bind(target) : value; + }, + construct(target, args, newTarget) { + const instance = Reflect.construct(target, args, newTarget); + return wrapMultiAgentInstance(instance as StrandsMultiAgent, kind); + }, + }); +} + +function wrapAgentInstance(agent: StrandsAgent): StrandsAgent { + if (!agent || typeof agent !== "object") { + return agent; + } + if ((agent as Record)[WRAPPED_INSTANCE]) { + return agent; + } + + const proxy: StrandsAgent = new Proxy(agent, { + get(target, prop, receiver) { + if (prop === WRAPPED_INSTANCE) { + return true; + } + + const value = Reflect.get(target, prop, receiver); + if (prop === "stream" && typeof value === "function") { + return function ( + args: StrandsInvokeArgs, + options?: StrandsInvokeOptions, + ): AsyncGenerator< + StrandsAgentStreamEvent, + StrandsAgentResult, + undefined + > { + const callArgs = [args, options] as [ + StrandsInvokeArgs, + StrandsInvokeOptions | undefined, + ]; + return strandsAgentSDKChannels.agentStream.traceSync( + () => Reflect.apply(value, target, callArgs), + { + agent: proxy, + arguments: callArgs, + self: proxy, + } as never, + ); + }; + } + + if (prop === "invoke" && typeof value === "function") { + return async function ( + args: StrandsInvokeArgs, + options?: StrandsInvokeOptions, + ): Promise { + return consumeAsyncGenerator(proxy.stream(args, options)); + }; + } + + return typeof value === "function" ? value.bind(target) : value; + }, + }); + + return proxy; +} + +function wrapMultiAgentInstance( + orchestrator: StrandsMultiAgent, + kind: "graph" | "swarm", +): StrandsMultiAgent { + if (!orchestrator || typeof orchestrator !== "object") { + return orchestrator; + } + if ((orchestrator as Record)[WRAPPED_INSTANCE]) { + return orchestrator; + } + + const proxy: StrandsMultiAgent = new Proxy(orchestrator, { + get(target, prop, receiver) { + if (prop === WRAPPED_INSTANCE) { + return true; + } + + const value = Reflect.get(target, prop, receiver); + if (prop === "stream" && typeof value === "function") { + return function ( + input: StrandsMultiAgentInput, + options?: StrandsMultiAgentInvokeOptions, + ): AsyncGenerator< + StrandsMultiAgentStreamEvent, + StrandsMultiAgentResult, + undefined + > { + const callArgs = [input, options] as [ + StrandsMultiAgentInput, + StrandsMultiAgentInvokeOptions | undefined, + ]; + const channel = + kind === "graph" + ? strandsAgentSDKChannels.graphStream + : strandsAgentSDKChannels.swarmStream; + return channel.traceSync( + () => Reflect.apply(value, target, callArgs), + { + arguments: callArgs, + orchestrator: proxy, + self: proxy, + } as never, + ); + }; + } + + if (prop === "invoke" && typeof value === "function") { + return async function ( + input: StrandsMultiAgentInput, + options?: StrandsMultiAgentInvokeOptions, + ): Promise { + return consumeAsyncGenerator(proxy.stream(input, options)); + }; + } + + return typeof value === "function" ? value.bind(target) : value; + }, + }); + + return proxy; +} + +async function consumeAsyncGenerator( + generator: AsyncGenerator, +): Promise { + let result = await generator.next(); + while (!result.done) { + result = await generator.next(); + } + return result.value; +} + +export type { StrandsAgentSDKModule };