diff --git a/.github/workflows/dojo-e2e.yml b/.github/workflows/dojo-e2e.yml index 4f1173ea3a..0ad56a253a 100644 --- a/.github/workflows/dojo-e2e.yml +++ b/.github/workflows/dojo-e2e.yml @@ -140,6 +140,10 @@ jobs: test_path: tests/awsStrandsTests services: ["dojo", "aws-strands"] wait_on: http://localhost:9999,tcp:localhost:8017 + - suite: aws-strands-typescript + test_path: tests/awsStrandsTypescriptTests + services: ["dojo", "aws-strands-typescript"] + wait_on: http://localhost:9999,tcp:localhost:8022 - suite: claude-agent-sdk-python test_path: tests/claudeAgentSdkPythonTests services: ["dojo", "claude-agent-sdk-python"] diff --git a/apps/dojo/e2e/tests/awsStrandsTypescriptTests/agenticChatMultimodalPage.spec.ts b/apps/dojo/e2e/tests/awsStrandsTypescriptTests/agenticChatMultimodalPage.spec.ts new file mode 100644 index 0000000000..f19457d659 --- /dev/null +++ b/apps/dojo/e2e/tests/awsStrandsTypescriptTests/agenticChatMultimodalPage.spec.ts @@ -0,0 +1,31 @@ +import { test, expect } from "../../test-isolation-helper"; +import * as path from "path"; +import { + sendChatMessage, + awaitLLMResponseDone, + openChat, +} from "../../utils/copilot-actions"; +import { CopilotSelectors } from "../../utils/copilot-selectors"; + +const TEST_IMAGE = path.join( + import.meta.dirname, + "../../fixtures/test-image.png", +); + +test.describe("[Integration] AWS Strands (TS) - Agentic Chat Multimodal", () => { + test("should upload an image and receive a description", async ({ page }) => { + await page.goto("/aws-strands-typescript/feature/agentic_chat_multimodal"); + await openChat(page); + + const fileInput = page.locator('input[type="file"]'); + await fileInput.setInputFiles(TEST_IMAGE); + + await sendChatMessage(page, "Tell me what do you see in this image"); + await awaitLLMResponseDone(page); + + const lastAssistant = CopilotSelectors.assistantMessages(page).last(); + await expect(lastAssistant).toContainText(/image|visual|content/i, { + timeout: 10000, + }); + }); +}); diff --git a/apps/dojo/e2e/tests/awsStrandsTypescriptTests/agenticChatPage.spec.ts b/apps/dojo/e2e/tests/awsStrandsTypescriptTests/agenticChatPage.spec.ts new file mode 100644 index 0000000000..de9295ab18 --- /dev/null +++ b/apps/dojo/e2e/tests/awsStrandsTypescriptTests/agenticChatPage.spec.ts @@ -0,0 +1,81 @@ +import { test, expect } from "../../test-isolation-helper"; +import { AgenticChatPage } from "../../featurePages/AgenticChatPage"; + +test("[StrandsTS] Agentic Chat sends and receives a message", async ({ + page, +}) => { + await page.goto("/aws-strands-typescript/feature/agentic_chat"); + + const chat = new AgenticChatPage(page); + + await chat.openChat(); + await expect(chat.agentGreeting).toBeVisible(); + await chat.sendMessage("Hi, I am duaa"); + + await chat.assertUserMessageVisible("Hi, I am duaa"); + await chat.assertAgentReplyVisible(/Hello duaa/i); +}); + +test("[StrandsTS] Agentic Chat changes background on message and reset", async ({ + page, +}) => { + await page.goto("/aws-strands-typescript/feature/agentic_chat"); + + const chat = new AgenticChatPage(page); + + await chat.openChat(); + await expect(chat.agentGreeting).toBeVisible(); + + const backgroundContainer = page.locator( + '[data-testid="background-container"]', + ); + const getBackground = () => + backgroundContainer.evaluate((el) => el.style.background); + const initialBackground = await getBackground(); + + await chat.sendMessage("Hi change the background color to blue"); + await chat.assertUserMessageVisible("Hi change the background color to blue"); + + await expect.poll(getBackground).not.toBe(initialBackground); + const backgroundAfterBlue = await getBackground(); + + await chat.sendMessage("Hi change the background color to pink"); + await chat.assertUserMessageVisible("Hi change the background color to pink"); + + await expect.poll(getBackground).not.toBe(backgroundAfterBlue); +}); + +test("[StrandsTS] Agentic Chat retains memory of user messages during a conversation", async ({ + page, +}) => { + await page.goto("/aws-strands-typescript/feature/agentic_chat"); + + const chat = new AgenticChatPage(page); + await chat.openChat(); + await chat.agentGreeting.click(); + + await chat.sendMessage("Hey there"); + await chat.assertUserMessageVisible("Hey there"); + await chat.assertAgentReplyVisible(/Hello! How can I assist you today\?/); + + const favFruit = "Mango"; + await chat.sendMessage(`My favorite fruit is ${favFruit}`); + await chat.assertUserMessageVisible(`My favorite fruit is ${favFruit}`); + await chat.assertAgentReplyVisible(/Mango is a wonderful tropical fruit/); + + await chat.sendMessage("and I love listening to Kaavish"); + await chat.assertUserMessageVisible("and I love listening to Kaavish"); + await chat.assertAgentReplyVisible(/Kaavish is a wonderful musical group/); + + await chat.sendMessage("tell me an interesting fact about Moon"); + await chat.assertUserMessageVisible("tell me an interesting fact about Moon"); + await chat.assertAgentReplyVisible( + /The Moon is Earth's only natural satellite/, + ); + + await chat.sendMessage("Can you remind me what my favorite fruit is?"); + await chat.assertUserMessageVisible( + "Can you remind me what my favorite fruit is?", + ); + await chat.assertAgentReplyVisible(/Your favorite fruit is Mango!/); +}); diff --git a/apps/dojo/e2e/tests/awsStrandsTypescriptTests/agenticChatReasoningPage.spec.ts b/apps/dojo/e2e/tests/awsStrandsTypescriptTests/agenticChatReasoningPage.spec.ts new file mode 100644 index 0000000000..d4053b7093 --- /dev/null +++ b/apps/dojo/e2e/tests/awsStrandsTypescriptTests/agenticChatReasoningPage.spec.ts @@ -0,0 +1,28 @@ +import { test, expect } from "../../test-isolation-helper"; +import { + sendChatMessage, + awaitLLMResponseDone, + openChat, +} from "../../utils/copilot-actions"; +import { CopilotSelectors } from "../../utils/copilot-selectors"; + +test.describe("[Integration] AWS Strands (TS) - Agentic Chat Reasoning", () => { + test("should show reasoning indicator and then the response", async ({ + page, + }) => { + await page.goto("/aws-strands-typescript/feature/agentic_chat_reasoning"); + await openChat(page); + + await sendChatMessage(page, "What is the best car to buy?"); + await awaitLLMResponseDone(page); + + const reasoningIndicator = page.getByText(/Thought for/i); + await expect(reasoningIndicator).toBeVisible({ timeout: 10000 }); + + const lastAssistant = CopilotSelectors.assistantMessages(page).last(); + await expect(lastAssistant).toContainText( + /Toyota|Honda|Mazda|recommendations|car|vehicle/i, + { timeout: 10000 }, + ); + }); +}); diff --git a/apps/dojo/e2e/tests/awsStrandsTypescriptTests/agenticGenUI.spec.ts b/apps/dojo/e2e/tests/awsStrandsTypescriptTests/agenticGenUI.spec.ts new file mode 100644 index 0000000000..7243c65f9b --- /dev/null +++ b/apps/dojo/e2e/tests/awsStrandsTypescriptTests/agenticGenUI.spec.ts @@ -0,0 +1,40 @@ +import { awaitLLMResponseDone } from "../../utils/copilot-actions"; +import { test, expect } from "../../test-isolation-helper"; +import { AgenticGenUIPage } from "../../pages/awsStrandsPages/AgenticUIGenPage"; + +test.describe("Agent Generative UI Feature", () => { + test("[StrandsTS] should interact with the chat to get a planner on prompt", async ({ + page, + }) => { + const genUIAgent = new AgenticGenUIPage(page); + + await page.goto("/aws-strands-typescript/feature/agentic_generative_ui"); + + await genUIAgent.openChat(); + await genUIAgent.sendMessage("Hi"); + await genUIAgent.assertAgentReplyVisible(/Hello/); + + await genUIAgent.sendMessage("give me a plan to make brownies"); + await expect(genUIAgent.agentPlannerContainer).toBeVisible(); + await genUIAgent.plan(); + await awaitLLMResponseDone(page); + }); + + test("[StrandsTS] should interact with the chat using predefined prompts and perform steps", async ({ + page, + }) => { + const genUIAgent = new AgenticGenUIPage(page); + + await page.goto("/aws-strands-typescript/feature/agentic_generative_ui"); + + await genUIAgent.openChat(); + await genUIAgent.sendMessage("Hi"); + await genUIAgent.assertAgentReplyVisible(/Hello/); + + await genUIAgent.sendMessage("Go to Mars"); + + await expect(genUIAgent.agentPlannerContainer).toBeVisible(); + await genUIAgent.plan(); + await awaitLLMResponseDone(page); + }); +}); diff --git a/apps/dojo/e2e/tests/awsStrandsTypescriptTests/backendToolRenderingPage.spec.ts b/apps/dojo/e2e/tests/awsStrandsTypescriptTests/backendToolRenderingPage.spec.ts new file mode 100644 index 0000000000..43c0a3e8e8 --- /dev/null +++ b/apps/dojo/e2e/tests/awsStrandsTypescriptTests/backendToolRenderingPage.spec.ts @@ -0,0 +1,50 @@ +import { test, expect } from "../../test-isolation-helper"; + +test("[StrandsTS] Backend Tool Rendering displays weather cards", async ({ + page, +}) => { + test.setTimeout(30000); + + await page.goto("/aws-strands-typescript/feature/backend_tool_rendering"); + + await expect( + page.getByRole("button", { name: "Weather in San Francisco" }), + ).toBeVisible({ + timeout: 5000, + }); + + await page.getByRole("button", { name: "Weather in San Francisco" }).click(); + + const weatherCard = page.getByTestId("weather-card"); + const currentWeatherText = page.getByText("Current Weather"); + + try { + await expect(weatherCard).toBeVisible(); + } catch { + await expect(currentWeatherText.first()).toBeVisible(); + } + + const hasHumidity = await page + .getByText("Humidity") + .isVisible() + .catch(() => false); + const hasWind = await page + .getByText("Wind") + .isVisible() + .catch(() => false); + const hasCityName = await page + .locator("h3") + .filter({ hasText: /San Francisco/i }) + .isVisible() + .catch(() => false); + + expect(hasHumidity || hasWind || hasCityName).toBeTruthy(); + + await page.getByRole("button", { name: "Weather in New York" }).click(); + await page.waitForTimeout(2000); + + const weatherElements = await page + .getByText(/Weather|Humidity|Wind|Temperature/i) + .count(); + expect(weatherElements).toBeGreaterThan(0); +}); diff --git a/apps/dojo/e2e/tests/awsStrandsTypescriptTests/humanInTheLoopPage.spec.ts b/apps/dojo/e2e/tests/awsStrandsTypescriptTests/humanInTheLoopPage.spec.ts new file mode 100644 index 0000000000..a243fae768 --- /dev/null +++ b/apps/dojo/e2e/tests/awsStrandsTypescriptTests/humanInTheLoopPage.spec.ts @@ -0,0 +1,54 @@ +import { test, expect } from "../../test-isolation-helper"; +import { HumanInLoopPage } from "../../pages/awsStrandsPages/HumanInLoopPage"; + +test.describe("Human in the Loop Feature", () => { + test("[StrandsTS] should interact with the chat and perform steps", async ({ + page, + }) => { + const humanInLoop = new HumanInLoopPage(page); + + await page.goto("/aws-strands-typescript/feature/human_in_the_loop"); + + await humanInLoop.openChat(); + + await humanInLoop.sendMessage("Hi"); + + await humanInLoop.sendMessage( + "Give me a plan to make brownies, there should be only one step with eggs and one step with oven, this is a strict requirement so adhere", + ); + await expect(humanInLoop.plan).toBeVisible(); + + const itemText = "eggs"; + await humanInLoop.uncheckItem(itemText); + await humanInLoop.performStepsAndAwait(); + + await humanInLoop.sendMessage( + `Does the planner include ${itemText}? ⚠️ Reply with only words 'Yes' or 'No' (no explanation, no punctuation).`, + ); + }); + + test("[StrandsTS] should interact with the chat using predefined prompts and perform steps", async ({ + page, + }) => { + const humanInLoop = new HumanInLoopPage(page); + + await page.goto("/aws-strands-typescript/feature/human_in_the_loop"); + + await humanInLoop.openChat(); + + await humanInLoop.sendMessage("Hi"); + await humanInLoop.sendMessage( + "Plan a mission to Mars with the first step being Start The Planning", + ); + await expect(humanInLoop.plan).toBeVisible(); + + const uncheckedItem = "Start The Planning"; + + await humanInLoop.uncheckItem(uncheckedItem); + await humanInLoop.performStepsAndAwait(); + + await humanInLoop.sendMessage( + `Does the planner include ${uncheckedItem}? ⚠️ Reply with only words 'Yes' or 'No' (no explanation, no punctuation).`, + ); + }); +}); diff --git a/apps/dojo/e2e/tests/awsStrandsTypescriptTests/sharedStatePage.spec.ts b/apps/dojo/e2e/tests/awsStrandsTypescriptTests/sharedStatePage.spec.ts new file mode 100644 index 0000000000..f141e4b41e --- /dev/null +++ b/apps/dojo/e2e/tests/awsStrandsTypescriptTests/sharedStatePage.spec.ts @@ -0,0 +1,54 @@ +import { test, expect } from "../../test-isolation-helper"; +import { SharedStatePage } from "../../featurePages/SharedStatePage"; + +test.describe("Shared State Feature", () => { + test("[StrandsTS] should interact with the chat to get a recipe on prompt", async ({ + page, + }) => { + const sharedStateAgent = new SharedStatePage(page); + + await page.goto("/aws-strands-typescript/feature/shared_state"); + + await sharedStateAgent.openChat(); + await sharedStateAgent.sendMessage( + 'Please give me a pasta recipe of your choosing, but one of the ingredients should be "Pasta". Not a type of pasta, exactly the word "Pasta".', + ); + await sharedStateAgent.loader(); + await sharedStateAgent.awaitIngredientCard("Pasta"); + await sharedStateAgent.getInstructionItems( + sharedStateAgent.instructionsContainer, + ); + }); + + test("[StrandsTS] should share state between UI and chat", async ({ + page, + }) => { + const sharedStateAgent = new SharedStatePage(page); + + await page.goto("/aws-strands-typescript/feature/shared_state"); + + await sharedStateAgent.openChat(); + + await sharedStateAgent.addIngredient.click(); + + const newIngredientCard = page.locator(".ingredient-card").last(); + await newIngredientCard.locator(".ingredient-name-input").fill("Potatoes"); + await newIngredientCard.locator(".ingredient-amount-input").fill("12"); + + await page.waitForTimeout(1000); + + await sharedStateAgent.sendMessage("Please list all of the ingredients"); + await sharedStateAgent.loader(); + + await expect( + sharedStateAgent.agentMessage.getByText(/Potatoes/), + ).toBeVisible(); + await expect(sharedStateAgent.agentMessage.getByText(/12/)).toBeVisible(); + await expect( + sharedStateAgent.agentMessage.getByText(/Carrots/), + ).toBeVisible(); + await expect( + sharedStateAgent.agentMessage.getByText(/All-Purpose Flour/), + ).toBeVisible(); + }); +}); diff --git a/apps/dojo/e2e/tests/awsStrandsTypescriptTests/toolBasedGenUIPage.spec.ts b/apps/dojo/e2e/tests/awsStrandsTypescriptTests/toolBasedGenUIPage.spec.ts new file mode 100644 index 0000000000..a8385896f5 --- /dev/null +++ b/apps/dojo/e2e/tests/awsStrandsTypescriptTests/toolBasedGenUIPage.spec.ts @@ -0,0 +1,37 @@ +import { test, expect } from "../../test-isolation-helper"; +import { ToolBaseGenUIPage } from "../../featurePages/ToolBaseGenUIPage"; + +const pageURL = "/aws-strands-typescript/feature/tool_based_generative_ui"; + +test("[StrandsTS] Haiku generation and display verification", async ({ + page, +}) => { + await page.goto(pageURL); + + const genAIAgent = new ToolBaseGenUIPage(page); + + await expect(genAIAgent.haikuAgentIntro).toBeVisible(); + await genAIAgent.generateHaiku('Generate Haiku for "I will always win"'); + await genAIAgent.checkGeneratedHaiku(); + await genAIAgent.checkHaikuDisplay(page); +}); + +test("[StrandsTS] Haiku generation and UI consistency for two different prompts", async ({ + page, +}) => { + await page.goto(pageURL); + + const genAIAgent = new ToolBaseGenUIPage(page); + + await expect(genAIAgent.haikuAgentIntro).toBeVisible(); + + const prompt1 = 'Generate Haiku for "I will always win"'; + await genAIAgent.generateHaiku(prompt1); + await genAIAgent.checkGeneratedHaiku(); + await genAIAgent.checkHaikuDisplay(page); + + const prompt2 = 'Generate Haiku for "The moon shines bright"'; + await genAIAgent.generateHaiku(prompt2); + await genAIAgent.checkGeneratedHaiku(); + await genAIAgent.checkHaikuDisplay(page); +}); diff --git a/apps/dojo/e2e/tests/awsStrandsTypescriptTests/v1AgenticChatPage.spec.ts b/apps/dojo/e2e/tests/awsStrandsTypescriptTests/v1AgenticChatPage.spec.ts new file mode 100644 index 0000000000..74f6094547 --- /dev/null +++ b/apps/dojo/e2e/tests/awsStrandsTypescriptTests/v1AgenticChatPage.spec.ts @@ -0,0 +1,14 @@ +import { test } from "../../test-isolation-helper"; +import { V1AgenticChatPage } from "../../featurePages/V1AgenticChatPage"; + +test("[V1] AWS Strands (TypeScript) sends and receives a message", async ({ + page, +}) => { + await page.goto("/aws-strands-typescript/feature/v1_agentic_chat"); + + const chat = new V1AgenticChatPage(page); + await chat.sendMessage("Hi"); + + await chat.assertUserMessageVisible("Hi"); + await chat.assertAgentReplyVisible(/Hello! How can I assist you today\?/); +}); diff --git a/apps/dojo/scripts/generate-content-json.ts b/apps/dojo/scripts/generate-content-json.ts index 01d06ad9e4..612bce4605 100644 --- a/apps/dojo/scripts/generate-content-json.ts +++ b/apps/dojo/scripts/generate-content-json.ts @@ -346,6 +346,23 @@ const agentFilesMapper: Record< {}, ); }, + "aws-strands-typescript": (agentKeys: string[]) => { + // TS example filenames use hyphens — map underscore keys (agentic_chat) + // to hyphenated filenames (agentic-chat.ts). + return agentKeys.reduce( + (acc, agentId) => ({ + ...acc, + [agentId]: [ + path.join( + __dirname, + integrationsFolderPath, + `/aws-strands/typescript/examples/server/api/${agentId.replace(/_/g, "-")}.ts`, + ), + ], + }), + {}, + ); + }, "microsoft-agent-framework-python": (agentKeys: string[]) => { return agentKeys.reduce( (acc, agentId) => ({ diff --git a/apps/dojo/scripts/prep-dojo-everything.js b/apps/dojo/scripts/prep-dojo-everything.js index d5e510e8a5..d9a83aeed5 100755 --- a/apps/dojo/scripts/prep-dojo-everything.js +++ b/apps/dojo/scripts/prep-dojo-everything.js @@ -109,6 +109,11 @@ const ALL_TARGETS = { name: "AWS Strands", cwd: path.join(integrationsRoot, "aws-strands/python/examples"), }, + "aws-strands-typescript": { + command: "pnpm install", + name: "AWS Strands (TypeScript)", + cwd: path.join(integrationsRoot, "aws-strands/typescript/examples"), + }, "adk-middleware": { command: "uv sync", name: "ADK Middleware", diff --git a/apps/dojo/scripts/run-dojo-everything.js b/apps/dojo/scripts/run-dojo-everything.js index d92989a3d6..92f2eadfab 100755 --- a/apps/dojo/scripts/run-dojo-everything.js +++ b/apps/dojo/scripts/run-dojo-everything.js @@ -175,6 +175,14 @@ const ALL_SERVICES = { env: { PORT: 8017 }, }, ], + "aws-strands-typescript": [ + { + command: "pnpm run dojo", + name: "AWS Strands (TypeScript)", + cwd: path.join(integrationsRoot, "aws-strands/typescript/examples"), + env: { PORT: 8022 }, + }, + ], "adk-middleware": [ { command: "uv run dev", @@ -290,6 +298,7 @@ const ALL_SERVICES = { AGENT_FRAMEWORK_PYTHON_URL: "http://localhost:8015", AGENT_FRAMEWORK_DOTNET_URL: "http://localhost:8016", AWS_STRANDS_URL: "http://localhost:8017", + AWS_STRANDS_TYPESCRIPT_URL: "http://localhost:8022", CLAUDE_AGENT_SDK_PYTHON_URL: "http://localhost:8019", CLAUDE_AGENT_SDK_TYPESCRIPT_URL: "http://localhost:8020", LANGROID_URL: "http://localhost:8021", @@ -324,6 +333,7 @@ const ALL_SERVICES = { AGENT_FRAMEWORK_PYTHON_URL: "http://localhost:8015", AGENT_FRAMEWORK_DOTNET_URL: "http://localhost:8016", AWS_STRANDS_URL: "http://localhost:8017", + AWS_STRANDS_TYPESCRIPT_URL: "http://localhost:8022", CLAUDE_AGENT_SDK_PYTHON_URL: "http://localhost:8019", CLAUDE_AGENT_SDK_TYPESCRIPT_URL: "http://localhost:8020", LANGROID_URL: "http://localhost:8021", diff --git a/apps/dojo/src/agents.ts b/apps/dojo/src/agents.ts index 4a4a1f1844..8374022335 100644 --- a/apps/dojo/src/agents.ts +++ b/apps/dojo/src/agents.ts @@ -414,6 +414,33 @@ export const agentsIntegrations = { }), }), + "aws-strands-typescript": async () => ({ + // TS example server mounts every endpoint on hyphenated paths (matching the + // Python reference server) so the same curl payloads drive both adapters. + // v1_agentic_chat reuses the agentic-chat endpoint — the dojo page renders + // the same agent via the v1 CopilotChat UI instead of the v2 shell. + ...mapAgents( + (path) => + new AWSStrandsAgent({ + url: `${envVars.awsStrandsTypescriptUrl}/${path}/`, + }), + { + agentic_chat: "agentic-chat", + agentic_chat_reasoning: "agentic-chat-reasoning", + agentic_chat_multimodal: "agentic-chat-multimodal", + v1_agentic_chat: "agentic-chat", + backend_tool_rendering: "backend-tool-rendering", + agentic_generative_ui: "agentic-generative-ui", + shared_state: "shared-state", + tool_based_generative_ui: "tool-based-generative-ui", + }, + ), + human_in_the_loop: new AWSStrandsAgent({ + url: `${envVars.awsStrandsTypescriptUrl}/human-in-the-loop`, + debug: true, + }), + }), + ag2: async () => mapAgents((path) => new Ag2Agent({ url: `${envVars.ag2Url}/${path}` }), { agentic_chat: "agentic_chat", diff --git a/apps/dojo/src/env.ts b/apps/dojo/src/env.ts index 75dc8e37ba..d81bd4ed90 100644 --- a/apps/dojo/src/env.ts +++ b/apps/dojo/src/env.ts @@ -21,6 +21,7 @@ type envVars = { a2aMiddlewareItUrl: string; a2aMiddlewareOrchestratorUrl: string; awsStrandsUrl: string; + awsStrandsTypescriptUrl: string; claudeAgentSdkPythonUrl: string; claudeAgentSdkTypescriptUrl: string; langroidUrl: string; @@ -75,6 +76,8 @@ export default function getEnvVars(): envVars { a2aMiddlewareOrchestratorUrl: process.env.A2A_MIDDLEWARE_ORCHESTRATOR_URL || "http://localhost:9000", awsStrandsUrl: process.env.AWS_STRANDS_URL || "http://localhost:8000", + awsStrandsTypescriptUrl: + process.env.AWS_STRANDS_TYPESCRIPT_URL || "http://localhost:8022", claudeAgentSdkPythonUrl: process.env.CLAUDE_AGENT_SDK_PYTHON_URL || "http://localhost:8019", claudeAgentSdkTypescriptUrl: diff --git a/apps/dojo/src/files.json b/apps/dojo/src/files.json index 32d1f101c3..d5f2d791c7 100644 --- a/apps/dojo/src/files.json +++ b/apps/dojo/src/files.json @@ -3281,6 +3281,210 @@ "type": "file" } ], + "aws-strands-typescript::agentic_chat": [ + { + "name": "page.tsx", + "content": "\"use client\";\nimport React, { useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { \n useFrontendTool,\n useRenderTool,\n useAgentContext,\n useConfigureSuggestions,\n CopilotChat,\n} from \"@copilotkit/react-core/v2\";\nimport { z } from \"zod\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface AgenticChatProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst AgenticChat: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n const [background, setBackground] = useState(\"--copilot-kit-background-color\");\n\n useAgentContext({\n description: 'Name of the user',\n value: 'Bob'\n });\n\n useFrontendTool({\n name: \"change_background\",\n description:\n \"Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.\",\n parameters: z.object({\n background: z.string().describe(\"The background. Prefer gradients. Only use when asked.\"),\n }) ,\n handler: async ({ background }: { background: string }) => {\n setBackground(background);\n return {\n status: \"success\",\n message: `Background changed to ${background}`,\n };\n },\n });\n\n useRenderTool({\n name: \"get_weather\",\n parameters: z.object({\n location: z.string(),\n }) ,\n render: ({ args, result, status }: any) => {\n if (status !== \"complete\") {\n return
Loading weather...
;\n }\n\n // Some integrations (e.g. LangGraph) deliver tool results as a JSON-encoded\n // string in the ToolMessage content rather than a parsed object. Normalize\n // so property access works in either case; otherwise every field reads as\n // undefined and the card renders empty values.\n let parsed: any = result;\n if (typeof parsed === \"string\") {\n try {\n parsed = JSON.parse(parsed);\n } catch {\n parsed = {};\n }\n }\n parsed = parsed ?? {};\n\n return (\n
\n Weather in {parsed.city ?? args.location}\n
Temperature: {parsed.temperature}°C
\n
Humidity: {parsed.humidity}%
\n
Wind Speed: {parsed.windSpeed ?? parsed.wind_speed} mph
\n
Conditions: {parsed.conditions}
\n
\n );\n },\n });\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Change background\",\n message: \"Change the background to something new.\",\n },\n {\n title: \"Generate sonnet\",\n message: \"Write a short sonnet about AI.\",\n },\n ],\n available: \"always\",\n });\n\n return (\n \n
\n \n
\n \n );\n};\n\nexport default AgenticChat;\n", + "language": "typescript", + "type": "file" + }, + { + "name": "README.mdx", + "content": "# 🤖 Agentic Chat with Frontend Tools\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **agentic chat** capabilities with **frontend\ntool integration**:\n\n1. **Natural Conversation**: Chat with your Copilot in a familiar chat interface\n2. **Frontend Tool Execution**: The Copilot can directly interacts with your UI\n by calling frontend functions\n3. **Seamless Integration**: Tools defined in the frontend and automatically\n discovered and made available to the agent\n\n## How to Interact\n\nTry asking your Copilot to:\n\n- \"Can you change the background color to something more vibrant?\"\n- \"Make the background a blue to purple gradient\"\n- \"Set the background to a sunset-themed gradient\"\n- \"Change it back to a simple light color\"\n\nYou can also chat about other topics - the agent will respond conversationally\nwhile having the ability to use your UI tools when appropriate.\n\n## ✨ Frontend Tool Integration in Action\n\n**What's happening technically:**\n\n- The React component defines a frontend function using `useCopilotAction`\n- CopilotKit automatically exposes this function to the agent\n- When you make a request, the agent determines whether to use the tool\n- The agent calls the function with the appropriate parameters\n- The UI immediately updates in response\n\n**What you'll see in this demo:**\n\n- The Copilot understands requests to change the background\n- It generates CSS values for colors and gradients\n- When it calls the tool, the background changes instantly\n- The agent provides a conversational response about the changes it made\n\nThis technique of exposing frontend functions to your Copilot can be extended to\nany UI manipulation you want to enable, from theme changes to data filtering,\nnavigation, or complex UI state management!\n", + "language": "markdown", + "type": "file" + }, + { + "name": "agentic-chat.ts", + "content": "/**\n * Agentic Chat example for AWS Strands (TypeScript).\n *\n * Simple conversational agent. Frontend tools sent in RunAgentInput.tools\n * are automatically registered as proxy tools so no server-side @tool\n * definition is needed — the LLM calls them and the browser executes them.\n */\n\nimport { Agent } from \"@strands-agents/sdk\";\nimport { StrandsAgent } from \"@ag-ui/aws-strands\";\nimport { createStrandsApp } from \"@ag-ui/aws-strands/server\";\nimport { createModel } from \"../model-factory\";\n\nasync function main(): Promise {\n const strandsAgent = new Agent({\n model: await createModel(),\n systemPrompt: `\n You are a helpful assistant.\n When the user greets you, always greet them back. Your greeting should always start with \"Hello\".\n Your greeting should also always ask (exact wording) \"how can I assist you?\"\n `,\n });\n\n const aguiAgent = new StrandsAgent({\n agent: strandsAgent,\n name: \"agentic_chat\",\n description: \"Conversational Strands agent with AG-UI streaming\",\n });\n\n const app = await createStrandsApp(aguiAgent, { path: \"/\" });\n const port = Number(process.env.PORT ?? 8000);\n app.listen(port, () => {\n // eslint-disable-next-line no-console\n console.log(`Listening on http://localhost:${port}`);\n });\n}\n\nvoid main();\n", + "language": "ts", + "type": "file" + } + ], + "aws-strands-typescript::agentic_chat_reasoning": [ + { + "name": "page.tsx", + "content": "\"use client\";\nimport React, { useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport {\n useAgent,\n UseAgentUpdate,\n useFrontendTool,\n useConfigureSuggestions,\n CopilotChat,\n} from \"@copilotkit/react-core/v2\";\nimport { z } from \"zod\";\nimport { ChevronDown } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface AgenticChatProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst AgenticChat: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\ninterface AgentState {\n model: string;\n}\n\nconst Chat = () => {\n const [background, setBackground] = useState(\"--copilot-kit-background-color\");\n const { agent } = useAgent({\n agentId: \"agentic_chat_reasoning\",\n updates: [UseAgentUpdate.OnStateChanged],\n });\n\n const agentState = agent.state as AgentState | undefined;\n\n // Initialize model if not set\n const selectedModel = agentState?.model || \"OpenAI\";\n\n const handleModelChange = (model: string) => {\n agent.setState({ model });\n };\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Change background\",\n message: \"Change the background to something new.\",\n },\n {\n title: \"Generate sonnet\",\n message: \"Write a short sonnet about AI.\",\n },\n ],\n available: \"always\",\n });\n\n useFrontendTool({\n agentId: \"agentic_chat_reasoning\",\n name: \"change_background\",\n description:\n \"Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.\",\n parameters: z.object({\n background: z.string().describe(\"The background. Prefer gradients.\"),\n }) ,\n handler: async ({ background }: { background: string }) => {\n setBackground(background);\n },\n });\n\n return (\n
\n {/* Reasoning Model Dropdown */}\n
\n
\n
\n \n Reasoning Model:\n \n \n \n \n \n \n Select Model\n \n handleModelChange(\"OpenAI\")}>\n OpenAI\n \n handleModelChange(\"Anthropic\")}>\n Anthropic\n \n handleModelChange(\"Gemini\")}>\n Gemini\n \n \n \n
\n
\n
\n\n {/* Chat Container */}\n
\n
\n \n
\n
\n
\n );\n};\n\nexport default AgenticChat;\n", + "language": "typescript", + "type": "file" + }, + { + "name": "style.css", + "content": ".copilotKitInput {\n border-bottom-left-radius: 0.75rem;\n border-bottom-right-radius: 0.75rem;\n border-top-left-radius: 0.75rem;\n border-top-right-radius: 0.75rem;\n border: 1px solid var(--copilot-kit-separator-color) !important;\n}\n \n.copilotKitChat {\n background-color: #fff !important;\n}\n ", + "language": "css", + "type": "file" + }, + { + "name": "README.mdx", + "content": "# 🤖 Agentic Chat with Reasoning\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **agentic chat** capabilities with **frontend\ntool integration**:\n\n1. **Natural Conversation**: Chat with your Copilot in a familiar chat interface\n2. **Frontend Tool Execution**: The Copilot can directly interacts with your UI\n by calling frontend functions\n3. **Seamless Integration**: Tools defined in the frontend and automatically\n discovered and made available to the agent\n\n## How to Interact\n\nTry asking your Copilot to:\n\n- \"Can you change the background color to something more vibrant?\"\n- \"Make the background a blue to purple gradient\"\n- \"Set the background to a sunset-themed gradient\"\n- \"Change it back to a simple light color\"\n\nYou can also chat about other topics - the agent will respond conversationally\nwhile having the ability to use your UI tools when appropriate.\n\n## ✨ Frontend Tool Integration in Action\n\n**What's happening technically:**\n\n- The React component defines a frontend function using `useCopilotAction`\n- CopilotKit automatically exposes this function to the agent\n- When you make a request, the agent determines whether to use the tool\n- The agent calls the function with the appropriate parameters\n- The UI immediately updates in response\n\n**What you'll see in this demo:**\n\n- The Copilot understands requests to change the background\n- It generates CSS values for colors and gradients\n- When it calls the tool, the background changes instantly\n- The agent provides a conversational response about the changes it made\n\nThis technique of exposing frontend functions to your Copilot can be extended to\nany UI manipulation you want to enable, from theme changes to data filtering,\nnavigation, or complex UI state management!\n", + "language": "markdown", + "type": "file" + }, + { + "name": "agentic-chat-reasoning.ts", + "content": "/**\n * Agentic Chat with Reasoning example for AWS Strands (TypeScript).\n *\n * Demonstrates reasoning/thinking event streaming. When the underlying model\n * supports extended thinking, the adapter emits REASONING_* events that the\n * frontend can display as a \"thinking\" indicator.\n */\n\nimport { Agent } from \"@strands-agents/sdk\";\nimport { StrandsAgent } from \"@ag-ui/aws-strands\";\nimport { createStrandsApp } from \"@ag-ui/aws-strands/server\";\nimport { createModel } from \"../model-factory\";\n\nasync function main(): Promise {\n const strandsAgent = new Agent({\n model: await createModel(),\n systemPrompt: `\n You are a helpful assistant that thinks through problems step by step.\n When the user greets you, always greet them back. Your greeting should always start with \"Hello\".\n Your greeting should also always ask (exact wording) \"how can I assist you?\"\n When reasoning about a problem, break it down into clear steps before answering.\n `,\n });\n\n const aguiAgent = new StrandsAgent({\n agent: strandsAgent,\n name: \"agentic_chat_reasoning\",\n description:\n \"Conversational Strands agent with reasoning/thinking event streaming\",\n });\n\n const app = await createStrandsApp(aguiAgent, { path: \"/\" });\n const port = Number(process.env.PORT ?? 8000);\n app.listen(port, () => {\n console.log(`Listening on http://localhost:${port}`);\n });\n}\n\nvoid main();\n", + "language": "ts", + "type": "file" + } + ], + "aws-strands-typescript::agentic_chat_multimodal": [ + { + "name": "page.tsx", + "content": "\"use client\";\nimport React, { useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport {\n useFrontendTool,\n useConfigureSuggestions,\n CopilotChat,\n} from \"@copilotkit/react-core/v2\";\nimport { z } from \"zod\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface AgenticChatMultimodalProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst AgenticChatMultimodal: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n const [background, setBackground] = useState(\"--copilot-kit-background-color\");\n\n useFrontendTool({\n name: \"change_background\",\n description:\n \"Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear or radial gradients etc.\",\n parameters: z.object({\n background: z.string().describe(\"The background. Prefer gradients. Only use when asked.\"),\n }),\n handler: async ({ background }: { background: string }) => {\n setBackground(background);\n return {\n status: \"success\",\n message: `Background changed to ${background}`,\n };\n },\n });\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Upload an image\",\n message: \"Describe what you see in the image I upload.\",\n },\n {\n title: \"Analyze a photo\",\n message: \"What objects can you identify in this photo?\",\n },\n ],\n available: \"always\",\n });\n\n return (\n \n
\n \n
\n \n );\n};\n\nexport default AgenticChatMultimodal;\n", + "language": "typescript", + "type": "file" + }, + { + "name": "README.mdx", + "content": "# Agentic Chat Multimodal\n\nThis example demonstrates multimodal input support in AG-UI. Users can upload images and other media alongside text messages, and the agent analyzes them.\n\n## How it works\n\n- The `CopilotChat` component is configured with `attachments={{ enabled: true }}` to allow file uploads\n- Uploaded images are sent as `ImageInputContent` with base64-encoded data through the AG-UI protocol\n- The backend agent uses a vision-capable model (GPT-5.2) to analyze the uploaded content\n- The AG-UI integration layer automatically converts between AG-UI's multimodal content types and the framework's native format\n\n## Try it\n\n1. Click the attachment icon in the chat input\n2. Upload an image\n3. Ask the agent to describe or analyze the image\n", + "language": "markdown", + "type": "file" + }, + { + "name": "agentic-chat-multimodal.ts", + "content": "/**\n * Agentic Chat with Multimodal support for AWS Strands (TypeScript).\n *\n * Demonstrates multimodal message handling. When the user uploads an image,\n * the adapter converts AG-UI InputContent to Strands ContentBlock format\n * and passes it to the vision-capable model.\n */\n\nimport { Agent } from \"@strands-agents/sdk\";\nimport { StrandsAgent } from \"@ag-ui/aws-strands\";\nimport { createStrandsApp } from \"@ag-ui/aws-strands/server\";\nimport { createModel } from \"../model-factory\";\n\nasync function main(): Promise {\n const strandsAgent = new Agent({\n model: await createModel(),\n systemPrompt: `\n You are a helpful assistant that can analyze images and documents.\n When the user shares an image, describe what you see in detail.\n When the user shares a document, summarize its contents.\n Always be descriptive and specific about visual content.\n `,\n });\n\n const aguiAgent = new StrandsAgent({\n agent: strandsAgent,\n name: \"agentic_chat_multimodal\",\n description: \"Conversational Strands agent with multimodal content support\",\n });\n\n const app = await createStrandsApp(aguiAgent, { path: \"/\" });\n const port = Number(process.env.PORT ?? 8000);\n app.listen(port, () => {\n console.log(`Listening on http://localhost:${port}`);\n });\n}\n\nvoid main();\n", + "language": "ts", + "type": "file" + } + ], + "aws-strands-typescript::v1_agentic_chat": [ + { + "name": "page.tsx", + "content": "\"use client\";\nimport React from \"react\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\nimport { CopilotChat } from \"@copilotkit/react-ui\";\nimport \"@copilotkit/react-ui/styles.css\";\n\ninterface V1AgenticChatProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst V1AgenticChat: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n
\n
\n \n
\n
\n \n );\n};\n\nexport default V1AgenticChat;\n", + "language": "typescript", + "type": "file" + }, + { + "name": "README.mdx", + "content": "# 🤖 V1 Agentic Chat\n\n## What This Demo Shows\n\nThis demo verifies **CopilotKit v1 API compatibility**. It uses the original v1\ncomponents (`CopilotKit` provider and `CopilotChat`) to ensure that v1 APIs\ncontinue to work correctly against the current runtime.\n\n1. **V1 Provider**: Uses `CopilotKit` from `@copilotkit/react-core` with the\n `agent` prop for agent selection\n2. **V1 Chat UI**: Uses `CopilotChat` from `@copilotkit/react-ui` with v1\n styling\n3. **Same Backend**: Connects to the same runtime endpoint as v2, validating\n backward compatibility\n\n## How to Interact\n\nThis is a standard chat interface — type a message and the agent will respond\nconversationally, just like the v2 agentic chat demo.\n\n## ✨ V1 Compatibility\n\n**What's happening technically:**\n\n- The v1 `CopilotKit` provider connects to the same `/api/copilotkit/[integration]` endpoint\n- The v1 chat UI renders with v1 CSS classes (`.copilotKitInput`, `.copilotKitAssistantMessage`, etc.)\n- The agent selected via the `agent` prop maps to the same `agentic_chat` backend agent\n- This ensures that applications built with v1 APIs continue to function after runtime upgrades\n", + "language": "markdown", + "type": "file" + } + ], + "aws-strands-typescript::backend_tool_rendering": [ + { + "name": "page.tsx", + "content": "\"use client\";\nimport React from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { \n useRenderTool,\n useConfigureSuggestions,\n CopilotChat,\n} from \"@copilotkit/react-core/v2\";\nimport { z } from \"zod\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface AgenticChatProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst AgenticChat: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\nconst Chat = () => {\n useRenderTool({\n \n name: \"get_weather\",\n parameters: z.object({\n location: z.string(),\n }) ,\n render: ({ args, result, status }: any) => {\n if (status !== \"complete\") {\n return (\n
\n ⚙️ Retrieving weather...\n
\n );\n }\n\n // Some integrations (e.g. LangGraph) deliver tool results as a JSON-encoded\n // string in the ToolMessage content rather than a parsed object. Normalize\n // so property access works in either case; otherwise every field falls\n // through to its `|| 0` default and the card shows 0° C.\n let parsed: any = result;\n if (typeof parsed === \"string\") {\n try {\n parsed = JSON.parse(parsed);\n } catch {\n parsed = {};\n }\n }\n parsed = parsed ?? {};\n\n const weatherResult: WeatherToolResult = {\n temperature: parsed.temperature ?? 0,\n conditions: parsed.conditions ?? \"clear\",\n humidity: parsed.humidity ?? 0,\n windSpeed: parsed.wind_speed ?? parsed.windSpeed ?? 0,\n feelsLike:\n parsed.feels_like ?? parsed.feelsLike ?? parsed.temperature ?? 0,\n };\n\n const themeColor = getThemeColor(weatherResult.conditions);\n\n return (\n \n );\n },\n });\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Weather in San Francisco\",\n message: \"What's the weather like in San Francisco?\",\n },\n {\n title: \"Weather in New York\",\n message: \"Tell me about the weather in New York.\",\n },\n {\n title: \"Weather in Tokyo\",\n message: \"How's the weather in Tokyo today?\",\n },\n ],\n available: \"always\",\n });\n\n return (\n
\n
\n \n
\n
\n );\n};\n\ninterface WeatherToolResult {\n temperature: number;\n conditions: string;\n humidity: number;\n windSpeed: number;\n feelsLike: number;\n}\n\nfunction getThemeColor(conditions: string): string {\n const conditionLower = conditions.toLowerCase();\n if (conditionLower.includes(\"clear\") || conditionLower.includes(\"sunny\")) {\n return \"#667eea\";\n }\n if (conditionLower.includes(\"rain\") || conditionLower.includes(\"storm\")) {\n return \"#4A5568\";\n }\n if (conditionLower.includes(\"cloud\")) {\n return \"#718096\";\n }\n if (conditionLower.includes(\"snow\")) {\n return \"#63B3ED\";\n }\n return \"#764ba2\";\n}\n\nfunction WeatherCard({\n location,\n themeColor,\n result,\n status,\n}: {\n location?: string;\n themeColor: string;\n result: WeatherToolResult;\n status: \"inProgress\" | \"executing\" | \"complete\";\n}) {\n return (\n \n
\n
\n
\n

\n {location}\n

\n

Current Weather

\n
\n \n
\n\n
\n
\n {result.temperature}° C\n \n {\" / \"}\n {((result.temperature * 9) / 5 + 32).toFixed(1)}° F\n \n
\n
{result.conditions}
\n
\n\n
\n
\n
\n

Humidity

\n

{result.humidity}%

\n
\n
\n

Wind

\n

{result.windSpeed} mph

\n
\n
\n

Feels Like

\n

{result.feelsLike}°

\n
\n
\n
\n
\n \n );\n}\n\nfunction WeatherIcon({ conditions }: { conditions: string }) {\n if (!conditions) return null;\n\n if (conditions.toLowerCase().includes(\"clear\") || conditions.toLowerCase().includes(\"sunny\")) {\n return ;\n }\n\n if (\n conditions.toLowerCase().includes(\"rain\") ||\n conditions.toLowerCase().includes(\"drizzle\") ||\n conditions.toLowerCase().includes(\"snow\") ||\n conditions.toLowerCase().includes(\"thunderstorm\")\n ) {\n return ;\n }\n\n if (\n conditions.toLowerCase().includes(\"fog\") ||\n conditions.toLowerCase().includes(\"cloud\") ||\n conditions.toLowerCase().includes(\"overcast\")\n ) {\n return ;\n }\n\n return ;\n}\n\n// Simple sun icon for the weather card\nfunction SunIcon() {\n return (\n \n \n \n \n );\n}\n\nfunction RainIcon() {\n return (\n \n {/* Cloud */}\n \n {/* Rain drops */}\n \n \n );\n}\n\nfunction CloudIcon() {\n return (\n \n \n \n );\n}\n\nexport default AgenticChat;\n", + "language": "typescript", + "type": "file" + }, + { + "name": "style.css", + "content": ".copilotKitInput {\n border-bottom-left-radius: 0.75rem;\n border-bottom-right-radius: 0.75rem;\n border-top-left-radius: 0.75rem;\n border-top-right-radius: 0.75rem;\n border: 1px solid var(--copilot-kit-separator-color) !important;\n}\n\n.copilotKitChat {\n background-color: #fff !important;\n}\n", + "language": "css", + "type": "file" + }, + { + "name": "README.mdx", + "content": "# 🤖 Agentic Chat with Frontend Tools\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **agentic chat** capabilities with **frontend\ntool integration**:\n\n1. **Natural Conversation**: Chat with your Copilot in a familiar chat interface\n2. **Frontend Tool Execution**: The Copilot can directly interacts with your UI\n by calling frontend functions\n3. **Seamless Integration**: Tools defined in the frontend and automatically\n discovered and made available to the agent\n\n## How to Interact\n\nTry asking your Copilot to:\n\n- \"Can you change the background color to something more vibrant?\"\n- \"Make the background a blue to purple gradient\"\n- \"Set the background to a sunset-themed gradient\"\n- \"Change it back to a simple light color\"\n\nYou can also chat about other topics - the agent will respond conversationally\nwhile having the ability to use your UI tools when appropriate.\n\n## ✨ Frontend Tool Integration in Action\n\n**What's happening technically:**\n\n- The React component defines a frontend function using `useCopilotAction`\n- CopilotKit automatically exposes this function to the agent\n- When you make a request, the agent determines whether to use the tool\n- The agent calls the function with the appropriate parameters\n- The UI immediately updates in response\n\n**What you'll see in this demo:**\n\n- The Copilot understands requests to change the background\n- It generates CSS values for colors and gradients\n- When it calls the tool, the background changes instantly\n- The agent provides a conversational response about the changes it made\n\nThis technique of exposing frontend functions to your Copilot can be extended to\nany UI manipulation you want to enable, from theme changes to data filtering,\nnavigation, or complex UI state management!\n", + "language": "markdown", + "type": "file" + }, + { + "name": "backend-tool-rendering.ts", + "content": "/**\n * Backend Tool Rendering example for AWS Strands (TypeScript).\n *\n * Demonstrates backend-executed tools. Tool results flow through the\n * adapter as `TOOL_CALL_RESULT` events that the frontend can render\n * directly (e.g. charts, weather cards) without extra plumbing.\n */\n\nimport { Agent, tool } from \"@strands-agents/sdk\";\nimport { z } from \"zod\";\nimport { StrandsAgent } from \"@ag-ui/aws-strands\";\nimport { createStrandsApp } from \"@ag-ui/aws-strands/server\";\nimport { createModel } from \"../model-factory\";\n\nconst getWeather = tool({\n name: \"get_weather\",\n description: \"Gets the current weather for a given city.\",\n inputSchema: z.object({\n city: z.string().describe(\"The city to fetch weather for.\"),\n }),\n callback({ city }) {\n return {\n city,\n temperatureC: 21,\n conditions: \"Sunny\",\n };\n },\n});\n\nconst renderChart = tool({\n name: \"render_chart\",\n description: \"Renders a chart for the given data series.\",\n inputSchema: z.object({\n title: z.string(),\n points: z.array(z.object({ x: z.number(), y: z.number() })),\n }),\n callback(input) {\n return { rendered: true, ...input };\n },\n});\n\nasync function main(): Promise {\n const strandsAgent = new Agent({\n model: await createModel(),\n systemPrompt:\n \"You are a helpful assistant. Use the tools to answer user questions, then narrate the result.\",\n tools: [getWeather, renderChart],\n });\n\n const aguiAgent = new StrandsAgent({\n agent: strandsAgent,\n name: \"backend_tool_rendering\",\n description:\n \"Strands agent that invokes backend tools and renders the results in the UI\",\n });\n\n const app = await createStrandsApp(aguiAgent, { path: \"/\" });\n app.listen(Number(process.env.PORT ?? 8000));\n}\n\nvoid main();\n", + "language": "ts", + "type": "file" + } + ], + "aws-strands-typescript::agentic_generative_ui": [ + { + "name": "page.tsx", + "content": "\"use client\";\nimport React from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { \n useAgent,\n UseAgentUpdate,\n useConfigureSuggestions,\n CopilotChat,\n} from \"@copilotkit/react-core/v2\";\nimport { useTheme } from \"next-themes\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface AgenticGenerativeUIProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst AgenticGenerativeUI: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n return (\n \n \n \n );\n};\n\ninterface AgentState {\n steps: {\n description: string;\n status: \"pending\" | \"completed\";\n }[];\n}\n\nconst Chat = () => {\n const { theme } = useTheme();\n const { agent } = useAgent({\n agentId: \"agentic_generative_ui\",\n updates: [UseAgentUpdate.OnStateChanged],\n });\n\n const agentState = agent.state as AgentState | undefined;\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Simple plan\",\n message: \"Please build a plan to go to mars in 5 steps.\",\n },\n {\n title: \"Complex plan\",\n message: \"Please build a plan to go to make pizza in 10 steps.\",\n },\n ],\n available: \"always\",\n });\n\n const steps = agentState?.steps;\n\n return (\n
\n
\n (\n
\n {messageElements}\n {steps && steps.length > 0 && (\n
\n \n
\n )}\n {interruptElement}\n
\n ),\n }}\n />\n
\n
\n );\n};\n\nfunction TaskProgress({ steps, theme }: { steps: AgentState[\"steps\"]; theme?: string }) {\n const completedCount = steps.filter((step) => step.status === \"completed\").length;\n const progressPercentage = (completedCount / steps.length) * 100;\n\n return (\n
\n \n {/* Header */}\n
\n
\n

\n Task Progress\n

\n
\n {completedCount}/{steps.length} Complete\n
\n
\n\n {/* Progress Bar */}\n \n \n \n
\n
\n\n {/* Steps */}\n
\n {steps.map((step, index) => {\n const isCompleted = step.status === \"completed\";\n const isCurrentPending =\n step.status === \"pending\" &&\n index === steps.findIndex((s) => s.status === \"pending\");\n const isFuturePending = step.status === \"pending\" && !isCurrentPending;\n\n return (\n \n {/* Connector Line */}\n {index < steps.length - 1 && (\n \n )}\n\n {/* Status Icon */}\n \n {isCompleted ? (\n \n ) : isCurrentPending ? (\n \n ) : (\n \n )}\n
\n\n {/* Step Content */}\n
\n \n {step.description}\n
\n {isCurrentPending && (\n \n Processing...\n \n )}\n \n\n {/* Animated Background for Current Step */}\n {isCurrentPending && (\n \n )}\n \n );\n })}\n \n\n {/* Decorative Elements */}\n \n \n \n \n );\n}\n\n// Enhanced Icons\nfunction CheckIcon() {\n return (\n \n \n \n );\n}\n\nfunction SpinnerIcon() {\n return (\n \n \n \n \n );\n}\n\nfunction ClockIcon({ theme }: { theme?: string }) {\n return (\n \n \n \n \n );\n}\n\nexport default AgenticGenerativeUI;\n", + "language": "typescript", + "type": "file" + }, + { + "name": "style.css", + "content": ".copilotKitInput {\n border-bottom-left-radius: 0.75rem;\n border-bottom-right-radius: 0.75rem;\n border-top-left-radius: 0.75rem;\n border-top-right-radius: 0.75rem;\n border: 1px solid var(--copilot-kit-separator-color) !important;\n}\n\n.copilotKitChat {\n background-color: #fff !important;\n}\n", + "language": "css", + "type": "file" + }, + { + "name": "README.mdx", + "content": "# 🚀 Agentic Generative UI Task Executor\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **agentic generative UI** capabilities:\n\n1. **Real-time Status Updates**: The Copilot provides live feedback as it works\n through complex tasks\n2. **Long-running Task Execution**: See how agents can handle extended processes\n with continuous feedback\n3. **Dynamic UI Generation**: The interface updates in real-time to reflect the\n agent's progress\n\n## How to Interact\n\nSimply ask your Copilot to perform any moderately complex task:\n\n- \"Make me a sandwich\"\n- \"Plan a vacation to Japan\"\n- \"Create a weekly workout routine\"\n\nThe Copilot will break down the task into steps and begin \"executing\" them,\nproviding real-time status updates as it progresses.\n\n## ✨ Agentic Generative UI in Action\n\n**What's happening technically:**\n\n- The agent analyzes your request and creates a detailed execution plan\n- Each step is processed sequentially with realistic timing\n- Status updates are streamed to the frontend using CopilotKit's streaming\n capabilities\n- The UI dynamically renders these updates without page refreshes\n- The entire flow is managed by the agent, requiring no manual intervention\n\n**What you'll see in this demo:**\n\n- The Copilot breaks your task into logical steps\n- A status indicator shows the current progress\n- Each step is highlighted as it's being executed\n- Detailed status messages explain what's happening at each moment\n- Upon completion, you receive a summary of the task execution\n\nThis pattern of providing real-time progress for long-running tasks is perfect\nfor scenarios where users benefit from transparency into complex processes -\nfrom data analysis to content creation, system configurations, or multi-stage\nworkflows!\n", + "language": "markdown", + "type": "file" + }, + { + "name": "agentic-generative-ui.ts", + "content": "/**\n * Agentic Generative UI example for AWS Strands (TypeScript).\n *\n * Demonstrates streaming agent state updates to the frontend for real-time\n * UI rendering. Uses ONLY the canonical Strands + @ag-ui/aws-strands surface:\n *\n * - `predictState` mapping streams the predicted `steps` to the FE while\n * the LLM is still emitting `plan_task_steps` arguments.\n * - The tool itself is an async generator. Each `yield` of `{ state: {...} }`\n * becomes a Strands `ToolStreamEvent` which the @ag-ui/aws-strands adapter\n * translates into an AG-UI `StateSnapshotEvent`.\n * - The FINAL value returned by the generator is the tool's result.\n *\n * The agent never emits AG-UI events directly. State updates flow through\n * Strands' native streaming mechanism, mirroring the Python reference\n * (integrations/aws-strands/python/examples/server/api/agentic_generative_ui.py).\n */\n\nimport { Agent, tool } from \"@strands-agents/sdk\";\nimport { z } from \"zod\";\nimport { StrandsAgent, type StrandsAgentConfig } from \"@ag-ui/aws-strands\";\nimport { createStrandsApp } from \"@ag-ui/aws-strands/server\";\nimport { createModel } from \"../model-factory\";\n\nconst stepSchema = z.object({\n description: z\n .string()\n .describe(\"Gerund phrase describing the action, e.g. 'Sketching layout'\"),\n status: z\n .string()\n .default(\"pending\")\n .describe(\"Must be 'pending' when proposed\"),\n});\n\nfunction sleep(ms: number): Promise {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * `plan_task_steps` as an async-generator tool. Each yielded `{ state: {...} }`\n * becomes a Strands `ToolStreamEvent` that the adapter translates into an\n * AG-UI `StateSnapshotEvent`. The final return value is the tool result.\n */\nconst planTaskSteps = tool({\n name: \"plan_task_steps\",\n description:\n \"Plan the concrete steps required to accomplish a task and walk each step from 'pending' through 'in_progress' to 'completed' so the UI sees progress in real time.\",\n inputSchema: z.object({\n task: z\n .string()\n .describe(\"Brief description of what the user wants to achieve\"),\n context: z\n .string()\n .default(\"\")\n .describe(\"Optional additional instructions\"),\n steps: z\n .array(stepSchema)\n .describe(\"Ordered list of pending steps in gerund form\"),\n }),\n callback: async function* ({ task, context, steps }) {\n const normalized = (steps ?? []).map(\n (s: { description: string; status?: string }) => ({\n description: s.description,\n status: s.status || \"pending\",\n }),\n );\n const workingSteps =\n normalized.length > 0\n ? normalized\n : fallbackSteps(task || \"the task\", context);\n const mutable = workingSteps.map((s) => ({ ...s }));\n\n // Re-confirm the canonical shape now that the tool body owns the state\n // (predictState will already have streamed something similar from args).\n yield { state: { steps: mutable.map((s) => ({ ...s })) } };\n\n for (let i = 0; i < mutable.length; i++) {\n await sleep(300 + Math.random() * 500);\n mutable[i]!.status = \"in_progress\";\n yield { state: { steps: mutable.map((s) => ({ ...s })) } };\n\n await sleep(400 + Math.random() * 600);\n mutable[i]!.status = \"completed\";\n yield { state: { steps: mutable.map((s) => ({ ...s })) } };\n }\n\n return { task, context, steps: mutable };\n },\n});\n\nfunction fallbackSteps(\n task: string,\n context: string,\n): { description: string; status: string }[] {\n let count = 6;\n for (const token of (context ?? \"\").split(/\\s+/)) {\n if (/^\\d+$/.test(token)) {\n count = Math.max(4, Math.min(10, parseInt(token, 10)));\n break;\n }\n }\n const templates = [\n \"Clarifying goals for {task}\",\n \"Gathering resources for {task}\",\n \"Preparing workspace for {task}\",\n \"Executing core work on {task}\",\n \"Reviewing results for {task}\",\n \"Wrapping up {task}\",\n \"Documenting learnings from {task}\",\n \"Celebrating completion of {task}\",\n ];\n const plan: { description: string; status: string }[] = [];\n for (let i = 0; i < count; i++) {\n const raw = templates[i % templates.length]!.replace(\"{task}\", task).trim();\n const description = raw.charAt(0).toUpperCase() + raw.slice(1);\n plan.push({ description, status: \"pending\" });\n }\n return plan;\n}\n\nasync function main() {\n const config: StrandsAgentConfig = {\n stateContextBuilder: (input, prompt) => {\n const state = (input.state ?? {}) as Record;\n const steps = state.steps;\n if (steps) {\n return (\n \"A plan is already in progress. NEVER call plan_task_steps again unless the user explicitly \" +\n \"asks to restart. Discuss progress or ask clarifying questions instead.\\n\\n\" +\n `Current steps:\\n${JSON.stringify(steps, null, 2)}\\n\\nUser: ${prompt}`\n );\n }\n return prompt;\n },\n toolBehaviors: {\n plan_task_steps: {\n predictState: [\n { stateKey: \"steps\", tool: \"plan_task_steps\", toolArgument: \"steps\" },\n ],\n stateFromResult: async (ctx) => {\n const result = (ctx.resultData ?? {}) as { steps?: unknown[] };\n return result.steps ? { steps: result.steps } : null;\n },\n },\n },\n };\n\n const strandsAgent = new Agent({\n model: await createModel(),\n tools: [planTaskSteps],\n systemPrompt: `You are an energetic project assistant who decomposes user goals into action plans.\n\nPlanning rules:\n1. When the user asks for help with a task or making a plan, call plan_task_steps exactly once.\n2. Do NOT call plan_task_steps again unless the user explicitly says to restart.\n3. Generate 4-6 concise steps in gerund form (e.g., \"Setting up repo\", \"Testing prototype\") with status \"pending\".\n4. After the tool call, send a short confirmation (<= 2 sentences) plus one emoji.\n5. If the user is just chatting, respond conversationally without calling the tool.\n6. If a plan already exists, reference the current steps instead of creating a new plan.\n`,\n });\n\n const aguiAgent = new StrandsAgent({\n agent: strandsAgent,\n name: \"agentic_generative_ui\",\n description: \"AWS Strands agent with generative UI and state streaming\",\n config,\n });\n\n const app = await createStrandsApp(aguiAgent, { path: \"/\" });\n const port = Number(process.env.PORT ?? 8000);\n app.listen(port, () => {\n console.log(`Listening on http://localhost:${port}`);\n });\n}\n\nvoid main();\n", + "language": "ts", + "type": "file" + } + ], + "aws-strands-typescript::shared_state": [ + { + "name": "page.tsx", + "content": "\"use client\";\nimport {\n useAgent,\n UseAgentUpdate,\n useCopilotKit,\n useConfigureSuggestions,\n CopilotChat,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport React, { useState, useEffect, useRef } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport \"./style.css\";\nimport { useMobileView } from \"@/utils/use-mobile-view\";\nimport { useMobileChat } from \"@/utils/use-mobile-chat\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface SharedStateProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nexport default function SharedState({ params }: SharedStateProps) {\n const { integrationId } = React.use(params);\n const { isMobile } = useMobileView();\n const { chatDefaultOpen } = useURLParams();\n const defaultChatHeight = 50;\n const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } =\n useMobileChat(defaultChatHeight);\n\n const chatTitle = \"AI Recipe Assistant\";\n const chatDescription = \"Ask me to craft recipes\";\n\n return (\n \n
\n \n {isMobile ? (\n <>\n {/* Chat Toggle Button */}\n
\n
\n {\n if (!isChatOpen) {\n setChatHeight(defaultChatHeight); // Reset to good default when opening\n }\n setIsChatOpen(!isChatOpen);\n }}\n >\n
\n
\n
{chatTitle}
\n
{chatDescription}
\n
\n
\n \n \n \n \n
\n
\n \n\n {/* Pull-Up Chat Container */}\n \n {/* Drag Handle Bar */}\n \n
\n \n\n {/* Chat Header */}\n
\n
\n
\n

{chatTitle}

\n
\n setIsChatOpen(false)}\n className=\"p-2 hover:bg-gray-100 rounded-full transition-colors\"\n >\n \n \n \n \n
\n
\n\n {/* Chat Content - Flexible container for messages and input */}\n
\n \n
\n \n\n {/* Backdrop */}\n {isChatOpen && (\n
setIsChatOpen(false)} />\n )}\n \n ) : (\n \n )}\n
\n \n );\n}\n\nenum SkillLevel {\n BEGINNER = \"Beginner\",\n INTERMEDIATE = \"Intermediate\",\n ADVANCED = \"Advanced\",\n}\n\nenum CookingTime {\n FiveMin = \"5 min\",\n FifteenMin = \"15 min\",\n ThirtyMin = \"30 min\",\n FortyFiveMin = \"45 min\",\n SixtyPlusMin = \"60+ min\",\n}\n\nconst cookingTimeValues = [\n { label: CookingTime.FiveMin, value: 0 },\n { label: CookingTime.FifteenMin, value: 1 },\n { label: CookingTime.ThirtyMin, value: 2 },\n { label: CookingTime.FortyFiveMin, value: 3 },\n { label: CookingTime.SixtyPlusMin, value: 4 },\n];\n\nenum SpecialPreferences {\n HighProtein = \"High Protein\",\n LowCarb = \"Low Carb\",\n Spicy = \"Spicy\",\n BudgetFriendly = \"Budget-Friendly\",\n OnePotMeal = \"One-Pot Meal\",\n Vegetarian = \"Vegetarian\",\n Vegan = \"Vegan\",\n}\n\ninterface Ingredient {\n icon: string;\n name: string;\n amount: string;\n}\n\ninterface Recipe {\n title: string;\n skill_level: SkillLevel;\n cooking_time: CookingTime;\n special_preferences: string[];\n ingredients: Ingredient[];\n instructions: string[];\n}\n\ninterface RecipeAgentState {\n recipe: Recipe;\n}\n\nconst INITIAL_STATE: RecipeAgentState = {\n recipe: {\n title: \"Make Your Recipe\",\n skill_level: SkillLevel.INTERMEDIATE,\n cooking_time: CookingTime.FortyFiveMin,\n special_preferences: [],\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n },\n};\n\nfunction Recipe() {\n const { isMobile } = useMobileView();\n const { agent } = useAgent({\n agentId: \"shared_state\",\n updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged],\n });\n const { copilotkit } = useCopilotKit();\n\n useConfigureSuggestions({\n suggestions: [\n {\n title: \"Create Italian recipe\",\n message: \"Create a delicious Italian pasta recipe.\",\n },\n {\n title: \"Make it healthier\",\n message: \"Make the recipe healthier with more vegetables.\",\n },\n {\n title: \"Suggest variations\",\n message: \"Suggest some creative variations of this recipe.\",\n },\n ],\n available: \"always\",\n });\n\n const agentState = agent.state as RecipeAgentState | undefined;\n const setAgentState = (s: RecipeAgentState) => agent.setState(s);\n const isLoading = agent.isRunning;\n\n // Set initial state on mount\n useEffect(() => {\n if (!agentState?.recipe) {\n setAgentState(INITIAL_STATE);\n }\n }, []);\n\n const [recipe, setRecipe] = useState(INITIAL_STATE.recipe);\n const [editingInstructionIndex, setEditingInstructionIndex] = useState(null);\n const newInstructionRef = useRef(null);\n\n const updateRecipe = (partialRecipe: Partial) => {\n setAgentState({\n ...(agentState || INITIAL_STATE),\n recipe: {\n ...recipe,\n ...partialRecipe,\n },\n });\n setRecipe({\n ...recipe,\n ...partialRecipe,\n });\n };\n\n const newRecipeState = { ...recipe };\n const newChangedKeys = [];\n const changedKeysRef = useRef([]);\n\n for (const key in recipe) {\n if (\n agentState &&\n agentState.recipe &&\n (agentState.recipe as any)[key] !== undefined &&\n (agentState.recipe as any)[key] !== null\n ) {\n let agentValue = (agentState.recipe as any)[key];\n const recipeValue = (recipe as any)[key];\n\n // Check if agentValue is a string and replace \\n with actual newlines\n if (typeof agentValue === \"string\") {\n agentValue = agentValue.replace(/\\\\n/g, \"\\n\");\n }\n\n if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) {\n (newRecipeState as any)[key] = agentValue;\n newChangedKeys.push(key);\n }\n }\n }\n\n if (newChangedKeys.length > 0) {\n changedKeysRef.current = newChangedKeys;\n } else if (!isLoading) {\n changedKeysRef.current = [];\n }\n\n useEffect(() => {\n setRecipe(newRecipeState);\n }, [JSON.stringify(newRecipeState)]);\n\n const handleTitleChange = (event: React.ChangeEvent) => {\n updateRecipe({\n title: event.target.value,\n });\n };\n\n const handleSkillLevelChange = (event: React.ChangeEvent) => {\n updateRecipe({\n skill_level: event.target.value as SkillLevel,\n });\n };\n\n const handleDietaryChange = (preference: string, checked: boolean) => {\n if (checked) {\n updateRecipe({\n special_preferences: [...recipe.special_preferences, preference],\n });\n } else {\n updateRecipe({\n special_preferences: recipe.special_preferences.filter((p) => p !== preference),\n });\n }\n };\n\n const handleCookingTimeChange = (event: React.ChangeEvent) => {\n updateRecipe({\n cooking_time: cookingTimeValues[Number(event.target.value)].label,\n });\n };\n\n const addIngredient = () => {\n // Pick a random food emoji from our valid list\n updateRecipe({\n ingredients: [...recipe.ingredients, { icon: \"🍴\", name: \"\", amount: \"\" }],\n });\n };\n\n const updateIngredient = (index: number, field: keyof Ingredient, value: string) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients[index] = {\n ...updatedIngredients[index],\n [field]: value,\n };\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const removeIngredient = (index: number) => {\n const updatedIngredients = [...recipe.ingredients];\n updatedIngredients.splice(index, 1);\n updateRecipe({ ingredients: updatedIngredients });\n };\n\n const addInstruction = () => {\n const newIndex = recipe.instructions.length;\n updateRecipe({\n instructions: [...recipe.instructions, \"\"],\n });\n // Set the new instruction as the editing one\n setEditingInstructionIndex(newIndex);\n\n // Focus the new instruction after render\n setTimeout(() => {\n const textareas = document.querySelectorAll(\".instructions-container textarea\");\n const newTextarea = textareas[textareas.length - 1] as HTMLTextAreaElement;\n if (newTextarea) {\n newTextarea.focus();\n }\n }, 50);\n };\n\n const updateInstruction = (index: number, value: string) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions[index] = value;\n updateRecipe({ instructions: updatedInstructions });\n };\n\n const removeInstruction = (index: number) => {\n const updatedInstructions = [...recipe.instructions];\n updatedInstructions.splice(index, 1);\n updateRecipe({ instructions: updatedInstructions });\n };\n\n // Simplified icon handler that defaults to a fork/knife for any problematic icons\n const getProperIcon = (icon: string | undefined): string => {\n // If icon is undefined return the default\n if (!icon) {\n return \"🍴\";\n }\n\n return icon;\n };\n\n return (\n \n {/* Recipe Title */}\n
\n \n\n
\n
\n 🕒\n t.label === recipe.cooking_time)?.value || 3}\n onChange={handleCookingTimeChange}\n style={{\n backgroundImage:\n \"url(\\\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23555' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\\\")\",\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"right 0px center\",\n backgroundSize: \"12px\",\n appearance: \"none\",\n WebkitAppearance: \"none\",\n }}\n >\n {cookingTimeValues.map((time) => (\n \n ))}\n \n
\n\n
\n 🏆\n \n {Object.values(SkillLevel).map((level) => (\n \n ))}\n \n
\n
\n
\n\n {/* Dietary Preferences */}\n
\n {changedKeysRef.current.includes(\"special_preferences\") && }\n

Dietary Preferences

\n
\n {Object.values(SpecialPreferences).map((option) => (\n \n ))}\n
\n
\n\n {/* Ingredients */}\n
\n {changedKeysRef.current.includes(\"ingredients\") && }\n
\n

Ingredients

\n \n + Add Ingredient\n \n
\n
\n {recipe.ingredients.map((ingredient, index) => (\n
\n
{getProperIcon(ingredient.icon)}
\n
\n updateIngredient(index, \"name\", e.target.value)}\n placeholder=\"Ingredient name\"\n className=\"ingredient-name-input\"\n />\n updateIngredient(index, \"amount\", e.target.value)}\n placeholder=\"Amount\"\n className=\"ingredient-amount-input\"\n />\n
\n removeIngredient(index)}\n aria-label=\"Remove ingredient\"\n >\n ×\n \n
\n ))}\n
\n
\n\n {/* Instructions */}\n
\n {changedKeysRef.current.includes(\"instructions\") && }\n
\n

Instructions

\n \n
\n
\n {recipe.instructions.map((instruction, index) => (\n
\n {/* Number Circle */}\n
{index + 1}
\n\n {/* Vertical Line */}\n {index < recipe.instructions.length - 1 &&
}\n\n {/* Instruction Content */}\n setEditingInstructionIndex(index)}\n >\n updateInstruction(index, e.target.value)}\n placeholder={!instruction ? \"Enter cooking instruction...\" : \"\"}\n onFocus={() => setEditingInstructionIndex(index)}\n onBlur={(e) => {\n // Only blur if clicking outside this instruction\n if (!e.relatedTarget || !e.currentTarget.contains(e.relatedTarget as Node)) {\n setEditingInstructionIndex(null);\n }\n }}\n />\n\n {/* Delete Button (only visible on hover) */}\n {\n e.stopPropagation(); // Prevent triggering parent onClick\n removeInstruction(index);\n }}\n aria-label=\"Remove instruction\"\n >\n ×\n \n
\n
\n ))}\n
\n
\n\n {/* Improve with AI Button */}\n
\n {\n if (!isLoading) {\n agent.addMessage({\n id: crypto.randomUUID(),\n role: \"user\",\n content: \"Improve the recipe\",\n });\n copilotkit.runAgent({ agent });\n }\n }}\n disabled={isLoading}\n >\n {isLoading ? \"Please Wait...\" : \"Improve with AI\"}\n \n
\n \n );\n}\n\nfunction Ping() {\n return (\n \n \n \n \n );\n}\n", + "language": "typescript", + "type": "file" + }, + { + "name": "style.css", + "content": "/* Recipe App Styles */\n.app-container {\n min-height: 100vh;\n width: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n background-size: cover;\n background-position: center;\n background-repeat: no-repeat;\n background-attachment: fixed;\n position: relative;\n overflow: auto;\n}\n\n.recipe-card {\n background-color: rgba(255, 255, 255, 0.97);\n border-radius: 16px;\n box-shadow: 0 15px 30px rgba(0, 0, 0, 0.25), 0 5px 15px rgba(0, 0, 0, 0.15);\n width: 100%;\n max-width: 750px;\n margin: 20px auto;\n padding: 14px 32px;\n position: relative;\n z-index: 1;\n backdrop-filter: blur(5px);\n border: 1px solid rgba(255, 255, 255, 0.3);\n transition: transform 0.2s ease, box-shadow 0.2s ease;\n animation: fadeIn 0.5s ease-out forwards;\n box-sizing: border-box;\n overflow: hidden;\n}\n\n.recipe-card:hover {\n transform: translateY(-5px);\n box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3), 0 10px 20px rgba(0, 0, 0, 0.2);\n}\n\n/* Recipe Header */\n.recipe-header {\n margin-bottom: 24px;\n}\n\n.recipe-title-input {\n width: 100%;\n font-size: 24px;\n font-weight: bold;\n border: none;\n outline: none;\n padding: 8px 0;\n margin-bottom: 0px;\n}\n\n.recipe-meta {\n display: flex;\n align-items: center;\n gap: 20px;\n margin-top: 5px;\n margin-bottom: 14px;\n}\n\n.meta-item {\n display: flex;\n align-items: center;\n gap: 8px;\n color: #555;\n}\n\n.meta-icon {\n font-size: 20px;\n color: #777;\n}\n\n.meta-text {\n font-size: 15px;\n}\n\n/* Recipe Meta Selects */\n.meta-item select {\n border: none;\n background: transparent;\n font-size: 15px;\n color: #555;\n cursor: pointer;\n outline: none;\n padding-right: 18px;\n transition: color 0.2s, transform 0.1s;\n font-weight: 500;\n}\n\n.meta-item select:hover,\n.meta-item select:focus {\n color: #FF5722;\n}\n\n.meta-item select:active {\n transform: scale(0.98);\n}\n\n.meta-item select option {\n color: #333;\n background-color: white;\n font-weight: normal;\n padding: 8px;\n}\n\n/* Section Container */\n.section-container {\n margin-bottom: 20px;\n position: relative;\n width: 100%;\n}\n\n.section-title {\n font-size: 20px;\n font-weight: 700;\n margin-bottom: 20px;\n color: #333;\n position: relative;\n display: inline-block;\n}\n\n.section-title:after {\n content: \"\";\n position: absolute;\n bottom: -8px;\n left: 0;\n width: 40px;\n height: 3px;\n background-color: #ff7043;\n border-radius: 3px;\n}\n\n/* Dietary Preferences */\n.dietary-options {\n display: flex;\n flex-wrap: wrap;\n gap: 10px 16px;\n margin-bottom: 16px;\n width: 100%;\n}\n\n.dietary-option {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 14px;\n cursor: pointer;\n margin-bottom: 4px;\n}\n\n.dietary-option input {\n cursor: pointer;\n}\n\n/* Ingredients */\n.ingredients-container {\n display: flex;\n flex-wrap: wrap;\n gap: 10px;\n margin-bottom: 15px;\n width: 100%;\n box-sizing: border-box;\n}\n\n.ingredient-card {\n display: flex;\n align-items: center;\n background-color: rgba(255, 255, 255, 0.9);\n border-radius: 12px;\n padding: 12px;\n margin-bottom: 10px;\n box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);\n position: relative;\n transition: all 0.2s ease;\n border: 1px solid rgba(240, 240, 240, 0.8);\n width: calc(33.333% - 7px);\n box-sizing: border-box;\n}\n\n.ingredient-card:hover {\n transform: translateY(-2px);\n box-shadow: 0 6px 15px rgba(0, 0, 0, 0.12);\n}\n\n.ingredient-card .remove-button {\n position: absolute;\n right: 10px;\n top: 10px;\n background: none;\n border: none;\n color: #ccc;\n font-size: 16px;\n cursor: pointer;\n display: none;\n padding: 0;\n width: 24px;\n height: 24px;\n line-height: 1;\n}\n\n.ingredient-card:hover .remove-button {\n display: block;\n}\n\n.ingredient-icon {\n font-size: 24px;\n margin-right: 12px;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n background-color: #f7f7f7;\n border-radius: 50%;\n flex-shrink: 0;\n}\n\n.ingredient-content {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 3px;\n min-width: 0;\n}\n\n.ingredient-name-input,\n.ingredient-amount-input {\n border: none;\n background: transparent;\n outline: none;\n width: 100%;\n padding: 0;\n text-overflow: ellipsis;\n overflow: hidden;\n white-space: nowrap;\n}\n\n.ingredient-name-input {\n font-weight: 500;\n font-size: 14px;\n}\n\n.ingredient-amount-input {\n font-size: 13px;\n color: #666;\n}\n\n.ingredient-name-input::placeholder,\n.ingredient-amount-input::placeholder {\n color: #aaa;\n}\n\n.remove-button {\n background: none;\n border: none;\n color: #999;\n font-size: 20px;\n cursor: pointer;\n padding: 0;\n width: 28px;\n height: 28px;\n display: flex;\n align-items: center;\n justify-content: center;\n margin-left: 10px;\n}\n\n.remove-button:hover {\n color: #FF5722;\n}\n\n/* Instructions */\n.instructions-container {\n display: flex;\n flex-direction: column;\n gap: 6px;\n position: relative;\n margin-bottom: 12px;\n width: 100%;\n}\n\n.instruction-item {\n position: relative;\n display: flex;\n width: 100%;\n box-sizing: border-box;\n margin-bottom: 8px;\n align-items: flex-start;\n}\n\n.instruction-number {\n display: flex;\n align-items: center;\n justify-content: center;\n min-width: 26px;\n height: 26px;\n background-color: #ff7043;\n color: white;\n border-radius: 50%;\n font-weight: 600;\n flex-shrink: 0;\n box-shadow: 0 2px 4px rgba(255, 112, 67, 0.3);\n z-index: 1;\n font-size: 13px;\n margin-top: 2px;\n}\n\n.instruction-line {\n position: absolute;\n left: 13px; /* Half of the number circle width */\n top: 22px;\n bottom: -18px;\n width: 2px;\n background: linear-gradient(to bottom, #ff7043 60%, rgba(255, 112, 67, 0.4));\n z-index: 0;\n}\n\n.instruction-content {\n background-color: white;\n border-radius: 10px;\n padding: 10px 14px;\n margin-left: 12px;\n flex-grow: 1;\n transition: all 0.2s ease;\n box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);\n border: 1px solid rgba(240, 240, 240, 0.8);\n position: relative;\n width: calc(100% - 38px);\n box-sizing: border-box;\n display: flex;\n align-items: center;\n}\n\n.instruction-content-editing {\n background-color: #fff9f6;\n box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12), 0 0 0 2px rgba(255, 112, 67, 0.2);\n}\n\n.instruction-content:hover {\n transform: translateY(-2px);\n box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);\n}\n\n.instruction-textarea {\n width: 100%;\n background: transparent;\n border: none;\n resize: vertical;\n font-family: inherit;\n font-size: 14px;\n line-height: 1.4;\n min-height: 20px;\n outline: none;\n padding: 0;\n margin: 0;\n}\n\n.instruction-delete-btn {\n position: absolute;\n background: none;\n border: none;\n color: #ccc;\n font-size: 16px;\n cursor: pointer;\n display: none;\n padding: 0;\n width: 20px;\n height: 20px;\n line-height: 1;\n top: 50%;\n transform: translateY(-50%);\n right: 8px;\n}\n\n.instruction-content:hover .instruction-delete-btn {\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n/* Action Button */\n.action-container {\n display: flex;\n justify-content: center;\n margin-top: 40px;\n padding-bottom: 20px;\n position: relative;\n}\n\n.improve-button {\n background-color: #ff7043;\n border: none;\n color: white;\n border-radius: 30px;\n font-size: 18px;\n font-weight: 600;\n padding: 14px 28px;\n cursor: pointer;\n transition: all 0.3s ease;\n box-shadow: 0 4px 15px rgba(255, 112, 67, 0.4);\n display: flex;\n align-items: center;\n justify-content: center;\n text-align: center;\n position: relative;\n min-width: 180px;\n}\n\n.improve-button:hover {\n background-color: #ff5722;\n transform: translateY(-2px);\n box-shadow: 0 8px 20px rgba(255, 112, 67, 0.5);\n}\n\n.improve-button.loading {\n background-color: #ff7043;\n opacity: 0.8;\n cursor: not-allowed;\n padding-left: 42px; /* Reduced padding to bring text closer to icon */\n padding-right: 22px; /* Balance the button */\n justify-content: flex-start; /* Left align text for better alignment with icon */\n}\n\n.improve-button.loading:after {\n content: \"\"; /* Add space between icon and text */\n display: inline-block;\n width: 8px; /* Width of the space */\n}\n\n.improve-button:before {\n content: \"\";\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83'/%3E%3C/svg%3E\");\n width: 20px; /* Slightly smaller icon */\n height: 20px;\n background-repeat: no-repeat;\n background-size: contain;\n position: absolute;\n left: 16px; /* Slightly adjusted */\n top: 50%;\n transform: translateY(-50%);\n display: none;\n}\n\n.improve-button.loading:before {\n display: block;\n animation: spin 1.5s linear infinite;\n}\n\n@keyframes spin {\n 0% { transform: translateY(-50%) rotate(0deg); }\n 100% { transform: translateY(-50%) rotate(360deg); }\n}\n\n/* Ping Animation */\n.ping-animation {\n position: absolute;\n display: flex;\n width: 12px;\n height: 12px;\n top: 0;\n right: 0;\n}\n\n.ping-circle {\n position: absolute;\n display: inline-flex;\n width: 100%;\n height: 100%;\n border-radius: 50%;\n background-color: #38BDF8;\n opacity: 0.75;\n animation: ping 1.5s cubic-bezier(0, 0, 0.2, 1) infinite;\n}\n\n.ping-dot {\n position: relative;\n display: inline-flex;\n width: 12px;\n height: 12px;\n border-radius: 50%;\n background-color: #0EA5E9;\n}\n\n@keyframes ping {\n 75%, 100% {\n transform: scale(2);\n opacity: 0;\n }\n}\n\n/* Instruction hover effects */\n.instruction-item:hover .instruction-delete-btn {\n display: flex !important;\n}\n\n/* Add some subtle animations */\n@keyframes fadeIn {\n from { opacity: 0; transform: translateY(20px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n/* Better center alignment for the recipe card */\n.recipe-card-container {\n display: flex;\n justify-content: center;\n width: 100%;\n position: relative;\n z-index: 1;\n margin: 0 auto;\n box-sizing: border-box;\n}\n\n/* Add Buttons */\n.add-button {\n background-color: transparent;\n color: #FF5722;\n border: 1px dashed #FF5722;\n border-radius: 8px;\n padding: 10px 16px;\n cursor: pointer;\n font-weight: 500;\n display: inline-block;\n font-size: 14px;\n margin-bottom: 0;\n}\n\n.add-step-button {\n background-color: transparent;\n color: #FF5722;\n border: 1px dashed #FF5722;\n border-radius: 6px;\n padding: 6px 12px;\n cursor: pointer;\n font-weight: 500;\n font-size: 13px;\n}\n\n/* Section Headers */\n.section-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 12px;\n}", + "language": "css", + "type": "file" + }, + { + "name": "README.mdx", + "content": "# 🍳 Shared State Recipe Creator\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **shared state** functionality - a powerful\nfeature that enables bidirectional data flow between:\n\n1. **Frontend → Agent**: UI controls update the agent's context in real-time\n2. **Agent → Frontend**: The Copilot's recipe creations instantly update the UI\n components\n\nIt's like having a cooking buddy who not only listens to what you want but also\nupdates your recipe card as you chat - no refresh needed! ✨\n\n## How to Interact\n\nMix and match any of these parameters (or none at all - it's up to you!):\n\n- **Skill Level**: Beginner to expert 👨‍🍳\n- **Cooking Time**: Quick meals or slow cooking ⏱️\n- **Special Preferences**: Dietary needs, flavor profiles, health goals 🥗\n- **Ingredients**: Items you want to include 🧅🥩🍄\n- **Instructions**: Any specific steps\n\nThen chat with your Copilot chef with prompts like:\n\n- \"I'm a beginner cook. Can you make me a quick dinner?\"\n- \"I need something spicy with chicken that takes under 30 minutes!\"\n\n## ✨ Shared State Magic in Action\n\n**What's happening technically:**\n\n- The UI and Copilot agent share the same state object (**Agent State = UI\n State**)\n- Changes from either side automatically update the other\n- Neither side needs to manually request updates from the other\n\n**What you'll see in this demo:**\n\n- Set cooking time to 20 minutes in the UI and watch the Copilot immediately\n respect your time constraint\n- Add ingredients through the UI and see them appear in your recipe\n- When the Copilot suggests new ingredients, watch them automatically appear in\n the UI ingredients list\n- Change your skill level and see how the Copilot adapts its instructions in\n real-time\n\nThis synchronized state creates a seamless experience where the agent always has\nyour current preferences, and any updates to the recipe are instantly reflected\nin both places.\n\nThis shared state pattern can be applied to any application where you want your\nUI and Copilot to work together in perfect harmony!\n", + "language": "markdown", + "type": "file" + }, + { + "name": "shared-state.ts", + "content": "/**\n * Shared State example for AWS Strands (TypeScript) — a collaborative recipe\n * editor. Shows how `stateContextBuilder`, `stateFromArgs`, and\n * `stateFromResult` keep a shared object in sync between the server and the\n * UI while the agent is streaming.\n */\n\nimport { Agent, tool } from \"@strands-agents/sdk\";\nimport { z } from \"zod\";\nimport { StrandsAgent, type StrandsAgentConfig } from \"@ag-ui/aws-strands\";\nimport { createStrandsApp } from \"@ag-ui/aws-strands/server\";\nimport { createModel } from \"../model-factory\";\n\nconst recipeSchema = z.object({\n title: z.string(),\n skillLevel: z.string(),\n specialPreferences: z.array(z.string()),\n cookingTime: z.string(),\n ingredients: z.array(\n z.object({ icon: z.string(), name: z.string(), amount: z.string() }),\n ),\n instructions: z.array(z.string()),\n changes: z.string().default(\"\"),\n});\n\nconst generateRecipe = tool({\n name: \"generate_recipe\",\n description:\n \"Using the existing (if any) ingredients and instructions, proceed with the recipe to finish it.\",\n inputSchema: z.object({ recipe: recipeSchema }),\n callback() {\n return \"Recipe updated successfully\";\n },\n});\n\nconst initialRecipe = {\n title: \"Make Your Recipe\",\n skillLevel: \"Intermediate\",\n specialPreferences: [] as string[],\n cookingTime: \"45 min\",\n ingredients: [\n { icon: \"🥕\", name: \"Carrots\", amount: \"3 large, grated\" },\n { icon: \"🌾\", name: \"All-Purpose Flour\", amount: \"2 cups\" },\n ],\n instructions: [\"Preheat oven to 350°F (175°C)\"],\n changes: \"\",\n};\n\nasync function main(): Promise {\n const strandsAgent = new Agent({\n model: await createModel(),\n systemPrompt: \"You are a helpful recipe editor.\",\n tools: [generateRecipe],\n });\n\n const config: StrandsAgentConfig = {\n stateContextBuilder: (input, prompt) => {\n const state = (input.state ?? {}) as Record;\n const recipe = state.recipe ?? initialRecipe;\n return `Current recipe state:\\n${JSON.stringify(recipe, null, 2)}\\n\\nUser request: ${prompt}\\n\\nPlease update the recipe by calling the registered tool.`;\n },\n toolBehaviors: {\n generate_recipe: {\n stateFromArgs: async (ctx) => {\n const args = ctx.toolInput as { recipe?: unknown };\n return args?.recipe ? { recipe: args.recipe } : null;\n },\n },\n },\n };\n\n const aguiAgent = new StrandsAgent({\n agent: strandsAgent,\n name: \"shared_state\",\n description: \"Strands agent with shared recipe state\",\n config,\n });\n\n const app = await createStrandsApp(aguiAgent, { path: \"/\" });\n app.listen(Number(process.env.PORT ?? 8000));\n}\n\nvoid main();\n", + "language": "ts", + "type": "file" + } + ], + "aws-strands-typescript::human_in_the_loop": [ + { + "name": "page.tsx", + "content": "\"use client\";\nimport React, { useState, useEffect } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { \n useHumanInTheLoop,\n useConfigureSuggestions,\n CopilotChat,\n CopilotChatConfigurationProvider,\n} from \"@copilotkit/react-core/v2\";\nimport { CopilotKit,\nuseLangGraphInterrupt } from \"@copilotkit/react-core\";\nimport { z } from \"zod\";\nimport { useTheme } from \"next-themes\";\n\ninterface HumanInTheLoopProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\nconst HumanInTheLoop: React.FC = ({ params }) => {\n const { integrationId } = React.use(params);\n\n return (\n \n \n \n );\n};\n\ninterface Step {\n description: string;\n status: \"disabled\" | \"enabled\" | \"executing\";\n}\n\n// Shared UI Components\nconst StepContainer = ({ theme, children }: { theme?: string; children: React.ReactNode }) => (\n
\n \n {children}\n
\n \n);\n\nconst StepHeader = ({\n theme,\n enabledCount,\n totalCount,\n status,\n showStatus = false,\n}: {\n theme?: string;\n enabledCount: number;\n totalCount: number;\n status?: string;\n showStatus?: boolean;\n}) => (\n
\n
\n

\n Select Steps\n

\n
\n
\n {enabledCount}/{totalCount} Selected\n
\n {showStatus && (\n \n {status === \"executing\" ? \"Ready\" : \"Waiting\"}\n
\n )}\n
\n
\n\n \n 0 ? (enabledCount / totalCount) * 100 : 0}%` }}\n />\n \n \n);\n\nconst StepItem = ({\n step,\n theme,\n status,\n onToggle,\n disabled = false,\n}: {\n step: { description: string; status: string };\n theme?: string;\n status?: string;\n onToggle: () => void;\n disabled?: boolean;\n}) => (\n \n \n \n);\n\nconst ActionButton = ({\n variant,\n theme,\n disabled,\n onClick,\n children,\n}: {\n variant: \"primary\" | \"secondary\" | \"success\" | \"danger\";\n theme?: string;\n disabled?: boolean;\n onClick: () => void;\n children: React.ReactNode;\n}) => {\n const baseClasses = \"px-6 py-3 rounded-lg font-semibold transition-all duration-200\";\n const enabledClasses = \"hover:scale-105 shadow-md hover:shadow-lg\";\n const disabledClasses = \"opacity-50 cursor-not-allowed\";\n\n const variantClasses = {\n primary:\n \"bg-gradient-to-r from-purple-500 to-purple-700 hover:from-purple-600 hover:to-purple-800 text-white shadow-lg hover:shadow-xl\",\n secondary:\n theme === \"dark\"\n ? \"bg-slate-700 hover:bg-slate-600 text-white border border-slate-600 hover:border-slate-500\"\n : \"bg-gray-100 hover:bg-gray-200 text-gray-800 border border-gray-300 hover:border-gray-400\",\n success:\n \"bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 text-white shadow-lg hover:shadow-xl\",\n danger:\n \"bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 text-white shadow-lg hover:shadow-xl\",\n };\n\n return (\n \n {children}\n \n );\n};\n\nconst DecorativeElements = ({\n theme,\n variant = \"default\",\n}: {\n theme?: string;\n variant?: \"default\" | \"success\" | \"danger\";\n}) => (\n <>\n \n \n \n);\nconst InterruptHumanInTheLoop: React.FC<{\n event: { value: { steps: Step[] } };\n resolve: (value: string) => void;\n}> = ({ event, resolve }) => {\n const { theme } = useTheme();\n\n // Parse and initialize steps data\n let initialSteps: Step[] = [];\n if (event.value && event.value.steps && Array.isArray(event.value.steps)) {\n initialSteps = event.value.steps.map((step: any) => ({\n description: typeof step === \"string\" ? step : step.description || \"\",\n status: typeof step === \"object\" && step.status ? step.status : \"enabled\",\n }));\n }\n\n const [localSteps, setLocalSteps] = useState(initialSteps);\n const enabledCount = localSteps.filter((step) => step.status === \"enabled\").length;\n\n const handleStepToggle = (index: number) => {\n setLocalSteps((prevSteps) =>\n prevSteps.map((step, i) =>\n i === index\n ? { ...step, status: step.status === \"enabled\" ? \"disabled\" : \"enabled\" }\n : step,\n ),\n );\n };\n\n const handlePerformSteps = () => {\n const selectedSteps = localSteps\n .filter((step) => step.status === \"enabled\")\n .map((step) => step.description);\n resolve(\"The user selected the following steps: \" + selectedSteps.join(\", \"));\n };\n\n return (\n \n \n\n
\n {localSteps.map((step, index) => (\n handleStepToggle(index)}\n />\n ))}\n
\n\n
\n \n \n Perform Steps\n \n {enabledCount}\n \n \n
\n\n \n
\n );\n};\n\nconst Chat = ({ integrationId }: { integrationId: string }) => {\n return (\n \n \n \n );\n};\n\nconst ChatContent = () => {\n useConfigureSuggestions({\n suggestions: [\n { title: \"Simple plan\", message: \"Please plan a trip to mars in 5 steps.\" },\n { title: \"Complex plan\", message: \"Please plan a pasta dish in 10 steps.\" },\n ],\n available: \"always\",\n });\n\n // Langgraph uses it's own hook to handle human-in-the-loop interactions via langgraph interrupts,\n // This hook won't do anything for other integrations.\n useLangGraphInterrupt({\n \n render: ({ event, resolve }) => ,\n });\n useHumanInTheLoop({\n agentId: \"human_in_the_loop\",\n name: \"generate_task_steps\",\n description: \"Generates a list of steps for the user to perform\",\n parameters: z.object({\n steps: z.array(\n z.object({\n description: z.string(),\n status: z.enum([\"enabled\", \"disabled\", \"executing\"]),\n }),\n ),\n }) ,\n // Note: In v1, `available` was used to disable this for langgraph integrations.\n // In v2, availability is handled at the agent/backend level.\n render: ({ args, respond, status }: any) => {\n return ;\n },\n });\n\n return (\n
\n
\n \n
\n
\n );\n};\n\nconst StepsFeedback = ({ args, respond, status }: { args: any; respond: any; status: any }) => {\n const { theme } = useTheme();\n const [localSteps, setLocalSteps] = useState([]);\n const [accepted, setAccepted] = useState(null);\n\n useEffect(() => {\n if (status === \"executing\" && localSteps.length === 0 && Array.isArray(args?.steps) && args.steps.length > 0) {\n setLocalSteps(args.steps);\n }\n }, [status, args?.steps, localSteps]);\n\n if (!Array.isArray(args?.steps) || args.steps.length === 0) {\n return <>;\n }\n\n const steps = Array.isArray(localSteps) && localSteps.length > 0 ? localSteps : args.steps;\n const enabledCount = steps.filter((step: any) => step.status === \"enabled\").length;\n\n const handleStepToggle = (index: number) => {\n setLocalSteps((prevSteps) =>\n prevSteps.map((step, i) =>\n i === index\n ? { ...step, status: step.status === \"enabled\" ? \"disabled\" : \"enabled\" }\n : step,\n ),\n );\n };\n\n const handleReject = () => {\n if (respond) {\n setAccepted(false);\n respond({ accepted: false });\n }\n };\n\n const handleConfirm = () => {\n if (respond) {\n const confirmedSteps = localSteps.filter((step) => step.status === \"enabled\");\n setAccepted(true);\n respond({ accepted: true, steps: confirmedSteps });\n }\n };\n\n return (\n \n \n\n
\n {steps.map((step: any, index: any) => (\n handleStepToggle(index)}\n disabled={status !== \"executing\"}\n />\n ))}\n
\n\n {/* Action Buttons - Different logic from InterruptHumanInTheLoop */}\n {accepted === null && (\n
\n \n \n Reject\n \n \n \n Confirm\n \n {enabledCount}\n \n \n
\n )}\n\n {/* Result State - Unique to StepsFeedback */}\n {accepted !== null && (\n
\n \n {accepted ? \"✓\" : \"✗\"}\n {accepted ? \"Accepted\" : \"Rejected\"}\n
\n \n )}\n\n \n
\n );\n};\n\nexport default HumanInTheLoop;\n", + "language": "typescript", + "type": "file" + }, + { + "name": "README.mdx", + "content": "# 🤝 Human-in-the-Loop Task Planner\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **human-in-the-loop** capabilities:\n\n1. **Collaborative Planning**: The Copilot generates task steps and lets you\n decide which ones to perform\n2. **Interactive Decision Making**: Select or deselect steps to customize the\n execution plan\n3. **Adaptive Responses**: The Copilot adapts its execution based on your\n choices, even handling missing steps\n\n## How to Interact\n\nTry these steps to experience the demo:\n\n1. Ask your Copilot to help with a task, such as:\n\n - \"Make me a sandwich\"\n - \"Plan a weekend trip\"\n - \"Organize a birthday party\"\n - \"Start a garden\"\n\n2. Review the suggested steps provided by your Copilot\n\n3. Select or deselect steps using the checkboxes to customize the plan\n\n - Try removing essential steps to see how the Copilot adapts!\n\n4. Click \"Execute Plan\" to see the outcome based on your selections\n\n## ✨ Human-in-the-Loop Magic in Action\n\n**What's happening technically:**\n\n- The agent analyzes your request and breaks it down into logical steps\n- These steps are presented to you through a dynamic UI component\n- Your selections are captured as user input\n- The agent considers your choices when executing the plan\n- The agent adapts to missing steps with creative problem-solving\n\n**What you'll see in this demo:**\n\n- The Copilot provides a detailed, step-by-step plan for your task\n- You have complete control over which steps to include\n- If you remove essential steps, the Copilot provides entertaining and creative\n workarounds\n- The final execution reflects your choices, showing how human input shapes the\n outcome\n- Each response is tailored to your specific selections\n\nThis human-in-the-loop pattern creates a powerful collaborative experience where\nboth human judgment and AI capabilities work together to achieve better results\nthan either could alone!\n", + "language": "markdown", + "type": "file" + }, + { + "name": "human-in-the-loop.ts", + "content": "/**\n * Human-in-the-Loop example for AWS Strands (TypeScript).\n *\n * The `generate_task_steps` tool is declared on the frontend via\n * `useHumanInTheLoop`. The @ag-ui/aws-strands adapter auto-registers it as a\n * proxy tool when `RunAgentInput.tools` arrives, so the backend does not\n * register a native tool here — Strands invokes the proxy, the adapter halts\n * the run after the proxy returns, the user reviews and approves the plan in\n * the UI, and the tool result is fed back to the agent on the next turn.\n *\n * No backend tool stub. No agent-side AG-UI event emission.\n *\n * Mirrors the Python reference\n * (integrations/aws-strands/python/examples/server/api/human_in_the_loop.py).\n */\n\nimport { Agent } from \"@strands-agents/sdk\";\nimport { StrandsAgent } from \"@ag-ui/aws-strands\";\nimport { createStrandsApp } from \"@ag-ui/aws-strands/server\";\nimport { createModel } from \"../model-factory\";\n\nasync function main(): Promise {\n const strandsAgent = new Agent({\n model: await createModel(),\n tools: [],\n systemPrompt: `You are a task planning assistant specialized in creating clear, actionable step-by-step plans.\n\n**Your Primary Role:**\n- Break down any user request into exactly 10 clear, actionable steps\n- Generate steps that require human review and approval\n- Execute only human-approved steps\n\n**When a user requests help with a task:**\n1. ALWAYS use the \\`generate_task_steps\\` tool to create a breakdown (default to 10 steps unless told otherwise)\n2. Each step must be:\n - Brief (only a few words)\n - In imperative form (e.g., \"Dig hole\", \"Open door\", \"Mix ingredients\")\n - Clear and actionable\n - Logically ordered from start to finish\n3. Set all steps to \"enabled\" status initially\n4. After the user reviews the plan:\n - If accepted: Briefly confirm the plan (only include the approved steps) and proceed (don't repeat the steps). Do not ask for more clarifying information.\n - If rejected: Ask what they'd like to change (don't call generate_task_steps again until they provide input)\n5. When the user accepts the plan, \"execute\" the plan by repeating the approved steps in order as if you have just done them. Then let the user know you have completed the plan.\n - example: if the user accepts the steps \"Dig hole\", \"Open door\", \"Mix ingredients\", you would respond with \"Digging hole... Opening door... Mixing ingredients...\"\n\n**Important:**\n- NEVER call \\`generate_task_steps\\` twice in a row without user input\n- NEVER repeat the list of steps in your response after calling the tool\n- DO provide a brief, creative summary of how you would execute the approved steps\n`,\n });\n\n const aguiAgent = new StrandsAgent({\n agent: strandsAgent,\n name: \"human_in_the_loop\",\n description: \"AWS Strands agent with human-in-the-loop task planning\",\n });\n\n const app = await createStrandsApp(aguiAgent, { path: \"/\" });\n const port = Number(process.env.PORT ?? 8000);\n app.listen(port, () => {\n console.log(`Listening on http://localhost:${port}`);\n });\n}\n\nvoid main();\n", + "language": "ts", + "type": "file" + } + ], + "aws-strands-typescript::tool_based_generative_ui": [ + { + "name": "page.tsx", + "content": "\"use client\";\nimport React, { useState } from \"react\";\nimport \"@copilotkit/react-core/v2/styles.css\";\nimport { \n useFrontendTool,\n useConfigureSuggestions,\n CopilotSidebar,\n} from \"@copilotkit/react-core/v2\";\nimport { z } from \"zod\";\nimport {\n Carousel,\n CarouselContent,\n CarouselItem,\n CarouselNext,\n CarouselPrevious,\n} from \"@/components/ui/carousel\";\nimport { useURLParams } from \"@/contexts/url-params-context\";\nimport { CopilotKit } from \"@copilotkit/react-core\";\n\ninterface ToolBasedGenerativeUIProps {\n params: Promise<{\n integrationId: string;\n }>;\n}\n\ninterface Haiku {\n japanese: string[];\n english: string[];\n image_name: string | null;\n gradient: string;\n}\n\nexport default function ToolBasedGenerativeUI({ params }: ToolBasedGenerativeUIProps) {\n const { integrationId } = React.use(params);\n const { chatDefaultOpen } = useURLParams();\n\n return (\n \n \n \n \n );\n}\n\nfunction SidebarWithSuggestions({ defaultOpen }: { defaultOpen: boolean }) {\n useConfigureSuggestions({\n suggestions: [\n { title: \"Nature Haiku\", message: \"Write me a haiku about nature.\" },\n { title: \"Ocean Haiku\", message: \"Create a haiku about the ocean.\" },\n { title: \"Spring Haiku\", message: \"Generate a haiku about spring.\" },\n ],\n available: \"always\",\n });\n\n return (\n \n );\n}\n\nconst VALID_IMAGE_NAMES = [\n \"Osaka_Castle_Turret_Stone_Wall_Pine_Trees_Daytime.jpg\",\n \"Tokyo_Skyline_Night_Tokyo_Tower_Mount_Fuji_View.jpg\",\n \"Itsukushima_Shrine_Miyajima_Floating_Torii_Gate_Sunset_Long_Exposure.jpg\",\n \"Takachiho_Gorge_Waterfall_River_Lush_Greenery_Japan.jpg\",\n \"Bonsai_Tree_Potted_Japanese_Art_Green_Foliage.jpeg\",\n \"Shirakawa-go_Gassho-zukuri_Thatched_Roof_Village_Aerial_View.jpg\",\n \"Ginkaku-ji_Silver_Pavilion_Kyoto_Japanese_Garden_Pond_Reflection.jpg\",\n \"Senso-ji_Temple_Asakusa_Cherry_Blossoms_Kimono_Umbrella.jpg\",\n \"Cherry_Blossoms_Sakura_Night_View_City_Lights_Japan.jpg\",\n \"Mount_Fuji_Lake_Reflection_Cherry_Blossoms_Sakura_Spring.jpg\",\n];\n\nfunction HaikuDisplay() {\n const [activeIndex, setActiveIndex] = useState(0);\n const [haikus, setHaikus] = useState([\n {\n japanese: [\"仮の句よ\", \"まっさらながら\", \"花を呼ぶ\"],\n english: [\"A placeholder verse—\", \"even in a blank canvas,\", \"it beckons flowers.\"],\n image_name: null,\n gradient: \"\",\n },\n ]);\n\n useFrontendTool(\n {\n agentId: \"tool_based_generative_ui\",\n name: \"generate_haiku\",\n parameters: z.object({\n japanese: z.array(z.string()).describe(\"3 lines of haiku in Japanese\"),\n english: z.array(z.string()).describe(\"3 lines of haiku translated to English\"),\n image_name: z.string().describe(`One relevant image name from: ${VALID_IMAGE_NAMES.join(\", \")}`),\n gradient: z.string().describe(\"CSS Gradient color for the background\"),\n }) ,\n followUp: false,\n handler: async ({ japanese, english, image_name, gradient }: { japanese: string[]; english: string[]; image_name: string; gradient: string }) => {\n const newHaiku: Haiku = {\n japanese: japanese || [],\n english: english || [],\n image_name: image_name || null,\n gradient: gradient || \"\",\n };\n setHaikus((prev) => [\n newHaiku,\n ...prev.filter((h) => h.english[0] !== \"A placeholder verse—\"),\n ]);\n setActiveIndex(0);\n return \"Haiku generated!\";\n },\n render: ({ args }: { args: Partial }) => {\n if (!args.japanese) return <>;\n return ;\n },\n },\n [haikus],\n );\n\n const currentHaiku = haikus[activeIndex];\n\n return (\n
\n
\n \n \n {haikus.map((haiku, index) => (\n \n \n \n ))}\n \n {haikus.length > 1 && (\n <>\n \n \n \n )}\n \n
\n
\n );\n}\n\nfunction HaikuCard({ haiku }: { haiku: Partial }) {\n return (\n \n {/* Decorative background elements */}\n
\n
\n\n {/* Haiku Text */}\n
\n {haiku.japanese?.map((line, index) => (\n \n \n {line}\n

\n \n {haiku.english?.[index]}\n

\n
\n ))}\n
\n\n {/* Image */}\n {haiku.image_name && (\n
\n
\n \n
\n
\n
\n )}\n
\n );\n}\n", + "language": "typescript", + "type": "file" + }, + { + "name": "style.css", + "content": ".page-background {\n /* Darker gradient background */\n background: linear-gradient(170deg, #e9ecef 0%, #ced4da 100%);\n}\n\n@keyframes fade-scale-in {\n from {\n opacity: 0;\n transform: translateY(10px) scale(0.98);\n }\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n/* Updated card entry animation */\n@keyframes pop-in {\n 0% {\n opacity: 0;\n transform: translateY(15px) scale(0.95);\n }\n 70% {\n opacity: 1;\n transform: translateY(-2px) scale(1.02);\n }\n 100% {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n/* Animation for subtle background gradient movement */\n@keyframes animated-gradient {\n 0% {\n background-position: 0% 50%;\n }\n 50% {\n background-position: 100% 50%;\n }\n 100% {\n background-position: 0% 50%;\n }\n}\n\n/* Animation for flash effect on apply */\n@keyframes flash-border-glow {\n 0% {\n /* Start slightly intensified */\n border-top-color: #ff5b4a !important;\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.07),\n inset 0 1px 2px rgba(0, 0, 0, 0.01),\n 0 0 25px rgba(255, 91, 74, 0.5);\n }\n 50% {\n /* Peak intensity */\n border-top-color: #ff4733 !important;\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08),\n inset 0 1px 2px rgba(0, 0, 0, 0.01),\n 0 0 35px rgba(255, 71, 51, 0.7);\n }\n 100% {\n /* Return to default state appearance */\n border-top-color: #ff6f61 !important;\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.07),\n inset 0 1px 2px rgba(0, 0, 0, 0.01),\n 0 0 10px rgba(255, 111, 97, 0.15);\n }\n}\n\n/* Existing animation for haiku lines */\n@keyframes fade-slide-in {\n from {\n opacity: 0;\n transform: translateX(-15px);\n }\n to {\n opacity: 1;\n transform: translateX(0);\n }\n}\n\n.animated-fade-in {\n /* Use the new pop-in animation */\n animation: pop-in 0.6s ease-out forwards;\n}\n\n.haiku-card {\n /* Subtle animated gradient background */\n background: linear-gradient(120deg, #ffffff 0%, #fdfdfd 50%, #ffffff 100%);\n background-size: 200% 200%;\n animation: animated-gradient 10s ease infinite;\n\n /* === Explicit Border Override Attempt === */\n /* 1. Set the default grey border for all sides */\n border: 1px solid #dee2e6;\n\n /* 2. Explicitly override the top border immediately after */\n border-top: 10px solid #ff6f61 !important; /* Orange top - Added !important */\n /* === End Explicit Border Override Attempt === */\n\n padding: 2.5rem 3rem;\n border-radius: 20px;\n\n /* Default glow intensity */\n box-shadow: 0 10px 30px rgba(0, 0, 0, 0.07),\n inset 0 1px 2px rgba(0, 0, 0, 0.01),\n 0 0 15px rgba(255, 111, 97, 0.25);\n text-align: left;\n max-width: 745px;\n margin: 3rem auto;\n min-width: 600px;\n\n /* Transition */\n transition: transform 0.35s ease, box-shadow 0.35s ease, border-top-width 0.35s ease, border-top-color 0.35s ease;\n}\n\n.haiku-card:hover {\n transform: translateY(-8px) scale(1.03);\n /* Enhanced shadow + Glow */\n box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1),\n inset 0 1px 2px rgba(0, 0, 0, 0.01),\n 0 0 25px rgba(255, 91, 74, 0.5);\n /* Modify only top border properties */\n border-top-width: 14px !important; /* Added !important */\n border-top-color: #ff5b4a !important; /* Added !important */\n}\n\n.haiku-card .flex {\n margin-bottom: 1.5rem;\n}\n\n.haiku-card .flex.haiku-line { /* Target the lines specifically */\n margin-bottom: 1.5rem;\n opacity: 0; /* Start hidden for animation */\n animation: fade-slide-in 0.5s ease-out forwards;\n /* animation-delay is set inline in page.tsx */\n}\n\n/* Remove previous explicit color overrides - rely on Tailwind */\n/* .haiku-card p.text-4xl {\n color: #212529;\n}\n\n.haiku-card p.text-base {\n color: #495057;\n} */\n\n.haiku-card.applied-flash {\n /* Apply the flash animation once */\n /* Note: animation itself has !important on border-top-color */\n animation: flash-border-glow 0.6s ease-out forwards;\n}\n\n/* Styling for images within the main haiku card */\n.haiku-card-image {\n width: 9.5rem; /* Increased size (approx w-48) */\n height: 9.5rem; /* Increased size (approx h-48) */\n object-fit: cover;\n border-radius: 1.5rem; /* rounded-xl */\n border: 1px solid #e5e7eb;\n /* Enhanced shadow with subtle orange hint */\n box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1),\n 0 3px 6px rgba(0, 0, 0, 0.08),\n 0 0 10px rgba(255, 111, 97, 0.2);\n /* Inherit animation delay from inline style */\n animation-name: fadeIn;\n animation-duration: 0.5s;\n animation-fill-mode: both;\n}\n\n/* Styling for images within the suggestion card */\n.suggestion-card-image {\n width: 6.5rem; /* Increased slightly (w-20) */\n height: 6.5rem; /* Increased slightly (h-20) */\n object-fit: cover;\n border-radius: 1rem; /* Equivalent to rounded-md */\n border: 1px solid #d1d5db; /* Equivalent to border (using Tailwind gray-300) */\n margin-top: 0.5rem;\n /* Added shadow for suggestion images */\n box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1),\n 0 2px 4px rgba(0, 0, 0, 0.06);\n transition: all 0.2s ease-in-out; /* Added for smooth deselection */\n}\n\n/* Styling for the focused suggestion card image */\n.suggestion-card-image-focus {\n width: 6.5rem;\n height: 6.5rem;\n object-fit: cover;\n border-radius: 1rem;\n margin-top: 0.5rem;\n /* Highlight styles */\n border: 2px solid #ff6f61; /* Thicker, themed border */\n box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1), /* Base shadow for depth */\n 0 0 12px rgba(255, 111, 97, 0.6); /* Orange glow */\n transform: scale(1.05); /* Slightly scale up */\n transition: all 0.2s ease-in-out; /* Smooth transition for focus */\n}\n\n/* Styling for the suggestion card container in the sidebar */\n.suggestion-card {\n border: 1px solid #dee2e6; /* Same default border as haiku-card */\n border-top: 10px solid #ff6f61; /* Same orange top border */\n border-radius: 0.375rem; /* Default rounded-md */\n /* Note: background-color is set by Tailwind bg-gray-100 */\n /* Other styles like padding, margin, flex are handled by Tailwind */\n}\n\n.suggestion-image-container {\n display: flex;\n gap: 1rem;\n justify-content: space-between;\n width: 100%;\n height: 6.5rem;\n}\n\n/* Mobile responsive styles - matches useMobileView hook breakpoint */\n@media (max-width: 767px) {\n .haiku-card {\n padding: 1rem 1.5rem; /* Reduced from 2.5rem 3rem */\n min-width: auto; /* Remove min-width constraint */\n max-width: 100%; /* Full width on mobile */\n margin: 1rem auto; /* Reduced margin */\n }\n\n .haiku-card-image {\n width: 5.625rem; /* 90px - smaller on mobile */\n height: 5.625rem; /* 90px - smaller on mobile */\n }\n\n .suggestion-card-image {\n width: 5rem; /* Slightly smaller on mobile */\n height: 5rem; /* Slightly smaller on mobile */\n }\n\n .suggestion-card-image-focus {\n width: 5rem; /* Slightly smaller on mobile */\n height: 5rem; /* Slightly smaller on mobile */\n }\n}\n", + "language": "css", + "type": "file" + }, + { + "name": "README.mdx", + "content": "# 🪶 Tool-Based Generative UI Haiku Creator\n\n## What This Demo Shows\n\nThis demo showcases CopilotKit's **tool-based generative UI** capabilities:\n\n1. **Frontend Rendering of Tool Calls**: Backend tool calls are automatically\n rendered in the UI\n2. **Dynamic UI Generation**: The UI updates in real-time as the agent generates\n content\n3. **Elegant Content Presentation**: Complex structured data (haikus) are\n beautifully displayed\n\n## How to Interact\n\nChat with your Copilot and ask for haikus about different topics:\n\n- \"Create a haiku about nature\"\n- \"Write a haiku about technology\"\n- \"Generate a haiku about the changing seasons\"\n- \"Make a humorous haiku about programming\"\n\nEach request will trigger the agent to generate a haiku and display it in a\nvisually appealing card format in the UI.\n\n## ✨ Tool-Based Generative UI in Action\n\n**What's happening technically:**\n\n- The agent processes your request and determines it should create a haiku\n- It calls a backend tool that returns structured haiku data\n- CopilotKit automatically renders this tool call in the frontend\n- The rendering is handled by the registered tool component in your React app\n- No manual state management is required to display the results\n\n**What you'll see in this demo:**\n\n- As you request a haiku, a beautifully formatted card appears in the UI\n- The haiku follows the traditional 5-7-5 syllable structure\n- Each haiku is presented with consistent styling\n- Multiple haikus can be generated in sequence\n- The UI adapts to display each new piece of content\n\nThis pattern of tool-based generative UI can be extended to create any kind of\ndynamic content - from data visualizations to interactive components, all driven\nby your Copilot's tool calls!\n", + "language": "markdown", + "type": "file" + }, + { + "name": "tool-based-generative-ui.ts", + "content": "/**\n * Tool-based Generative UI example for AWS Strands (TypeScript).\n *\n * The `generate_haiku` tool is declared on the frontend via `useFrontendTool`\n * — the @ag-ui/aws-strands adapter auto-registers it as a proxy tool when\n * `RunAgentInput.tools` arrives, so the backend does not register a native\n * tool here. Strands invokes the proxy with the structured haiku args, the\n * adapter halts the run after the proxy returns, and the browser renders the\n * haiku card from the streamed `TOOL_CALL_*` events.\n */\n\nimport { Agent } from \"@strands-agents/sdk\";\nimport { StrandsAgent } from \"@ag-ui/aws-strands\";\nimport { createStrandsApp } from \"@ag-ui/aws-strands/server\";\nimport { createModel } from \"../model-factory\";\n\nasync function main(): Promise {\n const strandsAgent = new Agent({\n model: await createModel(),\n tools: [],\n systemPrompt: `You are a creative haiku generator.\n\nWhen the user asks for a haiku, ALWAYS call the \\`generate_haiku\\` tool with:\n- 3 lines of haiku in Japanese\n- 3 lines of haiku translated to English\n- One relevant image_name from the provided list\n- A CSS gradient for the card background\n\nDo not respond with plain text — always use the tool.`,\n });\n\n const aguiAgent = new StrandsAgent({\n agent: strandsAgent,\n name: \"tool_based_generative_ui\",\n description: \"AWS Strands haiku generator with frontend-rendered tool\",\n });\n\n const app = await createStrandsApp(aguiAgent, { path: \"/\" });\n const port = Number(process.env.PORT ?? 8000);\n app.listen(port, () => {\n console.log(`Listening on http://localhost:${port}`);\n });\n}\n\nvoid main();\n", + "language": "ts", + "type": "file" + } + ], "claude-agent-sdk-python::agentic_chat": [ { "name": "page.tsx", diff --git a/apps/dojo/src/menu.ts b/apps/dojo/src/menu.ts index 1875671a4d..25e39c9ab7 100644 --- a/apps/dojo/src/menu.ts +++ b/apps/dojo/src/menu.ts @@ -292,6 +292,21 @@ export const menuIntegrations = [ "human_in_the_loop", ], }, + { + id: "aws-strands-typescript", + name: "AWS Strands (TypeScript)", + features: [ + "agentic_chat", + "agentic_chat_reasoning", + "agentic_chat_multimodal", + "v1_agentic_chat", + "backend_tool_rendering", + "agentic_generative_ui", + "shared_state", + "human_in_the_loop", + "tool_based_generative_ui", + ], + }, { id: "claude-agent-sdk-python", name: "Claude Agent SDK (Python)", diff --git a/integrations/aws-strands/ARCHITECTURE.md b/integrations/aws-strands/ARCHITECTURE.md index c3b5272b2d..ea67ca66ab 100644 --- a/integrations/aws-strands/ARCHITECTURE.md +++ b/integrations/aws-strands/ARCHITECTURE.md @@ -1,38 +1,41 @@ # AWS Strands Integration Architecture -This document explains how the AWS Strands integration inside `integrations/aws-strands/` is implemented today. It covers the Python adapter that speaks the AG-UI protocol and the FastAPI transport helpers. +This document explains how the AWS Strands integration inside `integrations/aws-strands/` is implemented today. It covers the Python adapter (FastAPI) and the TypeScript adapter (Express), which share the same AG-UI event contract; the Python implementation is the reference, and the TypeScript adapter documents only what it does differently. --- ## System Overview ``` -┌─────────────┐ RunAgentInput ┌──────────────────────────┐ -│ AG-UI UI │ ────────────────► │ AG-UI HttpAgent (standard) │ -└─────────────┘ (messages, │ e.g., @ag-ui/client │ - tools, state) └──────────────────────────┬──────┘ +┌─────────────┐ RunAgentInput ┌────────────────────────────┐ +│ AG-UI UI │ ────────────────────────► │ AG-UI HttpAgent (standard) │ +└─────────────┘ (messages, │ e.g., @ag-ui/client │ + tools, state) └──────────────────┬─────────┘ │ HTTP(S) POST + SSE ▼ ┌────────────────────────────┐ - │ FastAPI endpoint (Python) │ - │ add_strands_fastapi_endpoint│ + │ Transport endpoint │ + │ Python: FastAPI │ + │ TypeScript: Express │ └─────────────┬──────────────┘ │ ▼ ┌─────────────────────────┐ │ StrandsAgent adapter │ - │ (src/ag_ui_strands/...) │ + │ python/src/ag_ui_strands│ + │ typescript/src │ └─────────────┬───────────┘ │ ▼ - strands.Agent.stream_async() + Python: strands.Agent.stream_async() + TypeScript: Agent.stream() (async iterator) ``` 1. The browser (or any AG-UI client) instantiates the standard AG-UI `HttpAgent` (or equivalent) and targets the Strands endpoint URL; there is no Strands-specific SDK on the client. 2. The client sends a `RunAgentInput` payload that contains the current thread state, previously executed tools, shared UI state, and the latest user message(s). -3. `add_strands_fastapi_endpoint` (or `create_strands_app`) registers a POST route that deserializes `RunAgentInput`, instantiates an `EventEncoder`, and streams whatever the Python `StrandsAgent` yields. -4. `StrandsAgent.run` wraps a concrete `strands.Agent` instance, forwards the derived user prompt into `stream_async`, and translates every event into AG-UI protocol events (text deltas, tool invocations, snapshots, etc.). -5. The encoded stream is delivered back to the client over `text/event-stream` (or JSON chunked mode) and rendered by AG-UI without any Strands-specific code on the frontend. +3. The transport layer (`add_strands_fastapi_endpoint` in Python, `addStrandsExpressEndpoint` in TypeScript) registers a POST route that deserializes `RunAgentInput`, instantiates an `EventEncoder`, and streams whatever the `StrandsAgent` yields. +4. `StrandsAgent.run` wraps a concrete Strands `Agent` instance, forwards the derived user prompt into the streaming call, and translates every event into AG-UI protocol events (text deltas, tool invocations, snapshots, etc.). +5. The encoded stream is delivered back to the client over `text/event-stream` (or binary protobuf) and rendered by AG-UI without any Strands-specific code on the frontend. --- @@ -51,7 +54,7 @@ This document explains how the AWS Strands integration inside `integrations/aws- 2. After each `ToolCallEndEvent`, with the new `AssistantMessage(tool_calls=[…])` appended. 3. After each `ToolCallResultEvent`, with the new `ToolMessage` appended. 4. After each terminal `TextMessageEndEvent`, with the new `AssistantMessage(content=…)` appended. - - Each snapshot carries the *complete* thread state as known so far. Toggle globally via `StrandsAgentConfig.emit_messages_snapshot` (default `True`); suppress per-tool with `ToolBehavior.skip_messages_snapshot=True`. + - Each snapshot carries the _complete_ thread state as known so far. Toggle globally via `StrandsAgentConfig.emit_messages_snapshot` (default `True`); suppress per-tool with `ToolBehavior.skip_messages_snapshot=True`. - **State priming** - If `RunAgentInput.state` is provided, it immediately publishes a `StateSnapshotEvent`, filtering out any `messages` field so the frontend remains the source of truth for the timeline. - Optionally rewrites the outgoing user prompt via `StrandsAgentConfig.state_context_builder`. @@ -80,7 +83,7 @@ This document explains how the AWS Strands integration inside `integrations/aws- - **Frontend tool awareness** - `input_data.tools` supplies the frontend tool registry. Their names are used to (a) avoid double-invoking tool results that were literally produced by the UI, and (b) stop the Strands run after the LLM has issued a UI-only instruction. - **Reasoning streaming** - - When Strands yields events with `reasoningText` and `reasoning=true`, the adapter emits REASONING_* events. + - When Strands yields events with `reasoningText` and `reasoning=true`, the adapter emits REASONING\_\* events. - Emits `ReasoningStartEvent`, `ReasoningMessageStartEvent`, content events, then `ReasoningMessageEndEvent` and `ReasoningEndEvent`. - For encrypted/redacted reasoning content (`reasoningRedactedContent`), emits `ReasoningEncryptedValueEvent` with base64-encoded payload. - Reasoning events are automatically closed when a `contentBlockStop` event is received. @@ -105,6 +108,9 @@ This document explains how the AWS Strands integration inside `integrations/aws- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `tool_behaviors: Dict[str, ToolBehavior]` | Per-tool overrides keyed by the Strands tool name. | | `state_context_builder` | Callable that enriches the outgoing prompt with the current shared state (useful for reiterating plan steps, recipes, etc.). | +| `session_manager_provider` | Factory invoked once per thread to produce a per-thread `SessionManager`. | +| `emit_messages_snapshot` | Global opt-out of the four-point `MESSAGES_SNAPSHOT` emission. Default `True`. | +| `replay_history_into_strands` | Global opt-out of the per-run Strands history reconciliation. Default `True`. | `ToolBehavior` captures how the adapter should react: @@ -128,7 +134,7 @@ The transport layer is intentionally lightweight: - `add_strands_fastapi_endpoint(app, agent, path)` registers a POST route that: - Accepts a `RunAgentInput` body. - - Instantiates `EventEncoder` using the requester’s `Accept` header to choose between SSE (`text/event-stream`) and newline-delimited JSON. + - Instantiates `EventEncoder` using the requester's `Accept` header to choose between SSE (`text/event-stream`) and newline-delimited JSON. - Streams whatever `StrandsAgent.run` yields, automatically encoding every AG-UI event. - Sends a `RunErrorEvent` with `code="ENCODING_ERROR"` if serialization fails mid-stream. - `create_strands_app(agent, path="/")` bootstraps a FastAPI application, adds permissive CORS middleware (allowing any origin/method/header so AG-UI localhost builds can connect), and mounts the agent route. @@ -147,21 +153,87 @@ This mirrors other AG-UI integrations (Agno, LangGraph, etc.), so documentation --- -## Example Entry Points (`python/examples/server/api/*.py`) +## TypeScript Adapter (`typescript/src/`) + +The TypeScript adapter is a line-by-line port of the Python adapter — same splice points, same config primitives, same event emission order. Only the differences below matter; everything else in the Python section above applies unchanged (with camelCase substituted for snake_case, e.g. `stateFromArgs` ↔ `state_from_args`). + +### Module Layout + +``` +typescript/src/ +├── agent.ts ← StrandsAgent (port of agent.py) +├── client-proxy-tool.ts ← sync of RunAgentInput.tools into Strands registry +├── config.ts ← StrandsAgentConfig, ToolBehavior, helpers +├── endpoint.ts ← Express route registration + capabilities endpoint +├── logger.ts ← injectable Logger interface + internal default +├── types.ts ← internal SeenToolCall bookkeeping +├── utils.ts ← content conversion + createStrandsApp factory +└── index.ts ← public exports +``` + +### SDK-Shape Differences + +These are forced by the upstream SDK and do not reflect behavioral divergence: + +- **Event dispatch**: Python matches on dict keys (`event.get("current_tool_use")`, `event.get("data")`, `"message" in event`); TypeScript matches on the typed event `.type` (`modelContentBlockDeltaEvent`, `toolUseInputDelta`, `afterToolCallEvent`). Outcomes map 1:1; each dispatch branch carries a `// Maps to Python's X branch` comment. +- **Tool proxy**: Python uses `PythonAgentTool` + `tool.mark_dynamic()` + raw `tool_registry.registry[…]` dict access. TypeScript uses a plain object implementing the `Tool` interface + `toolRegistry.add()` / `remove()` / `get()`. +- **Content blocks**: Python returns plain dicts from `convert_agui_content_to_strands`; TypeScript returns SDK class instances (`TextBlock`, `ImageBlock`, etc.) which the history replay path unwraps via `toJSON()`. +- **History seeding**: Python mutates `strands_agent.messages` in place after construction. TypeScript consumes `AgentConfig.messages` at construction time, so `buildStrandsSeed` / `convertMessagesForStrandsSeed` produce the seed outside the per-thread init lock (to avoid serialising cold-cache starts behind one slow replay). +- **Template agent cloning**: Python introspects `StrandsAgentCore.__init__` via `inspect.signature` to forward every caller-set kwarg into per-thread clones. TypeScript hardcodes the forwardable fields (`TemplateAgentCloneFields`) because the TS SDK doesn't expose a comparable introspection hook. + +### Additions Beyond the Python Adapter + +Behaviors the Python adapter does not currently implement, added to match TypeScript-ecosystem expectations or to close conformance gaps: + +- **Multi-agent orchestrator mode** (`_runOrchestrator`): accepts a Strands `Graph` or `Swarm` in place of a single `Agent` and drives its `.stream()` directly. Per-thread caching, session managers, and proxy-tool sync are bypassed because orchestrators are stateless per invocation. +- **`THREAD_BUSY` guard**: `_activeRunsByThread` rejects concurrent runs on the same thread with `RUN_ERROR { code: "THREAD_BUSY" }`. The TS SDK throws `"Agent is already processing an invocation"` if this isn't caught up front; Python's SDK has no equivalent collision. +- **`AbortController` wiring**: the Strands `.stream()` call receives a `cancelSignal`; the transport's disconnect listener fires it so Bedrock stops streaming when the HTTP client drops. +- **Native interrupt bridge (Strands SDK 1.1.0+)**: when `AgentResult.stopReason === "interrupt"`, the adapter records the outstanding `Interrupt[]` on `_pendingInterruptsByThread` and emits `RUN_FINISHED { outcome: { type: "interrupt", interrupts: [...] } }` (interrupts.mdx "State at the interrupt boundary"). A follow-up `RunAgentInput.resume[]` is validated against the pending set: known IDs are converted to `InterruptResponseContent[]` and forwarded to `agent.stream()` as the invoke args (replacing the normal `messages` seed so Strands picks up from its own checkpoint); unknown IDs short-circuit with `RUN_ERROR { code: "UNKNOWN_INTERRUPT" }`. **Python conformance gap**: the Python adapter does not currently read `RunAgentInput.resume[]` at all (silently ignored), violating interrupts.mdx rule 4. Tracked for follow-up. +- **Request-boundary validation** (`addStrandsExpressEndpoint`): returns `415` for non-JSON `Content-Type`, `400` for bodies that fail the shared Zod `RunAgentInputSchema`, and normalizes snake_case top-level keys (`thread_id`, `run_id`, `parent_run_id`, `forwarded_props`) into camelCase before validating. FastAPI's Pydantic layer handles the equivalent on the Python side. +- **Client-disconnect handling**: HTTP/1.1 `res.close` and HTTP/2 `req.aborted` both trigger `iterator.return()`, firing the agent generator's `finally` so the `_activeRunsByThread` slot releases and the Bedrock stream aborts. +- **Protobuf content negotiation**: only selected when `Accept` explicitly contains `application/vnd.ag-ui.event+proto`; `*/*` or omitted Accept falls back to SSE. +- **Capabilities endpoint** (`addCapabilities`, `DEFAULT_CAPABILITIES`, `capabilitiesFor`): optional `GET /capabilities` returning a static matrix of supported event families, transports, and protocol features so frontends don't have to probe empirically. +- **Chunk-event emission** (`emitChunkEvents`): optional flag that collapses explicit `*_START` / `*_CONTENT` / `*_END` triples into `TEXT_MESSAGE_CHUNK` / `TOOL_CALL_CHUNK` / `REASONING_MESSAGE_CHUNK` self-expanding chunks per `concepts/events.mdx`. Halves the event count on high-frequency deltas. +- **`ToolCallContextExtras`** (`buildContextExtras`): `context` + `forwardedProps` are flattened onto every `ToolCallContext` / `ToolResultContext` and passed as a 3rd argument to `stateContextBuilder`, so hooks can read per-request auth tokens / locale without re-parsing `inputData`. Python passes `input_data` directly and callers pull these fields off themselves. +- **Injectable logger** (`StrandsAgentConfig.logger`): matches Python's `logging.getLogger(__name__)` surface. Any `{ debug, warn, error }` record works — wire in pino / winston / bunyan / a silent stub directly. Debug message strings match the Python adapter field-for-field (modulo camelCase) so cross-SDK log diffs are straightforward. +- **`AWSStrandsAgent extends HttpAgent`**: thin client-side shim re-export so AG-UI TypeScript clients can `new AWSStrandsAgent({ url })` instead of constructing a bare `HttpAgent`. + +### Transport Helpers + +- `addStrandsExpressEndpoint(app, agent, { path })` — Express analogue of `add_strands_fastapi_endpoint`. +- `createStrandsApp(agent, { path, pingPath, capabilitiesPath, capabilities, corsOrigin })` — bootstraps an Express app with permissive CORS and optional ping / capabilities routes. +- `addPing(app, path)` — `GET /ping` returning `{ status: "healthy" }`. +- `addCapabilities(app, path, { agent, overrides })` — `GET /capabilities` returning the advertised matrix; derives chunk flags from the live agent's `emitChunkEvents`. + +--- + +## Example Entry Points + +### Python (`python/examples/server/api/*.py`) The repository includes seven runnable FastAPI apps that showcase different features. Each example builds a Strands SDK agent, wraps it with `StrandsAgent`, and exposes it via `create_strands_app`: -| Module | Focus | Relevant Configuration | -| ----------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| `agentic_chat.py` | Baseline text generation with a frontend-only `change_background` tool. | No custom config; demonstrates automatic text streaming and frontend tool short-circuiting. | -| `agentic_chat_reasoning.py` | Reasoning/thinking event streaming with extended thinking models. | No custom config; demonstrates REASONING_* event emission. | -| `backend_tool_rendering.py` | Backend-executed tools (`render_chart`, `get_weather`). | Shows how tool results become `ToolCallResultEvent`s and can be rendered directly in the UI. | -| `shared_state.py` | Collaborative recipe editor that streams server-side state. | Uses `state_context_builder`, `state_from_args`, and `state_from_result` to keep the UI’s recipe object synchronized. | -| `agentic_generative_ui.py` | Predictive and reactive state updates for generative UI surfaces. | Demonstrates `PredictStateMapping`, `custom_result_handler` emitting `StateDeltaEvent`s, and the `stop_streaming_after_result` flag. | -| `agentic_chat_multimodal.py` | Multimodal image/document analysis with vision-capable model. | No custom config; demonstrates automatic multimodal content conversion. | -| `human_in_the_loop.py` | Human-in-the-loop confirmation flow with frontend tools. | Demonstrates frontend tool invocation and confirmation actions. | +| Module | Focus | Relevant Configuration | +| ---------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `agentic_chat.py` | Baseline text generation with a frontend-only `change_background` tool. | No custom config; demonstrates automatic text streaming and frontend tool short-circuiting. | +| `agentic_chat_reasoning.py` | Reasoning/thinking event streaming with extended thinking models. | No custom config; demonstrates REASONING\_\* event emission. | +| `backend_tool_rendering.py` | Backend-executed tools (`render_chart`, `get_weather`). | Shows how tool results become `ToolCallResultEvent`s and can be rendered directly in the UI. | +| `shared_state.py` | Collaborative recipe editor that streams server-side state. | Uses `state_context_builder`, `state_from_args`, and `state_from_result` to keep the UI's recipe object synchronized. | +| `agentic_generative_ui.py` | Predictive and reactive state updates for generative UI surfaces. | Demonstrates `PredictStateMapping`, `custom_result_handler` emitting `StateDeltaEvent`s, and the `stop_streaming_after_result` flag. | +| `agentic_chat_multimodal.py` | Multimodal image/document analysis with vision-capable model. | No custom config; demonstrates automatic multimodal content conversion. | +| `human_in_the_loop.py` | Human-in-the-loop confirmation flow with frontend tools. | Demonstrates frontend tool invocation and confirmation actions. | -These examples double as integration tests: they exercise every built-in hook so regressions surface quickly during manual QA. +### TypeScript (`typescript/examples/server/api/*.ts`) + +The TypeScript package ships the same seven Python examples under the matching filenames (`agentic-chat.ts`, `agentic-chat-reasoning.ts`, `agentic-chat-multimodal.ts`, `backend-tool-rendering.ts`, `shared-state.ts`, `agentic-generative-ui.ts`, `human-in-the-loop.ts`) plus one TypeScript-only addition: + +| Module | Focus | +| ------------------------------- | ------------------------------------------------------------------ | +| `tool-based-generative-ui.ts` | Frontend-rendered tool (haiku card) auto-registered as a proxy tool — exercises the `TOOL_CALL_*` stream the dojo's `tool_based_generative_ui` page consumes. No Python equivalent. | + +Each file is self-contained and can be run standalone (`pnpm ` from `examples/`). `examples/server/server.ts` is a "dojo" that mounts all eight at the paths the Python reference server uses, so both implementations can be driven by the same curl payloads. + +Both example sets double as integration tests: they exercise every built-in hook so regressions surface quickly during manual QA. --- @@ -170,7 +242,7 @@ These examples double as integration tests: they exercise every built-in hook so | Strands Signal | Adapter Reaction | AG-UI Consumer Impact | | ----------------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | | `stream_async` yields `{"data": ...}` | Emit text start/content/end | Updates conversational transcript incrementally. | -| `stream_async` yields `{"reasoningText": ..., "reasoning": true}` | Emit REASONING_* events | Displays model's reasoning/thinking process in UI. | +| `stream_async` yields `{"reasoningText": ..., "reasoning": true}` | Emit REASONING\_\* events | Displays model's reasoning/thinking process in UI. | | `stream_async` yields `{"reasoningRedactedContent": ...}` | Emit `ReasoningEncryptedValueEvent` with base64 payload | Handles encrypted reasoning content for models that redact thinking. | | `current_tool_use` announced | Emit tool call events, optional PredictState/state snapshots | Shows tool invocation cards and, when configured, optimistic UI updates. | | `toolResult` packaged within `message.content[].toolResult` | Emit `ToolCallResultEvent`, tool result hooks, optional halt | Renders backend tool outputs and state changes without additional frontend logic. | @@ -179,14 +251,17 @@ These examples double as integration tests: they exercise every built-in hook so | Stream sends `complete` or adapter decides to halt | Close text/reasoning envelopes and emit `RunFinishedEvent` | Signals the UI that the run ended; frontends may start follow-up runs or show idle states. | | Exceptions anywhere in the stack | Emit `RunErrorEvent` with the exception message | Frontend surfaces the failure and can offer retries. | +The TypeScript adapter maps the equivalent SDK-typed events (`modelContentBlockDeltaEvent`, `toolUseBlock`, `afterToolCallEvent`, `beforeNodeCallEvent`, `afterNodeCallEvent`, `multiAgentHandoffEvent`) to the same AG-UI events. + --- ## Deployment & Runtime Characteristics -- **HTTP/SSE transport**: The adapter currently supports only HTTP POST requests plus streaming responses. Longer-lived transports (WebSockets, queues) are not part of the implemented surface. -- **Per-thread agent caching**: The transport layer is stateless (plain HTTP POST), but `StrandsAgent` caches `strands.Agent` instances per thread to preserve conversation context across requests. -- **Model compatibility**: The examples use `strands.models.gemini.GeminiModel`, but `StrandsAgent` works with any `strands.Agent` configured with compatible tools and prompts because it only relies on `stream_async`. +- **HTTP/SSE transport**: Both adapters support HTTP POST plus streaming responses. Longer-lived transports (WebSockets, queues) are not part of the implemented surface. +- **Per-thread agent caching**: The transport layer is stateless (plain HTTP POST), but `StrandsAgent` caches Strands `Agent` instances per thread to preserve conversation context across requests. +- **Model compatibility**: The examples use `strands.models.gemini.GeminiModel` (Python) and Bedrock (TypeScript), but `StrandsAgent` works with any Strands-compatible model because it only relies on the streaming interface. - **Error isolation**: Failures inside tool hooks (`state_from_args`, etc.) are swallowed so the main run can continue. Only uncaught exceptions in the core loop trigger `RunErrorEvent`. +- **Amazon Bedrock AgentCore**: Both adapters support the AgentCore contract (`/invocations` POST + `/ping` GET on port 8080). --- @@ -194,8 +269,8 @@ These examples double as integration tests: they exercise every built-in hook so The AWS Strands integration adapts the Strands SDK to the AG-UI protocol by: -1. Wrapping `strands.Agent.stream_async` with `StrandsAgent`, which understands AG-UI events, tool semantics, and shared-state conventions. -2. Exposing a trivial FastAPI transport layer that handles encoding and CORS while remaining stateless. +1. Wrapping the Strands `Agent` streaming interface with `StrandsAgent`, which understands AG-UI events, tool semantics, and shared-state conventions. +2. Exposing a trivial transport layer (FastAPI for Python, Express for TypeScript) that handles encoding and CORS while remaining stateless. 3. Letting any existing AG-UI HTTP client connect directly to the endpoint—no Strands-specific frontend package is required. -All current behavior lives in `integrations/aws-strands/python/src/ag_ui_strands`. There are no hidden services or background workers; what is described above is the complete, production-ready implementation that powers today’s Strands integration. +All behavior lives in `integrations/aws-strands/python/src/ag_ui_strands` and `integrations/aws-strands/typescript/src`. There are no hidden services or background workers; what is described above is the complete, production-ready implementation that powers today's Strands integration. diff --git a/integrations/aws-strands/typescript/README.md b/integrations/aws-strands/typescript/README.md index 1a046f451b..ca726084c2 100644 --- a/integrations/aws-strands/typescript/README.md +++ b/integrations/aws-strands/typescript/README.md @@ -1,13 +1,309 @@ -# Strands Integration (TypeScript) +# AWS Strands Integration for AG-UI (TypeScript) -TypeScript client for the Strands integration using OpenAI models. +This package exposes a lightweight wrapper that lets any `@strands-agents/sdk` `Agent` speak the AG-UI protocol. It mirrors the developer experience of the other integrations: give us a Strands agent instance, plug it into `StrandsAgent`, and wire it to Express via `createStrandsApp` (or `addStrandsExpressEndpoint`). -## Usage +## Prerequisites -```typescript -import { AWSStrandsAgent } from "@ag-ui/aws-strands"; +- Node.js 18+ +- `pnpm` (recommended) or `npm` +- A Strands-compatible model key (e.g., AWS credentials for Bedrock, `OPENAI_API_KEY` for OpenAI) -const agent = new AWSStrandsAgent({ url: "http://localhost:8000" }); +## Quick Start + +The `examples/` package ships a "dojo" server that mounts every demo on a +single port, plus seven standalone servers — one per feature — that you can +run independently. + +```bash +# from the repo root +pnpm install +pnpm --filter @ag-ui/aws-strands build + +cd integrations/aws-strands/typescript/examples +pnpm dojo # all examples at http://localhost:8002 +``` + +Or run any single example on its own port (default `8000`): + +```bash +pnpm agentic-chat +pnpm agentic-chat-reasoning +pnpm agentic-chat-multimodal +pnpm backend-tool-rendering +pnpm shared-state +pnpm agentic-generative-ui +pnpm human-in-the-loop +``` + +The dojo exposes: + +| Route | Description | +| -------------------------- | ------------------------------------------------------------------------ | +| `/agentic-chat` | Baseline chat; frontend tools auto-registered from `RunAgentInput.tools` | +| `/agentic-chat-reasoning` | Reasoning / thinking event streaming | +| `/agentic-chat-multimodal` | Multimodal image / document analysis | +| `/backend-tool-rendering` | Backend-executed tools (`get_weather`, `render_chart`) | +| `/shared-state` | Shared recipe state (`stateFromArgs`) | +| `/agentic-generative-ui` | Async-generator tool streams `STATE_SNAPSHOT`s + `PredictState` | +| `/human-in-the-loop` | Frontend proxy tool with halt-after-call | + +Each standalone file under `examples/server/api/*.ts` follows the same pattern: build a Strands `Agent`, wrap it in a `StrandsAgent`, hand it to `createStrandsApp`, listen. + +## Architecture Overview + +The integration has three main layers: + +- **StrandsAgent** – wraps `Agent.stream()` from `@strands-agents/sdk`. It translates Strands streaming events into AG-UI events (text chunks, tool calls, PredictState, snapshots, reasoning/thinking, multi-agent steps, etc.). +- **Configuration** – `StrandsAgentConfig` + `ToolBehavior` + `PredictStateMapping` let you describe tool-specific quirks declaratively (skip message snapshots, emit state, stream args, etc.). +- **Transport helpers** – `createStrandsApp` and `addStrandsExpressEndpoint` expose the agent via SSE. They are thin shells over the shared `@ag-ui/encoder` `EventEncoder`. Imported from `@ag-ui/aws-strands/server` — kept off the main entry so client-side bundlers (Next.js, Vite) don't pull Express into the browser graph. + +See [../ARCHITECTURE.md](../ARCHITECTURE.md) for diagrams and a deeper dive. + +## Key Files + +| File | Description | +| -------------------------- | ------------------------------------------------------------------------------- | +| `src/agent.ts` | Core wrapper translating Strands streams into AG-UI events | +| `src/config.ts` | Config primitives (`StrandsAgentConfig`, `ToolBehavior`, `PredictStateMapping`) | +| `src/server.ts` | `createStrandsApp` + Express transport (subpath: `@ag-ui/aws-strands/server`) | +| `src/endpoint.ts` | Express endpoint helpers (used by `server.ts`) | +| `src/utils.ts` | Multimodal content conversion | +| `src/client-proxy-tool.ts` | Dynamic frontend tool registration/deregistration | +| `examples/server/api/*.ts` | Ready-to-run demo apps | + +## Amazon Bedrock AgentCore Considerations + +If you are planning to deploy your agent into Amazon Bedrock AgentCore (AC), please note that AC expects the following: + +- The server is running on port 8080. +- The path `/invocations - POST` is implemented and can be used for interacting with the agent. +- The path `/ping - GET` is implemented and can be used for verifying that the agent is operational and ready to handle requests. + +To implement the paths mentioned above, you can use the helper function `createStrandsApp` and pass the agent interaction path and the ping path as shown below: + +```ts +const app = await createStrandsApp(aguiAgent, { + path: "/invocations", + pingPath: "/ping", +}); +app.listen(8080); +``` + +You can also use the helper functions `addStrandsExpressEndpoint` and `addPing` for adding the mentioned paths to an Express app that you are creating separately: + +```ts +import express from "express"; +import cors from "cors"; +import { addStrandsExpressEndpoint, addPing } from "@ag-ui/aws-strands/server"; + +const app = express(); +app.use(cors()); +app.use(express.json({ limit: "50mb" })); +addStrandsExpressEndpoint(app, aguiAgent, { path: "/invocations" }); +addPing(app, "/ping"); +app.listen(8080); +``` + +Requests to the AC endpoint must be authenticated. You can configure your agent runtime to accept JWT bearer tokens (via Amazon Cognito) or use SigV4. See [Set up authentication](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-agui.html) in the AgentCore documentation. + +For details on how AgentCore handles AG-UI requests, event streaming, and error formatting, see the [AG-UI protocol contract](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-agui-protocol-contract.html). + +To deploy, use the [AgentCore Starter Toolkit](https://github.com/awslabs/bedrock-agentcore-starter-toolkit): + +```bash +pip install bedrock-agentcore-starter-toolkit +agentcore configure -e my_agui_server.ts --protocol AGUI +agentcore deploy +``` + +For the complete deployment walkthrough, see [Deploy AG-UI servers in AgentCore Runtime](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-agui.html). + +## Supported AG-UI Events + +The integration supports the following AG-UI event families: + +- **Lifecycle**: `RUN_STARTED`, `RUN_FINISHED`, `RUN_ERROR` +- **Text streaming**: `TEXT_MESSAGE_START`, `TEXT_MESSAGE_CONTENT`, `TEXT_MESSAGE_END` (optionally collapsed into `TEXT_MESSAGE_CHUNK` via `StrandsAgentConfig.emitChunkEvents`) +- **Reasoning**: `REASONING_*` events for models with extended thinking (`REASONING_MESSAGE_CHUNK` when `emitChunkEvents` is on) +- **Tool calls**: `TOOL_CALL_START`, `TOOL_CALL_ARGS`, `TOOL_CALL_END`, `TOOL_CALL_RESULT` (or `TOOL_CALL_CHUNK` with `emitChunkEvents`) +- **State management**: `STATE_SNAPSHOT` +- **Multi-agent**: `STEP_STARTED`, `STEP_FINISHED`, and `MultiAgentHandoff` custom events +- **Generative UI**: `PredictState` custom events for optimistic UI updates +- **Multimodal**: Image, document, and video content in user messages (converted to Strands ContentBlock format) + +The adapter advertises its full event / feature matrix at GET +`/capabilities` (enabled by default; override via `createStrandsApp({ capabilitiesPath, capabilities })` or mount manually with `addCapabilities(app, path, overrides)`). + +## Passing tools to the Agent + +The adapter clones the template `Agent`'s `tools` array onto every per-thread +clone. That means whatever the Strands SDK has resolved into `agent.tools` at +construction time is what the model sees — including for `McpClient` +instances. If you pass an **unconnected** `McpClient` directly, its tools +won't be in the resolved list and the model can't call them. + +Connect MCP clients first and spread the resolved tools into `tools`: + +```ts +import { Agent } from "@strands-agents/sdk"; +import { McpClient } from "@strands-agents/sdk/mcp"; + +const spellbook = new McpClient({ /* transport config */ }); +await spellbook.connect(); +const mcpTools = await spellbook.listTools(); + +const agent = new Agent({ + model: "anthropic.claude-sonnet-4-5-20250929-v1:0", + tools: [ + ...mcpTools, + myLocalTool, + ], +}); + +const aguiAgent = new StrandsAgent({ agent }); +``` + +The adapter logs a warning at construction time if it spots an entry in +`tools` that looks like an unconnected client (has a `.connect()` method but +no `.name`). + +## Human-in-the-loop interrupts + +Two complementary patterns are supported: + +- **Frontend tools.** The `/human-in-the-loop` example declares + `generate_task_steps` on the frontend via `useHumanInTheLoop` — the adapter + auto-registers it as a proxy tool, halts the run after the proxy resolves, + and hands control back to the UI for approval. +- **Native Strands interrupts (SDK 1.1.0+).** Backend hooks and tools can call + `event.interrupt(...)` / `context.interrupt(...)` to raise a + `stopReason: 'interrupt'`. The adapter forwards the outstanding interrupts + on `RUN_FINISHED`: + + ```json + { + "type": "RUN_FINISHED", + "outcome": { + "type": "interrupt", + "interrupts": [ + { "id": "...", "reason": "...", "metadata": { "strandsName": "..." } } + ] + } + } + ``` + + The next `RunAgentInput` carries `resume[]` entries keyed by those `id`s. + The adapter converts each entry into a Strands `InterruptResponseContent` + (forwarding `payload` for `resolved` and `{ status: "cancelled" }` for + `cancelled`) and hands them straight to `agent.stream(...)`. Unknown + `interruptId`s still short-circuit with + `RUN_ERROR { code: "UNKNOWN_INTERRUPT" }` per + [interrupts.mdx rule 4](https://docs.ag-ui.com/concepts/interrupts). + +## Reasoning / extended thinking + +The `/agentic-chat-reasoning` demo only emits `REASONING_*` events when the +underlying Strands model is configured with thinking / reasoning params. The +default `BedrockModel(...)` without `additional_request_fields` returns plain +text; for Claude extended thinking, configure the model like so: + +```ts +import { BedrockModel } from "@strands-agents/sdk/models/bedrock"; + +const model = new BedrockModel({ + modelId: "global.anthropic.claude-sonnet-4-6", + additionalRequestFields: { + thinking: { type: "enabled", budget_tokens: 5000 }, + }, +}); +``` + +## Install + +```bash +pnpm add @ag-ui/aws-strands @strands-agents/sdk @ag-ui/core @ag-ui/encoder +# Server-side helpers (createStrandsApp / addStrandsExpressEndpoint) require express + cors: +pnpm add express cors +pnpm add -D @types/express @types/cors +# @modelcontextprotocol/sdk is loaded unconditionally by @strands-agents/sdk +# — required at runtime even for agents that don't use MCP: +pnpm add @modelcontextprotocol/sdk +``` + +## Server: Expose a Strands Agent via AG-UI + +```ts +import { Agent } from "@strands-agents/sdk"; +import { StrandsAgent } from "@ag-ui/aws-strands"; +import { createStrandsApp } from "@ag-ui/aws-strands/server"; + +// `model` accepts either a Bedrock model ID string or a constructed +// Model instance (e.g. BedrockModel / AnthropicModel / OpenAIResponsesModel). +// Omitting it uses Strands' current Bedrock default. +const strandsAgent = new Agent({ + systemPrompt: "You are a helpful assistant.", + tools: [], +}); + +const aguiAgent = new StrandsAgent({ + agent: strandsAgent, + name: "MyAgent", + description: "A Strands agent exposed via AG-UI", +}); + +const app = await createStrandsApp(aguiAgent, { path: "/invocations" }); +app.listen(8000); +``` + +## Configuration + +```ts +import { + StrandsAgent, + type StrandsAgentConfig, + type ToolBehavior, +} from "@ag-ui/aws-strands"; + +const config: StrandsAgentConfig = { + toolBehaviors: { + set_recipe: { + stateFromArgs: async (ctx) => ({ recipe: ctx.toolInput }), + predictState: [ + { stateKey: "recipe", tool: "set_recipe", toolArgument: "data" }, + ], + }, + render_chart: { + stopStreamingAfterResult: true, + }, + }, + sessionManagerProvider: async (input) => { + // Optional: vend a SessionManager per-thread from your own state store. + return undefined; + }, + stateContextBuilder: (input, prompt) => { + // Optional: decorate the outgoing prompt with any server-side state. + return prompt; + }, +}; + +const agent = new StrandsAgent({ agent: strandsAgent, name: "x", config }); +``` + +## Low-Level Transport + +If you have an existing Express app, mount the endpoint directly instead of +using `createStrandsApp`: + +```ts +import express from "express"; +import cors from "cors"; +import { addStrandsExpressEndpoint, addPing } from "@ag-ui/aws-strands/server"; + +const app = express(); +app.use(cors()); +app.use(express.json({ limit: "50mb" })); +addStrandsExpressEndpoint(app, aguiAgent, { path: "/invocations" }); +addPing(app, "/ping"); ``` ## Development @@ -15,5 +311,5 @@ const agent = new AWSStrandsAgent({ url: "http://localhost:8000" }); ```bash pnpm install pnpm build -pnpm dev +pnpm test ``` diff --git a/integrations/aws-strands/typescript/examples/package.json b/integrations/aws-strands/typescript/examples/package.json new file mode 100644 index 0000000000..8a5ad9b70c --- /dev/null +++ b/integrations/aws-strands/typescript/examples/package.json @@ -0,0 +1,35 @@ +{ + "private": true, + "name": "@ag-ui/aws-strands-examples", + "version": "0.0.0", + "description": "Runnable AG-UI + Strands examples. Each file under server/api/ is a standalone server; server.ts mounts them all at once for the dojo.", + "scripts": { + "dojo": "tsx server/server.ts", + "agentic-chat": "tsx server/api/agentic-chat.ts", + "agentic-chat-multimodal": "tsx server/api/agentic-chat-multimodal.ts", + "agentic-chat-reasoning": "tsx server/api/agentic-chat-reasoning.ts", + "agentic-generative-ui": "tsx server/api/agentic-generative-ui.ts", + "backend-tool-rendering": "tsx server/api/backend-tool-rendering.ts", + "human-in-the-loop": "tsx server/api/human-in-the-loop.ts", + "shared-state": "tsx server/api/shared-state.ts", + "tool-based-generative-ui": "tsx server/api/tool-based-generative-ui.ts" + }, + "dependencies": { + "@ag-ui/aws-strands": "workspace:*", + "@ag-ui/core": "workspace:*", + "@ag-ui/encoder": "workspace:*", + "@strands-agents/sdk": "^1.1.0", + "@modelcontextprotocol/sdk": "^1.29.0", + "cors": "^2.8.5", + "express": "^5.0.0", + "openai": "^6.0.0", + "zod": "^4.4.3" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/node": "^20.11.19", + "tsx": "^4.20.6", + "typescript": "^5.3.3" + } +} diff --git a/integrations/aws-strands/typescript/examples/server/api/agentic-chat-multimodal.ts b/integrations/aws-strands/typescript/examples/server/api/agentic-chat-multimodal.ts new file mode 100644 index 0000000000..7d3aac5132 --- /dev/null +++ b/integrations/aws-strands/typescript/examples/server/api/agentic-chat-multimodal.ts @@ -0,0 +1,38 @@ +/** + * Agentic Chat with Multimodal support for AWS Strands (TypeScript). + * + * Demonstrates multimodal message handling. When the user uploads an image, + * the adapter converts AG-UI InputContent to Strands ContentBlock format + * and passes it to the vision-capable model. + */ + +import { Agent } from "@strands-agents/sdk"; +import { StrandsAgent } from "@ag-ui/aws-strands"; +import { createStrandsApp } from "@ag-ui/aws-strands/server"; +import { createModel } from "../model-factory"; + +async function main(): Promise { + const strandsAgent = new Agent({ + model: await createModel(), + systemPrompt: ` + You are a helpful assistant that can analyze images and documents. + When the user shares an image, describe what you see in detail. + When the user shares a document, summarize its contents. + Always be descriptive and specific about visual content. + `, + }); + + const aguiAgent = new StrandsAgent({ + agent: strandsAgent, + name: "agentic_chat_multimodal", + description: "Conversational Strands agent with multimodal content support", + }); + + const app = await createStrandsApp(aguiAgent, { path: "/" }); + const port = Number(process.env.PORT ?? 8000); + app.listen(port, () => { + console.log(`Listening on http://localhost:${port}`); + }); +} + +void main(); diff --git a/integrations/aws-strands/typescript/examples/server/api/agentic-chat-reasoning.ts b/integrations/aws-strands/typescript/examples/server/api/agentic-chat-reasoning.ts new file mode 100644 index 0000000000..ad75fae975 --- /dev/null +++ b/integrations/aws-strands/typescript/examples/server/api/agentic-chat-reasoning.ts @@ -0,0 +1,39 @@ +/** + * Agentic Chat with Reasoning example for AWS Strands (TypeScript). + * + * Demonstrates reasoning/thinking event streaming. When the underlying model + * supports extended thinking, the adapter emits REASONING_* events that the + * frontend can display as a "thinking" indicator. + */ + +import { Agent } from "@strands-agents/sdk"; +import { StrandsAgent } from "@ag-ui/aws-strands"; +import { createStrandsApp } from "@ag-ui/aws-strands/server"; +import { createModel } from "../model-factory"; + +async function main(): Promise { + const strandsAgent = new Agent({ + model: await createModel(), + systemPrompt: ` + You are a helpful assistant that thinks through problems step by step. + When the user greets you, always greet them back. Your greeting should always start with "Hello". + Your greeting should also always ask (exact wording) "how can I assist you?" + When reasoning about a problem, break it down into clear steps before answering. + `, + }); + + const aguiAgent = new StrandsAgent({ + agent: strandsAgent, + name: "agentic_chat_reasoning", + description: + "Conversational Strands agent with reasoning/thinking event streaming", + }); + + const app = await createStrandsApp(aguiAgent, { path: "/" }); + const port = Number(process.env.PORT ?? 8000); + app.listen(port, () => { + console.log(`Listening on http://localhost:${port}`); + }); +} + +void main(); diff --git a/integrations/aws-strands/typescript/examples/server/api/agentic-chat.ts b/integrations/aws-strands/typescript/examples/server/api/agentic-chat.ts new file mode 100644 index 0000000000..00bd24d8fb --- /dev/null +++ b/integrations/aws-strands/typescript/examples/server/api/agentic-chat.ts @@ -0,0 +1,38 @@ +/** + * Agentic Chat example for AWS Strands (TypeScript). + * + * Simple conversational agent. Frontend tools sent in RunAgentInput.tools + * are automatically registered as proxy tools so no server-side @tool + * definition is needed — the LLM calls them and the browser executes them. + */ + +import { Agent } from "@strands-agents/sdk"; +import { StrandsAgent } from "@ag-ui/aws-strands"; +import { createStrandsApp } from "@ag-ui/aws-strands/server"; +import { createModel } from "../model-factory"; + +async function main(): Promise { + const strandsAgent = new Agent({ + model: await createModel(), + systemPrompt: ` + You are a helpful assistant. + When the user greets you, always greet them back. Your greeting should always start with "Hello". + Your greeting should also always ask (exact wording) "how can I assist you?" + `, + }); + + const aguiAgent = new StrandsAgent({ + agent: strandsAgent, + name: "agentic_chat", + description: "Conversational Strands agent with AG-UI streaming", + }); + + const app = await createStrandsApp(aguiAgent, { path: "/" }); + const port = Number(process.env.PORT ?? 8000); + app.listen(port, () => { + // eslint-disable-next-line no-console + console.log(`Listening on http://localhost:${port}`); + }); +} + +void main(); diff --git a/integrations/aws-strands/typescript/examples/server/api/agentic-generative-ui.ts b/integrations/aws-strands/typescript/examples/server/api/agentic-generative-ui.ts new file mode 100644 index 0000000000..069fbd12ba --- /dev/null +++ b/integrations/aws-strands/typescript/examples/server/api/agentic-generative-ui.ts @@ -0,0 +1,177 @@ +/** + * Agentic Generative UI example for AWS Strands (TypeScript). + * + * Demonstrates streaming agent state updates to the frontend for real-time + * UI rendering. Uses ONLY the canonical Strands + @ag-ui/aws-strands surface: + * + * - `predictState` mapping streams the predicted `steps` to the FE while + * the LLM is still emitting `plan_task_steps` arguments. + * - The tool itself is an async generator. Each `yield` of `{ state: {...} }` + * becomes a Strands `ToolStreamEvent` which the @ag-ui/aws-strands adapter + * translates into an AG-UI `StateSnapshotEvent`. + * - The FINAL value returned by the generator is the tool's result. + * + * The agent never emits AG-UI events directly. State updates flow through + * Strands' native streaming mechanism, mirroring the Python reference + * (integrations/aws-strands/python/examples/server/api/agentic_generative_ui.py). + */ + +import { Agent, tool } from "@strands-agents/sdk"; +import { z } from "zod"; +import { StrandsAgent, type StrandsAgentConfig } from "@ag-ui/aws-strands"; +import { createStrandsApp } from "@ag-ui/aws-strands/server"; +import { createModel } from "../model-factory"; + +const stepSchema = z.object({ + description: z + .string() + .describe("Gerund phrase describing the action, e.g. 'Sketching layout'"), + status: z + .string() + .default("pending") + .describe("Must be 'pending' when proposed"), +}); + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * `plan_task_steps` as an async-generator tool. Each yielded `{ state: {...} }` + * becomes a Strands `ToolStreamEvent` that the adapter translates into an + * AG-UI `StateSnapshotEvent`. The final return value is the tool result. + */ +const planTaskSteps = tool({ + name: "plan_task_steps", + description: + "Plan the concrete steps required to accomplish a task and walk each step from 'pending' through 'in_progress' to 'completed' so the UI sees progress in real time.", + inputSchema: z.object({ + task: z + .string() + .describe("Brief description of what the user wants to achieve"), + context: z + .string() + .default("") + .describe("Optional additional instructions"), + steps: z + .array(stepSchema) + .describe("Ordered list of pending steps in gerund form"), + }), + callback: async function* ({ task, context, steps }) { + const normalized = (steps ?? []).map( + (s: { description: string; status?: string }) => ({ + description: s.description, + status: s.status || "pending", + }), + ); + const workingSteps = + normalized.length > 0 + ? normalized + : fallbackSteps(task || "the task", context); + const mutable = workingSteps.map((s) => ({ ...s })); + + // Re-confirm the canonical shape now that the tool body owns the state + // (predictState will already have streamed something similar from args). + yield { state: { steps: mutable.map((s) => ({ ...s })) } }; + + for (let i = 0; i < mutable.length; i++) { + await sleep(300 + Math.random() * 500); + mutable[i]!.status = "in_progress"; + yield { state: { steps: mutable.map((s) => ({ ...s })) } }; + + await sleep(400 + Math.random() * 600); + mutable[i]!.status = "completed"; + yield { state: { steps: mutable.map((s) => ({ ...s })) } }; + } + + return { task, context, steps: mutable }; + }, +}); + +function fallbackSteps( + task: string, + context: string, +): { description: string; status: string }[] { + let count = 6; + for (const token of (context ?? "").split(/\s+/)) { + if (/^\d+$/.test(token)) { + count = Math.max(4, Math.min(10, parseInt(token, 10))); + break; + } + } + const templates = [ + "Clarifying goals for {task}", + "Gathering resources for {task}", + "Preparing workspace for {task}", + "Executing core work on {task}", + "Reviewing results for {task}", + "Wrapping up {task}", + "Documenting learnings from {task}", + "Celebrating completion of {task}", + ]; + const plan: { description: string; status: string }[] = []; + for (let i = 0; i < count; i++) { + const raw = templates[i % templates.length]!.replace("{task}", task).trim(); + const description = raw.charAt(0).toUpperCase() + raw.slice(1); + plan.push({ description, status: "pending" }); + } + return plan; +} + +async function main() { + const config: StrandsAgentConfig = { + stateContextBuilder: (input, prompt) => { + const state = (input.state ?? {}) as Record; + const steps = state.steps; + if (steps) { + return ( + "A plan is already in progress. NEVER call plan_task_steps again unless the user explicitly " + + "asks to restart. Discuss progress or ask clarifying questions instead.\n\n" + + `Current steps:\n${JSON.stringify(steps, null, 2)}\n\nUser: ${prompt}` + ); + } + return prompt; + }, + toolBehaviors: { + plan_task_steps: { + predictState: [ + { stateKey: "steps", tool: "plan_task_steps", toolArgument: "steps" }, + ], + stateFromResult: async (ctx) => { + const result = (ctx.resultData ?? {}) as { steps?: unknown[] }; + return result.steps ? { steps: result.steps } : null; + }, + }, + }, + }; + + const strandsAgent = new Agent({ + model: await createModel(), + tools: [planTaskSteps], + systemPrompt: `You are an energetic project assistant who decomposes user goals into action plans. + +Planning rules: +1. When the user asks for help with a task or making a plan, call plan_task_steps exactly once. +2. Do NOT call plan_task_steps again unless the user explicitly says to restart. +3. Generate 4-6 concise steps in gerund form (e.g., "Setting up repo", "Testing prototype") with status "pending". +4. After the tool call, send a short confirmation (<= 2 sentences) plus one emoji. +5. If the user is just chatting, respond conversationally without calling the tool. +6. If a plan already exists, reference the current steps instead of creating a new plan. +`, + }); + + const aguiAgent = new StrandsAgent({ + agent: strandsAgent, + name: "agentic_generative_ui", + description: "AWS Strands agent with generative UI and state streaming", + config, + }); + + const app = await createStrandsApp(aguiAgent, { path: "/" }); + const port = Number(process.env.PORT ?? 8000); + app.listen(port, () => { + console.log(`Listening on http://localhost:${port}`); + }); +} + +void main(); diff --git a/integrations/aws-strands/typescript/examples/server/api/backend-tool-rendering.ts b/integrations/aws-strands/typescript/examples/server/api/backend-tool-rendering.ts new file mode 100644 index 0000000000..14e34c5796 --- /dev/null +++ b/integrations/aws-strands/typescript/examples/server/api/backend-tool-rendering.ts @@ -0,0 +1,61 @@ +/** + * Backend Tool Rendering example for AWS Strands (TypeScript). + * + * Demonstrates backend-executed tools. Tool results flow through the + * adapter as `TOOL_CALL_RESULT` events that the frontend can render + * directly (e.g. charts, weather cards) without extra plumbing. + */ + +import { Agent, tool } from "@strands-agents/sdk"; +import { z } from "zod"; +import { StrandsAgent } from "@ag-ui/aws-strands"; +import { createStrandsApp } from "@ag-ui/aws-strands/server"; +import { createModel } from "../model-factory"; + +const getWeather = tool({ + name: "get_weather", + description: "Gets the current weather for a given city.", + inputSchema: z.object({ + city: z.string().describe("The city to fetch weather for."), + }), + callback({ city }) { + return { + city, + temperatureC: 21, + conditions: "Sunny", + }; + }, +}); + +const renderChart = tool({ + name: "render_chart", + description: "Renders a chart for the given data series.", + inputSchema: z.object({ + title: z.string(), + points: z.array(z.object({ x: z.number(), y: z.number() })), + }), + callback(input) { + return { rendered: true, ...input }; + }, +}); + +async function main(): Promise { + const strandsAgent = new Agent({ + model: await createModel(), + systemPrompt: + "You are a helpful assistant. Use the tools to answer user questions, then narrate the result.", + tools: [getWeather, renderChart], + }); + + const aguiAgent = new StrandsAgent({ + agent: strandsAgent, + name: "backend_tool_rendering", + description: + "Strands agent that invokes backend tools and renders the results in the UI", + }); + + const app = await createStrandsApp(aguiAgent, { path: "/" }); + app.listen(Number(process.env.PORT ?? 8000)); +} + +void main(); diff --git a/integrations/aws-strands/typescript/examples/server/api/human-in-the-loop.ts b/integrations/aws-strands/typescript/examples/server/api/human-in-the-loop.ts new file mode 100644 index 0000000000..aa497dbbf3 --- /dev/null +++ b/integrations/aws-strands/typescript/examples/server/api/human-in-the-loop.ts @@ -0,0 +1,67 @@ +/** + * Human-in-the-Loop example for AWS Strands (TypeScript). + * + * The `generate_task_steps` tool is declared on the frontend via + * `useHumanInTheLoop`. The @ag-ui/aws-strands adapter auto-registers it as a + * proxy tool when `RunAgentInput.tools` arrives, so the backend does not + * register a native tool here — Strands invokes the proxy, the adapter halts + * the run after the proxy returns, the user reviews and approves the plan in + * the UI, and the tool result is fed back to the agent on the next turn. + * + * No backend tool stub. No agent-side AG-UI event emission. + * + * Mirrors the Python reference + * (integrations/aws-strands/python/examples/server/api/human_in_the_loop.py). + */ + +import { Agent } from "@strands-agents/sdk"; +import { StrandsAgent } from "@ag-ui/aws-strands"; +import { createStrandsApp } from "@ag-ui/aws-strands/server"; +import { createModel } from "../model-factory"; + +async function main(): Promise { + const strandsAgent = new Agent({ + model: await createModel(), + tools: [], + systemPrompt: `You are a task planning assistant specialized in creating clear, actionable step-by-step plans. + +**Your Primary Role:** +- Break down any user request into exactly 10 clear, actionable steps +- Generate steps that require human review and approval +- Execute only human-approved steps + +**When a user requests help with a task:** +1. ALWAYS use the \`generate_task_steps\` tool to create a breakdown (default to 10 steps unless told otherwise) +2. Each step must be: + - Brief (only a few words) + - In imperative form (e.g., "Dig hole", "Open door", "Mix ingredients") + - Clear and actionable + - Logically ordered from start to finish +3. Set all steps to "enabled" status initially +4. After the user reviews the plan: + - If accepted: Briefly confirm the plan (only include the approved steps) and proceed (don't repeat the steps). Do not ask for more clarifying information. + - If rejected: Ask what they'd like to change (don't call generate_task_steps again until they provide input) +5. When the user accepts the plan, "execute" the plan by repeating the approved steps in order as if you have just done them. Then let the user know you have completed the plan. + - example: if the user accepts the steps "Dig hole", "Open door", "Mix ingredients", you would respond with "Digging hole... Opening door... Mixing ingredients..." + +**Important:** +- NEVER call \`generate_task_steps\` twice in a row without user input +- NEVER repeat the list of steps in your response after calling the tool +- DO provide a brief, creative summary of how you would execute the approved steps +`, + }); + + const aguiAgent = new StrandsAgent({ + agent: strandsAgent, + name: "human_in_the_loop", + description: "AWS Strands agent with human-in-the-loop task planning", + }); + + const app = await createStrandsApp(aguiAgent, { path: "/" }); + const port = Number(process.env.PORT ?? 8000); + app.listen(port, () => { + console.log(`Listening on http://localhost:${port}`); + }); +} + +void main(); diff --git a/integrations/aws-strands/typescript/examples/server/api/shared-state.ts b/integrations/aws-strands/typescript/examples/server/api/shared-state.ts new file mode 100644 index 0000000000..813c08841e --- /dev/null +++ b/integrations/aws-strands/typescript/examples/server/api/shared-state.ts @@ -0,0 +1,83 @@ +/** + * Shared State example for AWS Strands (TypeScript) — a collaborative recipe + * editor. Shows how `stateContextBuilder`, `stateFromArgs`, and + * `stateFromResult` keep a shared object in sync between the server and the + * UI while the agent is streaming. + */ + +import { Agent, tool } from "@strands-agents/sdk"; +import { z } from "zod"; +import { StrandsAgent, type StrandsAgentConfig } from "@ag-ui/aws-strands"; +import { createStrandsApp } from "@ag-ui/aws-strands/server"; +import { createModel } from "../model-factory"; + +const recipeSchema = z.object({ + title: z.string(), + skillLevel: z.string(), + specialPreferences: z.array(z.string()), + cookingTime: z.string(), + ingredients: z.array( + z.object({ icon: z.string(), name: z.string(), amount: z.string() }), + ), + instructions: z.array(z.string()), + changes: z.string().default(""), +}); + +const generateRecipe = tool({ + name: "generate_recipe", + description: + "Using the existing (if any) ingredients and instructions, proceed with the recipe to finish it.", + inputSchema: z.object({ recipe: recipeSchema }), + callback() { + return "Recipe updated successfully"; + }, +}); + +const initialRecipe = { + title: "Make Your Recipe", + skillLevel: "Intermediate", + specialPreferences: [] as string[], + cookingTime: "45 min", + ingredients: [ + { icon: "🥕", name: "Carrots", amount: "3 large, grated" }, + { icon: "🌾", name: "All-Purpose Flour", amount: "2 cups" }, + ], + instructions: ["Preheat oven to 350°F (175°C)"], + changes: "", +}; + +async function main(): Promise { + const strandsAgent = new Agent({ + model: await createModel(), + systemPrompt: "You are a helpful recipe editor.", + tools: [generateRecipe], + }); + + const config: StrandsAgentConfig = { + stateContextBuilder: (input, prompt) => { + const state = (input.state ?? {}) as Record; + const recipe = state.recipe ?? initialRecipe; + return `Current recipe state:\n${JSON.stringify(recipe, null, 2)}\n\nUser request: ${prompt}\n\nPlease update the recipe by calling the registered tool.`; + }, + toolBehaviors: { + generate_recipe: { + stateFromArgs: async (ctx) => { + const args = ctx.toolInput as { recipe?: unknown }; + return args?.recipe ? { recipe: args.recipe } : null; + }, + }, + }, + }; + + const aguiAgent = new StrandsAgent({ + agent: strandsAgent, + name: "shared_state", + description: "Strands agent with shared recipe state", + config, + }); + + const app = await createStrandsApp(aguiAgent, { path: "/" }); + app.listen(Number(process.env.PORT ?? 8000)); +} + +void main(); diff --git a/integrations/aws-strands/typescript/examples/server/api/tool-based-generative-ui.ts b/integrations/aws-strands/typescript/examples/server/api/tool-based-generative-ui.ts new file mode 100644 index 0000000000..dfec02c80c --- /dev/null +++ b/integrations/aws-strands/typescript/examples/server/api/tool-based-generative-ui.ts @@ -0,0 +1,45 @@ +/** + * Tool-based Generative UI example for AWS Strands (TypeScript). + * + * The `generate_haiku` tool is declared on the frontend via `useFrontendTool` + * — the @ag-ui/aws-strands adapter auto-registers it as a proxy tool when + * `RunAgentInput.tools` arrives, so the backend does not register a native + * tool here. Strands invokes the proxy with the structured haiku args, the + * adapter halts the run after the proxy returns, and the browser renders the + * haiku card from the streamed `TOOL_CALL_*` events. + */ + +import { Agent } from "@strands-agents/sdk"; +import { StrandsAgent } from "@ag-ui/aws-strands"; +import { createStrandsApp } from "@ag-ui/aws-strands/server"; +import { createModel } from "../model-factory"; + +async function main(): Promise { + const strandsAgent = new Agent({ + model: await createModel(), + tools: [], + systemPrompt: `You are a creative haiku generator. + +When the user asks for a haiku, ALWAYS call the \`generate_haiku\` tool with: +- 3 lines of haiku in Japanese +- 3 lines of haiku translated to English +- One relevant image_name from the provided list +- A CSS gradient for the card background + +Do not respond with plain text — always use the tool.`, + }); + + const aguiAgent = new StrandsAgent({ + agent: strandsAgent, + name: "tool_based_generative_ui", + description: "AWS Strands haiku generator with frontend-rendered tool", + }); + + const app = await createStrandsApp(aguiAgent, { path: "/" }); + const port = Number(process.env.PORT ?? 8000); + app.listen(port, () => { + console.log(`Listening on http://localhost:${port}`); + }); +} + +void main(); diff --git a/integrations/aws-strands/typescript/examples/server/model-factory.ts b/integrations/aws-strands/typescript/examples/server/model-factory.ts new file mode 100644 index 0000000000..3d8725536c --- /dev/null +++ b/integrations/aws-strands/typescript/examples/server/model-factory.ts @@ -0,0 +1,82 @@ +/** + * Shared model factory for Strands TypeScript examples. + * + * Mirrors `python/examples/server/model_factory.py` field-for-field: same + * `MODEL_PROVIDER` env-var shape, same defaults, same model IDs. The CI dojo + * runner injects `OPENAI_BASE_URL=http://localhost:5555/v1` + a mock API key, + * so the default `openai` provider routes to the aimock server automatically. + * + * Supported providers: `openai` (default), `anthropic`, `gemini`, `bedrock`. + */ + +import type { Model } from "@strands-agents/sdk"; + +export async function createModel(): Promise { + const provider = (process.env.MODEL_PROVIDER ?? "openai").toLowerCase(); + + if (provider === "openai") { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error( + "OPENAI_API_KEY environment variable is required when MODEL_PROVIDER=openai. " + + "Set it in your .env file or environment.", + ); + } + const { OpenAIModel } = await import("@strands-agents/sdk/models/openai"); + // OPENAI_BASE_URL routes through aimock during e2e tests. The default + // Responses API surfaces fixture `reasoning` content for the + // `/agentic-chat-reasoning` demo. + const baseURL = process.env.OPENAI_BASE_URL; + return new OpenAIModel({ + apiKey, + modelId: process.env.MODEL_ID ?? "gpt-5.4", + params: { + reasoning: { effort: "medium", summary: "auto" }, + }, + ...(baseURL ? { clientConfig: { baseURL } } : {}), + }); + } + + if (provider === "anthropic") { + const apiKey = process.env.ANTHROPIC_API_KEY; + if (!apiKey) { + throw new Error( + "ANTHROPIC_API_KEY environment variable is required when MODEL_PROVIDER=anthropic. " + + "Set it in your .env file or environment.", + ); + } + const { AnthropicModel } = await import( + "@strands-agents/sdk/models/anthropic" + ); + return new AnthropicModel({ + apiKey, + modelId: process.env.MODEL_ID ?? "claude-sonnet-4-6", + }); + } + + if (provider === "gemini") { + const apiKey = process.env.GOOGLE_API_KEY; + if (!apiKey) { + throw new Error( + "GOOGLE_API_KEY environment variable is required when MODEL_PROVIDER=gemini. " + + "Set it in your .env file or environment.", + ); + } + const { GoogleModel } = await import("@strands-agents/sdk/models/google"); + return new GoogleModel({ + apiKey, + modelId: process.env.MODEL_ID ?? "gemini-2.5-flash", + }); + } + + if (provider === "bedrock") { + const { BedrockModel } = await import("@strands-agents/sdk"); + return new BedrockModel({ + modelId: process.env.MODEL_ID ?? "global.anthropic.claude-sonnet-4-6", + }); + } + + throw new Error( + `Unknown MODEL_PROVIDER: ${provider}. Supported: openai, anthropic, gemini, bedrock`, + ); +} diff --git a/integrations/aws-strands/typescript/examples/server/server.ts b/integrations/aws-strands/typescript/examples/server/server.ts new file mode 100644 index 0000000000..af7b4a42f9 --- /dev/null +++ b/integrations/aws-strands/typescript/examples/server/server.ts @@ -0,0 +1,294 @@ +/** + * Verification server: mounts every TS example on the same paths the Python + * reference server uses, so both implementations can be driven by the same + * curl payloads. + */ +import express from "express"; +import cors from "cors"; +import { Agent, tool } from "@strands-agents/sdk"; +import { z } from "zod"; +import { StrandsAgent, type StrandsAgentConfig } from "@ag-ui/aws-strands"; +import { + addStrandsExpressEndpoint, + addPing, + addCapabilities, +} from "@ag-ui/aws-strands/server"; +import { createModel } from "./model-factory"; + +function mountAgent( + app: express.Express, + path: string, + aguiAgent: StrandsAgent, +): void { + addStrandsExpressEndpoint(app, aguiAgent, { path }); + addStrandsExpressEndpoint(app, aguiAgent, { path: `${path}/` }); +} + +async function main(): Promise { + const app = express(); + app.use(cors({ origin: true, credentials: true })); + app.use(express.json({ limit: "50mb" })); + addPing(app, "/ping"); + addCapabilities(app, "/capabilities"); + + /* ---------------- agentic-chat ---------------- */ + const chatAgent = new Agent({ + model: await createModel(), + systemPrompt: ` + You are a helpful assistant. + When the user greets you, always greet them back. Your greeting should always start with "Hello". + Your greeting should also always ask (exact wording) "how can I assist you?" + `, + }); + mountAgent( + app, + "/agentic-chat", + new StrandsAgent({ + agent: chatAgent, + name: "agentic_chat", + description: "Conversational Strands agent with AG-UI streaming", + }), + ); + + /* ---------------- agentic-chat-reasoning ---------------- */ + const reasoningAgent = new Agent({ + model: await createModel(), + systemPrompt: ` + You are a helpful assistant that thinks through problems step by step. + When reasoning about a problem, break it down into clear steps before answering. + `, + }); + mountAgent( + app, + "/agentic-chat-reasoning", + new StrandsAgent({ + agent: reasoningAgent, + name: "agentic_chat_reasoning", + description: "Reasoning agent", + }), + ); + + /* ---------------- agentic-chat-multimodal ---------------- */ + const multimodalAgent = new Agent({ + model: await createModel(), + systemPrompt: + "You are a helpful assistant that can analyze images and documents. Describe images in detail.", + }); + mountAgent( + app, + "/agentic-chat-multimodal", + new StrandsAgent({ + agent: multimodalAgent, + name: "agentic_chat_multimodal", + description: "Multimodal chat", + }), + ); + + /* ---------------- backend-tool-rendering ---------------- */ + const getWeather = tool({ + name: "get_weather", + description: "Gets the current weather for a given city.", + inputSchema: z.object({ + city: z.string().describe("The city to fetch weather for."), + }), + callback: ({ city }) => ({ city, temperatureC: 21, conditions: "Sunny" }), + }); + const renderChart = tool({ + name: "render_chart", + description: "Renders a chart for the given data series.", + inputSchema: z.object({ + title: z.string(), + points: z.array(z.object({ x: z.number(), y: z.number() })), + }), + callback: (input) => ({ rendered: true, ...input }), + }); + const backendToolAgent = new Agent({ + model: await createModel(), + systemPrompt: + "You are a helpful assistant. Use the tools to answer user questions, then narrate the result.", + tools: [getWeather, renderChart], + }); + mountAgent( + app, + "/backend-tool-rendering", + new StrandsAgent({ + agent: backendToolAgent, + name: "backend_tool_rendering", + description: "Strands agent that invokes backend tools", + }), + ); + + /* ---------------- shared-state ---------------- */ + const recipeSchema = z.object({ + title: z.string(), + skillLevel: z.string(), + specialPreferences: z.array(z.string()), + cookingTime: z.string(), + ingredients: z.array( + z.object({ icon: z.string(), name: z.string(), amount: z.string() }), + ), + instructions: z.array(z.string()), + changes: z.string().default(""), + }); + const generateRecipe = tool({ + name: "generate_recipe", + description: "Produce a complete updated recipe.", + inputSchema: z.object({ recipe: recipeSchema }), + callback: () => "Recipe updated successfully", + }); + const sharedConfig: StrandsAgentConfig = { + stateContextBuilder: (input, prompt) => { + const state = (input.state ?? {}) as Record; + const recipe = state.recipe ?? {}; + return `Current recipe state:\n${JSON.stringify(recipe, null, 2)}\n\nUser request: ${prompt}\n\nPlease update the recipe by calling the registered tool.`; + }, + toolBehaviors: { + generate_recipe: { + stateFromArgs: async (ctx) => { + const args = ctx.toolInput as { recipe?: unknown }; + return args?.recipe ? { recipe: args.recipe } : null; + }, + }, + }, + }; + const sharedAgent = new Agent({ + model: await createModel(), + systemPrompt: "You are a helpful recipe editor.", + tools: [generateRecipe], + }); + mountAgent( + app, + "/shared-state", + new StrandsAgent({ + agent: sharedAgent, + name: "shared_state", + description: "Shared recipe state", + config: sharedConfig, + }), + ); + + /* ---------------- agentic-generative-ui ---------------- */ + const stepSchema = z.object({ + description: z.string(), + status: z.string().default("pending"), + }); + function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + const planTaskSteps = tool({ + name: "plan_task_steps", + description: "Plan the concrete steps required to accomplish a task.", + inputSchema: z.object({ + task: z.string(), + context: z.string().default(""), + steps: z.array(stepSchema), + }), + callback: async function* ({ task, context, steps }) { + const normalized = (steps ?? []).map( + (s: { description: string; status?: string }) => ({ + description: s.description, + status: s.status || "pending", + }), + ); + if (normalized.length === 0) { + return { task, context, steps: [] }; + } + yield { state: { steps: normalized.map((s) => ({ ...s })) } }; + for (let i = 0; i < normalized.length; i++) { + await sleep(100); + normalized[i]!.status = "in_progress"; + yield { state: { steps: normalized.map((s) => ({ ...s })) } }; + await sleep(100); + normalized[i]!.status = "completed"; + yield { state: { steps: normalized.map((s) => ({ ...s })) } }; + } + return { task, context, steps: normalized }; + }, + }); + const genuiConfig: StrandsAgentConfig = { + stateContextBuilder: (input, prompt) => { + const state = (input.state ?? {}) as Record; + const steps = state.steps; + if (steps) { + return `A plan is already in progress. NEVER call plan_task_steps again.\n\nCurrent steps:\n${JSON.stringify(steps, null, 2)}\n\nUser: ${prompt}`; + } + return prompt; + }, + toolBehaviors: { + plan_task_steps: { + predictState: [ + { stateKey: "steps", tool: "plan_task_steps", toolArgument: "steps" }, + ], + stateFromResult: async (ctx) => { + const result = (ctx.resultData ?? {}) as { steps?: unknown[] }; + return result.steps ? { steps: result.steps } : null; + }, + }, + }, + }; + const genuiAgent = new Agent({ + model: await createModel(), + tools: [planTaskSteps], + systemPrompt: + "You are an energetic project assistant. When the user asks for a plan, call plan_task_steps once with 4-6 gerund-form steps.", + }); + mountAgent( + app, + "/agentic-generative-ui", + new StrandsAgent({ + agent: genuiAgent, + name: "agentic_generative_ui", + description: "Generative UI agent", + config: genuiConfig, + }), + ); + + /* ---------------- human-in-the-loop ---------------- */ + const hitlAgent = new Agent({ + model: await createModel(), + tools: [], + systemPrompt: + "You are a task planner. Always use generate_task_steps exactly once with 10 imperative-form steps.", + }); + mountAgent( + app, + "/human-in-the-loop", + new StrandsAgent({ + agent: hitlAgent, + name: "human_in_the_loop", + description: "HITL agent", + }), + ); + + /* ---------------- tool-based-generative-ui ---------------- */ + const haikuAgent = new Agent({ + model: await createModel(), + tools: [], + systemPrompt: `You are a creative haiku generator. + +When the user asks for a haiku, ALWAYS call the \`generate_haiku\` tool with: +- 3 lines of haiku in Japanese +- 3 lines of haiku translated to English +- One relevant image_name from the provided list +- A CSS gradient for the card background + +Do not respond with plain text — always use the tool.`, + }); + mountAgent( + app, + "/tool-based-generative-ui", + new StrandsAgent({ + agent: haikuAgent, + name: "tool_based_generative_ui", + description: "Haiku generator with frontend-rendered tool", + }), + ); + + const port = Number(process.env.PORT ?? 8002); + const host = process.env.HOST ?? "0.0.0.0"; + app.listen(port, host, () => { + console.log(`TS strands server listening on ${host}:${port}`); + }); +} + +void main(); diff --git a/integrations/aws-strands/typescript/package.json b/integrations/aws-strands/typescript/package.json index feabde2ea9..0155a26a51 100644 --- a/integrations/aws-strands/typescript/package.json +++ b/integrations/aws-strands/typescript/package.json @@ -1,7 +1,8 @@ { "name": "@ag-ui/aws-strands", "author": "AG-UI Contributors", - "version": "0.0.1", + "version": "0.1.0", + "description": "AWS Strands Agents integration for the AG-UI protocol", "main": "./dist/index.js", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", @@ -22,26 +23,60 @@ "unlink:global": "pnpm unlink --global" }, "peerDependencies": { - "@ag-ui/core": ">=0.0.37", "@ag-ui/client": ">=0.0.37", - "rxjs": "7.8.1" + "@ag-ui/core": ">=0.0.37", + "@ag-ui/encoder": ">=0.0.37", + "@modelcontextprotocol/sdk": ">=1.0.0", + "@strands-agents/sdk": ">=1.1.0", + "cors": "^2.8.5", + "express": "^4.18.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + }, + "express": { + "optional": true + }, + "cors": { + "optional": true + } }, "devDependencies": { - "@ag-ui/core": "workspace:*", "@ag-ui/client": "workspace:*", + "@ag-ui/core": "workspace:*", + "@ag-ui/encoder": "workspace:*", + "@arethetypeswrong/cli": "^0.17.4", + "@modelcontextprotocol/sdk": "^1.29.0", + "@strands-agents/sdk": "^1.1.0", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", "@types/node": "^20.11.19", + "@vitest/coverage-istanbul": "^4.0.18", + "cors": "^2.8.5", + "express": "^5.0.0", "publint": "^0.3.12", - "@arethetypeswrong/cli": "^0.17.4", "tsdown": "^0.20.1", "typescript": "^5.3.3", - "@vitest/coverage-istanbul": "^4.0.18", - "vitest": "^4.0.18" + "vitest": "^4.0.18", + "zod": "^4.4.3" }, "exports": { ".": { - "import": "./dist/index.mjs", - "require": "./dist/index.js" + "require": "./dist/index.js", + "import": "./dist/index.mjs" + }, + "./server": { + "require": "./dist/server.js", + "import": "./dist/server.mjs" }, "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "server": [ + "dist/server.d.ts" + ] + } } -} \ No newline at end of file +} diff --git a/integrations/aws-strands/typescript/src/__tests__/agent-config-forwarded.test.ts b/integrations/aws-strands/typescript/src/__tests__/agent-config-forwarded.test.ts new file mode 100644 index 0000000000..2040f99a11 --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/agent-config-forwarded.test.ts @@ -0,0 +1,177 @@ +/** + * Every forwardable field on the template Agent must reach the per-thread + * AgentConfig. Mirrors Python's `_extract_agent_kwargs`. + */ + +import { describe, it, expect, vi } from "vitest"; +import type { AgentConfig, Plugin } from "@strands-agents/sdk"; +import { StrandsAgent } from "../agent"; +import { collect } from "./helpers"; + +const capturedConfigs: AgentConfig[] = []; + +vi.mock("@strands-agents/sdk", async (importOriginal) => { + const actual = await importOriginal(); + class MockAgent { + model: unknown; + tools: unknown[] = []; + systemPrompt?: unknown; + name?: string; + description?: string; + id?: string; + toolRegistry = { + _tools: new Map(), + add(t: unknown) { + this._tools.set((t as { name: string }).name, t); + }, + getByName(name: string) { + return this._tools.get(name); + }, + get(name: string) { + return this._tools.get(name); + }, + removeByName(name: string) { + this._tools.delete(name); + }, + remove() {}, + values() { + return Array.from(this._tools.values()); + }, + }; + constructor(cfg?: AgentConfig) { + if (cfg) { + capturedConfigs.push(cfg); + this.model = cfg.model; + this.tools = (cfg.tools as unknown[]) ?? []; + if (cfg.systemPrompt !== undefined) + this.systemPrompt = cfg.systemPrompt; + if (cfg.name !== undefined) this.name = cfg.name; + if (cfg.description !== undefined) this.description = cfg.description; + if (cfg.id !== undefined) this.id = cfg.id; + } + } + // eslint-disable-next-line require-yield + async *stream() {} + } + return { ...actual, Agent: MockAgent }; +}); + +/** Build a template Agent stub populated with every forwardable field. */ +function richTemplate(): import("@strands-agents/sdk").Agent { + return { + model: { name: "template-model" }, + tools: [], + systemPrompt: "you are helpful", + name: "my-template-agent", + description: "a wizard", + id: "wizard-001", + appState: { + getAll: () => ({ seed: 42, region: "us-west-2" }), + }, + modelState: { + getAll: () => ({ responseId: "abc" }), + }, + traceAttributes: { team: "agui" }, + structuredOutputSchema: { type: "zod-placeholder" }, + toolExecutor: "concurrent", + toolRegistry: { + _tools: new Map(), + add: () => {}, + getByName: () => undefined, + get: () => undefined, + removeByName: () => {}, + remove: () => {}, + values: () => [], + }, + } as unknown as import("@strands-agents/sdk").Agent; +} + +describe("AgentConfig forwarding", () => { + it("forwards name, description, id to every per-thread AgentConfig", async () => { + capturedConfigs.length = 0; + const sa = new StrandsAgent({ agent: richTemplate(), name: "agui-name" }); + await collect(sa); + const cfg = capturedConfigs.at(-1)!; + expect(cfg.name).toBe("my-template-agent"); + expect(cfg.description).toBe("a wizard"); + expect(cfg.id).toBe("wizard-001"); + }); + + it("forwards appState and modelState as plain dicts", async () => { + capturedConfigs.length = 0; + const sa = new StrandsAgent({ agent: richTemplate(), name: "t" }); + await collect(sa); + const cfg = capturedConfigs.at(-1)!; + expect(cfg.appState).toEqual({ seed: 42, region: "us-west-2" }); + expect(cfg.modelState).toEqual({ responseId: "abc" }); + }); + + it("forwards traceAttributes, structuredOutputSchema, toolExecutor", async () => { + capturedConfigs.length = 0; + const sa = new StrandsAgent({ agent: richTemplate(), name: "t" }); + await collect(sa); + const cfg = capturedConfigs.at(-1)!; + expect(cfg.traceAttributes).toEqual({ team: "agui" }); + expect(cfg.structuredOutputSchema).toBeDefined(); + expect(cfg.toolExecutor).toBe("concurrent"); + }); + + it("omits optional fields entirely when the template doesn't set them", async () => { + capturedConfigs.length = 0; + // Bare template with only the mandatory fields. + const bare = { + model: { name: "m" }, + tools: [], + toolRegistry: { + _tools: new Map(), + add: () => {}, + getByName: () => undefined, + get: () => undefined, + removeByName: () => {}, + remove: () => {}, + values: () => [], + }, + } as unknown as import("@strands-agents/sdk").Agent; + const sa = new StrandsAgent({ agent: bare, name: "t" }); + await collect(sa); + const cfg = capturedConfigs.at(-1)!; + expect("systemPrompt" in cfg).toBe(false); + expect("name" in cfg).toBe(false); + expect("description" in cfg).toBe(false); + expect("id" in cfg).toBe(false); + expect("appState" in cfg).toBe(false); + expect("modelState" in cfg).toBe(false); + expect("traceAttributes" in cfg).toBe(false); + expect("structuredOutputSchema" in cfg).toBe(false); + expect("toolExecutor" in cfg).toBe(false); + }); + + it("explicitly does NOT forward the template's conversationManager (documented exclusion)", async () => { + capturedConfigs.length = 0; + const tpl = richTemplate() as unknown as Record; + tpl.conversationManager = { name: "sliding-window", initAgent: () => {} }; + const sa = new StrandsAgent({ + agent: tpl as unknown as import("@strands-agents/sdk").Agent, + name: "t", + }); + await collect(sa); + const cfg = capturedConfigs.at(-1)!; + // conversationManager is NOT in the forwarded config; Strands will + // construct its default (SlidingWindowConversationManager) per-thread. + expect("conversationManager" in cfg).toBe(false); + }); + + it("forwards alongside plugins and sessionManager when all are set", async () => { + capturedConfigs.length = 0; + const plugin: Plugin = { name: "p", initAgent: () => {} }; + const sa = new StrandsAgent({ + agent: richTemplate(), + name: "t", + plugins: [plugin], + }); + await collect(sa); + const cfg = capturedConfigs.at(-1)!; + expect(cfg.name).toBe("my-template-agent"); + expect(cfg.plugins).toEqual([plugin]); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/agent.test.ts b/integrations/aws-strands/typescript/src/__tests__/agent.test.ts new file mode 100644 index 0000000000..b6faac67bb --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/agent.test.ts @@ -0,0 +1,358 @@ +/** + * Unit tests for StrandsAgent. + * + * We don't spin up a full Strands Agent — instead we inject a stub that + * yields a scripted sequence of events whose `type` discriminators match + * what `@strands-agents/sdk`'s `Agent.stream()` produces. This keeps tests + * fast and hermetic and avoids needing a model provider. + */ + +import { describe, it, expect } from "vitest"; +import { ToolUseBlock, ToolResultBlock, TextBlock } from "@strands-agents/sdk"; +import type { AgentStreamEvent } from "@strands-agents/sdk"; +import { EventType } from "@ag-ui/core"; +import type { BaseEvent } from "@ag-ui/core"; + +import { StrandsAgent } from "../agent"; +import { + collect, + minimalRunInput, + scriptedAgent, + scriptedStrandsAgent, + stream, +} from "./helpers"; + +function types(events: BaseEvent[]): string[] { + return events.map((e) => e.type); +} + +describe("StrandsAgent.run — lifecycle", () => { + it("emits RUN_STARTED + STATE_SNAPSHOT(s) + RUN_FINISHED for an empty stream", async () => { + const agent = scriptedStrandsAgent([]); + const events = await collect(agent); + // Initial snapshot is always emitted when state is provided (even {}), + // plus the final snapshot before RUN_FINISHED. This matches Python's + // behavior so a client that wires the initial snapshot's state onto + // its UI doesn't diverge if the server later updates the state. + const kinds = types(events); + expect(kinds[0]).toBe(EventType.RUN_STARTED); + expect(kinds[kinds.length - 1]).toBe(EventType.RUN_FINISHED); + expect( + kinds.filter((k) => k === EventType.STATE_SNAPSHOT).length, + ).toBeGreaterThanOrEqual(1); + }); + + it("filters `messages` out of the INITIAL state snapshot but keeps it in the FINAL (Py parity)", async () => { + const agent = scriptedStrandsAgent([]); + const input = minimalRunInput({ + state: { foo: "bar", messages: [{ role: "user", content: "x" }] }, + }); + const events = await collect(agent, input); + const stateEvents = events.filter( + (e) => e.type === EventType.STATE_SNAPSHOT, + ); + expect(stateEvents).toHaveLength(2); + // Initial snapshot filters `messages` (frontend doesn't recognize role="tool"). + const initial = ( + stateEvents[0] as unknown as { + snapshot: Record; + } + ).snapshot; + expect(initial).not.toHaveProperty("messages"); + expect(initial).toHaveProperty("foo", "bar"); + // Final snapshot preserves `messages` verbatim — matches Py adapter. + const final = ( + stateEvents[1] as unknown as { + snapshot: Record; + } + ).snapshot; + expect(final).toHaveProperty("messages"); + expect(final).toHaveProperty("foo", "bar"); + }); + + it("emits RUN_ERROR with STRANDS_ERROR code when the stream throws", async () => { + const agent = scriptedStrandsAgent([], { + stubOverrides: { + stream: async function* () { + throw new Error("boom"); + } as unknown as import("@strands-agents/sdk").Agent["stream"], + }, + }); + const events = await collect(agent); + const last = events[events.length - 1] as unknown as { + type: string; + code: string; + message: string; + }; + expect(last.type).toBe(EventType.RUN_ERROR); + expect(last.code).toBe("STRANDS_ERROR"); + expect(last.message).toBe("boom"); + }); +}); + +describe("StrandsAgent.run — text streaming", () => { + it("wraps text deltas in TEXT_MESSAGE_START/_CONTENT/_END", async () => { + const agent = scriptedStrandsAgent([ + stream.textDelta("Hello"), + stream.blockStop(), + ]); + const events = await collect(agent); + const kinds = types(events); + expect(kinds).toContain(EventType.TEXT_MESSAGE_START); + expect(kinds).toContain(EventType.TEXT_MESSAGE_CONTENT); + expect(kinds).toContain(EventType.TEXT_MESSAGE_END); + const content = events.find( + (e) => e.type === EventType.TEXT_MESSAGE_CONTENT, + ) as unknown as { delta: string }; + expect(content.delta).toBe("Hello"); + }); + + it("unwraps Strands v1.0 ModelStreamUpdateEvent wrappers", async () => { + // Real Strands v1.x yields hook-event wrappers that carry the inner + // ModelStreamEvent on `.event`. The adapter unwraps these before + // dispatching so the same codepath handles both wrapped and raw events. + const agent = scriptedStrandsAgent([ + { + type: "modelStreamUpdateEvent", + event: { + type: "modelContentBlockDeltaEvent", + delta: { type: "textDelta", text: "wrapped" }, + }, + } as unknown as AgentStreamEvent, + { + type: "modelStreamUpdateEvent", + event: { type: "modelContentBlockStopEvent" }, + } as unknown as AgentStreamEvent, + ]); + const events = await collect(agent); + const content = events.find( + (e) => e.type === EventType.TEXT_MESSAGE_CONTENT, + ) as unknown as { delta: string }; + expect(content).toBeDefined(); + expect(content.delta).toBe("wrapped"); + }); +}); + +describe("StrandsAgent.run — tool calls", () => { + it("unwraps ContentBlockEvent wrappers around ToolUseBlock", async () => { + // Strands v1.0 wraps completed content blocks in `ContentBlockEvent` + // hook events. The adapter unwraps those so the same code path handles + // both wrapped and raw ToolUseBlock values. + const block = new ToolUseBlock({ + name: "get_weather", + toolUseId: "strands-2", + input: { city: "Seattle" }, + }); + const wrapped = { + type: "contentBlockEvent", + contentBlock: block, + } as unknown as AgentStreamEvent; + const agent = scriptedStrandsAgent([wrapped]); + const events = await collect(agent); + const start = events.find( + (e) => e.type === EventType.TOOL_CALL_START, + ) as unknown as { toolCallName: string; toolCallId: string }; + expect(start).toBeDefined(); + expect(start.toolCallName).toBe("get_weather"); + expect(start.toolCallId).toBe("strands-2"); + }); + + it("emits TOOL_CALL_START/ARGS/END when a ToolUseBlock is yielded directly", async () => { + const block = new ToolUseBlock({ + name: "get_weather", + toolUseId: "strands-1", + input: { city: "Portland" }, + }); + const agent = scriptedStrandsAgent([block as unknown as AgentStreamEvent]); + const events = await collect(agent); + const kinds = types(events); + expect(kinds).toContain(EventType.TOOL_CALL_START); + expect(kinds).toContain(EventType.TOOL_CALL_ARGS); + expect(kinds).toContain(EventType.TOOL_CALL_END); + + const start = events.find( + (e) => e.type === EventType.TOOL_CALL_START, + ) as unknown as { toolCallName: string; toolCallId: string }; + expect(start.toolCallName).toBe("get_weather"); + expect(start.toolCallId).toBe("strands-1"); + + const args = events.find( + (e) => e.type === EventType.TOOL_CALL_ARGS, + ) as unknown as { delta: string }; + expect(JSON.parse(args.delta)).toEqual({ city: "Portland" }); + }); + + it("emits TOOL_CALL_RESULT for backend tool results (afterToolCallEvent)", async () => { + const block = new ToolUseBlock({ + name: "backend_tool", + toolUseId: "backend-1", + input: { x: 1 }, + }); + const resultBlock = new ToolResultBlock({ + toolUseId: "backend-1", + status: "success", + content: [new TextBlock(JSON.stringify({ ok: true }))], + }); + const agent = scriptedStrandsAgent([ + block as unknown as AgentStreamEvent, + { + type: "afterToolCallEvent", + toolUse: { + toolUseId: "backend-1", + name: "backend_tool", + input: { x: 1 }, + }, + tool: undefined, + result: resultBlock, + } as unknown as AgentStreamEvent, + ]); + const events = await collect(agent); + const result = events.find( + (e) => e.type === EventType.TOOL_CALL_RESULT, + ) as unknown as { toolCallId: string; content: string }; + expect(result).toBeDefined(); + expect(result.toolCallId).toBe("backend-1"); + expect(JSON.parse(result.content)).toEqual({ ok: true }); + }); + + it("emits a PredictState CustomEvent when ToolBehavior.predictState is configured", async () => { + const block = new ToolUseBlock({ + name: "set_recipe", + toolUseId: "u-1", + input: { name: "Soup" }, + }); + const agent = scriptedStrandsAgent([block as unknown as AgentStreamEvent]); + (agent as unknown as { config: Record }).config = { + toolBehaviors: { + set_recipe: { + predictState: [ + { stateKey: "recipe", tool: "set_recipe", toolArgument: "data" }, + ], + }, + }, + }; + const events = await collect(agent); + const custom = events.find( + (e) => + e.type === EventType.CUSTOM && + (e as unknown as { name: string }).name === "PredictState", + ) as unknown as { value: unknown[] }; + expect(custom).toBeDefined(); + expect(custom.value).toEqual([ + { state_key: "recipe", tool: "set_recipe", tool_argument: "data" }, + ]); + }); +}); + +describe("StrandsAgent.run — reasoning", () => { + it("emits REASONING_* events and closes on contentBlockStop", async () => { + const agent = scriptedStrandsAgent([ + stream.reasoningDelta("thinking..."), + stream.blockStop(), + ]); + const events = await collect(agent); + const kinds = types(events); + expect(kinds).toContain(EventType.REASONING_START); + expect(kinds).toContain(EventType.REASONING_MESSAGE_START); + expect(kinds).toContain(EventType.REASONING_MESSAGE_CONTENT); + expect(kinds).toContain(EventType.REASONING_MESSAGE_END); + expect(kinds).toContain(EventType.REASONING_END); + }); + + it("base64-encodes redactedContent into REASONING_ENCRYPTED_VALUE", async () => { + const agent = scriptedStrandsAgent([ + stream.reasoningRedacted(new Uint8Array([0x41, 0x42, 0x43])), + ]); + const events = await collect(agent); + const enc = events.find( + (e) => e.type === EventType.REASONING_ENCRYPTED_VALUE, + ) as unknown as { encryptedValue: string }; + expect(enc).toBeDefined(); + expect(enc.encryptedValue).toBe("QUJD"); + }); +}); + +describe("StrandsAgent.run — session-manager provider", () => { + it("emits RUN_ERROR(SESSION_MANAGER_ERROR) if the provider throws", async () => { + const stub = scriptedAgent([]); + const agent = new StrandsAgent({ + agent: stub, + name: "t", + config: { + sessionManagerProvider: () => { + throw new Error("no session for you"); + }, + }, + }); + const events = await collect( + agent, + minimalRunInput({ threadId: "fresh-thread" }), + ); + const kinds = types(events); + expect(kinds).toEqual([EventType.RUN_STARTED, EventType.RUN_ERROR]); + const err = events[1] as unknown as { message: string; code: string }; + expect(err.code).toBe("SESSION_MANAGER_ERROR"); + expect(err.message).toContain("no session for you"); + }); + + it("emits RUN_ERROR(SESSION_MANAGER_INVALID_TYPE) if the provider returns garbage", async () => { + const stub = scriptedAgent([]); + const agent = new StrandsAgent({ + agent: stub, + name: "t", + config: { + // Empty object with no HookProvider shape. + sessionManagerProvider: () => ({ unrelated: true }) as never, + }, + }); + const events = await collect( + agent, + minimalRunInput({ threadId: "fresh-thread-2" }), + ); + const kinds = types(events); + expect(kinds).toEqual([EventType.RUN_STARTED, EventType.RUN_ERROR]); + expect((events[1] as unknown as { code: string }).code).toBe( + "SESSION_MANAGER_INVALID_TYPE", + ); + }); +}); + +describe("StrandsAgent.run — state context builder", () => { + it("lets the builder rewrite the prompt before it's forwarded to Strands", async () => { + let capturedArgs: unknown = null; + const stub = scriptedAgent([], { + messages: [], + stream: async function* (prompt: unknown) { + capturedArgs = prompt; + } as unknown as import("@strands-agents/sdk").Agent["stream"], + }); + const agent = new StrandsAgent({ agent: stub, name: "test" }); + const byThread = ( + agent as unknown as { _agentsByThread: Map } + )._agentsByThread; + byThread.set("thread-1", stub); + byThread.set("default", stub); + (agent as unknown as { config: Record }).config = { + stateContextBuilder: (_input: unknown, prompt: string) => + `${prompt} [STATE:ok]`, + }; + + await collect( + agent, + minimalRunInput({ + messages: [{ id: "m1", role: "user", content: "Hi there" }], + }), + ); + // History reconciliation moves the prompt onto agent.messages and the + // adapter calls stream(undefined). The builder is applied to the last + // user-text turn in the replayed history (Python parity). + expect(capturedArgs).toBeUndefined(); + const replayed = (stub as unknown as { messages: unknown[] }) + .messages as Array<{ + role: string; + content: Array<{ text?: string }>; + }>; + expect(replayed).toHaveLength(1); + expect(replayed[0]!.content[0]!.text).toBe("Hi there [STATE:ok]"); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/cancel-propagation.test.ts b/integrations/aws-strands/typescript/src/__tests__/cancel-propagation.test.ts new file mode 100644 index 0000000000..ebb65eb324 --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/cancel-propagation.test.ts @@ -0,0 +1,88 @@ +/** + * When the outer generator is abandoned, the Strands `Agent.stream()` call + * must receive an aborted `cancelSignal` so Bedrock streaming stops + * instead of silently burning tokens in the background. + */ + +import { describe, it, expect } from "vitest"; +import type { AgentStreamEvent } from "@strands-agents/sdk"; + +import { StrandsAgent } from "../agent"; +import { minimalRunInput } from "./helpers"; + +function capturingStub(): { + stub: import("@strands-agents/sdk").Agent; + observed: { + cancelSignal?: AbortSignal; + aborted: () => boolean; + }; +} { + let captured: AbortSignal | undefined; + const stub = { + model: {}, + tools: [], + toolRegistry: { + list: () => [], + add() {}, + get: () => undefined, + remove() {}, + }, + sessionManager: undefined, + async *stream( + _args: unknown, + options?: { cancelSignal?: AbortSignal }, + ): AsyncGenerator { + captured = options?.cancelSignal; + // Emit at least one benign event so the adapter's consumer loop + // advances past the first `next()`. Then idle until the signal fires + // so the test can observe the abort. + yield { + type: "modelContentBlockDeltaEvent", + delta: { type: "textDelta", text: "hi" }, + } as unknown as AgentStreamEvent; + while (true) { + if (captured?.aborted) return; + await new Promise((r) => setTimeout(r, 5)); + } + }, + } as unknown as import("@strands-agents/sdk").Agent; + return { + stub, + observed: { + get cancelSignal() { + return captured; + }, + aborted: () => captured?.aborted ?? false, + }, + }; +} + +describe("Strands cancelSignal propagation", () => { + it("passes a cancelSignal to agent.stream() and aborts it on consumer bail", async () => { + const { stub, observed } = capturingStub(); + const agent = new StrandsAgent({ agent: stub, name: "c" }); + ( + agent as unknown as { _agentsByThread: Map } + )._agentsByThread.set("thread-1", stub); + + const it = agent.run(minimalRunInput()); + // Drain events until the adapter is inside the Strands stream loop + // (evidenced by the stub's `stream()` having been called). The adapter + // yields RUN_STARTED + STATE_SNAPSHOT + (eventually) text events before + // Strands's stream runs — drain up to 20 events or until the signal is + // observed. + for (let i = 0; i < 20; i++) { + const step = await it.next(); + if (step.done) break; + if (observed.cancelSignal) break; + } + expect(observed.cancelSignal).toBeInstanceOf(AbortSignal); + expect(observed.aborted()).toBe(false); + + // Caller bails early (simulates HTTP client disconnect). + await it.return?.(); + // The adapter's finally fires controller.abort() before draining the + // generator, so the signal observed by Strands should now be aborted. + expect(observed.aborted()).toBe(true); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/chunk-events.test.ts b/integrations/aws-strands/typescript/src/__tests__/chunk-events.test.ts new file mode 100644 index 0000000000..171663d01d --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/chunk-events.test.ts @@ -0,0 +1,132 @@ +/** + * StrandsAgentConfig.emitChunkEvents collapses START/CONTENT/END triples + * into self-expanding TEXT_MESSAGE_CHUNK / TOOL_CALL_CHUNK / + * REASONING_MESSAGE_CHUNK events. + */ + +import { describe, it, expect } from "vitest"; +import { EventType, type BaseEvent, type RunAgentInput } from "@ag-ui/core"; +import { StrandsAgent } from "../agent"; +import { collect, minimalRunInput, scriptedAgent } from "./helpers"; + +class ScriptedAgent extends StrandsAgent { + private readonly _events: BaseEvent[]; + constructor(events: BaseEvent[], emit: boolean) { + super({ + agent: scriptedAgent(), + name: "t", + config: { emitChunkEvents: emit }, + }); + this._events = events; + } + // Bypass the real _runRaw logic; emit the scripted events through + // the public run(), which applies the chunk-collapse filter when + // emitChunkEvents is true. + protected async *_runRaw( + _input: RunAgentInput, + ): AsyncGenerator { + for (const e of this._events) yield e; + } +} + +const runInput = (): RunAgentInput => + minimalRunInput({ threadId: "t", runId: "r" }); + +describe("emitChunkEvents collapse", () => { + const scripted: BaseEvent[] = [ + { type: EventType.RUN_STARTED, threadId: "t", runId: "r" }, + { + type: EventType.TEXT_MESSAGE_START, + messageId: "m1", + role: "assistant", + } as BaseEvent, + { + type: EventType.TEXT_MESSAGE_CONTENT, + messageId: "m1", + delta: "hel", + } as BaseEvent, + { + type: EventType.TEXT_MESSAGE_CONTENT, + messageId: "m1", + delta: "lo", + } as BaseEvent, + { type: EventType.TEXT_MESSAGE_END, messageId: "m1" } as BaseEvent, + { + type: EventType.TOOL_CALL_START, + toolCallId: "tc1", + toolCallName: "noop", + parentMessageId: "m1", + } as BaseEvent, + { + type: EventType.TOOL_CALL_ARGS, + toolCallId: "tc1", + delta: "{}", + } as BaseEvent, + { type: EventType.TOOL_CALL_END, toolCallId: "tc1" } as BaseEvent, + { type: EventType.REASONING_MESSAGE_START, messageId: "r1" } as BaseEvent, + { + type: EventType.REASONING_MESSAGE_CONTENT, + messageId: "r1", + delta: "why", + } as BaseEvent, + { type: EventType.REASONING_MESSAGE_END, messageId: "r1" } as BaseEvent, + { type: EventType.RUN_FINISHED, threadId: "t", runId: "r" }, + ]; + + it("default emits triples, no chunk events", async () => { + const out = await collect(new ScriptedAgent(scripted, false), runInput()); + const types = out.map((e) => e.type); + expect(types).not.toContain(EventType.TEXT_MESSAGE_CHUNK); + expect(types).not.toContain(EventType.TOOL_CALL_CHUNK); + expect(types).not.toContain(EventType.REASONING_MESSAGE_CHUNK); + expect( + types.filter((t) => t === EventType.TEXT_MESSAGE_START), + ).toHaveLength(1); + }); + + it("emitChunkEvents collapses TEXT/TOOL/REASONING triples into chunks, dropping *_END", async () => { + const out = await collect(new ScriptedAgent(scripted, true), runInput()); + const types = out.map((e) => e.type); + // Triples replaced with chunks; END events are DROPPED, not translated + // (client auto-emits TextMessageEnd when the messageId changes or the + // stream finishes). + expect(types).not.toContain(EventType.TEXT_MESSAGE_START); + expect(types).not.toContain(EventType.TEXT_MESSAGE_CONTENT); + expect(types).not.toContain(EventType.TEXT_MESSAGE_END); + expect(types).not.toContain(EventType.TOOL_CALL_START); + expect(types).not.toContain(EventType.TOOL_CALL_ARGS); + expect(types).not.toContain(EventType.TOOL_CALL_END); + expect(types).not.toContain(EventType.REASONING_MESSAGE_START); + expect(types).not.toContain(EventType.REASONING_MESSAGE_CONTENT); + expect(types).not.toContain(EventType.REASONING_MESSAGE_END); + // Chunks emitted: START → identity-only chunk, CONTENT(s) → chunks with + // delta. No trailing END chunk. + const textChunks = out.filter( + (e) => e.type === EventType.TEXT_MESSAGE_CHUNK, + ); + expect(textChunks).toHaveLength(3); // start + 2 content + expect((textChunks[0] as unknown as { messageId?: string }).messageId).toBe( + "m1", + ); + expect((textChunks[0] as unknown as { role?: string }).role).toBe( + "assistant", + ); + expect((textChunks[1] as unknown as { delta?: string }).delta).toBe("hel"); + const toolChunks = out.filter((e) => e.type === EventType.TOOL_CALL_CHUNK); + expect(toolChunks).toHaveLength(2); // start + args + expect( + (toolChunks[0] as unknown as { toolCallName?: string }).toolCallName, + ).toBe("noop"); + const reasoningChunks = out.filter( + (e) => e.type === EventType.REASONING_MESSAGE_CHUNK, + ); + expect(reasoningChunks).toHaveLength(2); // start + content + }); + + it("non-triple events pass through unchanged", async () => { + const out = await collect(new ScriptedAgent(scripted, true), runInput()); + const types = out.map((e) => e.type); + expect(types[0]).toBe(EventType.RUN_STARTED); + expect(types[types.length - 1]).toBe(EventType.RUN_FINISHED); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/client-proxy-tool.test.ts b/integrations/aws-strands/typescript/src/__tests__/client-proxy-tool.test.ts new file mode 100644 index 0000000000..79d6c77ca0 --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/client-proxy-tool.test.ts @@ -0,0 +1,193 @@ +import { describe, it, expect } from "vitest"; +import type { Tool as AguiTool } from "@ag-ui/core"; + +import { + createProxyTool, + syncProxyTools, + isProxyTool, +} from "../client-proxy-tool"; +import { fakeTool } from "./helpers"; + +/** + * Minimal fake that satisfies the Strands `ToolRegistry` contract used by + * `syncProxyTools`: `add`, `get`, `remove`, `list`. + */ +function fakeRegistry() { + const tools = new Map< + string, + { name: string; description: string; toolSpec: unknown } + >(); + return { + add( + t: + | { name: string; description: string; toolSpec: unknown } + | Array<{ name: string; description: string; toolSpec: unknown }>, + ) { + for (const x of Array.isArray(t) ? t : [t]) tools.set(x.name, x); + }, + get(name: string) { + return tools.get(name); + }, + remove(name: string) { + tools.delete(name); + }, + list() { + return Array.from(tools.values()); + }, + }; +} + +function aguiTool(name: string, overrides: Partial = {}): AguiTool { + return { + name, + description: overrides.description ?? `Tool ${name}`, + parameters: overrides.parameters ?? { type: "object", properties: {} }, + ...overrides, + }; +} + +describe("createProxyTool", () => { + it("produces a Tool carrying the AG-UI tool's name and description", () => { + const tool = createProxyTool( + aguiTool("my_tool", { description: "Does a thing" }), + ); + expect(tool.name).toBe("my_tool"); + expect(tool.description).toBe("Does a thing"); + expect(tool.toolSpec.name).toBe("my_tool"); + expect(tool.toolSpec.inputSchema).toBeTypeOf("object"); + }); + + it("marks proxy tools so they're distinguishable from native tools", () => { + const tool = createProxyTool(aguiTool("x")); + expect(isProxyTool(tool)).toBe(true); + }); +}); + +describe("syncProxyTools", () => { + it("registers new proxies for each AG-UI tool", () => { + const reg = fakeRegistry(); + const names = syncProxyTools( + reg as unknown as Parameters[0], + [aguiTool("a"), aguiTool("b")], + new Set(), + ); + expect(names).toEqual(new Set(["a", "b"])); + expect( + reg + .list() + .map((t) => t.name) + .sort(), + ).toEqual(["a", "b"]); + }); + + it("evicts proxies that were tracked previously but are absent now", () => { + const reg = fakeRegistry(); + const first = syncProxyTools( + reg as unknown as Parameters[0], + [aguiTool("a"), aguiTool("b")], + new Set(), + ); + expect(first).toEqual(new Set(["a", "b"])); + + const second = syncProxyTools( + reg as unknown as Parameters[0], + [aguiTool("a")], + first, + ); + expect(second).toEqual(new Set(["a"])); + expect(reg.get("a")).toBeDefined(); + expect(reg.get("b")).toBeUndefined(); + }); + + it("never overwrites a native (non-proxy) tool with the same name", () => { + const reg = fakeRegistry(); + reg.add(fakeTool("native")); + const names = syncProxyTools( + reg as unknown as Parameters[0], + [aguiTool("native"), aguiTool("new_proxy")], + new Set(), + ); + // native survives, new_proxy registers + expect(reg.get("native")).toBeDefined(); + // The native tool does not carry the proxy marker → still native. + expect(isProxyTool(reg.get("native"))).toBe(false); + // new_proxy is registered as proxy + expect(names.has("new_proxy")).toBe(true); + expect(names.has("native")).toBe(false); + }); + + it("passes an empty aguiTools array to evict every tracked proxy", () => { + const reg = fakeRegistry(); + const first = syncProxyTools( + reg as unknown as Parameters[0], + [aguiTool("a"), aguiTool("b")], + new Set(), + ); + const second = syncProxyTools( + reg as unknown as Parameters[0], + [], + first, + ); + expect(second.size).toBe(0); + expect(reg.list()).toEqual([]); + }); + + it("is idempotent when the same tool list is passed twice", () => { + const reg = fakeRegistry(); + const first = syncProxyTools( + reg as unknown as Parameters[0], + [aguiTool("a", { description: "v1" })], + new Set(), + ); + const second = syncProxyTools( + reg as unknown as Parameters[0], + [aguiTool("a", { description: "v2" })], + first, + ); + expect(second).toEqual(new Set(["a"])); + // Picks up the new description + expect(reg.get("a")?.description).toBe("v2"); + }); + + // The Strands v1 `ToolRegistry` throws if the same name is registered + // twice, so `syncProxyTools` must explicitly remove before re-registering. + it("re-registers a proxy across successive calls on a strict registry", () => { + const tools = new Map< + string, + { name: string; description: string; toolSpec: unknown } + >(); + const strictRegistry = { + add(t: { name: string; description: string; toolSpec: unknown }) { + if (tools.has(t.name)) { + throw new Error(`Tool with name '${t.name}' already registered`); + } + tools.set(t.name, t); + }, + get(name: string) { + return tools.get(name); + }, + remove(name: string) { + tools.delete(name); + }, + list() { + return Array.from(tools.values()); + }, + }; + + const first = syncProxyTools( + strictRegistry as unknown as Parameters[0], + [aguiTool("change_background")], + new Set(), + ); + expect(first).toEqual(new Set(["change_background"])); + + expect(() => + syncProxyTools( + strictRegistry as unknown as Parameters[0], + [aguiTool("change_background")], + first, + ), + ).not.toThrow(); + expect(strictRegistry.list().length).toBe(1); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/concurrent-tools.test.ts b/integrations/aws-strands/typescript/src/__tests__/concurrent-tools.test.ts new file mode 100644 index 0000000000..d622c1dfd5 --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/concurrent-tools.test.ts @@ -0,0 +1,158 @@ +/** + * Concurrent tool executor — event ordering integrity. + * + * Strands defaults to `toolExecutor: 'concurrent'` which executes multiple + * tool calls from one model turn in parallel. The AG-UI adapter must keep + * per-toolCallId envelope integrity: for each id X, TOOL_CALL_START{X} + * must precede ARGS{X} which must precede END{X} which must precede + * RESULT{X}. Interleaving across ids is allowed — but never within an id. + * + * We drive a stub Strands stream that interleaves two tools' events and + * verify the adapter's output preserves per-id ordering. + */ + +import { describe, it, expect } from "vitest"; +import { ToolUseBlock, TextBlock, ToolResultBlock } from "@strands-agents/sdk"; +import type { AgentStreamEvent } from "@strands-agents/sdk"; +import { EventType, type BaseEvent } from "@ag-ui/core"; + +import { collect, scriptedStrandsAgent } from "./helpers"; + +/** + * Returns, for each toolCallId, the ordered indices of its START/ARGS/END + * /RESULT events in the output stream. Used to assert per-id envelope + * integrity even when events interleave across tools. + */ +function groupByToolCallId(events: BaseEvent[]) { + const idx: Record> = {}; + events.forEach((e, i) => { + const id = (e as unknown as { toolCallId?: string }).toolCallId; + if (!id) return; + (idx[id] ??= []).push({ type: e.type, i }); + }); + return idx; +} + +describe("Concurrent tool executor envelope integrity (proof 4)", () => { + it("interleaved two-tool stream preserves per-id START → ARGS → END → RESULT ordering", async () => { + // Interleave two tool calls' events in a pattern a concurrent executor + // might actually produce. Strands yields ToolUseBlocks as each one + // finishes streaming from the model, then executes them in parallel + // and yields AfterToolCallEvents in whatever order completes. + const events: AgentStreamEvent[] = [ + // Tool A completes (ToolUseBlock emitted) + new ToolUseBlock({ + name: "Multiply", + toolUseId: "strands-A", + input: { a: 2, b: 3 }, + }) as unknown as AgentStreamEvent, + // Tool B completes (ToolUseBlock emitted) + new ToolUseBlock({ + name: "Multiply", + toolUseId: "strands-B", + input: { a: 5, b: 7 }, + }) as unknown as AgentStreamEvent, + // Concurrent execution finishes in B-first order + { + type: "afterToolCallEvent", + toolUse: { + toolUseId: "strands-B", + name: "Multiply", + input: { a: 5, b: 7 }, + }, + tool: undefined, + result: new ToolResultBlock({ + toolUseId: "strands-B", + status: "success", + content: [new TextBlock("35")], + }), + } as unknown as AgentStreamEvent, + { + type: "afterToolCallEvent", + toolUse: { + toolUseId: "strands-A", + name: "Multiply", + input: { a: 2, b: 3 }, + }, + tool: undefined, + result: new ToolResultBlock({ + toolUseId: "strands-A", + status: "success", + content: [new TextBlock("6")], + }), + } as unknown as AgentStreamEvent, + ]; + const out = await collect(scriptedStrandsAgent(events)); + + // Both tools must produce START/ARGS/END/RESULT. + const grouped = groupByToolCallId(out); + expect(Object.keys(grouped).sort()).toEqual(["strands-A", "strands-B"]); + for (const id of Object.keys(grouped)) { + const kinds = grouped[id]!.map((x) => x.type); + const startIdx = kinds.indexOf(EventType.TOOL_CALL_START); + const argsIdx = kinds.indexOf(EventType.TOOL_CALL_ARGS); + const endIdx = kinds.indexOf(EventType.TOOL_CALL_END); + const resultIdx = kinds.indexOf(EventType.TOOL_CALL_RESULT); + expect(startIdx, `${id} has TOOL_CALL_START`).toBeGreaterThanOrEqual(0); + expect(argsIdx, `${id} has TOOL_CALL_ARGS`).toBeGreaterThan(startIdx); + expect(endIdx, `${id} has TOOL_CALL_END`).toBeGreaterThan(argsIdx); + expect(resultIdx, `${id} has TOOL_CALL_RESULT`).toBeGreaterThan(endIdx); + } + + // Verify result values match inputs (no cross-contamination). + const resA = out.find( + (e) => + e.type === EventType.TOOL_CALL_RESULT && + (e as unknown as { toolCallId: string }).toolCallId === "strands-A", + ) as unknown as { content: string }; + const resB = out.find( + (e) => + e.type === EventType.TOOL_CALL_RESULT && + (e as unknown as { toolCallId: string }).toolCallId === "strands-B", + ) as unknown as { content: string }; + expect(Number(JSON.parse(resA.content))).toBe(6); + expect(Number(JSON.parse(resB.content))).toBe(35); + }); + + it("three-way interleave — each id's envelope is self-contained", async () => { + const make = (id: string, a: number, b: number) => + new ToolUseBlock({ + name: "Multiply", + toolUseId: id, + input: { a, b }, + }) as unknown as AgentStreamEvent; + const result = (id: string, v: number) => + ({ + type: "afterToolCallEvent", + toolUse: { toolUseId: id, name: "Multiply", input: {} }, + tool: undefined, + result: new ToolResultBlock({ + toolUseId: id, + status: "success", + content: [new TextBlock(String(v))], + }), + }) as unknown as AgentStreamEvent; + + const events: AgentStreamEvent[] = [ + make("t1", 2, 3), + make("t2", 5, 7), + make("t3", 11, 13), + // Finishes in scrambled order + result("t3", 143), + result("t1", 6), + result("t2", 35), + ]; + const out = await collect(scriptedStrandsAgent(events)); + const grouped = groupByToolCallId(out); + expect(Object.keys(grouped).sort()).toEqual(["t1", "t2", "t3"]); + + const resultByTid: Record = {}; + for (const e of out) { + if (e.type === EventType.TOOL_CALL_RESULT) { + resultByTid[(e as unknown as { toolCallId: string }).toolCallId] = + Number(JSON.parse((e as unknown as { content: string }).content)); + } + } + expect(resultByTid).toEqual({ t1: 6, t2: 35, t3: 143 }); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/config.test.ts b/integrations/aws-strands/typescript/src/__tests__/config.test.ts new file mode 100644 index 0000000000..dad5393bab --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/config.test.ts @@ -0,0 +1,53 @@ +import { describe, it, expect } from "vitest"; +import { + normalizePredictState, + predictStateMappingToPayload, + maybeAwait, + type PredictStateMapping, +} from "../config"; + +describe("normalizePredictState", () => { + it("returns [] for undefined", () => { + expect(normalizePredictState(undefined)).toEqual([]); + }); + it("wraps a single mapping", () => { + const m: PredictStateMapping = { + stateKey: "x", + tool: "t", + toolArgument: "a", + }; + expect(normalizePredictState(m)).toEqual([m]); + }); + it("passes through an iterable", () => { + const arr: PredictStateMapping[] = [ + { stateKey: "x", tool: "t", toolArgument: "a" }, + { stateKey: "y", tool: "t", toolArgument: "b" }, + ]; + expect(normalizePredictState(arr)).toEqual(arr); + }); +}); + +describe("predictStateMappingToPayload", () => { + it("snake-cases the wire-format keys", () => { + expect( + predictStateMappingToPayload({ + stateKey: "recipe", + tool: "set_recipe", + toolArgument: "data", + }), + ).toEqual({ + state_key: "recipe", + tool: "set_recipe", + tool_argument: "data", + }); + }); +}); + +describe("maybeAwait", () => { + it("awaits promises", async () => { + await expect(maybeAwait(Promise.resolve(7))).resolves.toBe(7); + }); + it("returns plain values unchanged", async () => { + await expect(maybeAwait(42)).resolves.toBe(42); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/context-extras.test.ts b/integrations/aws-strands/typescript/src/__tests__/context-extras.test.ts new file mode 100644 index 0000000000..5da904067a --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/context-extras.test.ts @@ -0,0 +1,139 @@ +/** + * RunAgentInput.context[] and .forwardedProps are exposed on hook contexts + * as a ToolCallContextExtras convenience. The AG-UI `Context` schema is + * `{ description: string; value: string }` — entries are keyed by + * `description`. + */ + +import { describe, it, expect } from "vitest"; +import type { RunAgentInput } from "@ag-ui/core"; +import { buildContextExtras } from "../config"; +import { minimalRunInput } from "./helpers"; + +const input = (overrides: Partial): RunAgentInput => + minimalRunInput({ threadId: "t", runId: "r", ...overrides }); + +describe("buildContextExtras", () => { + it("flattens context[] keyed by description", () => { + const r = buildContextExtras( + input({ + context: [ + { + description: "locale", + value: "en-US", + } as unknown as RunAgentInput["context"][number], + { + description: "userId", + value: "u-42", + } as unknown as RunAgentInput["context"][number], + ], + }), + ); + expect(r.context.locale).toBe("en-US"); + expect(r.context.userId).toBe("u-42"); + }); + + it("ignores entries without a valid description", () => { + const r = buildContextExtras( + input({ + context: [ + { value: "no-desc" } as unknown as RunAgentInput["context"][number], + { + description: "", + value: "empty", + } as unknown as RunAgentInput["context"][number], + { + description: null, + value: "null", + } as unknown as RunAgentInput["context"][number], + "a string" as unknown as RunAgentInput["context"][number], + null as unknown as RunAgentInput["context"][number], + ], + }), + ); + expect(Object.keys(r.context)).toEqual([]); + }); + + it("blocks prototype-pollution keys", () => { + const r = buildContextExtras( + input({ + context: [ + { + description: "__proto__", + value: "nope", + } as unknown as RunAgentInput["context"][number], + { + description: "constructor", + value: "nope", + } as unknown as RunAgentInput["context"][number], + { + description: "prototype", + value: "nope", + } as unknown as RunAgentInput["context"][number], + { + description: "ok", + value: "yes", + } as unknown as RunAgentInput["context"][number], + ], + }), + ); + expect(Object.keys(r.context)).toEqual(["ok"]); + // And the returned object has no prototype at all — sanity-check. + expect(Object.getPrototypeOf(r.context)).toBeNull(); + }); + + it("uses forwardedProps verbatim when it's an object", () => { + const r = buildContextExtras( + input({ forwardedProps: { auth: "Bearer ...", tenantId: "t-1" } }), + ); + expect(r.forwardedProps).toEqual({ auth: "Bearer ...", tenantId: "t-1" }); + }); + + it("empty defaults when fields are missing or wrong-typed", () => { + const empty = buildContextExtras(input({})); + expect(Object.keys(empty.context)).toEqual([]); + expect(empty.forwardedProps).toEqual({}); + expect( + buildContextExtras( + input({ + forwardedProps: ["not", "an", "object"] as unknown as Record< + string, + unknown + >, + }), + ).forwardedProps, + ).toEqual({}); + }); + + it("later duplicate keys overwrite earlier ones", () => { + const r = buildContextExtras( + input({ + context: [ + { + description: "locale", + value: "en", + } as unknown as RunAgentInput["context"][number], + { + description: "locale", + value: "en-GB", + } as unknown as RunAgentInput["context"][number], + ], + }), + ); + expect(r.context.locale).toBe("en-GB"); + }); + + it("coerces non-string values to string", () => { + const r = buildContextExtras( + input({ + context: [ + { + description: "n", + value: 42 as unknown as string, + } as unknown as RunAgentInput["context"][number], + ], + }), + ); + expect(r.context.n).toBe("42"); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/continue-after-frontend.test.ts b/integrations/aws-strands/typescript/src/__tests__/continue-after-frontend.test.ts new file mode 100644 index 0000000000..8500bac120 --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/continue-after-frontend.test.ts @@ -0,0 +1,226 @@ +/** + * `ToolBehavior.continueAfterFrontendCall = true` must keep the stream + * alive after a frontend tool call completes. Without the flag, the + * adapter sets `pendingHalt` after emitting TOOL_CALL_END and silences + * subsequent events (including any trailing text). With the flag, the + * adapter must NOT halt — subsequent text deltas should flow through to + * the client. + */ + +import { describe, it, expect } from "vitest"; +import { ToolUseBlock } from "@strands-agents/sdk"; +import type { AgentStreamEvent } from "@strands-agents/sdk"; +import { EventType, type RunAgentInput } from "@ag-ui/core"; + +import { + collect, + minimalRunInput, + scriptedStrandsAgent, + stream, +} from "./helpers"; + +function frontendToolInput(): RunAgentInput { + return minimalRunInput({ + messages: [{ id: "u1", role: "user", content: "do a thing" }], + tools: [ + { + name: "set_color", + description: "Sets a UI color.", + parameters: { + type: "object", + properties: { color: { type: "string" } }, + required: ["color"], + }, + }, + ], + }); +} + +/** + * Realistic Strands stream shape for a frontend tool call: + * 1. ToolUseBlock emitted for `set_color` + * 2. afterToolCallEvent fires with Strands' placeholder proxy result + * ("Forwarded to client") — this is the signal that flips + * pendingHalt → haltEventStream when the flag is off. + * 3. A text delta Strands would stream after the tool — should be + * suppressed in default halt mode, passed through with continue flag. + */ +const scriptedEvents: AgentStreamEvent[] = [ + new ToolUseBlock({ + name: "set_color", + toolUseId: "fe-1", + input: { color: "red" }, + }) as unknown as AgentStreamEvent, + { + type: "afterToolCallEvent", + toolUse: { toolUseId: "fe-1", name: "set_color", input: { color: "red" } }, + tool: undefined, + result: { + toolUseId: "fe-1", + status: "success", + content: [{ text: "Forwarded to client" }], + }, + } as unknown as AgentStreamEvent, + stream.textDelta("after-tool"), + stream.blockStop(), +]; + +describe("continueAfterFrontendCall", () => { + it("default (halt): trailing text after a frontend tool call is suppressed", async () => { + const agent = scriptedStrandsAgent(scriptedEvents); + // No override — default is halt after frontend tool call. + const events = await collect(agent, frontendToolInput()); + const k = events.map((e) => e.type); + expect(k).toContain(EventType.TOOL_CALL_START); + expect(k).toContain(EventType.TOOL_CALL_END); + // Trailing text MUST NOT reach the client. + const content = events + .filter((e) => e.type === EventType.TEXT_MESSAGE_CONTENT) + .map((e) => (e as unknown as { delta: string }).delta) + .join(""); + expect(content).not.toContain("after-tool"); + }); + + it("continueAfterFrontendCall=true: trailing text IS delivered to the client", async () => { + const agent = scriptedStrandsAgent(scriptedEvents); + (agent as unknown as { config: Record }).config = { + toolBehaviors: { + set_color: { continueAfterFrontendCall: true }, + }, + }; + const events = await collect(agent, frontendToolInput()); + const k = events.map((e) => e.type); + expect(k).toContain(EventType.TOOL_CALL_END); + const content = events + .filter((e) => e.type === EventType.TEXT_MESSAGE_CONTENT) + .map((e) => (e as unknown as { delta: string }).delta) + .join(""); + expect(content).toContain("after-tool"); + }); + + it("swallows 'Stream ended without completing a message' when halting from a frontend tool call", async () => { + // Strands v1.0+ raises when the agent loop halts before a final + // assistant message is produced. Our adapter should treat that as a + // clean end-of-stream (not RUN_ERROR) as long as we've already + // decided to halt because of a frontend tool call. + const block = new ToolUseBlock({ + name: "set_color", + toolUseId: "fe-99", + input: { color: "red" }, + }); + const preEvents: AgentStreamEvent[] = [ + block as unknown as AgentStreamEvent, + // afterToolCallEvent fires before the throw, flipping pendingHalt. + { + type: "afterToolCallEvent", + toolUse: { + toolUseId: "fe-99", + name: "set_color", + input: { color: "red" }, + }, + tool: undefined, + result: { + toolUseId: "fe-99", + status: "success", + content: [{ text: "Forwarded to client" }], + }, + } as unknown as AgentStreamEvent, + ]; + const agent = scriptedStrandsAgent([], { + stubOverrides: { + stream: async function* () { + for (const e of preEvents) yield e; + throw new Error("Stream ended without completing a message"); + } as unknown as import("@strands-agents/sdk").Agent["stream"], + }, + }); + const events = await collect(agent, frontendToolInput()); + const k = events.map((e) => e.type); + expect(k).toContain(EventType.TOOL_CALL_END); + expect(k).not.toContain(EventType.RUN_ERROR); + expect(k[k.length - 1]).toBe(EventType.RUN_FINISHED); + }); + + it("continueAfterFrontendCall=true still skips TOOL_CALL_RESULT for frontend tools", async () => { + // Even when we don't halt, the frontend tool's placeholder result from + // the Strands proxy must not be emitted — the real result comes from + // the client on the next run. + const events: AgentStreamEvent[] = [ + new ToolUseBlock({ + name: "set_color", + toolUseId: "fe-2", + input: { color: "blue" }, + }) as unknown as AgentStreamEvent, + // afterToolCallEvent for the frontend tool — Strands' proxy produces + // a placeholder "Forwarded to client" result that must be suppressed. + { + type: "afterToolCallEvent", + toolUse: { + toolUseId: "fe-2", + name: "set_color", + input: { color: "blue" }, + }, + tool: undefined, + result: { + toolUseId: "fe-2", + status: "success", + content: [{ text: "Forwarded to client" }], + }, + } as unknown as AgentStreamEvent, + ]; + const agent = scriptedStrandsAgent(events); + (agent as unknown as { config: Record }).config = { + toolBehaviors: { + set_color: { continueAfterFrontendCall: true }, + }, + }; + const collected = await collect(agent, frontendToolInput()); + const k = collected.map((e) => e.type); + expect(k).not.toContain(EventType.TOOL_CALL_RESULT); + expect(k).toContain(EventType.TOOL_CALL_END); + }); + + it("DOES surface RUN_ERROR when the stream throws WITHOUT a prior halt signal", async () => { + // Tightness check: the stream-end swallow added for frontend-halt + // parity (agent.ts `if (pendingHalt || haltEventStream)`) must NOT + // mask real model failures. Stream throws outside a halt context → + // RUN_ERROR must flow back to the client. + const agent = scriptedStrandsAgent([], { + stubOverrides: { + stream: async function* () { + throw new Error("Bedrock upstream 500: internal server error"); + } as unknown as import("@strands-agents/sdk").Agent["stream"], + }, + }); + const events = await collect(agent); + const k = events.map((e) => e.type); + const err = events.find( + (e) => e.type === EventType.RUN_ERROR, + ) as unknown as { code?: string; message?: string } | undefined; + expect(err).toBeDefined(); + expect(err?.code).toBe("STRANDS_ERROR"); + expect(err?.message).toContain("Bedrock upstream 500"); + // And no false RUN_FINISHED — the error is the terminator. + expect(k[k.length - 1]).toBe(EventType.RUN_ERROR); + }); + + it("surfaces RUN_ERROR when the stream throws with the Strands 'Stream ended' message but no pending halt", async () => { + // Also make sure we're not doing a naive string match on the error + // message — the swallow must only fire when the halt flags are set, + // regardless of what the thrown message says. + const agent = scriptedStrandsAgent([], { + stubOverrides: { + stream: async function* () { + // Same error text that triggers the halt-swallow in the earlier + // test, but this time no frontend tool call / no halt flag. + throw new Error("Stream ended without completing a message"); + } as unknown as import("@strands-agents/sdk").Agent["stream"], + }, + }); + // No frontend tools advertised — adapter has no reason to halt. + const events = await collect(agent); + const err = events.find((e) => e.type === EventType.RUN_ERROR); + expect(err).toBeDefined(); + expect((err as unknown as { code?: string }).code).toBe("STRANDS_ERROR"); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/disconnect-unhandled-rejection.test.ts b/integrations/aws-strands/typescript/src/__tests__/disconnect-unhandled-rejection.test.ts new file mode 100644 index 0000000000..3424602124 --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/disconnect-unhandled-rejection.test.ts @@ -0,0 +1,79 @@ +/** + * A throwing generator finally on client disconnect must not produce an + * unhandled rejection (which crashes the Node process on default + * settings). + */ + +import { describe, it, expect } from "vitest"; +import { EventType, type BaseEvent, type RunAgentInput } from "@ag-ui/core"; +import express from "express"; +import type { AddressInfo } from "net"; + +import { addStrandsExpressEndpoint } from "../endpoint"; +import { StrandsAgent } from "../agent"; +import { minimalRunInput, scriptedAgent } from "./helpers"; + +class BooleanAgent extends StrandsAgent { + constructor() { + super({ agent: scriptedAgent(), name: "boom" }); + } + + async *run(_input: RunAgentInput): AsyncGenerator { + try { + yield { type: EventType.RUN_STARTED, threadId: "t", runId: "r" }; + for (let i = 0; i < 1000; i++) { + yield { + type: EventType.TEXT_MESSAGE_CONTENT, + messageId: "m", + delta: "chunk", + } as BaseEvent; + await new Promise((r) => setTimeout(r, 20)); + } + } finally { + throw new Error("cleanup-hook-boom"); + } + } +} + +describe("endpoint disconnect + throwing generator finally", () => { + it("does NOT surface an unhandled rejection when the generator's finally throws", async () => { + const agent = new BooleanAgent(); + const app = express(); + app.use(express.json({ limit: "1mb" })); + addStrandsExpressEndpoint(app, agent, { path: "/" }); + const server = await new Promise((resolve) => { + const s = app.listen(0, () => resolve(s)); + }); + const port = (server.address() as AddressInfo).port; + + const rejections: unknown[] = []; + const onRejection = (reason: unknown): void => { + rejections.push(reason); + }; + process.on("unhandledRejection", onRejection); + try { + const ctrl = new AbortController(); + const pending = fetch(`http://127.0.0.1:${port}/`, { + method: "POST", + signal: ctrl.signal, + headers: { + "Content-Type": "application/json", + Accept: "text/event-stream", + }, + body: JSON.stringify(minimalRunInput()), + }).catch(() => undefined); + await new Promise((r) => setTimeout(r, 100)); + ctrl.abort(); + await pending; + // Drain the microtask queue + a few more macrotask ticks so any + // unhandled rejection would have surfaced. + await new Promise((r) => setTimeout(r, 200)); + expect(rejections).toEqual([]); + } finally { + process.off("unhandledRejection", onRejection); + await new Promise((resolve, reject) => + server.close((err) => (err ? reject(err) : resolve(undefined))), + ); + } + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/endpoint-accept.test.ts b/integrations/aws-strands/typescript/src/__tests__/endpoint-accept.test.ts new file mode 100644 index 0000000000..0bc8f9b57a --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/endpoint-accept.test.ts @@ -0,0 +1,137 @@ +import { describe, it, expect } from "vitest"; +import { EventType, type BaseEvent, type RunAgentInput } from "@ag-ui/core"; +import express from "express"; +import type { AddressInfo } from "net"; + +import { addStrandsExpressEndpoint } from "../endpoint"; +import { StrandsAgent } from "../agent"; +import { minimalRunInput } from "./helpers"; + +class FixedAgent extends StrandsAgent { + private readonly _events: BaseEvent[]; + constructor(events: BaseEvent[]) { + super({ + agent: { + model: {}, + tools: [], + toolRegistry: { + list: () => [], + add() {}, + get: () => undefined, + remove() {}, + }, + sessionManager: undefined, + } as unknown as import("@strands-agents/sdk").Agent, + name: "fixed", + }); + this._events = events; + } + async *run(_input: RunAgentInput): AsyncGenerator { + for (const e of this._events) yield e; + } +} + +async function startApp(): Promise<{ + port: number; + close: () => Promise; +}> { + const app = express(); + app.use(express.json({ limit: "1mb" })); + addStrandsExpressEndpoint( + app, + new FixedAgent([ + { type: EventType.RUN_STARTED, threadId: "t", runId: "r" }, + { type: EventType.TEXT_MESSAGE_START, messageId: "m", role: "assistant" }, + { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "m", delta: "hi" }, + { type: EventType.TEXT_MESSAGE_END, messageId: "m" }, + { type: EventType.RUN_FINISHED, threadId: "t", runId: "r" }, + ]), + { path: "/" }, + ); + const server = await new Promise((resolve) => { + const s = app.listen(0, () => resolve(s)); + }); + const port = (server.address() as AddressInfo).port; + return { + port, + close: () => + new Promise((resolve, reject) => + server.close((err) => (err ? reject(err) : resolve())), + ), + }; +} + +async function postWithAccept(port: number, accept: string): Promise { + const res = await fetch(`http://127.0.0.1:${port}/`, { + method: "POST", + headers: { "Content-Type": "application/json", Accept: accept }, + body: JSON.stringify(minimalRunInput()), + }); + return res.headers.get("content-type") ?? ""; +} + +describe("addStrandsExpressEndpoint content negotiation", () => { + it("returns SSE for wildcard Accept (does not silently choose protobuf)", async () => { + const { port, close } = await startApp(); + try { + const ct = await postWithAccept(port, "*/*"); + expect(ct.toLowerCase()).toContain("text/event-stream"); + expect(ct.toLowerCase()).not.toContain( + "application/vnd.ag-ui.event+proto", + ); + } finally { + await close(); + } + }); + + it("returns SSE when Accept is missing", async () => { + const { port, close } = await startApp(); + try { + const res = await fetch(`http://127.0.0.1:${port}/`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(minimalRunInput()), + }); + const ct = res.headers.get("content-type") ?? ""; + expect(ct.toLowerCase()).toContain("text/event-stream"); + } finally { + await close(); + } + }); + + it("returns SSE for Accept: text/event-stream", async () => { + const { port, close } = await startApp(); + try { + const ct = await postWithAccept(port, "text/event-stream"); + expect(ct.toLowerCase()).toContain("text/event-stream"); + } finally { + await close(); + } + }); + + it("returns protobuf when the client explicitly asks for it", async () => { + const { port, close } = await startApp(); + try { + const ct = await postWithAccept( + port, + "application/vnd.ag-ui.event+proto", + ); + expect(ct.toLowerCase()).toContain("application/vnd.ag-ui.event+proto"); + } finally { + await close(); + } + }); + + it("still honours protobuf when listed alongside SSE with a q-factor", async () => { + const { port, close } = await startApp(); + try { + const ct = await postWithAccept( + port, + "application/vnd.ag-ui.event+proto;q=1, text/event-stream;q=0.5", + ); + expect(ct.toLowerCase()).toContain("application/vnd.ag-ui.event+proto"); + } finally { + await close(); + } + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/endpoint-capabilities.test.ts b/integrations/aws-strands/typescript/src/__tests__/endpoint-capabilities.test.ts new file mode 100644 index 0000000000..2ade01c701 --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/endpoint-capabilities.test.ts @@ -0,0 +1,151 @@ +import { describe, it, expect } from "vitest"; +import express from "express"; +import type { AddressInfo } from "net"; + +import { + addCapabilities, + capabilitiesFor, + DEFAULT_CAPABILITIES, +} from "../endpoint"; +import { StrandsAgent } from "../agent"; + +async function startApp(configure: (app: express.Express) => void): Promise<{ + port: number; + close: () => Promise; +}> { + const app = express(); + app.use(express.json({ limit: "1mb" })); + configure(app); + const server = await new Promise((resolve) => { + const s = app.listen(0, () => resolve(s)); + }); + const port = (server.address() as AddressInfo).port; + return { + port, + close: () => + new Promise((resolve, reject) => + server.close((err) => (err ? reject(err) : resolve())), + ), + }; +} + +describe("addCapabilities", () => { + it("returns the default capabilities document", async () => { + const { port, close } = await startApp((app) => + addCapabilities(app, "/capabilities"), + ); + try { + const res = await fetch(`http://127.0.0.1:${port}/capabilities`); + expect(res.ok).toBe(true); + const body = await res.json(); + expect(body).toEqual(DEFAULT_CAPABILITIES); + expect(body.events.RUN_STARTED).toBe(true); + expect(body.events.ACTIVITY_SNAPSHOT).toBe(false); + expect(body.features.interrupts).toBe(true); + expect(body.features.protobuf).toBe(true); + } finally { + await close(); + } + }); + + it("merges consumer overrides over the defaults", async () => { + const { port, close } = await startApp((app) => + addCapabilities(app, "/capabilities", { + events: { MESSAGES_SNAPSHOT: true, ACTIVITY_SNAPSHOT: true }, + features: { messagesSnapshot: true }, + }), + ); + try { + const res = await fetch(`http://127.0.0.1:${port}/capabilities`); + const body = await res.json(); + expect(body.events.MESSAGES_SNAPSHOT).toBe(true); + expect(body.events.ACTIVITY_SNAPSHOT).toBe(true); + // Untouched defaults survive the merge. + expect(body.events.RUN_STARTED).toBe(true); + expect(body.features.messagesSnapshot).toBe(true); + expect(body.features.interrupts).toBe(true); + } finally { + await close(); + } + }); + + it("strips unknown override keys (typos don't leak into the JSON)", async () => { + const { port, close } = await startApp((app) => + addCapabilities(app, "/capabilities", { + events: { + RUN_SRARTED: true, // typo + Run_Started: false, // wrong case + RUN_STARTED: true, + } as unknown as Partial<(typeof DEFAULT_CAPABILITIES)["events"]>, + features: { + bogusFeature: true, + } as unknown as Partial<(typeof DEFAULT_CAPABILITIES)["features"]>, + }), + ); + try { + const res = await fetch(`http://127.0.0.1:${port}/capabilities`); + const body = await res.json(); + expect("RUN_SRARTED" in body.events).toBe(false); + expect("Run_Started" in body.events).toBe(false); + expect(body.events.RUN_STARTED).toBe(true); + expect("bogusFeature" in body.features).toBe(false); + } finally { + await close(); + } + }); +}); + +describe("capabilitiesFor / addCapabilities { agent }", () => { + function makeAgent(emitChunkEvents: boolean): StrandsAgent { + return new StrandsAgent({ + agent: { + model: {}, + tools: [], + toolRegistry: { + list: () => [], + add() {}, + get: () => undefined, + remove() {}, + }, + sessionManager: undefined, + } as unknown as import("@strands-agents/sdk").Agent, + name: "cap", + config: { emitChunkEvents }, + }); + } + + it("advertises chunk events when the agent emits chunks", () => { + const caps = capabilitiesFor(makeAgent(true)); + expect(caps.events.TEXT_MESSAGE_CHUNK).toBe(true); + expect(caps.events.TOOL_CALL_CHUNK).toBe(true); + expect(caps.events.REASONING_MESSAGE_CHUNK).toBe(true); + // Triples flip off — the client will NOT observe them in chunk mode. + expect(caps.events.TEXT_MESSAGE_START).toBe(false); + expect(caps.events.TEXT_MESSAGE_CONTENT).toBe(false); + expect(caps.events.TEXT_MESSAGE_END).toBe(false); + expect(caps.events.TOOL_CALL_START).toBe(false); + expect(caps.events.TOOL_CALL_END).toBe(false); + }); + + it("keeps triples when the agent is in default triple mode", () => { + const caps = capabilitiesFor(makeAgent(false)); + expect(caps.events.TEXT_MESSAGE_CHUNK).toBe(false); + expect(caps.events.TEXT_MESSAGE_START).toBe(true); + expect(caps.events.TEXT_MESSAGE_END).toBe(true); + }); + + it("addCapabilities({ agent }) serves the derived matrix", async () => { + const agent = makeAgent(true); + const { port, close } = await startApp((app) => + addCapabilities(app, "/capabilities", { agent }), + ); + try { + const res = await fetch(`http://127.0.0.1:${port}/capabilities`); + const body = await res.json(); + expect(body.events.TEXT_MESSAGE_CHUNK).toBe(true); + expect(body.events.TEXT_MESSAGE_START).toBe(false); + } finally { + await close(); + } + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/endpoint-disconnect.test.ts b/integrations/aws-strands/typescript/src/__tests__/endpoint-disconnect.test.ts new file mode 100644 index 0000000000..44c86f517c --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/endpoint-disconnect.test.ts @@ -0,0 +1,104 @@ +/** + * Client disconnect while streaming must `.return()` the iterator so the + * agent generator's `finally` runs and `_activeRunsByThread` releases the + * slot. + */ + +import { describe, it, expect } from "vitest"; +import { EventType, type BaseEvent, type RunAgentInput } from "@ag-ui/core"; +import express from "express"; +import type { AddressInfo } from "net"; + +import { addStrandsExpressEndpoint } from "../endpoint"; +import { StrandsAgent } from "../agent"; +import { minimalRunInput } from "./helpers"; + +/** Agent that streams slowly forever so we can test mid-stream abort. */ +class SlowEndlessAgent extends StrandsAgent { + public finallyRan = false; + + constructor() { + super({ + agent: { + model: {}, + tools: [], + toolRegistry: { + list: () => [], + add() {}, + get: () => undefined, + remove() {}, + }, + sessionManager: undefined, + } as unknown as import("@strands-agents/sdk").Agent, + name: "slow", + }); + } + + async *run(_input: RunAgentInput): AsyncGenerator { + try { + yield { type: EventType.RUN_STARTED, threadId: "t", runId: "r" }; + for (let i = 0; i < 1000; i++) { + yield { + type: EventType.TEXT_MESSAGE_CONTENT, + messageId: "m", + delta: `chunk-${i}`, + } as BaseEvent; + await new Promise((r) => setTimeout(r, 20)); + } + } finally { + this.finallyRan = true; + } + } +} + +async function startApp(agent: StrandsAgent): Promise<{ + port: number; + close: () => Promise; +}> { + const app = express(); + app.use(express.json({ limit: "1mb" })); + addStrandsExpressEndpoint(app, agent, { path: "/" }); + const server = await new Promise((resolve) => { + const s = app.listen(0, () => resolve(s)); + }); + const port = (server.address() as AddressInfo).port; + return { + port, + close: () => + new Promise((resolve, reject) => + server.close((err) => (err ? reject(err) : resolve())), + ), + }; +} + +describe("client disconnect mid-stream", () => { + it("invokes the agent generator's finally block when the client aborts", async () => { + const agent = new SlowEndlessAgent(); + const { port, close } = await startApp(agent); + try { + const ctrl = new AbortController(); + const pending = fetch(`http://127.0.0.1:${port}/`, { + method: "POST", + signal: ctrl.signal, + headers: { + "Content-Type": "application/json", + Accept: "text/event-stream", + }, + body: JSON.stringify(minimalRunInput()), + }).catch(() => undefined); + // Give the handler time to start streaming, then abort. + await new Promise((r) => setTimeout(r, 150)); + ctrl.abort(); + await pending; + // Give the handler's `res.on('close')` callback time to fire and + // propagate `.return()` into the generator. + const deadline = Date.now() + 2000; + while (!agent.finallyRan && Date.now() < deadline) { + await new Promise((r) => setTimeout(r, 25)); + } + expect(agent.finallyRan).toBe(true); + } finally { + await close(); + } + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/endpoint-validation.test.ts b/integrations/aws-strands/typescript/src/__tests__/endpoint-validation.test.ts new file mode 100644 index 0000000000..e9079acdfe --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/endpoint-validation.test.ts @@ -0,0 +1,425 @@ +import { describe, it, expect } from "vitest"; +import express from "express"; +import type { AddressInfo } from "net"; +import { EventType, type BaseEvent, type RunAgentInput } from "@ag-ui/core"; + +import { addStrandsExpressEndpoint } from "../endpoint"; +import { StrandsAgent } from "../agent"; + +/** + * Stub StrandsAgent: bypasses the real Strands SDK so these tests exercise the + * endpoint's request-boundary validation in isolation. The agent only emits a + * RUN_STARTED / RUN_FINISHED pair so a validation-path request is visibly + * distinct from a successfully-routed one. + */ +class RecordingStrandsAgent extends StrandsAgent { + public readonly seen: RunAgentInput[] = []; + + constructor() { + super({ + agent: { + model: {}, + tools: [], + toolRegistry: { + list: () => [], + add() {}, + get: () => undefined, + remove() {}, + }, + sessionManager: undefined, + } as unknown as import("@strands-agents/sdk").Agent, + name: "recording", + }); + } + + // Overriding `_runRaw` (not `run`) preserves the interrupt/resume gate in + // the parent's `run()` — that's the behavior we want to exercise. + protected async *_runRaw( + input: RunAgentInput, + ): AsyncGenerator { + this.seen.push(input); + yield { + type: EventType.RUN_STARTED, + threadId: input.threadId, + runId: input.runId, + }; + yield { + type: EventType.RUN_FINISHED, + threadId: input.threadId, + runId: input.runId, + }; + } +} + +async function startApp(): Promise<{ + port: number; + agent: RecordingStrandsAgent; + close: () => Promise; +}> { + const app = express(); + app.use(express.json({ limit: "10mb" })); + app.use(express.urlencoded({ extended: false })); + const agent = new RecordingStrandsAgent(); + addStrandsExpressEndpoint(app, agent, { path: "/" }); + const server = await new Promise((resolve) => { + const s = app.listen(0, () => resolve(s)); + }); + const port = (server.address() as AddressInfo).port; + return { + port, + agent, + close: () => + new Promise((resolve, reject) => + server.close((err) => (err ? reject(err) : resolve())), + ), + }; +} + +async function readSse(res: Response): Promise { + const text = await res.text(); + return text + .split("\n\n") + .filter(Boolean) + .map((line) => JSON.parse(line.replace(/^data:\s*/, "")) as BaseEvent); +} + +describe("addStrandsExpressEndpoint request validation", () => { + it("rejects a request with a non-JSON Content-Type (415)", async () => { + const { port, agent, close } = await startApp(); + try { + const res = await fetch(`http://127.0.0.1:${port}/`, { + method: "POST", + headers: { "Content-Type": "application/xml" }, + body: "", + }); + expect(res.status).toBe(415); + expect(agent.seen).toHaveLength(0); + } finally { + await close(); + } + }); + + it("rejects a form-encoded body (415)", async () => { + const { port, agent, close } = await startApp(); + try { + const res = await fetch(`http://127.0.0.1:${port}/`, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: "thread_id=x", + }); + expect(res.status).toBe(415); + expect(agent.seen).toHaveLength(0); + } finally { + await close(); + } + }); + + it("rejects a body missing threadId (400) without invoking the agent", async () => { + const { port, agent, close } = await startApp(); + try { + const res = await fetch(`http://127.0.0.1:${port}/`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + runId: "r", + messages: [], + state: {}, + tools: [], + context: [], + forwardedProps: {}, + }), + }); + expect(res.status).toBe(400); + const body = (await res.json()) as { error: string }; + expect(body.error).toMatch(/Invalid RunAgentInput/); + expect(agent.seen).toHaveLength(0); + } finally { + await close(); + } + }); + + it("accepts a snake_case body by normalizing keys", async () => { + const { port, agent, close } = await startApp(); + try { + const res = await fetch(`http://127.0.0.1:${port}/`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "text/event-stream", + }, + body: JSON.stringify({ + thread_id: "t-snake", + run_id: "r-snake", + messages: [], + state: {}, + tools: [], + context: [], + forwardedProps: {}, + }), + }); + expect(res.status).toBe(200); + const events = await readSse(res as unknown as Response); + expect(events[0].type).toBe(EventType.RUN_STARTED); + expect(agent.seen).toHaveLength(1); + expect(agent.seen[0]?.threadId).toBe("t-snake"); + expect(agent.seen[0]?.runId).toBe("r-snake"); + } finally { + await close(); + } + }); + + it("rejects a body missing runId (400) without invoking the agent", async () => { + const { port, agent, close } = await startApp(); + try { + const res = await fetch(`http://127.0.0.1:${port}/`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + threadId: "t", + messages: [], + state: {}, + tools: [], + context: [], + forwardedProps: {}, + }), + }); + expect(res.status).toBe(400); + const body = (await res.json()) as { issues: { path: unknown[] }[] }; + expect(body.issues.some((i) => i.path.includes("runId"))).toBe(true); + expect(agent.seen).toHaveLength(0); + } finally { + await close(); + } + }); + + it("accepts a Content-Type with +json subtype", async () => { + const { port, agent, close } = await startApp(); + try { + const res = await fetch(`http://127.0.0.1:${port}/`, { + method: "POST", + headers: { + "Content-Type": "application/vnd.custom+json", + Accept: "text/event-stream", + }, + body: JSON.stringify({ + threadId: "t", + runId: "r", + messages: [], + state: {}, + tools: [], + context: [], + forwardedProps: {}, + }), + }); + // Express' json() middleware only parses `application/json` by default, + // so `+json` bodies land here with `req.body` undefined. We still want + // the Content-Type check to let them through (415 is the wrong answer); + // downstream schema validation will report the empty body as a 400. + expect(res.status).toBe(400); + expect(agent.seen).toHaveLength(0); + } finally { + await close(); + } + }); + + it("accepts a Content-Type with charset parameter", async () => { + const { port, agent, close } = await startApp(); + try { + const res = await fetch(`http://127.0.0.1:${port}/`, { + method: "POST", + headers: { + "Content-Type": "application/json; charset=utf-8", + Accept: "text/event-stream", + }, + body: JSON.stringify({ + threadId: "t-charset", + runId: "r-charset", + messages: [], + state: {}, + tools: [], + context: [], + forwardedProps: {}, + }), + }); + expect(res.status).toBe(200); + expect(agent.seen).toHaveLength(1); + expect(agent.seen[0]?.threadId).toBe("t-charset"); + } finally { + await close(); + } + }); + + it("rejects a request with no Content-Type header (415)", async () => { + const { port, agent, close } = await startApp(); + try { + const res = await fetch(`http://127.0.0.1:${port}/`, { + method: "POST", + // fetch() will auto-set `Content-Type: text/plain;charset=UTF-8` for a + // string body. That's still non-JSON, so the 415 path applies. + body: "ignored", + }); + expect(res.status).toBe(415); + expect(agent.seen).toHaveLength(0); + } finally { + await close(); + } + }); + + it("prefers explicit camelCase when both snake_case and camelCase keys are present", async () => { + const { port, agent, close } = await startApp(); + try { + const res = await fetch(`http://127.0.0.1:${port}/`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "text/event-stream", + }, + body: JSON.stringify({ + threadId: "camel-wins", + thread_id: "snake-loses", + runId: "r", + run_id: "r-snake", + messages: [], + state: {}, + tools: [], + context: [], + forwardedProps: {}, + }), + }); + expect(res.status).toBe(200); + expect(agent.seen).toHaveLength(1); + expect(agent.seen[0]?.threadId).toBe("camel-wins"); + expect(agent.seen[0]?.runId).toBe("r"); + } finally { + await close(); + } + }); + + it("surfaces RUN_ERROR when resume[] references an unknown interrupt", async () => { + const { port, close } = await startApp(); + try { + const res = await fetch(`http://127.0.0.1:${port}/`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "text/event-stream", + }, + body: JSON.stringify({ + threadId: "t", + runId: "r", + messages: [], + state: {}, + tools: [], + context: [], + forwardedProps: {}, + resume: [ + { interruptId: "does-not-exist", status: "resolved", payload: {} }, + ], + }), + }); + expect(res.status).toBe(200); + const events = await readSse(res as unknown as Response); + const types = events.map((e) => e.type); + expect(types[0]).toBe(EventType.RUN_STARTED); + expect(types).toContain(EventType.RUN_ERROR); + expect(types).not.toContain(EventType.RUN_FINISHED); + const err = events.find((e) => e.type === EventType.RUN_ERROR) as + | { code?: string; message?: string } + | undefined; + expect(err?.code).toBe("UNKNOWN_INTERRUPT"); + expect(err?.message).toMatch(/does-not-exist/); + } finally { + await close(); + } + }); + + it("allows resume: [] (explicit empty array is not a resume request)", async () => { + const { port, agent, close } = await startApp(); + try { + const res = await fetch(`http://127.0.0.1:${port}/`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "text/event-stream", + }, + body: JSON.stringify({ + threadId: "t", + runId: "r", + messages: [], + state: {}, + tools: [], + context: [], + forwardedProps: {}, + resume: [], + }), + }); + expect(res.status).toBe(200); + const events = await readSse(res as unknown as Response); + const types = events.map((e) => e.type); + expect(types).toContain(EventType.RUN_FINISHED); + expect(types).not.toContain(EventType.RUN_ERROR); + expect(agent.seen).toHaveLength(1); + } finally { + await close(); + } + }); + + it("rejects a malformed JSON body with 4xx", async () => { + const { port, agent, close } = await startApp(); + try { + const res = await fetch(`http://127.0.0.1:${port}/`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: "{not json", + }); + // express.json() surfaces SyntaxError via Express' error handler as 400. + // Any 4xx satisfies the protocol contract (the harness accepts 400-499). + expect(res.status).toBeGreaterThanOrEqual(400); + expect(res.status).toBeLessThan(500); + expect(agent.seen).toHaveLength(0); + } finally { + await close(); + } + }); + + it("rejects a plain-text body under application/json Content-Type with 4xx", async () => { + const { port, agent, close } = await startApp(); + try { + const res = await fetch(`http://127.0.0.1:${port}/`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: "not json at all", + }); + expect(res.status).toBeGreaterThanOrEqual(400); + expect(res.status).toBeLessThan(500); + expect(agent.seen).toHaveLength(0); + } finally { + await close(); + } + }); + + it("rejects a malformed resume[] entry at the schema layer (400)", async () => { + const { port, agent, close } = await startApp(); + try { + const res = await fetch(`http://127.0.0.1:${port}/`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + threadId: "t", + runId: "r", + messages: [], + state: {}, + tools: [], + context: [], + forwardedProps: {}, + // interruptId missing + status not in enum — both schema violations. + resume: [{ status: "pending" }], + }), + }); + expect(res.status).toBe(400); + expect(agent.seen).toHaveLength(0); + } finally { + await close(); + } + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/endpoint.test.ts b/integrations/aws-strands/typescript/src/__tests__/endpoint.test.ts new file mode 100644 index 0000000000..622ff9a6ab --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/endpoint.test.ts @@ -0,0 +1,118 @@ +import { describe, it, expect } from "vitest"; +import { EventType, type BaseEvent, type RunAgentInput } from "@ag-ui/core"; +import express from "express"; +import type { AddressInfo } from "net"; + +import { addStrandsExpressEndpoint, addPing } from "../endpoint"; +import { StrandsAgent } from "../agent"; +import { minimalRunInput } from "./helpers"; + +/** + * Minimal Strands stub that the endpoint's agent.run() iterates over. We + * inject the ready-to-stream events directly so we don't depend on the + * full Strands SDK at this layer. + */ +class FixedStrandsAgent extends StrandsAgent { + private readonly _events: BaseEvent[]; + + constructor(events: BaseEvent[]) { + // We never use the template agent — override run() entirely. + super({ + agent: { + model: {}, + tools: [], + toolRegistry: { + list: () => [], + add() {}, + get: () => undefined, + remove() {}, + }, + sessionManager: undefined, + } as unknown as import("@strands-agents/sdk").Agent, + name: "fixed", + }); + this._events = events; + } + + async *run(_input: RunAgentInput): AsyncGenerator { + for (const e of this._events) { + yield e; + } + } +} + +async function startApp(agent: StrandsAgent): Promise<{ + port: number; + close: () => Promise; +}> { + const app = express(); + app.use(express.json({ limit: "10mb" })); + addStrandsExpressEndpoint(app, agent, { path: "/" }); + addPing(app, "/ping"); + const server = await new Promise((resolve) => { + const s = app.listen(0, () => resolve(s)); + }); + const port = (server.address() as AddressInfo).port; + return { + port, + close: () => + new Promise((resolve, reject) => + server.close((err) => (err ? reject(err) : resolve())), + ), + }; +} + +describe("addStrandsExpressEndpoint", () => { + it("streams SSE frames for each yielded event", async () => { + const agent = new FixedStrandsAgent([ + { type: EventType.RUN_STARTED, threadId: "t", runId: "r" }, + { type: EventType.TEXT_MESSAGE_START, messageId: "m", role: "assistant" }, + { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "m", delta: "hi" }, + { type: EventType.TEXT_MESSAGE_END, messageId: "m" }, + { type: EventType.RUN_FINISHED, threadId: "t", runId: "r" }, + ]); + const { port, close } = await startApp(agent); + try { + const res = await fetch(`http://127.0.0.1:${port}/`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "text/event-stream", + }, + body: JSON.stringify(minimalRunInput()), + }); + expect(res.ok).toBe(true); + expect(res.headers.get("content-type")).toContain("text/event-stream"); + const text = await res.text(); + // SSE frames are "data: {json}\n\n" + const frames = text + .split("\n\n") + .filter(Boolean) + .map((line) => JSON.parse(line.replace(/^data:\s*/, ""))); + expect(frames.map((f) => f.type)).toEqual([ + EventType.RUN_STARTED, + EventType.TEXT_MESSAGE_START, + EventType.TEXT_MESSAGE_CONTENT, + EventType.TEXT_MESSAGE_END, + EventType.RUN_FINISHED, + ]); + } finally { + await close(); + } + }); +}); + +describe("addPing", () => { + it("responds with {status:'healthy'}", async () => { + const agent = new FixedStrandsAgent([]); + const { port, close } = await startApp(agent); + try { + const res = await fetch(`http://127.0.0.1:${port}/ping`); + expect(res.ok).toBe(true); + const body = await res.json(); + expect(body).toEqual({ status: "healthy" }); + } finally { + await close(); + } + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/exports.test.ts b/integrations/aws-strands/typescript/src/__tests__/exports.test.ts new file mode 100644 index 0000000000..efa37e2725 --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/exports.test.ts @@ -0,0 +1,56 @@ +/** Every public helper must be reachable from its package entry point. */ + +import { describe, it, expect } from "vitest"; +import * as pkg from "../index"; +import * as serverPkg from "../server"; + +describe("public export surface", () => { + it("main entry exposes the adapter, proxy helpers, content helpers, and context helper", () => { + const expected = [ + "StrandsAgent", + "AWSStrandsAgent", + "buildSnapshotMessages", + "buildStrandsSeed", + "convertMessagesForStrandsSeed", + "buildContextExtras", + "convertAguiContentToStrands", + "flattenContentToText", + "createProxyTool", + "syncProxyTools", + "isProxyTool", + ]; + for (const name of expected) { + expect(pkg).toHaveProperty(name); + expect((pkg as Record)[name]).toBeDefined(); + } + }); + + it("server subpath exposes the Express transport helpers", () => { + const expected = [ + "createStrandsApp", + "addStrandsExpressEndpoint", + "addPing", + "addCapabilities", + "capabilitiesFor", + "DEFAULT_CAPABILITIES", + ]; + for (const name of expected) { + expect(serverPkg).toHaveProperty(name); + expect((serverPkg as Record)[name]).toBeDefined(); + } + }); + + it("main entry does NOT expose server-side helpers (bundler safety)", () => { + // Keeping these off the main entry lets client bundlers (Next.js, Vite) + // trace this package without pulling in Express / cors. + const serverOnly = [ + "createStrandsApp", + "addStrandsExpressEndpoint", + "addPing", + "addCapabilities", + ]; + for (const name of serverOnly) { + expect(pkg).not.toHaveProperty(name); + } + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/helpers.ts b/integrations/aws-strands/typescript/src/__tests__/helpers.ts new file mode 100644 index 0000000000..0cc821f2b7 --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/helpers.ts @@ -0,0 +1,187 @@ +/** + * Shared test helpers. These mirror the Python test helpers but adapted for + * the TS Strands SDK's streaming shape. + */ + +import type { Agent, AgentStreamEvent } from "@strands-agents/sdk"; +import type { BaseEvent, RunAgentInput } from "@ag-ui/core"; + +import { StrandsAgent } from "../agent"; +import type { StrandsAgentConfig } from "../config"; + +export function minimalRunInput( + overrides: Partial = {}, +): RunAgentInput { + return { + threadId: overrides.threadId ?? "thread-1", + runId: overrides.runId ?? "run-1", + state: overrides.state ?? {}, + messages: overrides.messages ?? [], + tools: overrides.tools ?? [], + context: overrides.context ?? [], + forwardedProps: overrides.forwardedProps, + ...overrides, + }; +} + +/** + * Builds a fake `Tool` instance whose identity we can assert on without + * actually driving a Strands Agent. Matches the minimal Tool contract + * (`name`, `description`, `toolSpec`, async `stream`). + */ +export function fakeTool(name: string, description = "") { + return { + name, + description, + toolSpec: { + name, + description, + inputSchema: { json: {} }, + }, + // eslint-disable-next-line require-yield + async *stream() { + return { toolUseId: "x", status: "success" as const, content: [] }; + }, + }; +} + +/** + * Fake Strands `Agent` stub that yields a scripted stream of events. Covers + * the attributes the adapter reads (`model`, `tools`, `toolRegistry`, async + * `stream()`); every other field on `Agent` is not exercised in tests and + * stays unset. `overrides` lets an individual test swap in a custom `stream` + * or expose extra state (e.g. to capture the args passed to `stream`). + */ +export function scriptedAgent( + events: AgentStreamEvent[] | unknown[] = [], + overrides: Partial & Record = {}, +): Agent { + const tools = new Map(); + const registry = { + add: (t: unknown) => { + const name = (t as { name?: string })?.name; + if (name) tools.set(name, t); + }, + get: (n: string) => tools.get(n), + getByName: (n: string) => tools.get(n), + remove: (t: unknown) => { + const name = typeof t === "string" ? t : (t as { name?: string })?.name; + if (name) tools.delete(name); + }, + removeByName: (n: string) => tools.delete(n), + values: () => Array.from(tools.values()), + }; + return { + model: { name: "stub-model", modelId: "stub-model" }, + tools: [], + toolRegistry: registry, + async *stream(_args: unknown) { + for (const e of events) yield e; + }, + ...overrides, + } as unknown as Agent; +} + +/** + * Build a StrandsAgent wrapping a scripted stub and seed `_agentsByThread` + * with the stub for both `"thread-1"` and `"default"`, so the scripted stream + * fires regardless of which threadId the test's RunAgentInput carries. This + * is the pattern ~90% of adapter tests need; tests that want the real + * per-thread cloning path (e.g. session-manager tests) should build the + * StrandsAgent directly. + */ +export function scriptedStrandsAgent( + events: AgentStreamEvent[] | unknown[] = [], + options: { + config?: StrandsAgentConfig; + name?: string; + stubOverrides?: Partial & Record; + } = {}, +): StrandsAgent { + const stub = scriptedAgent(events, options.stubOverrides); + const sa = new StrandsAgent({ + agent: stub, + name: options.name ?? "test", + config: options.config, + }); + const byThread = (sa as unknown as { _agentsByThread: Map }) + ._agentsByThread; + byThread.set("thread-1", stub); + byThread.set("default", stub); + return sa; +} + +/** Iterate `agent.run()` into an array. Defaults to `minimalRunInput()`. */ +export async function collect( + agent: StrandsAgent, + input: RunAgentInput = minimalRunInput(), +): Promise { + const out: BaseEvent[] = []; + for await (const e of agent.run(input)) out.push(e); + return out; +} + +/** + * Factories for the TS Strands SDK's AgentStreamEvent shapes the adapter + * consumes. Centralized so SDK-shape changes update one place. + */ +export const stream = { + textDelta: (text: string): AgentStreamEvent => + ({ + type: "modelContentBlockDeltaEvent", + delta: { type: "textDelta", text }, + }) as unknown as AgentStreamEvent, + + reasoningDelta: (text: string): AgentStreamEvent => + ({ + type: "modelContentBlockDeltaEvent", + delta: { type: "reasoningContentDelta", text }, + }) as unknown as AgentStreamEvent, + + reasoningRedacted: (redactedContent: Uint8Array): AgentStreamEvent => + ({ + type: "modelContentBlockDeltaEvent", + delta: { type: "reasoningContentDelta", redactedContent }, + }) as unknown as AgentStreamEvent, + + toolUseStart: (toolUseId: string, name: string): AgentStreamEvent => + ({ + type: "modelContentBlockStartEvent", + start: { type: "toolUseStart", toolUseId, name }, + }) as unknown as AgentStreamEvent, + + toolUseDelta: (input: string): AgentStreamEvent => + ({ + type: "modelContentBlockDeltaEvent", + delta: { type: "toolUseInputDelta", input }, + }) as unknown as AgentStreamEvent, + + blockStop: (): AgentStreamEvent => + ({ type: "modelContentBlockStopEvent" }) as unknown as AgentStreamEvent, + + beforeNode: (nodeId: string, nodeType = "agent"): AgentStreamEvent => + ({ + type: "beforeNodeCallEvent", + nodeId, + nodeType, + }) as unknown as AgentStreamEvent, + + afterNode: (nodeId: string, nodeType = "agent"): AgentStreamEvent => + ({ + type: "afterNodeCallEvent", + nodeId, + nodeType, + }) as unknown as AgentStreamEvent, + + handoff: ( + source: string, + targets: string[], + message?: string, + ): AgentStreamEvent => + ({ + type: "multiAgentHandoffEvent", + source, + targets, + message, + }) as unknown as AgentStreamEvent, +}; diff --git a/integrations/aws-strands/typescript/src/__tests__/history-replay.test.ts b/integrations/aws-strands/typescript/src/__tests__/history-replay.test.ts new file mode 100644 index 0000000000..3ec87d4f8c --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/history-replay.test.ts @@ -0,0 +1,138 @@ +/** + * History reconciliation (replayHistoryIntoStrands). Fixes the "chart loops + * forever" symptom: without replay, the LLM never sees the client-produced + * tool result on the next turn and re-fires the same tool. + * + * Python parity: adapter mirrors agent.py's _build_strands_history and + * stream_async(None) flow. + */ + +import { describe, it, expect } from "vitest"; +import { StrandsAgent } from "../agent"; +import type { StrandsAgentConfig } from "../config"; +import { collect, minimalRunInput, scriptedAgent } from "./helpers"; + +function recordingAgent() { + const calls: { args: unknown; messages: unknown[] }[] = []; + const stub = scriptedAgent([], { + messages: [] as never, + sessionManager: undefined as never, + stream: async function* (args: unknown) { + calls.push({ + args, + messages: [...(stub as unknown as { messages: unknown[] }).messages], + }); + } as unknown as import("@strands-agents/sdk").Agent["stream"], + }); + return { stub, calls }; +} + +function makeAgent( + stub: import("@strands-agents/sdk").Agent, + config?: StrandsAgentConfig, +): StrandsAgent { + const sa = new StrandsAgent({ agent: stub, name: "t", config }); + const byThread = (sa as unknown as { _agentsByThread: Map }) + ._agentsByThread; + byThread.set("thread-1", stub); + byThread.set("default", stub); + return sa; +} + +describe("replayHistoryIntoStrands", () => { + it("rebuilds agent.messages before stream() and calls stream(undefined)", async () => { + const { stub, calls } = recordingAgent(); + const agent = makeAgent(stub); + await collect( + agent, + minimalRunInput({ + messages: [ + { id: "u1", role: "user", content: "hello" }, + { id: "a1", role: "assistant", content: "hi" }, + { id: "u2", role: "user", content: "another" }, + ], + }), + ); + expect(calls).toHaveLength(1); + // stream(undefined) is the signal to Strands: "use my this.messages as-is". + expect(calls[0]!.args).toBeUndefined(); + expect(calls[0]!.messages).toHaveLength(3); + }); + + it("renders prior tool_calls as toolUse ContentBlocks so the LLM sees them", async () => { + const { stub, calls } = recordingAgent(); + const agent = makeAgent(stub); + await collect( + agent, + minimalRunInput({ + messages: [ + { id: "u1", role: "user", content: "do something" }, + { + id: "a1", + role: "assistant", + content: "", + toolCalls: [ + { + id: "tc1", + type: "function", + function: { name: "render_chart", arguments: '{"x":1}' }, + }, + ], + }, + { id: "t1", role: "tool", content: "ok", toolCallId: "tc1" }, + ], + }), + ); + const history = calls[0]!.messages as Array<{ + role: string; + content: unknown[]; + }>; + // 3 turns: user, assistant(toolUse), user(toolResult) + expect(history).toHaveLength(3); + expect(history[1]!.role).toBe("assistant"); + // Message.fromMessageData converts plain { toolUse: {...} } objects to + // ToolUseBlock instances — inspect fields on the instance directly. + const toolUseBlock = history[1]!.content[0] as { + type: string; + toolUseId: string; + name: string; + }; + expect(toolUseBlock.type).toBe("toolUseBlock"); + expect(toolUseBlock.toolUseId).toBe("tc1"); + expect(toolUseBlock.name).toBe("render_chart"); + expect(history[2]!.role).toBe("user"); + expect((history[2]!.content[0] as { type: string }).type).toBe( + "toolResultBlock", + ); + }); + + it("is disabled when replayHistoryIntoStrands=false", async () => { + const { stub, calls } = recordingAgent(); + const agent = makeAgent(stub, { replayHistoryIntoStrands: false }); + await collect( + agent, + minimalRunInput({ + messages: [{ id: "u1", role: "user", content: "hello" }], + }), + ); + // Falls back to the legacy path: stream("hello"), agent.messages empty. + expect(calls[0]!.args).toBe("hello"); + expect(calls[0]!.messages).toHaveLength(0); + }); + + it("is disabled when the agent has a session manager (Strands owns history)", async () => { + const { stub, calls } = recordingAgent(); + (stub as { sessionManager: unknown }).sessionManager = { + // presence is enough — adapter only checks truthiness + }; + const agent = makeAgent(stub); + await collect( + agent, + minimalRunInput({ + messages: [{ id: "u1", role: "user", content: "hello" }], + }), + ); + expect(calls[0]!.args).toBe("hello"); + expect(calls[0]!.messages).toHaveLength(0); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/hook-error-logging.test.ts b/integrations/aws-strands/typescript/src/__tests__/hook-error-logging.test.ts new file mode 100644 index 0000000000..6052ed4016 --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/hook-error-logging.test.ts @@ -0,0 +1,102 @@ +/** + * Hook exceptions must be logged with the raw Error object so Node prints + * the stack trace, not `String(e)` which produces "Error: boom" with no + * context. + */ + +import { describe, it, expect, vi, afterEach } from "vitest"; +import { EventType } from "@ag-ui/core"; +import { collect, minimalRunInput, scriptedStrandsAgent } from "./helpers"; + +describe("hook error logging", () => { + let spy: ReturnType; + + afterEach(() => { + spy?.mockRestore(); + }); + + it("stateContextBuilder exception logs the Error object", async () => { + spy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const agent = scriptedStrandsAgent([]); + (agent as unknown as { config: Record }).config = { + stateContextBuilder: () => { + throw new Error("builder bombed"); + }, + }; + await collect( + agent, + minimalRunInput({ + messages: [{ id: "u1", role: "user", content: "hi" }], + }), + ); + // First arg is the prefix string, second is the Error itself. + expect(spy).toHaveBeenCalled(); + const lastCall = spy.mock.calls.find((c: unknown[]) => + String(c[0] ?? "").includes("stateContextBuilder"), + ); + expect(lastCall).toBeTruthy(); + expect(lastCall?.[1]).toBeInstanceOf(Error); + expect((lastCall?.[1] as Error).message).toBe("builder bombed"); + }); + + it("stateFromArgs exception logs the Error object", async () => { + spy = vi.spyOn(console, "warn").mockImplementation(() => {}); + // Emit a tool-use block so the hook site fires. + const { ToolUseBlock } = await import("@strands-agents/sdk"); + const block = new ToolUseBlock({ + name: "Multiply", + toolUseId: "u1", + input: { a: 1, b: 2 }, + }); + const agent = scriptedStrandsAgent([block]); + (agent as unknown as { config: Record }).config = { + toolBehaviors: { + Multiply: { + stateFromArgs: () => { + throw new Error("args hook bombed"); + }, + }, + }, + }; + await collect(agent); + const lastCall = spy.mock.calls.find((c: unknown[]) => + String(c[0] ?? "").includes("stateFromArgs"), + ); + expect(lastCall).toBeTruthy(); + expect(lastCall?.[1]).toBeInstanceOf(Error); + expect((lastCall?.[1] as Error).message).toBe("args hook bombed"); + }); + + it("argsStreamer exception logs the Error and still emits fallback args", async () => { + spy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const { ToolUseBlock } = await import("@strands-agents/sdk"); + const block = new ToolUseBlock({ + name: "Multiply", + toolUseId: "u1", + input: { a: 1, b: 2 }, + }); + const agent = scriptedStrandsAgent([block]); + // eslint-disable-next-line require-yield + (agent as unknown as { config: Record }).config = { + toolBehaviors: { + Multiply: { + argsStreamer: async function* () { + throw new Error("streamer bombed"); + }, + }, + }, + }; + const events = await collect(agent); + const lastCall = spy.mock.calls.find((c: unknown[]) => + String(c[0] ?? "").includes("argsStreamer"), + ); + expect(lastCall).toBeTruthy(); + expect(lastCall?.[1]).toBeInstanceOf(Error); + expect((lastCall?.[1] as Error).message).toBe("streamer bombed"); + // Fallback TOOL_CALL_ARGS should still fire with the full args blob. + const args = events.find( + (e) => e.type === EventType.TOOL_CALL_ARGS, + ) as unknown as { delta: string }; + expect(JSON.parse(args.delta)).toEqual({ a: 1, b: 2 }); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/hook-provider.test.ts b/integrations/aws-strands/typescript/src/__tests__/hook-provider.test.ts new file mode 100644 index 0000000000..407437811e --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/hook-provider.test.ts @@ -0,0 +1,218 @@ +/** + * Tests for hook provider independence across threads. + * + * Port of Python's test_template_hooks_preservation.py — validates that + * per-thread agents receive independent hook/config state. + */ + +import { describe, it, expect } from "vitest"; +import { ToolUseBlock, TextBlock, ToolResultBlock } from "@strands-agents/sdk"; +import type { AgentStreamEvent } from "@strands-agents/sdk"; +import { EventType } from "@ag-ui/core"; + +import { StrandsAgent } from "../agent"; +import type { StrandsAgentConfig, ToolResultContext } from "../config"; +import { + collect, + minimalRunInput, + scriptedAgent, + scriptedStrandsAgent, +} from "./helpers"; + +function injectThread( + agent: StrandsAgent, + threadId: string, + stub: import("@strands-agents/sdk").Agent, +): void { + const byThread = ( + agent as unknown as { _agentsByThread: Map } + )._agentsByThread; + byThread.set(threadId, stub); +} + +describe("Hook provider — stateFromResult independence across threads", () => { + it("stateFromResult fires independently per thread", async () => { + const callLog: { threadId: string; result: unknown }[] = []; + + const config: StrandsAgentConfig = { + toolBehaviors: { + my_tool: { + stateFromResult: (ctx: ToolResultContext) => { + callLog.push({ + threadId: ctx.inputData.threadId!, + result: ctx.resultData, + }); + return { counter: callLog.length }; + }, + }, + }, + }; + + const makeEvents = (resultValue: unknown): AgentStreamEvent[] => { + const block = new ToolUseBlock({ + name: "my_tool", + toolUseId: "t1", + input: {}, + }); + const result = new ToolResultBlock({ + toolUseId: "t1", + status: "success", + content: [new TextBlock(JSON.stringify(resultValue))], + }); + return [ + block as unknown as AgentStreamEvent, + { + type: "afterToolCallEvent", + toolUse: { toolUseId: "t1", name: "my_tool", input: {} }, + result, + } as unknown as AgentStreamEvent, + ]; + }; + + const agent = new StrandsAgent({ + agent: scriptedAgent(), + name: "test", + config, + }); + injectThread(agent, "thread-A", scriptedAgent(makeEvents({ x: 1 }))); + injectThread(agent, "thread-B", scriptedAgent(makeEvents({ x: 2 }))); + + await collect(agent, minimalRunInput({ threadId: "thread-A" })); + await collect(agent, minimalRunInput({ threadId: "thread-B" })); + + expect(callLog).toHaveLength(2); + expect(callLog[0].threadId).toBe("thread-A"); + expect(callLog[0].result).toEqual({ x: 1 }); + expect(callLog[1].threadId).toBe("thread-B"); + expect(callLog[1].result).toEqual({ x: 2 }); + }); + + it("customResultHandler fires independently per thread", async () => { + const handlerLog: string[] = []; + + const config: StrandsAgentConfig = { + toolBehaviors: { + my_tool: { + async *customResultHandler(ctx: ToolResultContext) { + handlerLog.push(ctx.inputData.threadId!); + yield { + type: EventType.CUSTOM, + name: "Hook", + value: ctx.inputData.threadId, + }; + }, + }, + }, + }; + + const makeEvents = (): AgentStreamEvent[] => { + const block = new ToolUseBlock({ + name: "my_tool", + toolUseId: "t1", + input: {}, + }); + const result = new ToolResultBlock({ + toolUseId: "t1", + status: "success", + content: [new TextBlock('"ok"')], + }); + return [ + block as unknown as AgentStreamEvent, + { + type: "afterToolCallEvent", + toolUse: { toolUseId: "t1", name: "my_tool", input: {} }, + result, + } as unknown as AgentStreamEvent, + ]; + }; + + const agent = new StrandsAgent({ + agent: scriptedAgent(), + name: "test", + config, + }); + injectThread(agent, "thread-X", scriptedAgent(makeEvents())); + injectThread(agent, "thread-Y", scriptedAgent(makeEvents())); + + const eventsX = await collect( + agent, + minimalRunInput({ threadId: "thread-X" }), + ); + const eventsY = await collect( + agent, + minimalRunInput({ threadId: "thread-Y" }), + ); + + expect(handlerLog).toEqual(["thread-X", "thread-Y"]); + + const customX = eventsX.find( + (e) => + e.type === EventType.CUSTOM && + (e as unknown as { name: string }).name === "Hook", + ) as unknown as { value: string }; + const customY = eventsY.find( + (e) => + e.type === EventType.CUSTOM && + (e as unknown as { name: string }).name === "Hook", + ) as unknown as { value: string }; + + expect(customX.value).toBe("thread-X"); + expect(customY.value).toBe("thread-Y"); + }); +}); + +describe("Hook provider — argsStreamer per-tool isolation", () => { + it("argsStreamer fires only for the configured tool", async () => { + const streamerLog: string[] = []; + + const config: StrandsAgentConfig = { + toolBehaviors: { + streamed_tool: { + async *argsStreamer(ctx) { + streamerLog.push(ctx.toolName); + yield '{"partial":'; + yield '"value"}'; + }, + }, + }, + }; + + const block1 = new ToolUseBlock({ + name: "streamed_tool", + toolUseId: "s1", + input: { partial: "value" }, + }); + const block2 = new ToolUseBlock({ + name: "other_tool", + toolUseId: "s2", + input: { foo: 1 }, + }); + + const agent = scriptedStrandsAgent( + [ + block1 as unknown as AgentStreamEvent, + block2 as unknown as AgentStreamEvent, + ], + { config }, + ); + + const events = await collect(agent); + expect(streamerLog).toEqual(["streamed_tool"]); + + // streamed_tool should have 2 TOOL_CALL_ARGS events (from the streamer) + const argsEvents = events.filter( + (e) => + e.type === EventType.TOOL_CALL_ARGS && + (e as unknown as { toolCallId: string }).toolCallId === "s1", + ); + expect(argsEvents).toHaveLength(2); + + // other_tool should have 1 TOOL_CALL_ARGS event (default full args) + const otherArgs = events.filter( + (e) => + e.type === EventType.TOOL_CALL_ARGS && + (e as unknown as { toolCallId: string }).toolCallId === "s2", + ); + expect(otherArgs).toHaveLength(1); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/interrupt-native.test.ts b/integrations/aws-strands/typescript/src/__tests__/interrupt-native.test.ts new file mode 100644 index 0000000000..e49cae033e --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/interrupt-native.test.ts @@ -0,0 +1,210 @@ +/** + * Native interrupt flow (Strands SDK 1.1.0+): when the underlying + * `AgentResult` comes back with `stopReason === "interrupt"`, the adapter + * emits the interrupt-variant `RUN_FINISHED` and records the interrupt IDs + * on the thread so the follow-up `resume[]` request is recognised as known + * (rather than falling into the UNKNOWN_INTERRUPT gate). + */ +import { describe, it, expect } from "vitest"; +import { EventType, type BaseEvent, type RunAgentInput } from "@ag-ui/core"; +import { + AgentResult as StrandsAgentResult, + InterruptResponseContent, + Message as StrandsMessage, + TextBlock, + type Interrupt as StrandsInterrupt, +} from "@strands-agents/sdk"; + +import { StrandsAgent } from "../agent"; +import { collect, minimalRunInput, scriptedAgent } from "./helpers"; + +function makeAgentResultStream( + result: StrandsAgentResult, + events: unknown[] = [], +) { + return async function* () { + for (const e of events) yield e; + return result; + }; +} + +function strandsInterrupt(id: string, name: string): StrandsInterrupt { + // The concrete class is internal; the adapter only reads .id / .name / + // .reason, so a plain object that matches the interface is sufficient. + return { + id, + name, + reason: `Approve ${name}?`, + } as unknown as StrandsInterrupt; +} + +function buildAgentResult(interrupts: StrandsInterrupt[]): StrandsAgentResult { + return new StrandsAgentResult({ + stopReason: "interrupt", + lastMessage: StrandsMessage.fromMessageData({ + role: "assistant", + content: [new TextBlock("awaiting approval").toJSON()], + }), + invocationState: {}, + interrupts, + }); +} + +describe("StrandsAgent native interrupt bridge (Strands SDK 1.1.0+)", () => { + it("emits RUN_FINISHED with outcome.interrupt when Strands stops for interrupt", async () => { + const interrupts = [strandsInterrupt("int-1", "confirm_delete")]; + const stubAgent = scriptedAgent([], { + stream: makeAgentResultStream(buildAgentResult(interrupts)) as never, + }); + const sa = new StrandsAgent({ agent: stubAgent, name: "t" }); + ( + sa as unknown as { _agentsByThread: Map } + )._agentsByThread.set("thread-1", stubAgent); + + const events = await collect(sa); + const finished = events.at(-1) as BaseEvent & { + outcome?: { type: string; interrupts?: unknown[] }; + }; + expect(finished.type).toBe(EventType.RUN_FINISHED); + expect(finished.outcome?.type).toBe("interrupt"); + expect(finished.outcome?.interrupts).toHaveLength(1); + const first = finished.outcome?.interrupts?.[0] as { + id: string; + reason: string; + message?: string; + metadata?: { strandsName?: string }; + }; + expect(first.id).toBe("int-1"); + expect(first.reason).toBe("Approve confirm_delete?"); + expect(first.metadata?.strandsName).toBe("confirm_delete"); + + // The interrupt is now pending on the thread. + const pending = ( + sa as unknown as { + _pendingInterruptsByThread: Map>; + } + )._pendingInterruptsByThread.get("thread-1"); + expect(pending?.has("int-1")).toBe(true); + }); + + it("accepts a matching resume[] and forwards InterruptResponseContent to Strands", async () => { + let capturedArgs: unknown = null; + const stubAgent = scriptedAgent([], { + stream: ((args: unknown) => { + capturedArgs = args; + // After resume, Strands completes normally. + return (async function* () { + return new StrandsAgentResult({ + stopReason: "endTurn", + lastMessage: StrandsMessage.fromMessageData({ + role: "assistant", + content: [new TextBlock("done").toJSON()], + }), + invocationState: {}, + }); + })(); + }) as never, + }); + const sa = new StrandsAgent({ agent: stubAgent, name: "t" }); + ( + sa as unknown as { _agentsByThread: Map } + )._agentsByThread.set("thread-1", stubAgent); + // Seed a pending interrupt on the thread so the gate accepts the resume. + ( + sa as unknown as { + _pendingInterruptsByThread: Map>; + } + )._pendingInterruptsByThread.set("thread-1", new Set(["int-7"])); + const input: RunAgentInput = minimalRunInput({ + resume: [ + { + interruptId: "int-7", + status: "resolved", + payload: { approved: true }, + }, + ], + }); + const events = await collect(sa, input); + + expect(events.map((e) => e.type)).toContain(EventType.RUN_FINISHED); + expect(events.map((e) => e.type)).not.toContain(EventType.RUN_ERROR); + + // Strands received InterruptResponseContent[] as its invoke args. + expect(Array.isArray(capturedArgs)).toBe(true); + const [first] = capturedArgs as InterruptResponseContent[]; + expect(first).toBeInstanceOf(InterruptResponseContent); + expect(first.interruptResponse.interruptId).toBe("int-7"); + expect(first.interruptResponse.response).toEqual({ approved: true }); + + // The pending set was cleared once resume was accepted. + const cleared = ( + sa as unknown as { + _pendingInterruptsByThread: Map>; + } + )._pendingInterruptsByThread.get("thread-1"); + expect(cleared).toBeUndefined(); + }); + + it("still emits UNKNOWN_INTERRUPT when resume[] references an unknown id", async () => { + const stubAgent = scriptedAgent([]); + const sa = new StrandsAgent({ agent: stubAgent, name: "t" }); + // One pending interrupt, but the resume references a different id. + ( + sa as unknown as { + _pendingInterruptsByThread: Map>; + } + )._pendingInterruptsByThread.set("thread-1", new Set(["known"])); + + const events = await collect( + sa, + minimalRunInput({ + resume: [{ interruptId: "unknown-id", status: "resolved" }], + }), + ); + expect(events.map((e) => e.type)).toEqual([ + EventType.RUN_STARTED, + EventType.RUN_ERROR, + ]); + const err = events[1] as unknown as { code: string; message: string }; + expect(err.code).toBe("UNKNOWN_INTERRUPT"); + expect(err.message).toContain("unknown-id"); + }); + + it("forwards a cancelled resume as { status: 'cancelled' }", async () => { + let capturedArgs: unknown = null; + const stubAgent = scriptedAgent([], { + stream: ((args: unknown) => { + capturedArgs = args; + return (async function* () { + return new StrandsAgentResult({ + stopReason: "endTurn", + lastMessage: StrandsMessage.fromMessageData({ + role: "assistant", + content: [new TextBlock("ok").toJSON()], + }), + invocationState: {}, + }); + })(); + }) as never, + }); + const sa = new StrandsAgent({ agent: stubAgent, name: "t" }); + ( + sa as unknown as { _agentsByThread: Map } + )._agentsByThread.set("thread-1", stubAgent); + ( + sa as unknown as { + _pendingInterruptsByThread: Map>; + } + )._pendingInterruptsByThread.set("thread-1", new Set(["ic"])); + + await collect( + sa, + minimalRunInput({ + resume: [{ interruptId: "ic", status: "cancelled" }], + }), + ); + + const [first] = capturedArgs as InterruptResponseContent[]; + expect(first.interruptResponse.response).toEqual({ status: "cancelled" }); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/interrupt-rule-gate.test.ts b/integrations/aws-strands/typescript/src/__tests__/interrupt-rule-gate.test.ts new file mode 100644 index 0000000000..d3d1e9a3e0 --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/interrupt-rule-gate.test.ts @@ -0,0 +1,105 @@ +import { describe, it, expect } from "vitest"; +import { EventType, type BaseEvent, type RunAgentInput } from "@ag-ui/core"; + +import { StrandsAgent } from "../agent"; +import { collect, minimalRunInput, scriptedAgent } from "./helpers"; + +/** + * Interrupt-rule-4 gate lives in `StrandsAgent.run()` above `_runRaw` so any + * subclass that overrides only `_runRaw` still inherits the check and Strands + * isn't spun up for a doomed request. These tests exercise the gate at the + * agent layer directly (no HTTP) to pin its semantics. + */ +class NeverRanAgent extends StrandsAgent { + public rawCalled = 0; + + constructor() { + super({ agent: scriptedAgent(), name: "never" }); + } + + protected async *_runRaw( + input: RunAgentInput, + ): AsyncGenerator { + this.rawCalled += 1; + yield { + type: EventType.RUN_STARTED, + threadId: input.threadId, + runId: input.runId, + }; + yield { + type: EventType.RUN_FINISHED, + threadId: input.threadId, + runId: input.runId, + }; + } +} + +describe("StrandsAgent resume[] gate (interrupts.mdx rule 4)", () => { + it("emits RUN_STARTED then RUN_ERROR and never touches _runRaw", async () => { + const agent = new NeverRanAgent(); + const events = await collect( + agent, + minimalRunInput({ + threadId: "t", + runId: "r", + resume: [ + { interruptId: "unknown-id", status: "resolved", payload: {} }, + ], + }), + ); + expect(agent.rawCalled).toBe(0); + expect(events.map((e) => e.type)).toEqual([ + EventType.RUN_STARTED, + EventType.RUN_ERROR, + ]); + const [started, err] = events as unknown as [ + { threadId: string; runId: string }, + { code: string; message: string }, + ]; + expect(started.threadId).toBe("t"); + expect(started.runId).toBe("r"); + expect(err.code).toBe("UNKNOWN_INTERRUPT"); + expect(err.message).toMatch(/unknown-id/); + }); + + it("echoes up to four unknown interruptIds into the error message", async () => { + const agent = new NeverRanAgent(); + const resume = Array.from({ length: 6 }, (_, i) => ({ + interruptId: `i-${i}`, + status: "resolved" as const, + payload: {}, + })); + const events = await collect(agent, minimalRunInput({ resume })); + const err = events.find( + (e) => e.type === EventType.RUN_ERROR, + ) as unknown as { + message: string; + }; + expect(err.message).toContain("i-0"); + expect(err.message).toContain("i-3"); + // Only the first 4 are quoted; i-4 and i-5 are elided to keep the message + // from unbounded growth. + expect(err.message).not.toContain("i-4"); + expect(err.message).not.toContain("i-5"); + }); + + it("passes empty resume[] through to _runRaw (not a resume request)", async () => { + const agent = new NeverRanAgent(); + const events = await collect(agent, minimalRunInput({ resume: [] })); + expect(agent.rawCalled).toBe(1); + expect(events.map((e) => e.type)).toEqual([ + EventType.RUN_STARTED, + EventType.RUN_FINISHED, + ]); + }); + + it("passes missing resume through to _runRaw", async () => { + const agent = new NeverRanAgent(); + const events = await collect(agent, minimalRunInput()); + expect(agent.rawCalled).toBe(1); + expect(events.map((e) => e.type)).toEqual([ + EventType.RUN_STARTED, + EventType.RUN_FINISHED, + ]); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/messages-snapshot.test.ts b/integrations/aws-strands/typescript/src/__tests__/messages-snapshot.test.ts new file mode 100644 index 0000000000..610eaf0fab --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/messages-snapshot.test.ts @@ -0,0 +1,263 @@ +/** + * MessagesSnapshotEvent emission + message_id rotation. + * + * Four lifecycle splice points (Python parity, PR #1638): + * 1. after the initial STATE_SNAPSHOT + * 2. after each TOOL_CALL_END + * 3. after each TOOL_CALL_RESULT + * 4. after each terminal TEXT_MESSAGE_END + * + * message_id rotates after each assistant snapshot entry is appended so + * CopilotKit v2's id-keyed message map doesn't overwrite an entry with + * its successor (the "orphan ToolMessage → OpenAI role='tool' must follow + * tool_calls" failure mode). + */ + +import { describe, it, expect } from "vitest"; +import { ToolUseBlock, TextBlock, ToolResultBlock } from "@strands-agents/sdk"; +import type { AgentStreamEvent } from "@strands-agents/sdk"; +import { EventType } from "@ag-ui/core"; + +import { buildSnapshotMessages } from "../agent"; +import { + collect, + minimalRunInput, + scriptedStrandsAgent, + stream, +} from "./helpers"; + +describe("MESSAGES_SNAPSHOT — initial seed", () => { + it("emits an initial MESSAGES_SNAPSHOT seeded from RunAgentInput.messages", async () => { + const agent = scriptedStrandsAgent([]); + const events = await collect( + agent, + minimalRunInput({ + messages: [ + { id: "u1", role: "user", content: "hello" }, + { id: "a1", role: "assistant", content: "hi" }, + ], + }), + ); + const snapshots = events.filter( + (e) => e.type === EventType.MESSAGES_SNAPSHOT, + ) as unknown as Array<{ messages: Array> }>; + expect(snapshots.length).toBeGreaterThanOrEqual(1); + const first = snapshots[0]!.messages; + expect(first).toHaveLength(2); + expect(first[0]!.role).toBe("user"); + expect(first[1]!.role).toBe("assistant"); + }); + + it("does NOT emit an initial snapshot when messages[] is empty", async () => { + const agent = scriptedStrandsAgent([]); + const events = await collect(agent); + expect( + events.filter((e) => e.type === EventType.MESSAGES_SNAPSHOT), + ).toHaveLength(0); + }); + + it("is globally suppressed by emitMessagesSnapshot=false", async () => { + const agent = scriptedStrandsAgent([], { + config: { emitMessagesSnapshot: false }, + }); + const events = await collect( + agent, + minimalRunInput({ + messages: [{ id: "u1", role: "user", content: "hi" }], + }), + ); + expect( + events.filter((e) => e.type === EventType.MESSAGES_SNAPSHOT), + ).toHaveLength(0); + }); +}); + +describe("MESSAGES_SNAPSHOT — after tool-call end", () => { + it("appends an AssistantMessage(toolCalls=[…]) after TOOL_CALL_END", async () => { + const block = new ToolUseBlock({ + name: "backend_tool", + toolUseId: "tc1", + input: { q: "why" }, + }); + const agent = scriptedStrandsAgent([block as unknown as AgentStreamEvent]); + const events = await collect(agent); + const snapshots = events.filter( + (e) => e.type === EventType.MESSAGES_SNAPSHOT, + ) as unknown as Array<{ messages: Array> }>; + // Last snapshot should include the assistant tool-call entry. + const last = snapshots[snapshots.length - 1]!.messages; + const assistant = last.find((m) => m.role === "assistant") as { + toolCalls?: Array<{ + id: string; + function: { name: string; arguments: string }; + }>; + }; + expect(assistant.toolCalls).toHaveLength(1); + expect(assistant.toolCalls![0]!.id).toBe("tc1"); + expect(assistant.toolCalls![0]!.function.name).toBe("backend_tool"); + }); + + it("skipMessagesSnapshot suppresses the per-tool snapshot", async () => { + const block = new ToolUseBlock({ + name: "quiet_tool", + toolUseId: "tc2", + input: {}, + }); + const agent = scriptedStrandsAgent([block as unknown as AgentStreamEvent], { + config: { + toolBehaviors: { quiet_tool: { skipMessagesSnapshot: true } }, + }, + }); + const events = await collect(agent); + const snapshots = events.filter( + (e) => e.type === EventType.MESSAGES_SNAPSHOT, + ) as unknown as Array<{ messages: Array> }>; + // No snapshot should contain an assistant with a tool-call entry + // for quiet_tool. + const hasQuietToolCall = snapshots.some((s) => + s.messages.some((m) => { + if (m.role !== "assistant") return false; + const tcs = (m as { toolCalls?: Array<{ function: { name: string } }> }) + .toolCalls; + return tcs?.some((tc) => tc.function.name === "quiet_tool") ?? false; + }), + ); + expect(hasQuietToolCall).toBe(false); + }); +}); + +describe("MESSAGES_SNAPSHOT — after tool result", () => { + it("appends a ToolMessage after TOOL_CALL_RESULT", async () => { + const block = new ToolUseBlock({ + name: "compute", + toolUseId: "tc3", + input: {}, + }); + const result = new ToolResultBlock({ + toolUseId: "tc3", + status: "success", + content: [new TextBlock(JSON.stringify({ answer: 42 }))], + }); + const events: AgentStreamEvent[] = [ + block as unknown as AgentStreamEvent, + { + type: "afterToolCallEvent", + toolUse: { toolUseId: "tc3", name: "compute", input: {} }, + result, + } as unknown as AgentStreamEvent, + ]; + const agent = scriptedStrandsAgent(events); + const output = await collect(agent); + const snapshots = output.filter( + (e) => e.type === EventType.MESSAGES_SNAPSHOT, + ) as unknown as Array<{ messages: Array> }>; + const last = snapshots[snapshots.length - 1]!.messages; + const toolMsg = last.find((m) => m.role === "tool") as { + toolCallId: string; + content: string; + }; + expect(toolMsg).toBeTruthy(); + expect(toolMsg.toolCallId).toBe("tc3"); + expect(toolMsg.content).toContain("42"); + }); +}); + +describe("message_id rotation", () => { + it("back-to-back tool calls in one run produce assistant entries with distinct ids", async () => { + const blockA = new ToolUseBlock({ + name: "tool_a", + toolUseId: "tc-a", + input: {}, + }); + const blockB = new ToolUseBlock({ + name: "tool_b", + toolUseId: "tc-b", + input: {}, + }); + const agent = scriptedStrandsAgent([ + blockA as unknown as AgentStreamEvent, + blockB as unknown as AgentStreamEvent, + ]); + const output = await collect(agent); + const snapshots = output.filter( + (e) => e.type === EventType.MESSAGES_SNAPSHOT, + ) as unknown as Array<{ messages: Array> }>; + const lastSnapshot = snapshots[snapshots.length - 1]!.messages; + const assistantEntries = lastSnapshot.filter( + (m) => m.role === "assistant", + ) as Array<{ + id: string; + toolCalls?: Array<{ function: { name: string } }>; + }>; + expect(assistantEntries).toHaveLength(2); + // Both have tool calls; their ids MUST differ so CopilotKit's id-keyed + // map doesn't overwrite one with the next. + expect(assistantEntries[0]!.id).not.toBe(assistantEntries[1]!.id); + }); + + it("text → tool → text run commits accumulated text to the snapshot with its original id", async () => { + // This replays the canonical sequence that motivated splice points 2 + // and 4: text streams until a tool is announced, we close text and + // snapshot it, then the tool call runs, then the agent text continues. + // The accumulated text must appear in a snapshot with the id that was + // used for TEXT_MESSAGE_START, not the rotated id. + const events: AgentStreamEvent[] = [ + stream.textDelta("opening "), + stream.textDelta("line"), + ]; + const agent = scriptedStrandsAgent(events); + const output = await collect(agent); + const snapshots = output.filter( + (e) => e.type === EventType.MESSAGES_SNAPSHOT, + ) as unknown as Array<{ messages: Array> }>; + const last = snapshots[snapshots.length - 1]!.messages; + const assistant = last.find( + (m) => + m.role === "assistant" && + typeof (m as { content?: unknown }).content === "string", + ) as { content: string } | undefined; + expect(assistant?.content).toBe("opening line"); + }); +}); + +describe("buildSnapshotMessages — standalone helper", () => { + it("normalises tool-call arguments to a JSON string", () => { + const out = buildSnapshotMessages([ + { + id: "a1", + role: "assistant", + content: "", + toolCalls: [ + { + id: "tc1", + type: "function", + function: { name: "foo", arguments: '{"x":1}' }, + }, + ], + }, + ]); + expect(out).toHaveLength(1); + const assistant = out[0] as { + toolCalls?: Array<{ function: { arguments: string } }>; + }; + expect(assistant.toolCalls![0]!.function.arguments).toBe('{"x":1}'); + }); + + it("fabricates ids for entries missing one", () => { + const out = buildSnapshotMessages([ + { id: "", role: "user", content: "hi" } as never, + ]); + expect(out).toHaveLength(1); + expect(typeof (out[0] as { id: string }).id).toBe("string"); + expect((out[0] as { id: string }).id.length).toBeGreaterThan(0); + }); + + it("drops developer / system / reasoning / activity roles", () => { + const out = buildSnapshotMessages([ + { id: "s1", role: "system", content: "sys" } as never, + { id: "d1", role: "developer", content: "dev" } as never, + { id: "r1", role: "reasoning", content: "think" } as never, + ]); + expect(out).toHaveLength(0); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/multiagent-events.test.ts b/integrations/aws-strands/typescript/src/__tests__/multiagent-events.test.ts new file mode 100644 index 0000000000..ba0cb58412 --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/multiagent-events.test.ts @@ -0,0 +1,167 @@ +/** + * Multi-agent events from the TS Strands SDK must translate to + * AG-UI STEP_STARTED / STEP_FINISHED / CUSTOM{MultiAgentHandoff}. + * + * The TS SDK emits hook-event class instances — see + * `@strands-agents/sdk/dist/src/multiagent/events.d.ts`: + * class BeforeNodeCallEvent { type: 'beforeNodeCallEvent'; nodeId } + * class AfterNodeCallEvent { type: 'afterNodeCallEvent'; nodeId, nodeType } + * class MultiAgentHandoffEvent{ type: 'multiAgentHandoffEvent'; source, targets } + * + * The Py adapter emits MultiAgentHandoff CustomEvents with + * { from_nodes: [...], to_nodes: [...] }. TS converts `source` to a single- + * element `from_nodes` array to preserve that wire shape for clients that + * already consume Py events. + */ + +import { describe, it, expect } from "vitest"; +import { EventType } from "@ag-ui/core"; +import type { AgentStreamEvent } from "@strands-agents/sdk"; + +import { collect, scriptedStrandsAgent, stream } from "./helpers"; + +describe("Multi-agent event dispatch", () => { + it("beforeNodeCallEvent → STEP_STARTED uses nodeType prefix", async () => { + const agent = scriptedStrandsAgent([ + stream.beforeNode("researcher", "multiAgent"), + ]); + const events = await collect(agent); + const starts = events.filter( + (e) => e.type === EventType.STEP_STARTED, + ) as unknown as Array<{ stepName: string }>; + expect(starts).toHaveLength(1); + expect(starts[0].stepName).toBe("multiAgent:researcher"); + }); + + it("beforeNodeCallEvent without nodeType falls back to 'agent:' prefix", async () => { + const agent = scriptedStrandsAgent([ + { + type: "beforeNodeCallEvent", + nodeId: "researcher", + } as unknown as AgentStreamEvent, + ]); + const events = await collect(agent); + const starts = events.filter( + (e) => e.type === EventType.STEP_STARTED, + ) as unknown as Array<{ stepName: string }>; + expect(starts).toHaveLength(1); + expect(starts[0].stepName).toBe("agent:researcher"); + }); + + // Spec (events.mdx §StepFinished): "The stepName must match the corresponding + // StepStarted event to properly pair the beginning and end of the step." + // Both START and FINISH must derive their prefix from the same `nodeType` + // source so the pair stays matchable regardless of nodeType value. + it("STEP_STARTED and STEP_FINISHED stepNames match when nodeType is set", async () => { + const agent = scriptedStrandsAgent([ + stream.beforeNode("writer", "multiAgent"), + stream.afterNode("writer", "multiAgent"), + ]); + const events = await collect(agent); + const starts = events.filter( + (e) => e.type === EventType.STEP_STARTED, + ) as unknown as Array<{ stepName: string }>; + const stops = events.filter( + (e) => e.type === EventType.STEP_FINISHED, + ) as unknown as Array<{ stepName: string }>; + expect(starts).toHaveLength(1); + expect(stops).toHaveLength(1); + expect(starts[0].stepName).toBe(stops[0].stepName); + }); + + it("afterNodeCallEvent → STEP_FINISHED with nodeType prefix", async () => { + const agent = scriptedStrandsAgent([ + stream.afterNode("writer", "multiAgent"), + ]); + const events = await collect(agent); + const stops = events.filter( + (e) => e.type === EventType.STEP_FINISHED, + ) as unknown as Array<{ stepName: string }>; + expect(stops).toHaveLength(1); + expect(stops[0].stepName).toBe("multiAgent:writer"); + }); + + it("multiAgentHandoffEvent → CUSTOM{MultiAgentHandoff} with Py-compatible from_nodes/to_nodes", async () => { + const agent = scriptedStrandsAgent([ + stream.handoff("researcher", ["writer", "editor"]), + ]); + const events = await collect(agent); + const customs = events.filter( + (e) => e.type === EventType.CUSTOM, + ) as unknown as Array<{ + name: string; + value: Record; + }>; + expect(customs).toHaveLength(1); + expect(customs[0].name).toBe("MultiAgentHandoff"); + expect(customs[0].value).toMatchObject({ + from_nodes: ["researcher"], + to_nodes: ["writer", "editor"], + }); + }); + + // The Py adapter forwards `message` inside the CustomEvent.value so a frontend + // consuming either adapter can show the handoff caption. + it("multiAgentHandoffEvent forwards the message field (Py parity)", async () => { + const agent = scriptedStrandsAgent([ + stream.handoff("researcher", ["writer"], "Handing off draft to writer"), + ]); + const events = await collect(agent); + const customs = events.filter( + (e) => e.type === EventType.CUSTOM, + ) as unknown as Array<{ value: Record }>; + expect(customs).toHaveLength(1); + expect(customs[0].value.message).toBe("Handing off draft to writer"); + }); + + it("full multi-node sequence produces paired STEP events and a handoff", async () => { + const agent = scriptedStrandsAgent([ + { + type: "beforeNodeCallEvent", + nodeId: "n1", + } as unknown as AgentStreamEvent, + stream.afterNode("n1", "agent"), + stream.handoff("n1", ["n2"]), + { + type: "beforeNodeCallEvent", + nodeId: "n2", + } as unknown as AgentStreamEvent, + stream.afterNode("n2", "agent"), + ]); + const events = await collect(agent); + const starts = events.filter((e) => e.type === EventType.STEP_STARTED); + const stops = events.filter((e) => e.type === EventType.STEP_FINISHED); + const customs = events.filter((e) => e.type === EventType.CUSTOM); + expect(starts).toHaveLength(2); + expect(stops).toHaveLength(2); + expect(customs).toHaveLength(1); + }); + + it("legacy snake_case multiagent_* event names are ignored (TS SDK uses different names)", async () => { + const agent = scriptedStrandsAgent([ + // Neither Py nor TS SDKs yield events in this snake_case shape at + // the TS SDK boundary. The adapter must not match them and must + // let them pass through without crashing. + { + type: "multiagent_node_start", + node_id: "x", + node_type: "agent", + } as unknown as AgentStreamEvent, + { + type: "multiagent_handoff", + from_node_ids: ["a"], + to_node_ids: ["b"], + } as unknown as AgentStreamEvent, + ]); + const events = await collect(agent); + // No STEP or MultiAgentHandoff emitted for legacy snake_case. + expect(events.some((e) => e.type === EventType.STEP_STARTED)).toBe(false); + expect( + events.some( + (e) => + e.type === EventType.CUSTOM && + (e as unknown as { name?: string }).name === "MultiAgentHandoff", + ), + ).toBe(false); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/multimodal-conversion.test.ts b/integrations/aws-strands/typescript/src/__tests__/multimodal-conversion.test.ts new file mode 100644 index 0000000000..0c8aa666a1 --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/multimodal-conversion.test.ts @@ -0,0 +1,149 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import type { InputContent } from "@ag-ui/core"; + +import { convertAguiContentToStrands, flattenContentToText } from "../utils"; + +function b64(input: string): string { + return Buffer.from(input).toString("base64"); +} + +describe("convertAguiContentToStrands", () => { + it("maps TextInputContent to a TextBlock", async () => { + const blocks = await convertAguiContentToStrands([ + { type: "text", text: "hello" }, + ] as InputContent[]); + expect(blocks).toHaveLength(1); + expect((blocks[0] as { type: string }).type).toBe("textBlock"); + expect((blocks[0] as unknown as { text: string }).text).toBe("hello"); + }); + + it("maps ImageInputContent with a data source to an ImageBlock", async () => { + const blocks = await convertAguiContentToStrands([ + { + type: "image", + source: { type: "data", value: b64("PNG"), mimeType: "image/png" }, + }, + ] as InputContent[]); + expect(blocks).toHaveLength(1); + expect((blocks[0] as { type: string }).type).toBe("imageBlock"); + expect((blocks[0] as unknown as { format: string }).format).toBe("png"); + }); + + it("skips images with an unsupported MIME type", async () => { + const blocks = await convertAguiContentToStrands([ + { + type: "image", + source: { type: "data", value: b64("xxx"), mimeType: "image/bmp" }, + }, + ] as InputContent[]); + expect(blocks).toHaveLength(0); + }); + + it("fetches url-sourced images", async () => { + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer, + }); + const original = globalThis.fetch; + globalThis.fetch = fetchMock as unknown as typeof fetch; + try { + const blocks = await convertAguiContentToStrands([ + { + type: "image", + source: { + type: "url", + value: "https://example.test/x.png", + mimeType: "image/png", + }, + }, + ] as InputContent[]); + expect(fetchMock).toHaveBeenCalledOnce(); + expect(blocks).toHaveLength(1); + } finally { + globalThis.fetch = original; + } + }); + + it("maps DocumentInputContent to a DocumentBlock", async () => { + const blocks = await convertAguiContentToStrands([ + { + type: "document", + source: { + type: "data", + value: b64("pdfdata"), + mimeType: "application/pdf", + }, + }, + ] as InputContent[]); + expect(blocks).toHaveLength(1); + expect((blocks[0] as { type: string }).type).toBe("documentBlock"); + }); + + it("maps VideoInputContent to a VideoBlock", async () => { + const blocks = await convertAguiContentToStrands([ + { + type: "video", + source: { type: "data", value: b64("movie"), mimeType: "video/mp4" }, + }, + ] as InputContent[]); + expect(blocks).toHaveLength(1); + expect((blocks[0] as { type: string }).type).toBe("videoBlock"); + }); + + it("skips audio content silently", async () => { + const blocks = await convertAguiContentToStrands([ + { type: "text", text: "before" }, + { + type: "audio", + source: { type: "data", value: b64("sound"), mimeType: "audio/wav" }, + }, + { type: "text", text: "after" }, + ] as InputContent[]); + // Just the two text blocks remain + expect(blocks).toHaveLength(2); + }); + + it("drops items with bad base64 data rather than throwing", async () => { + const blocks = await convertAguiContentToStrands([ + { + type: "image", + source: { + type: "data", + value: "!!!not base64!!!", + mimeType: "image/png", + }, + }, + ] as InputContent[]); + expect(blocks).toEqual([]); + }); +}); + +describe("flattenContentToText", () => { + it("returns a string input as-is", () => { + expect(flattenContentToText("hi")).toBe("hi"); + }); + it("returns empty string for null / undefined", () => { + expect(flattenContentToText(null)).toBe(""); + expect(flattenContentToText(undefined)).toBe(""); + }); + it("joins TextInputContent segments with a space", () => { + expect( + flattenContentToText([ + { type: "text", text: "hello" }, + { type: "text", text: "world" }, + ]), + ).toBe("hello world"); + }); + it("ignores non-text blocks", () => { + expect( + flattenContentToText([ + { type: "text", text: "a" }, + { + type: "image", + source: { type: "data", value: "x", mimeType: "image/png" }, + }, + { type: "text", text: "b" }, + ]), + ).toBe("a b"); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/multimodal-passthrough.test.ts b/integrations/aws-strands/typescript/src/__tests__/multimodal-passthrough.test.ts new file mode 100644 index 0000000000..0c9c3f46d7 --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/multimodal-passthrough.test.ts @@ -0,0 +1,180 @@ +/** + * Multimodal `RunAgentInput.messages[*].content` must be passed to + * `agent.stream()` as `ContentBlock[]`, not flattened to a text string. + * + * The v1.0 Strands SDK's `InvokeArgs` accepts both `string` and + * `ContentBlock[]`, matching the Python adapter's behavior. + */ + +import { describe, it, expect } from "vitest"; +import type { InputContent } from "@ag-ui/core"; +import { StrandsAgent } from "../agent"; +import { collect, minimalRunInput, scriptedAgent } from "./helpers"; + +function b64(s: string): string { + return Buffer.from(s).toString("base64"); +} + +/** + * Build a stub Strands Agent whose `.stream()` records the arguments it + * received, alongside whatever history was seeded onto `agent.messages`. + * History reconciliation (replayHistoryIntoStrands) makes the adapter + * call `stream(undefined)` and move the payload to `agent.messages`, so + * tests need to inspect both to see what actually reached the LLM. + */ +function recordingAgent() { + const calls: { args: unknown; messages: unknown[] }[] = []; + const stub = scriptedAgent([], { + messages: [] as never, + stream: async function* (args: unknown) { + calls.push({ + args, + messages: [...(stub as unknown as { messages: unknown[] }).messages], + }); + } as unknown as import("@strands-agents/sdk").Agent["stream"], + }); + return { stub, calls }; +} + +function makeAgent(stub: import("@strands-agents/sdk").Agent): StrandsAgent { + const sa = new StrandsAgent({ agent: stub, name: "t" }); + const byThread = (sa as unknown as { _agentsByThread: Map }) + ._agentsByThread; + byThread.set("thread-1", stub); + byThread.set("default", stub); + return sa; +} + +describe("multimodal pass-through", () => { + it("passes ContentBlock[] to agent.stream when the message contains an image", async () => { + const { stub, calls } = recordingAgent(); + const agent = makeAgent(stub); + const content: InputContent[] = [ + { type: "text", text: "what is in this image?" }, + { + type: "image", + source: { + type: "data", + value: b64("fake-png-bytes"), + mimeType: "image/png", + }, + }, + ]; + await collect( + agent, + minimalRunInput({ + messages: [{ id: "u1", role: "user", content }], + }), + ); + expect(calls).toHaveLength(1); + // Replay routes multimodal content into agent.messages and calls + // stream(undefined); the `user` turn's content carries a TextBlock + + // ImageBlock pair (as Strands class instances after Message.fromMessageData). + expect(calls[0]!.args).toBeUndefined(); + const replayed = calls[0]!.messages as Array<{ + role: string; + content: Array<{ type: string }>; + }>; + expect(replayed).toHaveLength(1); + expect(replayed[0]!.role).toBe("user"); + expect(replayed[0]!.content).toHaveLength(2); + expect(replayed[0]!.content[0]!.type).toBe("textBlock"); + expect(replayed[0]!.content[1]!.type).toBe("imageBlock"); + }); + + it("falls back to text when ALL media blocks fail conversion (unsupported MIME)", async () => { + const { stub, calls } = recordingAgent(); + const agent = makeAgent(stub); + const content: InputContent[] = [ + { + type: "image", + source: { + type: "data", + value: b64("anything"), + // image/bmp is not in the allowlist — conversion will skip it. + mimeType: "image/bmp", + }, + }, + ]; + await collect( + agent, + minimalRunInput({ + messages: [{ id: "u1", role: "user", content }], + }), + ); + expect(calls).toHaveLength(1); + // Unsupported MIME: conversion yields zero blocks, replay falls back + // to a text-only user turn on agent.messages. + expect(calls[0]!.args).toBeUndefined(); + const replayed = calls[0]!.messages as Array<{ + role: string; + content: Array<{ type: string }>; + }>; + expect(replayed).toHaveLength(1); + expect(replayed[0]!.content).toHaveLength(1); + expect(replayed[0]!.content[0]!.type).toBe("textBlock"); + }); + + it("preserves ContentBlock[] even when stateContextBuilder is configured", async () => { + const { stub, calls } = recordingAgent(); + const agent = makeAgent(stub); + // Install a stateContextBuilder that would wrap text prompts. It MUST NOT + // be applied to multimodal prompts — the image content would be lost. + (agent as unknown as { config: Record }).config = { + stateContextBuilder: (_input: unknown, prompt: string) => + `[STATE: wrapped] ${prompt}`, + }; + const content: InputContent[] = [ + { type: "text", text: "describe the picture" }, + { + type: "image", + source: { + type: "data", + value: b64("fake-jpeg"), + mimeType: "image/jpeg", + }, + }, + ]; + await collect( + agent, + minimalRunInput({ + messages: [{ id: "u1", role: "user", content }], + }), + ); + // The builder runs on the replay path's last user-text turn, not on + // a synthetic prompt — so the multimodal content persists as a proper + // ContentBlock[] on agent.messages[0].content alongside any wrapped + // text block. Assert the image survives the builder. + expect(calls[0]!.args).toBeUndefined(); + const replayed = calls[0]!.messages as Array<{ + role: string; + content: Array<{ type: string }>; + }>; + expect(replayed[0]!.content.some((b) => b.type === "imageBlock")).toBe( + true, + ); + }); + + it("applies stateContextBuilder to plain-text prompts as before", async () => { + const { stub, calls } = recordingAgent(); + const agent = makeAgent(stub); + (agent as unknown as { config: Record }).config = { + stateContextBuilder: (_input: unknown, prompt: string) => + `${prompt} [STATE: ok]`, + }; + await collect( + agent, + minimalRunInput({ + messages: [{ id: "u1", role: "user", content: "plain text prompt" }], + }), + ); + // Replay routes the prompt into agent.messages[*].content[*].text, with + // the builder's augmentation applied. The adapter calls stream(undefined). + expect(calls[0]!.args).toBeUndefined(); + const replayed = calls[0]!.messages as Array<{ + role: string; + content: Array<{ text?: string }>; + }>; + expect(replayed[0]!.content[0]!.text).toBe("plain text prompt [STATE: ok]"); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/orchestrator-path.test.ts b/integrations/aws-strands/typescript/src/__tests__/orchestrator-path.test.ts new file mode 100644 index 0000000000..87d3d83603 --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/orchestrator-path.test.ts @@ -0,0 +1,214 @@ +/** + * StrandsAgent, when given a multi-agent orchestrator (Graph/Swarm) + * instead of an Agent, drives `.stream()` and translates the real TS SDK + * multi-agent event classes into AG-UI STEP_* / MultiAgentHandoff / + * nested TEXT_MESSAGE events. + */ + +import { describe, it, expect } from "vitest"; +import { EventType } from "@ag-ui/core"; +import { StrandsAgent } from "../agent"; +import { collect } from "./helpers"; + +/** + * Fake orchestrator shape: exposes `.stream()` but no `.model` accessor, + * which is how the adapter's constructor discriminates between an Agent + * and a Graph/Swarm. + */ +function fakeOrchestrator(events: unknown[]) { + return { + id: "test-graph", + // No `model` field — triggers the orchestrator code path. + async *stream(_input: string) { + for (const e of events) yield e; + }, + }; +} + +describe("Orchestrator path", () => { + it("drives a fake Graph through the adapter and emits STEP_* / MultiAgentHandoff", async () => { + const stream = fakeOrchestrator([ + { type: "beforeNodeCallEvent", nodeId: "researcher" }, + // Inner agent-level text delta wrapped in a node stream update + { + type: "nodeStreamUpdateEvent", + nodeId: "researcher", + inner: { + source: "agent", + event: { + type: "modelContentBlockDeltaEvent", + delta: { type: "textDelta", text: "Found it." }, + }, + }, + }, + { type: "afterNodeCallEvent", nodeId: "researcher", nodeType: "agent" }, + { + type: "multiAgentHandoffEvent", + source: "researcher", + targets: ["writer"], + }, + { type: "beforeNodeCallEvent", nodeId: "writer" }, + { + type: "nodeStreamUpdateEvent", + nodeId: "writer", + inner: { + source: "agent", + event: { + type: "modelContentBlockDeltaEvent", + delta: { type: "textDelta", text: "Final answer." }, + }, + }, + }, + { type: "afterNodeCallEvent", nodeId: "writer", nodeType: "agent" }, + ]); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const sa = new StrandsAgent({ agent: stream as any, name: "t" }); + const events = await collect(sa); + const kinds = events.map((e) => e.type); + + // Run lifecycle + expect(kinds[0]).toBe(EventType.RUN_STARTED); + expect(kinds[kinds.length - 1]).toBe(EventType.RUN_FINISHED); + + // Two STEP_STARTED and two STEP_FINISHED + expect(kinds.filter((k) => k === EventType.STEP_STARTED)).toHaveLength(2); + expect(kinds.filter((k) => k === EventType.STEP_FINISHED)).toHaveLength(2); + + // One handoff + const customs = events.filter((e) => e.type === EventType.CUSTOM); + expect(customs).toHaveLength(1); + expect((customs[0] as unknown as { name: string }).name).toBe( + "MultiAgentHandoff", + ); + expect( + (customs[0] as unknown as { value: Record }).value, + ).toEqual({ + from_nodes: ["researcher"], + to_nodes: ["writer"], + }); + + // Both nodes streamed text + const textContent = events + .filter((e) => e.type === EventType.TEXT_MESSAGE_CONTENT) + .map((e) => (e as unknown as { delta: string }).delta) + .join("|"); + expect(textContent).toContain("Found it."); + expect(textContent).toContain("Final answer."); + + // Each node's text envelope closes on afterNodeCallEvent + expect( + kinds.filter((k) => k === EventType.TEXT_MESSAGE_START).length, + ).toBeGreaterThanOrEqual(1); + expect( + kinds.filter((k) => k === EventType.TEXT_MESSAGE_END).length, + ).toBeGreaterThanOrEqual(1); + }); + + it("orchestrator path is chosen when the agent has no .model accessor", async () => { + const noModel = fakeOrchestrator([]); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const sa = new StrandsAgent({ agent: noModel as any, name: "t" }); + // If the adapter took the Agent path, it would try to clone the + // template and fail on the missing model/tools. Orchestrator path + // skips cloning entirely. + const events = await collect(sa); + const kinds = events.map((e) => e.type); + expect(kinds[0]).toBe(EventType.RUN_STARTED); + expect(kinds[kinds.length - 1]).toBe(EventType.RUN_FINISHED); + expect(kinds).not.toContain(EventType.RUN_ERROR); + }); + + it("failed node emits STEP_FINISHED (Py parity: error field ignored)", async () => { + // Py control in /tmp/py-control/test_multiagent_error_parity.py proved + // the Py adapter does NOT branch on node failure — a node_stop with + // status=FAILED still yields STEP_FINISHED, no error surfaced. TS + // must match: the TS SDK's AfterNodeCallEvent has an optional `error` + // field, which we deliberately ignore. + const stream = fakeOrchestrator([ + { type: "beforeNodeCallEvent", nodeId: "flaky" }, + { + type: "afterNodeCallEvent", + nodeId: "flaky", + nodeType: "agent", + error: new Error("boom from node"), + }, + ]); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const sa = new StrandsAgent({ agent: stream as any, name: "t" }); + const events = await collect(sa); + const kinds = events.map((e) => e.type); + expect(kinds.filter((k) => k === EventType.STEP_STARTED)).toHaveLength(1); + // STEP_FINISHED fires even though the node errored — Py parity. + expect(kinds.filter((k) => k === EventType.STEP_FINISHED)).toHaveLength(1); + // No RUN_ERROR surfaced from the node-level failure. + expect(kinds).not.toContain(EventType.RUN_ERROR); + // The stream still ends cleanly with RUN_FINISHED. + expect(kinds[kinds.length - 1]).toBe(EventType.RUN_FINISHED); + }); + + it("orchestrator STEP_STARTED/FINISHED stepNames match when nodeType is set", async () => { + // Spec (events.mdx §StepFinished): STEP_FINISHED stepName must match its + // paired STEP_STARTED. The orchestrator path must honour nodeType on + // START just like the single-agent path. + const stream = fakeOrchestrator([ + { type: "beforeNodeCallEvent", nodeId: "writer", nodeType: "multiAgent" }, + { type: "afterNodeCallEvent", nodeId: "writer", nodeType: "multiAgent" }, + ]); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const sa = new StrandsAgent({ agent: stream as any, name: "t" }); + const events = await collect(sa); + const starts = events.filter( + (e) => e.type === EventType.STEP_STARTED, + ) as unknown as Array<{ stepName: string }>; + const stops = events.filter( + (e) => e.type === EventType.STEP_FINISHED, + ) as unknown as Array<{ stepName: string }>; + expect(starts).toHaveLength(1); + expect(stops).toHaveLength(1); + expect(starts[0].stepName).toBe("multiAgent:writer"); + expect(starts[0].stepName).toBe(stops[0].stepName); + }); + + it("orchestrator multiAgentHandoffEvent forwards the message field (Py parity)", async () => { + const stream = fakeOrchestrator([ + { + type: "multiAgentHandoffEvent", + source: "a", + targets: ["b"], + message: "passing the baton", + }, + ]); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const sa = new StrandsAgent({ agent: stream as any, name: "t" }); + const events = await collect(sa); + const customs = events.filter( + (e) => e.type === EventType.CUSTOM, + ) as unknown as Array<{ value: Record }>; + expect(customs).toHaveLength(1); + expect(customs[0].value.message).toBe("passing the baton"); + }); + + it("nodeCancelEvent is silently dropped (Py has no handler; TS matches)", async () => { + // Py's SDK emits `multiagent_node_cancel` when a node is cancelled via + // BeforeNodeCallEvent.cancel. The Py adapter has no elif branch for + // this event type — it falls through and crashes in its generic + // message-dict handler (which mis-types the `message` string). The + // TS SDK emits `NodeCancelEvent` with a different shape; we match the + // safer part of Py's intent by silently dropping it. + const stream = fakeOrchestrator([ + { type: "beforeNodeCallEvent", nodeId: "n1" }, + { type: "nodeCancelEvent", nodeId: "n1", message: "cancelled" }, + ]); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const sa = new StrandsAgent({ agent: stream as any, name: "t" }); + const events = await collect(sa); + const kinds = events.map((e) => e.type); + // Started fires; cancelled node doesn't emit STEP_FINISHED (no + // afterNodeCallEvent). No crash, no RUN_ERROR. + expect(kinds.filter((k) => k === EventType.STEP_STARTED)).toHaveLength(1); + expect(kinds.filter((k) => k === EventType.STEP_FINISHED)).toHaveLength(0); + expect(kinds).not.toContain(EventType.RUN_ERROR); + expect(kinds[kinds.length - 1]).toBe(EventType.RUN_FINISHED); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/orchestrator-thread-busy.test.ts b/integrations/aws-strands/typescript/src/__tests__/orchestrator-thread-busy.test.ts new file mode 100644 index 0000000000..02c88e29be --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/orchestrator-thread-busy.test.ts @@ -0,0 +1,67 @@ +/** + * Concurrent runs on the orchestrator path (Graph/Swarm) must be rejected + * with RUN_ERROR/THREAD_BUSY, not leak Strands's internal "already + * processing" error. + */ + +import { describe, it, expect } from "vitest"; +import { EventType, type BaseEvent, type RunAgentInput } from "@ag-ui/core"; + +import { StrandsAgent } from "../agent"; +import { collect, minimalRunInput } from "./helpers"; + +function blockableOrchestrator(): { + stub: { + stream: (input: string) => AsyncGenerator; + }; + release: () => void; +} { + let resolveGate!: () => void; + const gate = new Promise((r) => { + resolveGate = r; + }); + const stub = { + // No .model → adapter treats this as an orchestrator (Graph/Swarm). + async *stream(_input: string) { + await gate; + return; + }, + }; + return { stub, release: resolveGate }; +} + +async function drainIter( + gen: AsyncGenerator, +): Promise { + const out: BaseEvent[] = []; + for await (const e of gen) out.push(e); + return out; +} + +describe("Orchestrator concurrent same-thread → THREAD_BUSY", () => { + it("rejects second run with THREAD_BUSY and lets the first finish", async () => { + const { stub, release } = blockableOrchestrator(); + const agent = new StrandsAgent({ + agent: stub as unknown as import("@strands-agents/sdk").Agent, + name: "orch", + }); + const input: RunAgentInput = minimalRunInput({ threadId: "orch-1" }); + + const firstIter = agent.run(input); + const firstStarted = (await firstIter.next()).value as + | BaseEvent + | undefined; + expect(firstStarted?.type).toBe(EventType.RUN_STARTED); + + const secondEvents = await collect(agent, input); + expect(secondEvents.map((e) => e.type)).toEqual([ + EventType.RUN_STARTED, + EventType.RUN_ERROR, + ]); + const err = secondEvents[1] as unknown as { code: string }; + expect(err.code).toBe("THREAD_BUSY"); + + release(); + await drainIter(firstIter); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/parallel-tool-calls.test.ts b/integrations/aws-strands/typescript/src/__tests__/parallel-tool-calls.test.ts new file mode 100644 index 0000000000..2d13d283ee --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/parallel-tool-calls.test.ts @@ -0,0 +1,261 @@ +/** + * Tests for parallel frontend tool-call handling in StrandsAgent. + * + * Port of Python's test_parallel_tool_call_handling.py. + * + * Scenario A – Multiple parallel frontend tool calls must all be emitted. + * Scenario B – New tool calls must not be suppressed by a pending tool result + * on continuation turns. + * Scenario C – Backend tool results must not leak after halt flag is set. + */ + +import { describe, it, expect } from "vitest"; +import { ToolUseBlock, TextBlock, ToolResultBlock } from "@strands-agents/sdk"; +import type { AgentStreamEvent } from "@strands-agents/sdk"; +import { EventType } from "@ag-ui/core"; + +import type { StrandsAgentConfig } from "../config"; +import { + collect, + minimalRunInput, + scriptedStrandsAgent, + stream, +} from "./helpers"; + +// --------------------------------------------------------------------------- +// Scenario A – All parallel frontend tool calls must be emitted +// --------------------------------------------------------------------------- + +describe("Parallel frontend tool calls — all emitted", () => { + const TOOLS = [ + { name: "frontend_a", description: "a", parameters: {} }, + { name: "frontend_b", description: "b", parameters: {} }, + ]; + + it("both tool calls are emitted via ToolUseBlock path", async () => { + const blockA = new ToolUseBlock({ + name: "frontend_a", + toolUseId: "st-a", + input: {}, + }); + const blockB = new ToolUseBlock({ + name: "frontend_b", + toolUseId: "st-b", + input: {}, + }); + const agent = scriptedStrandsAgent([ + blockA as unknown as AgentStreamEvent, + blockB as unknown as AgentStreamEvent, + ]); + const events = await collect(agent, minimalRunInput({ tools: TOOLS })); + const starts = events.filter( + (e) => e.type === EventType.TOOL_CALL_START, + ) as unknown as { toolCallName: string }[]; + const names = new Set(starts.map((s) => s.toolCallName)); + expect(names.has("frontend_a")).toBe(true); + expect(names.has("frontend_b")).toBe(true); + expect(starts).toHaveLength(2); + }); + + it("both tool calls are emitted via streaming contentBlockStop path", async () => { + const events: AgentStreamEvent[] = [ + stream.toolUseStart("st-a", "frontend_a"), + stream.toolUseDelta("{}"), + stream.blockStop(), + stream.toolUseStart("st-b", "frontend_b"), + stream.toolUseDelta("{}"), + stream.blockStop(), + ]; + const agent = scriptedStrandsAgent(events); + const result = await collect(agent, minimalRunInput({ tools: TOOLS })); + const starts = result.filter( + (e) => e.type === EventType.TOOL_CALL_START, + ) as unknown as { toolCallName: string }[]; + const names = new Set(starts.map((s) => s.toolCallName)); + expect(names.has("frontend_a")).toBe(true); + expect(names.has("frontend_b")).toBe(true); + expect(starts).toHaveLength(2); + }); + + it("every TOOL_CALL_START has a matching TOOL_CALL_END", async () => { + const blockA = new ToolUseBlock({ + name: "frontend_a", + toolUseId: "st-a", + input: {}, + }); + const blockB = new ToolUseBlock({ + name: "frontend_b", + toolUseId: "st-b", + input: {}, + }); + const agent = scriptedStrandsAgent([ + blockA as unknown as AgentStreamEvent, + blockB as unknown as AgentStreamEvent, + ]); + const result = await collect(agent, minimalRunInput({ tools: TOOLS })); + const startIds = new Set( + ( + result.filter( + (e) => e.type === EventType.TOOL_CALL_START, + ) as unknown as { + toolCallId: string; + }[] + ).map((e) => e.toolCallId), + ); + const endIds = new Set( + ( + result.filter((e) => e.type === EventType.TOOL_CALL_END) as unknown as { + toolCallId: string; + }[] + ).map((e) => e.toolCallId), + ); + expect(startIds).toEqual(endIds); + expect(startIds.size).toBe(2); + }); +}); + +// --------------------------------------------------------------------------- +// Scenario B – New tool calls must not be suppressed by pending tool result +// --------------------------------------------------------------------------- + +describe("Continuation turn emits new tool calls", () => { + const TOOLS = [{ name: "frontend_tool", description: "d", parameters: {} }]; + + function continuationMessages() { + return [ + { id: "u1", role: "user" as const, content: "do something" }, + { + id: "a1", + role: "assistant" as const, + content: "", + toolCalls: [ + { + id: "prev-tc", + type: "function" as const, + function: { name: "frontend_tool", arguments: "{}" }, + }, + ], + }, + { + id: "t1", + role: "tool" as const, + content: "done", + toolCallId: "prev-tc", + }, + ]; + } + + it("new tool call ID is emitted on continuation", async () => { + const block = new ToolUseBlock({ + name: "frontend_tool", + toolUseId: "st-new", + input: { x: 1 }, + }); + const agent = scriptedStrandsAgent([block as unknown as AgentStreamEvent]); + const events = await collect( + agent, + minimalRunInput({ messages: continuationMessages(), tools: TOOLS }), + ); + const starts = events.filter( + (e) => e.type === EventType.TOOL_CALL_START, + ) as unknown as { toolCallName: string }[]; + expect(starts).toHaveLength(1); + expect(starts[0].toolCallName).toBe("frontend_tool"); + }); + + it("already-resolved backend tool call is suppressed", async () => { + const messages = [ + { id: "u1", role: "user" as const, content: "do something" }, + { + id: "a1", + role: "assistant" as const, + content: "", + toolCalls: [ + { + id: "prev-tc", + type: "function" as const, + function: { name: "backend_tool", arguments: "{}" }, + }, + ], + }, + { + id: "t1", + role: "tool" as const, + content: "result", + toolCallId: "prev-tc", + }, + ]; + const block = new ToolUseBlock({ + name: "backend_tool", + toolUseId: "prev-tc", + input: {}, + }); + const agent = scriptedStrandsAgent([block as unknown as AgentStreamEvent]); + const events = await collect( + agent, + minimalRunInput({ messages, tools: [] }), + ); + const starts = events.filter((e) => e.type === EventType.TOOL_CALL_START); + expect(starts).toHaveLength(0); + }); +}); + +// --------------------------------------------------------------------------- +// Scenario C – No backend tool results must leak after halt +// --------------------------------------------------------------------------- + +describe("No backend result leak after halt", () => { + it("only the halting result is emitted", async () => { + const config: StrandsAgentConfig = { + toolBehaviors: { + backend_halt_tool: { stopStreamingAfterResult: true }, + }, + }; + const block1 = new ToolUseBlock({ + name: "backend_halt_tool", + toolUseId: "st1", + input: {}, + }); + const block2 = new ToolUseBlock({ + name: "backend_other", + toolUseId: "st2", + input: {}, + }); + const result1 = new ToolResultBlock({ + toolUseId: "st1", + status: "success", + content: [new TextBlock(JSON.stringify({ value: 1 }))], + }); + const result2 = new ToolResultBlock({ + toolUseId: "st2", + status: "success", + content: [new TextBlock(JSON.stringify({ value: 2 }))], + }); + + const events: AgentStreamEvent[] = [ + block1 as unknown as AgentStreamEvent, + block2 as unknown as AgentStreamEvent, + { + type: "afterToolCallEvent", + toolUse: { toolUseId: "st1", name: "backend_halt_tool", input: {} }, + result: result1, + } as unknown as AgentStreamEvent, + { + type: "afterToolCallEvent", + toolUse: { toolUseId: "st2", name: "backend_other", input: {} }, + result: result2, + } as unknown as AgentStreamEvent, + ]; + + const agent = scriptedStrandsAgent(events, { config }); + const result = await collect(agent); + const resultEvents = result.filter( + (e) => e.type === EventType.TOOL_CALL_RESULT, + ) as unknown as { toolCallId: string }[]; + const resultIds = resultEvents.map((e) => e.toolCallId); + + expect(resultIds).toContain("st1"); + expect(resultIds).not.toContain("st2"); + expect(resultEvents).toHaveLength(1); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/plugins-forwarded.test.ts b/integrations/aws-strands/typescript/src/__tests__/plugins-forwarded.test.ts new file mode 100644 index 0000000000..bb775e3aa6 --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/plugins-forwarded.test.ts @@ -0,0 +1,143 @@ +/** + * Plugins passed to `StrandsAgent` via the constructor must be forwarded + * into `AgentConfig.plugins` on every per-thread Strands agent. + * + * Mirrors the Python adapter's hook-forwarding behavior. + */ + +import { describe, it, expect, vi } from "vitest"; +import type { Plugin } from "@strands-agents/sdk"; +import { StrandsAgent } from "../agent"; +import { collect, minimalRunInput } from "./helpers"; + +// Capture the AgentConfig passed to every per-thread Strands Agent +// constructor so we can assert plugins were forwarded. +const capturedConfigs: Array> = []; + +vi.mock("@strands-agents/sdk", async (importOriginal) => { + const actual = await importOriginal(); + class MockAgent { + model: unknown; + tools: unknown[] = []; + systemPrompt?: unknown; + toolRegistry = { + _tools: new Map(), + add(t: unknown) { + this._tools.set((t as { name: string }).name, t); + }, + getByName(name: string) { + return this._tools.get(name); + }, + get(name: string) { + return this._tools.get(name); + }, + removeByName(name: string) { + this._tools.delete(name); + }, + remove(name: unknown) { + if (typeof name === "string") this._tools.delete(name); + }, + values() { + return Array.from(this._tools.values()); + }, + }; + constructor(cfg?: Record) { + if (cfg) { + capturedConfigs.push(cfg); + this.model = cfg.model; + this.tools = (cfg.tools as unknown[]) ?? []; + this.systemPrompt = cfg.systemPrompt; + } + } + // eslint-disable-next-line require-yield + async *stream() { + /* empty */ + } + } + return { ...actual, Agent: MockAgent }; +}); + +function templateAgent(): import("@strands-agents/sdk").Agent { + return { + model: { name: "template-model" }, + tools: [], + systemPrompt: "template", + toolRegistry: { + _tools: new Map(), + add: () => void 0, + getByName: () => undefined, + get: () => undefined, + removeByName: () => void 0, + remove: () => void 0, + values: () => [], + }, + } as unknown as import("@strands-agents/sdk").Agent; +} + +describe("Plugin forwarding", () => { + it("forwards the plugins array to every per-thread Strands agent", async () => { + capturedConfigs.length = 0; + const plugin1: Plugin = { + name: "plugin-1", + initAgent: vi.fn(), + }; + const plugin2: Plugin = { + name: "plugin-2", + initAgent: vi.fn(), + }; + const sa = new StrandsAgent({ + agent: templateAgent(), + name: "t", + plugins: [plugin1, plugin2], + }); + + await collect(sa, minimalRunInput({ threadId: "thread-A" })); + await collect(sa, minimalRunInput({ threadId: "thread-B" })); + await collect(sa, minimalRunInput({ threadId: "thread-C" })); + + // One per-thread agent per distinct thread — three AgentConfigs captured. + expect(capturedConfigs).toHaveLength(3); + for (const cfg of capturedConfigs) { + const plugins = cfg.plugins as Plugin[] | undefined; + expect(plugins).toBeDefined(); + expect(plugins).toHaveLength(2); + expect(plugins?.[0]).toBe(plugin1); + expect(plugins?.[1]).toBe(plugin2); + } + }); + + it("omits the plugins key entirely when no plugins were supplied", async () => { + capturedConfigs.length = 0; + const sa = new StrandsAgent({ agent: templateAgent(), name: "t" }); + await collect(sa, minimalRunInput({ threadId: "no-plugins" })); + expect(capturedConfigs).toHaveLength(1); + expect("plugins" in capturedConfigs[0]!).toBe(false); + }); + + it("omits the plugins key when an empty array is supplied", async () => { + capturedConfigs.length = 0; + const sa = new StrandsAgent({ + agent: templateAgent(), + name: "t", + plugins: [], + }); + await collect(sa, minimalRunInput({ threadId: "empty-plugins" })); + expect(capturedConfigs).toHaveLength(1); + expect("plugins" in capturedConfigs[0]!).toBe(false); + }); + + it("defensive copy: mutating the caller's array does not leak", async () => { + capturedConfigs.length = 0; + const callerArr: Plugin[] = [{ name: "p1", initAgent: vi.fn() }]; + const sa = new StrandsAgent({ + agent: templateAgent(), + name: "t", + plugins: callerArr, + }); + // Mutate AFTER construction but BEFORE any run + callerArr.push({ name: "p-injected", initAgent: vi.fn() }); + await collect(sa, minimalRunInput({ threadId: "defensive" })); + const cfg = capturedConfigs[0]!; + expect((cfg.plugins as Plugin[]).map((p) => p.name)).toEqual(["p1"]); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/reasoning-signature.test.ts b/integrations/aws-strands/typescript/src/__tests__/reasoning-signature.test.ts new file mode 100644 index 0000000000..a3bca99805 --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/reasoning-signature.test.ts @@ -0,0 +1,121 @@ +/** + * Tests that `reasoningSignatureEvent` events are silently consumed + * (not yielded) by the StrandsAgent adapter. + * + * The adapter's dispatch loop has: + * if (kind === "reasoningSignatureEvent") { continue; } + * + * These tests verify that reasoning signature events never leak into + * the AG-UI output and that surrounding events flow correctly. + */ + +import { describe, it, expect } from "vitest"; +import type { AgentStreamEvent } from "@strands-agents/sdk"; +import { EventType, type BaseEvent } from "@ag-ui/core"; + +import { + collect, + minimalRunInput, + scriptedStrandsAgent, + stream, +} from "./helpers"; + +function types(events: BaseEvent[]): string[] { + return events.map((e) => e.type); +} + +describe("reasoning signature handling", () => { + it("reasoning signature events are silently consumed", async () => { + // Simulate: reasoning text delta -> reasoningSignatureEvent -> more reasoning text -> stop + const agent = scriptedStrandsAgent([ + stream.reasoningDelta("Let me think..."), + { + type: "reasoningSignatureEvent", + signature: "abc123-sig-data", + } as unknown as AgentStreamEvent, + stream.reasoningDelta(" Done thinking."), + stream.blockStop(), + stream.textDelta("Here is my answer."), + ]); + + const input = minimalRunInput({ + threadId: "thread-1", + runId: "r1", + messages: [{ id: "1", role: "user", content: "hi" }], + tools: [], + }); + const events = await collect(agent, input); + const kinds = types(events); + + // No event should reference the reasoning signature + for (const event of events) { + const serialized = JSON.stringify(event); + expect(serialized).not.toContain("reasoningSignature"); + expect(serialized).not.toContain("reasoning_signature"); + } + expect(kinds).not.toContain("reasoningSignatureEvent"); + + // Reasoning text before and after the signature event should still flow + const reasoningContents = events.filter( + (e) => e.type === EventType.REASONING_MESSAGE_CONTENT, + ) as unknown as { delta: string }[]; + expect(reasoningContents).toHaveLength(2); + expect(reasoningContents[0].delta).toBe("Let me think..."); + expect(reasoningContents[1].delta).toBe(" Done thinking."); + + // Text message after reasoning also flows correctly + const textContents = events.filter( + (e) => e.type === EventType.TEXT_MESSAGE_CONTENT, + ) as unknown as { delta: string }[]; + expect(textContents).toHaveLength(1); + expect(textContents[0].delta).toBe("Here is my answer."); + }); + + it("reasoning signature does not interrupt text streaming", async () => { + // Simulate: text delta -> reasoningSignatureEvent -> more text delta -> stop + // The signature sits between two text deltas that should both flow uninterrupted. + const agent = scriptedStrandsAgent([ + stream.textDelta("Hello "), + { + type: "reasoningSignatureEvent", + signature: "xyz-signature-payload", + } as unknown as AgentStreamEvent, + stream.textDelta("world!"), + ]); + + const input = minimalRunInput({ + threadId: "thread-1", + runId: "r1", + messages: [{ id: "1", role: "user", content: "hi" }], + tools: [], + }); + const events = await collect(agent, input); + const kinds = types(events); + + // Text message lifecycle should be intact + expect(kinds).toContain(EventType.TEXT_MESSAGE_START); + expect(kinds).toContain(EventType.TEXT_MESSAGE_CONTENT); + expect(kinds).toContain(EventType.TEXT_MESSAGE_END); + + // Both text deltas must be present and in order + const textContents = events.filter( + (e) => e.type === EventType.TEXT_MESSAGE_CONTENT, + ) as unknown as { delta: string }[]; + expect(textContents).toHaveLength(2); + expect(textContents[0].delta).toBe("Hello "); + expect(textContents[1].delta).toBe("world!"); + + // No reasoning signature leaked + for (const event of events) { + const serialized = JSON.stringify(event); + expect(serialized).not.toContain("reasoningSignature"); + expect(serialized).not.toContain("reasoning_signature"); + } + expect(kinds).not.toContain("reasoningSignatureEvent"); + + // Only a single TEXT_MESSAGE_START — the signature did not cause the + // adapter to close and reopen the message envelope. + const starts = kinds.filter((k) => k === EventType.TEXT_MESSAGE_START); + expect(starts).toHaveLength(1); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/seed-concurrency.test.ts b/integrations/aws-strands/typescript/src/__tests__/seed-concurrency.test.ts new file mode 100644 index 0000000000..df239b89ab --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/seed-concurrency.test.ts @@ -0,0 +1,82 @@ +/** + * Seed building for one thread with slow multimodal URL fetches must not + * serialize cold-cache inits for OTHER threads behind the global + * _threadInitLock. + */ + +import { describe, it, expect, vi, afterEach } from "vitest"; +import { StrandsAgent } from "../agent"; +import { minimalRunInput, scriptedAgent } from "./helpers"; + +// Each cold-init needs a fresh stub (first run on the thread returns quickly). +const fastStub = (): import("@strands-agents/sdk").Agent => scriptedAgent(); + +describe("seed build is outside _threadInitLock", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("concurrent cold-inits on different threads don't serialise on a slow seed", async () => { + // Spy on global fetch; make all fetches slow (200ms) so the seed helper's + // URL resolution for thread A blocks only A, not B. + const origFetch = globalThis.fetch; + globalThis.fetch = (async (): Promise => { + await new Promise((r) => setTimeout(r, 200)); + // Return an empty PNG body so the content converter drops it harmlessly. + return new Response(new Uint8Array([0x89, 0x50, 0x4e, 0x47]).buffer, { + status: 200, + }) as Response; + }) as typeof fetch; + + const agent = new StrandsAgent({ agent: fastStub(), name: "s" }); + + const inputA = minimalRunInput({ + threadId: "a", + messages: [ + { + id: "u-a1", + role: "user", + content: [ + { + type: "image", + source: { + type: "url", + value: "https://example.invalid/slow.png", + }, + }, + ], + } as never, + { id: "u-a2", role: "user", content: "hi" } as never, + ], + }); + const inputB = minimalRunInput({ threadId: "b" }); + + const t0 = Date.now(); + // Launch A first (it will start fetching the slow URL for its seed). + const a = agent.run(inputA); + const aFirst = a.next(); + // Give A a tick to start but NOT enough time to finish the fetch. + await new Promise((r) => setTimeout(r, 30)); + // Now B starts. B has no seed to fetch, should complete promptly even + // while A is still blocked. + const bStart = Date.now(); + const b = agent.run(inputB); + const bFirst = await b.next(); + const bDur = Date.now() - bStart; + expect(bFirst.done).toBe(false); // got an event + // B's first event arrived well under A's 200ms seed-fetch delay → + // confirms B is not serialised behind A's lock. + expect(bDur).toBeLessThan(150); + // Now finish A (drain both). + await aFirst; + for await (const _ of a) { + void _; + } + for await (const _ of b) { + void _; + } + const total = Date.now() - t0; + expect(total).toBeGreaterThan(150); // A still paid its fetch cost + globalThis.fetch = origFetch; + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/seed-messages.test.ts b/integrations/aws-strands/typescript/src/__tests__/seed-messages.test.ts new file mode 100644 index 0000000000..99ec7920d7 --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/seed-messages.test.ts @@ -0,0 +1,263 @@ +/** + * Prior conversation history is converted into Strands `MessageData` and + * seeded on the per-thread agent's `AgentConfig.messages`. + */ + +import { describe, it, expect } from "vitest"; +import type { Message as AguiMessage } from "@ag-ui/core"; +import { convertMessagesForStrandsSeed, buildStrandsSeed } from "../agent"; + +describe("convertMessagesForStrandsSeed", () => { + it("drops system and developer messages", async () => { + const seed = await convertMessagesForStrandsSeed([ + { + id: "s", + role: "system", + content: "You are a cat.", + } as unknown as AguiMessage, + { + id: "d", + role: "developer", + content: "be terse", + } as unknown as AguiMessage, + { id: "u1", role: "user", content: "hi" } as unknown as AguiMessage, + { + id: "a1", + role: "assistant", + content: "hello", + } as unknown as AguiMessage, + ]); + expect(seed.map((m) => m.role)).toEqual(["user", "assistant"]); + }); + + it("preserves text content", async () => { + const seed = await convertMessagesForStrandsSeed([ + { + id: "u", + role: "user", + content: "what is 2+2?", + } as unknown as AguiMessage, + { id: "a", role: "assistant", content: "4" } as unknown as AguiMessage, + ]); + expect(seed[0]).toEqual({ + role: "user", + content: [{ text: "what is 2+2?" }], + }); + expect(seed[1]).toEqual({ role: "assistant", content: [{ text: "4" }] }); + }); + + it("emits toolUse blocks for assistant toolCalls", async () => { + const seed = await convertMessagesForStrandsSeed([ + { id: "u", role: "user", content: "lookup" } as unknown as AguiMessage, + { + id: "a", + role: "assistant", + content: "", + toolCalls: [ + { + id: "tc-1", + type: "function", + function: { name: "search", arguments: '{"q":"x"}' }, + }, + ], + } as unknown as AguiMessage, + ]); + expect(seed[1].content).toEqual([ + { toolUse: { name: "search", toolUseId: "tc-1", input: { q: "x" } } }, + ]); + }); + + it("merges tool messages into a single user message of toolResult blocks", async () => { + const seed = await convertMessagesForStrandsSeed([ + { id: "u", role: "user", content: "lookup" } as unknown as AguiMessage, + { + id: "a", + role: "assistant", + content: "", + toolCalls: [ + { + id: "tc-1", + type: "function", + function: { name: "s1", arguments: "{}" }, + }, + { + id: "tc-2", + type: "function", + function: { name: "s2", arguments: "{}" }, + }, + ], + } as unknown as AguiMessage, + { + id: "t1", + role: "tool", + toolCallId: "tc-1", + content: "A", + } as unknown as AguiMessage, + { + id: "t2", + role: "tool", + toolCallId: "tc-2", + content: "B", + } as unknown as AguiMessage, + ]); + // user, assistant, (merged user with both toolResults) + expect(seed).toHaveLength(3); + expect(seed[2].role).toBe("user"); + expect(seed[2].content).toEqual([ + { + toolResult: { + toolUseId: "tc-1", + status: "success", + content: [{ text: "A" }], + }, + }, + { + toolResult: { + toolUseId: "tc-2", + status: "success", + content: [{ text: "B" }], + }, + }, + ]); + }); + + it("drops orphaned tool messages whose call id wasn't announced", async () => { + const seed = await convertMessagesForStrandsSeed([ + { id: "u", role: "user", content: "hi" } as unknown as AguiMessage, + { + id: "t", + role: "tool", + toolCallId: "bogus", + content: "stale", + } as unknown as AguiMessage, + ]); + expect(seed.map((m) => m.role)).toEqual(["user"]); + }); + + it("returns an empty array for an empty history", async () => { + expect(await convertMessagesForStrandsSeed([])).toEqual([]); + }); + + it("preserves multimodal image/document content on user turns", async () => { + // 1x1 red PNG as base64 + const PNG = + "iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAIAAAAmkwkpAAAAEElEQVR4nGP4z8AARwzEcQCukw/x0F8jngAAAABJRU5ErkJggg=="; + const seed = await convertMessagesForStrandsSeed([ + { + id: "u", + role: "user", + content: [ + { type: "text", text: "describe" }, + { + type: "image", + source: { type: "data", mimeType: "image/png", value: PNG }, + }, + ], + } as unknown as AguiMessage, + { id: "a", role: "assistant", content: "nice" } as unknown as AguiMessage, + ]); + // First entry should include at minimum the text block; the image block + // is best-effort depending on the content converter's format support. + expect(seed[0].role).toBe("user"); + const texts = seed[0].content.filter( + (c: unknown) => c && typeof c === "object" && "text" in (c as object), + ); + expect(texts.length).toBeGreaterThanOrEqual(1); + // Assert the image block survived — shape is `{ image: { source, format, ... } }`. + const images = seed[0].content.filter( + (c: unknown) => c && typeof c === "object" && "image" in (c as object), + ); + expect(images.length).toBe(1); + }); +}); + +describe("buildStrandsSeed", () => { + it("drops the final user turn when tail is user (trim-for-prompt)", async () => { + const out = await buildStrandsSeed([ + { id: "u1", role: "user", content: "first" } as unknown as AguiMessage, + { + id: "a1", + role: "assistant", + content: "response", + } as unknown as AguiMessage, + { + id: "u2", + role: "user", + content: "becomes prompt", + } as unknown as AguiMessage, + ]); + expect(Array.isArray(out)).toBe(true); + const arr = out as Array<{ role: string }>; + expect(arr).toHaveLength(2); + expect(arr.map((m) => m.role)).toEqual(["user", "assistant"]); + }); + + it("seeds the FULL history on continuation (tail is a tool message)", async () => { + // Scenario: frontend-tool round-trip completed, client POSTs the next + // run with [user, assistant+toolCalls, tool]. Without this fix the seed + // would be undefined, losing all context. + const out = await buildStrandsSeed([ + { + id: "u", + role: "user", + content: "set bg to red", + } as unknown as AguiMessage, + { + id: "a", + role: "assistant", + content: "", + toolCalls: [ + { + id: "tc1", + type: "function", + function: { name: "change_bg", arguments: '{"color":"red"}' }, + }, + ], + } as unknown as AguiMessage, + { + id: "t", + role: "tool", + toolCallId: "tc1", + content: "ok", + } as unknown as AguiMessage, + ]); + expect(out).toBeDefined(); + const arr = out as Array<{ role: string }>; + expect(arr).toHaveLength(3); // user, assistant+toolUse, user w/ toolResult + expect(arr.map((m) => m.role)).toEqual(["user", "assistant", "user"]); + }); + + it("drops leading assistant turns (Bedrock requires user-first history)", async () => { + const out = await buildStrandsSeed([ + { + id: "a0", + role: "assistant", + content: "Hi, how can I help?", + } as unknown as AguiMessage, + { + id: "u1", + role: "user", + content: "actually, bye", + } as unknown as AguiMessage, + ]); + // After trimming the tail user, seed is [assistant-only]. Bedrock + // rejects assistant-first history, so we drop it → undefined. + expect(out).toBeUndefined(); + }); + + it("returns undefined on an empty history", async () => { + expect(await buildStrandsSeed([])).toBeUndefined(); + }); + + it("returns undefined when the only message is the prompt user turn", async () => { + expect( + await buildStrandsSeed([ + { + id: "u", + role: "user", + content: "only turn", + } as unknown as AguiMessage, + ]), + ).toBeUndefined(); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/session-manager.test.ts b/integrations/aws-strands/typescript/src/__tests__/session-manager.test.ts new file mode 100644 index 0000000000..bc038e9f9d --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/session-manager.test.ts @@ -0,0 +1,284 @@ +/** + * Tests for session manager provider lifecycle in StrandsAgent. + * + * Port of Python's test_session_manager.py — covers caching, async providers, + * null returns, and retry semantics. + */ + +import { describe, it, expect, vi } from "vitest"; +import { SessionManager } from "@strands-agents/sdk"; +import { EventType } from "@ag-ui/core"; + +import { StrandsAgent } from "../agent"; +import { collect, minimalRunInput, scriptedAgent } from "./helpers"; + +// Mock the Strands Agent constructor so tests don't need a real model provider. +vi.mock("@strands-agents/sdk", async (importOriginal) => { + const actual = await importOriginal(); + class MockAgent { + model = { name: "mock" }; + tools: unknown[] = []; + toolRegistry = { + _tools: new Map(), + add(t: unknown) { + this._tools.set((t as { name: string }).name, t); + }, + getByName(name: string) { + return this._tools.get(name); + }, + get(name: string) { + return this._tools.get(name); + }, + removeByName(name: string) { + this._tools.delete(name); + }, + remove(name: unknown) { + if (typeof name === "string") this._tools.delete(name); + }, + values() { + return Array.from(this._tools.values()); + }, + }; + async *stream() { + /* empty */ + } + } + return { + ...actual, + Agent: MockAgent, + }; +}); + +/** + * A minimal SessionManager subclass that records calls but doesn't hit any + * real storage. Subclassing ensures `instanceof SessionManager` passes. + */ +class FakeSessionManager extends SessionManager { + static instances: FakeSessionManager[] = []; + constructor() { + super({ + sessionId: `fake-${Math.random().toString(36).slice(2)}`, + storage: { + snapshot: { save: vi.fn(), load: vi.fn(), delete: vi.fn() } as never, + }, + }); + FakeSessionManager.instances.push(this); + } +} +function fakeSessionManager(): FakeSessionManager { + return new FakeSessionManager(); +} + +describe("Session manager provider — caching", () => { + it("provider is called only once per thread", async () => { + const provider = vi.fn().mockReturnValue(fakeSessionManager()); + const agent = new StrandsAgent({ + agent: scriptedAgent(), + name: "t", + config: { sessionManagerProvider: provider }, + }); + + await collect(agent, minimalRunInput({ threadId: "thread-A" })); + await collect(agent, minimalRunInput({ threadId: "thread-A" })); + + expect(provider).toHaveBeenCalledTimes(1); + }); + + it("different threads get separate provider invocations", async () => { + const provider = vi.fn().mockReturnValue(fakeSessionManager()); + const agent = new StrandsAgent({ + agent: scriptedAgent(), + name: "t", + config: { sessionManagerProvider: provider }, + }); + + await collect(agent, minimalRunInput({ threadId: "thread-1" })); + await collect(agent, minimalRunInput({ threadId: "thread-2" })); + + expect(provider).toHaveBeenCalledTimes(2); + }); + + it("failed provider does not cache — allows retry", async () => { + let callCount = 0; + const provider = vi.fn().mockImplementation(() => { + callCount++; + if (callCount === 1) throw new Error("transient failure"); + return fakeSessionManager(); + }); + const agent = new StrandsAgent({ + agent: scriptedAgent(), + name: "t", + config: { sessionManagerProvider: provider }, + }); + + // First call fails + const events1 = await collect( + agent, + minimalRunInput({ threadId: "retry-thread" }), + ); + expect( + events1.some( + (e) => + (e as unknown as { code?: string }).code === "SESSION_MANAGER_ERROR", + ), + ).toBe(true); + + // Second call succeeds (provider retried) + const events2 = await collect( + agent, + minimalRunInput({ threadId: "retry-thread" }), + ); + expect(events2.some((e) => e.type === EventType.RUN_FINISHED)).toBe(true); + expect(provider).toHaveBeenCalledTimes(2); + }); +}); + +describe("Session manager provider — async", () => { + it("awaits an async provider", async () => { + const provider = vi.fn().mockImplementation(async () => { + await new Promise((r) => setTimeout(r, 10)); + return fakeSessionManager(); + }); + const agent = new StrandsAgent({ + agent: scriptedAgent(), + name: "t", + config: { sessionManagerProvider: provider }, + }); + + const events = await collect( + agent, + minimalRunInput({ threadId: "async-thread" }), + ); + expect(events.some((e) => e.type === EventType.RUN_FINISHED)).toBe(true); + expect(provider).toHaveBeenCalledTimes(1); + }); +}); + +describe("Session manager provider — null/undefined return", () => { + it("null return logs warning but does not error", async () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const provider = vi.fn().mockReturnValue(null); + const agent = new StrandsAgent({ + agent: scriptedAgent(), + name: "t", + config: { sessionManagerProvider: provider }, + }); + + const events = await collect( + agent, + minimalRunInput({ threadId: "null-thread" }), + ); + expect(events.some((e) => e.type === EventType.RUN_FINISHED)).toBe(true); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("returned null/undefined"), + ); + warnSpy.mockRestore(); + }); + + it("undefined return logs warning but does not error", async () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const provider = vi.fn().mockReturnValue(undefined); + const agent = new StrandsAgent({ + agent: scriptedAgent(), + name: "t", + config: { sessionManagerProvider: provider }, + }); + + const events = await collect( + agent, + minimalRunInput({ threadId: "undef-thread" }), + ); + expect(events.some((e) => e.type === EventType.RUN_FINISHED)).toBe(true); + expect(warnSpy).toHaveBeenCalled(); + warnSpy.mockRestore(); + }); +}); + +describe("Session manager provider — empty/falsy threadId", () => { + it("uses 'default' key when threadId is empty string", async () => { + const provider = vi.fn().mockReturnValue(fakeSessionManager()); + const agent = new StrandsAgent({ + agent: scriptedAgent(), + name: "t", + config: { sessionManagerProvider: provider }, + }); + + await collect(agent, minimalRunInput({ threadId: "" })); + await collect(agent, minimalRunInput({ threadId: "" })); + + // Both should resolve to "default" thread, so only one provider call + expect(provider).toHaveBeenCalledTimes(1); + }); +}); + +describe("Session manager provider — strict instanceof validation", () => { + it("rejects plain string with SESSION_MANAGER_INVALID_TYPE", async () => { + const agent = new StrandsAgent({ + agent: scriptedAgent(), + name: "t", + config: { sessionManagerProvider: () => "not a sm" as unknown as never }, + }); + const events = await collect( + agent, + minimalRunInput({ threadId: "invalid-string" }), + ); + const err = events.find((e) => e.type === EventType.RUN_ERROR); + expect(err).toBeDefined(); + expect((err as unknown as { code: string }).code).toBe( + "SESSION_MANAGER_INVALID_TYPE", + ); + }); + + it("rejects plain object with register() (HookProvider-shaped)", async () => { + const agent = new StrandsAgent({ + agent: scriptedAgent(), + name: "t", + config: { + sessionManagerProvider: () => + ({ register: () => void 0 }) as unknown as never, + }, + }); + const events = await collect( + agent, + minimalRunInput({ threadId: "invalid-hook-provider" }), + ); + const err = events.find((e) => e.type === EventType.RUN_ERROR); + expect(err).toBeDefined(); + expect((err as unknown as { code: string }).code).toBe( + "SESSION_MANAGER_INVALID_TYPE", + ); + }); + + it("accepts a SessionManager subclass instance", async () => { + const agent = new StrandsAgent({ + agent: scriptedAgent(), + name: "t", + config: { sessionManagerProvider: () => fakeSessionManager() }, + }); + const events = await collect( + agent, + minimalRunInput({ threadId: "valid-subclass" }), + ); + expect(events.some((e) => e.type === EventType.RUN_ERROR)).toBe(false); + expect(events.some((e) => e.type === EventType.RUN_FINISHED)).toBe(true); + }); + + it("instanceof survives constructor name mangling (minifier-safe)", async () => { + const sm = fakeSessionManager(); + // Simulate a minifier renaming the subclass constructor name. + Object.defineProperty(sm.constructor, "name", { value: "M_a" }); + expect(sm.constructor.name).toBe("M_a"); + // Adapter should STILL accept this instance — instanceof walks the + // prototype chain by identity, not by name. + const agent = new StrandsAgent({ + agent: scriptedAgent(), + name: "t", + config: { sessionManagerProvider: () => sm }, + }); + const events = await collect( + agent, + minimalRunInput({ threadId: "minified-name" }), + ); + expect(events.some((e) => e.type === EventType.RUN_ERROR)).toBe(false); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/streaming-predict-state.test.ts b/integrations/aws-strands/typescript/src/__tests__/streaming-predict-state.test.ts new file mode 100644 index 0000000000..d5025eff05 --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/streaming-predict-state.test.ts @@ -0,0 +1,201 @@ +/** + * Tool-call wire ordering for streaming predict_state. + * + * The documented order (Python adapter's fix #1638) is: + * PredictState (CustomEvent) — once, BEFORE any args + * TOOL_CALL_START + * TOOL_CALL_ARGS (delta) — one per toolUseInputDelta chunk + * ... + * TOOL_CALL_ARGS (final delta) — any residual growth at contentBlockStop + * STATE_SNAPSHOT (stateFromArgs) — BEFORE end, so CopilotKit has real + * state when prediction is released + * TOOL_CALL_END + * MESSAGES_SNAPSHOT — rotates message_id + * + * These tests pin that order against the streaming (toolUseInputDelta) path. + */ + +import { describe, it, expect } from "vitest"; +import type { AgentStreamEvent } from "@strands-agents/sdk"; +import { EventType } from "@ag-ui/core"; + +import type { StrandsAgentConfig } from "../config"; +import { + collect, + minimalRunInput, + scriptedStrandsAgent, + stream, +} from "./helpers"; + +// Stream a tool call across N toolUseInputDelta chunks. The adapter must +// emit TOOL_CALL_ARGS deltas that concatenate back to the full payload. +// +// Event shape matches Strands v1 SDK's Bedrock adapter — `start.type = +// "toolUseStart"` on the block-start, not `contentBlock.type = "toolUse"`. +function streamingScript( + toolName: string, + toolUseId: string, + chunks: string[], +): AgentStreamEvent[] { + const events: AgentStreamEvent[] = [stream.toolUseStart(toolUseId, toolName)]; + for (const c of chunks) { + events.push(stream.toolUseDelta(c)); + } + events.push(stream.blockStop()); + return events; +} + +describe("tool-call wire ordering (streaming path)", () => { + it("emits PredictState BEFORE TOOL_CALL_START and streams args incrementally", async () => { + const chunks = ['{"steps"', ":[1,2", ",3]}"]; + const config: StrandsAgentConfig = { + toolBehaviors: { + make_plan: { + predictState: { + stateKey: "plan", + tool: "make_plan", + toolArgument: "steps", + }, + }, + }, + }; + const agent = scriptedStrandsAgent( + streamingScript("make_plan", "tc1", chunks), + { config }, + ); + const events = await collect( + agent, + minimalRunInput({ + tools: [{ name: "make_plan", description: "", parameters: {} }], + }), + ); + + // Strip out MESSAGES_SNAPSHOT and other frames to isolate the tool-call order. + const orderingRelevant = events + .map((e) => e.type as string) + .filter((t) => + [ + EventType.CUSTOM, + EventType.TOOL_CALL_START, + EventType.TOOL_CALL_ARGS, + EventType.TOOL_CALL_END, + ].includes(t as EventType), + ); + + // PredictState (CustomEvent) comes first. + expect(orderingRelevant[0]).toBe(EventType.CUSTOM); + expect(orderingRelevant[1]).toBe(EventType.TOOL_CALL_START); + // Then one TOOL_CALL_ARGS per chunk (3 deltas). + expect( + orderingRelevant.filter((t) => t === EventType.TOOL_CALL_ARGS).length, + ).toBeGreaterThanOrEqual(3); + // TOOL_CALL_END is last of the tool-call family. + expect(orderingRelevant[orderingRelevant.length - 1]).toBe( + EventType.TOOL_CALL_END, + ); + + // Args deltas concatenate back to the full payload. + const deltas = events + .filter((e) => e.type === EventType.TOOL_CALL_ARGS) + .map((e) => (e as unknown as { delta: string }).delta); + expect(deltas.join("")).toBe(chunks.join("")); + }); + + it("emits stateFromArgs STATE_SNAPSHOT BEFORE TOOL_CALL_END", async () => { + const config: StrandsAgentConfig = { + toolBehaviors: { + update_recipe: { + stateFromArgs: () => ({ recipe: { status: "drafted" } }), + }, + }, + }; + const agent = scriptedStrandsAgent( + streamingScript("update_recipe", "tc2", ['{"name"', ':"pie"}']), + { config }, + ); + const events = await collect(agent); + + const types = events.map((e) => e.type as string); + const endIdx = types.indexOf(EventType.TOOL_CALL_END); + const stateIdx = types.findIndex( + (_t, i) => + i < endIdx && + events[i]!.type === EventType.STATE_SNAPSHOT && + // Skip the initial state snapshot (from inputData.state). + i > types.indexOf(EventType.TOOL_CALL_START), + ); + expect(stateIdx).toBeGreaterThan(-1); + expect(stateIdx).toBeLessThan(endIdx); + }); + + it("stream(undefined) flush: residual growth between last delta and stop is sent as final TOOL_CALL_ARGS", async () => { + // Construct a script where the LLM tacks on a suffix AFTER the last + // toolUseInputDelta (simulated by the adapter picking up currentToolUse.inputChunks + // joined at contentBlockStop). In practice this happens when Strands + // combines multiple underlying chunks — the adapter re-reads the full + // raw on stop and emits the missing tail. + // + // We approximate here by sending a partial chunk then expecting the + // stop-time flush to still produce a complete args payload equal to + // the input. + const chunks = ['{"x":', "1}"]; + const agent = scriptedStrandsAgent( + streamingScript("frontend_tool", "tc3", chunks), + ); + const events = await collect( + agent, + minimalRunInput({ + tools: [{ name: "frontend_tool", description: "", parameters: {} }], + }), + ); + + const deltas = events + .filter((e) => e.type === EventType.TOOL_CALL_ARGS) + .map((e) => (e as unknown as { delta: string }).delta); + expect(deltas.join("")).toBe(chunks.join("")); + // Exactly one TOOL_CALL_START / TOOL_CALL_END pair. + expect( + events.filter((e) => e.type === EventType.TOOL_CALL_START), + ).toHaveLength(1); + expect( + events.filter((e) => e.type === EventType.TOOL_CALL_END), + ).toHaveLength(1); + }); + + it("continuation run: already-resolved backend tool is suppressed (no re-emit)", async () => { + // When Strands replays a backend tool already resolved in history, the + // adapter must not re-emit TOOL_CALL_START/ARGS/END — Strands reuses + // the original toolUseId, which lands in pendingToolResultIds, and the + // adapter's streaming path routes that into the "pending" branch that + // only fires state callbacks. Backend tools pass through as an empty + // `tools` input (no frontend registry). + const agent = scriptedStrandsAgent( + streamingScript("backend_tool", "prev-tc", ['{"x":1}']), + ); + const events = await collect( + agent, + minimalRunInput({ + tools: [], + messages: [ + { id: "u1", role: "user", content: "go" }, + { + id: "a1", + role: "assistant", + content: "", + toolCalls: [ + { + id: "prev-tc", + type: "function", + function: { name: "backend_tool", arguments: '{"x":1}' }, + }, + ], + }, + { id: "t1", role: "tool", content: "ok", toolCallId: "prev-tc" }, + ], + }), + ); + + const starts = events.filter((e) => e.type === EventType.TOOL_CALL_START); + expect(starts).toHaveLength(0); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/thread-busy.test.ts b/integrations/aws-strands/typescript/src/__tests__/thread-busy.test.ts new file mode 100644 index 0000000000..8cb989e865 --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/thread-busy.test.ts @@ -0,0 +1,98 @@ +/** + * Concurrent runs on the same thread must be rejected with a + * protocol-shaped RUN_ERROR/THREAD_BUSY, not the internal Strands error + * message. + */ + +import { describe, it, expect } from "vitest"; +import { EventType, type BaseEvent, type RunAgentInput } from "@ag-ui/core"; + +import { StrandsAgent } from "../agent"; +import { minimalRunInput, scriptedAgent } from "./helpers"; + +function blockableAgent(): { + stub: import("@strands-agents/sdk").Agent; + release: () => void; +} { + let resolveGate!: () => void; + const gate = new Promise((r) => { + resolveGate = r; + }); + const stub = scriptedAgent([], { + stream: async function* () { + // Block until the test releases the gate. Yielding nothing keeps the + // adapter inside its main loop. + await gate; + // Emit a trivial finish so the caller's generator terminates cleanly. + return; + } as unknown as import("@strands-agents/sdk").Agent["stream"], + }); + return { stub, release: resolveGate }; +} + +async function collectEvents( + gen: AsyncGenerator, +): Promise { + const out: BaseEvent[] = []; + for await (const e of gen) out.push(e); + return out; +} + +describe("Concurrent runs on same thread → THREAD_BUSY", () => { + it("rejects second invocation with RUN_ERROR/THREAD_BUSY and leaves first alone", async () => { + const { stub, release } = blockableAgent(); + const agent = new StrandsAgent({ agent: stub, name: "t" }); + ( + agent as unknown as { _agentsByThread: Map } + )._agentsByThread.set("thread-1", stub); + + const input: RunAgentInput = minimalRunInput({ threadId: "thread-1" }); + + // Kick off the first run and pull its first event so we know it has + // registered itself as active before we start the second. + const firstIter = agent.run(input); + const firstStarted = (await firstIter.next()).value as + | BaseEvent + | undefined; + expect(firstStarted?.type).toBe(EventType.RUN_STARTED); + + // Now the second run on the same thread should short-circuit. + const secondEvents = await collectEvents(agent.run(input)); + expect(secondEvents.map((e) => e.type)).toEqual([ + EventType.RUN_STARTED, + EventType.RUN_ERROR, + ]); + const err = secondEvents[1] as unknown as { code: string; message: string }; + expect(err.code).toBe("THREAD_BUSY"); + expect(err.message).toMatch(/thread-1/); + + // Release the gate so the first iterator can finish, ensuring we're not + // leaking a hung agent across tests. + release(); + await collectEvents(firstIter); + }); + + it("separate threads can run concurrently without collision", async () => { + const { stub: stub1, release: release1 } = blockableAgent(); + const { stub: stub2, release: release2 } = blockableAgent(); + const agent = new StrandsAgent({ agent: stub1, name: "t" }); + const internal = ( + agent as unknown as { _agentsByThread: Map } + )._agentsByThread; + internal.set("a", stub1); + internal.set("b", stub2); + + const inA = minimalRunInput({ threadId: "a", runId: "r-a" }); + const inB = minimalRunInput({ threadId: "b", runId: "r-b" }); + const itA = agent.run(inA); + const itB = agent.run(inB); + const firstA = (await itA.next()).value as BaseEvent; + const firstB = (await itB.next()).value as BaseEvent; + expect(firstA.type).toBe(EventType.RUN_STARTED); + expect(firstB.type).toBe(EventType.RUN_STARTED); + release1(); + release2(); + await collectEvents(itA); + await collectEvents(itB); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/tool-result-void.test.ts b/integrations/aws-strands/typescript/src/__tests__/tool-result-void.test.ts new file mode 100644 index 0000000000..af91b31896 --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/tool-result-void.test.ts @@ -0,0 +1,51 @@ +/** + * A tool that returns void / null / empty still produces TOOL_CALL_RESULT, + * so legitimate side-effect tools get a result card in the UI instead of + * silently dropping the emission. + */ + +import { describe, it, expect } from "vitest"; +import { ToolUseBlock, ToolResultBlock } from "@strands-agents/sdk"; +import type { AgentStreamEvent } from "@strands-agents/sdk"; +import { EventType } from "@ag-ui/core"; + +import { collect, scriptedStrandsAgent } from "./helpers"; + +describe("Tool callback returning null/empty", () => { + it("still emits TOOL_CALL_RESULT with empty content", async () => { + // Simulate Strands emitting an AfterToolCallEvent where `content` is + // an empty array (e.g. a side-effect tool that returned undefined). + const events: AgentStreamEvent[] = [ + new ToolUseBlock({ + name: "log_event", + toolUseId: "tu-1", + input: { msg: "hello" }, + }) as unknown as AgentStreamEvent, + { + type: "afterToolCallEvent", + toolUse: { name: "log_event", toolUseId: "tu-1" }, + result: new ToolResultBlock({ + toolUseId: "tu-1", + status: "success", + content: [], + }), + } as unknown as AgentStreamEvent, + ]; + const agent = scriptedStrandsAgent(events); + const out = await collect(agent); + const kinds = out.map((e) => e.type); + expect(kinds).toContain(EventType.TOOL_CALL_START); + expect(kinds).toContain(EventType.TOOL_CALL_END); + expect(kinds).toContain(EventType.TOOL_CALL_RESULT); + const result = out.find( + (e) => e.type === EventType.TOOL_CALL_RESULT, + ) as unknown as { + content?: string; + toolCallId?: string; + }; + expect(result?.toolCallId).toBe("tu-1"); + // Empty content (not JSON `null`, not a stringified null) — the UI can + // still render a result card. + expect(result?.content).toBe(""); + }); +}); diff --git a/integrations/aws-strands/typescript/src/__tests__/tool-stream-events.test.ts b/integrations/aws-strands/typescript/src/__tests__/tool-stream-events.test.ts new file mode 100644 index 0000000000..50f873523e --- /dev/null +++ b/integrations/aws-strands/typescript/src/__tests__/tool-stream-events.test.ts @@ -0,0 +1,98 @@ +/** + * Verifies that mid-execution tool yields arrive as STATE_SNAPSHOT events. + * + * The Strands v1 SDK wraps `ToolStreamEvent` (produced by an async-generator + * tool yielding `{ state: ... }`) inside `ToolStreamUpdateEvent`: + * + * { type: "toolStreamUpdateEvent", + * agent, invocationState, + * event: { type: "toolStreamEvent", data: { state: { ... } } } } + * + * The adapter must unwrap the outer envelope before dispatching; otherwise + * the inner `kind === "toolStreamEvent"` branch never fires and mid-tool + * state updates are silently lost. + */ + +import { describe, it, expect } from "vitest"; +import type { AgentStreamEvent } from "@strands-agents/sdk"; +import { EventType } from "@ag-ui/core"; + +import { collect, scriptedStrandsAgent } from "./helpers"; + +describe("tool_stream_event handling", () => { + it("emits STATE_SNAPSHOT for each { state } yielded by an async-generator tool", async () => { + // Three mid-tool yields, each producing a different `{ state: ... }` + // payload, exactly as a tool's async generator would. + const script: AgentStreamEvent[] = [ + { + type: "toolStreamUpdateEvent", + event: { + type: "toolStreamEvent", + data: { state: { steps: [{ description: "a", status: "pending" }] } }, + }, + } as unknown as AgentStreamEvent, + { + type: "toolStreamUpdateEvent", + event: { + type: "toolStreamEvent", + data: { + state: { steps: [{ description: "a", status: "in_progress" }] }, + }, + }, + } as unknown as AgentStreamEvent, + { + type: "toolStreamUpdateEvent", + event: { + type: "toolStreamEvent", + data: { + state: { steps: [{ description: "a", status: "completed" }] }, + }, + }, + } as unknown as AgentStreamEvent, + ]; + + const agent = scriptedStrandsAgent(script); + const events = await collect(agent); + + const snapshots = events.filter( + (e) => e.type === EventType.STATE_SNAPSHOT, + ) as unknown as Array<{ + snapshot: { steps?: Array<{ status: string }> }; + }>; + + // Initial (run start) + 3 mid-tool + final (run end) = 5. We only assert + // that the three yields produced distinct status transitions, without + // pinning the exact total so future framing changes don't regress this. + const statuses = snapshots + .map((s) => s.snapshot?.steps?.[0]?.status) + .filter((x): x is string => typeof x === "string"); + expect(statuses).toContain("pending"); + expect(statuses).toContain("in_progress"); + expect(statuses).toContain("completed"); + }); + + it("ignores toolStreamUpdateEvent whose inner event carries no { state }", async () => { + // Tools can yield arbitrary progress payloads; only `{ state: ... }` + // yields should translate into STATE_SNAPSHOT. A yield like + // `{ progress: 42 }` (or any other non-state shape) should pass through + // without emitting a spurious empty snapshot. + const script: AgentStreamEvent[] = [ + { + type: "toolStreamUpdateEvent", + event: { type: "toolStreamEvent", data: { progress: 42 } }, + } as unknown as AgentStreamEvent, + { + type: "toolStreamUpdateEvent", + event: { type: "toolStreamEvent", data: "plain string payload" }, + } as unknown as AgentStreamEvent, + ]; + + const agent = scriptedStrandsAgent(script); + const events = await collect(agent); + + // Only the lifecycle snapshots (run-start + run-end) — no extras for the + // non-state tool yields. + const snapshots = events.filter((e) => e.type === EventType.STATE_SNAPSHOT); + expect(snapshots.length).toBeLessThanOrEqual(2); + }); +}); diff --git a/integrations/aws-strands/typescript/src/agent.ts b/integrations/aws-strands/typescript/src/agent.ts new file mode 100644 index 0000000000..c88bbf4bb4 --- /dev/null +++ b/integrations/aws-strands/typescript/src/agent.ts @@ -0,0 +1,2492 @@ +/** + * AWS Strands Agent adapter for AG-UI. + * + * Translates Strands streaming events into the AG-UI event protocol. + */ + +import { randomUUID } from "crypto"; + +import { + Agent as StrandsAgentCore, + InterruptResponseContent, + Message as StrandsMessage, + SessionManager, + TextBlock, + ToolResultBlock, + ToolUseBlock, + type AgentConfig, + type AgentResult as StrandsAgentResult, + type AgentStreamEvent, + type ContentBlock, + type Interrupt as StrandsInterrupt, + type JSONValue, + type Plugin, +} from "@strands-agents/sdk"; +import { + EventType, + type AssistantMessage as AguiAssistantMessage, + type BaseEvent, + type Interrupt as AguiInterrupt, + type Message as AguiMessage, + type ResumeEntry, + type RunAgentInput, + type ToolCall as AguiToolCall, + type ToolMessage as AguiToolMessage, + type UserMessage as AguiUserMessage, +} from "@ag-ui/core"; + +import { + buildContextExtras, + maybeAwait, + normalizePredictState, + predictStateMappingToPayload, + type StrandsAgentConfig, + type ToolCallContext, + type ToolResultContext, +} from "./config"; +import { syncProxyTools } from "./client-proxy-tool"; +import { convertAguiContentToStrands, flattenContentToText } from "./utils"; +import type { SeenToolCall } from "./types"; +import { DEFAULT_LOGGER, resolveLogger, type Logger } from "./logger"; + +const LOG_PREFIX = "[@ag-ui/aws-strands]"; + +// Strands' `randomUUID` return type is branded; normalise to plain string. +const uuid = (): string => randomUUID(); + +/** + * Structural interface for a Strands multi-agent orchestrator (Graph/Swarm). + * TypeScript-only: the Python SDK currently has no orchestrator equivalent. + */ +interface StrandsOrchestrator { + readonly id?: string; + stream(input: string): AsyncGenerator; +} + +/** + * Fields cloned from the caller-supplied template Agent into every per-thread + * Agent. Mirrors Python's `_extract_agent_kwargs`. Deliberately NOT forwarded: + * - sessionManager: supplied per-thread via sessionManagerProvider. + * - plugins: supplied explicitly via StrandsAgentOptions.plugins. + * - conversationManager: bound to template state; sharing across threads + * would share conversation-window state. Rely on Strands' default. + * - messages: per-thread agents start empty; AG-UI delivers at runtime. + * - hooks: Strands' HookRegistry ephemeral state, not forwarded. + */ +interface TemplateAgentCloneFields { + model: AgentConfig["model"]; + tools: StrandsAgentCore["tools"]; + systemPrompt?: AgentConfig["systemPrompt"]; + name?: string; + description?: string; + id?: string; + appState?: Record; + modelState?: Record; + traceAttributes?: AgentConfig["traceAttributes"]; + structuredOutputSchema?: AgentConfig["structuredOutputSchema"]; + toolExecutor?: AgentConfig["toolExecutor"]; +} + +/** + * Extract every forwardable field from the template Agent into per-thread + * clones. Mirrors Python's ``_extract_agent_kwargs``. + */ +function _extractTemplateFields( + agent: StrandsAgentCore, +): TemplateAgentCloneFields { + const model = agent.model; + const modelId = model?.modelId; + // Only pass `modelId` as a string when the template uses BedrockModel — + // that's Strands' one string-coercion path. Other providers must keep + // their Model instance or `new Agent({ model: "..." })` rebuilds them as + // BedrockModel and inference fails with "provided model identifier is + // invalid". + const isBedrock = + model != null && + typeof model === "object" && + model.constructor?.name === "BedrockModel"; + const fields: TemplateAgentCloneFields = { + model: isBedrock && modelId ? modelId : model, + tools: agent.tools.slice(), + }; + if (agent.systemPrompt !== undefined) + fields.systemPrompt = agent.systemPrompt; + // Strands defaults `name` to "Strands Agent" and `id` to "agent" when the + // caller doesn't set them — forward them unconditionally so the per-thread + // agent matches the template regardless of whether the default or an + // override was used. + if (agent.name !== undefined) fields.name = agent.name; + if (agent.id !== undefined) fields.id = agent.id; + if (agent.description !== undefined) fields.description = agent.description; + // appState / modelState are StateStore instances; serialize to plain dicts. + const appStateDump = ( + agent.appState as { getAll?: () => Record } + )?.getAll?.(); + if (appStateDump && Object.keys(appStateDump).length > 0) + fields.appState = appStateDump; + const modelStateDump = ( + agent.modelState as { getAll?: () => Record } + )?.getAll?.(); + if (modelStateDump && Object.keys(modelStateDump).length > 0) + fields.modelState = modelStateDump; + // These aren't exposed via the Agent's public accessors in all SDK versions; + // read them optimistically and forward only when set. + const extra = agent as unknown as { + traceAttributes?: AgentConfig["traceAttributes"]; + structuredOutputSchema?: AgentConfig["structuredOutputSchema"]; + toolExecutor?: AgentConfig["toolExecutor"]; + }; + if (extra.traceAttributes !== undefined) + fields.traceAttributes = extra.traceAttributes; + if (extra.structuredOutputSchema !== undefined) + fields.structuredOutputSchema = extra.structuredOutputSchema; + if (extra.toolExecutor !== undefined) + fields.toolExecutor = extra.toolExecutor; + return fields; +} + +/** Best-effort string view of an AG-UI message content field. */ +function _coerceText(content: unknown): string { + if (typeof content === "string") return content; + if (content == null) return ""; + if (Array.isArray(content)) return flattenContentToText(content); + return String(content); +} + +/** Return ``value`` if it is a non-empty string, else a fresh UUID. */ +function _coerceId(value: unknown): string { + return typeof value === "string" && value.length > 0 ? value : uuid(); +} + +/** Extract a human-readable message from an unknown error. */ +function _errorMessage(e: unknown): string { + return e instanceof Error ? e.message : String(e); +} + +/** + * Resolve the AG-UI-side tool call id from an incoming Strands tool use. + * + * - If we've already seen this Strands tool (by internal id), reuse the + * existing AG-UI id so every envelope event carries the same id. + * - Frontend tools get a fresh UUID to avoid cross-request collisions. + * - Backend tools reuse Strands' own id so result lookup works. + */ +function _resolveToolUseId( + seen: Map, + strandsToolId: string, + isFrontendTool: boolean, +): string { + for (const [tid, data] of seen) { + if (data.strandsToolId === strandsToolId) return tid; + } + if (isFrontendTool) return uuid(); + return strandsToolId || uuid(); +} + +/** + * Convert ``RunAgentInput.messages`` to AG-UI message objects. + * + * Used to seed the running ``MessagesSnapshotEvent`` payload so each snapshot + * carries the full thread history. + */ +export function buildSnapshotMessages( + input_messages: AguiMessage[], +): AguiMessage[] { + const out: AguiMessage[] = []; + for (const msg of input_messages ?? []) { + const role = msg.role; + if (role !== "user" && role !== "assistant" && role !== "tool") continue; + const msgId = _coerceId((msg as { id?: string }).id); + if (role === "user") { + out.push({ + id: msgId, + role: "user", + content: _coerceText(msg.content), + } as AguiUserMessage); + } else if (role === "assistant") { + const rawToolCalls = (msg as { toolCalls?: AguiToolCall[] }).toolCalls; + let toolCalls: AguiToolCall[] | undefined; + if (rawToolCalls && rawToolCalls.length > 0) { + toolCalls = rawToolCalls.map((tc) => { + const fn = tc.function as + | { name?: string; arguments?: string } + | undefined; + return { + id: _coerceId(tc.id), + type: "function" as const, + function: { + name: fn?.name ?? "unknown", + arguments: fn?.arguments ?? "{}", + }, + }; + }); + } + const assistant: AguiAssistantMessage = { + id: msgId, + role: "assistant", + content: _coerceText(msg.content), + }; + if (toolCalls) assistant.toolCalls = toolCalls; + out.push(assistant); + } else { + const toolCallId = (msg as { toolCallId?: string }).toolCallId ?? ""; + out.push({ + id: msgId, + role: "tool", + content: _coerceText(msg.content), + toolCallId, + } as AguiToolMessage); + } + } + return out; +} + +/** + * Convert ``RunAgentInput.messages`` to Strands native ``Messages``. + * + * Strands has only ``user`` and ``assistant`` roles; tool calls and tool + * results live as ``toolUse`` / ``toolResult`` ContentBlocks. Reconciling + * the cached agent's ``self.messages`` with this list before invoking + * ``stream(undefined)`` ensures the LLM sees the real conversation state — + * including frontend tool results — rather than a fresh prompt that + * re-fires the same tool every turn. + * + * Multimodal content is routed through ``convertAguiContentToStrands`` so + * image/document/video blocks reach the LLM intact across replay. + */ +async function _buildStrandsHistory( + input_messages: AguiMessage[], + log: Logger, +): Promise> { + const out: Array<{ role: "user" | "assistant"; content: unknown[] }> = []; + for (const msg of input_messages ?? []) { + const role = msg.role; + if (role === "user") { + const content: unknown[] = []; + const raw = msg.content; + if (Array.isArray(raw)) { + const hasMedia = raw.some((item: { type?: string }) => + ["image", "audio", "video", "document"].includes(item.type ?? ""), + ); + if (hasMedia) { + try { + const blocks = await convertAguiContentToStrands(raw as never, log); + for (const b of blocks) { + if (b instanceof TextBlock) { + content.push({ text: b.text }); + } else { + const serialised = + typeof (b as { toJSON?: () => unknown }).toJSON === "function" + ? (b as { toJSON: () => unknown }).toJSON() + : b; + content.push(serialised); + } + } + } catch (e) { + log.warn( + `${LOG_PREFIX} history replay multimodal conversion failed; falling back to text`, + e, + ); + } + if (content.length === 0) { + content.push({ text: flattenContentToText(raw as never) || "" }); + } + } else { + content.push({ text: flattenContentToText(raw as never) }); + } + } else { + content.push({ text: _coerceText(raw) }); + } + out.push({ role: "user", content }); + } else if (role === "assistant") { + const blocks: unknown[] = []; + const text = _coerceText(msg.content); + if (text) blocks.push({ text }); + const rawToolCalls = + (msg as { toolCalls?: AguiToolCall[] }).toolCalls ?? []; + for (const tc of rawToolCalls) { + const fn = tc.function as + | { name?: string; arguments?: string } + | undefined; + const name = fn?.name || "unknown"; + const rawArgs = fn?.arguments || "{}"; + let parsed: unknown; + try { + parsed = JSON.parse(rawArgs); + } catch { + parsed = {}; + } + if ( + typeof parsed !== "object" || + parsed === null || + Array.isArray(parsed) + ) + parsed = {}; + blocks.push({ + toolUse: { toolUseId: tc.id, name, input: parsed }, + }); + } + if (blocks.length === 0) blocks.push({ text: "" }); + out.push({ role: "assistant", content: blocks }); + } else if (role === "tool") { + const toolCallId = (msg as { toolCallId?: string }).toolCallId || ""; + out.push({ + role: "user", + content: [ + { + toolResult: { + toolUseId: toolCallId, + content: [{ text: _coerceText(msg.content) }], + status: "success" as const, + }, + }, + ], + }); + } + } + return out; +} + +/** Options accepted by `StrandsAgent`. */ +export interface StrandsAgentOptions { + /** + * Either an `Agent` (the template — adapter clones it per thread and syncs + * proxy tools) OR a multi-agent orchestrator (`Graph`, `Swarm`). + * Orchestrators are stateless per invocation so the same instance serves + * every thread. + */ + agent: StrandsAgentCore | StrandsOrchestrator; + name: string; + description?: string; + config?: StrandsAgentConfig; + /** + * Plugins forwarded to every per-thread Strands agent created by this + * adapter (observability, loop caps, policy checks, ...). Mirrors the + * Python adapter's `hooks=` kwarg. Ignored when `agent` is a multi-agent + * orchestrator. + */ + plugins?: Plugin[]; +} + +/** AWS Strands Agent wrapper for AG-UI integration. */ +export class StrandsAgent { + readonly name: string; + readonly description: string; + readonly config: StrandsAgentConfig; + + // Template agent configuration for creating fresh per-thread instances. + private readonly _templateFields: TemplateAgentCloneFields; + + /** + * Hook providers forwarded to each per-thread StrandsAgentCore. + * + * Taken directly from the caller rather than read off the template because + * Strands' `Agent.hooks` is a `HookRegistry` containing only registered + * callbacks — the original list of provider objects is not retained, and + * the registry also contains callbacks bound to internal Strands objects + * that must not be cross-wired into per-thread agents. + */ + private readonly _plugins: Plugin[]; + + private readonly _agentsByThread = new Map(); + private readonly _proxyToolNamesByThread = new Map>(); + /** + * Guards first-time thread initialization. The sessionManagerProvider call + * introduces an async yield point between the "is this thread new?" check + * and the map assignment, so concurrent requests for the same new threadId + * could otherwise both create an agent and one would clobber the other. + */ + private readonly _threadInitLock = new AsyncMutex(); + /** + * Threads with an in-flight run. Strands `Agent.stream()` throws if a + * second invocation is started on a busy agent; we detect the collision + * up front and emit a protocol-shaped RUN_ERROR/THREAD_BUSY instead. + * TypeScript-only: the Python adapter has no equivalent guard. + */ + private readonly _activeRunsByThread = new Set(); + /** Outstanding Strands interrupt IDs per thread, used to validate + * incoming `RunAgentInput.resume[]` (interrupts.mdx rule 4). */ + private readonly _pendingInterruptsByThread = new Map>(); + /** + * When non-null, the adapter bypasses per-thread cloning and invokes + * the orchestrator directly. See `StrandsAgentOptions.agent`. + */ + private readonly _orchestrator: StrandsOrchestrator | null; + /** + * Injectable logger. Defaults to console `warn`/`error` with `debug` + * suppressed, matching Python's stdlib `logging.getLogger(__name__)`. + */ + private readonly _log: Logger; + + constructor(options: StrandsAgentOptions) { + const { agent, name, description = "", config = {}, plugins } = options; + + // Detect a multi-agent orchestrator. Graph / Swarm expose `nodes` + `edges` + // (Graph) or `nodes` + invoke semantics (Swarm) and have no `.model` + // accessor — branching on the presence of `.model` is the cleanest + // structural check. + const isOrchestrator = + typeof (agent as { model?: unknown }).model === "undefined" || + (agent as { model?: unknown }).model === null; + + this.name = name; + this.description = description; + this.config = config; + this._log = resolveLogger(config.logger); + + if (isOrchestrator) { + this._orchestrator = agent as StrandsOrchestrator; + this._templateFields = { model: undefined as never, tools: [] }; + this._plugins = []; + return; + } + + this._orchestrator = null; + const agentCore = agent as StrandsAgentCore; + this._templateFields = _extractTemplateFields(agentCore); + this._plugins = plugins ? [...plugins] : []; + + // Detect the common pitfall: sessionManager set on the template Agent + // with no per-thread provider. Forwarding it would make every AG-UI + // thread share one session_id. + if (agentCore.sessionManager && !this.config.sessionManagerProvider) { + this._log.warn( + `${LOG_PREFIX} sessionManager was set on the template Agent but will ` + + "be ignored: forwarding it would cause every AG-UI thread to share the " + + "same session_id. Construct per-thread session managers via " + + "StrandsAgentConfig.sessionManagerProvider instead.", + ); + } + + // Detect unconnected MCP clients passed directly into `tools: [...]`. + // Strands resolves a connected `McpClient`'s tools into `agent.tools` at + // construction time; an unconnected one stays as the bare client and the + // resolved tool list never appears here. The fix is on the caller's + // side: `await client.connect()` and spread `await client.listTools()` + // into the `tools` array. + for (const tool of this._templateFields.tools ?? []) { + if ( + tool != null && + typeof (tool as { connect?: unknown }).connect === "function" && + typeof (tool as { name?: unknown }).name !== "string" + ) { + this._log.warn( + `${LOG_PREFIX} an entry in the template Agent's \`tools\` looks like ` + + "an unconnected McpClient — its tools will not be available to the " + + "model. Call `await client.connect()` and spread the resolved tool " + + "list into `tools: [...]` before constructing the Agent.", + ); + } + } + } + + /** Run the Strands agent and yield AG-UI events. */ + async *run(inputData: RunAgentInput): AsyncGenerator { + // interrupts.mdx rule 4: any resume[] entry referencing an unknown + // interruptId MUST produce RUN_ERROR. Known IDs flow through to + // `InterruptResponseContent[]`. Gated above `_runRaw` so subclasses + // that override only `_runRaw` still inherit the check. + if (Array.isArray(inputData.resume) && inputData.resume.length > 0) { + const threadId = inputData.threadId || "default"; + const pending = this._pendingInterruptsByThread.get(threadId); + const unknown = inputData.resume + .map((entry) => entry.interruptId) + .filter((id) => !pending?.has(id)); + if (unknown.length > 0) { + yield _runStarted(inputData); + yield _runError( + `This agent did not issue any interrupts to resume: ${unknown + .slice(0, 4) + .join(", ")}. ` + + "Resume entries must reference an outstanding interruptId.", + "UNKNOWN_INTERRUPT", + ); + return; + } + } + const source = this._runRaw(inputData); + if (this.config.emitChunkEvents) { + yield* collapseToChunkEvents(source); + return; + } + yield* source; + } + + protected async *_runRaw( + inputData: RunAgentInput, + ): AsyncGenerator { + const threadId = inputData.threadId || "default"; + + // Reject concurrent runs on the same thread up front. Strands cannot + // multiplex a single Agent across invocations and emits a confusing + // internal error ("Agent is already processing an invocation") if we try. + if (this._activeRunsByThread.has(threadId)) { + yield _runStarted(inputData); + yield _runError( + `Another run is already in progress on thread "${threadId}". Wait for RUN_FINISHED before starting a new run on the same thread.`, + "THREAD_BUSY", + ); + return; + } + this._activeRunsByThread.add(threadId); + try { + if (this._orchestrator !== null) { + yield* this._runOrchestrator(inputData); + } else { + yield* this._runSingleAgent(inputData, threadId); + } + } finally { + this._activeRunsByThread.delete(threadId); + } + } + + private async *_runSingleAgent( + inputData: RunAgentInput, + threadId: string, + ): AsyncGenerator { + // Get or create agent instance for this thread. When a + // sessionManagerProvider is configured, the SessionManager handles + // conversation persistence; otherwise state is held in-memory per thread. + let strandsAgent = this._agentsByThread.get(threadId); + if (!strandsAgent) { + // Build the message-history seed BEFORE acquiring the global thread + // init lock. The seed helper may make async fetches for URL-based + // multimodal attachments; doing that inside the lock would serialise + // cold-cache initialisations for every OTHER thread behind one slow + // replay request. Skipped entirely when a SessionManager will own + // persistence. + let seedMessages: AgentConfig["messages"] | undefined; + if (!this.config.sessionManagerProvider) { + try { + seedMessages = await buildStrandsSeed( + inputData.messages ?? [], + this._log, + ); + } catch (e) { + this._log.warn( + `${LOG_PREFIX} buildStrandsSeed failed for thread ${threadId}; ` + + "starting agent with empty history", + e, + ); + } + } + + const release = await this._threadInitLock.acquire(); + try { + // Double-check inside the lock: another coroutine may have completed + // initialization while we were waiting. + strandsAgent = this._agentsByThread.get(threadId); + if (!strandsAgent) { + let sessionManager: SessionManager | null | undefined; + if (this.config.sessionManagerProvider) { + try { + sessionManager = (await maybeAwait( + this.config.sessionManagerProvider(inputData), + )) as SessionManager | null | undefined; + } catch (e) { + const msg = _errorMessage(e); + this._log.error( + `${LOG_PREFIX} sessionManagerProvider failed: ${msg}`, + e, + ); + yield _runStarted(inputData); + yield _runError( + `Failed to initialize session manager: ${msg}`, + "SESSION_MANAGER_ERROR", + ); + return; + } + if ( + sessionManager != null && + !(sessionManager instanceof SessionManager) + ) { + const actual = + (sessionManager as object)?.constructor?.name ?? + typeof sessionManager; + this._log.error( + `${LOG_PREFIX} sessionManagerProvider returned ${actual}; expected a SessionManager instance.`, + ); + yield _runStarted(inputData); + yield _runError( + `sessionManagerProvider returned ${actual}; expected a SessionManager instance`, + "SESSION_MANAGER_INVALID_TYPE", + ); + return; + } + if (!sessionManager) { + this._log.warn( + `${LOG_PREFIX} sessionManagerProvider returned null/undefined for threadId=${threadId}; ` + + "agent will run without session persistence", + ); + } + } + // If a SessionManager materialised, skip the pre-computed seed — + // the session owns persistence and seeding on top would duplicate + // turns. + const effectiveSeed = sessionManager ? undefined : seedMessages; + strandsAgent = new StrandsAgentCore( + this._buildThreadAgentConfig( + sessionManager ?? undefined, + effectiveSeed, + ), + ); + this._agentsByThread.set(threadId, strandsAgent); + } + } finally { + release(); + } + } + + // Sync proxy tools from client-defined tools. + if (inputData.tools && inputData.tools.length > 0) { + const proxyNames = syncProxyTools( + strandsAgent.toolRegistry, + inputData.tools, + this._proxyToolNamesByThread.get(threadId) ?? new Set(), + this._log, + ); + this._proxyToolNamesByThread.set(threadId, proxyNames); + } else { + const previous = this._proxyToolNamesByThread.get(threadId); + if (previous && previous.size > 0) { + syncProxyTools(strandsAgent.toolRegistry, [], previous, this._log); + this._proxyToolNamesByThread.set(threadId, new Set()); + } + } + + yield _runStarted(inputData); + + try { + // Seed the running ``MessagesSnapshotEvent`` payload from the full + // conversation history so each emitted snapshot carries prior turns + // plus whatever this turn adds. + const emitMessagesSnapshot = this.config.emitMessagesSnapshot !== false; + const snapshotMessages: AguiMessage[] = emitMessagesSnapshot + ? buildSnapshotMessages(inputData.messages ?? []) + : []; + + // Emit state snapshot if provided. Filter out `messages` from state to + // avoid "Unknown message role" errors — the frontend manages messages + // separately and doesn't recognize the "tool" role. + if (inputData.state && typeof inputData.state === "object") { + const snapshot: Record = {}; + for (const [k, v] of Object.entries( + inputData.state as Record, + )) { + if (k !== "messages") snapshot[k] = v; + } + yield { type: EventType.STATE_SNAPSHOT, snapshot }; + } + + // Splice point 1 of 4: emit the initial messages snapshot so the + // frontend can render the seeded thread before any new content streams. + if (emitMessagesSnapshot && snapshotMessages.length > 0) { + yield { + type: EventType.MESSAGES_SNAPSHOT, + messages: snapshotMessages.slice(), + }; + } + + const frontendToolNames = new Set(); + for (const t of inputData.tools ?? []) { + if (t.name) frontendToolNames.add(t.name); + } + + // Collect tool_call_ids that already have results in the message + // history so we suppress duplicate TOOL_CALL_START events for them. + const pendingToolResultIds = new Set(); + if (inputData.messages) { + for (let i = inputData.messages.length - 1; i >= 0; i--) { + const msg = inputData.messages[i]; + if (!msg) break; + if (msg.role === "tool") { + const tid = (msg as { toolCallId?: string }).toolCallId; + if (tid) pendingToolResultIds.add(tid); + } else { + break; + } + } + if (pendingToolResultIds.size > 0) { + this._log.debug( + `${LOG_PREFIX} Has pending tool results detected: toolCallIds=${JSON.stringify([...pendingToolResultIds])}, threadId=${inputData.threadId}`, + ); + } + } + + // Lookup of tool_call_id -> tool_name from assistant messages. + const toolCallIdToName = new Map(); + for (const msg of inputData.messages ?? []) { + if (msg.role !== "assistant") continue; + const calls = (msg as { toolCalls?: AguiToolCall[] }).toolCalls; + if (!calls) continue; + for (const tc of calls) { + const fn = tc.function as { name?: string } | undefined; + if (tc.id && fn?.name) toolCallIdToName.set(tc.id, fn.name); + } + } + + // Derive the outgoing user message. For continuation runs (pending + // tool results in history), synthesise a "frontend tool executed" + // message so the model understands the context. + let userMessage: string | ContentBlock[] = "Hello"; + if (pendingToolResultIds.size > 0 && inputData.messages) { + for (let i = inputData.messages.length - 1; i >= 0; i--) { + const msg = inputData.messages[i]; + if (!msg) break; + if (msg.role === "tool") { + const toolCallId = (msg as { toolCallId?: string }).toolCallId; + if (toolCallId) { + const name = toolCallIdToName.get(toolCallId); + if (name && frontendToolNames.has(name)) { + userMessage = `${name} executed successfully with no return value.`; + } + } + break; + } + } + } else if (inputData.messages) { + for (let i = inputData.messages.length - 1; i >= 0; i--) { + const msg = inputData.messages[i]; + if (!msg) break; + if ( + (msg.role === "user" || msg.role === "tool") && + msg.content != null + ) { + if (Array.isArray(msg.content)) { + const hasMedia = msg.content.some((item: { type?: string }) => + ["image", "audio", "video", "document"].includes( + item.type ?? "", + ), + ); + if (hasMedia) { + const blocks = await convertAguiContentToStrands( + msg.content, + this._log, + ); + if (blocks.length > 0) { + userMessage = blocks; + } else { + userMessage = flattenContentToText(msg.content) || "Hello"; + this._log.warn( + `${LOG_PREFIX} all media content blocks failed conversion; falling back to text`, + ); + } + } else { + userMessage = flattenContentToText(msg.content); + } + } else { + userMessage = msg.content as string; + } + break; + } + } + } + + // Allow configuration to enrich the outgoing user message. Multimodal + // prompts pass through unchanged so binary payloads reach the model + // intact. + if (this.config.stateContextBuilder) { + try { + const textForBuilder = Array.isArray(userMessage) + ? flattenContentToText(userMessage) + : userMessage; + const builderResult = this.config.stateContextBuilder( + inputData, + textForBuilder, + buildContextExtras(inputData), + ); + if (!Array.isArray(userMessage)) { + userMessage = builderResult; + } + } catch (e) { + this._log.warn(`${LOG_PREFIX} stateContextBuilder failed:`, e); + } + } + + // Per-run state. + let messageId = uuid(); + let messageStarted = false; + let accumulatedText = ""; + const toolCallsSeen = new Map(); + const currentState: Record = { + ...((inputData.state ?? {}) as object), + }; + let stopTextStreaming = false; + let haltEventStream = false; + let pendingHalt = false; + + let reasoningStarted = false; + let reasoningMessageId: string | undefined; + + // Tool currently being streamed via toolUseInputDelta events. Populated + // by modelContentBlockStartEvent or toolUseInputDelta, flushed on + // modelContentBlockStopEvent. + let currentToolUse: { + name: string; + toolUseId: string; + inputChunks: string[]; + } | null = null; + + // Reconcile Strands' internal conversation history with + // ``RunAgentInput.messages`` when no ``sessionManager`` is wired. + // Without this, frontend tool results never reach the LLM — Strands + // sees an open ``toolUse`` from the prior turn and the LLM re-fires + // the same tool every run. + const replayHistory = + this.config.replayHistoryIntoStrands !== false && + !(strandsAgent as { sessionManager?: unknown }).sessionManager; + let invokeArgs: + | string + | ContentBlock[] + | InterruptResponseContent[] + | undefined = userMessage; + + // Resume path: convert AG-UI `resume[]` into Strands + // `InterruptResponseContent[]`. The `run()` gate has already + // filtered unknown IDs by this point. + const resumeEntries = resolveResumeEntries(inputData); + if (resumeEntries.length > 0) { + invokeArgs = resumeEntries.map( + (entry) => + new InterruptResponseContent({ + interruptId: entry.interruptId, + response: toResumeResponse(entry) as JSONValue, + }), + ); + this._pendingInterruptsByThread.delete(threadId); + } + if (replayHistory && resumeEntries.length === 0) { + const nativeHistory = await _buildStrandsHistory( + inputData.messages ?? [], + this._log, + ); + if (nativeHistory.length > 0) { + // Apply stateContextBuilder to the last user-text message in the + // reconciled history rather than to the synthetic `userMessage` + // string — this is what the LLM actually sees. + if (this.config.stateContextBuilder) { + for (let i = nativeHistory.length - 1; i >= 0; i--) { + const m = nativeHistory[i]; + if (!m || m.role !== "user") continue; + const first = (m.content as Array<{ text?: string }>)[0]; + if (first && typeof first.text === "string") { + try { + const augmented = this.config.stateContextBuilder( + inputData, + first.text, + buildContextExtras(inputData), + ); + if (typeof augmented === "string") first.text = augmented; + } catch (e) { + this._log.warn( + `${LOG_PREFIX} stateContextBuilder failed:`, + e, + ); + } + break; + } + } + } + // Convert plain-object history into real Message instances — + // Bedrock's request formatter dispatches on `block.type`, which + // only the class instances carry. + (strandsAgent as { messages: unknown[] }).messages = + nativeHistory.map((m) => + StrandsMessage.fromMessageData({ + role: m.role, + content: m.content as never, + }), + ); + // `stream(undefined)` tells Strands to use `this.messages` as-is. + invokeArgs = undefined; + } + } + + this._log.debug( + `${LOG_PREFIX} Starting agent run: threadId=${inputData.threadId}, runId=${inputData.runId}, ` + + `pendingToolResultIds=${JSON.stringify([...pendingToolResultIds])}, ` + + `messageCount=${inputData.messages?.length ?? 0}`, + ); + + // AbortController wired into Strands's `cancelSignal` so that abandoning + // the outer generator (HTTP client disconnect) stops the underlying + // Bedrock streaming call rather than silently burning tokens. + const runAbort = new AbortController(); + const agentStream = strandsAgent.stream(invokeArgs as never, { + cancelSignal: runAbort.signal, + }); + // `agent.stream()` returns the final `AgentResult` on `{ done: true }`. + // Captured here so the interrupt-variant RUN_FINISHED below can pull + // `stopReason` and `interrupts[]` off it. + let finalAgentResult: StrandsAgentResult | undefined; + + try { + while (true) { + let next: IteratorResult; + try { + next = await agentStream.next(); + } catch (streamErr) { + // Strands throws "Stream ended without completing a message" when + // a frontend tool call halts the agent before the model emits a + // final assistant message. If we've already decided to halt, + // swallow the error — it's expected flow. + if (pendingHalt || haltEventStream) { + haltEventStream = true; + break; + } + throw streamErr; + } + if (next.done) { + finalAgentResult = next.value as StrandsAgentResult | undefined; + break; + } + if (haltEventStream) continue; + + // Strands v1 wraps raw model events inside `ModelStreamUpdateEvent` + // (type: 'modelStreamUpdateEvent', event: ModelStreamEvent) before + // yielding them from `agent.stream()`. Unwrap once so the dispatch + // below operates on the inner event shape. + const event = unwrapStrandsEvent(next.value); + const kind = getEventKind(event); + + // --- Delta events (text, reasoning, tool-use input streaming) --- + // Maps to Python's top-level "data" / "reasoningText" / + // "current_tool_use" branches. + if (kind === "modelContentBlockDeltaEvent") { + const delta = ( + event as unknown as { + delta: + | { type: "textDelta"; text: string } + | { + type: "reasoningContentDelta"; + text?: string; + redactedContent?: Uint8Array; + } + | { type: "toolUseInputDelta"; input: string }; + } + ).delta; + + // Text data chunks. + if (delta.type === "textDelta" && delta.text) { + if (stopTextStreaming) continue; + if (!messageStarted) { + yield { + type: EventType.TEXT_MESSAGE_START, + messageId, + role: "assistant", + }; + messageStarted = true; + } + accumulatedText += delta.text; + yield { + type: EventType.TEXT_MESSAGE_CONTENT, + messageId, + delta: delta.text, + }; + continue; + } + + // Reasoning/thinking text streaming. + if (delta.type === "reasoningContentDelta") { + if (delta.text) { + if (!reasoningStarted) { + reasoningMessageId = uuid(); + yield { + type: EventType.REASONING_START, + messageId: reasoningMessageId, + }; + yield { + type: EventType.REASONING_MESSAGE_START, + messageId: reasoningMessageId, + role: "reasoning", + }; + reasoningStarted = true; + } + yield { + type: EventType.REASONING_MESSAGE_CONTENT, + messageId: reasoningMessageId!, + delta: delta.text, + }; + } else if (delta.redactedContent) { + if (!reasoningStarted) { + reasoningMessageId = uuid(); + yield { + type: EventType.REASONING_START, + messageId: reasoningMessageId, + }; + yield { + type: EventType.REASONING_MESSAGE_START, + messageId: reasoningMessageId, + role: "reasoning", + }; + reasoningStarted = true; + } + yield { + type: EventType.REASONING_ENCRYPTED_VALUE, + subtype: "message", + entityId: reasoningMessageId!, + encryptedValue: Buffer.from(delta.redactedContent).toString( + "base64", + ), + }; + } + continue; + } + + // Tool call input streaming — emits PredictState → TOOL_CALL_START + // → incremental TOOL_CALL_ARGS deltas. Tools declaring an + // argsStreamer take the legacy burst-at-contentBlockStop path. + if (delta.type === "toolUseInputDelta" && currentToolUse) { + currentToolUse.inputChunks.push(delta.input); + const { name: toolName, toolUseId: strandsToolId } = + currentToolUse; + const isFrontendTool = frontendToolNames.has(toolName); + const toolUseId = _resolveToolUseId( + toolCallsSeen, + strandsToolId, + isFrontendTool, + ); + + let entry = toolCallsSeen.get(toolUseId); + if (!entry) { + const isPendingNow = pendingToolResultIds.has(toolUseId); + const behaviorNow = this.config.toolBehaviors?.[toolName]; + this._log.debug( + `${LOG_PREFIX} Tool call event received: toolName=${toolName}, ` + + `toolUseId=${toolUseId}, strandsId=${strandsToolId}, ` + + `isFrontend=${isFrontendTool}, threadId=${inputData.threadId}`, + ); + // Use streaming (emit ToolCallStart + PredictState now, + // ToolCallArgs on each growth, ToolCallEnd at + // contentBlockStop) unless the tool is a continuation or + // supplies a custom argsStreamer. + const useStreaming = + !isPendingNow && !behaviorNow?.argsStreamer; + entry = { + name: toolName, + args: "", + input: {}, + raw: "", + emitted: false, + startEmitted: false, + endEmitted: false, + lastEmittedRawLen: 0, + isPending: isPendingNow, + isFrontend: isFrontendTool, + useStreaming, + strandsToolId, + }; + toolCallsSeen.set(toolUseId, entry); + + if (useStreaming) { + // Close any open assistant text turn so the snapshot order + // matches the wire-event order and message_id can rotate. + if (messageStarted) { + yield { type: EventType.TEXT_MESSAGE_END, messageId }; + if (emitMessagesSnapshot && accumulatedText) { + snapshotMessages.push({ + id: messageId, + role: "assistant", + content: accumulatedText, + } as AguiAssistantMessage); + accumulatedText = ""; + yield { + type: EventType.MESSAGES_SNAPSHOT, + messages: snapshotMessages.slice(), + }; + } + messageStarted = false; + messageId = uuid(); + } + + // PredictState must reach the FE BEFORE any args delta so + // the FE knows which tool argument feeds which state key + // while parsing incremental JSON. + if (behaviorNow) { + const predict = normalizePredictState( + behaviorNow.predictState, + ).map(predictStateMappingToPayload); + if (predict.length > 0) { + yield { + type: EventType.CUSTOM, + name: "PredictState", + value: predict, + }; + } + } + + yield { + type: EventType.TOOL_CALL_START, + toolCallId: toolUseId, + toolCallName: toolName, + parentMessageId: messageId, + }; + entry.startEmitted = true; + } + } + + // Rebuild the accumulated raw string and emit the growth as a + // single TOOL_CALL_ARGS delta. The FE concatenates these into + // the full args payload and parses incrementally. + const rawStr = currentToolUse.inputChunks.join(""); + entry.raw = rawStr; + try { + entry.input = JSON.parse(rawStr); + } catch { + entry.input = rawStr; + } + entry.args = + typeof entry.input === "string" + ? entry.input + : JSON.stringify(entry.input); + + if (entry.startEmitted && entry.useStreaming) { + const lastLen = entry.lastEmittedRawLen ?? 0; + if (rawStr.length > lastLen) { + yield { + type: EventType.TOOL_CALL_ARGS, + toolCallId: toolUseId, + delta: rawStr.slice(lastLen), + }; + entry.lastEmittedRawLen = rawStr.length; + } + } + } + continue; + } + + // Reasoning signature (verification token) — not exposed to UI. + if (kind === "reasoningSignatureEvent") continue; + + // Content block start records tool metadata so toolUseInputDelta + // can correlate its chunks to a tool. Strands v1 emits + // `{ start: { type: "toolUseStart", name, toolUseId } }` — the + // field is `.start`, not `.contentBlock`. + if (kind === "modelContentBlockStartEvent") { + const startWrap = event as unknown as { + start?: { type?: string; name?: string; toolUseId?: string }; + }; + const s = startWrap.start; + if (s?.type === "toolUseStart" && s.name) { + currentToolUse = { + name: s.name, + toolUseId: s.toolUseId ?? uuid(), + inputChunks: [], + }; + } + continue; + } + + // Content block stop — signals tool input is complete. + if (kind === "modelContentBlockStopEvent") { + if (reasoningStarted) { + yield { + type: EventType.REASONING_MESSAGE_END, + messageId: reasoningMessageId!, + }; + yield { + type: EventType.REASONING_END, + messageId: reasoningMessageId!, + }; + reasoningStarted = false; + reasoningMessageId = undefined; + } + + if (currentToolUse) { + const { + name: toolName, + toolUseId: strandsToolId, + inputChunks, + } = currentToolUse; + currentToolUse = null; + const rawInput = inputChunks.join(""); + let parsedInput: unknown = {}; + if (rawInput) { + try { + parsedInput = JSON.parse(rawInput); + } catch { + parsedInput = rawInput; + } + } + const isFrontendTool = frontendToolNames.has(toolName); + const toolUseId = _resolveToolUseId( + toolCallsSeen, + strandsToolId, + isFrontendTool, + ); + const argsStr = + typeof parsedInput === "string" + ? parsedInput + : JSON.stringify(parsedInput); + + if (!toolCallsSeen.has(toolUseId)) { + toolCallsSeen.set(toolUseId, { + name: toolName, + args: argsStr, + input: parsedInput, + emitted: false, + strandsToolId, + raw: rawInput, + }); + } else { + const entry = toolCallsSeen.get(toolUseId)!; + entry.args = argsStr; + entry.input = parsedInput; + entry.raw = rawInput; + } + + const entry = toolCallsSeen.get(toolUseId)!; + const behavior = this.config.toolBehaviors?.[toolName]; + this._log.debug( + `${LOG_PREFIX} contentBlockStop close: toolName=${toolName}, ` + + `toolUseId=${toolUseId}, isFrontendTool=${isFrontendTool}, ` + + `isPending=${entry.isPending ?? false}, useStreaming=${entry.useStreaming ?? false}, ` + + `threadId=${inputData.threadId}`, + ); + + if (entry.startEmitted && entry.useStreaming) { + // Streaming path — PredictState + TOOL_CALL_START + per-delta + // TOOL_CALL_ARGS already went on the wire. Flush any final + // delta, then close the call. + const lastLen = entry.lastEmittedRawLen ?? 0; + if (rawInput.length > lastLen) { + yield { + type: EventType.TOOL_CALL_ARGS, + toolCallId: toolUseId, + delta: rawInput.slice(lastLen), + }; + entry.lastEmittedRawLen = rawInput.length; + } + + // stateFromArgs BEFORE TOOL_CALL_END: CopilotKit v2 releases + // the predict_state buffer at TOOL_CALL_END. Delivering the + // snapshot first means the FE has authoritative state in + // hand at the moment prediction is released. + if (behavior?.stateFromArgs) { + const callCtx: ToolCallContext = { + inputData, + toolName, + toolUseId, + toolInput: parsedInput, + argsStr, + ...buildContextExtras(inputData), + }; + try { + const snapshot = await maybeAwait( + behavior.stateFromArgs(callCtx), + ); + if (snapshot) { + Object.assign(currentState, snapshot); + yield { type: EventType.STATE_SNAPSHOT, snapshot }; + } + } catch (e) { + this._log.warn( + `${LOG_PREFIX} stateFromArgs failed for ${toolName}:`, + e, + ); + } + } + + yield { type: EventType.TOOL_CALL_END, toolCallId: toolUseId }; + entry.endEmitted = true; + entry.emitted = true; + + // Splice point 2 of 4: append the assistant tool-call entry + // to the running snapshot, then rotate message_id so the + // next assistant turn carries a distinct id. + if (emitMessagesSnapshot && !behavior?.skipMessagesSnapshot) { + snapshotMessages.push({ + id: messageId, + role: "assistant", + content: "", + toolCalls: [ + { + id: toolUseId, + type: "function", + function: { + name: toolName || "unknown", + arguments: argsStr || "{}", + }, + }, + ], + } as AguiAssistantMessage); + yield { + type: EventType.MESSAGES_SNAPSHOT, + messages: snapshotMessages.slice(), + }; + messageId = uuid(); + } + + if (isFrontendTool && !behavior?.continueAfterFrontendCall) { + this._log.debug( + `${LOG_PREFIX} Deferring halt after frontend tool call: ` + + `toolName=${toolName}, toolCallId=${toolUseId}, threadId=${inputData.threadId}`, + ); + pendingHalt = true; + } + } else { + // Legacy burst path — behavior.argsStreamer is configured, + // or a continuation turn where the tool is already resolved. + yield* this._emitToolCall({ + inputData, + toolUseId, + isFrontendTool, + pendingToolResultIds, + getMessageId: () => messageId, + setMessageId: (id: string) => { + messageId = id; + }, + getMessageStarted: () => messageStarted, + setMessageStarted: (v: boolean) => { + messageStarted = v; + }, + getAccumulatedText: () => accumulatedText, + setAccumulatedText: (v: string) => { + accumulatedText = v; + }, + snapshotMessages, + emitMessagesSnapshot, + toolCallsSeen, + currentState, + onPendingHalt: () => { + pendingHalt = true; + }, + }); + } + } + continue; + } + + // ContentBlock yielded post-stream as a completed `ToolUseBlock`. + // The streaming path above already emitted the envelope via + // `modelContentBlockStopEvent`; the `emitted` guard inside + // `_emitToolCall` makes this a no-op when that already happened. + // This branch also fires when a provider skips delta events + // entirely (tests, some non-streaming configurations). + if (kind === "toolUseBlock") { + const block = event as unknown as ToolUseBlock; + const isFrontendTool = frontendToolNames.has(block.name); + const toolUseId = _resolveToolUseId( + toolCallsSeen, + block.toolUseId, + isFrontendTool, + ); + const argsStr = + typeof block.input === "string" + ? block.input + : JSON.stringify(block.input); + if (!toolCallsSeen.has(toolUseId)) { + toolCallsSeen.set(toolUseId, { + name: block.name, + args: argsStr, + input: block.input, + emitted: false, + strandsToolId: block.toolUseId, + }); + } else { + const e = toolCallsSeen.get(toolUseId)!; + e.args = argsStr; + e.input = block.input; + } + yield* this._emitToolCall({ + inputData, + toolUseId, + isFrontendTool, + pendingToolResultIds, + getMessageId: () => messageId, + setMessageId: (id: string) => { + messageId = id; + }, + getMessageStarted: () => messageStarted, + setMessageStarted: (v: boolean) => { + messageStarted = v; + }, + getAccumulatedText: () => accumulatedText, + setAccumulatedText: (v: string) => { + accumulatedText = v; + }, + snapshotMessages, + emitMessagesSnapshot, + toolCallsSeen, + currentState, + onPendingHalt: () => { + pendingHalt = true; + }, + }); + continue; + } + + // Tool results from Strands (backend tools). Maps to Python's + // `"message" in event and event["message"]["role"] == "user"` branch. + if (kind === "afterToolCallEvent") { + if (pendingHalt) { + haltEventStream = true; + continue; + } + const hookEvent = event as unknown as { + toolUse: { toolUseId: string; name: string }; + result: ToolResultBlock; + }; + const resultToolId = hookEvent.toolUse.toolUseId; + const toolName = hookEvent.toolUse.name; + + // Skip placeholder results for proxied frontend tools. + if (frontendToolNames.has(toolName)) continue; + + // Parse the content into a usable value. `result.content` is + // required by the SDK type but can be missing on errors or + // malformed tools. A void tool call (returns undefined/null) is + // legitimate — emit an empty TOOL_CALL_RESULT so the UI still + // renders a result card. + let resultData: unknown = null; + const contentBlocks = hookEvent.result?.content; + if (Array.isArray(contentBlocks)) { + for (const cb of contentBlocks) { + if (cb instanceof TextBlock) { + try { + resultData = JSON.parse(cb.text); + } catch { + try { + resultData = JSON.parse(cb.text.replace(/'/g, '"')); + } catch { + resultData = cb.text; + } + } + break; + } + const maybeJson = (cb as unknown as { json?: unknown }).json; + if (maybeJson !== undefined) { + resultData = maybeJson; + break; + } + } + } + + if (!resultToolId) continue; + + const callInfo = toolCallsSeen.get(resultToolId); + const toolArgs = callInfo?.args; + const toolInput = callInfo?.input; + const behavior = this.config.toolBehaviors?.[toolName]; + + this._log.debug( + `${LOG_PREFIX} Processing tool result: toolName=${toolName}, ` + + `resultToolId=${resultToolId}, threadId=${inputData.threadId}`, + ); + + // Emit TOOL_CALL_RESULT without a role field so the frontend + // completes the tool in UI without adding it to the conversation + // history. A fresh message id ensures CopilotKit creates a + // standalone ToolMessage and closes the spinner correctly. + const toolResultMessageId = uuid(); + const toolResultContent = + resultData == null ? "" : JSON.stringify(resultData); + yield { + type: EventType.TOOL_CALL_RESULT, + toolCallId: resultToolId, + messageId: toolResultMessageId, + content: toolResultContent, + }; + + // Splice point 3 of 4: append the ToolMessage to the running + // snapshot so the frontend can pair call + result. + if (emitMessagesSnapshot && !behavior?.skipMessagesSnapshot) { + snapshotMessages.push({ + id: toolResultMessageId, + role: "tool", + content: toolResultContent, + toolCallId: resultToolId, + } as AguiToolMessage); + yield { + type: EventType.MESSAGES_SNAPSHOT, + messages: snapshotMessages.slice(), + }; + } + + const resultContext: ToolResultContext = { + inputData, + toolName, + toolUseId: resultToolId, + toolInput, + argsStr: toolArgs ?? "{}", + resultData, + messageId, + ...buildContextExtras(inputData), + }; + + if (behavior?.stateFromResult) { + try { + const snapshot = await maybeAwait( + behavior.stateFromResult(resultContext), + ); + if (snapshot) { + Object.assign(currentState, snapshot); + yield { type: EventType.STATE_SNAPSHOT, snapshot }; + } + } catch (e) { + this._log.warn( + `${LOG_PREFIX} stateFromResult failed for ${toolName}:`, + e, + ); + } + } + + if (behavior?.customResultHandler) { + try { + for await (const customEvent of behavior.customResultHandler( + resultContext, + )) { + if (customEvent) yield customEvent; + } + } catch (e) { + this._log.warn( + `${LOG_PREFIX} customResultHandler failed for ${toolName}:`, + e, + ); + } + } + + if (behavior?.stopStreamingAfterResult) { + stopTextStreaming = true; + if (messageStarted) { + yield { type: EventType.TEXT_MESSAGE_END, messageId }; + messageStarted = false; + // Splice point 4 of 4 (early-exit): commit accumulated + // assistant text into the snapshot. + if (emitMessagesSnapshot && accumulatedText) { + snapshotMessages.push({ + id: messageId, + role: "assistant", + content: accumulatedText, + } as AguiAssistantMessage); + accumulatedText = ""; + yield { + type: EventType.MESSAGES_SNAPSHOT, + messages: snapshotMessages.slice(), + }; + } + } + this._log.debug( + `${LOG_PREFIX} Breaking event stream: stopStreamingAfterResult behavior triggered ` + + `(threadId=${inputData.threadId}, toolName=${toolName})`, + ); + haltEventStream = true; + break; + } + continue; + } + + // Tools can yield state updates mid-execution as toolStreamEvent. + if (kind === "toolStreamEvent") { + const stream = event as unknown as { data?: unknown }; + const data = stream.data; + if (data && typeof data === "object" && "state" in data) { + yield { + type: EventType.STATE_SNAPSHOT, + snapshot: (data as { state: Record }).state, + }; + } + continue; + } + + // Multi-agent events (only fire when `agent` is a Graph/Swarm — + // also possible when an agent wraps a subgraph). + const maEvent = event as unknown as { + type?: string; + nodeId?: string; + nodeType?: string; + source?: string; + targets?: string[]; + }; + if (maEvent?.type === "beforeNodeCallEvent") { + // stepName must match the paired afterNodeCallEvent below so + // frontends can pair START/FINISH (events.mdx §StepFinished). + yield { + type: EventType.STEP_STARTED, + stepName: `${maEvent.nodeType ?? "agent"}:${maEvent.nodeId ?? "unknown"}`, + }; + continue; + } + if (maEvent?.type === "afterNodeCallEvent") { + yield { + type: EventType.STEP_FINISHED, + stepName: `${maEvent.nodeType ?? "agent"}:${maEvent.nodeId ?? "unknown"}`, + }; + continue; + } + if (maEvent?.type === "multiAgentHandoffEvent") { + // Py wire shape: { from_nodes, to_nodes, message }. TS SDK gives + // `source` + `targets`; wrap source in an array to preserve the + // Py shape so downstream consumers don't need per-backend branching. + const handoffMsg = (maEvent as { message?: string }).message; + yield { + type: EventType.CUSTOM, + name: "MultiAgentHandoff", + value: { + from_nodes: maEvent.source ? [maEvent.source] : [], + to_nodes: maEvent.targets ?? [], + message: handoffMsg, + }, + }; + continue; + } + // Ignore events we don't translate (BeforeInvocationEvent, + // ModelStreamEventHook wrappers, etc.). + } + } finally { + // Consumer bailed (client disconnect, frontend-tool halt, error). + // Fire the abort signal so Strands stops its Bedrock fetch at the + // next checkpoint, then drain the generator so cleanup hooks run. + try { + runAbort.abort(); + } catch { + // ignore + } + try { + await agentStream.return(undefined as never); + } catch { + // ignore — cancellation typically surfaces as CancelledError + } + } + + if (reasoningStarted) { + yield { + type: EventType.REASONING_MESSAGE_END, + messageId: reasoningMessageId!, + }; + yield { type: EventType.REASONING_END, messageId: reasoningMessageId! }; + } + + if (messageStarted) { + yield { type: EventType.TEXT_MESSAGE_END, messageId }; + // Splice point 4 of 4 (terminal): commit the final assistant text + // turn into the snapshot. + if (emitMessagesSnapshot && accumulatedText) { + snapshotMessages.push({ + id: messageId, + role: "assistant", + content: accumulatedText, + } as AguiAssistantMessage); + accumulatedText = ""; + yield { + type: EventType.MESSAGES_SNAPSHOT, + messages: snapshotMessages.slice(), + }; + } + } + + // Final state snapshot with `currentState` verbatim. Unlike the initial + // snapshot this is not filtered — the initial filter exists only to + // protect frontends that don't recognise the "tool" role. + yield { type: EventType.STATE_SNAPSHOT, snapshot: currentState }; + + // Interrupt-variant RUN_FINISHED. The STATE_SNAPSHOT + + // MESSAGES_SNAPSHOT above precede this per interrupts.mdx §"State at + // the interrupt boundary". IDs are recorded on + // `_pendingInterruptsByThread` for the `run()` resume gate. + if (finalAgentResult?.stopReason === "interrupt") { + const strandsInterrupts = finalAgentResult.interrupts ?? []; + if (strandsInterrupts.length > 0) { + const interruptIds = strandsInterrupts.map((i) => i.id); + this._pendingInterruptsByThread.set(threadId, new Set(interruptIds)); + yield { + type: EventType.RUN_FINISHED, + threadId: inputData.threadId, + runId: inputData.runId, + outcome: { + type: "interrupt", + interrupts: strandsInterrupts.map(strandsInterruptToAgui), + }, + }; + return; + } + } + + yield { + type: EventType.RUN_FINISHED, + threadId: inputData.threadId, + runId: inputData.runId, + }; + } catch (e) { + yield _runError(_errorMessage(e), "STRANDS_ERROR"); + } + } + + /** + * Legacy burst path for tool calls — invoked when the Strands SDK delivers + * a complete `ToolUseBlock` or when a `ToolBehavior.argsStreamer` takes + * over args emission at contentBlockStop. + * + * The streaming path inside `_runSingleAgent` handles the common case + * directly; this helper handles continuation turns and custom streamers. + * + * Getters/setters surface the caller's local variables because JS closures + * capture by reference only for `const` / `let` in scope — an object of + * mutable fields would work but would require threading `state` through + * `_runSingleAgent`'s long body. + */ + private async *_emitToolCall(ctx: { + inputData: RunAgentInput; + toolUseId: string; + isFrontendTool: boolean; + pendingToolResultIds: Set; + getMessageId: () => string; + setMessageId: (id: string) => void; + getMessageStarted: () => boolean; + setMessageStarted: (v: boolean) => void; + getAccumulatedText: () => string; + setAccumulatedText: (v: string) => void; + snapshotMessages: AguiMessage[]; + emitMessagesSnapshot: boolean; + toolCallsSeen: Map; + currentState: Record; + onPendingHalt: () => void; + }): AsyncGenerator { + const entry = ctx.toolCallsSeen.get(ctx.toolUseId); + if (!entry || entry.emitted) return; + entry.emitted = true; + const toolName = entry.name; + const argsStr = entry.args; + const toolInput = entry.input; + const behavior = this.config.toolBehaviors?.[toolName]; + const isPending = ctx.pendingToolResultIds.has(ctx.toolUseId); + + const callContext: ToolCallContext = { + inputData: ctx.inputData, + toolName, + toolUseId: ctx.toolUseId, + toolInput, + argsStr, + ...buildContextExtras(ctx.inputData), + }; + + // Continuation turn — tool already resolved in conversation history. + // Don't re-emit wire events, but fire state callbacks so derived state + // stays consistent. + if (isPending) { + if (behavior?.stateFromArgs) { + try { + const snapshot = await maybeAwait( + behavior.stateFromArgs(callContext), + ); + if (snapshot) { + Object.assign(ctx.currentState, snapshot); + yield { type: EventType.STATE_SNAPSHOT, snapshot }; + } + } catch (e) { + this._log.warn( + `${LOG_PREFIX} stateFromArgs failed for ${toolName}:`, + e, + ); + } + } + return; + } + + // stateFromArgs BEFORE TOOL_CALL_START seeds the frontend's derived + // state before the predict_state buffer opens. + if (behavior?.stateFromArgs) { + try { + const snapshot = await maybeAwait(behavior.stateFromArgs(callContext)); + if (snapshot) { + Object.assign(ctx.currentState, snapshot); + yield { type: EventType.STATE_SNAPSHOT, snapshot }; + } + } catch (e) { + this._log.warn( + `${LOG_PREFIX} stateFromArgs failed for ${toolName}:`, + e, + ); + } + } + + if (behavior) { + const predict = normalizePredictState(behavior.predictState).map( + predictStateMappingToPayload, + ); + if (predict.length > 0) { + yield { type: EventType.CUSTOM, name: "PredictState", value: predict }; + } + } + + // Close any open assistant text turn and commit its content to the + // snapshot before rotating message_id. + if (ctx.getMessageStarted()) { + yield { type: EventType.TEXT_MESSAGE_END, messageId: ctx.getMessageId() }; + const acc = ctx.getAccumulatedText(); + if (ctx.emitMessagesSnapshot && acc) { + ctx.snapshotMessages.push({ + id: ctx.getMessageId(), + role: "assistant", + content: acc, + } as AguiAssistantMessage); + ctx.setAccumulatedText(""); + yield { + type: EventType.MESSAGES_SNAPSHOT, + messages: ctx.snapshotMessages.slice(), + }; + } + ctx.setMessageStarted(false); + ctx.setMessageId(uuid()); + } + + yield { + type: EventType.TOOL_CALL_START, + toolCallId: ctx.toolUseId, + toolCallName: toolName, + parentMessageId: ctx.getMessageId(), + }; + + if (behavior?.argsStreamer) { + try { + for await (const chunk of behavior.argsStreamer(callContext)) { + if (chunk == null) continue; + yield { + type: EventType.TOOL_CALL_ARGS, + toolCallId: ctx.toolUseId, + delta: String(chunk), + }; + } + } catch (e) { + this._log.warn( + `${LOG_PREFIX} argsStreamer failed for ${toolName}, falling back to full args:`, + e, + ); + yield { + type: EventType.TOOL_CALL_ARGS, + toolCallId: ctx.toolUseId, + delta: argsStr, + }; + } + } else { + yield { + type: EventType.TOOL_CALL_ARGS, + toolCallId: ctx.toolUseId, + delta: argsStr, + }; + } + + yield { type: EventType.TOOL_CALL_END, toolCallId: ctx.toolUseId }; + + // Splice point 2 of 4: append the assistant tool-call entry to the + // snapshot, then rotate message_id. + if (ctx.emitMessagesSnapshot && !behavior?.skipMessagesSnapshot) { + ctx.snapshotMessages.push({ + id: ctx.getMessageId(), + role: "assistant", + content: "", + toolCalls: [ + { + id: ctx.toolUseId, + type: "function", + function: { + name: toolName || "unknown", + arguments: argsStr || "{}", + }, + }, + ], + } as AguiAssistantMessage); + yield { + type: EventType.MESSAGES_SNAPSHOT, + messages: ctx.snapshotMessages.slice(), + }; + ctx.setMessageId(uuid()); + } + + if (ctx.isFrontendTool && !behavior?.continueAfterFrontendCall) { + this._log.debug( + `${LOG_PREFIX} Deferring halt after frontend tool call: ` + + `toolName=${toolName}, toolCallId=${ctx.toolUseId}, threadId=${ctx.inputData.threadId}`, + ); + ctx.onPendingHalt(); + } + } + + /** + * Orchestrator-mode run loop. TypeScript-only: drives a `Graph` or `Swarm` + * `.stream()` call and translates multi-agent events. Per-thread caching, + * session managers, and proxy-tool sync don't apply. + */ + private async *_runOrchestrator( + inputData: RunAgentInput, + ): AsyncGenerator { + yield _runStarted(inputData); + try { + if (inputData.state && typeof inputData.state === "object") { + const snapshot: Record = {}; + for (const [k, v] of Object.entries( + inputData.state as Record, + )) { + if (k !== "messages") snapshot[k] = v; + } + yield { type: EventType.STATE_SNAPSHOT, snapshot }; + } + + // Orchestrators take string | ContentBlock[] (MultiAgentInput); extract + // text from the last user/tool turn. + let prompt = "Hello"; + if (inputData.messages) { + for (let i = inputData.messages.length - 1; i >= 0; i--) { + const msg = inputData.messages[i]; + if (!msg) break; + if ( + (msg.role === "user" || msg.role === "tool") && + msg.content != null + ) { + prompt = + typeof msg.content === "string" + ? msg.content + : flattenContentToText(msg.content); + break; + } + } + } + + let messageId = uuid(); + let messageStarted = false; + let reasoningStarted = false; + let reasoningMessageId: string | undefined; + + for await (const rawEvent of this._orchestrator!.stream(prompt)) { + const event = unwrapStrandsEvent(rawEvent); + const kind = getEventKind(event); + + if (kind === "beforeNodeCallEvent") { + const ev = event as { nodeId?: string; nodeType?: string }; + // stepName must match the paired afterNodeCallEvent below so + // frontends can pair START/FINISH (events.mdx §StepFinished). + yield { + type: EventType.STEP_STARTED, + stepName: `${ev.nodeType ?? "agent"}:${ev.nodeId ?? "unknown"}`, + }; + continue; + } + if (kind === "afterNodeCallEvent") { + const ev = event as { nodeId?: string; nodeType?: string }; + if (messageStarted) { + yield { type: EventType.TEXT_MESSAGE_END, messageId }; + messageStarted = false; + messageId = uuid(); + } + if (reasoningStarted) { + yield { + type: EventType.REASONING_MESSAGE_END, + messageId: reasoningMessageId!, + }; + yield { + type: EventType.REASONING_END, + messageId: reasoningMessageId!, + }; + reasoningStarted = false; + reasoningMessageId = undefined; + } + yield { + type: EventType.STEP_FINISHED, + stepName: `${ev.nodeType ?? "agent"}:${ev.nodeId ?? "unknown"}`, + }; + continue; + } + if (kind === "multiAgentHandoffEvent") { + const ev = event as { + source?: string; + targets?: string[]; + message?: string; + }; + yield { + type: EventType.CUSTOM, + name: "MultiAgentHandoff", + value: { + from_nodes: ev.source ? [ev.source] : [], + to_nodes: ev.targets ?? [], + message: ev.message, + }, + }; + continue; + } + if (kind === "nodeStreamUpdateEvent") { + // Inner event is the agent-level event emitted by the wrapped agent. + const ev = event as { inner?: { source?: string; event?: unknown } }; + const inner = ev.inner?.event + ? unwrapStrandsEvent(ev.inner.event) + : undefined; + if (getEventKind(inner) === "modelContentBlockDeltaEvent") { + const delta = ( + inner as { delta?: { type?: string; text?: string } } + ).delta; + if (delta?.type === "textDelta" && delta.text) { + if (!messageStarted) { + yield { + type: EventType.TEXT_MESSAGE_START, + messageId, + role: "assistant", + }; + messageStarted = true; + } + yield { + type: EventType.TEXT_MESSAGE_CONTENT, + messageId, + delta: delta.text, + }; + } else if (delta?.type === "reasoningContentDelta" && delta.text) { + if (!reasoningStarted) { + reasoningMessageId = uuid(); + yield { + type: EventType.REASONING_START, + messageId: reasoningMessageId, + }; + yield { + type: EventType.REASONING_MESSAGE_START, + messageId: reasoningMessageId, + role: "reasoning", + }; + reasoningStarted = true; + } + yield { + type: EventType.REASONING_MESSAGE_CONTENT, + messageId: reasoningMessageId!, + delta: delta.text, + }; + } + } + continue; + } + } + + if (messageStarted) { + yield { type: EventType.TEXT_MESSAGE_END, messageId }; + } + if (reasoningStarted) { + yield { + type: EventType.REASONING_MESSAGE_END, + messageId: reasoningMessageId!, + }; + yield { type: EventType.REASONING_END, messageId: reasoningMessageId! }; + } + yield { type: EventType.STATE_SNAPSHOT, snapshot: {} }; + yield { + type: EventType.RUN_FINISHED, + threadId: inputData.threadId, + runId: inputData.runId, + }; + } catch (e) { + yield _runError(_errorMessage(e), "STRANDS_ERROR"); + } + } + + private _buildThreadAgentConfig( + sessionManager?: SessionManager, + seedMessages?: AgentConfig["messages"], + ): AgentConfig { + const t = this._templateFields; + const cfg: AgentConfig = { + model: t.model, + tools: t.tools.slice(), + printer: false, + }; + if (t.systemPrompt !== undefined) cfg.systemPrompt = t.systemPrompt; + if (t.name !== undefined) cfg.name = t.name; + if (t.description !== undefined) cfg.description = t.description; + if (t.id !== undefined) cfg.id = t.id; + if (t.appState !== undefined) cfg.appState = t.appState; + if (t.modelState !== undefined) cfg.modelState = t.modelState; + if (t.traceAttributes !== undefined) + cfg.traceAttributes = t.traceAttributes; + if (t.structuredOutputSchema !== undefined) + cfg.structuredOutputSchema = t.structuredOutputSchema; + if (t.toolExecutor !== undefined) cfg.toolExecutor = t.toolExecutor; + if (sessionManager) cfg.sessionManager = sessionManager; + if (seedMessages && seedMessages.length > 0) cfg.messages = seedMessages; + // Only forward plugins when the caller supplied them explicitly. Passing + // `plugins: []` risks being interpreted by a future SDK as "disable + // default plugins". + if (this._plugins.length > 0) cfg.plugins = [...this._plugins]; + return cfg; + } +} + +// ---------- TypeScript-only helpers (no Python equivalent) ---------- + +/** + * Async mutex modelled on Python's `asyncio.Lock`. Serializes first-time + * thread initialization so concurrent requests for the same new threadId + * don't both construct a per-thread agent. + */ +class AsyncMutex { + private _tail: Promise = Promise.resolve(); + async acquire(): Promise<() => void> { + let release!: () => void; + const next = new Promise((resolve) => { + release = resolve; + }); + const previous = this._tail; + this._tail = next; + await previous; + return release; + } +} + +function _runStarted(input: RunAgentInput): BaseEvent { + return { + type: EventType.RUN_STARTED, + threadId: input.threadId, + runId: input.runId, + }; +} + +function _runError(message: string, code: string): BaseEvent { + return { type: EventType.RUN_ERROR, message, code }; +} + +/** Non-empty `resume[]` entries, or `[]` if missing. */ +function resolveResumeEntries(input: RunAgentInput): ResumeEntry[] { + const resume = (input as { resume?: ResumeEntry[] }).resume; + return Array.isArray(resume) && resume.length > 0 ? resume : []; +} + +/** AG-UI `ResumeEntry` → Strands `InterruptResponseContent.response`. */ +function toResumeResponse(entry: ResumeEntry): unknown { + if (entry.status === "cancelled") { + return { status: "cancelled" }; + } + return entry.payload as unknown; +} + +/** Strands `Interrupt` → AG-UI `Interrupt`. */ +function strandsInterruptToAgui(interrupt: StrandsInterrupt): AguiInterrupt { + const reasonRaw = interrupt.reason; + const reason = + typeof reasonRaw === "string" && reasonRaw.length > 0 + ? reasonRaw + : "confirmation"; + const out: AguiInterrupt = { id: interrupt.id, reason }; + if (typeof reasonRaw === "string" && reasonRaw.length > 0) { + out.message = reasonRaw; + } else if (reasonRaw != null) { + try { + out.message = JSON.stringify(reasonRaw); + } catch { + // non-serializable reason; leave message unset + } + } + out.metadata = { strandsName: interrupt.name }; + return out; +} + +function getEventKind(event: unknown): string | undefined { + if (event && typeof event === "object" && "type" in event) { + const t = (event as { type: unknown }).type; + return typeof t === "string" ? t : undefined; + } + return undefined; +} + +/** + * Unwrap wrapper hook events the Strands v1 SDK uses to decorate raw model, + * content-block, and tool-stream events: + * `ModelStreamUpdateEvent → .event` + * `ContentBlockEvent → .contentBlock` + * `ToolStreamUpdateEvent → .event` (inner `ToolStreamEvent` carries + * the per-yield payload a tool's async generator produces) + * Anything else passes through. + */ +function unwrapStrandsEvent(event: unknown): unknown { + if (!event || typeof event !== "object") return event; + const kind = (event as { type?: unknown }).type; + if (kind === "modelStreamUpdateEvent" && "event" in event) { + return (event as { event: unknown }).event; + } + if (kind === "toolStreamUpdateEvent" && "event" in event) { + return (event as { event: unknown }).event; + } + if (kind === "contentBlockEvent" && "contentBlock" in event) { + return (event as { contentBlock: unknown }).contentBlock; + } + return event; +} + +/** + * Transform explicit START/CONTENT/END triples into self-expanding chunk + * equivalents, driven by `StrandsAgentConfig.emitChunkEvents`. + * + * Per `concepts/events.mdx` (TextMessageChunk): + * - First chunk carries `messageId` (+ optional `role`) — the client + * transformer auto-emits `TEXT_MESSAGE_START`. + * - Each chunk with a `delta` auto-emits `TEXT_MESSAGE_CONTENT`. + * - `TEXT_MESSAGE_END` is auto-emitted by the client transformer when + * ids change or the stream ends — we drop our explicit END event. + * + * Same pattern for `TOOL_CALL_*` and `REASONING_MESSAGE_*`. + */ +async function* collapseToChunkEvents( + source: AsyncGenerator, +): AsyncGenerator { + for await (const event of source) { + switch (event.type) { + case EventType.TEXT_MESSAGE_START: { + const e = event as { messageId?: string; role?: string }; + yield { + type: EventType.TEXT_MESSAGE_CHUNK, + messageId: e.messageId, + role: e.role, + } as BaseEvent; + break; + } + case EventType.TEXT_MESSAGE_CONTENT: { + const e = event as { messageId?: string; delta?: string }; + yield { + type: EventType.TEXT_MESSAGE_CHUNK, + messageId: e.messageId, + delta: e.delta, + } as BaseEvent; + break; + } + case EventType.TEXT_MESSAGE_END: + break; + case EventType.TOOL_CALL_START: { + const e = event as { + toolCallId?: string; + toolCallName?: string; + parentMessageId?: string; + }; + yield { + type: EventType.TOOL_CALL_CHUNK, + toolCallId: e.toolCallId, + toolCallName: e.toolCallName, + parentMessageId: e.parentMessageId, + } as BaseEvent; + break; + } + case EventType.TOOL_CALL_ARGS: { + const e = event as { toolCallId?: string; delta?: string }; + yield { + type: EventType.TOOL_CALL_CHUNK, + toolCallId: e.toolCallId, + delta: e.delta, + } as BaseEvent; + break; + } + case EventType.TOOL_CALL_END: + break; + case EventType.REASONING_MESSAGE_START: { + const e = event as { messageId?: string }; + yield { + type: EventType.REASONING_MESSAGE_CHUNK, + messageId: e.messageId, + } as BaseEvent; + break; + } + case EventType.REASONING_MESSAGE_CONTENT: { + const e = event as { messageId?: string; delta?: string }; + yield { + type: EventType.REASONING_MESSAGE_CHUNK, + messageId: e.messageId, + delta: e.delta, + } as BaseEvent; + break; + } + case EventType.REASONING_MESSAGE_END: + break; + default: + yield event; + } + } +} + +/** + * Build the message-history seed handed to `AgentConfig.messages` on + * cold-cache agent creation. TypeScript-only: the Python SDK mutates + * `Agent.messages` in place after construction via + * `_buildStrandsHistory`, whereas the TS SDK consumes a seed at + * construction time. + * + * - Normal run (tail is a `user` turn): seed everything except the final + * user turn; the final turn is passed to `agent.stream(...)` as the + * fresh prompt. + * - Continuation run (tail is a `tool` message) or orphan tail: seed the + * entire history so the agent sees its own tool call + result before the + * synthetic continuation prompt fires. + * + * Returns `undefined` when the resulting seed would be empty or would + * start with an `assistant` turn (Bedrock rejects assistant-first history). + */ +export async function buildStrandsSeed( + messages: AguiMessage[], + log?: Logger, +): Promise { + if (messages.length === 0) return undefined; + + let sliceEnd = messages.length; + const tail = messages[messages.length - 1]; + if (tail?.role === "user") sliceEnd = messages.length - 1; + if (sliceEnd <= 0) return undefined; + + const seed = await convertMessagesForStrandsSeed( + messages.slice(0, sliceEnd), + log, + ); + if (seed.length === 0) return undefined; + + // Bedrock requires history to start with `user`; trim any leading + // assistant turns (rare, e.g. bot-initiated UIs). + while (seed.length > 0 && seed[0]?.role !== "user") seed.shift(); + if (seed.length === 0) return undefined; + + return seed as unknown as AgentConfig["messages"]; +} + +/** + * Convert AG-UI messages into the `MessageData` shape `AgentConfig.messages` + * accepts on cold-cache agent construction. Similar in spirit to + * `_buildStrandsHistory` but drops orphan tool turns (Bedrock rejects them). + */ +export async function convertMessagesForStrandsSeed( + messages: AguiMessage[], + log?: Logger, +): Promise> { + const out: Array<{ role: "user" | "assistant"; content: unknown[] }> = []; + let pendingToolCalls: Map | null = null; + let pendingToolResults: unknown[] | null = null; + + const flushToolResults = (): void => { + if (pendingToolResults && pendingToolResults.length > 0) { + out.push({ role: "user", content: pendingToolResults }); + } + pendingToolResults = null; + pendingToolCalls = null; + }; + + for (const msg of messages) { + const role = msg.role; + if (role === "system" || role === "developer") continue; + + if (role === "assistant") { + flushToolResults(); + const toolCalls = ( + msg as { + toolCalls?: { + id: string; + function: { name: string; arguments: string }; + }[]; + } + ).toolCalls; + const content: unknown[] = []; + if (typeof msg.content === "string" && msg.content.length > 0) { + content.push({ text: msg.content }); + } else if (Array.isArray(msg.content)) { + // Assistant-side multimodal history is rare — preserve text only. + for (const c of msg.content) { + if (c && typeof c === "object" && "text" in (c as object)) { + content.push({ text: (c as { text: string }).text }); + } + } + } + if (toolCalls && toolCalls.length > 0) { + pendingToolCalls = new Map(); + for (const tc of toolCalls) { + if (!tc?.id || !tc.function?.name) continue; + let input: unknown = {}; + try { + input = tc.function.arguments + ? JSON.parse(tc.function.arguments) + : {}; + } catch { + input = tc.function.arguments ?? {}; + } + content.push({ + toolUse: { name: tc.function.name, toolUseId: tc.id, input }, + }); + pendingToolCalls.set(tc.id, tc.function.name); + } + } + if (content.length === 0) continue; + out.push({ role: "assistant", content }); + continue; + } + + if (role === "tool") { + const toolCallId = (msg as { toolCallId?: string }).toolCallId; + if (!toolCallId || !pendingToolCalls || !pendingToolCalls.has(toolCallId)) + continue; + const rawContent: unknown = (msg as { content?: unknown }).content; + const textContent = + typeof rawContent === "string" + ? rawContent + : Array.isArray(rawContent) + ? (rawContent as unknown[]) + .map((c) => + c && typeof c === "object" && "text" in (c as object) + ? ((c as { text?: string }).text ?? "") + : "", + ) + .join("") + : ""; + pendingToolResults ??= []; + pendingToolResults.push({ + toolResult: { + toolUseId: toolCallId, + status: "success" as const, + content: [{ text: textContent }], + }, + }); + continue; + } + + // role === "user" + flushToolResults(); + const content: unknown[] = []; + const rawUserContent = msg.content; + if (typeof rawUserContent === "string") { + if (rawUserContent.length > 0) content.push({ text: rawUserContent }); + } else if (Array.isArray(rawUserContent)) { + const hasMedia = rawUserContent.some((c: unknown) => { + if (!c || typeof c !== "object") return false; + const type = (c as { type?: string }).type; + return ( + type === "image" || + type === "audio" || + type === "video" || + type === "document" + ); + }); + if (hasMedia) { + try { + const blocks = await convertAguiContentToStrands( + rawUserContent as never, + log, + ); + for (const b of blocks) { + if (b instanceof TextBlock) { + content.push({ text: b.text }); + } else { + // Image/Video/Document `toJSON()` emits the wrapped + // discriminated union the MessageData schema expects. + const serialised = + typeof (b as { toJSON?: () => unknown }).toJSON === "function" + ? (b as { toJSON: () => unknown }).toJSON() + : b; + content.push(serialised); + } + } + } catch (e) { + (log ?? DEFAULT_LOGGER).warn( + `${LOG_PREFIX} seed multimodal conversion failed; dropping attachments for this turn`, + e, + ); + const text = flattenContentToText(rawUserContent as never); + if (text.length > 0) content.push({ text }); + } + } else { + for (const c of rawUserContent) { + if (c && typeof c === "object" && "text" in (c as object)) { + content.push({ text: (c as { text: string }).text }); + } + } + } + } + if (content.length === 0) continue; + out.push({ role: "user", content }); + } + + flushToolResults(); + return out; +} diff --git a/integrations/aws-strands/typescript/src/client-proxy-tool.ts b/integrations/aws-strands/typescript/src/client-proxy-tool.ts new file mode 100644 index 0000000000..f63fb10184 --- /dev/null +++ b/integrations/aws-strands/typescript/src/client-proxy-tool.ts @@ -0,0 +1,123 @@ +/** Utilities for forwarding client-defined tools to the Strands agent at runtime. */ + +import { + Agent, + TextBlock, + ToolResultBlock, + type Tool, + type ToolContext, + type ToolSpec, + type ToolStreamGenerator, +} from "@strands-agents/sdk"; +import type { Tool as AguiTool } from "@ag-ui/core"; + +import { DEFAULT_LOGGER, type Logger } from "./logger"; + +const LOG_PREFIX = "[@ag-ui/aws-strands]"; + +/** Derived from `Agent.toolRegistry` because Strands doesn't re-export the type. */ +export type StrandsToolRegistry = Agent["toolRegistry"]; + +// Symbol set on proxy tools so we can distinguish them from native tools. +const PROXY_MARKER = Symbol.for("@ag-ui/aws-strands.proxyTool"); + +interface ProxyTool extends Tool { + readonly [PROXY_MARKER]: true; +} + +/** + * Convert an AG-UI `Tool` into a Strands proxy `Tool`. + * + * When invoked server-side the proxy returns a placeholder result — the real + * execution happens on the client. Proxy tools are distinguishable from + * tools registered at server startup via an internal symbol marker. + */ +export function createProxyTool(tool: AguiTool): Tool { + const spec: ToolSpec = { + name: tool.name, + description: tool.description ?? "", + inputSchema: (tool.parameters ?? { + type: "object", + properties: {}, + }) as ToolSpec["inputSchema"], + }; + const proxy: ProxyTool = { + name: spec.name, + description: spec.description ?? "", + toolSpec: spec, + [PROXY_MARKER]: true, + // `yield` is deliberately omitted — the adapter filters the placeholder + // result out before it becomes a TOOL_CALL_RESULT on the wire. The + // generator type keeps the Strands contract happy. + async *stream(toolContext: ToolContext): ToolStreamGenerator { + return new ToolResultBlock({ + toolUseId: toolContext.toolUse.toolUseId, + status: "success", + content: [new TextBlock("Forwarded to client")], + }); + }, + }; + return proxy; +} + +/** Returns `true` if `tool` was created by `createProxyTool`. */ +export function isProxyTool(tool: unknown): boolean { + return ( + typeof tool === "object" && + tool !== null && + (tool as { [PROXY_MARKER]?: boolean })[PROXY_MARKER] === true + ); +} + +/** + * Synchronise proxy tools in `toolRegistry` with `aguiTools`. + * + * - New tools present in `aguiTools` but absent from the registry are + * registered (unless a native, non-proxy tool with the same name exists). + * - Stale proxy tools in `trackedNames` but absent from `aguiTools` are + * removed. + * + * Returns the updated set of proxy tool names currently registered. + */ +export function syncProxyTools( + toolRegistry: StrandsToolRegistry, + aguiTools: AguiTool[], + trackedNames: Set, + log: Logger = DEFAULT_LOGGER, +): Set { + const desiredNames = new Set(); + for (const t of aguiTools) { + if (t.name) desiredNames.add(t.name); + } + + // Remove stale proxy tools. + for (const name of trackedNames) { + if (desiredNames.has(name)) continue; + const existing = toolRegistry.get(name); + if (existing && isProxyTool(existing)) { + toolRegistry.remove(name); + log.debug(`${LOG_PREFIX} Removed stale proxy tool: ${name}`); + } + } + + // Add or refresh proxy tools. + const current = new Set(); + for (const t of aguiTools) { + if (!t.name) continue; + const existing = toolRegistry.get(t.name); + if (existing && !isProxyTool(existing)) { + // Native tool — do not overwrite. + log.debug(`${LOG_PREFIX} Skipping proxy for native tool: ${t.name}`); + continue; + } + if (existing) { + // Remove then re-register to pick up any schema changes. + toolRegistry.remove(t.name); + } + toolRegistry.add(createProxyTool(t)); + current.add(t.name); + log.debug(`${LOG_PREFIX} Registered proxy tool: ${t.name}`); + } + + return current; +} diff --git a/integrations/aws-strands/typescript/src/config.ts b/integrations/aws-strands/typescript/src/config.ts new file mode 100644 index 0000000000..8d093004ad --- /dev/null +++ b/integrations/aws-strands/typescript/src/config.ts @@ -0,0 +1,239 @@ +/** Configuration primitives for customizing Strands agent behavior. */ + +import type { RunAgentInput, BaseEvent } from "@ag-ui/core"; +import type { SessionManager } from "@strands-agents/sdk"; + +import type { Logger } from "./logger"; + +export type StatePayload = Record; + +/** + * Free-form key/value map carried on `RunAgentInput.context[]` and + * `RunAgentInput.forwardedProps`. Exposed on hook contexts so behaviors can + * react to e.g. per-request auth tokens or locale without re-parsing + * `inputData`. + * + * TypeScript-only: the Python adapter passes `input_data` directly to hooks + * and callers pull these fields off themselves. + */ +export interface ToolCallContextExtras { + /** + * `RunAgentInput.context[]` flattened by `description` → `value`. + * Duplicates: later entries overwrite earlier ones. Keys `__proto__`, + * `constructor`, and `prototype` are dropped to prevent prototype-pollution + * surprises in downstream `Object.assign(target, ctx.context)` usage. + */ + context: Readonly>; + /** + * `RunAgentInput.forwardedProps` as an opaque record. Shape is defined by + * the frontend; the adapter does not introspect it. + */ + forwardedProps: Readonly>; +} + +/** Context passed to tool call hooks. */ +export interface ToolCallContext extends ToolCallContextExtras { + inputData: RunAgentInput; + toolName: string; + toolUseId: string; + toolInput: unknown; + argsStr: string; +} + +/** Context passed to tool result hooks. */ +export interface ToolResultContext extends ToolCallContext { + resultData: unknown; + messageId: string; +} + +export type MaybePromise = T | Promise; + +export type ArgsStreamer = (ctx: ToolCallContext) => AsyncIterable; +export type StateFromArgs = ( + ctx: ToolCallContext, +) => MaybePromise; +export type StateFromResult = ( + ctx: ToolResultContext, +) => MaybePromise; +export type CustomResultHandler = ( + ctx: ToolResultContext, +) => AsyncIterable; +export type StateContextBuilder = ( + inputData: RunAgentInput, + prompt: string, + /** Convenience view over `inputData.context[]` + `inputData.forwardedProps`. */ + extras?: ToolCallContextExtras, +) => string; +export type SessionManagerProvider = ( + inputData: RunAgentInput, +) => MaybePromise; + +/** Declarative mapping telling the UI how to predict state from tool args. */ +export interface PredictStateMapping { + stateKey: string; + tool: string; + toolArgument: string; +} + +export function predictStateMappingToPayload(m: PredictStateMapping): { + state_key: string; + tool: string; + tool_argument: string; +} { + return { + state_key: m.stateKey, + tool: m.tool, + tool_argument: m.toolArgument, + }; +} + +/** Declarative configuration for tool-specific handling. */ +export interface ToolBehavior { + /** + * Suppress the `MessagesSnapshotEvent` that would normally follow this + * tool's `TOOL_CALL_END` / `TOOL_CALL_RESULT`. Useful when + * `customResultHandler` emits its own snapshot. + */ + skipMessagesSnapshot?: boolean; + /** Keep the stream alive after emitting a frontend tool call. */ + continueAfterFrontendCall?: boolean; + /** Close text streaming and halt the agent after a tool result arrives. */ + stopStreamingAfterResult?: boolean; + /** `PredictStateMapping[]` that inform the UI how to project tool args into state. */ + predictState?: PredictStateMapping | Iterable; + /** Async generator controlling how tool arguments are streamed to the frontend. */ + argsStreamer?: ArgsStreamer; + /** Derive a `StateSnapshotEvent` from the tool call arguments. */ + stateFromArgs?: StateFromArgs; + /** Derive a `StateSnapshotEvent` from the tool result. */ + stateFromResult?: StateFromResult; + /** Async iterator that can emit arbitrary AG-UI events in response to a result. */ + customResultHandler?: CustomResultHandler; +} + +/** Top-level configuration for the Strands agent adapter. */ +export interface StrandsAgentConfig { + /** Per-tool overrides keyed by the Strands tool name. */ + toolBehaviors?: Record; + /** Callable that enriches the outgoing prompt with the current shared state. */ + stateContextBuilder?: StateContextBuilder; + /** + * Optional factory for creating per-thread `SessionManager` instances. + * + * Called exactly once per `threadId` the first time that thread is seen. + * Subsequent requests on the same thread reuse the cached agent (and its + * SessionManager). If the provider depends on per-request data (e.g. auth + * tokens in `forwardedProps`), only the first request's data is used. + * + * If the provider throws, the run yields `RUN_ERROR` and returns early; + * the thread is NOT cached so the provider will be retried on the next + * request. + * + * If the provider returns `null` or `undefined`, a warning is logged and + * the agent runs without session persistence; the thread IS cached. + */ + sessionManagerProvider?: SessionManagerProvider; + /** + * Emit `MessagesSnapshotEvent` at lifecycle boundaries (after the initial + * `STATE_SNAPSHOT`, after each `TOOL_CALL_END` / `TOOL_CALL_RESULT`, and + * after each terminal `TEXT_MESSAGE_END`). + * + * Required for CopilotKit v2 frontends; set to `false` for raw AG-UI + * consumers that reconstruct messages themselves. Default: `true`. + */ + emitMessagesSnapshot?: boolean; + /** + * When `true` (and the cached Strands agent has no `sessionManager`), + * reconcile the per-thread `Agent.messages` list with + * `RunAgentInput.messages` before invoking `stream()`. + * + * Prevents the LLM from re-firing frontend tools every turn because + * Strands' internal history was missing the tool result the frontend + * produced. Disable only if you manage Strands history yourself. + * Default: `true`. + */ + replayHistoryIntoStrands?: boolean; + /** + * Emit the self-expanding AG-UI chunk events (`TEXT_MESSAGE_CHUNK`, + * `TOOL_CALL_CHUNK`, `REASONING_MESSAGE_CHUNK`) instead of the explicit + * `*_START` / `*_CONTENT` / `*_END` triples. Halves the event count on + * high-frequency deltas; useful for bandwidth-constrained transports. + * TypeScript-only. Default: `false`. + */ + emitChunkEvents?: boolean; + /** + * Optional injectable logger. Mirrors the Python adapter's + * `logging.getLogger("ag_ui_strands")`: the default surfaces `warn` / `error` + * via the `console` and drops `debug`, matching Python's stdlib default + * (WARNING-and-up to stderr). Pass `{ debug: console.debug, warn: + * console.warn, error: console.error }` to enable verbose traces, `{ debug() + * {}, warn() {}, error() {} }` to silence everything, or wire in pino / + * winston / bunyan directly — the `Logger` shape matches the `console` + * methods. + * + * Debug messages match the Python adapter's message strings field-for-field + * (modulo camelCase / snake_case) so cross-SDK log diffs are straightforward. + */ + logger?: Logger; +} + +// Prototype-pollution guard for keys flattened from `context[]`. Plain +// `Object.create(null)` maps have no prototype chain, so `__proto__` becomes +// a regular string key; `constructor` and `prototype` are likewise unfiltered. +const UNSAFE_CONTEXT_KEYS = new Set(["__proto__", "constructor", "prototype"]); + +function isPredictStateMapping(v: unknown): v is PredictStateMapping { + return ( + typeof v === "object" && + v !== null && + "stateKey" in v && + "tool" in v && + "toolArgument" in v + ); +} + +/** + * Flatten `RunAgentInput.context[]` into a plain key/value record and ensure + * `forwardedProps` is a record. Exported so hook implementations can call it + * when they have an `inputData` but not a fully-populated hook context. + */ +export function buildContextExtras( + inputData: RunAgentInput, +): ToolCallContextExtras { + const context = Object.create(null) as Record; + const rawContext = (inputData as { context?: unknown }).context; + if (Array.isArray(rawContext)) { + for (const entry of rawContext) { + if (!entry || typeof entry !== "object") continue; + const e = entry as { description?: unknown; value?: unknown }; + if (typeof e.description !== "string" || e.description.length === 0) + continue; + if (UNSAFE_CONTEXT_KEYS.has(e.description)) continue; + context[e.description] = + typeof e.value === "string" ? e.value : String(e.value ?? ""); + } + } + const rawForwarded = (inputData as { forwardedProps?: unknown }) + .forwardedProps; + const forwardedProps: Record = + rawForwarded && + typeof rawForwarded === "object" && + !Array.isArray(rawForwarded) + ? (rawForwarded as Record) + : {}; + return { context, forwardedProps }; +} + +/** Resolve promise-like values produced by hook callables. */ +export async function maybeAwait(value: MaybePromise): Promise { + return await Promise.resolve(value); +} + +/** Normalize predict-state config into a concrete list. */ +export function normalizePredictState( + value: PredictStateMapping | Iterable | undefined, +): PredictStateMapping[] { + if (value === undefined) return []; + if (isPredictStateMapping(value)) return [value]; + return Array.from(value); +} diff --git a/integrations/aws-strands/typescript/src/endpoint.ts b/integrations/aws-strands/typescript/src/endpoint.ts new file mode 100644 index 0000000000..7653c6415b --- /dev/null +++ b/integrations/aws-strands/typescript/src/endpoint.ts @@ -0,0 +1,425 @@ +/** Express endpoint utilities for AWS Strands integration. */ + +import type { Express, Request, Response } from "express"; +import { + EventType, + RunAgentInputSchema, + type BaseEvent, + type RunAgentInput, +} from "@ag-ui/core"; +import { EventEncoder } from "@ag-ui/encoder"; +import type { StrandsAgent } from "./agent"; + +export interface AddStrandsEndpointOptions { + path: string; +} + +// The wire format is camelCase per the protocol, but the Python reference +// server accepts snake_case aliases (pydantic `populate_by_name=True`). Mirror +// that here so cross-SDK clients that send `thread_id` / `run_id` / etc. keep +// working against the TS adapter. +const SNAKE_TO_CAMEL: Record = { + thread_id: "threadId", + run_id: "runId", + parent_run_id: "parentRunId", + forwarded_props: "forwardedProps", +}; + +function normalizeRunAgentInputKeys(raw: unknown): unknown { + if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return raw; + const src = raw as Record; + const out: Record = {}; + for (const [key, value] of Object.entries(src)) { + const target = SNAKE_TO_CAMEL[key] ?? key; + // Prefer an explicit camelCase value if both were supplied — the camelCase + // form is the canonical wire key. + if (target in out) continue; + out[target] = value; + } + return out; +} + +function isJsonContentType(req: Request): boolean { + // `req.is()` returns false for absent/mismatching Content-Type and tolerates + // subtypes like `application/vnd.custom+json`. + return Boolean(req.is("application/json") || req.is("+json")); +} + +// Binary protobuf framing for AG-UI events. Only selected when the caller +// explicitly mentions this media type in the Accept header — callers that +// send `*/*` or omit Accept get SSE, which is the more forgiving format for +// casual `curl -N` inspection and matches the protocol's default transport. +const PROTOBUF_MEDIA_TYPE = "application/vnd.ag-ui.event+proto"; + +function clientExplicitlyRequestsProtobuf(accept: string | undefined): boolean { + if (!accept) return false; + return accept + .split(",") + .map((piece) => piece.split(";")[0]?.trim().toLowerCase() ?? "") + .some((mt) => mt === PROTOBUF_MEDIA_TYPE); +} + +/** Add a Strands agent endpoint to an Express app. */ +export function addStrandsExpressEndpoint( + app: Express, + agent: StrandsAgent, + options: AddStrandsEndpointOptions, +): void { + app.post(options.path, async (req: Request, res: Response) => { + // Request boundary validation. Express's `express.json()` middleware + // skips bodies whose Content-Type isn't JSON — it leaves `req.body` as + // `{}` instead of rejecting, so silently invalid requests would otherwise + // look indistinguishable from a request with an empty body. Reject them + // here so the protocol contract (events.mdx §RunAgentInput) is enforced + // at the HTTP edge rather than halfway through a streaming response. + if (!isJsonContentType(req)) { + res + .status(415) + .json({ error: "Unsupported Media Type: expected application/json" }); + return; + } + + const normalized = normalizeRunAgentInputKeys(req.body); + const parsed = RunAgentInputSchema.safeParse(normalized); + if (!parsed.success) { + res.status(400).json({ + error: "Invalid RunAgentInput", + issues: parsed.error.issues.map((i) => ({ + path: i.path, + message: i.message, + })), + }); + return; + } + // Preserve the resume[] field if present — the protocol schema validates + // its shape, but passes opaque payloads through unchanged for the adapter + // to inspect (see {@link StrandsAgent._runRaw} interrupt-rule enforcement). + const inputData: RunAgentInput = parsed.data; + + const acceptHeader = req.header("accept") ?? undefined; + // Only hand the encoder the Accept header when the caller explicitly + // opted into protobuf. Otherwise force SSE so `Accept: */*` doesn't + // surprise callers with binary frames — the encoder's media-type + // sort ranks protobuf above SSE for wildcard Accepts. + const encoder = clientExplicitlyRequestsProtobuf(acceptHeader) + ? new EventEncoder({ accept: acceptHeader }) + : new EventEncoder({ accept: "text/event-stream" }); + const contentType = encoder.getContentType(); + + res.setHeader("Content-Type", contentType); + res.setHeader("Cache-Control", "no-cache"); + res.setHeader("Connection", "keep-alive"); + res.flushHeaders?.(); + + const writeEvent = (event: BaseEvent): void => { + // Guard against writes to a socket the client has already dropped. + // `res.write()` on a destroyed socket throws ERR_STREAM_DESTROYED in + // recent Node versions; older versions silently no-op. Short-circuit + // either way so the main loop sees the disconnect on the next + // iteration. + if (res.destroyed || res.writableEnded) return; + if (contentType === "text/event-stream") { + res.write(encoder.encode(event)); + } else { + const bytes = encoder.encodeBinary(event); + res.write(Buffer.from(bytes)); + } + }; + + // Hold an explicit iterator so we can call `.return()` on client + // disconnect. Without this, `res.write()` silently buffers into a + // closed socket and the agent generator's `finally` never runs — + // in particular, THREAD_BUSY slots never release, wedging the thread. + const iterator = agent.run(inputData); + let clientDisconnected = false; + const onDisconnect = (): void => { + if (clientDisconnected) return; + clientDisconnected = true; + // Fire-and-forget — the iterator's own finally will settle the + // active-runs set, session manager, etc. A throwing finally inside + // the generator (e.g. a cleanup hook) must NOT surface as an + // unhandled rejection and crash the Node process, so swallow here. + iterator.return?.().catch(() => { + /* intentional swallow — disconnect path */ + }); + }; + // HTTP/1.1 fires `close` on the Response when the socket closes; + // HTTP/2 reliably fires `aborted` on the Request. Listen to both so + // disconnects under both transports trigger cleanup. + res.once("close", onDisconnect); + req.once("aborted", onDisconnect); + + try { + while (true) { + if (clientDisconnected || res.writableEnded || res.destroyed) break; + let step: IteratorResult; + try { + step = await iterator.next(); + } catch (e) { + // Uncaught error from the generator (should be rare; agent.run() + // normally wraps exceptions as RUN_ERROR itself). + if (!clientDisconnected && !res.writableEnded) { + try { + writeEvent({ + type: EventType.RUN_ERROR, + message: e instanceof Error ? e.message : String(e), + code: "STRANDS_ERROR", + }); + } catch { + // ignore + } + } + break; + } + if (step.done) break; + if (clientDisconnected || res.writableEnded || res.destroyed) break; + try { + writeEvent(step.value); + } catch (e) { + // Encoder failure. Try to deliver a RUN_ERROR, then bail. + const errEvent: BaseEvent = { + type: EventType.RUN_ERROR, + message: `Encoding error: ${String(e)}`, + code: "ENCODING_ERROR", + }; + try { + writeEvent(errEvent); + } catch { + // Swallow — response might already be broken. + } + break; + } + } + } finally { + res.removeListener("close", onDisconnect); + req.removeListener("aborted", onDisconnect); + // Make sure the generator shuts down even if we broke out without + // consuming everything — idempotent when already exhausted. + try { + await iterator.return?.(); + } catch { + // ignore + } + if (!res.writableEnded) res.end(); + } + }); +} + +/** Add a ping endpoint returning `{status: "healthy"}`. */ +export function addPing(app: Express, path: string): void { + app.get(path, (_req, res) => { + res.json({ status: "healthy" }); + }); +} + +/** + * Static description of what this adapter actually supports. Every event + * family here can be observed on the wire; anything missing is either not + * emitted by this adapter (e.g. `ACTIVITY_*`, `RAW`) or only emitted in + * specific configurations (the `*_CHUNK` events, gated by + * `emitChunkEvents` — use {@link capabilitiesFor} to derive the matrix + * from a concrete agent and pick those flags up automatically). + * + * Exported as a plain object so consumers can fold overrides in — for + * example, advertising `events.ACTIVITY_SNAPSHOT: true` after wiring a + * `customResultHandler` that emits those events themselves. + */ +export interface StrandsAguiCapabilities { + /** Semver of the AG-UI contract surface this adapter targets. */ + protocol: string; + /** Content types the HTTP endpoint can stream. */ + transports: { sse: boolean; protobuf: boolean; websocket: boolean }; + /** Event families the adapter emits. Per-event flags, not categories. */ + events: { + RUN_STARTED: boolean; + RUN_FINISHED: boolean; + RUN_ERROR: boolean; + TEXT_MESSAGE_START: boolean; + TEXT_MESSAGE_CONTENT: boolean; + TEXT_MESSAGE_END: boolean; + TEXT_MESSAGE_CHUNK: boolean; + TOOL_CALL_START: boolean; + TOOL_CALL_ARGS: boolean; + TOOL_CALL_END: boolean; + TOOL_CALL_RESULT: boolean; + TOOL_CALL_CHUNK: boolean; + STATE_SNAPSHOT: boolean; + STATE_DELTA: boolean; + MESSAGES_SNAPSHOT: boolean; + STEP_STARTED: boolean; + STEP_FINISHED: boolean; + REASONING_START: boolean; + REASONING_MESSAGE_START: boolean; + REASONING_MESSAGE_CONTENT: boolean; + REASONING_MESSAGE_END: boolean; + REASONING_MESSAGE_CHUNK: boolean; + REASONING_ENCRYPTED_VALUE: boolean; + REASONING_END: boolean; + CUSTOM: boolean; + ACTIVITY_SNAPSHOT: boolean; + ACTIVITY_DELTA: boolean; + RAW: boolean; + }; + /** Protocol feature flags advertised to the client. */ + features: { + /** RunFinished.outcome interrupt + RunAgentInput.resume loop. */ + interrupts: boolean; + /** Tool-call interrupts accept editedArgs in the resume payload. */ + toolCallInterruptEditedArgs: boolean; + /** Resumable streams with sequence numbers. Unsupported. */ + resumableStreams: boolean; + /** Adapter emits MESSAGES_SNAPSHOT at run lifecycle boundaries (Python parity). */ + messagesSnapshot: boolean; + /** State delta via RFC 6902 JSON Patch. Only when a customResultHandler emits them. */ + stateDelta: boolean; + /** Binary protobuf content negotiation (explicit Accept header). */ + protobuf: boolean; + /** Multiple sequential runs in one HTTP stream. One run per POST. */ + multipleRunsPerStream: boolean; + }; +} + +/** Default capabilities advertised by {@link addCapabilities}. */ +export const DEFAULT_CAPABILITIES: StrandsAguiCapabilities = { + protocol: "1", + transports: { sse: true, protobuf: true, websocket: false }, + events: { + RUN_STARTED: true, + RUN_FINISHED: true, + RUN_ERROR: true, + TEXT_MESSAGE_START: true, + TEXT_MESSAGE_CONTENT: true, + TEXT_MESSAGE_END: true, + TEXT_MESSAGE_CHUNK: false, + TOOL_CALL_START: true, + TOOL_CALL_ARGS: true, + TOOL_CALL_END: true, + TOOL_CALL_RESULT: true, + TOOL_CALL_CHUNK: false, + STATE_SNAPSHOT: true, + STATE_DELTA: false, + MESSAGES_SNAPSHOT: true, + STEP_STARTED: true, + STEP_FINISHED: true, + REASONING_START: true, + REASONING_MESSAGE_START: true, + REASONING_MESSAGE_CONTENT: true, + REASONING_MESSAGE_END: true, + REASONING_MESSAGE_CHUNK: false, + REASONING_ENCRYPTED_VALUE: true, + REASONING_END: true, + CUSTOM: true, + ACTIVITY_SNAPSHOT: false, + ACTIVITY_DELTA: false, + RAW: false, + }, + features: { + interrupts: true, + toolCallInterruptEditedArgs: true, + resumableStreams: false, + messagesSnapshot: true, + stateDelta: false, + protobuf: true, + multipleRunsPerStream: false, + }, +}; + +/** One level of sub-field partiality — shallow `Partial<>` on nested objects. */ +export type StrandsAguiCapabilitiesOverrides = { + protocol?: StrandsAguiCapabilities["protocol"]; + transports?: Partial; + events?: Partial; + features?: Partial; +}; + +/** + * Deep-merge consumer overrides on top of the default capabilities. Unknown + * keys in `events` / `features` / `transports` are dropped (typos shouldn't + * silently pollute the advertised matrix). + */ +function mergeCapabilities( + overrides?: StrandsAguiCapabilitiesOverrides, +): StrandsAguiCapabilities { + if (!overrides) return structuredClone(DEFAULT_CAPABILITIES); + const pick = ( + defaults: Record, + override: Partial> | undefined, + ): Record => { + const out = { ...defaults }; + if (!override) return out; + for (const key of Object.keys(override) as K[]) { + if (key in defaults) { + const v = override[key]; + if (typeof v === "boolean") out[key] = v; + } + // Silently drop unknown keys — typos shouldn't leak into the JSON. + } + return out; + }; + return { + protocol: overrides.protocol ?? DEFAULT_CAPABILITIES.protocol, + transports: pick(DEFAULT_CAPABILITIES.transports, overrides.transports), + events: pick(DEFAULT_CAPABILITIES.events, overrides.events), + features: pick(DEFAULT_CAPABILITIES.features, overrides.features), + }; +} + +/** + * Derive capabilities from a concrete StrandsAgent instance, flipping the + * chunk-event flags based on whether the agent is configured to emit chunks. + * When chunks are on, the explicit triples are suppressed, so the advertised + * matrix reflects what the client will actually observe. + */ +export function capabilitiesFor( + agent: { config: { emitChunkEvents?: boolean } }, + overrides?: StrandsAguiCapabilitiesOverrides, +): StrandsAguiCapabilities { + const base = mergeCapabilities(overrides); + if (agent.config.emitChunkEvents) { + base.events.TEXT_MESSAGE_START = false; + base.events.TEXT_MESSAGE_CONTENT = false; + base.events.TEXT_MESSAGE_END = false; + base.events.TEXT_MESSAGE_CHUNK = true; + base.events.TOOL_CALL_START = false; + base.events.TOOL_CALL_ARGS = false; + base.events.TOOL_CALL_END = false; + base.events.TOOL_CALL_CHUNK = true; + base.events.REASONING_MESSAGE_START = false; + base.events.REASONING_MESSAGE_CONTENT = false; + base.events.REASONING_MESSAGE_END = false; + base.events.REASONING_MESSAGE_CHUNK = true; + } + return base; +} + +/** + * Add a capabilities-advertisement endpoint. + * + * Frontends can GET this path to discover which AG-UI event families and + * protocol features the adapter supports, without having to probe empirically. + * + * Two forms: + * - `addCapabilities(app, path, overrides?)` — static matrix (back-compat). + * - `addCapabilities(app, path, { agent })` — derives the matrix from a live + * `StrandsAgent`, picking up `emitChunkEvents` automatically. + */ +export function addCapabilities( + app: Express, + path: string, + capabilities?: + | StrandsAguiCapabilitiesOverrides + | { + agent: { config: { emitChunkEvents?: boolean } }; + overrides?: StrandsAguiCapabilitiesOverrides; + }, +): void { + const resolved = + capabilities && typeof capabilities === "object" && "agent" in capabilities + ? capabilitiesFor(capabilities.agent, capabilities.overrides) + : mergeCapabilities( + capabilities as StrandsAguiCapabilitiesOverrides | undefined, + ); + app.get(path, (_req, res) => { + res.json(resolved); + }); +} diff --git a/integrations/aws-strands/typescript/src/index.ts b/integrations/aws-strands/typescript/src/index.ts index 2aee0b5f69..a3054763c5 100644 --- a/integrations/aws-strands/typescript/src/index.ts +++ b/integrations/aws-strands/typescript/src/index.ts @@ -1,8 +1,49 @@ -/** - * AWS Strands is a framework for building AI agents with AWS services. - * Check more about using AWS Strands: https://github.com/strands-agents/sdk-python - */ +/** AWS Strands integration for AG-UI. */ -import { HttpAgent } from "@ag-ui/client"; +export { + StrandsAgent, + buildSnapshotMessages, + buildStrandsSeed, + convertMessagesForStrandsSeed, +} from "./agent"; +export type { StrandsAgentOptions } from "./agent"; + +export { + createProxyTool, + syncProxyTools, + isProxyTool, +} from "./client-proxy-tool"; +export type { StrandsToolRegistry } from "./client-proxy-tool"; + +export { convertAguiContentToStrands, flattenContentToText } from "./utils"; + +// Server-side Express transport helpers (`createStrandsApp`, +// `addStrandsExpressEndpoint`, `addPing`, `addCapabilities`, +// `capabilitiesFor`, `DEFAULT_CAPABILITIES`, and associated types) live at +// `@ag-ui/aws-strands/server`. Keeping them off the main entry lets +// client-side bundlers (Next.js, Vite, etc.) trace this package without +// pulling Express / cors into the browser graph. +export type { Logger } from "./logger"; + +export { buildContextExtras } from "./config"; +export type { + StrandsAgentConfig, + ToolBehavior, + ToolCallContext, + ToolCallContextExtras, + ToolResultContext, + PredictStateMapping, + SessionManagerProvider, + StateContextBuilder, + StateFromArgs, + StateFromResult, + CustomResultHandler, + ArgsStreamer, + MaybePromise, + StatePayload, +} from "./config"; + +// Thin HttpAgent subclass for AG-UI clients pointing at a Strands endpoint. +import { HttpAgent } from "@ag-ui/client"; export class AWSStrandsAgent extends HttpAgent {} diff --git a/integrations/aws-strands/typescript/src/logger.ts b/integrations/aws-strands/typescript/src/logger.ts new file mode 100644 index 0000000000..6e28eb86a3 --- /dev/null +++ b/integrations/aws-strands/typescript/src/logger.ts @@ -0,0 +1,36 @@ +/** + * Injectable logger for the AWS Strands adapter. + * + * The Python sibling uses `logging.getLogger(__name__)` and emits warnings / + * errors to stderr at `WARNING` and up, with `DEBUG` opt-in. This module + * mirrors that behaviour: by default the adapter is silent below `warn`, + * surfaces warnings via `console.warn`, and lets callers redirect output by + * passing a `Logger` in `StrandsAgentConfig.logger`. + * + * Signature `(message: string, ...args: unknown[])` intentionally matches the + * `console` method shape so existing `vi.spyOn(console, "warn")` test + * scaffolding keeps working with the default logger in place, and so wiring + * in pino / winston / bunyan is a one-liner. + */ +export interface Logger { + debug(message: string, ...args: unknown[]): void; + warn(message: string, ...args: unknown[]): void; + error(message: string, ...args: unknown[]): void; +} + +/** + * Internal fallback used when `StrandsAgentConfig.logger` is omitted. + * Mirrors Python's stdlib default: warnings + errors go to the console, + * debug is dropped. Not exported — callers who want different behaviour + * inject their own logger. + */ +export const DEFAULT_LOGGER: Logger = { + debug() {}, + warn: (msg, ...args) => console.warn(msg, ...args), + error: (msg, ...args) => console.error(msg, ...args), +}; + +/** Return `provided ?? DEFAULT_LOGGER`. */ +export function resolveLogger(provided: Logger | undefined): Logger { + return provided ?? DEFAULT_LOGGER; +} diff --git a/integrations/aws-strands/typescript/src/server.ts b/integrations/aws-strands/typescript/src/server.ts new file mode 100644 index 0000000000..31baaf9c81 --- /dev/null +++ b/integrations/aws-strands/typescript/src/server.ts @@ -0,0 +1,84 @@ +/** + * Server-side entry point for `@ag-ui/aws-strands`. + * + * Import from `@ag-ui/aws-strands/server` when you need the Express transport + * helpers. The main entry point (`@ag-ui/aws-strands`) stays free of Express + * / cors references so Next.js / Turbopack / Vite bundlers tracing the + * client-side graph don't pull server-only modules into the browser build. + */ + +import { + addStrandsExpressEndpoint, + addPing, + addCapabilities, +} from "./endpoint"; +import type { StrandsAgent } from "./agent"; +import type { StrandsAguiCapabilitiesOverrides } from "./endpoint"; + +export { + addStrandsExpressEndpoint, + addPing, + addCapabilities, + capabilitiesFor, + DEFAULT_CAPABILITIES, +} from "./endpoint"; + +export type { + AddStrandsEndpointOptions, + StrandsAguiCapabilities, + StrandsAguiCapabilitiesOverrides, +} from "./endpoint"; + +export interface CreateStrandsAppOptions { + /** Path for the agent endpoint. Default `/`. */ + path?: string; + /** Path for the ping endpoint. Pass `null` or `""` to disable. Default `/ping`. */ + pingPath?: string | null; + /** + * Path for the capabilities endpoint. Pass `null` or `""` to disable. + * Default `/capabilities`. + */ + capabilitiesPath?: string | null; + /** Override capabilities advertised at {@link CreateStrandsAppOptions.capabilitiesPath}. */ + capabilities?: StrandsAguiCapabilitiesOverrides; + /** Override CORS origin. Default `*` (wide-open, matches the Python adapter). */ + corsOrigin?: string | string[] | boolean; +} + +/** Create an Express app with a single Strands agent endpoint and optional ping endpoint. */ +export async function createStrandsApp( + agent: StrandsAgent, + options: CreateStrandsAppOptions = {}, +): Promise { + const { + path = "/", + pingPath = "/ping", + capabilitiesPath = "/capabilities", + capabilities, + corsOrigin = true, + } = options; + + // Lazy dynamic imports so `express` / `cors` are only required at runtime + // when `createStrandsApp` is actually called. + const expressModule = await import("express"); + const corsModule = await import("cors"); + const express = (expressModule.default ?? + expressModule) as typeof import("express"); + const cors = (corsModule.default ?? corsModule) as typeof import("cors"); + + const app = express(); + app.use(cors({ origin: corsOrigin, credentials: true })); + app.use(express.json({ limit: "50mb" })); + + addStrandsExpressEndpoint(app, agent, { path }); + + if (pingPath) { + addPing(app, pingPath); + } + + if (capabilitiesPath) { + addCapabilities(app, capabilitiesPath, { agent, overrides: capabilities }); + } + + return app; +} diff --git a/integrations/aws-strands/typescript/src/types.ts b/integrations/aws-strands/typescript/src/types.ts new file mode 100644 index 0000000000..0d1f273f7e --- /dev/null +++ b/integrations/aws-strands/typescript/src/types.ts @@ -0,0 +1,21 @@ +/** Internal bookkeeping for an in-flight tool call. */ +export interface SeenToolCall { + name: string; + args: string; + input: unknown; + emitted: boolean; + strandsToolId: string; + /** Whether TOOL_CALL_START has already gone on the wire. */ + startEmitted?: boolean; + /** Whether TOOL_CALL_END has already gone on the wire. */ + endEmitted?: boolean; + /** + * High-water mark of the raw args string already emitted as TOOL_CALL_ARGS + * deltas. Each subsequent chunk emits only the growth. + */ + lastEmittedRawLen?: number; + isPending?: boolean; + isFrontend?: boolean; + useStreaming?: boolean; + raw?: string; +} diff --git a/integrations/aws-strands/typescript/src/utils.ts b/integrations/aws-strands/typescript/src/utils.ts new file mode 100644 index 0000000000..a6e7af83d2 --- /dev/null +++ b/integrations/aws-strands/typescript/src/utils.ts @@ -0,0 +1,260 @@ +/** Utility functions for AWS Strands integration. */ + +import type { + InputContent, + TextInputContent, + ImageInputContent, + DocumentInputContent, + VideoInputContent, + InputContentSource, +} from "@ag-ui/core"; +import { + ImageBlock, + DocumentBlock, + VideoBlock, + TextBlock, + type ContentBlock, + type ImageFormat, + type DocumentFormat, + type VideoFormat, +} from "@strands-agents/sdk"; +import { DEFAULT_LOGGER, type Logger } from "./logger"; + +const LOG_PREFIX = "[@ag-ui/aws-strands]"; + +// Allowed formats per media type for Strands ContentBlock +const IMAGE_FORMATS = new Set(["png", "jpeg", "gif", "webp"]); +const DOCUMENT_FORMATS = new Set([ + "pdf", + "csv", + "doc", + "docx", + "xls", + "xlsx", + "html", + "txt", + "md", +]); +const VIDEO_FORMATS = new Set([ + "flv", + "mkv", + "mov", + "mpeg", + "mpg", + "mp4", + "three_gp", + "webm", + "wmv", +]); + +/** Parse a MIME type into a short format string; returns null if absent or unsupported. */ +function mimeToFormat( + mimeType: string | undefined, + allowed: Set, + log: Logger, +): string | null { + if (!mimeType) { + log.warn(`${LOG_PREFIX} No MIME type provided, cannot determine format`); + return null; + } + const fmt = mimeType.split("/").pop()?.toLowerCase() ?? ""; + if (allowed.has(fmt)) { + return fmt; + } + log.warn( + `${LOG_PREFIX} Unsupported MIME type '${mimeType}' (parsed format '${fmt}' not in ${JSON.stringify([...allowed].sort())})`, + ); + return null; +} + +/** Fetch raw bytes from a URL using the global fetch (Node 20+). */ +async function fetchUrlBytes( + url: string, + log: Logger, +): Promise { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 30_000); + try { + const res = await fetch(url, { signal: controller.signal }); + if (!res.ok) { + log.warn(`${LOG_PREFIX} Failed to fetch URL ${url}: HTTP ${res.status}`); + return null; + } + const buf = await res.arrayBuffer(); + return new Uint8Array(buf); + } catch (e) { + log.warn(`${LOG_PREFIX} Failed to fetch URL ${url}:`, e); + return null; + } finally { + clearTimeout(timeout); + } +} + +function decodeBase64(value: string, log: Logger): Uint8Array | null { + try { + const bin = globalThis.atob(value); + const out = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) { + out[i] = bin.charCodeAt(i); + } + return out; + } catch (e) { + log.warn(`${LOG_PREFIX} Failed to decode base64 content:`, e); + return null; + } +} + +/** Resolve bytes from an AG-UI content source. */ +async function resolveSourceBytes( + source: InputContentSource, + log: Logger, +): Promise { + if (source.type === "data") { + return decodeBase64(source.value, log); + } + if (source.type === "url") { + return await fetchUrlBytes(source.value, log); + } + log.warn( + `${LOG_PREFIX} Unknown content source type: ${(source as { type?: string }).type}, cannot resolve bytes`, + ); + return null; +} + +/** + * Convert an AG-UI `InputContent` list to Strands `ContentBlock` values. + * + * Supported types: + * - `TextInputContent` -> `TextBlock` + * - `ImageInputContent` -> `ImageBlock` (png, jpeg, gif, webp) + * - `DocumentInputContent` -> `DocumentBlock` (pdf, csv, doc, docx, xls, xlsx, html, txt, md) + * - `VideoInputContent` -> `VideoBlock` (flv, mkv, mov, mpeg, mpg, mp4, three_gp, webm, wmv) + * - `AudioInputContent` — skipped (Strands has no audio support). + * - Unresolvable items (bad MIME, fetch failure) — skipped. + */ +export async function convertAguiContentToStrands( + content: InputContent[], + log: Logger = DEFAULT_LOGGER, +): Promise { + const blocks: ContentBlock[] = []; + + for (const item of content) { + if (item.type === "text") { + blocks.push(new TextBlock((item as TextInputContent).text)); + continue; + } + + if (item.type === "image") { + const imageItem = item as ImageInputContent; + const bytes = await resolveSourceBytes(imageItem.source, log); + if (!bytes) continue; + const fmt = mimeToFormat(imageItem.source.mimeType, IMAGE_FORMATS, log); + if (!fmt) continue; + blocks.push( + new ImageBlock({ format: fmt as ImageFormat, source: { bytes } }), + ); + continue; + } + + if (item.type === "document") { + const docItem = item as DocumentInputContent; + const bytes = await resolveSourceBytes(docItem.source, log); + if (!bytes) continue; + const fmt = mimeToFormat(docItem.source.mimeType, DOCUMENT_FORMATS, log); + if (!fmt) continue; + blocks.push( + new DocumentBlock({ + format: fmt as DocumentFormat, + name: "document", + source: { bytes }, + }), + ); + continue; + } + + if (item.type === "video") { + const vidItem = item as VideoInputContent; + const bytes = await resolveSourceBytes(vidItem.source, log); + if (!bytes) continue; + const fmt = mimeToFormat(vidItem.source.mimeType, VIDEO_FORMATS, log); + if (!fmt) continue; + blocks.push( + new VideoBlock({ format: fmt as VideoFormat, source: { bytes } }), + ); + continue; + } + + if (item.type === "audio") { + log.warn( + `${LOG_PREFIX} Skipping audio content: Strands has no audio support`, + ); + continue; + } + + if (item.type === "binary") { + // Deprecated legacy binary content — try to map to an image block. + const bin = item as { + type: "binary"; + mimeType: string; + url?: string; + data?: string; + }; + let bytes: Uint8Array | null = null; + if (bin.data) { + bytes = decodeBase64(bin.data, log); + } else if (bin.url) { + bytes = await fetchUrlBytes(bin.url, log); + } + if (!bytes) { + log.warn( + `${LOG_PREFIX} Skipping binary content: could not resolve bytes`, + ); + continue; + } + const fmt = mimeToFormat(bin.mimeType, IMAGE_FORMATS, log); + if (!fmt) { + log.warn( + `${LOG_PREFIX} Skipping binary content: unsupported MIME type '${bin.mimeType}'`, + ); + continue; + } + blocks.push( + new ImageBlock({ format: fmt as ImageFormat, source: { bytes } }), + ); + continue; + } + + log.warn( + `${LOG_PREFIX} Skipping unknown content type: ${(item as { type?: string }).type}`, + ); + } + + return blocks; +} + +/** Extract plain text from AG-UI message content or Strands content blocks. */ +export function flattenContentToText(content: unknown): string { + if (content === null || content === undefined) { + return ""; + } + if (typeof content === "string") { + return content; + } + if (Array.isArray(content)) { + const parts: string[] = []; + for (const item of content) { + if (!item || typeof item !== "object") continue; + const typed = item as { type?: string; text?: string }; + // AG-UI TextInputContent + if (typed.type === "text" && typeof typed.text === "string") { + parts.push(typed.text); + } + // Strands TextBlock + if (typed.type === "textBlock" && typeof typed.text === "string") { + parts.push(typed.text); + } + } + return parts.join(" "); + } + return ""; +} diff --git a/integrations/aws-strands/typescript/tsdown.config.ts b/integrations/aws-strands/typescript/tsdown.config.ts index 6f3030ec48..101950d356 100644 --- a/integrations/aws-strands/typescript/tsdown.config.ts +++ b/integrations/aws-strands/typescript/tsdown.config.ts @@ -1,7 +1,10 @@ import { defineConfig } from "tsdown"; export default defineConfig({ - entry: ["src/index.ts"], + entry: { + index: "src/index.ts", + server: "src/server.ts", + }, format: ["cjs", "esm"], dts: true, exports: true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d54664bf3..65d5a0bc0e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -175,7 +175,7 @@ importers: version: 1.55.1(@ag-ui/core@sdks+typescript+packages+core)(@types/mdast@4.0.4)(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(graphql@16.11.0)(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(zod@3.25.76) '@copilotkit/runtime': specifier: 1.55.1 - version: 1.55.1(3dd72e1b331c7057f722d889cc32f1ce) + version: 1.55.1(c3c32557d1ac98731bd405b9a6dd8f69) '@copilotkit/runtime-client-gql': specifier: 1.55.1 version: 1.55.1(@ag-ui/core@sdks+typescript+packages+core)(graphql@16.11.0)(react@19.2.1) @@ -396,7 +396,7 @@ importers: version: 20.19.21 '@vitest/coverage-istanbul': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4)) publint: specifier: ^0.3.12 version: 0.3.17 @@ -408,7 +408,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) integrations/adk-middleware/typescript: dependencies: @@ -439,7 +439,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) integrations/ag2/typescript: dependencies: @@ -470,7 +470,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) integrations/agno/typescript: dependencies: @@ -492,7 +492,7 @@ importers: version: 20.19.21 '@vitest/coverage-istanbul': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4)) publint: specifier: ^0.3.12 version: 0.3.17 @@ -504,13 +504,9 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) integrations/aws-strands/typescript: - dependencies: - rxjs: - specifier: 7.8.1 - version: 7.8.1 devDependencies: '@ag-ui/client': specifier: workspace:* @@ -518,15 +514,36 @@ importers: '@ag-ui/core': specifier: workspace:* version: link:../../../sdks/typescript/packages/core + '@ag-ui/encoder': + specifier: workspace:* + version: link:../../../sdks/typescript/packages/encoder '@arethetypeswrong/cli': specifier: ^0.17.4 version: 0.17.4 + '@modelcontextprotocol/sdk': + specifier: ^1.29.0 + version: 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3) + '@strands-agents/sdk': + specifier: ^1.1.0 + version: 1.1.0(@ai-sdk/provider@3.0.8)(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@opentelemetry/api@1.9.0)(@opentelemetry/resources@2.6.1(@opentelemetry/api@1.9.0))(express@5.1.0)(openai@6.10.0(ws@8.18.3)(zod@4.4.3))(zod@4.4.3) + '@types/cors': + specifier: ^2.8.17 + version: 2.8.19 + '@types/express': + specifier: ^5.0.0 + version: 5.0.6 '@types/node': specifier: ^20.11.19 version: 20.19.21 '@vitest/coverage-istanbul': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4)) + cors: + specifier: ^2.8.5 + version: 2.8.5 + express: + specifier: ^5.0.0 + version: 5.1.0 publint: specifier: ^0.3.12 version: 0.3.17 @@ -538,7 +555,56 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) + zod: + specifier: ^4.4.3 + version: 4.4.3 + + integrations/aws-strands/typescript/examples: + dependencies: + '@ag-ui/aws-strands': + specifier: workspace:* + version: link:.. + '@ag-ui/core': + specifier: workspace:* + version: link:../../../../sdks/typescript/packages/core + '@ag-ui/encoder': + specifier: workspace:* + version: link:../../../../sdks/typescript/packages/encoder + '@modelcontextprotocol/sdk': + specifier: ^1.29.0 + version: 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3) + '@strands-agents/sdk': + specifier: ^1.1.0 + version: 1.1.0(@ai-sdk/provider@3.0.8)(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@opentelemetry/api@1.9.0)(@opentelemetry/resources@2.6.1(@opentelemetry/api@1.9.0))(express@5.2.1)(openai@6.10.0(ws@8.18.3)(zod@4.4.3))(zod@4.4.3) + cors: + specifier: ^2.8.5 + version: 2.8.5 + express: + specifier: ^5.0.0 + version: 5.2.1 + openai: + specifier: ^6.0.0 + version: 6.10.0(ws@8.18.3)(zod@4.4.3) + zod: + specifier: ^4.4.3 + version: 4.4.3 + devDependencies: + '@types/cors': + specifier: ^2.8.17 + version: 2.8.19 + '@types/express': + specifier: ^5.0.0 + version: 5.0.6 + '@types/node': + specifier: ^20.11.19 + version: 20.19.21 + tsx: + specifier: ^4.20.6 + version: 4.20.6 + typescript: + specifier: ^5.3.3 + version: 5.9.3 integrations/claude-agent-sdk/typescript: dependencies: @@ -566,7 +632,7 @@ importers: version: 20.19.21 tsup: specifier: ^8.0.2 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.4) tsx: specifier: ^4.20.6 version: 4.20.6 @@ -575,7 +641,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) integrations/community/spring-ai/typescript: dependencies: @@ -597,7 +663,7 @@ importers: version: 20.19.21 '@vitest/coverage-istanbul': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4)) publint: specifier: ^0.3.12 version: 0.3.17 @@ -609,7 +675,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) integrations/crew-ai/typescript: dependencies: @@ -631,7 +697,7 @@ importers: version: 20.19.21 '@vitest/coverage-istanbul': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4)) publint: specifier: ^0.3.12 version: 0.3.17 @@ -643,7 +709,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) integrations/langchain/typescript: dependencies: @@ -671,7 +737,7 @@ importers: version: 20.19.21 '@vitest/coverage-istanbul': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4)) publint: specifier: ^0.3.12 version: 0.3.17 @@ -683,19 +749,19 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) integrations/langgraph/typescript: dependencies: '@langchain/core': specifier: ^1.1.40 - version: 1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3) + version: 1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@4.4.3))(ws@8.18.3) '@langchain/langgraph-sdk': specifier: ^1.8.8 - version: 1.8.8(@langchain/core@1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3))(react-dom@19.2.1(react@19.2.3))(react@19.2.3) + version: 1.8.8(@langchain/core@1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@4.4.3))(ws@8.18.3))(react-dom@19.2.1(react@19.2.3))(react@19.2.3) langchain: specifier: '>=1.2.0' - version: 1.2.32(@langchain/core@1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@3.25.76))(react-dom@19.2.1(react@19.2.3))(react@19.2.3)(ws@8.18.3)(zod-to-json-schema@3.25.2(zod@3.25.76)) + version: 1.2.32(@langchain/core@1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@4.4.3))(ws@8.18.3))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@4.4.3))(react-dom@19.2.1(react@19.2.3))(react@19.2.3)(ws@8.18.3)(zod-to-json-schema@3.25.2(zod@4.4.3)) partial-json: specifier: ^0.1.7 version: 0.1.7 @@ -717,7 +783,7 @@ importers: version: 20.19.21 '@vitest/coverage-istanbul': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4)) publint: specifier: ^0.3.12 version: 0.3.17 @@ -729,7 +795,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) integrations/langroid/typescript: dependencies: @@ -757,7 +823,7 @@ importers: version: 29.4.6(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.21)(babel-plugin-macros@3.1.0))(typescript@5.9.3) tsup: specifier: ^8.0.2 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.4) typescript: specifier: ^5.3.3 version: 5.9.3 @@ -782,7 +848,7 @@ importers: version: 20.19.21 '@vitest/coverage-istanbul': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4)) publint: specifier: ^0.3.12 version: 0.3.17 @@ -794,7 +860,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) integrations/mastra/typescript: dependencies: @@ -816,7 +882,7 @@ importers: version: 0.17.4 '@copilotkit/runtime': specifier: 0.0.0-mme-ag-ui-0-0-46-20260227141603 - version: 0.0.0-mme-ag-ui-0-0-46-20260227141603(e7e6345cef6c41890dc996975e0ebc23) + version: 0.0.0-mme-ag-ui-0-0-46-20260227141603(195df52f5f6b19e457dec17fe85475da) '@copilotkit/shared': specifier: 0.0.0-mme-ag-ui-0-0-46-20260227141603 version: 0.0.0-mme-ag-ui-0-0-46-20260227141603(@ag-ui/core@sdks+typescript+packages+core) @@ -831,7 +897,7 @@ importers: version: 20.19.21 '@vitest/coverage-istanbul': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4)) publint: specifier: ^0.3.12 version: 0.3.17 @@ -843,7 +909,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) integrations/mastra/typescript/examples: dependencies: @@ -896,7 +962,7 @@ importers: version: 20.19.21 '@vitest/coverage-istanbul': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4)) publint: specifier: ^0.3.12 version: 0.3.17 @@ -908,7 +974,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) integrations/server-starter-all-features/typescript: dependencies: @@ -927,7 +993,7 @@ importers: version: 20.19.21 '@vitest/coverage-istanbul': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4)) publint: specifier: ^0.3.12 version: 0.3.17 @@ -939,7 +1005,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) integrations/server-starter/typescript: dependencies: @@ -958,7 +1024,7 @@ importers: version: 20.19.21 '@vitest/coverage-istanbul': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4)) publint: specifier: ^0.3.12 version: 0.3.17 @@ -970,7 +1036,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) integrations/vercel-ai-sdk/typescript: dependencies: @@ -998,7 +1064,7 @@ importers: version: 20.19.21 '@vitest/coverage-istanbul': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4)) publint: specifier: ^0.3.12 version: 0.3.17 @@ -1010,7 +1076,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) integrations/watsonx/typescript: devDependencies: @@ -1028,7 +1094,7 @@ importers: version: 20.19.21 '@vitest/coverage-istanbul': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4)) publint: specifier: ^0.3.12 version: 0.3.17 @@ -1043,7 +1109,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) middlewares/a2a-middleware: dependencies: @@ -1071,7 +1137,7 @@ importers: version: 20.19.21 '@vitest/coverage-istanbul': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4)) publint: specifier: ^0.3.12 version: 0.3.17 @@ -1083,7 +1149,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) middlewares/a2ui-middleware: dependencies: @@ -1105,7 +1171,7 @@ importers: version: 7.8.1 tsup: specifier: ^8.0.2 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.4) typescript: specifier: ^5.3.3 version: 5.9.3 @@ -1160,7 +1226,7 @@ importers: version: 20.19.21 '@vitest/coverage-istanbul': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4)) publint: specifier: ^0.3.12 version: 0.3.17 @@ -1175,7 +1241,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) middlewares/middleware-starter: dependencies: @@ -1194,7 +1260,7 @@ importers: version: 20.19.21 '@vitest/coverage-istanbul': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4)) publint: specifier: ^0.3.12 version: 0.3.17 @@ -1206,7 +1272,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) sdks/typescript/packages/cli: dependencies: @@ -1231,7 +1297,7 @@ importers: version: 20.19.21 '@vitest/coverage-istanbul': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4)) publint: specifier: ^0.3.12 version: 0.3.17 @@ -1243,7 +1309,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) sdks/typescript/packages/client: dependencies: @@ -1286,7 +1352,7 @@ importers: version: 20.19.21 '@vitest/coverage-istanbul': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4)) publint: specifier: ^0.3.12 version: 0.3.17 @@ -1298,7 +1364,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) sdks/typescript/packages/core: dependencies: @@ -1311,7 +1377,7 @@ importers: version: 0.17.4 '@vitest/coverage-istanbul': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4)) publint: specifier: ^0.3.12 version: 0.3.17 @@ -1323,7 +1389,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) sdks/typescript/packages/encoder: dependencies: @@ -1339,7 +1405,7 @@ importers: version: 0.17.4 '@vitest/coverage-istanbul': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4)) publint: specifier: ^0.3.12 version: 0.3.17 @@ -1351,7 +1417,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) sdks/typescript/packages/proto: dependencies: @@ -1370,7 +1436,7 @@ importers: version: 0.17.4 '@vitest/coverage-istanbul': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4)) publint: specifier: ^0.3.12 version: 0.3.17 @@ -1385,7 +1451,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) packages: @@ -1696,9 +1762,9 @@ packages: resolution: {integrity: sha512-03xy78mQNBWmLrHT+Tx1Q3oVG4Y/cSQTYzuRPCdpzpgbWXTGJ5yNx8lzdX1Xys7yyGYbw2kX5IzdBiBxk66Vdw==} engines: {node: '>=18.0.0'} - '@aws-sdk/client-bedrock-runtime@3.910.0': - resolution: {integrity: sha512-qWzvNFuv0fZWvc5cpMm2S5CRsn5EKUeqb3OL8PAVk4QPSJmFnX3RMlELxnd4+o1mvpYNs6fxwjEHN0SYPBFdPw==} - engines: {node: '>=18.0.0'} + '@aws-sdk/client-bedrock-runtime@3.1044.0': + resolution: {integrity: sha512-uwJB0pVZIey+kYag8xeUirMvuaDhhEHuYgSsapOKT5XCAije5pLNlg160eECJdhX9JEf0IthIM3IiHKSEkFtMw==} + engines: {node: '>=20.0.0'} '@aws-sdk/client-dynamodb@3.910.0': resolution: {integrity: sha512-taIbikBDq1J3e6Hk1YIe3736l2Ep0blzY5JRuNnXeh1xJxnINaWH3BQW0w+OXmNThV/LRYruOru6+QAd2BekmA==} @@ -1716,41 +1782,77 @@ packages: resolution: {integrity: sha512-b/FVNyPxZMmBp+xDwANDgR6o5Ehh/RTY9U/labH56jJpte196Psru/FmQULX3S6kvIiafQA9JefWUq81SfWVLg==} engines: {node: '>=18.0.0'} + '@aws-sdk/core@3.974.8': + resolution: {integrity: sha512-njR2qoG6ZuB0kvAS2FyICsFZJ6gmCcf2X/7JcD14sUvGDm26wiZ5BrA6LOiUxKFEF+IVe7kdroxyE00YlkiYsw==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-env@3.910.0': resolution: {integrity: sha512-Os8I5XtTLBBVyHJLxrEB06gSAZeFMH2jVoKhAaFybjOTiV7wnjBgjvWjRfStnnXs7p9d+vc/gd6wIZHjony5YQ==} engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-env@3.972.34': + resolution: {integrity: sha512-XT0jtf8Fw9JE6ppsQeoNnZRiG+jqRixMT1v1ZR17G60UvVdsQmTG8nbEyHuEPfMxDXEhfdARaM/XiEhca4lGHQ==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-http@3.910.0': resolution: {integrity: sha512-3KiGsTlqMnvthv90K88Uv3SvaUbmcTShBIVWYNaHdbrhrjVRR08dm2Y6XjQILazLf1NPFkxUou1YwCWK4nae1Q==} engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-http@3.972.36': + resolution: {integrity: sha512-DPoGWfy7J7RKxvbf5kOKIGQkD2ek3dbKgzKIGrnLuvZBz5myU+Im/H6pmc14QcnFbqHMqxvtWSgRDSJW3qXLQg==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-ini@3.910.0': resolution: {integrity: sha512-/8x9LKKaLGarvF1++bFEFdIvd9/djBb+HTULbJAf4JVg3tUlpHtGe7uquuZaQkQGeW4XPbcpB9RMWx5YlZkw3w==} engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-ini@3.972.38': + resolution: {integrity: sha512-oDzUBu2MGJFgoar05sPMCwSrhw44ASyccrHzj66vO69OZqi7I6hZZxXfuPLC8OCzW7C+sU+bI73XHij41yekgQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.38': + resolution: {integrity: sha512-g1NosS8qe4OF++G2UFCM5ovSkgipC7YYor5KCWatG0UoMSO5YFj9C8muePlyVmOBV/WTI16Jo3/s1NUo/o1Bww==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-node@3.910.0': resolution: {integrity: sha512-Zz5tF/U4q9ir3rfVnPLlxbhMTHjPaPv78TarspFYn9mNN7cPVXBaXVVnMNu6ypZzBdTB8M44UYo827Qcw3kouA==} engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-node@3.972.39': + resolution: {integrity: sha512-HEswDQyxUtadoZ/bJsPPENHg7R0Lzym5LuMksJeHvqhCOpP+rtkDLKI4/ZChH4w3cf5kG8n6bZuI8PzajoiqMg==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-process@3.910.0': resolution: {integrity: sha512-l1lZfHIl/z0SxXibt7wMQ2HmRIyIZjlOrT6a554xlO//y671uxPPwScVw7QW4fPIvwfmKbl8dYCwGI//AgQ0bA==} engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-process@3.972.34': + resolution: {integrity: sha512-T3IFs4EVmVi1dVN5RciFnklCANSzvrQd/VuHY9ThHSQmYkTogjcGkoJEr+oNUPQZnso52183088NqysMPji1/Q==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-sso@3.910.0': resolution: {integrity: sha512-cwc9bmomjUqPDF58THUCmEnpAIsCFV3Y9FHlQmQbMkYUm7Wlrb5E2iFrZ4WDefAHuh25R/gtj+Yo74r3gl9kbw==} engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-sso@3.972.38': + resolution: {integrity: sha512-5ZxG+t0+3Q3QPh8KEjX6syskhgNf7I0MN7oGioTf6Lm1NTjfP7sIcYGNsthXC2qR8vcD3edNZwCr2ovfSSWuRA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-web-identity@3.910.0': resolution: {integrity: sha512-HFQgZm1+7WisJ8tqcZkNRRmnoFO+So+L12wViVxneVJ+OclfL2vE/CoKqHTozP6+JCOKMlv6Vi61Lu6xDtKdTA==} engines: {node: '>=18.0.0'} + '@aws-sdk/credential-provider-web-identity@3.972.38': + resolution: {integrity: sha512-lYHFF30DGI20jZcYX8cm6Ns0V7f1dDN6g/MBDLTyD/5iw+bXs3yBr2iAiHDkx4RFU5JgsnZvCHYKiRVPRdmOgw==} + engines: {node: '>=20.0.0'} + '@aws-sdk/endpoint-cache@3.893.0': resolution: {integrity: sha512-KSwTfyLZyNLszz5f/yoLC+LC+CRKpeJii/+zVAy7JUOQsKhSykiRUPYUx7o2Sdc4oJfqqUl26A/jSttKYnYtAA==} engines: {node: '>=18.0.0'} - '@aws-sdk/eventstream-handler-node@3.910.0': - resolution: {integrity: sha512-oh91l4hR0makDcdK2uPoIETI8QKrDxgEDdo5VZNPddnr7XBNPenm8bWLvSQI2sEtn0uaQw5q9eT75I5HaiWB5g==} - engines: {node: '>=18.0.0'} + '@aws-sdk/eventstream-handler-node@3.972.14': + resolution: {integrity: sha512-m4X56gxG76/CKfxNVbOFuYwnAZcHgS6HOH8lgp15HoGHIAVTcZfZrXvcYzJFOMLEJgVn+JHBu6EiNV+xSNXXFg==} + engines: {node: '>=20.0.0'} '@aws-sdk/lib-dynamodb@3.910.0': resolution: {integrity: sha512-ltGlB9p57RTHuyKBci5siNb92iq43kWd4EwwIIgPyADjgjcRIZcSbyAhB9eBdb2aX0IqsDpPUyBz7iAsOzOffQ==} @@ -1762,38 +1864,78 @@ packages: resolution: {integrity: sha512-KZvTt8lUUhkQptu00iSSdf5+6h6NP3L5tP251/4FRh9XDXMdpIoAAGsmamhVySkUSODDaALMHjXPSK5SJq/RYw==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-eventstream@3.910.0': - resolution: {integrity: sha512-zeV4DVypzV+77AQ7sqVfKacVWFBM2HVBVORZ4PnCjToCg1BQgw39IDVtklF1/Fs+mmGp4dJdTlJ7TKBCqBNdhw==} - engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-eventstream@3.972.10': + resolution: {integrity: sha512-QUqLs7Af1II9X4fCRAu+EGHG3KHyOp4RkuLhRKoA3NuFlh6TL8i+zXBl8w2LUxqm44B/Kom45hgSlwA1SpTsXQ==} + engines: {node: '>=20.0.0'} '@aws-sdk/middleware-host-header@3.910.0': resolution: {integrity: sha512-F9Lqeu80/aTM6S/izZ8RtwSmjfhWjIuxX61LX+/9mxJyEkgaECRxv0chsLQsLHJumkGnXRy/eIyMLBhcTPF5vg==} engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-host-header@3.972.10': + resolution: {integrity: sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg==} + engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-logger@3.910.0': resolution: {integrity: sha512-3LJyyfs1USvRuRDla1pGlzGRtXJBXD1zC9F+eE9Iz/V5nkmhyv52A017CvKWmYoR0DM9dzjLyPOI0BSSppEaTw==} engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-logger@3.972.10': + resolution: {integrity: sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==} + engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-recursion-detection@3.910.0': resolution: {integrity: sha512-m/oLz0EoCy+WoIVBnXRXJ4AtGpdl0kPE7U+VH9TsuUzHgxY1Re/176Q1HWLBRVlz4gr++lNsgsMWEC+VnAwMpw==} engines: {node: '>=18.0.0'} + '@aws-sdk/middleware-recursion-detection@3.972.11': + resolution: {integrity: sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.972.37': + resolution: {integrity: sha512-Km7M+i8DrLArVzrid1gfxeGhYHBd3uxvE77g0s5a52zPSVosxzQBnJ0gwWb6NIp/DOk8gsBMhi7V+cpJG0ndTA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-user-agent@3.910.0': resolution: {integrity: sha512-djpnECwDLI/4sck1wxK/cZJmZX5pAhRvjONyJqr0AaOfJyuIAG0PHLe7xwCrv2rCAvIBR9ofnNFzPIGTJPDUwg==} engines: {node: '>=18.0.0'} - '@aws-sdk/middleware-websocket@3.910.0': - resolution: {integrity: sha512-W0t8nHo6SY2g5+ZAofsnzxr3K8E1hRT2qq1BlYcNwX76m2Kw0wP+kaMhKlAdtY7rglu7HZhwErZHxQfenO9UZg==} + '@aws-sdk/middleware-user-agent@3.972.38': + resolution: {integrity: sha512-iz+B29TXcAZsJpwB+AwG/TTGA5l/VnmMZ2UxtiySOZjI6gCdmviXPwdgzcmuazMy16rXoPY4mYCGe7zdNKfx5A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-websocket@3.972.16': + resolution: {integrity: sha512-86+S9oCyRVGzoMRpQhxkArp7kD2K75GPmaNevd9B6EyNhWoNvnCZZ3WbgN4j7ZT+jvtvBCGZvI2XHsWZJ+BRIg==} engines: {node: '>= 14.0.0'} '@aws-sdk/nested-clients@3.910.0': resolution: {integrity: sha512-Jr/smgVrLZECQgMyP4nbGqgJwzFFbkjOVrU8wh/gbVIZy1+Gu6R7Shai7KHDkEjwkGcHpN1MCCO67jTAOoSlMw==} engines: {node: '>=18.0.0'} + '@aws-sdk/nested-clients@3.997.6': + resolution: {integrity: sha512-WBDnqatJl+kGObpfmfSxqnXeYTu3Me8wx8WCtvoxX3pfWrrTv8I4WTMSSs7PZqcRcVh8WeUKMgGFjMG+52SR1w==} + engines: {node: '>=20.0.0'} + '@aws-sdk/region-config-resolver@3.910.0': resolution: {integrity: sha512-gzQAkuHI3xyG6toYnH/pju+kc190XmvnB7X84vtN57GjgdQJICt9So/BD0U6h+eSfk9VBnafkVrAzBzWMEFZVw==} engines: {node: '>=18.0.0'} + '@aws-sdk/region-config-resolver@3.972.13': + resolution: {integrity: sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.25': + resolution: {integrity: sha512-+CMIt3e1VzlklAECmG+DtP1sV8iKq25FuA0OKpnJ4KA0kxUtd7CgClY7/RU6VzJBQwbN4EJ9Ue6plvqx1qGadw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1041.0': + resolution: {integrity: sha512-Th7kPI6YPtvJUcdznooXJMy+9rQWjmEF81LxaJssngBzuysK4a/x+l8kjm1zb7nYsUPbndnBdUnwng/3PLvtGw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1044.0': + resolution: {integrity: sha512-uvpWTfpzOM9qVrR0kdAjzBbvv2ERW7S1CKeBeRkHyHy21Ext5fNReIVrbAzhjuVC7UxRyZQ8PHYW/3T83hWlbg==} + engines: {node: '>=20.0.0'} + '@aws-sdk/token-providers@3.910.0': resolution: {integrity: sha512-dQr3pFpzemKyrB7SEJ2ipPtWrZiL5vaimg2PkXpwyzGrigYRc8F2R9DMUckU5zi32ozvQqq4PI3bOrw6xUfcbQ==} engines: {node: '>=18.0.0'} @@ -1802,6 +1944,14 @@ packages: resolution: {integrity: sha512-o67gL3vjf4nhfmuSUNNkit0d62QJEwwHLxucwVJkR/rw9mfUtAWsgBs8Tp16cdUbMgsyQtCQilL8RAJDoGtadQ==} engines: {node: '>=18.0.0'} + '@aws-sdk/types@3.973.8': + resolution: {integrity: sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-arn-parser@3.972.3': + resolution: {integrity: sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/util-dynamodb@3.910.0': resolution: {integrity: sha512-DI9mg8bcmvxPMDUcPgNZEgWqaeaeOTgvZKnHP8Rmdneaw5h7Cdnm+aall0jcxu7aIMnMytRmgIcu0QAWm8x5bw==} engines: {node: '>=18.0.0'} @@ -1812,9 +1962,13 @@ packages: resolution: {integrity: sha512-6XgdNe42ibP8zCQgNGDWoOF53RfEKzpU/S7Z29FTTJ7hcZv0SytC0ZNQQZSx4rfBl036YWYwJRoJMlT4AA7q9A==} engines: {node: '>=18.0.0'} - '@aws-sdk/util-format-url@3.910.0': - resolution: {integrity: sha512-cYfgDGxZnrAq7wvntBjW6/ZewRcwywOE1Q9KKPO05ZHXpWCrqKNkx0JG8h2xlu+2qX6lkLZS+NyFAlwCQa0qfA==} - engines: {node: '>=18.0.0'} + '@aws-sdk/util-endpoints@3.996.8': + resolution: {integrity: sha512-oOZHcRDihk5iEe5V25NVWg45b3qEA8OpHWVdU/XQh8Zj4heVPAJqWvMphQnU7LkufmUo10EpvFPZuQMiFLJK3g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-format-url@3.972.10': + resolution: {integrity: sha512-DEKiHNJVtNxdyTeQspzY+15Po/kHm6sF0Cs4HV9Q2+lplB63+DrvdeiSoOSdWEWAoO2RcY1veoXVDz2tWxWCgQ==} + engines: {node: '>=20.0.0'} '@aws-sdk/util-locate-window@3.893.0': resolution: {integrity: sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==} @@ -1823,6 +1977,9 @@ packages: '@aws-sdk/util-user-agent-browser@3.910.0': resolution: {integrity: sha512-iOdrRdLZHrlINk9pezNZ82P/VxO/UmtmpaOAObUN+xplCUJu31WNM2EE/HccC8PQw6XlAudpdA6HDTGiW6yVGg==} + '@aws-sdk/util-user-agent-browser@3.972.10': + resolution: {integrity: sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g==} + '@aws-sdk/util-user-agent-node@3.910.0': resolution: {integrity: sha512-qNV+rywWQDOOWmGpNlWLCU6zkJurocTBB2uLSdQ8b6Xg6U/i1VTJsoUQ5fbhSQpp/SuBGiIglyB1gSc0th7hPw==} engines: {node: '>=18.0.0'} @@ -1832,14 +1989,31 @@ packages: aws-crt: optional: true + '@aws-sdk/util-user-agent-node@3.973.24': + resolution: {integrity: sha512-ZWwlkjcIp7cEL8ZfTpTAPNkwx25p7xol0xlKoWVVf22+nsjwmLcHYtTPjIV1cSpmB/b6DaK4cb1fSkvCXHgRdw==} + engines: {node: '>=20.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + '@aws-sdk/xml-builder@3.910.0': resolution: {integrity: sha512-UK0NzRknzUITYlkDibDSgkWvhhC11OLhhhGajl6pYCACup+6QE4SsLvmAGMkyNtGVCJ6Q+BM6PwDCBZyBgwl9A==} engines: {node: '>=18.0.0'} + '@aws-sdk/xml-builder@3.972.22': + resolution: {integrity: sha512-PMYKKtJd70IsSG0yHrdAbxBr+ZWBKLvzFZfD3/urxgf6hXVMzuU5M+3MJ5G67RpOmLBu1fAUN65SbWuKUCOlAA==} + engines: {node: '>=20.0.0'} + '@aws/lambda-invoke-store@0.0.1': resolution: {integrity: sha512-ORHRQ2tmvnBXc8t/X9Z8IcSbBA4xTLKuN873FopzklHMeqBst7YG0d+AX97inkvDX+NChYtSr+qGfcqGFaI8Zw==} engines: {node: '>=18.0.0'} + '@aws/lambda-invoke-store@0.2.4': + resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -3302,6 +3476,12 @@ packages: react: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc + '@hono/node-server@1.19.14': + resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@hono/node-server@1.19.7': resolution: {integrity: sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==} engines: {node: '>=18.14.1'} @@ -3995,6 +4175,16 @@ packages: resolution: {integrity: sha512-kOQ4+fHuT4KbR2iq2IjeV32HiihueuOf1vJkq18z08CLZ1UQrTc8BXJpVfxZkq45+inLLD+D4xx4nBjUelJa4Q==} engines: {node: '>=18'} + '@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 + '@monaco-editor/loader@1.6.1': resolution: {integrity: sha512-w3tEnj9HYEC73wtjdpR089AqkUPskFRcdkxsiSFt3SoUc3OHpmu+leP94CXBm4mHfefmhsdfI0ZQu6qJ0wgtPg==} @@ -4082,6 +4272,9 @@ packages: cpu: [x64] os: [win32] + '@nodable/entities@2.1.0': + resolution: {integrity: sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -5161,42 +5354,66 @@ packages: resolution: {integrity: sha512-F/G+VaulIebINyfvcoXmODgIc7JU/lxWK9/iI0Divxyvd2QWB7/ZcF7JKwMssWI6/zZzlMkq/Pt6ow2AOEebPw==} engines: {node: '>=18.0.0'} + '@smithy/config-resolver@4.4.17': + resolution: {integrity: sha512-TzDZcAnhTyAHbXVxWZo7/tEcrIeFq20IBk8So3OLOetWpR8EwY/yEqBMBFaJMeyEiREDq4NfEl+qO3OAUD+vbQ==} + engines: {node: '>=18.0.0'} + '@smithy/core@3.16.1': resolution: {integrity: sha512-yRx5ag3xEQ/yGvyo80FVukS7ZkeUP49Vbzg0MjfHLkuCIgg5lFtaEJfZR178KJmjWPqLU4d0P4k7SKgF9UkOaQ==} engines: {node: '>=18.0.0'} + '@smithy/core@3.23.17': + resolution: {integrity: sha512-x7BlLbUFL8NWCGjMF9C+1N5cVCxcPa7g6Tv9B4A2luWx3be3oU8hQ96wIwxe/s7OhIzvoJH73HAUSg5JXVlEtQ==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.2.14': + resolution: {integrity: sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==} + engines: {node: '>=18.0.0'} + '@smithy/credential-provider-imds@4.2.2': resolution: {integrity: sha512-hOjFTK+4mfehDnfjNkPqHUKBKR2qmlix5gy7YzruNbTdeoBE3QkfNCPvuCK2r05VUJ02QQ9bz2G41CxhSexsMw==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-codec@4.2.2': - resolution: {integrity: sha512-TDJFBixL6p/CZ6VyTfU+9YrPtcriAouv2IECk5jM4Y3zRJYXyei8lvsCSMMgYW9mLMbtp3mvJbeI8SLOF2BunA==} + '@smithy/eventstream-codec@4.2.14': + resolution: {integrity: sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-browser@4.2.14': + resolution: {integrity: sha512-8IelTCtTctWRbb+0Dcy+C0aICh1qa0qWXqgjcXDmMuCvPJRnv26hiDZoAau2ILOniki65mCPKqOQs/BaWvO4CQ==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-browser@4.2.2': - resolution: {integrity: sha512-WDNt+DpzqlXibmCW/b4290dNPMPLL0KrrsXDUQsMycj1NhR60s90pgmRSqaVzNMI5jhdyYVVNMmSh6bgQ9/eiQ==} + '@smithy/eventstream-serde-config-resolver@4.3.14': + resolution: {integrity: sha512-sqHiHpYRYo3FJlaIxD1J8PhbcmJAm7IuM16mVnwSkCToD7g00IBZzKuiLNMGmftULmEUX6/UAz8/NN5uMP8bVA==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-config-resolver@4.3.2': - resolution: {integrity: sha512-vwc532Ji2FFaoXa+IaWXbO+OrG39t+atwlsLDwh2ayt5Ryn2Bd7gAnEZw6bHT/slreSn+4MKmO2fuRzA1wf1uA==} + '@smithy/eventstream-serde-node@4.2.14': + resolution: {integrity: sha512-Ht/8BuGlKfFTy0H3+8eEu0vdpwGztCnaLLXtpXNdQqiR7Hj4vFScU3T436vRAjATglOIPjJXronY+1WxxNLSiw==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-node@4.2.2': - resolution: {integrity: sha512-JJ+PhJ3jf+Xshx6fmz10evfu4k0Xk/uv+i43JnsvIonyugiY8fU4CNhTKScYOU6lL9mAEKxvEhy5DCnElKvkZw==} + '@smithy/eventstream-serde-universal@4.2.14': + resolution: {integrity: sha512-lWyt4T2XQZUZgK3tQ3Wn0w3XBvZsK/vjTuJl6bXbnGZBHH0ZUSONTYiK9TgjTTzU54xQr3DRFwpjmhp0oLm3gg==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-universal@4.2.2': - resolution: {integrity: sha512-QrHhyQV0s2D1RaXPLIPCIy/dAQD3bBSW8nw5IkOmgOHAPDs54nwe6UXR2nsl25fW92BTGVQeOOcHad6rJ2m81A==} + '@smithy/fetch-http-handler@5.3.17': + resolution: {integrity: sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==} engines: {node: '>=18.0.0'} '@smithy/fetch-http-handler@5.3.3': resolution: {integrity: sha512-cipIcM3xQ5NdIVwcRb37LaQwIxZNMEZb/ZOPmLFS9uGo9TGx2dGCyMBj9oT7ypH4TUD/kOTc/qHmwQzthrSk+g==} engines: {node: '>=18.0.0'} + '@smithy/hash-node@4.2.14': + resolution: {integrity: sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g==} + engines: {node: '>=18.0.0'} + '@smithy/hash-node@4.2.2': resolution: {integrity: sha512-xuOPGrF2GUP+9og5NU02fplRVjJjMhAaY8ZconB3eLKjv/VSV9/s+sFf72MYO5Q2jcSRVk/ywZHpyGbE3FYnFQ==} engines: {node: '>=18.0.0'} + '@smithy/invalid-dependency@4.2.14': + resolution: {integrity: sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw==} + engines: {node: '>=18.0.0'} + '@smithy/invalid-dependency@4.2.2': resolution: {integrity: sha512-Z0844Zpoid5L1DmKX2+cn2Qu9i3XWjhzwYBRJEWrKJwjUuhEkzf37jKPj9dYFsZeKsAbS2qI0JyLsYafbXJvpA==} engines: {node: '>=18.0.0'} @@ -5209,6 +5426,14 @@ packages: resolution: {integrity: sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==} engines: {node: '>=18.0.0'} + '@smithy/is-array-buffer@4.2.2': + resolution: {integrity: sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-content-length@4.2.14': + resolution: {integrity: sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw==} + engines: {node: '>=18.0.0'} + '@smithy/middleware-content-length@4.2.2': resolution: {integrity: sha512-aJ7LAuIXStF6EqzRVX9kAW+6/sYoJJv0QqoFrz2BhA9r/85kLYOJ6Ph47wYSGBxzSLxsYT5jqgMw/qpbv1+m+w==} engines: {node: '>=18.0.0'} @@ -5217,18 +5442,38 @@ packages: resolution: {integrity: sha512-CfxQ6X9L87/3C67Po6AGWXsx8iS4w2BO8vQEZJD6hwqg2vNRC/lMa2O5wXYCG9tKotdZ0R8KG33TS7kpUnYKiw==} engines: {node: '>=18.0.0'} + '@smithy/middleware-endpoint@4.4.32': + resolution: {integrity: sha512-ZZkgyjnJppiZbIm6Qbx92pbXYi1uzenIvGhBSCDlc7NwuAkiqSgS75j1czAD25ZLs2FjMjYy1q7gyRVWG6JA0Q==} + engines: {node: '>=18.0.0'} + '@smithy/middleware-retry@4.4.3': resolution: {integrity: sha512-EHnKGeFuzbmER4oSl/VJDxPLi+aiZUb3nk5KK8eNwHjMhI04jHlui2ZkaBzMfNmXOgymaS6zV//fyt6PSnI1ow==} engines: {node: '>=18.0.0'} + '@smithy/middleware-retry@4.5.7': + resolution: {integrity: sha512-bRt6ZImqVSeTk39Nm81K20ObIiAZ3WefY7G6+iz/0tZjs4dgRRjvRX2sgsH+zi6iDCRR/aQvQofLKxxz4rPBZg==} + engines: {node: '>=18.0.0'} + '@smithy/middleware-serde@4.2.2': resolution: {integrity: sha512-tDMPMBCsA1GBxanShhPvQYwdiau3NmctUp+eELMhUTDua+EUrugXlaKCnTMMoEB5mbHFebdv81uJPkVP02oihA==} engines: {node: '>=18.0.0'} + '@smithy/middleware-serde@4.2.20': + resolution: {integrity: sha512-Lx9JMO9vArPtiChE3wbEZ5akMIDQpWQtlu90lhACQmNOXcGXRbaDywMHDzuDZ2OkZzP+9wQfZi3YJT9F67zTQQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-stack@4.2.14': + resolution: {integrity: sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA==} + engines: {node: '>=18.0.0'} + '@smithy/middleware-stack@4.2.2': resolution: {integrity: sha512-7rgzDyLOQouh1bC6gOXnCGSX2dqvbOclgClsFkj735xQM2CHV63Ams8odNZGJgcqnBsEz44V/pDGHU6ALEUD+w==} engines: {node: '>=18.0.0'} + '@smithy/node-config-provider@4.3.14': + resolution: {integrity: sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg==} + engines: {node: '>=18.0.0'} + '@smithy/node-config-provider@4.3.2': resolution: {integrity: sha512-u38G0Audi2ORsL0QnzhopZ3yweMblQf8CZNbzUJ3wfTtZ7OiOwOzee0Nge/3dKeG/8lx0kt8K0kqDi6sYu0oKQ==} engines: {node: '>=18.0.0'} @@ -5237,18 +5482,38 @@ packages: resolution: {integrity: sha512-9gKJoL45MNyOCGTG082nmx0A6KrbLVQ+5QSSKyzRi0AzL0R81u3wC1+nPvKXgTaBdAKM73fFPdCBHpmtipQwdQ==} engines: {node: '>=18.0.0'} + '@smithy/node-http-handler@4.6.1': + resolution: {integrity: sha512-iB+orM4x3xrr57X3YaXazfKnntl0LHlZB1kcXSGzMV1Tt0+YwEjGlbjk/44qEGtBzXAz6yFDzkYTKSV6Pj2HUg==} + engines: {node: '>=18.0.0'} + + '@smithy/property-provider@4.2.14': + resolution: {integrity: sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ==} + engines: {node: '>=18.0.0'} + '@smithy/property-provider@4.2.2': resolution: {integrity: sha512-MW7MfI+qYe/Ue5RH0uEztEKB+vBlOMM+1Dz68qzTsY8fC9kanXMFPEVdiq35JTGKWt5wZAjU1R0uXYEjK2MM1g==} engines: {node: '>=18.0.0'} + '@smithy/protocol-http@5.3.14': + resolution: {integrity: sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ==} + engines: {node: '>=18.0.0'} + '@smithy/protocol-http@5.3.2': resolution: {integrity: sha512-nkKOI8xEkBXUmdxsFExomOb+wkU+Xgn0Fq2LMC7YIX5r4YPUg7PLayV/s/u3AtbyjWYlrvN7nAiDTLlqSdUjHw==} engines: {node: '>=18.0.0'} + '@smithy/querystring-builder@4.2.14': + resolution: {integrity: sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A==} + engines: {node: '>=18.0.0'} + '@smithy/querystring-builder@4.2.2': resolution: {integrity: sha512-YgXvq89o+R/8zIoeuXYv8Ysrbwgjx+iVYu9QbseqZjMDAhIg/FRt7jis0KASYFtd/Cnsnz4/nYTJXkJDWe8wHg==} engines: {node: '>=18.0.0'} + '@smithy/querystring-parser@4.2.14': + resolution: {integrity: sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw==} + engines: {node: '>=18.0.0'} + '@smithy/querystring-parser@4.2.2': resolution: {integrity: sha512-DczOD2yJy3NXcv1JvhjFC7bIb/tay6nnIRD/qrzBaju5lrkVBOwCT3Ps37tra20wy8PicZpworStK7ZcI9pCRQ==} engines: {node: '>=18.0.0'} @@ -5257,22 +5522,46 @@ packages: resolution: {integrity: sha512-1X17cMLwe/vb4RpZbQVpJ1xQQ7fhQKggMdt3qjdV3+6QNllzvUXyS3WFnyaFWLyaGqfYHKkNONbO1fBCMQyZtQ==} engines: {node: '>=18.0.0'} + '@smithy/service-error-classification@4.3.1': + resolution: {integrity: sha512-aUQuDGh760ts/8MU+APjIZhlLPKhIIfqyzZaJikLEIMrdxFvxuLYD0WxWzaYWpmLbQlXDe9p7EWM3HsBe0K6Gw==} + engines: {node: '>=18.0.0'} + '@smithy/shared-ini-file-loader@4.3.2': resolution: {integrity: sha512-AWnLgSmOTdDXM8aZCN4Im0X07M3GGffeL9vGfea4mdKZD0cPT9yLF9SsRbEa00tHLI+KfubDrmjpaKT2pM4GdQ==} engines: {node: '>=18.0.0'} + '@smithy/shared-ini-file-loader@4.4.9': + resolution: {integrity: sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.3.14': + resolution: {integrity: sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==} + engines: {node: '>=18.0.0'} + '@smithy/signature-v4@5.3.2': resolution: {integrity: sha512-BRnQGGyaRSSL0KtjjFF9YoSSg8qzSqHMub4H2iKkd+LZNzZ1b7H5amslZBzi+AnvuwPMyeiNv0oqay/VmIuoRA==} engines: {node: '>=18.0.0'} + '@smithy/smithy-client@4.12.13': + resolution: {integrity: sha512-y/Pcj1V9+qG98gyu1gvftHB7rDpdh+7kIBIggs55yGm3JdtBV8GT8IFF3a1qxZ79QnaJHX9GXzvBG6tAd+czJA==} + engines: {node: '>=18.0.0'} + '@smithy/smithy-client@4.8.1': resolution: {integrity: sha512-N5wK57pVThzLVK5NgmHxocTy5auqGDGQ+JsL5RjCTriPt8JLYgXT0Awa915zCpzc9hXHDOKqDX5g9BFdwkSfUA==} engines: {node: '>=18.0.0'} + '@smithy/types@4.14.1': + resolution: {integrity: sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==} + engines: {node: '>=18.0.0'} + '@smithy/types@4.7.1': resolution: {integrity: sha512-WwP7vzoDyzvIFLzF5UhLQ6AsEx/PvSObzlNtJNW3lLy+BaSvTqCU628QKVvcJI/dydlAS1mSHQP7anKcxDcOxA==} engines: {node: '>=18.0.0'} + '@smithy/url-parser@4.2.14': + resolution: {integrity: sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ==} + engines: {node: '>=18.0.0'} + '@smithy/url-parser@4.2.2': resolution: {integrity: sha512-s2EYKukaswzjiHJCss6asB1F4zjRc0E/MFyceAKzb3+wqKA2Z/+Gfhb5FP8xVVRHBAvBkregaQAydifgbnUlCw==} engines: {node: '>=18.0.0'} @@ -5281,14 +5570,26 @@ packages: resolution: {integrity: sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==} engines: {node: '>=18.0.0'} + '@smithy/util-base64@4.3.2': + resolution: {integrity: sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==} + engines: {node: '>=18.0.0'} + '@smithy/util-body-length-browser@4.2.0': resolution: {integrity: sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==} engines: {node: '>=18.0.0'} + '@smithy/util-body-length-browser@4.2.2': + resolution: {integrity: sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==} + engines: {node: '>=18.0.0'} + '@smithy/util-body-length-node@4.2.1': resolution: {integrity: sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==} engines: {node: '>=18.0.0'} + '@smithy/util-body-length-node@4.2.3': + resolution: {integrity: sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==} + 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'} @@ -5297,26 +5598,54 @@ packages: resolution: {integrity: sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==} engines: {node: '>=18.0.0'} + '@smithy/util-buffer-from@4.2.2': + resolution: {integrity: sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==} + engines: {node: '>=18.0.0'} + '@smithy/util-config-provider@4.2.0': resolution: {integrity: sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==} engines: {node: '>=18.0.0'} + '@smithy/util-config-provider@4.2.2': + resolution: {integrity: sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==} + engines: {node: '>=18.0.0'} + '@smithy/util-defaults-mode-browser@4.3.2': resolution: {integrity: sha512-6JvKHZ5GORYkEZ2+yJKEHp6dQQKng+P/Mu3g3CDy0fRLQgXEO8be+FLrBGGb4kB9lCW6wcQDkN7kRiGkkVAXgg==} engines: {node: '>=18.0.0'} + '@smithy/util-defaults-mode-browser@4.3.49': + resolution: {integrity: sha512-a5bNrdiONYB/qE2BuKegvUMd/+ZDwdg4vsNuuSzYE8qs2EYAdK9CynL+Rzn29PbPiUqoz/cbpRbcLzD5lEevHw==} + engines: {node: '>=18.0.0'} + '@smithy/util-defaults-mode-node@4.2.3': resolution: {integrity: sha512-bkTGuMmKvghfCh9NayADrQcjngoF8P+XTgID5r3rm+8LphFiuM6ERqpBS95YyVaLjDetnKus9zK/bGlkQOOtNQ==} engines: {node: '>=18.0.0'} + '@smithy/util-defaults-mode-node@4.2.54': + resolution: {integrity: sha512-g1cvrJvOnzeJgEdf7AE4luI7gp6L8weE0y9a9wQUSGtjb8QRHDbCJYuE4Sy0SD9N8RrnNPFsPltAz/OSoBR9Zw==} + engines: {node: '>=18.0.0'} + '@smithy/util-endpoints@3.2.2': resolution: {integrity: sha512-ZQi6fFTMBkfwwSPAlcGzArmNILz33QH99CL8jDfVWrzwVVcZc56Mge10jGk0zdRgWPXyL1/OXKjfw4vT5VtRQg==} engines: {node: '>=18.0.0'} + '@smithy/util-endpoints@3.4.2': + resolution: {integrity: sha512-a55Tr+3OKld4TTtnT+RhKOQHyPxm3j/xL4OR83WBUhLJaKDS9dnJ7arRMOp3t31dcLhApwG9bgvrRXBHlLdIkg==} + engines: {node: '>=18.0.0'} + '@smithy/util-hex-encoding@4.2.0': resolution: {integrity: sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==} engines: {node: '>=18.0.0'} + '@smithy/util-hex-encoding@4.2.2': + resolution: {integrity: sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-middleware@4.2.14': + resolution: {integrity: sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw==} + engines: {node: '>=18.0.0'} + '@smithy/util-middleware@4.2.2': resolution: {integrity: sha512-wL9tZwWKy0x0qf6ffN7tX5CT03hb1e7XpjdepaKfKcPcyn5+jHAWPqivhF1Sw/T5DYi9wGcxsX8Lu07MOp2Puw==} engines: {node: '>=18.0.0'} @@ -5325,14 +5654,26 @@ packages: resolution: {integrity: sha512-TlbnWAOoCuG2PgY0Hi3BGU1w2IXs3xDsD4E8WDfKRZUn2qx3wRA9mbYnmpWHPswTJCz2L+ebh+9OvD42sV4mNw==} engines: {node: '>=18.0.0'} + '@smithy/util-retry@4.3.8': + resolution: {integrity: sha512-LUIxbTBi+OpvXpg91poGA6BdyoleMDLnfXjVDqyi2RvZmTveY5loE/FgYUBCR5LU2BThW2SoZRh8dTIIy38IPw==} + engines: {node: '>=18.0.0'} + '@smithy/util-stream@4.5.2': resolution: {integrity: sha512-RWYVuQVKtNbr7E0IxV8XHDId714yHPTxU6dHScd6wSMWAXboErzTG7+xqcL+K3r0Xg0cZSlfuNhl1J0rzMLSSw==} engines: {node: '>=18.0.0'} + '@smithy/util-stream@4.5.25': + resolution: {integrity: sha512-/PFpG4k8Ze8Ei+mMKj3oiPICYekthuzePZMgZbCqMiXIHHf4n2aZ4Ps0aSRShycFTGuj/J6XldmC0x0DwednIA==} + engines: {node: '>=18.0.0'} + '@smithy/util-uri-escape@4.2.0': resolution: {integrity: sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==} engines: {node: '>=18.0.0'} + '@smithy/util-uri-escape@4.2.2': + resolution: {integrity: sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==} + engines: {node: '>=18.0.0'} + '@smithy/util-utf8@2.3.0': resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} engines: {node: '>=14.0.0'} @@ -5341,6 +5682,10 @@ packages: resolution: {integrity: sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==} engines: {node: '>=18.0.0'} + '@smithy/util-utf8@4.2.2': + resolution: {integrity: sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==} + engines: {node: '>=18.0.0'} + '@smithy/util-waiter@4.2.2': resolution: {integrity: sha512-ZkanmAo9F47PIxuxaQ1E+VPn/jNIbOM7cpJyABfyI15jnr4l5toSDVXPRuvHIyC2f4fMYC7EKe5DIde7YP7c7A==} engines: {node: '>=18.0.0'} @@ -5349,6 +5694,10 @@ packages: resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==} engines: {node: '>=18.0.0'} + '@smithy/uuid@1.1.2': + resolution: {integrity: sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==} + engines: {node: '>=18.0.0'} + '@standard-community/standard-json@0.3.5': resolution: {integrity: sha512-4+ZPorwDRt47i+O7RjyuaxHRK/37QY/LmgxlGrRrSTLYoFatEOzvqIc85GTlM18SFZ5E91C+v0o/M37wZPpUHA==} peerDependencies: @@ -5413,6 +5762,54 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@strands-agents/sdk@1.1.0': + resolution: {integrity: sha512-oYB7nzuGe9rmq7K7yP+fgC/BG/55z4z58L+xIP4Nsr9SJvKSFPcsZAq4R8oOhTJars+UNgIy9KMD+WE/2u+jeQ==} + 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-s3': ^3.943.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 + 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-s3': + 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 + express: + optional: true + openai: + optional: true + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -5811,9 +6208,15 @@ packages: '@types/express-serve-static-core@4.19.7': resolution: {integrity: sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==} + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + '@types/express@4.17.23': resolution: {integrity: sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==} + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} @@ -5927,6 +6330,9 @@ packages: '@types/serve-static@1.15.9': resolution: {integrity: sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==} + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -6021,6 +6427,7 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} @@ -6289,12 +6696,23 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} ajv@8.12.0: resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} @@ -6554,6 +6972,10 @@ packages: resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} engines: {node: '>=18'} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + bowser@2.12.1: resolution: {integrity: sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==} @@ -7735,6 +8157,12 @@ packages: peerDependencies: express: '>= 4.11' + express-rate-limit@8.5.1: + resolution: {integrity: sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + express@4.21.2: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} @@ -7743,6 +8171,10 @@ packages: resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} engines: {node: '>= 18'} + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + exsolve@1.0.7: resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} @@ -7778,10 +8210,20 @@ packages: fast-text-encoding@1.0.6: resolution: {integrity: sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==} + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + + fast-xml-builder@1.1.9: + resolution: {integrity: sha512-jcyKVSEX13iseJqg7n/KWw+xnu/7fdrZ333Fac54KjHDIELVCfDDJXYIm6DTJ0Su4gSzrhqiK0DzY/wZbF40mw==} + fast-xml-parser@5.2.5: resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==} hasBin: true + fast-xml-parser@5.7.2: + resolution: {integrity: sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==} + hasBin: true + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -8340,6 +8782,10 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + 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'} @@ -8741,6 +9187,9 @@ packages: jose@5.10.0: resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -8786,6 +9235,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-schema@0.4.0: resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} @@ -9916,6 +10368,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-expression-matcher@1.5.0: + resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} + engines: {node: '>=14.0.0'} + path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -10240,6 +10696,10 @@ packages: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} + qs@6.15.1: + resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} + engines: {node: '>=0.6'} + quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -10913,6 +11373,9 @@ packages: strnum@2.1.1: resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==} + strnum@2.2.3: + resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} + style-to-js@1.1.18: resolution: {integrity: sha512-JFPn62D4kJaPTnhFUI244MThx+FEGbi+9dw1b9yBBQ+1CZpV7QAT8kUtJ7b7EUNdHajjF/0x8fT+16oLJoojLg==} @@ -11454,6 +11917,7 @@ packages: uuid@10.0.0: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uuid@11.1.0: @@ -11464,8 +11928,13 @@ packages: resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} hasBin: true + uuid@14.0.0: + resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} + hasBin: true + uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uvu@0.5.6: @@ -11798,6 +12267,11 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yaml@2.8.4: + resolution: {integrity: sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -11858,6 +12332,9 @@ packages: zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -11988,13 +12465,15 @@ snapshots: - ws - zod-to-json-schema - '@ag-ui/mcp-apps-middleware@0.0.3(@ag-ui/client@0.0.52)': + '@ag-ui/mcp-apps-middleware@0.0.3(@ag-ui/client@0.0.52)(@cfworker/json-schema@4.1.1)(zod@3.25.76)': dependencies: '@ag-ui/client': 0.0.52 - '@modelcontextprotocol/sdk': 1.20.0 + '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76) rxjs: 7.8.1 transitivePeerDependencies: + - '@cfworker/json-schema' - supports-color + - zod '@ag-ui/proto@0.0.46': dependencies: @@ -12295,9 +12774,8 @@ snapshots: '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.910.0 + '@aws-sdk/types': 3.973.8 tslib: 2.8.1 - optional: true '@aws-crypto/sha256-browser@5.2.0': dependencies: @@ -12321,7 +12799,7 @@ snapshots: '@aws-crypto/util@5.2.0': dependencies: - '@aws-sdk/types': 3.910.0 + '@aws-sdk/types': 3.973.8 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 @@ -12340,100 +12818,98 @@ snapshots: '@aws-sdk/util-endpoints': 3.910.0 '@aws-sdk/util-user-agent-browser': 3.910.0 '@aws-sdk/util-user-agent-node': 3.910.0 - '@smithy/config-resolver': 4.3.2 - '@smithy/core': 3.16.1 - '@smithy/eventstream-serde-browser': 4.2.2 - '@smithy/eventstream-serde-config-resolver': 4.3.2 - '@smithy/eventstream-serde-node': 4.2.2 - '@smithy/fetch-http-handler': 5.3.3 - '@smithy/hash-node': 4.2.2 - '@smithy/invalid-dependency': 4.2.2 - '@smithy/middleware-content-length': 4.2.2 - '@smithy/middleware-endpoint': 4.3.3 - '@smithy/middleware-retry': 4.4.3 - '@smithy/middleware-serde': 4.2.2 - '@smithy/middleware-stack': 4.2.2 - '@smithy/node-config-provider': 4.3.2 - '@smithy/node-http-handler': 4.4.1 - '@smithy/protocol-http': 5.3.2 - '@smithy/smithy-client': 4.8.1 - '@smithy/types': 4.7.1 - '@smithy/url-parser': 4.2.2 - '@smithy/util-base64': 4.3.0 - '@smithy/util-body-length-browser': 4.2.0 - '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.2 - '@smithy/util-defaults-mode-node': 4.2.3 - '@smithy/util-endpoints': 3.2.2 - '@smithy/util-middleware': 4.2.2 - '@smithy/util-retry': 4.2.2 - '@smithy/util-utf8': 4.2.0 + '@smithy/config-resolver': 4.4.17 + '@smithy/core': 3.23.17 + '@smithy/eventstream-serde-browser': 4.2.14 + '@smithy/eventstream-serde-config-resolver': 4.3.14 + '@smithy/eventstream-serde-node': 4.2.14 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/hash-node': 4.2.14 + '@smithy/invalid-dependency': 4.2.14 + '@smithy/middleware-content-length': 4.2.14 + '@smithy/middleware-endpoint': 4.4.32 + '@smithy/middleware-retry': 4.5.7 + '@smithy/middleware-serde': 4.2.20 + '@smithy/middleware-stack': 4.2.14 + '@smithy/node-config-provider': 4.3.14 + '@smithy/node-http-handler': 4.6.1 + '@smithy/protocol-http': 5.3.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.49 + '@smithy/util-defaults-mode-node': 4.2.54 + '@smithy/util-endpoints': 3.4.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.8 + '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 transitivePeerDependencies: - aws-crt optional: true - '@aws-sdk/client-bedrock-runtime@3.910.0': + '@aws-sdk/client-bedrock-runtime@3.1044.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.8 + '@aws-sdk/credential-provider-node': 3.972.39 + '@aws-sdk/eventstream-handler-node': 3.972.14 + '@aws-sdk/middleware-eventstream': 3.972.10 + '@aws-sdk/middleware-host-header': 3.972.10 + '@aws-sdk/middleware-logger': 3.972.10 + '@aws-sdk/middleware-recursion-detection': 3.972.11 + '@aws-sdk/middleware-user-agent': 3.972.38 + '@aws-sdk/middleware-websocket': 3.972.16 + '@aws-sdk/region-config-resolver': 3.972.13 + '@aws-sdk/token-providers': 3.1044.0 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.8 + '@aws-sdk/util-user-agent-browser': 3.972.10 + '@aws-sdk/util-user-agent-node': 3.973.24 + '@smithy/config-resolver': 4.4.17 + '@smithy/core': 3.23.17 + '@smithy/eventstream-serde-browser': 4.2.14 + '@smithy/eventstream-serde-config-resolver': 4.3.14 + '@smithy/eventstream-serde-node': 4.2.14 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/hash-node': 4.2.14 + '@smithy/invalid-dependency': 4.2.14 + '@smithy/middleware-content-length': 4.2.14 + '@smithy/middleware-endpoint': 4.4.32 + '@smithy/middleware-retry': 4.5.7 + '@smithy/middleware-serde': 4.2.20 + '@smithy/middleware-stack': 4.2.14 + '@smithy/node-config-provider': 4.3.14 + '@smithy/node-http-handler': 4.6.1 + '@smithy/protocol-http': 5.3.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.49 + '@smithy/util-defaults-mode-node': 4.2.54 + '@smithy/util-endpoints': 3.4.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.8 + '@smithy/util-stream': 4.5.25 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-dynamodb@3.910.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 '@aws-sdk/core': 3.910.0 '@aws-sdk/credential-provider-node': 3.910.0 - '@aws-sdk/eventstream-handler-node': 3.910.0 - '@aws-sdk/middleware-eventstream': 3.910.0 - '@aws-sdk/middleware-host-header': 3.910.0 - '@aws-sdk/middleware-logger': 3.910.0 - '@aws-sdk/middleware-recursion-detection': 3.910.0 - '@aws-sdk/middleware-user-agent': 3.910.0 - '@aws-sdk/middleware-websocket': 3.910.0 - '@aws-sdk/region-config-resolver': 3.910.0 - '@aws-sdk/token-providers': 3.910.0 - '@aws-sdk/types': 3.910.0 - '@aws-sdk/util-endpoints': 3.910.0 - '@aws-sdk/util-user-agent-browser': 3.910.0 - '@aws-sdk/util-user-agent-node': 3.910.0 - '@smithy/config-resolver': 4.3.2 - '@smithy/core': 3.16.1 - '@smithy/eventstream-serde-browser': 4.2.2 - '@smithy/eventstream-serde-config-resolver': 4.3.2 - '@smithy/eventstream-serde-node': 4.2.2 - '@smithy/fetch-http-handler': 5.3.3 - '@smithy/hash-node': 4.2.2 - '@smithy/invalid-dependency': 4.2.2 - '@smithy/middleware-content-length': 4.2.2 - '@smithy/middleware-endpoint': 4.3.3 - '@smithy/middleware-retry': 4.4.3 - '@smithy/middleware-serde': 4.2.2 - '@smithy/middleware-stack': 4.2.2 - '@smithy/node-config-provider': 4.3.2 - '@smithy/node-http-handler': 4.4.1 - '@smithy/protocol-http': 5.3.2 - '@smithy/smithy-client': 4.8.1 - '@smithy/types': 4.7.1 - '@smithy/url-parser': 4.2.2 - '@smithy/util-base64': 4.3.0 - '@smithy/util-body-length-browser': 4.2.0 - '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.2 - '@smithy/util-defaults-mode-node': 4.2.3 - '@smithy/util-endpoints': 3.2.2 - '@smithy/util-middleware': 4.2.2 - '@smithy/util-retry': 4.2.2 - '@smithy/util-stream': 4.5.2 - '@smithy/util-utf8': 4.2.0 - '@smithy/uuid': 1.1.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - optional: true - - '@aws-sdk/client-dynamodb@3.910.0': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.910.0 - '@aws-sdk/credential-provider-node': 3.910.0 - '@aws-sdk/middleware-endpoint-discovery': 3.910.0 + '@aws-sdk/middleware-endpoint-discovery': 3.910.0 '@aws-sdk/middleware-host-header': 3.910.0 '@aws-sdk/middleware-logger': 3.910.0 '@aws-sdk/middleware-recursion-detection': 3.910.0 @@ -12489,32 +12965,32 @@ snapshots: '@aws-sdk/util-endpoints': 3.910.0 '@aws-sdk/util-user-agent-browser': 3.910.0 '@aws-sdk/util-user-agent-node': 3.910.0 - '@smithy/config-resolver': 4.3.2 - '@smithy/core': 3.16.1 - '@smithy/fetch-http-handler': 5.3.3 - '@smithy/hash-node': 4.2.2 - '@smithy/invalid-dependency': 4.2.2 - '@smithy/middleware-content-length': 4.2.2 - '@smithy/middleware-endpoint': 4.3.3 - '@smithy/middleware-retry': 4.4.3 - '@smithy/middleware-serde': 4.2.2 - '@smithy/middleware-stack': 4.2.2 - '@smithy/node-config-provider': 4.3.2 - '@smithy/node-http-handler': 4.4.1 - '@smithy/protocol-http': 5.3.2 - '@smithy/smithy-client': 4.8.1 - '@smithy/types': 4.7.1 - '@smithy/url-parser': 4.2.2 - '@smithy/util-base64': 4.3.0 - '@smithy/util-body-length-browser': 4.2.0 - '@smithy/util-body-length-node': 4.2.1 - '@smithy/util-defaults-mode-browser': 4.3.2 - '@smithy/util-defaults-mode-node': 4.2.3 - '@smithy/util-endpoints': 3.2.2 - '@smithy/util-middleware': 4.2.2 - '@smithy/util-retry': 4.2.2 - '@smithy/util-utf8': 4.2.0 - '@smithy/uuid': 1.1.0 + '@smithy/config-resolver': 4.4.17 + '@smithy/core': 3.23.17 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/hash-node': 4.2.14 + '@smithy/invalid-dependency': 4.2.14 + '@smithy/middleware-content-length': 4.2.14 + '@smithy/middleware-endpoint': 4.4.32 + '@smithy/middleware-retry': 4.5.7 + '@smithy/middleware-serde': 4.2.20 + '@smithy/middleware-stack': 4.2.14 + '@smithy/node-config-provider': 4.3.14 + '@smithy/node-http-handler': 4.6.1 + '@smithy/protocol-http': 5.3.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.49 + '@smithy/util-defaults-mode-node': 4.2.54 + '@smithy/util-endpoints': 3.4.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.8 + '@smithy/util-utf8': 4.2.2 + '@smithy/uuid': 1.1.2 tslib: 2.8.1 transitivePeerDependencies: - aws-crt @@ -12579,6 +13055,23 @@ snapshots: '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 + '@aws-sdk/core@3.974.8': + dependencies: + '@aws-sdk/types': 3.973.8 + '@aws-sdk/xml-builder': 3.972.22 + '@smithy/core': 3.23.17 + '@smithy/node-config-provider': 4.3.14 + '@smithy/property-provider': 4.2.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/signature-v4': 5.3.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/util-base64': 4.3.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.8 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + '@aws-sdk/credential-provider-env@3.910.0': dependencies: '@aws-sdk/core': 3.910.0 @@ -12587,6 +13080,14 @@ snapshots: '@smithy/types': 4.7.1 tslib: 2.8.1 + '@aws-sdk/credential-provider-env@3.972.34': + dependencies: + '@aws-sdk/core': 3.974.8 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@aws-sdk/credential-provider-http@3.910.0': dependencies: '@aws-sdk/core': 3.910.0 @@ -12600,6 +13101,19 @@ snapshots: '@smithy/util-stream': 4.5.2 tslib: 2.8.1 + '@aws-sdk/credential-provider-http@3.972.36': + dependencies: + '@aws-sdk/core': 3.974.8 + '@aws-sdk/types': 3.973.8 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/node-http-handler': 4.6.1 + '@smithy/property-provider': 4.2.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/util-stream': 4.5.25 + tslib: 2.8.1 + '@aws-sdk/credential-provider-ini@3.910.0': dependencies: '@aws-sdk/core': 3.910.0 @@ -12618,6 +13132,38 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/credential-provider-ini@3.972.38': + dependencies: + '@aws-sdk/core': 3.974.8 + '@aws-sdk/credential-provider-env': 3.972.34 + '@aws-sdk/credential-provider-http': 3.972.36 + '@aws-sdk/credential-provider-login': 3.972.38 + '@aws-sdk/credential-provider-process': 3.972.34 + '@aws-sdk/credential-provider-sso': 3.972.38 + '@aws-sdk/credential-provider-web-identity': 3.972.38 + '@aws-sdk/nested-clients': 3.997.6 + '@aws-sdk/types': 3.973.8 + '@smithy/credential-provider-imds': 4.2.14 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-login@3.972.38': + dependencies: + '@aws-sdk/core': 3.974.8 + '@aws-sdk/nested-clients': 3.997.6 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/credential-provider-node@3.910.0': dependencies: '@aws-sdk/credential-provider-env': 3.910.0 @@ -12635,6 +13181,23 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/credential-provider-node@3.972.39': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.34 + '@aws-sdk/credential-provider-http': 3.972.36 + '@aws-sdk/credential-provider-ini': 3.972.38 + '@aws-sdk/credential-provider-process': 3.972.34 + '@aws-sdk/credential-provider-sso': 3.972.38 + '@aws-sdk/credential-provider-web-identity': 3.972.38 + '@aws-sdk/types': 3.973.8 + '@smithy/credential-provider-imds': 4.2.14 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/credential-provider-process@3.910.0': dependencies: '@aws-sdk/core': 3.910.0 @@ -12644,6 +13207,15 @@ snapshots: '@smithy/types': 4.7.1 tslib: 2.8.1 + '@aws-sdk/credential-provider-process@3.972.34': + dependencies: + '@aws-sdk/core': 3.974.8 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@aws-sdk/credential-provider-sso@3.910.0': dependencies: '@aws-sdk/client-sso': 3.910.0 @@ -12657,6 +13229,19 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/credential-provider-sso@3.972.38': + dependencies: + '@aws-sdk/core': 3.974.8 + '@aws-sdk/nested-clients': 3.997.6 + '@aws-sdk/token-providers': 3.1041.0 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/credential-provider-web-identity@3.910.0': dependencies: '@aws-sdk/core': 3.910.0 @@ -12669,18 +13254,29 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/credential-provider-web-identity@3.972.38': + dependencies: + '@aws-sdk/core': 3.974.8 + '@aws-sdk/nested-clients': 3.997.6 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/endpoint-cache@3.893.0': dependencies: mnemonist: 0.38.3 tslib: 2.8.1 - '@aws-sdk/eventstream-handler-node@3.910.0': + '@aws-sdk/eventstream-handler-node@3.972.14': dependencies: - '@aws-sdk/types': 3.910.0 - '@smithy/eventstream-codec': 4.2.2 - '@smithy/types': 4.7.1 + '@aws-sdk/types': 3.973.8 + '@smithy/eventstream-codec': 4.2.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - optional: true '@aws-sdk/lib-dynamodb@3.910.0(@aws-sdk/client-dynamodb@3.910.0)': dependencies: @@ -12701,13 +13297,12 @@ snapshots: '@smithy/types': 4.7.1 tslib: 2.8.1 - '@aws-sdk/middleware-eventstream@3.910.0': + '@aws-sdk/middleware-eventstream@3.972.10': dependencies: - '@aws-sdk/types': 3.910.0 - '@smithy/protocol-http': 5.3.2 - '@smithy/types': 4.7.1 + '@aws-sdk/types': 3.973.8 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - optional: true '@aws-sdk/middleware-host-header@3.910.0': dependencies: @@ -12716,12 +13311,25 @@ snapshots: '@smithy/types': 4.7.1 tslib: 2.8.1 + '@aws-sdk/middleware-host-header@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@aws-sdk/middleware-logger@3.910.0': dependencies: '@aws-sdk/types': 3.910.0 '@smithy/types': 4.7.1 tslib: 2.8.1 + '@aws-sdk/middleware-logger@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@aws-sdk/middleware-recursion-detection@3.910.0': dependencies: '@aws-sdk/types': 3.910.0 @@ -12730,6 +13338,31 @@ snapshots: '@smithy/types': 4.7.1 tslib: 2.8.1 + '@aws-sdk/middleware-recursion-detection@3.972.11': + dependencies: + '@aws-sdk/types': 3.973.8 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.37': + dependencies: + '@aws-sdk/core': 3.974.8 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-arn-parser': 3.972.3 + '@smithy/core': 3.23.17 + '@smithy/node-config-provider': 4.3.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/signature-v4': 5.3.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/util-config-provider': 4.2.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-stream': 4.5.25 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + '@aws-sdk/middleware-user-agent@3.910.0': dependencies: '@aws-sdk/core': 3.910.0 @@ -12740,19 +13373,31 @@ snapshots: '@smithy/types': 4.7.1 tslib: 2.8.1 - '@aws-sdk/middleware-websocket@3.910.0': + '@aws-sdk/middleware-user-agent@3.972.38': dependencies: - '@aws-sdk/types': 3.910.0 - '@aws-sdk/util-format-url': 3.910.0 - '@smithy/eventstream-codec': 4.2.2 - '@smithy/eventstream-serde-browser': 4.2.2 - '@smithy/fetch-http-handler': 5.3.3 - '@smithy/protocol-http': 5.3.2 - '@smithy/signature-v4': 5.3.2 - '@smithy/types': 4.7.1 - '@smithy/util-hex-encoding': 4.2.0 + '@aws-sdk/core': 3.974.8 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.8 + '@smithy/core': 3.23.17 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + '@smithy/util-retry': 4.3.8 + tslib: 2.8.1 + + '@aws-sdk/middleware-websocket@3.972.16': + dependencies: + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-format-url': 3.972.10 + '@smithy/eventstream-codec': 4.2.14 + '@smithy/eventstream-serde-browser': 4.2.14 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/protocol-http': 5.3.14 + '@smithy/signature-v4': 5.3.14 + '@smithy/types': 4.14.1 + '@smithy/util-base64': 4.3.2 + '@smithy/util-hex-encoding': 4.2.2 + '@smithy/util-utf8': 4.2.2 tslib: 2.8.1 - optional: true '@aws-sdk/nested-clients@3.910.0': dependencies: @@ -12797,6 +13442,50 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/nested-clients@3.997.6': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.8 + '@aws-sdk/middleware-host-header': 3.972.10 + '@aws-sdk/middleware-logger': 3.972.10 + '@aws-sdk/middleware-recursion-detection': 3.972.11 + '@aws-sdk/middleware-user-agent': 3.972.38 + '@aws-sdk/region-config-resolver': 3.972.13 + '@aws-sdk/signature-v4-multi-region': 3.996.25 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.8 + '@aws-sdk/util-user-agent-browser': 3.972.10 + '@aws-sdk/util-user-agent-node': 3.973.24 + '@smithy/config-resolver': 4.4.17 + '@smithy/core': 3.23.17 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/hash-node': 4.2.14 + '@smithy/invalid-dependency': 4.2.14 + '@smithy/middleware-content-length': 4.2.14 + '@smithy/middleware-endpoint': 4.4.32 + '@smithy/middleware-retry': 4.5.7 + '@smithy/middleware-serde': 4.2.20 + '@smithy/middleware-stack': 4.2.14 + '@smithy/node-config-provider': 4.3.14 + '@smithy/node-http-handler': 4.6.1 + '@smithy/protocol-http': 5.3.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.49 + '@smithy/util-defaults-mode-node': 4.2.54 + '@smithy/util-endpoints': 3.4.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.8 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/region-config-resolver@3.910.0': dependencies: '@aws-sdk/types': 3.910.0 @@ -12806,6 +13495,47 @@ snapshots: '@smithy/util-middleware': 4.2.2 tslib: 2.8.1 + '@aws-sdk/region-config-resolver@3.972.13': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/config-resolver': 4.4.17 + '@smithy/node-config-provider': 4.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.25': + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.972.37 + '@aws-sdk/types': 3.973.8 + '@smithy/protocol-http': 5.3.14 + '@smithy/signature-v4': 5.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1041.0': + dependencies: + '@aws-sdk/core': 3.974.8 + '@aws-sdk/nested-clients': 3.997.6 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/token-providers@3.1044.0': + dependencies: + '@aws-sdk/core': 3.974.8 + '@aws-sdk/nested-clients': 3.997.6 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + '@aws-sdk/token-providers@3.910.0': dependencies: '@aws-sdk/core': 3.910.0 @@ -12823,6 +13553,15 @@ snapshots: '@smithy/types': 4.7.1 tslib: 2.8.1 + '@aws-sdk/types@3.973.8': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/util-arn-parser@3.972.3': + dependencies: + tslib: 2.8.1 + '@aws-sdk/util-dynamodb@3.910.0(@aws-sdk/client-dynamodb@3.910.0)': dependencies: '@aws-sdk/client-dynamodb': 3.910.0 @@ -12836,13 +13575,20 @@ snapshots: '@smithy/util-endpoints': 3.2.2 tslib: 2.8.1 - '@aws-sdk/util-format-url@3.910.0': + '@aws-sdk/util-endpoints@3.996.8': dependencies: - '@aws-sdk/types': 3.910.0 - '@smithy/querystring-builder': 4.2.2 - '@smithy/types': 4.7.1 + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-endpoints': 3.4.2 + tslib: 2.8.1 + + '@aws-sdk/util-format-url@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/querystring-builder': 4.2.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - optional: true '@aws-sdk/util-locate-window@3.893.0': dependencies: @@ -12855,6 +13601,13 @@ snapshots: bowser: 2.12.1 tslib: 2.8.1 + '@aws-sdk/util-user-agent-browser@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 + bowser: 2.12.1 + tslib: 2.8.1 + '@aws-sdk/util-user-agent-node@3.910.0': dependencies: '@aws-sdk/middleware-user-agent': 3.910.0 @@ -12863,14 +13616,32 @@ snapshots: '@smithy/types': 4.7.1 tslib: 2.8.1 + '@aws-sdk/util-user-agent-node@3.973.24': + dependencies: + '@aws-sdk/middleware-user-agent': 3.972.38 + '@aws-sdk/types': 3.973.8 + '@smithy/node-config-provider': 4.3.14 + '@smithy/types': 4.14.1 + '@smithy/util-config-provider': 4.2.2 + tslib: 2.8.1 + '@aws-sdk/xml-builder@3.910.0': dependencies: '@smithy/types': 4.7.1 fast-xml-parser: 5.2.5 tslib: 2.8.1 + '@aws-sdk/xml-builder@3.972.22': + dependencies: + '@nodable/entities': 2.1.0 + '@smithy/types': 4.14.1 + fast-xml-parser: 5.7.2 + tslib: 2.8.1 + '@aws/lambda-invoke-store@0.0.1': {} + '@aws/lambda-invoke-store@0.2.4': {} + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -13837,7 +14608,7 @@ snapshots: '@copilotkit/shared': 1.55.1(@ag-ui/core@0.0.52) phoenix: 1.8.5 rxjs: 7.8.1 - zod-to-json-schema: 3.24.6(zod@3.25.76) + zod-to-json-schema: 3.25.2(zod@3.25.76) transitivePeerDependencies: - '@ag-ui/core' - encoding @@ -13935,7 +14706,7 @@ snapshots: - encoding - graphql - '@copilotkit/runtime@0.0.0-mme-ag-ui-0-0-46-20260227141603(e7e6345cef6c41890dc996975e0ebc23)': + '@copilotkit/runtime@0.0.0-mme-ag-ui-0-0-46-20260227141603(195df52f5f6b19e457dec17fe85475da)': dependencies: '@ag-ui/client': 0.0.46 '@ag-ui/core': 0.0.46 @@ -13943,7 +14714,7 @@ snapshots: '@ai-sdk/anthropic': 2.0.23(zod@3.25.76) '@ai-sdk/openai': 2.0.52(zod@3.25.76) '@copilotkit/shared': 0.0.0-mme-ag-ui-0-0-46-20260227141603(@ag-ui/core@0.0.46) - '@copilotkitnext/agent': 0.0.0-mme-ag-ui-0-0-46-20260227141603 + '@copilotkitnext/agent': 0.0.0-mme-ag-ui-0-0-46-20260227141603(@cfworker/json-schema@4.1.1) '@copilotkitnext/runtime': 0.0.0-mme-ag-ui-0-0-46-20260227141603(@ag-ui/client@0.0.46)(@ag-ui/core@0.0.46)(@ag-ui/encoder@0.0.53)(@copilotkitnext/shared@0.0.0-mme-ag-ui-0-0-46-20260227141603) '@graphql-yoga/plugin-defer-stream': 3.16.0(graphql-yoga@5.16.0(graphql@16.11.0))(graphql@16.11.0) '@hono/node-server': 1.19.7(hono@4.11.5) @@ -13974,6 +14745,7 @@ snapshots: openai: 4.104.0(ws@8.18.3)(zod@3.25.76) transitivePeerDependencies: - '@ag-ui/encoder' + - '@cfworker/json-schema' - '@copilotkitnext/shared' - '@opentelemetry/api' - '@opentelemetry/exporter-trace-otlp-proto' @@ -13983,14 +14755,14 @@ snapshots: - react-dom - supports-color - '@copilotkit/runtime@1.55.1(3dd72e1b331c7057f722d889cc32f1ce)': + '@copilotkit/runtime@1.55.1(c3c32557d1ac98731bd405b9a6dd8f69)': dependencies: '@ag-ui/a2ui-middleware': 0.0.3(@ag-ui/client@0.0.52)(rxjs@7.8.1) '@ag-ui/client': 0.0.52 '@ag-ui/core': 0.0.52 '@ag-ui/encoder': 0.0.52 '@ag-ui/langgraph': 0.0.27(@ag-ui/client@0.0.52)(@ag-ui/core@0.0.52)(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(ws@8.18.3)(zod-to-json-schema@3.25.2(zod@3.25.76)) - '@ag-ui/mcp-apps-middleware': 0.0.3(@ag-ui/client@0.0.52) + '@ag-ui/mcp-apps-middleware': 0.0.3(@ag-ui/client@0.0.52)(@cfworker/json-schema@4.1.1)(zod@3.25.76) '@ai-sdk/anthropic': 3.0.68(zod@3.25.76) '@ai-sdk/google': 3.0.61(zod@3.25.76) '@ai-sdk/google-vertex': 3.0.127(zod@3.25.76) @@ -14036,6 +14808,7 @@ snapshots: langchain: 1.2.32(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(ws@8.18.3)(zod-to-json-schema@3.25.2(zod@3.25.76)) transitivePeerDependencies: - '@angular/core' + - '@cfworker/json-schema' - '@opentelemetry/api' - '@opentelemetry/exporter-trace-otlp-proto' - '@opentelemetry/sdk-trace-base' @@ -14115,18 +14888,19 @@ snapshots: - encoding - zod - '@copilotkitnext/agent@0.0.0-mme-ag-ui-0-0-46-20260227141603': + '@copilotkitnext/agent@0.0.0-mme-ag-ui-0-0-46-20260227141603(@cfworker/json-schema@4.1.1)': dependencies: '@ag-ui/client': 0.0.46 '@ai-sdk/anthropic': 2.0.23(zod@3.25.76) '@ai-sdk/google': 2.0.17(zod@3.25.76) '@ai-sdk/mcp': 0.0.8(zod@3.25.76) '@ai-sdk/openai': 2.0.52(zod@3.25.76) - '@modelcontextprotocol/sdk': 1.20.0 + '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76) ai: 5.0.117(zod@3.25.76) rxjs: 7.8.1 zod: 3.25.76 transitivePeerDependencies: + - '@cfworker/json-schema' - supports-color '@copilotkitnext/runtime@0.0.0-mme-ag-ui-0-0-46-20260227141603(@ag-ui/client@0.0.46)(@ag-ui/core@0.0.46)(@ag-ui/encoder@0.0.53)(@copilotkitnext/shared@0.0.0-mme-ag-ui-0-0-46-20260227141603)': @@ -14559,6 +15333,10 @@ snapshots: react-dom: 19.2.1(react@19.2.1) use-sync-external-store: 1.6.0(react@19.2.1) + '@hono/node-server@1.19.14(hono@4.11.5)': + dependencies: + hono: 4.11.5 + '@hono/node-server@1.19.7(hono@4.11.5)': dependencies: hono: 4.11.5 @@ -15027,9 +15805,9 @@ snapshots: '@langchain/aws@0.1.15(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))': dependencies: '@aws-sdk/client-bedrock-agent-runtime': 3.910.0 - '@aws-sdk/client-bedrock-runtime': 3.910.0 + '@aws-sdk/client-bedrock-runtime': 3.1044.0 '@aws-sdk/client-kendra': 3.910.0 - '@aws-sdk/credential-provider-node': 3.910.0 + '@aws-sdk/credential-provider-node': 3.972.39 '@langchain/core': 0.3.80(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)) transitivePeerDependencies: - aws-crt @@ -15075,7 +15853,7 @@ snapshots: - '@opentelemetry/sdk-trace-base' - openai - '@langchain/core@1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3)': + '@langchain/core@1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@4.4.3))(ws@8.18.3)': dependencies: '@cfworker/json-schema': 4.1.1 '@standard-schema/spec': 1.1.0 @@ -15083,11 +15861,11 @@ snapshots: camelcase: 6.3.0 decamelize: 1.2.0 js-tiktoken: 1.0.21 - langsmith: 0.5.10(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3) + langsmith: 0.5.10(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@4.4.3))(ws@8.18.3) mustache: 4.2.0 p-queue: 6.6.2 uuid: 11.1.0 - zod: 3.25.76 + zod: 4.4.3 transitivePeerDependencies: - '@opentelemetry/api' - '@opentelemetry/exporter-trace-otlp-proto' @@ -15120,9 +15898,9 @@ snapshots: '@langchain/core': 0.3.80(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)) uuid: 10.0.0 - '@langchain/langgraph-checkpoint@1.0.0(@langchain/core@1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3))': + '@langchain/langgraph-checkpoint@1.0.0(@langchain/core@1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@4.4.3))(ws@8.18.3))': dependencies: - '@langchain/core': 1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3) + '@langchain/core': 1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@4.4.3))(ws@8.18.3) uuid: 10.0.0 '@langchain/langgraph-sdk@0.1.10(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': @@ -15170,14 +15948,14 @@ snapshots: react-dom: 19.2.1(react@19.2.3) optional: true - '@langchain/langgraph-sdk@1.7.2(@langchain/core@1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3))(react-dom@19.2.1(react@19.2.3))(react@19.2.3)': + '@langchain/langgraph-sdk@1.7.2(@langchain/core@1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@4.4.3))(ws@8.18.3))(react-dom@19.2.1(react@19.2.3))(react@19.2.3)': dependencies: '@types/json-schema': 7.0.15 p-queue: 9.1.0 p-retry: 7.1.1 uuid: 13.0.0 optionalDependencies: - '@langchain/core': 1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3) + '@langchain/core': 1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@4.4.3))(ws@8.18.3) react: 19.2.3 react-dom: 19.2.1(react@19.2.3) @@ -15205,25 +15983,25 @@ snapshots: react-dom: 19.2.1(react@19.2.3) optional: true - '@langchain/langgraph-sdk@1.8.8(@langchain/core@1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3))(react-dom@19.2.1(react@19.2.3))(react@19.2.3)': + '@langchain/langgraph-sdk@1.8.8(@langchain/core@1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@4.4.3))(ws@8.18.3))(react-dom@19.2.1(react@19.2.3))(react@19.2.3)': dependencies: '@types/json-schema': 7.0.15 p-queue: 9.1.0 p-retry: 7.1.1 uuid: 13.0.0 optionalDependencies: - '@langchain/core': 1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3) + '@langchain/core': 1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@4.4.3))(ws@8.18.3) react: 19.2.3 react-dom: 19.2.1(react@19.2.3) - '@langchain/langgraph@1.2.2(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@3.25.76)': + '@langchain/langgraph@1.2.2(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@4.4.3)': dependencies: '@langchain/core': 0.3.80(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)) '@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76))) '@langchain/langgraph-sdk': 1.7.2(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) '@standard-schema/spec': 1.1.0 uuid: 10.0.0 - zod: 3.25.76 + zod: 4.4.3 optionalDependencies: zod-to-json-schema: 3.25.2(zod@3.25.76) transitivePeerDependencies: @@ -15233,14 +16011,14 @@ snapshots: - svelte - vue - '@langchain/langgraph@1.2.2(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))(react-dom@19.2.1(react@19.2.3))(react@19.2.3)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@3.25.76)': + '@langchain/langgraph@1.2.2(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))(react-dom@19.2.1(react@19.2.3))(react@19.2.3)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@4.4.3)': dependencies: '@langchain/core': 0.3.80(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)) '@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76))) '@langchain/langgraph-sdk': 1.7.2(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))(react-dom@19.2.1(react@19.2.3))(react@19.2.3) '@standard-schema/spec': 1.1.0 uuid: 10.0.0 - zod: 3.25.76 + zod: 4.4.3 optionalDependencies: zod-to-json-schema: 3.25.2(zod@3.25.76) transitivePeerDependencies: @@ -15251,16 +16029,16 @@ snapshots: - vue optional: true - '@langchain/langgraph@1.2.2(@langchain/core@1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3))(react-dom@19.2.1(react@19.2.3))(react@19.2.3)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@3.25.76)': + '@langchain/langgraph@1.2.2(@langchain/core@1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@4.4.3))(ws@8.18.3))(react-dom@19.2.1(react@19.2.3))(react@19.2.3)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3)': dependencies: - '@langchain/core': 1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3) - '@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3)) - '@langchain/langgraph-sdk': 1.7.2(@langchain/core@1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3))(react-dom@19.2.1(react@19.2.3))(react@19.2.3) + '@langchain/core': 1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@4.4.3))(ws@8.18.3) + '@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@4.4.3))(ws@8.18.3)) + '@langchain/langgraph-sdk': 1.7.2(@langchain/core@1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@4.4.3))(ws@8.18.3))(react-dom@19.2.1(react@19.2.3))(react@19.2.3) '@standard-schema/spec': 1.1.0 uuid: 10.0.0 - zod: 3.25.76 + zod: 4.4.3 optionalDependencies: - zod-to-json-schema: 3.25.2(zod@3.25.76) + zod-to-json-schema: 3.25.2(zod@4.4.3) transitivePeerDependencies: - '@angular/core' - react @@ -15588,7 +16366,7 @@ snapshots: zod: 3.25.76 zod-from-json-schema: 0.5.0 zod-from-json-schema-v3: zod-from-json-schema@0.0.5 - zod-to-json-schema: 3.24.6(zod@3.25.76) + zod-to-json-schema: 3.25.2(zod@3.25.76) '@mastra/schema-compat@1.0.0(zod@4.3.6)': dependencies: @@ -15596,7 +16374,7 @@ snapshots: zod: 4.3.6 zod-from-json-schema: 0.5.0 zod-from-json-schema-v3: zod-from-json-schema@0.0.5 - zod-to-json-schema: 3.24.6(zod@4.3.6) + zod-to-json-schema: 3.25.2(zod@4.3.6) '@mastra/server@1.0.4(@mastra/core@1.0.4(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(arktype@2.1.27)(quansync@1.0.0)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@3.25.76))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(arktype@2.1.27)(quansync@1.0.0)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@3.25.76))(@standard-schema/spec@1.1.0)(arktype@2.1.27)(openapi-types@12.1.3)(zod@3.25.76))(@types/json-schema@7.0.15)(openapi-types@12.1.3)(zod@3.25.76))(zod@3.25.76)': dependencies: @@ -15665,8 +16443,8 @@ snapshots: cross-spawn: 7.0.6 eventsource: 3.0.7 eventsource-parser: 3.0.6 - express: 5.1.0 - express-rate-limit: 7.5.1(express@5.1.0) + express: 5.2.1 + express-rate-limit: 7.5.1(express@5.2.1) pkce-challenge: 5.0.0 raw-body: 3.0.1 zod: 3.25.76 @@ -15674,6 +16452,54 @@ snapshots: transitivePeerDependencies: - supports-color + '@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.14(hono@4.11.5) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.5 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.5.1(express@5.2.1) + hono: 4.11.5 + jose: 6.2.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.0 + raw-body: 3.0.1 + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + optionalDependencies: + '@cfworker/json-schema': 4.1.1 + transitivePeerDependencies: + - supports-color + + '@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3)': + dependencies: + '@hono/node-server': 1.19.14(hono@4.11.5) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.5 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.5.1(express@5.2.1) + hono: 4.11.5 + jose: 6.2.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.0 + raw-body: 3.0.1 + zod: 4.4.3 + zod-to-json-schema: 3.25.2(zod@4.4.3) + optionalDependencies: + '@cfworker/json-schema': 4.1.1 + transitivePeerDependencies: + - supports-color + '@monaco-editor/loader@1.6.1': dependencies: state-local: 1.0.7 @@ -15744,6 +16570,8 @@ snapshots: '@next/swc-win32-x64-msvc@16.0.10': optional: true + '@nodable/entities@2.1.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -16739,6 +17567,15 @@ snapshots: '@smithy/util-middleware': 4.2.2 tslib: 2.8.1 + '@smithy/config-resolver@4.4.17': + dependencies: + '@smithy/node-config-provider': 4.3.14 + '@smithy/types': 4.14.1 + '@smithy/util-config-provider': 4.2.2 + '@smithy/util-endpoints': 3.4.2 + '@smithy/util-middleware': 4.2.14 + tslib: 2.8.1 + '@smithy/core@3.16.1': dependencies: '@smithy/middleware-serde': 4.2.2 @@ -16752,6 +17589,27 @@ snapshots: '@smithy/uuid': 1.1.0 tslib: 2.8.1 + '@smithy/core@3.23.17': + dependencies: + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-stream': 4.5.25 + '@smithy/util-utf8': 4.2.2 + '@smithy/uuid': 1.1.2 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.2.14': + dependencies: + '@smithy/node-config-provider': 4.3.14 + '@smithy/property-provider': 4.2.14 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + tslib: 2.8.1 + '@smithy/credential-provider-imds@4.2.2': dependencies: '@smithy/node-config-provider': 4.3.2 @@ -16760,40 +17618,43 @@ snapshots: '@smithy/url-parser': 4.2.2 tslib: 2.8.1 - '@smithy/eventstream-codec@4.2.2': + '@smithy/eventstream-codec@4.2.14': dependencies: '@aws-crypto/crc32': 5.2.0 - '@smithy/types': 4.7.1 - '@smithy/util-hex-encoding': 4.2.0 + '@smithy/types': 4.14.1 + '@smithy/util-hex-encoding': 4.2.2 tslib: 2.8.1 - optional: true - '@smithy/eventstream-serde-browser@4.2.2': + '@smithy/eventstream-serde-browser@4.2.14': dependencies: - '@smithy/eventstream-serde-universal': 4.2.2 - '@smithy/types': 4.7.1 + '@smithy/eventstream-serde-universal': 4.2.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - optional: true - '@smithy/eventstream-serde-config-resolver@4.3.2': + '@smithy/eventstream-serde-config-resolver@4.3.14': dependencies: - '@smithy/types': 4.7.1 + '@smithy/types': 4.14.1 tslib: 2.8.1 - optional: true - '@smithy/eventstream-serde-node@4.2.2': + '@smithy/eventstream-serde-node@4.2.14': dependencies: - '@smithy/eventstream-serde-universal': 4.2.2 - '@smithy/types': 4.7.1 + '@smithy/eventstream-serde-universal': 4.2.14 + '@smithy/types': 4.14.1 tslib: 2.8.1 - optional: true - '@smithy/eventstream-serde-universal@4.2.2': + '@smithy/eventstream-serde-universal@4.2.14': dependencies: - '@smithy/eventstream-codec': 4.2.2 - '@smithy/types': 4.7.1 + '@smithy/eventstream-codec': 4.2.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.3.17': + dependencies: + '@smithy/protocol-http': 5.3.14 + '@smithy/querystring-builder': 4.2.14 + '@smithy/types': 4.14.1 + '@smithy/util-base64': 4.3.2 tslib: 2.8.1 - optional: true '@smithy/fetch-http-handler@5.3.3': dependencies: @@ -16803,6 +17664,13 @@ snapshots: '@smithy/util-base64': 4.3.0 tslib: 2.8.1 + '@smithy/hash-node@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + '@smithy/hash-node@4.2.2': dependencies: '@smithy/types': 4.7.1 @@ -16810,6 +17678,11 @@ snapshots: '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 + '@smithy/invalid-dependency@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/invalid-dependency@4.2.2': dependencies: '@smithy/types': 4.7.1 @@ -16823,6 +17696,16 @@ snapshots: dependencies: tslib: 2.8.1 + '@smithy/is-array-buffer@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/middleware-content-length@4.2.14': + dependencies: + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/middleware-content-length@4.2.2': dependencies: '@smithy/protocol-http': 5.3.2 @@ -16840,6 +17723,17 @@ snapshots: '@smithy/util-middleware': 4.2.2 tslib: 2.8.1 + '@smithy/middleware-endpoint@4.4.32': + dependencies: + '@smithy/core': 3.23.17 + '@smithy/middleware-serde': 4.2.20 + '@smithy/node-config-provider': 4.3.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-middleware': 4.2.14 + tslib: 2.8.1 + '@smithy/middleware-retry@4.4.3': dependencies: '@smithy/node-config-provider': 4.3.2 @@ -16852,17 +17746,49 @@ snapshots: '@smithy/uuid': 1.1.0 tslib: 2.8.1 + '@smithy/middleware-retry@4.5.7': + dependencies: + '@smithy/core': 3.23.17 + '@smithy/node-config-provider': 4.3.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/service-error-classification': 4.3.1 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.8 + '@smithy/uuid': 1.1.2 + tslib: 2.8.1 + '@smithy/middleware-serde@4.2.2': dependencies: '@smithy/protocol-http': 5.3.2 '@smithy/types': 4.7.1 tslib: 2.8.1 + '@smithy/middleware-serde@4.2.20': + dependencies: + '@smithy/core': 3.23.17 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/middleware-stack@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/middleware-stack@4.2.2': dependencies: '@smithy/types': 4.7.1 tslib: 2.8.1 + '@smithy/node-config-provider@4.3.14': + dependencies: + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/node-config-provider@4.3.2': dependencies: '@smithy/property-provider': 4.2.2 @@ -16878,22 +17804,50 @@ snapshots: '@smithy/types': 4.7.1 tslib: 2.8.1 + '@smithy/node-http-handler@4.6.1': + dependencies: + '@smithy/protocol-http': 5.3.14 + '@smithy/querystring-builder': 4.2.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/property-provider@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/property-provider@4.2.2': dependencies: '@smithy/types': 4.7.1 tslib: 2.8.1 + '@smithy/protocol-http@5.3.14': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/protocol-http@5.3.2': dependencies: '@smithy/types': 4.7.1 tslib: 2.8.1 + '@smithy/querystring-builder@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + '@smithy/util-uri-escape': 4.2.2 + tslib: 2.8.1 + '@smithy/querystring-builder@4.2.2': dependencies: '@smithy/types': 4.7.1 '@smithy/util-uri-escape': 4.2.0 tslib: 2.8.1 + '@smithy/querystring-parser@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/querystring-parser@4.2.2': dependencies: '@smithy/types': 4.7.1 @@ -16903,11 +17857,31 @@ snapshots: dependencies: '@smithy/types': 4.7.1 + '@smithy/service-error-classification@4.3.1': + dependencies: + '@smithy/types': 4.14.1 + '@smithy/shared-ini-file-loader@4.3.2': dependencies: '@smithy/types': 4.7.1 tslib: 2.8.1 + '@smithy/shared-ini-file-loader@4.4.9': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/signature-v4@5.3.14': + dependencies: + '@smithy/is-array-buffer': 4.2.2 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + '@smithy/util-hex-encoding': 4.2.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-uri-escape': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + '@smithy/signature-v4@5.3.2': dependencies: '@smithy/is-array-buffer': 4.2.0 @@ -16919,6 +17893,16 @@ snapshots: '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 + '@smithy/smithy-client@4.12.13': + dependencies: + '@smithy/core': 3.23.17 + '@smithy/middleware-endpoint': 4.4.32 + '@smithy/middleware-stack': 4.2.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + '@smithy/util-stream': 4.5.25 + tslib: 2.8.1 + '@smithy/smithy-client@4.8.1': dependencies: '@smithy/core': 3.16.1 @@ -16929,10 +17913,20 @@ snapshots: '@smithy/util-stream': 4.5.2 tslib: 2.8.1 + '@smithy/types@4.14.1': + dependencies: + tslib: 2.8.1 + '@smithy/types@4.7.1': dependencies: tslib: 2.8.1 + '@smithy/url-parser@4.2.14': + dependencies: + '@smithy/querystring-parser': 4.2.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/url-parser@4.2.2': dependencies: '@smithy/querystring-parser': 4.2.2 @@ -16945,14 +17939,28 @@ snapshots: '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 + '@smithy/util-base64@4.3.2': + dependencies: + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + '@smithy/util-body-length-browser@4.2.0': dependencies: tslib: 2.8.1 + '@smithy/util-body-length-browser@4.2.2': + dependencies: + tslib: 2.8.1 + '@smithy/util-body-length-node@4.2.1': dependencies: tslib: 2.8.1 + '@smithy/util-body-length-node@4.2.3': + dependencies: + tslib: 2.8.1 + '@smithy/util-buffer-from@2.2.0': dependencies: '@smithy/is-array-buffer': 2.2.0 @@ -16963,10 +17971,19 @@ snapshots: '@smithy/is-array-buffer': 4.2.0 tslib: 2.8.1 + '@smithy/util-buffer-from@4.2.2': + dependencies: + '@smithy/is-array-buffer': 4.2.2 + tslib: 2.8.1 + '@smithy/util-config-provider@4.2.0': dependencies: tslib: 2.8.1 + '@smithy/util-config-provider@4.2.2': + dependencies: + tslib: 2.8.1 + '@smithy/util-defaults-mode-browser@4.3.2': dependencies: '@smithy/property-provider': 4.2.2 @@ -16974,6 +17991,13 @@ snapshots: '@smithy/types': 4.7.1 tslib: 2.8.1 + '@smithy/util-defaults-mode-browser@4.3.49': + dependencies: + '@smithy/property-provider': 4.2.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/util-defaults-mode-node@4.2.3': dependencies: '@smithy/config-resolver': 4.3.2 @@ -16984,16 +18008,41 @@ snapshots: '@smithy/types': 4.7.1 tslib: 2.8.1 + '@smithy/util-defaults-mode-node@4.2.54': + dependencies: + '@smithy/config-resolver': 4.4.17 + '@smithy/credential-provider-imds': 4.2.14 + '@smithy/node-config-provider': 4.3.14 + '@smithy/property-provider': 4.2.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/util-endpoints@3.2.2': dependencies: '@smithy/node-config-provider': 4.3.2 '@smithy/types': 4.7.1 tslib: 2.8.1 + '@smithy/util-endpoints@3.4.2': + dependencies: + '@smithy/node-config-provider': 4.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/util-hex-encoding@4.2.0': dependencies: tslib: 2.8.1 + '@smithy/util-hex-encoding@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-middleware@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/util-middleware@4.2.2': dependencies: '@smithy/types': 4.7.1 @@ -17005,6 +18054,12 @@ snapshots: '@smithy/types': 4.7.1 tslib: 2.8.1 + '@smithy/util-retry@4.3.8': + dependencies: + '@smithy/service-error-classification': 4.3.1 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + '@smithy/util-stream@4.5.2': dependencies: '@smithy/fetch-http-handler': 5.3.3 @@ -17016,10 +18071,25 @@ snapshots: '@smithy/util-utf8': 4.2.0 tslib: 2.8.1 + '@smithy/util-stream@4.5.25': + dependencies: + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/node-http-handler': 4.6.1 + '@smithy/types': 4.14.1 + '@smithy/util-base64': 4.3.2 + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-hex-encoding': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + '@smithy/util-uri-escape@4.2.0': dependencies: tslib: 2.8.1 + '@smithy/util-uri-escape@4.2.2': + dependencies: + tslib: 2.8.1 + '@smithy/util-utf8@2.3.0': dependencies: '@smithy/util-buffer-from': 2.2.0 @@ -17030,6 +18100,11 @@ snapshots: '@smithy/util-buffer-from': 4.2.0 tslib: 2.8.1 + '@smithy/util-utf8@4.2.2': + dependencies: + '@smithy/util-buffer-from': 4.2.2 + tslib: 2.8.1 + '@smithy/util-waiter@4.2.2': dependencies: '@smithy/abort-controller': 4.2.2 @@ -17040,6 +18115,10 @@ snapshots: dependencies: tslib: 2.8.1 + '@smithy/uuid@1.1.2': + dependencies: + tslib: 2.8.1 + '@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(arktype@2.1.27)(quansync@1.0.0)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@3.25.76)': dependencies: '@standard-schema/spec': 1.1.0 @@ -17080,6 +18159,40 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@strands-agents/sdk@1.1.0(@ai-sdk/provider@3.0.8)(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@opentelemetry/api@1.9.0)(@opentelemetry/resources@2.6.1(@opentelemetry/api@1.9.0))(express@5.1.0)(openai@6.10.0(ws@8.18.3)(zod@4.4.3))(zod@4.4.3)': + dependencies: + '@aws-sdk/client-bedrock-runtime': 3.1044.0 + '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3) + '@opentelemetry/api': 1.9.0 + '@types/json-schema': 7.0.15 + uuid: 14.0.0 + yaml: 2.8.4 + zod: 4.4.3 + optionalDependencies: + '@ai-sdk/provider': 3.0.8 + '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.0) + express: 5.1.0 + openai: 6.10.0(ws@8.18.3)(zod@4.4.3) + transitivePeerDependencies: + - aws-crt + + '@strands-agents/sdk@1.1.0(@ai-sdk/provider@3.0.8)(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(@opentelemetry/api@1.9.0)(@opentelemetry/resources@2.6.1(@opentelemetry/api@1.9.0))(express@5.2.1)(openai@6.10.0(ws@8.18.3)(zod@4.4.3))(zod@4.4.3)': + dependencies: + '@aws-sdk/client-bedrock-runtime': 3.1044.0 + '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3) + '@opentelemetry/api': 1.9.0 + '@types/json-schema': 7.0.15 + uuid: 14.0.0 + yaml: 2.8.4 + zod: 4.4.3 + optionalDependencies: + '@ai-sdk/provider': 3.0.8 + '@opentelemetry/resources': 2.6.1(@opentelemetry/api@1.9.0) + express: 5.2.1 + openai: 6.10.0(ws@8.18.3)(zod@4.4.3) + transitivePeerDependencies: + - aws-crt + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -17529,6 +18642,13 @@ snapshots: '@types/range-parser': 1.2.7 '@types/send': 1.2.0 + '@types/express-serve-static-core@5.1.1': + dependencies: + '@types/node': 20.19.21 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.0 + '@types/express@4.17.23': dependencies: '@types/body-parser': 1.19.6 @@ -17536,6 +18656,12 @@ snapshots: '@types/qs': 6.14.0 '@types/serve-static': 1.15.9 + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.1 + '@types/serve-static': 2.2.0 + '@types/geojson@7946.0.16': {} '@types/graceful-fs@4.1.9': @@ -17653,6 +18779,11 @@ snapshots: '@types/node': 20.19.21 '@types/send': 0.17.5 + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 20.19.21 + '@types/stack-utils@2.0.3': {} '@types/through@0.0.33': @@ -17861,7 +18992,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-istanbul@4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1))': + '@vitest/coverage-istanbul@4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4))': dependencies: '@istanbuljs/schema': 0.1.3 '@jridgewell/gen-mapping': 0.3.13 @@ -17873,7 +19004,7 @@ snapshots: magicast: 0.5.1 obug: 2.1.1 tinyrainbow: 3.0.3 - vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) transitivePeerDependencies: - supports-color @@ -17901,13 +19032,13 @@ snapshots: optionalDependencies: vite: 5.4.21(@types/node@20.19.21)(lightningcss@1.30.1) - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1))': + '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.3.1(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) '@vitest/pretty-format@2.1.9': dependencies: @@ -18065,6 +19196,10 @@ snapshots: '@opentelemetry/api': 1.9.0 zod: 3.25.76 + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -18079,6 +19214,13 @@ snapshots: require-from-string: 2.0.2 uri-js: 4.4.1 + 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 + ansi-align@3.0.1: dependencies: string-width: 4.2.3 @@ -18420,6 +19562,20 @@ snapshots: transitivePeerDependencies: - supports-color + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.0 + iconv-lite: 0.7.0 + on-finished: 2.4.1 + qs: 6.15.1 + raw-body: 3.0.1 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + bowser@2.12.1: {} boxen@7.0.0: @@ -19578,8 +20734,8 @@ snapshots: '@babel/parser': 7.28.5 eslint: 9.37.0(jiti@2.6.1) hermes-parser: 0.25.1 - zod: 3.25.76 - zod-validation-error: 4.0.2(zod@3.25.76) + zod: 4.4.3 + zod-validation-error: 4.0.2(zod@4.4.3) transitivePeerDependencies: - supports-color @@ -19778,9 +20934,14 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 - express-rate-limit@7.5.1(express@5.1.0): + express-rate-limit@7.5.1(express@5.2.1): dependencies: - express: 5.1.0 + express: 5.2.1 + + express-rate-limit@8.5.1(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.2.0 express@4.21.2: dependencies: @@ -19850,6 +21011,39 @@ snapshots: transitivePeerDependencies: - supports-color + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.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.0 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.1 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + exsolve@1.0.7: {} extend@3.0.2: {} @@ -19885,10 +21079,23 @@ snapshots: fast-text-encoding@1.0.6: optional: true + fast-uri@3.1.2: {} + + fast-xml-builder@1.1.9: + dependencies: + path-expression-matcher: 1.5.0 + fast-xml-parser@5.2.5: dependencies: strnum: 2.1.1 + fast-xml-parser@5.7.2: + dependencies: + '@nodable/entities': 2.1.0 + fast-xml-builder: 1.1.9 + path-expression-matcher: 1.5.0 + strnum: 2.2.3 + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -20607,6 +21814,8 @@ snapshots: internmap@2.0.3: {} + ip-address@10.2.0: {} + ipaddr.js@1.9.1: {} is-alphabetical@1.0.4: {} @@ -21176,6 +22385,8 @@ snapshots: jose@5.10.0: {} + jose@6.2.3: {} + joycon@3.1.1: {} js-base64@3.7.8: {} @@ -21211,6 +22422,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema-typed@8.0.2: {} + json-schema@0.4.0: {} json-stable-stringify-without-jsonify@1.0.1: {} @@ -21274,11 +22487,11 @@ snapshots: langchain@1.2.32(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(ws@8.18.3)(zod-to-json-schema@3.25.2(zod@3.25.76)): dependencies: '@langchain/core': 0.3.80(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)) - '@langchain/langgraph': 1.2.2(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@3.25.76) + '@langchain/langgraph': 1.2.2(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@4.4.3) '@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76))) langsmith: 0.5.10(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3) uuid: 11.1.0 - zod: 3.25.76 + zod: 4.4.3 transitivePeerDependencies: - '@angular/core' - '@opentelemetry/api' @@ -21295,11 +22508,11 @@ snapshots: langchain@1.2.32(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76))(react-dom@19.2.1(react@19.2.3))(react@19.2.3)(ws@8.18.3)(zod-to-json-schema@3.25.2(zod@3.25.76)): dependencies: '@langchain/core': 0.3.80(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)) - '@langchain/langgraph': 1.2.2(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))(react-dom@19.2.1(react@19.2.3))(react@19.2.3)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@3.25.76) + '@langchain/langgraph': 1.2.2(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76)))(react-dom@19.2.1(react@19.2.3))(react@19.2.3)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@4.4.3) '@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76))) langsmith: 0.5.10(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@4.104.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3) uuid: 11.1.0 - zod: 3.25.76 + zod: 4.4.3 transitivePeerDependencies: - '@angular/core' - '@opentelemetry/api' @@ -21314,14 +22527,14 @@ snapshots: - zod-to-json-schema optional: true - langchain@1.2.32(@langchain/core@1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@3.25.76))(react-dom@19.2.1(react@19.2.3))(react@19.2.3)(ws@8.18.3)(zod-to-json-schema@3.25.2(zod@3.25.76)): + langchain@1.2.32(@langchain/core@1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@4.4.3))(ws@8.18.3))(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@4.4.3))(react-dom@19.2.1(react@19.2.3))(react@19.2.3)(ws@8.18.3)(zod-to-json-schema@3.25.2(zod@4.4.3)): dependencies: - '@langchain/core': 1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3) - '@langchain/langgraph': 1.2.2(@langchain/core@1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3))(react-dom@19.2.1(react@19.2.3))(react@19.2.3)(zod-to-json-schema@3.25.2(zod@3.25.76))(zod@3.25.76) - '@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3)) - langsmith: 0.5.10(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3) + '@langchain/core': 1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@4.4.3))(ws@8.18.3) + '@langchain/langgraph': 1.2.2(@langchain/core@1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@4.4.3))(ws@8.18.3))(react-dom@19.2.1(react@19.2.3))(react@19.2.3)(zod-to-json-schema@3.25.2(zod@4.4.3))(zod@4.4.3) + '@langchain/langgraph-checkpoint': 1.0.0(@langchain/core@1.1.40(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@4.4.3))(ws@8.18.3)) + langsmith: 0.5.10(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@4.4.3))(ws@8.18.3) uuid: 11.1.0 - zod: 3.25.76 + zod: 4.4.3 transitivePeerDependencies: - '@angular/core' - '@opentelemetry/api' @@ -21388,7 +22601,7 @@ snapshots: openai: 4.104.0(ws@8.18.3)(zod@3.25.76) ws: 8.18.3 - langsmith@0.5.10(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@3.25.76))(ws@8.18.3): + langsmith@0.5.10(@opentelemetry/api@1.9.0)(@opentelemetry/exporter-trace-otlp-proto@0.203.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.10.0(ws@8.18.3)(zod@4.4.3))(ws@8.18.3): dependencies: '@types/uuid': 10.0.0 chalk: 5.6.2 @@ -21400,7 +22613,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/exporter-trace-otlp-proto': 0.203.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) - openai: 6.10.0(ws@8.18.3)(zod@3.25.76) + openai: 6.10.0(ws@8.18.3)(zod@4.4.3) ws: 8.18.3 language-subtag-registry@0.3.23: {} @@ -22734,6 +23947,11 @@ snapshots: ws: 8.18.3 zod: 3.25.76 + openai@6.10.0(ws@8.18.3)(zod@4.4.3): + optionalDependencies: + ws: 8.18.3 + zod: 4.4.3 + openapi-types@12.1.3: {} optionator@0.9.4: @@ -22883,6 +24101,8 @@ snapshots: path-exists@4.0.0: {} + path-expression-matcher@1.5.0: {} + path-is-absolute@1.0.1: {} path-is-inside@1.0.2: {} @@ -23023,14 +24243,14 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6)(yaml@2.8.1): + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6)(yaml@2.8.4): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 2.6.1 postcss: 8.5.6 tsx: 4.20.6 - yaml: 2.8.1 + yaml: 2.8.4 postcss-selector-parser@6.0.10: dependencies: @@ -23279,6 +24499,10 @@ snapshots: dependencies: side-channel: 1.1.0 + qs@6.15.1: + dependencies: + side-channel: 1.1.0 + quansync@0.2.11: {} quansync@1.0.0: {} @@ -24255,6 +25479,8 @@ snapshots: strnum@2.1.1: {} + strnum@2.2.3: {} + style-to-js@1.1.18: dependencies: style-to-object: 1.0.11 @@ -24483,7 +25709,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1): + tsup@8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.4): dependencies: bundle-require: 5.1.0(esbuild@0.27.2) cac: 6.7.14 @@ -24494,7 +25720,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6)(yaml@2.8.1) + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6)(yaml@2.8.4) resolve-from: 5.0.0 rollup: 4.52.4 source-map: 0.7.6 @@ -24835,6 +26061,8 @@ snapshots: uuid@13.0.0: {} + uuid@14.0.0: {} + uuid@9.0.1: {} uvu@0.5.6: @@ -24911,7 +26139,7 @@ snapshots: fsevents: 2.3.3 lightningcss: 1.30.1 - vite@7.3.1(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1): + vite@7.3.1(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) @@ -24925,7 +26153,7 @@ snapshots: jiti: 2.6.1 lightningcss: 1.30.1 tsx: 4.20.6 - yaml: 2.8.1 + yaml: 2.8.4 vitest@2.1.9(@types/node@20.19.21)(lightningcss@1.30.1): dependencies: @@ -24962,10 +26190,10 @@ snapshots: - supports-color - terser - vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1): + vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -24982,7 +26210,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.3.1(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.4) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 @@ -25158,6 +26386,8 @@ snapshots: yaml@2.8.1: {} + yaml@2.8.4: {} + yargs-parser@20.2.9: {} yargs-parser@21.1.1: {} @@ -25198,7 +26428,7 @@ snapshots: zod-from-json-schema@0.5.0: dependencies: - zod: 4.3.6 + zod: 4.4.3 zod-to-json-schema@3.24.6(zod@3.25.76): dependencies: @@ -25215,14 +26445,19 @@ snapshots: zod-to-json-schema@3.25.2(zod@4.3.6): dependencies: zod: 4.3.6 - optional: true - zod-validation-error@4.0.2(zod@3.25.76): + zod-to-json-schema@3.25.2(zod@4.4.3): dependencies: - zod: 3.25.76 + zod: 4.4.3 + + zod-validation-error@4.0.2(zod@4.4.3): + dependencies: + zod: 4.4.3 zod@3.25.76: {} zod@4.3.6: {} + zod@4.4.3: {} + zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a8415eed32..99fa1f4a93 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,6 +4,7 @@ packages: - sdks/typescript/packages/* - integrations/*/typescript - integrations/community/*/typescript + - integrations/aws-strands/typescript/examples - integrations/mastra/typescript/examples ignoredBuiltDependencies: