Repository: ag-ui-protocol/ag-ui Branch: main Commit: 0db03d09bcf2 Files: 1836 Total size: 20.9 MB Directory structure: gitextract_1usf67qd/ ├── .claude/ │ └── settings.json ├── .gitattributes ├── .github/ │ ├── CODEOWNERS │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── documentation.yml │ │ └── feature_request.yml │ ├── pull_request_template.md │ └── workflows/ │ ├── auto-approve-community.yml │ ├── build-python-preview.yml │ ├── dojo-e2e.yml │ ├── pr-check-binaries.yml │ ├── publish-commit.yml │ ├── publish-java-sdk.yml │ ├── publish-kotlin-sdk.yml │ ├── publish-python-package.yml │ ├── publish-python-preview.yml │ ├── rust-lint-test.yml │ ├── unit-dart-sdk.yml │ ├── unit-genkit-go.yml │ ├── unit-go-sdk.yml │ ├── unit-java-sdk.yml │ ├── unit-kotlin-sdk.yml │ ├── unit-python-sdk.yml │ ├── unit-ruby-sdk.yml │ └── unit-typescript-sdk.yml ├── .gitignore ├── .mcp.json ├── AGENTS.md ├── CLAUDE.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── apps/ │ ├── client-cli-example/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── agent.ts │ │ │ ├── index.ts │ │ │ └── tools/ │ │ │ ├── browser.tool.ts │ │ │ └── weather.tool.ts │ │ └── tsconfig.json │ └── dojo/ │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── components.json │ ├── e2e/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── VIDEO_SETUP.md │ │ ├── clean-reporter.cjs │ │ ├── featurePages/ │ │ │ ├── AgenticChatPage.ts │ │ │ ├── HumanInTheLoopPage.ts │ │ │ ├── SharedStatePage.ts │ │ │ ├── ToolBaseGenUIPage.ts │ │ │ └── V1AgenticChatPage.ts │ │ ├── fixtures/ │ │ │ └── openai/ │ │ │ ├── agentic-chat.json │ │ │ ├── agentic-gen-ui.json │ │ │ ├── backend-tool-rendering.json │ │ │ ├── human-in-the-loop.json │ │ │ ├── predictive-state.json │ │ │ ├── shared-state.json │ │ │ ├── subgraphs.json │ │ │ ├── tool-based-gen-ui.json │ │ │ └── v1-chat.json │ │ ├── lib/ │ │ │ ├── constants.ts │ │ │ ├── mock-agent.ts │ │ │ └── upload-video.ts │ │ ├── llmock-setup.ts │ │ ├── package.json │ │ ├── pages/ │ │ │ ├── a2aMiddlewarePages/ │ │ │ │ └── A2AChatPage.ts │ │ │ ├── adkMiddlewarePages/ │ │ │ │ ├── HumanInLoopPage.ts │ │ │ │ └── PredictiveStateUpdatesPage.ts │ │ │ ├── agnoPages/ │ │ │ │ └── HumanInLoopPage.ts │ │ │ ├── awsStrandsPages/ │ │ │ │ ├── AgenticUIGenPage.ts │ │ │ │ └── HumanInLoopPage.ts │ │ │ ├── crewAIPages/ │ │ │ │ ├── AgenticUIGenPage.ts │ │ │ │ ├── HumanInLoopPage.ts │ │ │ │ └── PredictiveStateUpdatesPage.ts │ │ │ ├── langGraphFastAPIPages/ │ │ │ │ ├── AgenticUIGenPage.ts │ │ │ │ ├── HumanInLoopPage.ts │ │ │ │ ├── PredictiveStateUpdatesPage.ts │ │ │ │ └── SubgraphsPage.ts │ │ │ ├── langGraphPages/ │ │ │ │ ├── AgenticUIGenPage.ts │ │ │ │ ├── HumanInLoopPage.ts │ │ │ │ ├── PredictiveStateUpdatesPage.ts │ │ │ │ └── SubgraphsPage.ts │ │ │ ├── langroidPages/ │ │ │ │ └── AgenticUIGenPage.ts │ │ │ ├── llamaIndexPages/ │ │ │ │ ├── AgenticUIGenPage.ts │ │ │ │ └── HumanInLoopPage.ts │ │ │ ├── pydanticAIPages/ │ │ │ │ ├── AgenticUIGenPage.ts │ │ │ │ ├── HumanInLoopPage.ts │ │ │ │ └── PredictiveStateUpdatesPage.ts │ │ │ └── serverStarterAllFeaturesPages/ │ │ │ ├── AgenticUIGenPage.ts │ │ │ ├── HumanInLoopPage.ts │ │ │ └── PredictiveStateUpdatesPage.ts │ │ ├── playwright.config.ts │ │ ├── pnpm-workspace.yaml │ │ ├── reporters/ │ │ │ └── s3-video-reporter.ts │ │ ├── setup-aws.sh │ │ ├── slack-layout-simple.ts │ │ ├── slack-layout.ts │ │ ├── test-isolation-helper.ts │ │ ├── test-isolation-setup.ts │ │ ├── test-isolation-teardown.ts │ │ ├── tests/ │ │ │ ├── a2aMiddlewareTests/ │ │ │ │ └── a2aChatPage.spec.ts │ │ │ ├── adkMiddlewareTests/ │ │ │ │ ├── agenticChatPage.spec.ts │ │ │ │ ├── backendToolRenderingPage.spec.ts │ │ │ │ ├── humanInTheLoopPage.spec.ts │ │ │ │ ├── predictiveStateUpdatePage.spec.ts │ │ │ │ ├── sharedStatePage.spec.ts │ │ │ │ ├── toolBasedGenUIPage.spec.ts │ │ │ │ └── v1AgenticChatPage.spec.ts │ │ │ ├── agnoTests/ │ │ │ │ ├── agenticChatPage.spec.ts │ │ │ │ ├── backendToolRenderingPage.spec.ts │ │ │ │ ├── humanInTheLoopPage.spec.ts │ │ │ │ ├── toolBasedGenUIPage.spec.ts │ │ │ │ └── v1AgenticChatPage.spec.ts │ │ │ ├── awsStrandsTests/ │ │ │ │ ├── agenticChatPage.spec.ts │ │ │ │ ├── agenticGenUI.spec.ts │ │ │ │ ├── backendToolRenderingPage.spec.ts │ │ │ │ ├── humanInTheLoopPage.spec.ts │ │ │ │ ├── sharedStatePage.spec.ts │ │ │ │ └── v1AgenticChatPage.spec.ts │ │ │ ├── claudeAgentSdkPythonTests/ │ │ │ │ ├── agenticChatPage.spec.ts │ │ │ │ ├── backendToolRenderingPage.spec.ts │ │ │ │ ├── humanInTheLoopPage.spec.ts │ │ │ │ ├── sharedStatePage.spec.ts │ │ │ │ └── toolBasedGenUIPage.spec.ts │ │ │ ├── claudeAgentSdkTypescriptTests/ │ │ │ │ ├── agenticChatPage.spec.ts │ │ │ │ ├── backendToolRenderingPage.spec.ts │ │ │ │ ├── humanInTheLoopPage.spec.ts │ │ │ │ ├── sharedStatePage.spec.ts │ │ │ │ └── toolBasedGenUIPage.spec.ts │ │ │ ├── crewAITests/ │ │ │ │ ├── agenticChatPage.spec.ts │ │ │ │ ├── agenticGenUI.spec.ts │ │ │ │ ├── humanInTheLoopPage.spec.ts │ │ │ │ ├── predictiveStateUpdatePage.spec.ts │ │ │ │ ├── sharedStatePage.spec.ts │ │ │ │ ├── toolBasedGenUIPage.spec.ts │ │ │ │ └── v1AgenticChatPage.spec.ts │ │ │ ├── langchainTests/ │ │ │ │ ├── agenticChatPage.spec.ts │ │ │ │ └── toolBasedGenUIPage.spec.ts │ │ │ ├── langgraphFastAPITests/ │ │ │ │ ├── agenticChatPage.spec.ts │ │ │ │ ├── agenticGenUI.spec.ts │ │ │ │ ├── backendToolRenderingPage.spec.ts │ │ │ │ ├── humanInTheLoopPage.spec.ts │ │ │ │ ├── predictiveStateUpdatePage.spec.ts │ │ │ │ ├── sharedStatePage.spec.ts │ │ │ │ ├── subgraphsPage.spec.ts │ │ │ │ ├── toolBasedGenUIPage.spec.ts │ │ │ │ └── v1AgenticChatPage.spec.ts │ │ │ ├── langgraphPythonTests/ │ │ │ │ ├── agenticChatPage.spec.ts │ │ │ │ ├── agenticGenUI.spec.ts │ │ │ │ ├── backendToolRenderingPage.spec.ts │ │ │ │ ├── humanInTheLoopPage.spec.ts │ │ │ │ ├── predictiveStateUpdatePage.spec.ts │ │ │ │ ├── sharedStatePage.spec.ts │ │ │ │ ├── subgraphsPage.spec.ts │ │ │ │ ├── toolBasedGenUIPage.spec.ts │ │ │ │ └── v1AgenticChatPage.spec.ts │ │ │ ├── langgraphTypescriptTests/ │ │ │ │ ├── agenticChatDeterministic.spec.ts │ │ │ │ ├── agenticChatPage.spec.ts │ │ │ │ ├── agenticGenUI.spec.ts │ │ │ │ ├── humanInTheLoopPage.spec.ts │ │ │ │ ├── predictiveStateUpdatePage.spec.ts │ │ │ │ ├── sharedStatePage.spec.ts │ │ │ │ ├── subgraphsPage.spec.ts │ │ │ │ ├── toolBasedGenUIPage.spec.ts │ │ │ │ └── v1AgenticChatPage.spec.ts │ │ │ ├── langroidTests/ │ │ │ │ ├── agenticChatPage.spec.ts │ │ │ │ ├── agenticGenUI.spec.ts │ │ │ │ ├── backendToolRenderingPage.spec.ts │ │ │ │ └── sharedStatePage.spec.ts │ │ │ ├── llamaIndexTests/ │ │ │ │ ├── agenticChatPage.spec.ts │ │ │ │ ├── agenticGenUI.spec.ts │ │ │ │ ├── backendToolRenderingPage.spec.ts │ │ │ │ ├── humanInTheLoopPage.spec.ts │ │ │ │ ├── sharedStatePage.spec.ts │ │ │ │ └── v1AgenticChatPage.spec.ts │ │ │ ├── mastraAgentLocalTests/ │ │ │ │ ├── agenticChatPage.spec.ts │ │ │ │ ├── backendToolRenderingPage.spec.ts │ │ │ │ ├── humanInTheLoopPage.spec.ts │ │ │ │ ├── sharedStatePage.spec.ts │ │ │ │ ├── toolBasedGenUIPage.spec.ts │ │ │ │ └── v1AgenticChatPage.spec.ts │ │ │ ├── mastraTests/ │ │ │ │ ├── agenticChatPage.spec.ts │ │ │ │ ├── backendToolRenderingPage.spec.ts │ │ │ │ ├── humanInTheLoopPage.spec.ts │ │ │ │ ├── toolBasedGenUIPage.spec.ts │ │ │ │ └── v1AgenticChatPage.spec.ts │ │ │ ├── middlewareStarterTests/ │ │ │ │ ├── agenticChatPage.spec.ts │ │ │ │ └── v1AgenticChatPage.spec.ts │ │ │ ├── pydanticAITests/ │ │ │ │ ├── agenticChatPage.spec.ts │ │ │ │ ├── agenticGenUI.spec.ts │ │ │ │ ├── backendToolRenderingPage.spec.ts │ │ │ │ ├── humanInTheLoopPage.spec.ts │ │ │ │ ├── predictiveStateUpdatePage.spec.ts │ │ │ │ ├── sharedStatePage.spec.ts │ │ │ │ ├── toolBasedGenUIPage.spec.ts │ │ │ │ └── v1AgenticChatPage.spec.ts │ │ │ ├── serverStarterAllFeaturesTests/ │ │ │ │ ├── agenticChatPage.spec.ts │ │ │ │ ├── agenticGenUI.spec.ts │ │ │ │ ├── backendToolRenderingPage.spec.ts │ │ │ │ ├── humanInTheLoopPage.spec.ts │ │ │ │ ├── predictiveStateUpdatePage.spec.ts │ │ │ │ ├── sharedStatePage.spec.ts │ │ │ │ ├── toolBasedGenUIPage.spec.ts │ │ │ │ └── v1AgenticChatPage.spec.ts │ │ │ └── serverStarterTests/ │ │ │ ├── agenticChatPage.spec.ts │ │ │ └── v1AgenticChatPage.spec.ts │ │ └── utils/ │ │ ├── aiWaitHelpers.ts │ │ ├── copilot-actions.ts │ │ └── copilot-selectors.ts │ ├── eslint.config.mjs │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── scripts/ │ │ ├── generate-content-json.ts │ │ ├── link-cpk.js │ │ ├── prep-dojo-everything.js │ │ └── run-dojo-everything.js │ ├── src/ │ │ ├── agents.ts │ │ ├── app/ │ │ │ ├── [integrationId]/ │ │ │ │ ├── feature/ │ │ │ │ │ ├── (v1)/ │ │ │ │ │ │ └── v1_agentic_chat/ │ │ │ │ │ │ ├── README.mdx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── (v2)/ │ │ │ │ │ │ ├── a2a_chat/ │ │ │ │ │ │ │ ├── README.mdx │ │ │ │ │ │ │ ├── a2a_chat.tsx │ │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ │ └── style.css │ │ │ │ │ │ ├── a2ui_chat/ │ │ │ │ │ │ │ ├── README.mdx │ │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ │ ├── style.css │ │ │ │ │ │ │ └── theme.ts │ │ │ │ │ │ ├── agentic_chat/ │ │ │ │ │ │ │ ├── README.mdx │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ ├── agentic_chat_reasoning/ │ │ │ │ │ │ │ ├── README.mdx │ │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ │ └── style.css │ │ │ │ │ │ ├── agentic_generative_ui/ │ │ │ │ │ │ │ ├── README.mdx │ │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ │ └── style.css │ │ │ │ │ │ ├── backend_tool_rendering/ │ │ │ │ │ │ │ ├── README.mdx │ │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ │ └── style.css │ │ │ │ │ │ ├── human_in_the_loop/ │ │ │ │ │ │ │ ├── README.mdx │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ ├── predictive_state_updates/ │ │ │ │ │ │ │ ├── README.mdx │ │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ │ └── style.css │ │ │ │ │ │ ├── shared_state/ │ │ │ │ │ │ │ ├── README.mdx │ │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ │ └── style.css │ │ │ │ │ │ ├── subgraphs/ │ │ │ │ │ │ │ ├── README.mdx │ │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ │ └── style.css │ │ │ │ │ │ ├── tool_based_generative_ui/ │ │ │ │ │ │ │ ├── README.mdx │ │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ │ └── style.css │ │ │ │ │ │ └── vnext_chat/ │ │ │ │ │ │ ├── README.mdx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── layout-client.tsx │ │ │ │ │ ├── layout.tsx │ │ │ │ │ └── not-found.tsx │ │ │ │ ├── not-found.tsx │ │ │ │ └── page.tsx │ │ │ ├── api/ │ │ │ │ ├── copilotkit/ │ │ │ │ │ ├── [integrationId]/ │ │ │ │ │ │ └── [[...slug]]/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── route.ts │ │ │ │ └── copilotkitnext/ │ │ │ │ └── [integrationId]/ │ │ │ │ └── [[...slug]]/ │ │ │ │ └── route.ts │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── components/ │ │ │ ├── code-viewer/ │ │ │ │ ├── code-editor.tsx │ │ │ │ └── code-viewer.tsx │ │ │ ├── demo-list/ │ │ │ │ └── demo-list.tsx │ │ │ ├── file-tree/ │ │ │ │ ├── file-tree-nav.tsx │ │ │ │ └── file-tree.tsx │ │ │ ├── layout/ │ │ │ │ ├── main-layout.tsx │ │ │ │ └── viewer-layout.tsx │ │ │ ├── readme/ │ │ │ │ └── readme.tsx │ │ │ ├── sidebar/ │ │ │ │ └── sidebar.tsx │ │ │ ├── theme-provider.tsx │ │ │ ├── theme-wrapper.tsx │ │ │ └── ui/ │ │ │ ├── badge.tsx │ │ │ ├── button.tsx │ │ │ ├── carousel.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── mdx-components.tsx │ │ │ ├── tabs.tsx │ │ │ └── theme-toggle.tsx │ │ ├── config.ts │ │ ├── contexts/ │ │ │ └── url-params-context.tsx │ │ ├── env.ts │ │ ├── files.json │ │ ├── lib/ │ │ │ └── utils.ts │ │ ├── mastra/ │ │ │ ├── agents/ │ │ │ │ ├── agentic-chat.ts │ │ │ │ ├── backend-tool-rendering.ts │ │ │ │ ├── human-in-the-loop.ts │ │ │ │ ├── shared-state.ts │ │ │ │ └── tool-based-generative-ui.ts │ │ │ ├── index.ts │ │ │ ├── storage.ts │ │ │ └── tools.ts │ │ ├── menu.ts │ │ ├── proxy.ts │ │ ├── styles/ │ │ │ └── typography.css │ │ ├── types/ │ │ │ ├── agents.ts │ │ │ ├── feature.ts │ │ │ ├── integration.ts │ │ │ └── interface.ts │ │ └── utils/ │ │ ├── agents.ts │ │ ├── domain-config.ts │ │ ├── mdx-utils.tsx │ │ ├── menu.ts │ │ ├── use-is-inside-iframe.ts │ │ ├── use-mobile-chat.ts │ │ └── use-mobile-view.ts │ ├── tailwind.config.ts │ └── tsconfig.json ├── docs/ │ ├── .gitignore │ ├── .prettierignore │ ├── .prettierrc │ ├── LICENSE │ ├── README.md │ ├── ag_ui.md │ ├── agentic-protocols.mdx │ ├── concepts/ │ │ ├── agents.mdx │ │ ├── architecture.mdx │ │ ├── capabilities.mdx │ │ ├── events.mdx │ │ ├── generative-ui-specs.mdx │ │ ├── messages.mdx │ │ ├── middleware.mdx │ │ ├── reasoning.mdx │ │ ├── serialization.mdx │ │ ├── state.mdx │ │ └── tools.mdx │ ├── development/ │ │ ├── contributing.mdx │ │ ├── roadmap.mdx │ │ └── updates.mdx │ ├── docs.json │ ├── drafts/ │ │ ├── generative-ui.mdx │ │ ├── interrupts.mdx │ │ ├── meta-events.mdx │ │ ├── multimodal-messages.mdx │ │ └── overview.mdx │ ├── icons/ │ │ ├── custom-icons.tsx │ │ └── index.tsx │ ├── images/ │ │ └── left-illustration.avif │ ├── integrations.mdx │ ├── introduction.mdx │ ├── package.json │ ├── quickstart/ │ │ ├── applications.mdx │ │ ├── clients.mdx │ │ ├── introduction.mdx │ │ ├── middleware.mdx │ │ └── server.mdx │ ├── sdk/ │ │ ├── dart/ │ │ │ ├── client/ │ │ │ │ ├── client.mdx │ │ │ │ └── overview.mdx │ │ │ ├── core/ │ │ │ │ ├── events.mdx │ │ │ │ ├── overview.mdx │ │ │ │ └── types.mdx │ │ │ ├── encoder/ │ │ │ │ └── overview.mdx │ │ │ └── overview.mdx │ │ ├── go/ │ │ │ ├── client/ │ │ │ │ ├── overview.mdx │ │ │ │ └── sse-client.mdx │ │ │ ├── core/ │ │ │ │ ├── events.mdx │ │ │ │ ├── overview.mdx │ │ │ │ └── types.mdx │ │ │ ├── encoding/ │ │ │ │ └── overview.mdx │ │ │ ├── errors/ │ │ │ │ └── overview.mdx │ │ │ └── overview.mdx │ │ ├── java/ │ │ │ ├── client/ │ │ │ │ ├── abstract-agent.mdx │ │ │ │ ├── http-agent.mdx │ │ │ │ ├── overview.mdx │ │ │ │ └── subscriber.mdx │ │ │ ├── core/ │ │ │ │ ├── events.mdx │ │ │ │ ├── overview.mdx │ │ │ │ ├── stream.mdx │ │ │ │ ├── subscription.mdx │ │ │ │ └── types.mdx │ │ │ ├── overview.mdx │ │ │ └── server/ │ │ │ ├── overview.mdx │ │ │ └── spring.mdx │ │ ├── js/ │ │ │ ├── client/ │ │ │ │ ├── abstract-agent.mdx │ │ │ │ ├── compaction.mdx │ │ │ │ ├── http-agent.mdx │ │ │ │ ├── middleware.mdx │ │ │ │ ├── overview.mdx │ │ │ │ └── subscriber.mdx │ │ │ ├── core/ │ │ │ │ ├── events.mdx │ │ │ │ ├── overview.mdx │ │ │ │ └── types.mdx │ │ │ ├── encoder.mdx │ │ │ ├── overview.mdx │ │ │ └── proto.mdx │ │ ├── kotlin/ │ │ │ ├── client/ │ │ │ │ ├── abstract-agent.mdx │ │ │ │ ├── agui-agent.mdx │ │ │ │ ├── http-agent.mdx │ │ │ │ ├── overview.mdx │ │ │ │ └── stateful-agui-agent.mdx │ │ │ ├── core/ │ │ │ │ ├── events.mdx │ │ │ │ ├── overview.mdx │ │ │ │ └── types.mdx │ │ │ ├── overview.mdx │ │ │ └── tools/ │ │ │ ├── overview.mdx │ │ │ ├── tool-executor.mdx │ │ │ └── tool-registry.mdx │ │ ├── python/ │ │ │ ├── core/ │ │ │ │ ├── events.mdx │ │ │ │ ├── overview.mdx │ │ │ │ └── types.mdx │ │ │ └── encoder/ │ │ │ └── overview.mdx │ │ ├── ruby/ │ │ │ ├── core/ │ │ │ │ ├── events.mdx │ │ │ │ ├── overview.mdx │ │ │ │ └── types.mdx │ │ │ ├── encoder/ │ │ │ │ └── overview.mdx │ │ │ └── overview.mdx │ │ └── rust/ │ │ ├── client/ │ │ │ ├── agent-trait.mdx │ │ │ ├── http-agent.mdx │ │ │ ├── overview.mdx │ │ │ └── subscriber.mdx │ │ ├── core/ │ │ │ ├── events.mdx │ │ │ ├── overview.mdx │ │ │ └── types.mdx │ │ └── overview.mdx │ ├── snippets/ │ │ └── snippet-intro.mdx │ └── tutorials/ │ ├── cursor.mdx │ └── debugging.mdx ├── integrations/ │ ├── a2a/ │ │ └── typescript/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __tests__/ │ │ │ │ ├── agent.test.ts │ │ │ │ └── utils.test.ts │ │ │ ├── agent.ts │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ ├── adk-middleware/ │ │ ├── python/ │ │ │ ├── .gitignore │ │ │ ├── ARCHITECTURE.md │ │ │ ├── CHANGELOG.md │ │ │ ├── CLAUDE.md │ │ │ ├── CONFIGURATION.md │ │ │ ├── LOGGING.md │ │ │ ├── README.md │ │ │ ├── STREAMING_FC_ARGS_RECONSTRUCTION.md │ │ │ ├── TOOLS.md │ │ │ ├── USAGE.md │ │ │ ├── examples/ │ │ │ │ ├── README.md │ │ │ │ ├── other/ │ │ │ │ │ ├── complete_setup.py │ │ │ │ │ ├── configure_adk_agent.py │ │ │ │ │ ├── context_usage.py │ │ │ │ │ └── simple_agent.py │ │ │ │ ├── pyproject.toml │ │ │ │ └── server/ │ │ │ │ ├── __init__.py │ │ │ │ └── api/ │ │ │ │ ├── __init__.py │ │ │ │ ├── agentic_chat.py │ │ │ │ ├── agentic_generative_ui.py │ │ │ │ ├── backend_tool_rendering.py │ │ │ │ ├── human_in_the_loop.py │ │ │ │ ├── predictive_state_updates.py │ │ │ │ ├── shared_state.py │ │ │ │ └── tool_based_generative_ui.py │ │ │ ├── pyproject.toml │ │ │ ├── pytest.ini │ │ │ ├── src/ │ │ │ │ └── ag_ui_adk/ │ │ │ │ ├── __init__.py │ │ │ │ ├── adk_agent.py │ │ │ │ ├── agui_toolset.py │ │ │ │ ├── client_proxy_tool.py │ │ │ │ ├── client_proxy_toolset.py │ │ │ │ ├── config.py │ │ │ │ ├── endpoint.py │ │ │ │ ├── event_translator.py │ │ │ │ ├── execution_state.py │ │ │ │ ├── session_manager.py │ │ │ │ └── utils/ │ │ │ │ ├── __init__.py │ │ │ │ └── converters.py │ │ │ └── tests/ │ │ │ ├── __init__.py │ │ │ ├── conftest.py │ │ │ ├── run_all_tests.sh │ │ │ ├── server_setup.py │ │ │ ├── test_adk_agent.py │ │ │ ├── test_adk_agent_memory_integration.py │ │ │ ├── test_adk_llm_flow_tool_override.py │ │ │ ├── test_app_name_extractor.py │ │ │ ├── test_chunk_event.py │ │ │ ├── test_claude_streaming.py │ │ │ ├── test_client_proxy_tool.py │ │ │ ├── test_client_proxy_toolset.py │ │ │ ├── test_concurrency.py │ │ │ ├── test_concurrent_limits.py │ │ │ ├── test_context_handling.py │ │ │ ├── test_context_integration.py │ │ │ ├── test_credential_service_defaults.py │ │ │ ├── test_duplicate_function_response.py │ │ │ ├── test_endpoint.py │ │ │ ├── test_endpoint_error_handling.py │ │ │ ├── test_event_bookending.py │ │ │ ├── test_event_translator_comprehensive.py │ │ │ ├── test_execution_state.py │ │ │ ├── test_from_app_integration.py │ │ │ ├── test_hitl_resumption_text_output.py │ │ │ ├── test_integration.py │ │ │ ├── test_integration_mixed_partials.py │ │ │ ├── test_issue_437_skip_summarization_integration.py │ │ │ ├── test_lro_filtering.py │ │ │ ├── test_lro_sse_id_remap.py │ │ │ ├── test_lro_sse_persistence.py │ │ │ ├── test_lro_tool_response_persistence.py │ │ │ ├── test_message_history.py │ │ │ ├── test_multi_turn_conversation.py │ │ │ ├── test_non_streaming_text_with_lro_tool.py │ │ │ ├── test_predictive_state.py │ │ │ ├── test_resumability_config.py │ │ │ ├── test_sequential_agent_hitl_resumption.py │ │ │ ├── test_session_cleanup.py │ │ │ ├── test_session_creation.py │ │ │ ├── test_session_deletion.py │ │ │ ├── test_session_memory.py │ │ │ ├── test_skip_summarization.py │ │ │ ├── test_stale_session_invocation_id.py │ │ │ ├── test_streaming.py │ │ │ ├── test_streaming_fc_args.py │ │ │ ├── test_text_events.py │ │ │ ├── test_thought_to_thinking_integration.py │ │ │ ├── test_tool_error_handling.py │ │ │ ├── test_tool_result_flow.py │ │ │ ├── test_tool_tracking_hitl.py │ │ │ ├── test_use_thread_id_as_session_id.py │ │ │ ├── test_user_id_extractor.py │ │ │ ├── test_utils_converters.py │ │ │ ├── test_utils_init.py │ │ │ └── test_vertex_session_service.py │ │ └── typescript/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsdown.config.ts │ ├── ag2/ │ │ ├── python/ │ │ │ ├── .gitignore │ │ │ └── examples/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── pyproject.toml │ │ │ └── server/ │ │ │ ├── __init__.py │ │ │ └── api/ │ │ │ ├── __init__.py │ │ │ ├── agentic_chat.py │ │ │ ├── agentic_generative_ui.py │ │ │ ├── backend_tool_rendering.py │ │ │ ├── human_in_the_loop.py │ │ │ ├── shared_state.py │ │ │ └── tool_based_generative_ui.py │ │ └── typescript/ │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ ├── agent-spec/ │ │ └── python/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── ag_ui_agentspec/ │ │ │ ├── __init__.py │ │ │ ├── agent.py │ │ │ ├── agentspec_tracing_exporter.py │ │ │ ├── agentspecloader.py │ │ │ ├── endpoint.py │ │ │ └── runtimes/ │ │ │ ├── __init__.py │ │ │ ├── langgraph_runner.py │ │ │ └── wayflow_runner.py │ │ ├── examples/ │ │ │ ├── README.md │ │ │ ├── pyproject.toml │ │ │ └── server/ │ │ │ ├── __init__.py │ │ │ └── api/ │ │ │ ├── A2UI_PROMPT.txt │ │ │ ├── __init__.py │ │ │ ├── a2ui_chat.py │ │ │ ├── agentic_chat.py │ │ │ ├── backend_tool_rendering.py │ │ │ ├── human_in_the_loop.py │ │ │ ├── routes.py │ │ │ └── tool_based_generative_ui.py │ │ └── pyproject.toml │ ├── agno/ │ │ ├── python/ │ │ │ ├── .gitignore │ │ │ └── examples/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── pyproject.toml │ │ │ ├── requirements.txt │ │ │ └── server/ │ │ │ ├── __init__.py │ │ │ └── api/ │ │ │ ├── __init__.py │ │ │ ├── agentic_chat.py │ │ │ ├── backend_tool_rendering.py │ │ │ ├── human_in_the_loop.py │ │ │ └── tool_based_generative_ui.py │ │ └── typescript/ │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ ├── aws-strands/ │ │ ├── ARCHITECTURE.md │ │ ├── python/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── examples/ │ │ │ │ ├── .gitignore │ │ │ │ ├── README.md │ │ │ │ ├── pyproject.toml │ │ │ │ └── server/ │ │ │ │ ├── __init__.py │ │ │ │ ├── __main__.py │ │ │ │ └── api/ │ │ │ │ ├── __init__.py │ │ │ │ ├── agentic_chat.py │ │ │ │ ├── agentic_generative_ui.py │ │ │ │ ├── backend_tool_rendering.py │ │ │ │ ├── human_in_the_loop.py │ │ │ │ └── shared_state.py │ │ │ ├── pyproject.toml │ │ │ ├── src/ │ │ │ │ └── ag_ui_strands/ │ │ │ │ ├── __init__.py │ │ │ │ ├── agent.py │ │ │ │ ├── client_proxy_tool.py │ │ │ │ ├── config.py │ │ │ │ ├── endpoint.py │ │ │ │ ├── types.py │ │ │ │ └── utils.py │ │ │ └── tests/ │ │ │ ├── __init__.py │ │ │ └── test_client_proxy_tool.py │ │ └── typescript/ │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ ├── claude-agent-sdk/ │ │ ├── .gitignore │ │ ├── python/ │ │ │ ├── README.md │ │ │ ├── ag_ui_claude_sdk/ │ │ │ │ ├── __init__.py │ │ │ │ ├── adapter.py │ │ │ │ ├── config.py │ │ │ │ ├── endpoint.py │ │ │ │ ├── handlers.py │ │ │ │ ├── session.py │ │ │ │ ├── types.py │ │ │ │ └── utils.py │ │ │ ├── examples/ │ │ │ │ ├── README.md │ │ │ │ ├── agents/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── agentic_chat.py │ │ │ │ │ ├── backend_tool_rendering.py │ │ │ │ │ ├── constants.py │ │ │ │ │ ├── human_in_the_loop.py │ │ │ │ │ ├── shared_state.py │ │ │ │ │ └── tool_based_generative_ui.py │ │ │ │ ├── pyproject.toml │ │ │ │ └── server.py │ │ │ └── pyproject.toml │ │ └── typescript/ │ │ ├── README.md │ │ ├── examples/ │ │ │ ├── agentic_chat.ts │ │ │ ├── backend_tool_rendering.ts │ │ │ ├── constants.ts │ │ │ ├── human_in_the_loop.ts │ │ │ ├── server.ts │ │ │ ├── shared_state.ts │ │ │ └── tool_based_generative_ui.ts │ │ ├── package.json │ │ ├── src/ │ │ │ ├── adapter.ts │ │ │ ├── config.ts │ │ │ ├── handlers.ts │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsup.config.ts │ │ └── vitest.config.ts │ ├── community/ │ │ ├── genkit/ │ │ │ └── go/ │ │ │ ├── README.md │ │ │ ├── examples/ │ │ │ │ ├── README.md │ │ │ │ ├── cmd/ │ │ │ │ │ └── server/ │ │ │ │ │ └── main.go │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ ├── internal/ │ │ │ │ │ ├── agents/ │ │ │ │ │ │ ├── agentic_chat/ │ │ │ │ │ │ │ └── agent.go │ │ │ │ │ │ └── registry.go │ │ │ │ │ ├── config/ │ │ │ │ │ │ └── config.go │ │ │ │ │ └── handlers/ │ │ │ │ │ └── agent.go │ │ │ │ └── mock/ │ │ │ │ └── mock.go │ │ │ └── genkit/ │ │ │ ├── genkit.go │ │ │ ├── genkit_test.go │ │ │ ├── go.mod │ │ │ └── go.sum │ │ └── spring-ai/ │ │ └── typescript/ │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ ├── crew-ai/ │ │ ├── python/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── ag_ui_crewai/ │ │ │ │ ├── __init__.py │ │ │ │ ├── context.py │ │ │ │ ├── crews.py │ │ │ │ ├── dojo.py │ │ │ │ ├── endpoint.py │ │ │ │ ├── enterprise.py │ │ │ │ ├── events.py │ │ │ │ ├── examples/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── agentic_chat.py │ │ │ │ │ ├── agentic_generative_ui.py │ │ │ │ │ ├── human_in_the_loop.py │ │ │ │ │ ├── predictive_state_updates.py │ │ │ │ │ ├── shared_state.py │ │ │ │ │ └── tool_based_generative_ui.py │ │ │ │ ├── sdk.py │ │ │ │ └── utils.py │ │ │ ├── pyproject.toml │ │ │ └── tests/ │ │ │ └── __init__.py │ │ └── typescript/ │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ ├── langchain/ │ │ └── typescript/ │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── agent.ts │ │ │ ├── index.ts │ │ │ ├── messages.ts │ │ │ ├── streaming.ts │ │ │ └── tools.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ ├── langgraph/ │ │ ├── python/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── ag_ui_langgraph/ │ │ │ │ ├── __init__.py │ │ │ │ ├── agent.py │ │ │ │ ├── endpoint.py │ │ │ │ ├── middlewares/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── state_streaming.py │ │ │ │ ├── types.py │ │ │ │ └── utils.py │ │ │ ├── examples/ │ │ │ │ ├── .gitignore │ │ │ │ ├── README.md │ │ │ │ ├── agents/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── a2ui_chat/ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── agent.py │ │ │ │ │ │ └── prompt.py │ │ │ │ │ ├── agentic_chat/ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ └── agent.py │ │ │ │ │ ├── agentic_chat_reasoning/ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ └── agent.py │ │ │ │ │ ├── agentic_generative_ui/ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ └── agent.py │ │ │ │ │ ├── backend_tool_rendering/ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ └── agent.py │ │ │ │ │ ├── dojo.py │ │ │ │ │ ├── human_in_the_loop/ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ └── agent.py │ │ │ │ │ ├── multimodal_messages/ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ └── agent.py │ │ │ │ │ ├── predictive_state_updates/ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ └── agent.py │ │ │ │ │ ├── shared_state/ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ └── agent.py │ │ │ │ │ ├── subgraphs/ │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ └── agent.py │ │ │ │ │ └── tool_based_generative_ui/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── agent.py │ │ │ │ ├── langgraph.json │ │ │ │ └── pyproject.toml │ │ │ ├── pyproject.toml │ │ │ └── tests/ │ │ │ ├── __init__.py │ │ │ ├── test_make_json_safe.py │ │ │ ├── test_multimodal.py │ │ │ └── test_state_streaming_middleware.py │ │ └── typescript/ │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── README.md │ │ ├── examples/ │ │ │ ├── .env.example │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── langgraph.json │ │ │ ├── package.json │ │ │ ├── pnpm-workspace.yaml │ │ │ ├── src/ │ │ │ │ └── agents/ │ │ │ │ ├── agentic_chat/ │ │ │ │ │ └── agent.ts │ │ │ │ ├── agentic_generative_ui/ │ │ │ │ │ └── agent.ts │ │ │ │ ├── backend_tool_rendering/ │ │ │ │ │ └── agent.ts │ │ │ │ ├── human_in_the_loop/ │ │ │ │ │ └── agent.ts │ │ │ │ ├── multimodal_messages/ │ │ │ │ │ └── agent.ts │ │ │ │ ├── predictive_state_updates/ │ │ │ │ │ └── agent.ts │ │ │ │ ├── shared_state/ │ │ │ │ │ └── agent.ts │ │ │ │ ├── subgraphs/ │ │ │ │ │ └── agent.ts │ │ │ │ └── tool_based_generative_ui/ │ │ │ │ └── agent.ts │ │ │ └── tsconfig.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── agent.ts │ │ │ ├── index.ts │ │ │ ├── messages-tuple.test.ts │ │ │ ├── middlewares/ │ │ │ │ ├── index.ts │ │ │ │ ├── state-streaming.test.ts │ │ │ │ └── state-streaming.ts │ │ │ ├── types.ts │ │ │ ├── utils.test.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ ├── langroid/ │ │ ├── ARCHITECTURE.md │ │ ├── README.md │ │ ├── python/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── examples/ │ │ │ │ ├── .gitignore │ │ │ │ ├── README.md │ │ │ │ ├── pyproject.toml │ │ │ │ └── server/ │ │ │ │ ├── __init__.py │ │ │ │ ├── __main__.py │ │ │ │ └── api/ │ │ │ │ ├── __init__.py │ │ │ │ ├── agentic_chat.py │ │ │ │ ├── agentic_generative_ui.py │ │ │ │ ├── backend_tool_rendering.py │ │ │ │ └── shared_state.py │ │ │ ├── pyproject.toml │ │ │ ├── src/ │ │ │ │ └── ag_ui_langroid/ │ │ │ │ ├── __init__.py │ │ │ │ ├── agent.py │ │ │ │ ├── endpoint.py │ │ │ │ ├── types.py │ │ │ │ └── utils.py │ │ │ └── tests/ │ │ │ ├── __init__.py │ │ │ ├── test_agent.py │ │ │ ├── test_endpoint.py │ │ │ └── test_types.py │ │ └── typescript/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── llama-index/ │ │ ├── python/ │ │ │ ├── .gitignore │ │ │ └── examples/ │ │ │ ├── README.md │ │ │ ├── pyproject.toml │ │ │ └── server/ │ │ │ ├── __init__.py │ │ │ └── routers/ │ │ │ ├── agentic_chat.py │ │ │ ├── agentic_generative_ui.py │ │ │ ├── backend_tool_rendering.py │ │ │ ├── human_in_the_loop.py │ │ │ └── shared_state.py │ │ └── typescript/ │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ ├── mastra/ │ │ └── typescript/ │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── README.md │ │ ├── examples/ │ │ │ ├── .env.example │ │ │ ├── .gitignore │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── mastra/ │ │ │ │ ├── agents/ │ │ │ │ │ ├── agentic-chat.ts │ │ │ │ │ ├── backend-tool-rendering.ts │ │ │ │ │ ├── human-in-the-loop.ts │ │ │ │ │ └── tool-based-generative-ui.ts │ │ │ │ ├── index.ts │ │ │ │ └── tools/ │ │ │ │ └── weather-tool.ts │ │ │ └── tsconfig.json │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __tests__/ │ │ │ │ ├── edge-cases.test.ts │ │ │ │ ├── helpers.ts │ │ │ │ ├── integration.test.ts │ │ │ │ └── message-conversion.test.ts │ │ │ ├── copilotkit.ts │ │ │ ├── index.ts │ │ │ ├── mastra.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ ├── microsoft-agent-framework/ │ │ ├── dotnet/ │ │ │ └── examples/ │ │ │ ├── .gitignore │ │ │ ├── AGUIDojoServer/ │ │ │ │ ├── .dockerignore │ │ │ │ ├── AGUIDojoServer.csproj │ │ │ │ ├── AGUIDojoServerSerializerContext.cs │ │ │ │ ├── AgenticUI/ │ │ │ │ │ ├── AgenticPlanningTools.cs │ │ │ │ │ ├── AgenticUIAgent.cs │ │ │ │ │ ├── JsonPatchOperation.cs │ │ │ │ │ ├── Plan.cs │ │ │ │ │ ├── Step.cs │ │ │ │ │ └── StepStatus.cs │ │ │ │ ├── BackendToolRendering/ │ │ │ │ │ └── WeatherInfo.cs │ │ │ │ ├── ChatClientAgentFactory.cs │ │ │ │ ├── Dockerfile │ │ │ │ ├── PredictiveStateUpdates/ │ │ │ │ │ ├── DocumentState.cs │ │ │ │ │ └── PredictiveStateUpdatesAgent.cs │ │ │ │ ├── Program.cs │ │ │ │ ├── Properties/ │ │ │ │ │ └── launchSettings.json │ │ │ │ └── SharedState/ │ │ │ │ ├── Ingredient.cs │ │ │ │ ├── Recipe.cs │ │ │ │ ├── RecipeResponse.cs │ │ │ │ └── SharedStateAgent.cs │ │ │ └── README.md │ │ └── python/ │ │ └── examples/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── agents/ │ │ │ ├── __init__.py │ │ │ └── dojo.py │ │ └── pyproject.toml │ ├── pydantic-ai/ │ │ ├── python/ │ │ │ ├── .gitignore │ │ │ └── examples/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── pyproject.toml │ │ │ └── server/ │ │ │ ├── __init__.py │ │ │ └── api/ │ │ │ ├── __init__.py │ │ │ ├── agentic_chat.py │ │ │ ├── agentic_generative_ui.py │ │ │ ├── backend_tool_rendering.py │ │ │ ├── human_in_the_loop.py │ │ │ ├── predictive_state_updates.py │ │ │ ├── shared_state.py │ │ │ └── tool_based_generative_ui.py │ │ └── typescript/ │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ ├── server-starter/ │ │ ├── python/ │ │ │ ├── .gitignore │ │ │ └── examples/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── example_server/ │ │ │ │ └── __init__.py │ │ │ ├── pyproject.toml │ │ │ └── tests/ │ │ │ └── __init__.py │ │ └── typescript/ │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ ├── server-starter-all-features/ │ │ ├── python/ │ │ │ ├── .gitignore │ │ │ └── examples/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── example_server/ │ │ │ │ ├── __init__.py │ │ │ │ ├── agentic_chat.py │ │ │ │ ├── agentic_generative_ui.py │ │ │ │ ├── backend_tool_rendering.py │ │ │ │ ├── human_in_the_loop.py │ │ │ │ ├── predictive_state_updates.py │ │ │ │ ├── shared_state.py │ │ │ │ └── tool_based_generative_ui.py │ │ │ ├── pyproject.toml │ │ │ └── tests/ │ │ │ └── __init__.py │ │ └── typescript/ │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ └── vercel-ai-sdk/ │ └── typescript/ │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── package.json │ ├── src/ │ │ └── index.ts │ ├── tsconfig.json │ ├── tsdown.config.ts │ └── vitest.config.ts ├── lefthook.yml ├── middlewares/ │ ├── a2a-middleware/ │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── README.md │ │ ├── examples/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── buildings_management.py │ │ │ ├── finance.py │ │ │ ├── it.py │ │ │ ├── orchestrator.py │ │ │ └── pyproject.toml │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ ├── a2ui-middleware/ │ │ ├── .gitignore │ │ ├── __tests__/ │ │ │ └── a2ui-middleware.test.ts │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ ├── schema.ts │ │ │ ├── tools.ts │ │ │ └── types.ts │ │ ├── tsconfig.json │ │ ├── tsup.config.ts │ │ └── vitest.config.ts │ ├── mcp-apps-middleware/ │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── README.md │ │ ├── __tests__/ │ │ │ ├── mcp-apps-middleware.test.ts │ │ │ └── test-utils.ts │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ └── middleware-starter/ │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── package.json │ ├── src/ │ │ └── index.ts │ ├── tsconfig.json │ ├── tsdown.config.ts │ └── vitest.config.ts ├── nx.json ├── package.json ├── pnpm-workspace.yaml ├── render.yaml ├── scripts/ │ ├── check-codeowners-auth.ts │ └── rewrite-python-preview-versions.py └── sdks/ ├── community/ │ ├── dart/ │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── TEST_GUIDE.md │ │ ├── analysis_options.yaml │ │ ├── example/ │ │ │ ├── README.md │ │ │ ├── analysis_options.yaml │ │ │ ├── dart_output_with_tools.json │ │ │ ├── dart_output_with_tools_fixed.json │ │ │ └── pubspec.yaml │ │ ├── lib/ │ │ │ ├── ag_ui.dart │ │ │ └── src/ │ │ │ ├── client/ │ │ │ │ ├── client.dart │ │ │ │ ├── config.dart │ │ │ │ ├── errors.dart │ │ │ │ └── validators.dart │ │ │ ├── encoder/ │ │ │ │ ├── client_codec.dart │ │ │ │ ├── decoder.dart │ │ │ │ ├── encoder.dart │ │ │ │ ├── errors.dart │ │ │ │ └── stream_adapter.dart │ │ │ ├── events/ │ │ │ │ ├── event_type.dart │ │ │ │ └── events.dart │ │ │ ├── sse/ │ │ │ │ ├── backoff_strategy.dart │ │ │ │ ├── sse_client.dart │ │ │ │ ├── sse_message.dart │ │ │ │ └── sse_parser.dart │ │ │ └── types/ │ │ │ ├── base.dart │ │ │ ├── context.dart │ │ │ ├── message.dart │ │ │ ├── tool.dart │ │ │ └── types.dart │ │ ├── pubspec.yaml │ │ └── test/ │ │ ├── ag_ui_test.dart │ │ ├── client/ │ │ │ ├── client_test.dart │ │ │ ├── config_test.dart │ │ │ ├── config_test.dill │ │ │ ├── errors_test.dart │ │ │ ├── http_endpoints_test.dart │ │ │ └── validators_test.dart │ │ ├── encoder/ │ │ │ ├── client_codec_test.dart │ │ │ ├── decoder_test.dart │ │ │ ├── encoder_test.dart │ │ │ ├── errors_test.dart │ │ │ └── stream_adapter_test.dart │ │ ├── events/ │ │ │ ├── event_test.dart │ │ │ └── event_type_test.dart │ │ ├── fixtures/ │ │ │ ├── events.json │ │ │ └── sse_streams.txt │ │ ├── integration/ │ │ │ ├── event_decoding_integration_test.dart │ │ │ ├── fixtures_integration_test.dart │ │ │ └── helpers/ │ │ │ └── test_helpers.dart │ │ ├── sse/ │ │ │ ├── backoff_strategy_test.dart │ │ │ ├── sse_client_basic_test.dart │ │ │ ├── sse_client_stream_test.dart │ │ │ ├── sse_client_test.dart.skip │ │ │ ├── sse_message_test.dart │ │ │ └── sse_parser_test.dart │ │ └── types/ │ │ ├── base_test.dart │ │ ├── message_test.dart │ │ └── tool_context_test.dart │ ├── go/ │ │ ├── .gitignore │ │ ├── example/ │ │ │ ├── client/ │ │ │ │ ├── cmd/ │ │ │ │ │ └── main.go │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ └── internal/ │ │ │ │ ├── agent/ │ │ │ │ │ └── chat.go │ │ │ │ ├── event/ │ │ │ │ │ └── parse.go │ │ │ │ ├── message/ │ │ │ │ │ └── message.go │ │ │ │ └── ui/ │ │ │ │ ├── model.go │ │ │ │ ├── splash.go │ │ │ │ ├── theme.go │ │ │ │ └── typing.go │ │ │ └── server/ │ │ │ ├── cmd/ │ │ │ │ └── main.go │ │ │ ├── go.mod │ │ │ ├── go.sum │ │ │ └── internal/ │ │ │ ├── agentic/ │ │ │ │ ├── agentic.go │ │ │ │ ├── agentic_integration_test.go │ │ │ │ ├── data/ │ │ │ │ │ ├── languages_prompt.md │ │ │ │ │ └── reminder.md │ │ │ │ └── handler.go │ │ │ ├── config/ │ │ │ │ └── config.go │ │ │ ├── mcp/ │ │ │ │ ├── adapter.go │ │ │ │ └── server.go │ │ │ └── routes/ │ │ │ └── routes.go │ │ ├── go.mod │ │ ├── go.sum │ │ └── pkg/ │ │ ├── client/ │ │ │ └── sse/ │ │ │ ├── client.go │ │ │ ├── client_stream_test.go │ │ │ └── client_test.go │ │ ├── core/ │ │ │ ├── events/ │ │ │ │ ├── activity_events.go │ │ │ │ ├── activity_events_test.go │ │ │ │ ├── additional_events_test.go │ │ │ │ ├── custom_events.go │ │ │ │ ├── decoder.go │ │ │ │ ├── decoder_test.go │ │ │ │ ├── events.go │ │ │ │ ├── events_test.go │ │ │ │ ├── id_utils.go │ │ │ │ ├── id_utils_test.go │ │ │ │ ├── message_events.go │ │ │ │ ├── reasoning_events.go │ │ │ │ ├── run_events.go │ │ │ │ ├── state_events.go │ │ │ │ ├── state_events_test.go │ │ │ │ ├── thinking_events.go │ │ │ │ ├── thinking_events_test.go │ │ │ │ └── tool_events.go │ │ │ └── types/ │ │ │ ├── message_helpers.go │ │ │ ├── types.go │ │ │ └── types_test.go │ │ ├── encoding/ │ │ │ ├── buffer_sizing.go │ │ │ ├── encoder/ │ │ │ │ └── encoder.go │ │ │ ├── errors.go │ │ │ ├── interface.go │ │ │ ├── json/ │ │ │ │ ├── json.go │ │ │ │ ├── json_codec.go │ │ │ │ ├── json_decoder.go │ │ │ │ └── json_encoder.go │ │ │ ├── negotiation/ │ │ │ │ ├── negotiator.go │ │ │ │ ├── negotiator_test.go │ │ │ │ ├── parser.go │ │ │ │ └── selector.go │ │ │ ├── pool.go │ │ │ └── sse/ │ │ │ ├── writer.go │ │ │ └── writer_test.go │ │ └── errors/ │ │ ├── error_types.go │ │ └── error_utils.go │ ├── java/ │ │ ├── .gitignore │ │ ├── CLAUDE.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── clients/ │ │ │ ├── ok-http/ │ │ │ │ ├── README.md │ │ │ │ ├── pom.xml │ │ │ │ └── src/ │ │ │ │ ├── main/ │ │ │ │ │ └── java/ │ │ │ │ │ └── com/ │ │ │ │ │ └── agui/ │ │ │ │ │ └── okhttp/ │ │ │ │ │ └── HttpClient.java │ │ │ │ └── test/ │ │ │ │ └── java/ │ │ │ │ └── com/ │ │ │ │ └── agui/ │ │ │ │ └── okhttp/ │ │ │ │ └── HttpClientTest.java │ │ │ └── spring-client/ │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── agui/ │ │ │ └── spring/ │ │ │ └── HttpClient.java │ │ ├── examples/ │ │ │ ├── copilot-app/ │ │ │ │ ├── .gitignore │ │ │ │ ├── README.md │ │ │ │ ├── eslint.config.mjs │ │ │ │ ├── next.config.ts │ │ │ │ ├── package.json │ │ │ │ ├── postcss.config.mjs │ │ │ │ ├── src/ │ │ │ │ │ └── app/ │ │ │ │ │ ├── api/ │ │ │ │ │ │ └── copilotkit/ │ │ │ │ │ │ └── route.ts │ │ │ │ │ ├── globals.css │ │ │ │ │ ├── layout.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── tsconfig.json │ │ │ └── spring-ai-example/ │ │ │ ├── .gitignore │ │ │ ├── Dockerfile │ │ │ ├── docker-compose.yml │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ └── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── agui/ │ │ │ └── example/ │ │ │ ├── AgUiController.java │ │ │ ├── Application.java │ │ │ ├── config/ │ │ │ │ ├── AgUiConfig.java │ │ │ │ └── CorsConfig.java │ │ │ └── tools/ │ │ │ ├── GeocodingResponse.java │ │ │ ├── WeatherRequest.java │ │ │ ├── WeatherResponse.java │ │ │ ├── WeatherTool.java │ │ │ └── WeatherToolResult.java │ │ ├── integrations/ │ │ │ └── spring-ai/ │ │ │ ├── README.md │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ ├── main/ │ │ │ │ └── java/ │ │ │ │ └── com/ │ │ │ │ └── agui/ │ │ │ │ └── spring/ │ │ │ │ └── ai/ │ │ │ │ ├── AgUiFunctionToolCallback.java │ │ │ │ ├── AgUiToolCallbackParams.java │ │ │ │ ├── SpringAIAgent.java │ │ │ │ ├── StateTool.java │ │ │ │ └── ToolMapper.java │ │ │ └── test/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── agui/ │ │ │ └── spring/ │ │ │ └── ai/ │ │ │ └── ToolMapperTest.java │ │ ├── packages/ │ │ │ ├── README.md │ │ │ ├── client/ │ │ │ │ ├── README.md │ │ │ │ ├── pom.xml │ │ │ │ └── src/ │ │ │ │ ├── main/ │ │ │ │ │ └── java/ │ │ │ │ │ └── com/ │ │ │ │ │ └── agui/ │ │ │ │ │ └── client/ │ │ │ │ │ ├── agent/ │ │ │ │ │ │ └── AbstractAgent.java │ │ │ │ │ ├── message/ │ │ │ │ │ │ └── MessageFactory.java │ │ │ │ │ └── verifier/ │ │ │ │ │ └── EventVerifier.java │ │ │ │ └── test/ │ │ │ │ └── java/ │ │ │ │ └── com/ │ │ │ │ └── agui/ │ │ │ │ └── client/ │ │ │ │ ├── agent/ │ │ │ │ │ └── AbstractAgentTest.java │ │ │ │ ├── message/ │ │ │ │ │ └── MessageFactoryTest.java │ │ │ │ └── verifier/ │ │ │ │ └── EventVerifierTest.java │ │ │ ├── core/ │ │ │ │ ├── README.md │ │ │ │ ├── pom.xml │ │ │ │ └── src/ │ │ │ │ ├── main/ │ │ │ │ │ └── java/ │ │ │ │ │ └── com/ │ │ │ │ │ └── agui/ │ │ │ │ │ └── core/ │ │ │ │ │ ├── agent/ │ │ │ │ │ │ ├── Agent.java │ │ │ │ │ │ ├── AgentSubscriber.java │ │ │ │ │ │ ├── AgentSubscriberParams.java │ │ │ │ │ │ ├── RunAgentInput.java │ │ │ │ │ │ └── RunAgentParameters.java │ │ │ │ │ ├── context/ │ │ │ │ │ │ └── Context.java │ │ │ │ │ ├── event/ │ │ │ │ │ │ ├── BaseEvent.java │ │ │ │ │ │ ├── CustomEvent.java │ │ │ │ │ │ ├── MessagesSnapshotEvent.java │ │ │ │ │ │ ├── RawEvent.java │ │ │ │ │ │ ├── RunErrorEvent.java │ │ │ │ │ │ ├── RunFinishedEvent.java │ │ │ │ │ │ ├── RunStartedEvent.java │ │ │ │ │ │ ├── StateDeltaEvent.java │ │ │ │ │ │ ├── StateSnapshotEvent.java │ │ │ │ │ │ ├── StepFinishedEvent.java │ │ │ │ │ │ ├── StepStartedEvent.java │ │ │ │ │ │ ├── TextMessageChunkEvent.java │ │ │ │ │ │ ├── TextMessageContentEvent.java │ │ │ │ │ │ ├── TextMessageEndEvent.java │ │ │ │ │ │ ├── TextMessageStartEvent.java │ │ │ │ │ │ ├── ThinkingEndEvent.java │ │ │ │ │ │ ├── ThinkingStartEvent.java │ │ │ │ │ │ ├── ThinkingTextMessageContentEvent.java │ │ │ │ │ │ ├── ThinkingTextMessageEndEvent.java │ │ │ │ │ │ ├── ThinkingTextMessageStartEvent.java │ │ │ │ │ │ ├── ToolCallArgsEvent.java │ │ │ │ │ │ ├── ToolCallChunkEvent.java │ │ │ │ │ │ ├── ToolCallEndEvent.java │ │ │ │ │ │ ├── ToolCallResultEvent.java │ │ │ │ │ │ └── ToolCallStartEvent.java │ │ │ │ │ ├── exception/ │ │ │ │ │ │ └── AGUIException.java │ │ │ │ │ ├── function/ │ │ │ │ │ │ └── FunctionCall.java │ │ │ │ │ ├── message/ │ │ │ │ │ │ ├── AssistantMessage.java │ │ │ │ │ │ ├── BaseMessage.java │ │ │ │ │ │ ├── DeveloperMessage.java │ │ │ │ │ │ ├── Role.java │ │ │ │ │ │ ├── SystemMessage.java │ │ │ │ │ │ ├── ToolMessage.java │ │ │ │ │ │ └── UserMessage.java │ │ │ │ │ ├── state/ │ │ │ │ │ │ └── State.java │ │ │ │ │ ├── stream/ │ │ │ │ │ │ ├── EventStream.java │ │ │ │ │ │ └── IEventStream.java │ │ │ │ │ ├── subscription/ │ │ │ │ │ │ └── Subscription.java │ │ │ │ │ ├── tool/ │ │ │ │ │ │ ├── Tool.java │ │ │ │ │ │ └── ToolCall.java │ │ │ │ │ └── type/ │ │ │ │ │ └── EventType.java │ │ │ │ └── test/ │ │ │ │ └── java/ │ │ │ │ └── com/ │ │ │ │ └── agui/ │ │ │ │ └── core/ │ │ │ │ ├── agent/ │ │ │ │ │ ├── AgentSubscriberParamsTest.java │ │ │ │ │ ├── AgentSubscriberTest.java │ │ │ │ │ ├── RunAgentInputTest.java │ │ │ │ │ └── RunAgentParametersTest.java │ │ │ │ ├── context/ │ │ │ │ │ └── ContextTest.java │ │ │ │ ├── event/ │ │ │ │ │ ├── BaseEventTest.java │ │ │ │ │ ├── CustomEventTest.java │ │ │ │ │ ├── MessagesSnapshotEventTest.java │ │ │ │ │ ├── RawEventTest.java │ │ │ │ │ ├── RunErrorEventTest.java │ │ │ │ │ ├── RunFinishedEventTest.java │ │ │ │ │ ├── RunStartedEventTest.java │ │ │ │ │ ├── StateDeltaEventTest.java │ │ │ │ │ ├── StateSnapshotEventTest.java │ │ │ │ │ ├── StepFinishedEventTest.java │ │ │ │ │ ├── StepStartedEventTest.java │ │ │ │ │ ├── TextMessageChunkEventTest.java │ │ │ │ │ ├── TextMessageContentEventTest.java │ │ │ │ │ ├── TextMessageEndEventTest.java │ │ │ │ │ ├── TextMessageStartEventTest.java │ │ │ │ │ ├── ThinkingEndEventTest.java │ │ │ │ │ ├── ThinkingStartEventTest.java │ │ │ │ │ ├── ThinkingTextMessageContentEventTest.java │ │ │ │ │ ├── ThinkingTextMessageEndEventTest.java │ │ │ │ │ ├── ThinkingTextMessageStartEventTest.java │ │ │ │ │ ├── ToolCallArgsEventTest.java │ │ │ │ │ ├── ToolCallChunkEventTest.java │ │ │ │ │ ├── ToolCallEndEventTest.java │ │ │ │ │ ├── ToolCallResultEventTest.java │ │ │ │ │ └── ToolCallStartEventTest.java │ │ │ │ ├── exception/ │ │ │ │ │ └── AGUIExceptionTest.java │ │ │ │ ├── function/ │ │ │ │ │ └── FunctionCallTest.java │ │ │ │ ├── message/ │ │ │ │ │ ├── AssistantMessageTest.java │ │ │ │ │ ├── DeveloperMessageTest.java │ │ │ │ │ ├── SystemMessageTest.java │ │ │ │ │ ├── ToolMessageTest.java │ │ │ │ │ └── UserMessageTest.java │ │ │ │ ├── state/ │ │ │ │ │ └── StateTest.java │ │ │ │ ├── stream/ │ │ │ │ │ ├── EventStreamTest.java │ │ │ │ │ └── IEventStreamTest.java │ │ │ │ ├── subscription/ │ │ │ │ │ └── SubscriptionTest.java │ │ │ │ ├── tool/ │ │ │ │ │ ├── ToolCallTest.java │ │ │ │ │ └── ToolTest.java │ │ │ │ └── type/ │ │ │ │ └── EventTypeTest.java │ │ │ ├── http/ │ │ │ │ ├── README.md │ │ │ │ ├── pom.xml │ │ │ │ └── src/ │ │ │ │ ├── main/ │ │ │ │ │ └── java/ │ │ │ │ │ └── com/ │ │ │ │ │ └── agui/ │ │ │ │ │ └── http/ │ │ │ │ │ ├── BaseHttpClient.java │ │ │ │ │ └── HttpAgent.java │ │ │ │ └── test/ │ │ │ │ └── java/ │ │ │ │ └── com/ │ │ │ │ └── agui/ │ │ │ │ └── http/ │ │ │ │ ├── BaseHttpClientTest.java │ │ │ │ └── HttpAgentTest.java │ │ │ └── server/ │ │ │ ├── README.md │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ ├── main/ │ │ │ │ └── java/ │ │ │ │ └── com/ │ │ │ │ └── agui/ │ │ │ │ └── server/ │ │ │ │ ├── EventFactory.java │ │ │ │ ├── LocalAgent.java │ │ │ │ └── streamer/ │ │ │ │ └── AgentStreamer.java │ │ │ └── test/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── agui/ │ │ │ └── server/ │ │ │ ├── EventFactoryTest.java │ │ │ ├── LocalAgentTest.java │ │ │ └── streamer/ │ │ │ └── AgentStreamerTest.java │ │ ├── pom.xml │ │ ├── servers/ │ │ │ └── spring/ │ │ │ ├── README.md │ │ │ ├── pom.xml │ │ │ └── src/ │ │ │ ├── main/ │ │ │ │ ├── java/ │ │ │ │ │ └── com/ │ │ │ │ │ └── agui/ │ │ │ │ │ └── server/ │ │ │ │ │ └── spring/ │ │ │ │ │ ├── AgUiAutoConfiguration.java │ │ │ │ │ ├── AgUiParameters.java │ │ │ │ │ └── AgUiService.java │ │ │ │ └── resources/ │ │ │ │ └── META-INF/ │ │ │ │ ├── spring/ │ │ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ │ │ └── spring.factories │ │ │ └── test/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── agui/ │ │ │ └── server/ │ │ │ └── spring/ │ │ │ └── AgUiParametersTest.java │ │ └── utils/ │ │ └── json/ │ │ ├── README.md │ │ ├── pom.xml │ │ └── src/ │ │ ├── main/ │ │ │ └── java/ │ │ │ └── com/ │ │ │ └── agui/ │ │ │ └── json/ │ │ │ ├── ObjectMapperFactory.java │ │ │ └── mixins/ │ │ │ ├── EventMixin.java │ │ │ ├── MessageMixin.java │ │ │ └── StateMixin.java │ │ └── test/ │ │ └── java/ │ │ └── com/ │ │ └── agui/ │ │ └── json/ │ │ └── ObjectMapperFactoryTest.java │ ├── kotlin/ │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── OVERVIEW.md │ │ ├── PERFORMANCE.md │ │ ├── README.md │ │ ├── build.bat │ │ ├── build.gradle.kts │ │ ├── build.sh │ │ ├── detekt-config.yml │ │ ├── examples/ │ │ │ ├── chatapp/ │ │ │ │ ├── .gitignore │ │ │ │ ├── CHANGELOG.md │ │ │ │ ├── README.md │ │ │ │ ├── androidApp/ │ │ │ │ │ ├── build.gradle.kts │ │ │ │ │ └── src/ │ │ │ │ │ └── main/ │ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ │ ├── assets/ │ │ │ │ │ │ └── logback.xml │ │ │ │ │ └── res/ │ │ │ │ │ ├── drawable/ │ │ │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ │ │ └── ic_launcher_foreground.xml │ │ │ │ │ ├── mipmap-anydpi/ │ │ │ │ │ │ ├── ic_launcher.xml │ │ │ │ │ │ └── ic_launcher_round.xml │ │ │ │ │ └── values/ │ │ │ │ │ └── strings.xml │ │ │ │ ├── build.gradle.kts │ │ │ │ ├── desktopApp/ │ │ │ │ │ └── build.gradle.kts │ │ │ │ ├── gradle/ │ │ │ │ │ ├── libs.versions.toml │ │ │ │ │ └── wrapper/ │ │ │ │ │ ├── gradle-wrapper.jar │ │ │ │ │ └── gradle-wrapper.properties │ │ │ │ ├── gradle.properties │ │ │ │ ├── gradlew │ │ │ │ ├── gradlew.bat │ │ │ │ ├── iosApp/ │ │ │ │ │ ├── README.md │ │ │ │ │ ├── iosApp/ │ │ │ │ │ │ ├── Assets.xcassets/ │ │ │ │ │ │ │ ├── AccentColor.colorset/ │ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ ├── ContentView.swift │ │ │ │ │ │ ├── Info.plist │ │ │ │ │ │ └── iOSApp.swift │ │ │ │ │ └── iosApp.xcodeproj/ │ │ │ │ │ ├── project.pbxproj │ │ │ │ │ └── project.xcworkspace/ │ │ │ │ │ └── contents.xcworkspacedata │ │ │ │ ├── settings.gradle.kts │ │ │ │ ├── shared/ │ │ │ │ │ ├── build.gradle.kts │ │ │ │ │ └── src/ │ │ │ │ │ ├── androidInstrumentedTest/ │ │ │ │ │ │ └── kotlin/ │ │ │ │ │ │ └── com/ │ │ │ │ │ │ └── agui/ │ │ │ │ │ │ └── example/ │ │ │ │ │ │ └── chatapp/ │ │ │ │ │ │ ├── AndroidIntegrationTest.kt │ │ │ │ │ │ ├── AndroidSettingsTest.kt │ │ │ │ │ │ ├── MinimalAndroidTest.kt │ │ │ │ │ │ └── ui/ │ │ │ │ │ │ ├── MessageBubbleTest.kt │ │ │ │ │ │ └── components/ │ │ │ │ │ │ └── MessageBubbleComponentTest.kt │ │ │ │ │ ├── androidMain/ │ │ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ │ │ └── kotlin/ │ │ │ │ │ │ └── com/ │ │ │ │ │ │ └── agui/ │ │ │ │ │ │ └── example/ │ │ │ │ │ │ └── chatapp/ │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ │ ├── commonMain/ │ │ │ │ │ │ ├── composeResources/ │ │ │ │ │ │ │ └── values/ │ │ │ │ │ │ │ └── strings.xml │ │ │ │ │ │ └── kotlin/ │ │ │ │ │ │ └── com/ │ │ │ │ │ │ └── agui/ │ │ │ │ │ │ └── example/ │ │ │ │ │ │ └── chatapp/ │ │ │ │ │ │ ├── App.kt │ │ │ │ │ │ ├── ui/ │ │ │ │ │ │ │ ├── screens/ │ │ │ │ │ │ │ │ ├── chat/ │ │ │ │ │ │ │ │ │ ├── ChatScreen.kt │ │ │ │ │ │ │ │ │ ├── ChatViewModel.kt │ │ │ │ │ │ │ │ │ └── components/ │ │ │ │ │ │ │ │ │ ├── ChatHeader.kt │ │ │ │ │ │ │ │ │ ├── ChatInput.kt │ │ │ │ │ │ │ │ │ ├── ClawgUiPairingDialog.kt │ │ │ │ │ │ │ │ │ ├── MessageBubble.kt │ │ │ │ │ │ │ │ │ └── MessageList.kt │ │ │ │ │ │ │ │ └── settings/ │ │ │ │ │ │ │ │ ├── SettingsScreen.kt │ │ │ │ │ │ │ │ ├── SettingsViewModel.kt │ │ │ │ │ │ │ │ └── components/ │ │ │ │ │ │ │ │ ├── AddAgentDialog.kt │ │ │ │ │ │ │ │ └── AgentCard.kt │ │ │ │ │ │ │ └── theme/ │ │ │ │ │ │ │ ├── Color.kt │ │ │ │ │ │ │ └── Theme.kt │ │ │ │ │ │ └── util/ │ │ │ │ │ │ └── Extensions.kt │ │ │ │ │ ├── commonTest/ │ │ │ │ │ │ └── kotlin/ │ │ │ │ │ │ └── com/ │ │ │ │ │ │ └── agui/ │ │ │ │ │ │ └── example/ │ │ │ │ │ │ └── chatapp/ │ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ │ └── AuthProviderTest.kt │ │ │ │ │ │ ├── test/ │ │ │ │ │ │ │ └── TestSettings.kt │ │ │ │ │ │ └── viewmodel/ │ │ │ │ │ │ └── ChatViewModelBehaviorTest.kt │ │ │ │ │ ├── desktopMain/ │ │ │ │ │ │ └── kotlin/ │ │ │ │ │ │ └── com/ │ │ │ │ │ │ └── agui/ │ │ │ │ │ │ └── example/ │ │ │ │ │ │ └── chatapp/ │ │ │ │ │ │ └── Main.kt │ │ │ │ │ ├── desktopTest/ │ │ │ │ │ │ └── kotlin/ │ │ │ │ │ │ └── com/ │ │ │ │ │ │ └── agui/ │ │ │ │ │ │ └── example/ │ │ │ │ │ │ └── chatapp/ │ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ │ └── AuthManagerIntegrationTest.kt │ │ │ │ │ │ └── repository/ │ │ │ │ │ │ └── AgentRepositoryPersistenceTest.kt │ │ │ │ │ ├── iosMain/ │ │ │ │ │ │ └── kotlin/ │ │ │ │ │ │ └── com/ │ │ │ │ │ │ └── agui/ │ │ │ │ │ │ └── example/ │ │ │ │ │ │ └── chatapp/ │ │ │ │ │ │ └── MainViewController.kt │ │ │ │ │ └── iosTest/ │ │ │ │ │ └── kotlin/ │ │ │ │ │ └── com/ │ │ │ │ │ └── agui/ │ │ │ │ │ └── example/ │ │ │ │ │ └── chatapp/ │ │ │ │ │ ├── IosPlatformTest.kt │ │ │ │ │ ├── IosSettingsTest.kt │ │ │ │ │ └── IosUserIdManagerTest.kt │ │ │ │ └── verify-ios-implementation.sh │ │ │ ├── chatapp-java/ │ │ │ │ ├── README.md │ │ │ │ ├── app/ │ │ │ │ │ ├── build.gradle │ │ │ │ │ ├── proguard-rules.pro │ │ │ │ │ └── src/ │ │ │ │ │ ├── androidTest/ │ │ │ │ │ │ └── java/ │ │ │ │ │ │ └── com/ │ │ │ │ │ │ └── agui/ │ │ │ │ │ │ └── chatapp/ │ │ │ │ │ │ └── java/ │ │ │ │ │ │ ├── MultiAgentInstrumentedTest.java │ │ │ │ │ │ └── ui/ │ │ │ │ │ │ ├── ChatActivityTest.java │ │ │ │ │ │ ├── SettingsActivityTest.java │ │ │ │ │ │ └── SettingsActivityTest.java.old │ │ │ │ │ └── main/ │ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ │ ├── java/ │ │ │ │ │ │ └── com/ │ │ │ │ │ │ └── agui/ │ │ │ │ │ │ └── chatapp/ │ │ │ │ │ │ └── java/ │ │ │ │ │ │ ├── ChatJavaApplication.java │ │ │ │ │ │ ├── model/ │ │ │ │ │ │ │ └── ChatMessage.java │ │ │ │ │ │ ├── repository/ │ │ │ │ │ │ │ └── MultiAgentRepository.kt │ │ │ │ │ │ ├── ui/ │ │ │ │ │ │ │ ├── ChatActivity.java │ │ │ │ │ │ │ ├── SettingsActivity.java │ │ │ │ │ │ │ └── adapter/ │ │ │ │ │ │ │ ├── AgentListAdapter.java │ │ │ │ │ │ │ └── MessageAdapter.java │ │ │ │ │ │ └── viewmodel/ │ │ │ │ │ │ └── ChatViewModel.kt │ │ │ │ │ └── res/ │ │ │ │ │ ├── drawable/ │ │ │ │ │ │ └── ic_launcher_foreground.xml │ │ │ │ │ ├── layout/ │ │ │ │ │ │ ├── activity_chat.xml │ │ │ │ │ │ ├── activity_settings.xml │ │ │ │ │ │ ├── dialog_agent_form.xml │ │ │ │ │ │ ├── item_agent_card.xml │ │ │ │ │ │ ├── item_message_assistant.xml │ │ │ │ │ │ ├── item_message_system.xml │ │ │ │ │ │ └── item_message_user.xml │ │ │ │ │ ├── menu/ │ │ │ │ │ │ └── chat_menu.xml │ │ │ │ │ ├── mipmap-anydpi-v26/ │ │ │ │ │ │ ├── ic_launcher.xml │ │ │ │ │ │ └── ic_launcher_round.xml │ │ │ │ │ ├── values/ │ │ │ │ │ │ ├── colors.xml │ │ │ │ │ │ ├── strings.xml │ │ │ │ │ │ └── themes.xml │ │ │ │ │ ├── values-night/ │ │ │ │ │ │ └── themes.xml │ │ │ │ │ └── xml/ │ │ │ │ │ ├── backup_rules.xml │ │ │ │ │ └── data_extraction_rules.xml │ │ │ │ ├── build.gradle │ │ │ │ ├── gradle/ │ │ │ │ │ ├── libs.versions.toml │ │ │ │ │ └── wrapper/ │ │ │ │ │ ├── gradle-wrapper.jar │ │ │ │ │ └── gradle-wrapper.properties │ │ │ │ ├── gradle.properties │ │ │ │ ├── gradlew │ │ │ │ ├── gradlew.bat │ │ │ │ └── settings.gradle │ │ │ ├── chatapp-shared/ │ │ │ │ ├── README.md │ │ │ │ ├── build.gradle.kts │ │ │ │ ├── gradle.properties │ │ │ │ ├── settings.gradle.kts │ │ │ │ └── src/ │ │ │ │ ├── androidMain/ │ │ │ │ │ └── kotlin/ │ │ │ │ │ └── com/ │ │ │ │ │ └── agui/ │ │ │ │ │ └── example/ │ │ │ │ │ └── chatapp/ │ │ │ │ │ ├── data/ │ │ │ │ │ │ └── pairing/ │ │ │ │ │ │ └── PairingHttpClientFactory.kt │ │ │ │ │ └── util/ │ │ │ │ │ └── AndroidPlatform.kt │ │ │ │ ├── commonMain/ │ │ │ │ │ └── kotlin/ │ │ │ │ │ └── com/ │ │ │ │ │ └── agui/ │ │ │ │ │ └── example/ │ │ │ │ │ ├── chatapp/ │ │ │ │ │ │ ├── chat/ │ │ │ │ │ │ │ ├── ChatAgent.kt │ │ │ │ │ │ │ └── ChatController.kt │ │ │ │ │ │ ├── data/ │ │ │ │ │ │ │ ├── auth/ │ │ │ │ │ │ │ │ ├── ApiKeyAuthProvider.kt │ │ │ │ │ │ │ │ ├── AuthManager.kt │ │ │ │ │ │ │ │ ├── AuthProvider.kt │ │ │ │ │ │ │ │ ├── BasicAuthProvider.kt │ │ │ │ │ │ │ │ └── BearerTokenAuthProvider.kt │ │ │ │ │ │ │ ├── model/ │ │ │ │ │ │ │ │ ├── AgentConfig.kt │ │ │ │ │ │ │ │ ├── AuthMethod.kt │ │ │ │ │ │ │ │ └── ClawgUiPairingResponse.kt │ │ │ │ │ │ │ ├── pairing/ │ │ │ │ │ │ │ │ ├── ClawgUiPairingService.kt │ │ │ │ │ │ │ │ └── PairingHttpClientFactory.kt │ │ │ │ │ │ │ └── repository/ │ │ │ │ │ │ │ └── AgentRepository.kt │ │ │ │ │ │ └── util/ │ │ │ │ │ │ ├── Platform.kt │ │ │ │ │ │ ├── StringResourceProvider.kt │ │ │ │ │ │ └── UserIdManager.kt │ │ │ │ │ └── tools/ │ │ │ │ │ └── ChangeBackgroundToolExecutor.kt │ │ │ │ ├── commonTest/ │ │ │ │ │ └── kotlin/ │ │ │ │ │ └── com/ │ │ │ │ │ └── agui/ │ │ │ │ │ └── example/ │ │ │ │ │ └── chatapp/ │ │ │ │ │ ├── chat/ │ │ │ │ │ │ └── ChatControllerTest.kt │ │ │ │ │ ├── data/ │ │ │ │ │ │ ├── AgentRepositoryTest.kt │ │ │ │ │ │ ├── AuthManagerTest.kt │ │ │ │ │ │ └── pairing/ │ │ │ │ │ │ └── ClawgUiPairingServiceTest.kt │ │ │ │ │ └── testutil/ │ │ │ │ │ └── FakeSettings.kt │ │ │ │ ├── desktopMain/ │ │ │ │ │ └── kotlin/ │ │ │ │ │ └── com/ │ │ │ │ │ └── agui/ │ │ │ │ │ └── example/ │ │ │ │ │ └── chatapp/ │ │ │ │ │ ├── data/ │ │ │ │ │ │ └── pairing/ │ │ │ │ │ │ └── PairingHttpClientFactory.kt │ │ │ │ │ └── util/ │ │ │ │ │ └── DesktopPlatform.kt │ │ │ │ └── iosMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── agui/ │ │ │ │ └── example/ │ │ │ │ └── chatapp/ │ │ │ │ ├── data/ │ │ │ │ │ └── pairing/ │ │ │ │ │ └── PairingHttpClientFactory.kt │ │ │ │ └── util/ │ │ │ │ └── IosPlatform.kt │ │ │ ├── chatapp-swiftui/ │ │ │ │ ├── README.md │ │ │ │ ├── build.gradle.kts │ │ │ │ ├── gradle/ │ │ │ │ │ ├── libs.versions.toml │ │ │ │ │ └── wrapper/ │ │ │ │ │ ├── gradle-wrapper.jar │ │ │ │ │ └── gradle-wrapper.properties │ │ │ │ ├── gradle.properties │ │ │ │ ├── gradlew │ │ │ │ ├── gradlew.bat │ │ │ │ ├── iosApp/ │ │ │ │ │ ├── ChatAppSwiftUI.xcodeproj/ │ │ │ │ │ │ ├── project.pbxproj │ │ │ │ │ │ └── project.xcworkspace/ │ │ │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ │ │ └── xcshareddata/ │ │ │ │ │ │ └── swiftpm/ │ │ │ │ │ │ └── Package.resolved │ │ │ │ │ ├── Resources/ │ │ │ │ │ │ ├── Assets.xcassets/ │ │ │ │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ ├── Info.plist │ │ │ │ │ │ └── LaunchScreen.storyboard │ │ │ │ │ ├── Sources/ │ │ │ │ │ │ ├── App/ │ │ │ │ │ │ │ └── ChatAppSwiftUIApp.swift │ │ │ │ │ │ ├── Store/ │ │ │ │ │ │ │ └── ChatAppStore.swift │ │ │ │ │ │ └── Views/ │ │ │ │ │ │ ├── AgentFormView.swift │ │ │ │ │ │ ├── ChatView.swift │ │ │ │ │ │ ├── Components/ │ │ │ │ │ │ │ └── AgentListView.swift │ │ │ │ │ │ └── RootView.swift │ │ │ │ │ └── project.yml │ │ │ │ ├── settings.gradle.kts │ │ │ │ └── shared/ │ │ │ │ ├── build.gradle.kts │ │ │ │ └── src/ │ │ │ │ └── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── agui/ │ │ │ │ └── example/ │ │ │ │ └── chatapp/ │ │ │ │ └── bridge/ │ │ │ │ └── SwiftBridge.kt │ │ │ ├── chatapp-wearos/ │ │ │ │ ├── README.md │ │ │ │ ├── build.gradle.kts │ │ │ │ ├── gradle/ │ │ │ │ │ ├── libs.versions.toml │ │ │ │ │ └── wrapper/ │ │ │ │ │ ├── gradle-wrapper.jar │ │ │ │ │ └── gradle-wrapper.properties │ │ │ │ ├── gradle.properties │ │ │ │ ├── gradlew │ │ │ │ ├── gradlew.bat │ │ │ │ ├── settings.gradle.kts │ │ │ │ └── wearApp/ │ │ │ │ ├── build.gradle.kts │ │ │ │ └── src/ │ │ │ │ └── main/ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ ├── java/ │ │ │ │ │ └── com/ │ │ │ │ │ └── agui/ │ │ │ │ │ └── example/ │ │ │ │ │ └── chatwear/ │ │ │ │ │ ├── MainActivity.kt │ │ │ │ │ └── ui/ │ │ │ │ │ ├── ChatWearApp.kt │ │ │ │ │ ├── WearChatViewModel.kt │ │ │ │ │ └── theme/ │ │ │ │ │ └── ChatWearTheme.kt │ │ │ │ └── res/ │ │ │ │ ├── drawable/ │ │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ │ └── ic_launcher_foreground.xml │ │ │ │ ├── mipmap-anydpi/ │ │ │ │ │ ├── ic_launcher.xml │ │ │ │ │ └── ic_launcher_round.xml │ │ │ │ └── values/ │ │ │ │ ├── colors.xml │ │ │ │ ├── strings.xml │ │ │ │ └── themes.xml │ │ │ └── tools/ │ │ │ ├── build.gradle.kts │ │ │ ├── gradle/ │ │ │ │ ├── libs.versions.toml │ │ │ │ └── wrapper/ │ │ │ │ ├── gradle-wrapper.jar │ │ │ │ └── gradle-wrapper.properties │ │ │ ├── gradle.properties │ │ │ ├── gradlew │ │ │ ├── gradlew.bat │ │ │ ├── settings.gradle.kts │ │ │ └── src/ │ │ │ ├── androidMain/ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── agui/ │ │ │ │ └── example/ │ │ │ │ └── tools/ │ │ │ │ └── AndroidLocationProvider.kt │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── agui/ │ │ │ │ └── example/ │ │ │ │ └── tools/ │ │ │ │ ├── ChangeBackgroundToolExecutor.kt │ │ │ │ └── CurrentLocationToolExecutor.kt │ │ │ ├── commonTest/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── agui/ │ │ │ │ └── example/ │ │ │ │ └── tools/ │ │ │ │ ├── CurrentLocationToolTest.kt │ │ │ │ └── ExampleToolsIntegrationTest.kt │ │ │ ├── iosMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── agui/ │ │ │ │ └── example/ │ │ │ │ └── tools/ │ │ │ │ └── IosLocationProvider.kt │ │ │ ├── iosTest/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── agui/ │ │ │ │ └── example/ │ │ │ │ └── tools/ │ │ │ │ ├── IosLocationIntegrationTest.kt │ │ │ │ └── IosLocationProviderTest.kt │ │ │ └── jvmMain/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── agui/ │ │ │ └── example/ │ │ │ └── tools/ │ │ │ └── JvmLocationProvider.kt │ │ ├── gradle.properties │ │ ├── library/ │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── build.gradle.kts │ │ │ ├── client/ │ │ │ │ ├── build.gradle.kts │ │ │ │ ├── consumer-rules.pro │ │ │ │ └── src/ │ │ │ │ ├── androidMain/ │ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ │ └── kotlin/ │ │ │ │ │ └── com/ │ │ │ │ │ └── agui/ │ │ │ │ │ └── client/ │ │ │ │ │ └── agent/ │ │ │ │ │ └── HttpClientFactory.kt │ │ │ │ ├── commonMain/ │ │ │ │ │ └── kotlin/ │ │ │ │ │ └── com/ │ │ │ │ │ └── agui/ │ │ │ │ │ └── client/ │ │ │ │ │ ├── AgUiAgent.kt │ │ │ │ │ ├── ClientStateManager.kt │ │ │ │ │ ├── StatefulAgUiAgent.kt │ │ │ │ │ ├── agent/ │ │ │ │ │ │ ├── AbstractAgent.kt │ │ │ │ │ │ ├── AgentSubscriber.kt │ │ │ │ │ │ ├── HttpAgent.kt │ │ │ │ │ │ └── HttpClientFactory.kt │ │ │ │ │ ├── builders/ │ │ │ │ │ │ └── AgentBuilders.kt │ │ │ │ │ ├── chunks/ │ │ │ │ │ │ └── ChunkTransform.kt │ │ │ │ │ ├── sse/ │ │ │ │ │ │ └── SseParser.kt │ │ │ │ │ ├── state/ │ │ │ │ │ │ ├── DefaultApplyEvents.kt │ │ │ │ │ │ ├── JsonPointer.kt │ │ │ │ │ │ ├── StateHandler.kt │ │ │ │ │ │ └── StateManager.kt │ │ │ │ │ ├── tools/ │ │ │ │ │ │ └── ClientToolResponseHandler.kt │ │ │ │ │ └── verify/ │ │ │ │ │ └── EventVerifier.kt │ │ │ │ ├── commonTest/ │ │ │ │ │ └── kotlin/ │ │ │ │ │ └── com/ │ │ │ │ │ └── agui/ │ │ │ │ │ └── client/ │ │ │ │ │ ├── AgUiAgentConfigTest.kt │ │ │ │ │ ├── AgUiAgentToolsTest.kt │ │ │ │ │ ├── IntegrationTest.kt │ │ │ │ │ ├── StatefulAgUiAgentConfigTest.kt │ │ │ │ │ ├── StatefulAgUiAgentTest.kt │ │ │ │ │ ├── UserIdTest.kt │ │ │ │ │ ├── agent/ │ │ │ │ │ │ └── AbstractAgentTest.kt │ │ │ │ │ ├── chunks/ │ │ │ │ │ │ └── ChunkTransformTest.kt │ │ │ │ │ ├── integration/ │ │ │ │ │ │ ├── AdvancedIntegrationTest.kt │ │ │ │ │ │ ├── AgentToolIntegrationTest.kt │ │ │ │ │ │ └── SimpleAgentToolIntegrationTest.kt │ │ │ │ │ ├── sse/ │ │ │ │ │ │ └── SseParserTest.kt │ │ │ │ │ ├── state/ │ │ │ │ │ │ ├── DefaultApplyEventsTest.kt │ │ │ │ │ │ └── StateManagerTest.kt │ │ │ │ │ └── verify/ │ │ │ │ │ └── EventVerifierTest.kt │ │ │ │ ├── iosMain/ │ │ │ │ │ └── kotlin/ │ │ │ │ │ └── com/ │ │ │ │ │ └── agui/ │ │ │ │ │ └── client/ │ │ │ │ │ └── agent/ │ │ │ │ │ └── HttpClientFactory.kt │ │ │ │ └── jvmMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── agui/ │ │ │ │ └── client/ │ │ │ │ └── agent/ │ │ │ │ └── HttpClientFactory.kt │ │ │ ├── core/ │ │ │ │ ├── build.gradle.kts │ │ │ │ └── src/ │ │ │ │ ├── androidMain/ │ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ │ └── kotlin/ │ │ │ │ │ └── com/ │ │ │ │ │ └── agui/ │ │ │ │ │ └── platform/ │ │ │ │ │ └── AndroidPlatform.kt │ │ │ │ ├── commonMain/ │ │ │ │ │ └── kotlin/ │ │ │ │ │ └── com/ │ │ │ │ │ └── agui/ │ │ │ │ │ ├── core/ │ │ │ │ │ │ └── types/ │ │ │ │ │ │ ├── AgUiJson.kt │ │ │ │ │ │ ├── AgUiSerializersModule.kt │ │ │ │ │ │ ├── Events.kt │ │ │ │ │ │ └── Types.kt │ │ │ │ │ └── platform/ │ │ │ │ │ └── Platform.kt │ │ │ │ ├── commonTest/ │ │ │ │ │ └── kotlin/ │ │ │ │ │ └── com/ │ │ │ │ │ └── agui/ │ │ │ │ │ └── tests/ │ │ │ │ │ ├── EventSerializationTest.kt │ │ │ │ │ ├── MessageProtocolComplianceTest.kt │ │ │ │ │ ├── MessageSerializationTest.kt │ │ │ │ │ ├── RunAgentInputProtocolTest.kt │ │ │ │ │ └── ToolSerializationDebugTest.kt │ │ │ │ ├── iosMain/ │ │ │ │ │ └── kotlin/ │ │ │ │ │ └── com/ │ │ │ │ │ └── agui/ │ │ │ │ │ └── platform/ │ │ │ │ │ └── IosPlatform.kt │ │ │ │ ├── jvmMain/ │ │ │ │ │ └── kotlin/ │ │ │ │ │ └── com/ │ │ │ │ │ └── agui/ │ │ │ │ │ └── platform/ │ │ │ │ │ └── JvmPlatform.kt │ │ │ │ └── jvmTest/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── agui/ │ │ │ │ └── platform/ │ │ │ │ └── PlatformJvmTest.kt │ │ │ ├── gradle/ │ │ │ │ ├── libs.versions.toml │ │ │ │ └── wrapper/ │ │ │ │ ├── gradle-wrapper.jar │ │ │ │ └── gradle-wrapper.properties │ │ │ ├── gradle.properties │ │ │ ├── gradlew │ │ │ ├── gradlew.bat │ │ │ ├── settings.gradle.kts │ │ │ └── tools/ │ │ │ ├── build.gradle.kts │ │ │ └── src/ │ │ │ ├── androidMain/ │ │ │ │ └── AndroidManifest.xml │ │ │ ├── commonMain/ │ │ │ │ └── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── agui/ │ │ │ │ └── tools/ │ │ │ │ ├── ToolErrorHandling.kt │ │ │ │ ├── ToolExecutionManager.kt │ │ │ │ ├── ToolExecutor.kt │ │ │ │ └── ToolRegistry.kt │ │ │ └── commonTest/ │ │ │ └── kotlin/ │ │ │ └── com/ │ │ │ └── agui/ │ │ │ └── tools/ │ │ │ ├── CircuitBreakerTest.kt │ │ │ ├── ToolErrorHandlerTest.kt │ │ │ ├── ToolExecutionManagerTest.kt │ │ │ └── ToolsModuleTest.kt │ │ ├── publish.sh │ │ └── settings.gradle.kts │ ├── ruby/ │ │ ├── .gitignore │ │ ├── .yardopts │ │ ├── CHANGELOG.md │ │ ├── Gemfile │ │ ├── README.md │ │ ├── Rakefile │ │ ├── ag-ui-protocol.gemspec │ │ ├── example/ │ │ │ ├── rails/ │ │ │ │ ├── Gemfile │ │ │ │ ├── README.md │ │ │ │ ├── app/ │ │ │ │ │ └── controllers/ │ │ │ │ │ └── ag_ui_controller.rb │ │ │ │ ├── config/ │ │ │ │ │ ├── application.rb │ │ │ │ │ ├── boot.rb │ │ │ │ │ ├── environment.rb │ │ │ │ │ ├── environments/ │ │ │ │ │ │ └── development.rb │ │ │ │ │ ├── puma.rb │ │ │ │ │ └── routes.rb │ │ │ │ └── config.ru │ │ │ └── simple-use/ │ │ │ ├── Gemfile │ │ │ ├── README.md │ │ │ └── main.rb │ │ ├── lib/ │ │ │ ├── ag-ui-protocol.rb │ │ │ ├── ag_ui_protocol/ │ │ │ │ ├── core/ │ │ │ │ │ ├── events.rb │ │ │ │ │ └── types.rb │ │ │ │ ├── encoder/ │ │ │ │ │ └── event_encoder.rb │ │ │ │ ├── util.rb │ │ │ │ └── version.rb │ │ │ └── ag_ui_protocol.rb │ │ ├── sorbet/ │ │ │ └── config │ │ ├── templates/ │ │ │ └── default/ │ │ │ └── fulldoc/ │ │ │ └── markdown/ │ │ │ └── setup.rb │ │ ├── test/ │ │ │ ├── ag_ui_protocol/ │ │ │ │ ├── core/ │ │ │ │ │ ├── events_test.rb │ │ │ │ │ └── types_test.rb │ │ │ │ └── encoder/ │ │ │ │ └── event_encoder_test.rb │ │ │ └── test_helper.rb │ │ └── yard_extensions.rb │ └── rust/ │ ├── Cargo.toml │ ├── TODO │ └── crates/ │ ├── ag-ui-client/ │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── examples/ │ │ │ ├── basic_agent.rs │ │ │ ├── generative_ui.rs │ │ │ ├── logging_subscriber.rs │ │ │ ├── shared_state.rs │ │ │ └── sse_example.rs │ │ ├── scripts/ │ │ │ ├── basic_agent.py │ │ │ ├── generative_ui.py │ │ │ └── shared_state.py │ │ ├── src/ │ │ │ ├── agent.rs │ │ │ ├── error.rs │ │ │ ├── event_handler.rs │ │ │ ├── http.rs │ │ │ ├── lib.rs │ │ │ ├── sse.rs │ │ │ ├── stream.rs │ │ │ └── subscriber.rs │ │ └── tests/ │ │ ├── http_agent_test.rs │ │ └── sse_test.rs │ └── ag-ui-core/ │ ├── Cargo.toml │ ├── README.md │ ├── src/ │ │ ├── error.rs │ │ ├── event.rs │ │ ├── lib.rs │ │ ├── state.rs │ │ └── types/ │ │ ├── context.rs │ │ ├── ids.rs │ │ ├── input.rs │ │ ├── message.rs │ │ ├── mod.rs │ │ └── tool.rs │ └── tests/ │ └── unit.rs ├── python/ │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── ag_ui/ │ │ ├── __init__.py │ │ ├── core/ │ │ │ ├── __init__.py │ │ │ ├── events.py │ │ │ └── types.py │ │ ├── encoder/ │ │ │ ├── __init__.py │ │ │ └── encoder.py │ │ └── py.typed │ ├── pyproject.toml │ └── tests/ │ ├── __init__.py │ ├── test_encoder.py │ ├── test_events.py │ ├── test_text_roles.py │ └── test_types.py └── typescript/ ├── .cursor/ │ └── rules/ │ └── project-rules.mdc ├── .gitignore ├── .npmrc ├── .prettierrc ├── LICENSE ├── README.md ├── packages/ │ ├── cli/ │ │ ├── .npmignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ ├── client/ │ │ ├── .npmignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── agent/ │ │ │ │ ├── __tests__/ │ │ │ │ │ ├── agent-clone.test.ts │ │ │ │ │ ├── agent-concurrent.test.ts │ │ │ │ │ ├── agent-multiple-runs.test.ts │ │ │ │ │ ├── agent-mutations.test.ts │ │ │ │ │ ├── agent-result.test.ts │ │ │ │ │ ├── agent-text-roles.test.ts │ │ │ │ │ ├── agent-version.test.ts │ │ │ │ │ ├── http.test.ts │ │ │ │ │ ├── legacy-bridged.test.ts │ │ │ │ │ └── subscriber.test.ts │ │ │ │ ├── agent.ts │ │ │ │ ├── http.ts │ │ │ │ ├── index.ts │ │ │ │ ├── subscriber.ts │ │ │ │ └── types.ts │ │ │ ├── apply/ │ │ │ │ ├── __tests__/ │ │ │ │ │ ├── default.activity.test.ts │ │ │ │ │ ├── default.concurrent.test.ts │ │ │ │ │ ├── default.reasoning.test.ts │ │ │ │ │ ├── default.state.test.ts │ │ │ │ │ ├── default.text-message.test.ts │ │ │ │ │ ├── default.tool-calls.test.ts │ │ │ │ │ └── run-started-input.test.ts │ │ │ │ ├── default.ts │ │ │ │ └── index.ts │ │ │ ├── chunks/ │ │ │ │ ├── __tests__/ │ │ │ │ │ ├── transform-roles.test.ts │ │ │ │ │ └── transform.test.ts │ │ │ │ ├── index.ts │ │ │ │ └── transform.ts │ │ │ ├── compact/ │ │ │ │ ├── __tests__/ │ │ │ │ │ └── compact.test.ts │ │ │ │ ├── compact.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── legacy/ │ │ │ │ ├── __tests__/ │ │ │ │ │ ├── convert.concurrent.test.ts │ │ │ │ │ ├── convert.predictive.test.ts │ │ │ │ │ ├── convert.state.test.ts │ │ │ │ │ └── convert.tool-calls.test.ts │ │ │ │ ├── convert.ts │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── middleware/ │ │ │ │ ├── __tests__/ │ │ │ │ │ ├── backward-compatibility-0-0-39.test.ts │ │ │ │ │ ├── backward-compatibility-0-0-45.test.ts │ │ │ │ │ ├── filter-tool-calls.test.ts │ │ │ │ │ ├── function-middleware.test.ts │ │ │ │ │ ├── middleware-chained-integration.test.ts │ │ │ │ │ ├── middleware-chained-run-next-with-state.test.ts │ │ │ │ │ ├── middleware-live-events.test.ts │ │ │ │ │ ├── middleware-usage-example.ts │ │ │ │ │ ├── middleware-with-state.test.ts │ │ │ │ │ └── middleware.test.ts │ │ │ │ ├── backward-compatibility-0-0-39.ts │ │ │ │ ├── backward-compatibility-0-0-45.ts │ │ │ │ ├── filter-tool-calls.ts │ │ │ │ ├── index.ts │ │ │ │ └── middleware.ts │ │ │ ├── run/ │ │ │ │ ├── __tests__/ │ │ │ │ │ └── http-request.test.ts │ │ │ │ ├── http-request.ts │ │ │ │ └── index.ts │ │ │ ├── transform/ │ │ │ │ ├── __tests__/ │ │ │ │ │ ├── http.test.ts │ │ │ │ │ ├── proto.test.ts │ │ │ │ │ └── sse.test.ts │ │ │ │ ├── http.ts │ │ │ │ ├── index.ts │ │ │ │ ├── proto.ts │ │ │ │ └── sse.ts │ │ │ ├── utils.ts │ │ │ └── verify/ │ │ │ ├── __tests__/ │ │ │ │ ├── verify.concurrent.test.ts │ │ │ │ ├── verify.events.test.ts │ │ │ │ ├── verify.lifecycle.test.ts │ │ │ │ ├── verify.multiple-runs.test.ts │ │ │ │ ├── verify.steps.test.ts │ │ │ │ ├── verify.text-messages.test.ts │ │ │ │ └── verify.tool-calls.test.ts │ │ │ ├── index.ts │ │ │ └── verify.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ ├── core/ │ │ ├── .npmignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __tests__/ │ │ │ │ ├── activity-events.test.ts │ │ │ │ ├── backwards-compatibility.test.ts │ │ │ │ ├── event-factories.test.ts │ │ │ │ ├── events-role-defaults.test.ts │ │ │ │ ├── index.test.ts │ │ │ │ └── multimodal-messages.test.ts │ │ │ ├── capabilities.ts │ │ │ ├── event-factories.ts │ │ │ ├── events.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ ├── encoder/ │ │ ├── .npmignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── __tests__/ │ │ │ │ └── encoder.test.ts │ │ │ ├── encoder.ts │ │ │ ├── index.ts │ │ │ └── media-type.ts │ │ ├── tsconfig.json │ │ ├── tsdown.config.ts │ │ └── vitest.config.ts │ └── proto/ │ ├── .npmignore │ ├── README.md │ ├── __tests__/ │ │ ├── message-events.test.ts │ │ ├── proto.test.ts │ │ ├── run-events.test.ts │ │ ├── state-events.test.ts │ │ ├── test-utils.ts │ │ └── tool-call-events.test.ts │ ├── package.json │ ├── src/ │ │ ├── index.ts │ │ ├── proto/ │ │ │ ├── events.proto │ │ │ ├── patch.proto │ │ │ └── types.proto │ │ └── proto.ts │ ├── tsconfig.json │ ├── tsdown.config.ts │ └── vitest.config.ts ├── scripts/ │ └── create-integration.ts ├── tsconfig.json └── vitest.base.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .claude/settings.json ================================================ { "permissions": { "allow": [ "Bash(npx nx show projects)", "Bash(pnpm nx run dojo:check-types)", "Bash(pnpm nx show projects)", "Bash(pnpm nx run demo-viewer:check-types)", "Bash(pnpm nx show project demo-viewer)", "Bash(pnpm nx run demo-viewer:lint)" ] } } ================================================ FILE: .gitattributes ================================================ * text=auto eol=lf *.mdx text eol=lf *.js text eol=lf *.ts text eol=lf *.jsx text eol=lf *.tsx text eol=lf *.py text eol=lf ================================================ FILE: .github/CODEOWNERS ================================================ * @ag-ui-protocol/copilotkit sdks/community/java @pascalwilbrink docs/sdk/java @pascalwilbrink sdks/community/kotlin @contextablemark docs/sdk/kotlin @contextablemark sdks/community/go @mattsp1290 docs/sdk/go @mattsp1290 sdks/community/dart @mattsp1290 docs/sdk/dart @mattsp1290 integrations/adk-middleware @contextablemark integrations/agent-spec @sonleoracle ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: "🐛 Bug Report" description: "Something isn't working as expected? Let us know so we can fix it." title: "[Bug]: " labels: ["bug", "triage"] body: - type: markdown attributes: value: | Thanks for helping improve AG-UI! Please fill this out so we can reproduce and fix the issue quickly. > **Before you file:** Search [existing issues](https://github.com/ag-ui-protocol/ag-ui/issues) to avoid duplicates. - type: checkboxes id: preflight attributes: label: Pre-flight Checklist options: - label: I have searched [existing issues](https://github.com/ag-ui-protocol/ag-ui/issues) and this hasn't been reported yet. required: true - label: I am using the **latest** version AG-UI. required: true - type: textarea id: description attributes: label: Describe the Bug description: A clear description of what went wrong. placeholder: "When I do X, Y happens instead of Z." validations: required: true - type: textarea id: steps attributes: label: Steps to Reproduce description: Walk us through exactly how to trigger this bug. placeholder: | 1. Install `@ag-ui/client` 2. Create an HttpAgent pointing at `http://localhost:8000/agent` 3. Call `agent.run(...)` with ... 4. Observe the error validations: required: true - type: textarea id: expected attributes: label: Expected Behavior description: What did you expect to happen instead? placeholder: "I expected X to happen when..." validations: required: true - type: textarea id: environment attributes: label: Environment description: Tell us what you're working with so we can reproduce your setup. placeholder: | AG-UI package(s) & version(s): e.g. @ag-ui/core@0.0.44 Runtime: e.g. Node 22 / Python 3.12 render: text validations: required: true - type: textarea attributes: label: Screenshots description: If applicable, add screenshots to help explain your problem. - type: textarea id: logs attributes: label: Logs & Errors description: Paste any relevant stack traces or error output. Auto-formatted as code. render: shell validations: required: false - type: textarea id: additional attributes: label: Additional Context description: Anything else — workarounds, screenshots, related issues, etc. validations: required: false ================================================ FILE: .github/ISSUE_TEMPLATE/documentation.yml ================================================ name: 📚 Documentation Issue description: Let us know how we can improve the AG-UI documentation. title: "📚 Documentation: " labels: ["documentation"] assignees: [] body: - type: markdown attributes: value: | We appreciate you taking the time to help us make our documentation better! - type: textarea id: description attributes: label: 💬 Let us know how we can improve our documentation. description: | If you are referring to an existing page in the documentation, please provide a link. placeholder: | Type your idea here... validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: "✨ Feature Request" description: "Suggest a new feature or improvement." title: "[Feature]: " labels: ["enhancement"] body: - type: markdown attributes: value: | Thanks for taking the time to suggest a feature! > **Before you file:** Search existing issues to avoid duplicates. - type: checkboxes id: preflight attributes: label: Pre-flight Checklist options: - label: I have searched existing issues and this hasn't been requested yet. required: true - type: textarea id: problem attributes: label: Problem or Motivation description: What problem does this feature solve? Why is it needed? placeholder: "I'm always frustrated when..." validations: required: true - type: textarea id: solution attributes: label: Proposed Solution description: Describe the solution you'd like. Be as specific as possible. placeholder: "It would be great if..." validations: required: true - type: textarea id: alternatives attributes: label: Alternatives Considered description: Have you considered any alternative solutions or workarounds? validations: required: false - type: textarea id: additional attributes: label: Additional Context description: Anything else — mockups, code snippets, related issues, links, etc. validations: required: false ================================================ FILE: .github/pull_request_template.md ================================================ ================================================ FILE: .github/workflows/auto-approve-community.yml ================================================ name: Auto-approve community PRs on: pull_request: types: [opened, synchronize, reopened] permissions: pull-requests: write contents: read jobs: auto-approve: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Fetch PR head run: | git fetch origin pull/${{ github.event.pull_request.number }}/head:pr-head - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: "22" - name: Auto-approve based on CODEOWNERS env: PR_AUTHOR: ${{ github.event.pull_request.user.login }} PR_NUMBER: ${{ github.event.pull_request.number }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BASE_REF: ${{ github.event.pull_request.base.ref }} run: | node << 'EOF' const { execSync } = require('child_process'); const fs = require('fs'); const path = require('path'); const prAuthor = process.env.PR_AUTHOR; const prNumber = process.env.PR_NUMBER; // Get changed files (use pr-head ref which works for both forks and same-repo PRs) const changedFiles = execSync( `git diff --name-only origin/${process.env.BASE_REF}...pr-head`, { encoding: 'utf-8' } ) .trim() .split('\n') .filter(f => f.trim()); console.log(`Changed files (${changedFiles.length}):`); changedFiles.forEach(f => console.log(` - ${f}`)); // Parse CODEOWNERS file const codeownersPath = '.github/CODEOWNERS'; const codeownersContent = fs.readFileSync(codeownersPath, 'utf-8'); const lines = codeownersContent.split('\n'); // Map of path patterns to owners (excluding root * rule) const codeownersRules = []; for (const line of lines) { const trimmed = line.trim(); // Skip empty lines and comments if (!trimmed || trimmed.startsWith('#')) { continue; } // Skip root * line if (trimmed.startsWith('* ')) { console.log('Skipping root * rule'); continue; } // Parse pattern and owners const parts = trimmed.split(/\s+/); if (parts.length < 2) { continue; } const pattern = parts[0]; const owners = parts.slice(1).map(o => o.replace('@', '')); codeownersRules.push({ pattern, owners }); } console.log('\nCODEOWNERS rules (excluding root):'); codeownersRules.forEach(rule => { console.log(` ${rule.pattern} -> ${rule.owners.join(', ')}`); }); // Function to check if a file matches a CODEOWNERS pattern // CODEOWNERS patterns match: // - Exact file/directory path // - pattern/ matches everything in that directory // - pattern/** matches everything recursively in that directory function matchesPattern(file, pattern) { // Normalize paths (handle both / and \ separators) const normalizePath = (p) => p.replace(/\\/g, '/'); const normalizedFile = normalizePath(file); const normalizedPattern = normalizePath(pattern); // Exact match if (normalizedFile === normalizedPattern) { return true; } // Pattern ends with /**: matches recursively in directory if (normalizedPattern.endsWith('/**')) { const dirPrefix = normalizedPattern.slice(0, -3); return normalizedFile.startsWith(dirPrefix + '/'); } // Pattern ends with /: matches everything in directory if (normalizedPattern.endsWith('/')) { const dirPrefix = normalizedPattern.slice(0, -1); return normalizedFile.startsWith(dirPrefix + '/'); } // Pattern is a directory prefix (matches subdirectories) if (normalizedFile.startsWith(normalizedPattern + '/')) { return true; } return false; } // Check each changed file // CODEOWNERS rules are evaluated top-to-bottom, first match wins const unapprovedFiles = []; for (const file of changedFiles) { let matched = false; let owned = false; // Find the first matching rule (CODEOWNERS uses first match semantics) for (const rule of codeownersRules) { if (matchesPattern(file, rule.pattern)) { matched = true; // First match wins in CODEOWNERS, so check ownership here owned = rule.owners.includes(prAuthor); break; // Stop at first match } } // File must be matched by a non-root CODEOWNERS rule AND author must own it if (!matched || !owned) { unapprovedFiles.push(file); } } // Decision if (unapprovedFiles.length === 0) { console.log(`\n✅ All changed files are owned by ${prAuthor} according to CODEOWNERS`); // Check if already approved by this workflow try { const reviews = JSON.parse( execSync(`gh pr view ${prNumber} --json reviews`, { encoding: 'utf-8' }) ); // Check if there's already an approval from GitHub Actions bot // (look for approval with the auto-approve message) const hasAutoApproval = reviews.reviews.some( review => review.state === 'APPROVED' && review.body && review.body.includes('Auto-approved: PR author has CODEOWNERS access') ); if (hasAutoApproval) { console.log('PR already auto-approved by this workflow'); } else { // Approve the PR using GitHub Actions bot account execSync( `gh pr review ${prNumber} --approve --body "Auto-approved: PR author ${prAuthor} has CODEOWNERS access to all changed files (excluding root rule)"`, { stdio: 'inherit' } ); console.log(`PR approved automatically for ${prAuthor}`); } } catch (error) { console.error('Error checking/approving PR:', error.message); // Don't fail the workflow if approval fails (might already be approved, etc.) console.log('Continuing despite approval error...'); } } else { console.log(`\n❌ Not auto-approved: Some files are not owned by ${prAuthor}`); console.log('Unauthorized files:'); unapprovedFiles.forEach(f => console.log(` - ${f}`)); } EOF ================================================ FILE: .github/workflows/build-python-preview.yml ================================================ name: Build Python Preview on: pull_request: types: [opened, synchronize, reopened] concurrency: group: ${{ github.repository }}-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: ubuntu-latest permissions: contents: read steps: - name: Checkout code uses: actions/checkout@v4 - name: Install uv uses: astral-sh/setup-uv@v4 with: version: ">=0.8.0" - name: Compute preview version id: version run: | TIMESTAMP=$(git log -1 --format=%ct HEAD) VERSION="0.0.0.dev${TIMESTAMP}" echo "version=${VERSION}" >> "$GITHUB_OUTPUT" echo "Preview version: ${VERSION}" - name: Rewrite pyproject.toml versions run: uv run python scripts/rewrite-python-preview-versions.py ${{ steps.version.outputs.version }} - name: Build ag-ui-protocol working-directory: sdks/python run: uv build - name: Build ag-ui-langgraph working-directory: integrations/langgraph/python run: uv build - name: Build ag-ui-crewai working-directory: integrations/crew-ai/python run: uv build - name: Build ag-ui-agent-spec working-directory: integrations/agent-spec/python run: uv build - name: Build ag_ui_adk working-directory: integrations/adk-middleware/python run: uv build - name: Build ag_ui_strands working-directory: integrations/aws-strands/python run: uv build - name: Collect dist artifacts run: | mkdir -p dist-preview cp sdks/python/dist/* dist-preview/ cp integrations/langgraph/python/dist/* dist-preview/ cp integrations/crew-ai/python/dist/* dist-preview/ cp integrations/agent-spec/python/dist/* dist-preview/ cp integrations/adk-middleware/python/dist/* dist-preview/ cp integrations/aws-strands/python/dist/* dist-preview/ echo "Artifacts to publish:" ls -1 dist-preview/ # Save metadata so the publish workflow can find the PR and version. - name: Save PR metadata run: | mkdir -p pr-metadata echo "${{ github.event.pull_request.number }}" > pr-metadata/pr-number echo "${{ steps.version.outputs.version }}" > pr-metadata/version echo "${{ github.sha }}" > pr-metadata/sha - name: Upload dist artifacts uses: actions/upload-artifact@v4 with: name: python-preview-dist path: dist-preview/ - name: Upload PR metadata uses: actions/upload-artifact@v4 with: name: python-preview-metadata path: pr-metadata/ ================================================ FILE: .github/workflows/dojo-e2e.yml ================================================ name: e2e on: workflow_dispatch: push: branches: [main] paths: - "integrations/**" - "apps/dojo/**" - "middlewares/**" - "pnpm-lock.yaml" - "pnpm-workspace.yaml" - ".github/workflows/dojo-e2e.yml" - "sdks/python/**" - "sdks/typescript/**" pull_request: branches: [main] paths: - "apps/dojo/**" - "integrations/**" - "middlewares/**" - "pnpm-lock.yaml" - "pnpm-workspace.yaml" - ".github/workflows/dojo-e2e.yml" - "sdks/python/**" - "sdks/typescript/**" jobs: check-generated-files: name: dojo / check-generated-files runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: "22" - name: Install pnpm uses: pnpm/action-setup@v4 with: version: 10.13.1 - name: Validate agentFilesMapper and regenerate files.json working-directory: apps/dojo run: pnpm generate-content-json - name: Check files.json is up to date working-directory: apps/dojo run: | if git diff --exit-code src/files.json > /dev/null; then echo "✅ No changes detected in dojo/src/files.json. Everything is up to date." else echo "❌ Detected changes in dojo/src/files.json." echo "" echo "The committed files.json doesn't match what would be generated." echo "Please run \`(p)npm run generate-content-json\` in the apps/dojo folder and commit the updated file." echo "" echo "The detected diff was as follows:" echo "::group::Diff for dojo/src/files.json" git diff src/files.json echo "::endgroup::" exit 1 fi dojo: name: dojo / ${{ matrix.suite }} needs: check-generated-files runs-on: depot-ubuntu-24.04 timeout-minutes: 20 strategy: fail-fast: false matrix: include: - suite: a2a-middleware test_path: tests/a2aMiddlewareTests services: ["dojo", "a2a-middleware"] wait_on: http://localhost:9999,http-get://localhost:8011/.well-known/agent.json,http-get://localhost:8012/.well-known/agent.json,http-get://localhost:8013/.well-known/agent.json,http-get://localhost:8014/openapi.json - suite: adk-middleware test_path: tests/adkMiddlewareTests services: ["dojo", "adk-middleware"] wait_on: http://localhost:9999,tcp:localhost:8010 - suite: agno test_path: tests/agnoTests services: ["dojo", "agno"] wait_on: http://localhost:9999,tcp:localhost:8002 - suite: crew-ai test_path: tests/crewAITests services: ["dojo", "crew-ai"] wait_on: http://localhost:9999,tcp:localhost:8003 - suite: langroid test_path: tests/langroidTests services: ["dojo", "langroid"] wait_on: http://localhost:9999,tcp:localhost:8021 - suite: langgraph-python test_path: tests/langgraphPythonTests services: ["dojo", "langgraph-platform-python"] wait_on: http://localhost:9999,tcp:localhost:8005 - suite: langgraph-typescript test_path: tests/langgraphTypescriptTests services: ["dojo", "langgraph-platform-typescript"] wait_on: http://localhost:9999,tcp:localhost:8006 - suite: langgraph-fastapi test_path: tests/langgraphFastAPITests services: ["dojo", "langgraph-fastapi"] wait_on: http://localhost:9999,tcp:localhost:8004 - suite: llama-index test_path: tests/llamaIndexTests services: ["dojo", "llama-index"] wait_on: http://localhost:9999,tcp:localhost:8007 - suite: mastra test_path: tests/mastraTests services: ["dojo", "mastra"] wait_on: http://localhost:9999,tcp:localhost:8008 - suite: mastra-agent-local test_path: tests/mastraAgentLocalTests services: ["dojo"] wait_on: http://localhost:9999 - suite: middleware-starter test_path: tests/middlewareStarterTests services: ["dojo"] wait_on: http://localhost:9999 - suite: pydantic-ai test_path: tests/pydanticAITests services: ["dojo", "pydantic-ai"] wait_on: http://localhost:9999,tcp:localhost:8009 - suite: server-starter test_path: tests/serverStarterTests services: ["dojo", "server-starter"] wait_on: http://localhost:9999,tcp:localhost:8000 - suite: server-starter-all test_path: tests/serverStarterAllFeaturesTests services: ["dojo", "server-starter-all"] wait_on: http://localhost:9999,tcp:localhost:8001 - suite: aws-strands test_path: tests/awsStrandsTests services: ["dojo", "aws-strands"] wait_on: http://localhost:9999,tcp:localhost:8017 - suite: claude-agent-sdk-python test_path: tests/claudeAgentSdkPythonTests services: ["dojo", "claude-agent-sdk-python"] wait_on: http://localhost:9999,tcp:localhost:8019 - suite: claude-agent-sdk-typescript test_path: tests/claudeAgentSdkTypescriptTests services: ["dojo", "claude-agent-sdk-typescript"] wait_on: http://localhost:9999,tcp:localhost:8020 # - suite: vercel-ai-sdk # test_path: tests/vercelAISdkTests # services: ["dojo"] # wait_on: http://localhost:9999 steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: "22" - name: Install pnpm uses: pnpm/action-setup@v4 with: version: 10.13.1 # Now that pnpm is available, cache its store to speed installs - name: Resolve pnpm store path id: pnpm-store run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - name: Cache pnpm store uses: actions/cache@v4 with: path: ${{ env.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- - name: Cache Python dependencies uses: actions/cache@v4 with: path: | ~/.cache/pip ~/.cache/pypoetry ~/.cache/uv **/.venv key: ${{ runner.os }}-pydeps-${{ matrix.suite }}-${{ hashFiles('**/poetry.lock', '**/pyproject.toml') }} restore-keys: | ${{ runner.os }}-pydeps-${{ matrix.suite }}- ${{ runner.os }}-pydeps- - name: Cache Next.js build uses: actions/cache@v4 with: path: ${{ github.workspace }}/apps/dojo/.next/cache key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('apps/dojo/src/**/*.ts', 'apps/dojo/src/**/*.tsx', 'apps/dojo/src/**/*.js', 'apps/dojo/src/**/*.jsx') }} restore-keys: | ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}- - name: Install Poetry uses: snok/install-poetry@v1 with: version: latest virtualenvs-create: true virtualenvs-in-project: true - name: Install uv uses: astral-sh/setup-uv@v6 - name: Install dependencies run: pnpm install --frozen-lockfile - name: Prepare dojo for e2e working-directory: apps/dojo if: ${{ join(matrix.services, ',') != '' }} run: node ./scripts/prep-dojo-everything.js --only ${{ join(matrix.services, ',') }} - name: Cache Playwright browsers id: cache-playwright uses: actions/cache@v4 with: path: ~/.cache/ms-playwright key: ${{ runner.os }}-playwright-${{ hashFiles('apps/dojo/e2e/package.json') }} restore-keys: | ${{ runner.os }}-playwright- - name: Install e2e dependencies working-directory: apps/dojo/e2e run: pnpm install --ignore-scripts - name: Install Playwright browsers working-directory: apps/dojo/e2e if: steps.cache-playwright.outputs.cache-hit != 'true' run: pnpm exec playwright install --with-deps chromium - name: Install Playwright system dependencies working-directory: apps/dojo/e2e if: steps.cache-playwright.outputs.cache-hit == 'true' run: pnpm exec playwright install-deps chromium - name: Create langgraph stub .env files if: ${{ contains(join(matrix.services, ','), 'langgraph-platform-python') || contains(join(matrix.services, ','), 'langgraph-platform-typescript') }} run: | # langgraph.json declares "env": ".env" — the CLI requires this file # to exist on disk. Values don't matter since run-dojo-everything.js # injects LLMock env vars into the process environment. touch integrations/langgraph/python/examples/.env touch integrations/langgraph/typescript/examples/.env - name: write langroid env files working-directory: integrations/langroid/python/examples env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} if: ${{ contains(join(matrix.services, ','), 'langroid') }} run: | echo "OPENAI_API_KEY=${OPENAI_API_KEY}" > .env - name: Run dojo+agents uses: JarvusInnovations/background-action@v1 if: ${{ join(matrix.services, ',') != '' && contains(join(matrix.services, ','), 'dojo') }} with: run: | node ../scripts/run-dojo-everything.js --only ${{ join(matrix.services, ',') }} working-directory: apps/dojo/e2e wait-on: ${{ matrix.wait_on }} wait-for: 300000 - name: Run tests – ${{ matrix.suite }} working-directory: apps/dojo/e2e env: BASE_URL: http://localhost:9999 PLAYWRIGHT_SUITE: ${{ matrix.suite }} run: | pnpm test -- ${{ matrix.test_path }} - name: Upload traces – ${{ matrix.suite }} if: always() # Uploads artifacts even if tests fail uses: actions/upload-artifact@v4 with: name: ${{ matrix.suite }}-playwright-traces path: | apps/dojo/e2e/test-results/${{ matrix.suite }}/**/* apps/dojo/e2e/playwright-report/**/* retention-days: 7 ================================================ FILE: .github/workflows/pr-check-binaries.yml ================================================ name: Check for binary artifacts on: pull_request: types: [opened, synchronize, reopened] permissions: contents: read jobs: check-binaries: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Check for binary and build artifacts env: BASE_REF: ${{ github.event.pull_request.base.ref }} run: | VIOLATIONS=0 # Get list of added/modified files in the PR CHANGED_FILES=$(git diff --name-only "origin/${BASE_REF}...HEAD") if [ -z "$CHANGED_FILES" ]; then echo "No changed files detected." exit 0 fi # Check for binary file extensions BINARY_FILES=$(echo "$CHANGED_FILES" | grep -iE '\.(exe|dll|so|dylib|o|obj|a|lib|wasm)$' || true) if [ -n "$BINARY_FILES" ]; then echo "::error::Binary files detected in PR:" echo "$BINARY_FILES" VIOLATIONS=1 fi # Check for build directories BUILD_FILES=$(echo "$CHANGED_FILES" | grep -E '/build/' || true) if [ -n "$BUILD_FILES" ]; then echo "::error::Files in build directories detected in PR:" echo "$BUILD_FILES" VIOLATIONS=1 fi # Check for dSYM directories DSYM_FILES=$(echo "$CHANGED_FILES" | grep -E '\.dSYM/' || true) if [ -n "$DSYM_FILES" ]; then echo "::error::dSYM debug symbol directories detected in PR:" echo "$DSYM_FILES" VIOLATIONS=1 fi # Check for large files (>1MB) among changed files # Exclude known generated files that are committed intentionally LARGE_FILE_EXCLUDES="apps/dojo/src/files.json" LARGE_FILES="" while IFS= read -r file; do if [ -f "$file" ] && ! echo "$LARGE_FILE_EXCLUDES" | grep -qF "$file"; then SIZE=$(wc -c < "$file" | tr -d ' ') if [ "$SIZE" -gt 1048576 ]; then LARGE_FILES="${LARGE_FILES}${file} ($(( SIZE / 1024 )) KB)\n" fi fi done <<< "$CHANGED_FILES" if [ -n "$LARGE_FILES" ]; then echo "::error::Files over 1 MB detected in PR:" echo -e "$LARGE_FILES" VIOLATIONS=1 fi if [ "$VIOLATIONS" -eq 1 ]; then echo "" echo "This PR contains binary artifacts, build outputs, or oversized files." echo "Please remove them and update your .gitignore if needed." exit 1 fi echo "No binary artifacts or oversized files detected." ================================================ FILE: .github/workflows/publish-commit.yml ================================================ name: 🚀 pkg-pr-new on: [push, pull_request] concurrency: group: ${{ github.repository }}-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Install pnpm uses: pnpm/action-setup@v4 - run: corepack enable - uses: actions/setup-node@v4 with: node-version-file: "package.json" - name: Install dependencies run: pnpm install - name: Build run: pnpm run build - name: Publish via pkg-pr-new run: | npx pkg-pr-new publish --pnpm --packageManager pnpm ./sdks/typescript/packages/* ./middlewares/* ./integrations/*/typescript || \ (sleep 10 && npx pkg-pr-new publish --pnpm --packageManager pnpm ./sdks/typescript/packages/* ./middlewares/* ./integrations/*/typescript) ================================================ FILE: .github/workflows/publish-java-sdk.yml ================================================ name: Publish Java SDK to Maven Central on: # Manual trigger only - no automatic publishing workflow_dispatch: jobs: publish: runs-on: ubuntu-latest defaults: run: working-directory: sdks/community/java permissions: contents: read steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up JDK 21 uses: actions/setup-java@v4 with: java-version: "21" distribution: "temurin" cache: 'maven' server-id: ossrh server-username: MAVEN_USERNAME server-password: MAVEN_PASSWORD gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} gpg-passphrase: MAVEN_GPG_PASSPHRASE - name: Publish to Maven Central run: mvn --batch-mode deploy -P release env: MAVEN_USERNAME: ${{ secrets.SONATYPE_USERNAME }} MAVEN_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} ================================================ FILE: .github/workflows/publish-kotlin-sdk.yml ================================================ name: Publish Kotlin SDK to Maven Central on: # Manual trigger only - no automatic publishing workflow_dispatch: inputs: dry_run: description: 'Run in dry-run mode (test without uploading)' required: false type: boolean default: false jobs: publish: runs-on: macos-latest permissions: contents: read steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up JDK 21 uses: actions/setup-java@v4 with: java-version: "21" distribution: "temurin" - name: Setup Gradle uses: gradle/gradle-build-action@v3 - name: Install Android SDK uses: android-actions/setup-android@v3 - name: Install Android SDK 36 components run: | echo "Installing Android SDK 36 components..." sdkmanager --install "platforms;android-36" sdkmanager --install "build-tools;36.0.0" - name: Accept Android licenses run: yes | sdkmanager --licenses || true - name: Verify Android SDK installation run: | echo "Checking Android SDK installation..." sdkmanager --list_installed | grep -E "(platforms;android-36|build-tools;36)" - name: Run tests working-directory: sdks/community/kotlin/library run: ./gradlew allTests --no-daemon --stacktrace - name: Parse test results if: always() working-directory: sdks/community/kotlin/library run: | echo "## Kotlin SDK Test Results Summary" echo "" total_tests=0 total_failures=0 total_errors=0 for module in core client tools; do xml_dir="$module/build/test-results/jvmTest" if [ -d "$xml_dir" ]; then # Sum up test counts from all XML files in the directory module_tests=$(find "$xml_dir" -name "*.xml" -exec grep -h '> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "The Kotlin SDK has been published to Maven Central." >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### Published Artifacts" >> $GITHUB_STEP_SUMMARY echo "- \`com.ag-ui.community:kotlin-core:${VERSION}\` (JVM, Android, iOS)" >> $GITHUB_STEP_SUMMARY echo "- \`com.ag-ui.community:kotlin-client:${VERSION}\` (JVM, Android, iOS)" >> $GITHUB_STEP_SUMMARY echo "- \`com.ag-ui.community:kotlin-tools:${VERSION}\` (JVM, Android, iOS)" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Note:** All platforms published including iOS artifacts in .klib format." >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### Next Steps" >> $GITHUB_STEP_SUMMARY echo "1. Check deployment status: https://central.sonatype.com/publishing" >> $GITHUB_STEP_SUMMARY echo "2. Artifacts will be validated automatically" >> $GITHUB_STEP_SUMMARY echo "3. Publishing completes in ~10-30 minutes" >> $GITHUB_STEP_SUMMARY - name: Dry-run Summary if: success() && inputs.dry_run == true run: | echo "## ✅ Dry-run Complete!" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "The dry-run completed successfully. No artifacts were uploaded." >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Run without the dry-run flag to publish to Maven Central." >> $GITHUB_STEP_SUMMARY ================================================ FILE: .github/workflows/publish-python-package.yml ================================================ name: Publish Python Package to PyPI on: # Manual trigger only - no automatic publishing workflow_dispatch: inputs: package: description: 'Package to publish' required: true type: choice options: - sdks/python - integrations/adk-middleware/python - integrations/agent-spec/python - integrations/aws-strands/python - integrations/crew-ai/python - integrations/langgraph/python dry_run: description: 'Run in dry-run mode (build without uploading)' required: false type: boolean default: false jobs: publish: runs-on: ubuntu-latest permissions: contents: read id-token: write # Required for PyPI trusted publishing steps: - name: Checkout code uses: actions/checkout@v4 - name: Authorize actor against CODEOWNERS env: ACTOR: ${{ github.actor }} PACKAGE: ${{ inputs.package }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: npx tsx scripts/check-codeowners-auth.ts - name: Install uv uses: astral-sh/setup-uv@v4 with: version: ">=0.8.0" - name: Install dependencies working-directory: ${{ inputs.package }} run: uv sync - name: Run tests working-directory: ${{ inputs.package }} run: | TEST_CMD=$(uv run python -c " import tomllib, sys cfg = tomllib.load(open('pyproject.toml', 'rb')) try: cmd = cfg['tool']['ag-ui']['scripts']['test'] except KeyError: print('ERROR: No test script configured in [tool.ag-ui.scripts]', file=sys.stderr) sys.exit(1) print(cmd) ") uv run $TEST_CMD - name: Build package working-directory: ${{ inputs.package }} run: uv build - name: Verify wheel permissions working-directory: ${{ inputs.package }} run: | echo "## Wheel file permissions check" uv run python -c " import zipfile, glob, sys whl = glob.glob('dist/*.whl')[0] print(f'Checking {whl}') bad = [] for info in zipfile.ZipFile(whl).infolist(): perms = (info.external_attr >> 16) & 0o777 readable = perms & 0o444 print(f' {oct(perms):>8s} {info.filename}') if not readable: bad.append(info.filename) if bad: print(f'ERROR: {len(bad)} file(s) missing read permissions:') for f in bad: print(f' - {f}') sys.exit(1) print('All files have correct permissions.') " - name: Publish to PyPI if: inputs.dry_run == false working-directory: ${{ inputs.package }} run: uv publish env: UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }} - name: Summary if: success() working-directory: ${{ inputs.package }} run: | NAME=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['name'])") VERSION=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])") if [ "${{ inputs.dry_run }}" = "true" ]; then echo "## ✅ Dry-run Complete!" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Built **${NAME} ${VERSION}** successfully. No artifacts were uploaded." >> $GITHUB_STEP_SUMMARY else echo "## ✅ Published ${NAME} ${VERSION} to PyPI" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Install with: \`pip install ${NAME}==${VERSION}\`" >> $GITHUB_STEP_SUMMARY fi ================================================ FILE: .github/workflows/publish-python-preview.yml ================================================ name: Publish Python Preview to TestPyPI # Triggered when the build workflow completes. Runs in the base repo context, # so it has access to secrets even for fork PRs. The code executed here comes # from the base branch, not the fork — only the built wheel artifacts come # from the fork's workflow run. on: workflow_run: workflows: ["Build Python Preview"] types: [completed] jobs: publish: runs-on: ubuntu-latest if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' permissions: actions: read pull-requests: write steps: - name: Download dist artifacts uses: actions/download-artifact@v4 with: name: python-preview-dist path: dist-preview/ run-id: ${{ github.event.workflow_run.id }} github-token: ${{ github.token }} - name: Download PR metadata uses: actions/download-artifact@v4 with: name: python-preview-metadata path: pr-metadata/ run-id: ${{ github.event.workflow_run.id }} github-token: ${{ github.token }} - name: Read PR metadata id: meta run: | echo "pr-number=$(cat pr-metadata/pr-number)" >> "$GITHUB_OUTPUT" echo "version=$(cat pr-metadata/version)" >> "$GITHUB_OUTPUT" echo "sha=$(cat pr-metadata/sha)" >> "$GITHUB_OUTPUT" - name: Install uv uses: astral-sh/setup-uv@v4 with: version: ">=0.8.0" - name: Publish all packages to TestPyPI run: | echo "Publishing artifacts:" ls -1 dist-preview/ uv publish \ --publish-url https://test.pypi.org/legacy/ \ --check-url https://test.pypi.org/simple/ \ dist-preview/* env: UV_PUBLISH_TOKEN: ${{ secrets.TEST_PYPI_API_TOKEN }} - name: Find existing preview comment if: always() id: find-comment uses: peter-evans/find-comment@v4 with: issue-number: ${{ steps.meta.outputs.pr-number }} comment-author: 'github-actions[bot]' body-includes: '' - name: Post or update install instructions if: success() uses: peter-evans/create-or-update-comment@v4 with: comment-id: ${{ steps.find-comment.outputs.comment-id }} issue-number: ${{ steps.meta.outputs.pr-number }} edit-mode: replace body: | ## Python Preview Packages Version `${{ steps.meta.outputs.version }}` published to [TestPyPI](https://test.pypi.org). > **Warning**: These packages are built from contributor code that may not yet have been vetted for correctness or security. Install at your own risk and do not use in production. ### Install with uv Add the TestPyPI index to your `pyproject.toml`: ```toml [[tool.uv.index]] name = "testpypi" url = "https://test.pypi.org/simple/" explicit = true ``` Then install the packages you need: ```bash # Core SDK uv add 'ag-ui-protocol==${{ steps.meta.outputs.version }}' --index testpypi # Integrations (each already depends on the matching ag-ui-protocol preview) uv add 'ag-ui-langgraph==${{ steps.meta.outputs.version }}' --index testpypi uv add 'ag-ui-crewai==${{ steps.meta.outputs.version }}' --index testpypi # NOTE: ag-ui-agent-spec depends on pyagentspec (git-only, not on PyPI). # You will need to install pyagentspec separately from its git repo. uv add 'ag-ui-agent-spec==${{ steps.meta.outputs.version }}' --index testpypi uv add 'ag_ui_adk==${{ steps.meta.outputs.version }}' --index testpypi uv add 'ag_ui_strands==${{ steps.meta.outputs.version }}' --index testpypi ``` ### Install with pip ```bash pip install \ --index-url https://test.pypi.org/simple/ \ --extra-index-url https://pypi.org/simple/ \ ag-ui-protocol==${{ steps.meta.outputs.version }} ``` > Use `--extra-index-url https://pypi.org/simple/` so pip can resolve > transitive dependencies (pydantic, fastapi, etc.) from real PyPI. --- _Commit: ${{ steps.meta.outputs.sha }}_ - name: Post failure comment if: failure() uses: peter-evans/create-or-update-comment@v4 with: comment-id: ${{ steps.find-comment.outputs.comment-id }} issue-number: ${{ steps.meta.outputs.pr-number }} edit-mode: replace body: | ## Python Preview Packages — Publish Failed Preview publish failed for commit ${{ steps.meta.outputs.sha }}. See the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details. ================================================ FILE: .github/workflows/rust-lint-test.yml ================================================ name: test on: push: branches: [ "main" ] paths: - "crates/**" - ".github/workflows/rust.yml" - "tests/**" - "Cargo.toml" - ".cargo/**" pull_request: branches: [ "main" ] paths: - "sdks/community/rust/crates/**" - "sdks/community/rust/**/tests/**" - "sdks/community/rust/Cargo.toml" - "sdks/community/rust/.cargo/**" - ".github/workflows/rust-lint-test.yml" defaults: run: working-directory: ./rust jobs: rust: strategy: fail-fast: false matrix: os: [ ubuntu-latest, macos-latest, windows-latest ] name: Rust SDK Tests [${{ matrix.os }}] runs-on: ${{ matrix.os }} env: CARGO_TERM_COLOR: always steps: - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 - name: Build run: cargo build --verbose - name: Check formatting run: cargo fmt -- --check - name: Check clippy run: cargo clippy -- -D warnings - name: Publish ag-ui-core dry-run run: cargo publish -p ag-ui-core --dry-run - name: Publish ag-ui-client dry-run run: cargo publish -p ag-ui-client --dry-run - name: Run tests run: cargo test --verbose ================================================ FILE: .github/workflows/unit-dart-sdk.yml ================================================ name: unit on: push: branches: [main] paths: - "sdks/community/dart/**" - ".github/workflows/unit-dart-sdk.yml" pull_request: branches: [main] paths: - "sdks/community/dart/**" - ".github/workflows/unit-dart-sdk.yml" jobs: dart: runs-on: ubuntu-latest defaults: run: working-directory: sdks/community/dart steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Dart uses: dart-lang/setup-dart@v1 with: sdk: stable - name: Install dependencies run: dart pub get - name: Run tests run: dart test --exclude-tags requires-server ================================================ FILE: .github/workflows/unit-genkit-go.yml ================================================ name: unit on: push: branches: [main] paths: - "integrations/community/genkit/go/**" - ".github/workflows/unit-genkit-go.yml" pull_request: branches: [main] paths: - "integrations/community/genkit/go/**" - ".github/workflows/unit-genkit-go.yml" jobs: go-genkit: name: Go Genkit Integration Tests runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: "1.25.0" - name: Setup Go module cache uses: actions/cache@v4 with: path: | ~/go/pkg/mod ~/.cache/go-build key: ${{ runner.os }}-go-genkit-${{ hashFiles('integrations/community/genkit/go/genkit/go.sum') }} restore-keys: | ${{ runner.os }}-go-genkit- - name: Download dependencies working-directory: integrations/community/genkit/go/genkit run: go mod download - name: Run tests working-directory: integrations/community/genkit/go/genkit run: go test ./... -v ================================================ FILE: .github/workflows/unit-go-sdk.yml ================================================ name: unit on: push: branches: [main] paths: - "sdks/community/go/**" - ".github/workflows/unit-go-sdk.yml" pull_request: branches: [main] paths: - "sdks/community/go/**" - ".github/workflows/unit-go-sdk.yml" jobs: go: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: "1.24.4" - name: Setup Go module cache uses: actions/cache@v4 with: path: | ~/go/pkg/mod ~/.cache/go-build key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: Download dependencies working-directory: sdks/community/go run: go mod download - name: Run tests working-directory: sdks/community/go run: go test ./... -v ================================================ FILE: .github/workflows/unit-java-sdk.yml ================================================ name: unit on: push: branches: [main] paths: - "sdks/community/java/**" - ".github/workflows/unit-java-sdk.yml" pull_request: branches: [main] paths: - "sdks/community/java/**" - ".github/workflows/unit-java-sdk.yml" jobs: java: runs-on: ubuntu-latest defaults: run: working-directory: sdks/community/java steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Java uses: actions/setup-java@v4 with: distribution: "temurin" java-version: "17" cache: "maven" - name: Run tests run: mvn -B -ntp test ================================================ FILE: .github/workflows/unit-kotlin-sdk.yml ================================================ name: unit on: push: branches: [main] paths: - "sdks/community/kotlin/**" - ".github/workflows/unit-kotlin-sdk.yml" pull_request: branches: [main] paths: - "sdks/community/kotlin/**" - ".github/workflows/unit-kotlin-sdk.yml" jobs: kotlin: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up JDK 21 uses: actions/setup-java@v4 with: java-version: "21" distribution: "temurin" - name: Setup Gradle uses: gradle/gradle-build-action@v3 - name: Run JVM tests working-directory: sdks/community/kotlin/library run: ./gradlew jvmTest --no-daemon --stacktrace - name: Parse test results if: always() working-directory: sdks/community/kotlin/library run: | echo "## Kotlin SDK Test Results Summary" echo "" total_tests=0 total_failures=0 total_errors=0 for module in core client tools; do xml_dir="$module/build/test-results/jvmTest" if [ -d "$xml_dir" ]; then # Sum up test counts from all XML files in the directory module_tests=$(find "$xml_dir" -name "*.xml" -exec grep -h '=0.8.0" - name: Load cached venv id: cached-uv-dependencies uses: actions/cache@v4 with: path: sdks/python/.venv key: venv-${{ runner.os }}-${{ hashFiles('sdks/python/uv.lock') }} - name: Install dependencies working-directory: sdks/python run: uv sync - name: Run tests working-directory: sdks/python run: uv run python -m unittest discover tests -v ================================================ FILE: .github/workflows/unit-ruby-sdk.yml ================================================ name: unit on: push: branches: [main] paths: - "sdks/community/ruby/**" - ".github/workflows/unit-ruby-sdk.yml" pull_request: branches: [main] paths: - "sdks/community/ruby/**" - ".github/workflows/unit-ruby-sdk.yml" jobs: ruby: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: ruby/setup-ruby@v1 with: ruby-version: '3.4' bundler-cache: true working-directory: sdks/community/ruby - run: bundle exec rake working-directory: sdks/community/ruby ================================================ FILE: .github/workflows/unit-typescript-sdk.yml ================================================ name: unit on: push: branches: [main] paths: - "sdks/typescript/**" - "typescript-sdk/**" - "integrations/**" - "pnpm-lock.yaml" - "pnpm-workspace.yaml" - "package.json" - "nx.json" - ".github/workflows/unit-typescript-sdk.yml" pull_request: branches: [main] paths: - "sdks/typescript/**" - "typescript-sdk/**" - "integrations/**" - "pnpm-lock.yaml" - "pnpm-workspace.yaml" - "package.json" - "nx.json" - ".github/workflows/unit-typescript-sdk.yml" jobs: typescript: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: "22" - name: Install protoc uses: arduino/setup-protoc@v3 with: version: "25.x" repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Install pnpm uses: pnpm/action-setup@v4 with: version: 10.13.1 - name: Setup pnpm cache uses: actions/cache@v4 with: path: ~/.local/share/pnpm/store key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- - name: Install dependencies run: pnpm install --frozen-lockfile - name: Test Build run: pnpm run build - name: Run tests run: pnpm run test ================================================ FILE: .gitignore ================================================ **/.claude/settings.local.json .claude/worktrees # Test coverage coverage/ mastra.db* **/.DS_Store test-results/ **/target .nx/cache .nx/workspace-data node_modules .vscode **/mastra.db* .pnpm-store **/.poetry-cache *.egg-info **/python/**/__pycache__/ **/python/**/.venv/ **/typescript/**/node_modules/ **/typescript/**/dist/ # Turborepo .turbo **/.turbo # Build artifacts and binaries **/build/ *.dSYM/ *.exe *.dll *.so *.dylib *.o *.obj *.a *.lib *.wasm ================================================ FILE: .mcp.json ================================================ { "mcpServers": { "nx-mcp": { "type": "stdio", "command": "npx", "args": [ "nx", "mcp" ] } } } ================================================ FILE: AGENTS.md ================================================ # General Guidelines for working with Nx - When running tasks (for example build, lint, test, e2e, etc.), always prefer running the task through `nx` (i.e. `nx run`, `nx run-many`, `nx affected`) instead of using the underlying tooling directly - You have access to the Nx MCP server and its tools, use them to help the user - When answering questions about the repository, use the `nx_workspace` tool first to gain an understanding of the workspace architecture where applicable. - When working in individual projects, use the `nx_project_details` mcp tool to analyze and understand the specific project structure and dependencies - For questions around nx configuration, best practices or if you're unsure, use the `nx_docs` tool to get relevant, up-to-date docs. Always use this instead of assuming things about nx configuration - If the user needs help with an Nx configuration or project graph error, use the `nx_workspace` tool to get any errors - For Nx plugin best practices, check `node_modules/@nx//PLUGIN.md`. Not all plugins have this file - proceed without it if unavailable. ================================================ FILE: CLAUDE.md ================================================ # CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Common Development Commands ### TypeScript SDK (Main Development) ```bash # Install dependencies (using pnpm) pnpm install # Build all packages pnpm build # Run development mode pnpm dev # Run linting pnpm lint # Run type checking pnpm check-types # Run tests pnpm test # Format code pnpm format # Clean build artifacts pnpm clean # Full clean build pnpm build:clean ``` ### Python SDK ```bash # Navigate to python-sdk directory cd python-sdk # Install dependencies (using poetry) poetry install # Run tests python -m unittest discover tests # Build distribution poetry build ``` ### Running Specific Integration Tests ```bash # For TypeScript packages/integrations cd packages/ pnpm test # For running a single test file cd packages/ pnpm test -- path/to/test.spec.ts ``` ## High-Level Architecture AG-UI is an event-based protocol that standardizes agent-user interactions. The codebase is organized as a monorepo with the following structure: ### Core Protocol Architecture - **Event-Driven Communication**: All agent-UI communication happens through typed events (BaseEvent and its subtypes) - **Transport Agnostic**: Protocol supports SSE, WebSockets, HTTP binary, and custom transports - **Observable Pattern**: Uses RxJS Observables for streaming agent responses ### Key Abstractions 1. **AbstractAgent**: Base class that all agents must implement with a `run(input: RunAgentInput) -> Observable` method 2. **HttpAgent**: Standard HTTP client supporting SSE and binary protocols for connecting to agent endpoints 3. **Event Types**: Lifecycle events (RUN_STARTED/FINISHED), message events (TEXT_MESSAGE_*), tool events (TOOL_CALL_*), and state management events (STATE_SNAPSHOT/DELTA) ### Repository Structure - `/sdks/typescript/`: Main TypeScript implementation - `/packages/`: Core protocol packages (@ag-ui/core, @ag-ui/client, @ag-ui/encoder, @ag-ui/proto) - `/integrations/`: Framework integrations (langgraph, mastra, crewai, etc.) - `/apps/`: Example applications including the AG-UI Dojo demo viewer - `/sdks/python/`: Python implementation of the protocol - `/docs/`: Documentation site content ### Integration Pattern Each framework integration follows a similar pattern: 1. Implements the AbstractAgent interface 2. Translates framework-specific events to AG-UI protocol events 3. Provides both TypeScript client and Python server implementations 4. Includes examples demonstrating key AG-UI features (agentic chat, generative UI, human-in-the-loop, etc.) ### State Management - Uses STATE_SNAPSHOT for complete state representations - Uses STATE_DELTA with JSON Patch (RFC 6902) for efficient incremental updates - MESSAGES_SNAPSHOT provides conversation history ### Multiple Sequential Runs - AG-UI supports multiple sequential runs in a single event stream - Each run must complete (RUN_FINISHED) before a new run can start (RUN_STARTED) - Messages accumulate across runs (e.g., messages from run1 + messages from run2) - State continues to evolve across runs unless explicitly reset with STATE_SNAPSHOT - Run-specific tracking (active messages, tool calls, steps) resets between runs ### Development Workflow - Nx is used for monorepo build orchestration - Each package has independent versioning - Integration tests demonstrate protocol compliance - The AG-UI Dojo app showcases all protocol features with live examples # General Guidelines for working with Nx - When running tasks (for example build, lint, test, e2e, etc.), always prefer running the task through `nx` (i.e. `nx run`, `nx run-many`, `nx affected`) instead of using the underlying tooling directly - You have access to the Nx MCP server and its tools, use them to help the user - When answering questions about the repository, use the `nx_workspace` tool first to gain an understanding of the workspace architecture where applicable. - When working in individual projects, use the `nx_project_details` mcp tool to analyze and understand the specific project structure and dependencies - For questions around nx configuration, best practices or if you're unsure, use the `nx_docs` tool to get relevant, up-to-date docs. Always use this instead of assuming things about nx configuration - If the user needs help with an Nx configuration or project graph error, use the `nx_workspace` tool to get any errors - For Nx plugin best practices, check `node_modules/@nx//PLUGIN.md`. Not all plugins have this file - proceed without it if unavailable. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to AG-UI Thanks for checking out AG-UI! Whether you're here to fix a bug, ship a feature, improve the docs, or just figure out how things work—we're glad you're here. Here's how to get involved: --- ## Have a Question or Ran Into Something? Pick the right spot so we can help you faster: - **I want to contribute [Fixes / Feature Requests]** → [GitHub Issues](https://github.com/ag-ui-protocol/ag-ui/issues) - **"How do I...?** → [Discord](https://discord.gg/Jd3FzfdJa8) → `#-💎-contributing` - **Introduce Yourself** → [Discord](https://discord.gg/Jd3FzfdJa8) → `🤝-intro` --- ## Want to Contribute Code? First, an important plea: **Please PLEASE reach out to us first before starting any significant work on new or existing features.** We love community contributions! That said, we want to make sure we're all on the same page before you start. Investing a lot of time and effort just to find out it doesn't align with the upstream project feels awful, and we don't want that to happen. It also helps to make sure the work you're planning isn't already in progress. If you'd confirmed that the **[x]** work hasn't been started yet, please file an issue first: https://github.com/ag-ui-protocol/ag-ui/issues 1. **Find Something to Work On** Browse open issues on [GitHub](https://github.com/ag-ui-protocol/ag-ui/issues). Got your own idea? Open an issue first so we can start the discussion. 2. **Ask to Be Assigned** Comment on the issue and tag a code owner: → [Code Owners](https://github.com/ag-ui-protocol/ag-ui/blob/main/.github/CODEOWNERS) 3. **Get on the Roadmap** Once approved, you'll be assigned the issue, and it'll get added to our [roadmap](https://github.com/orgs/ag-ui-protocol/projects/1). 4. **Coordinate With Others** - If you're collaborating or need feedback, start a thread in `#-💎-contributing` on Discord - Or just DM the assignee directly 5. **Open a Pull Request** - When you're ready, submit your PR - In the description, include: `Fixes #` (This links your PR to the issue and closes it automatically) 6. **Review & Merge** - A maintainer will review your code and leave comments if needed - Once it's approved, we'll merge it and move the issue to "done." **NOTE:** All community integrations (ie, .NET, Golang SDK, etc.) will need to be maintained by the community member who made the contribution. --- ## Step-by-Step Guide to Adding an Integration PR This guide walks you through everything needed to submit an integration PR to AG-UI. It covers adding the integration code, examples, dojo configuration, end-to-end tests, and CI setup. Use existing integrations in `integrations/` (e.g., `integrations/adk-middleware/` or `integrations/langgraph/`) as reference implementations throughout. ### Step 1: Add Your Integration Folder Your integration code goes inside the `integrations/` folder, under a subfolder named after your integration (e.g., `integrations/my-framework/`). - **Language subfolder** — Organize by language. For example, if your integration is in Python, place it under `integrations/my-framework/python/`. If it supports multiple languages (e.g., Python and Rust), use separate subfolders like `python/` and `rust/`. - **Examples subfolder** — Include an `examples/` directory inside your language folder (e.g., `integrations/my-framework/python/examples/`). The dojo examples must live here, but you can include additional examples as well. - **TypeScript client folder (required)** — No matter what language the integration is in, you must also include a `typescript/` folder. At minimum, this contains the TypeScript client code that re-exports the HTTP agent. You can copy this from an existing integration like `integrations/adk-middleware/typescript/` as a reference. It includes a `package.json`, TypeScript config, and the client code itself. If your framework natively supports TypeScript, the full TypeScript implementation should also live in this package. **Example structure:** ``` integrations/my-framework/ ├── python/ │ ├── examples/ # Dojo examples live here │ │ ├── pyproject.toml │ │ └── ... │ ├── pyproject.toml # Integration package │ └── ... └── typescript/ ├── package.json ├── tsconfig.json └── src/ └── index.ts # Re-exports the HTTP agent ``` ### Step 2: Register Your Integration in the Dojo You need to update three files inside `apps/dojo/src/` to make the dojo aware of your integration: - **`agents.ts`** — Add an entry for your integration. The **object key** you choose is important because it must match exactly in the other configuration files. If your framework supports multiple variants — different languages, runtimes, or transport modes — each variant gets its own separate entry. For example, LangGraph has entries for LangGraph Platform (Python), LangGraph FastAPI (Python), and LangGraph TypeScript. - **`menu.ts`** — Add your integration to the sidebar menu. The **`id`** must match the object key you used in `agents.ts`. The **`name`** is the human-readable display label shown in the left sidebar and does not need to match the ID. Each entry also defines which features it supports (e.g., `agentic_chat`, `human_in_the_loop`, `agentic_generative_ui`). This file is the single source of truth for integration configuration. - **`env.ts`** — Define the environment variable for your agent's hosted URL (one per agent). This is how the dojo knows where to reach your agent at runtime. The default should match whatever host/port your example code uses. ### Step 3: Configure the Agent Mapping Each entry in `agents.ts` contains a mapping of feature keys. This is typically a one-to-one mapping where each key corresponds to one agent. For most integrations, this is simple — one feature maps to one agent name. If your framework handles multiple agents talking together, there may be multiple agents listed, but each still gets its own entry. ### Step 4: Set Up Environment Variables Your example code must: - **Bind to host `0.0.0.0`** (or be overridable via the `HOST` environment variable) - **Respect the `PORT` environment variable** — when the dojo sets a specific port, your agent must bind to that exact port The port values defined in `env.ts` must match the URLs configured in `agents.ts`. If they don't line up, the dojo won't be able to find your agent. ### Step 5: Add Dojo Scripts Add entries for your integration in the dojo script configuration at `apps/dojo/scripts/`. There are two scripts to update: - **`prep-dojo-everything.js`** — This is the "prepare" command. It installs dependencies and builds your module (e.g., `pnpm install`, `uv sync`, `poetry install`, `go build`). It does **not** start any servers. - **`run-dojo-everything.js`** — This is the "run" command. It starts your integration's agent server. In both scripts, you add an entry to the `ALL_TARGETS` object. The **object key must match** the key you used in `agents.ts`. Each entry includes: - The **name** for logging - The **command** to execute (e.g., `uv sync` for prep, `uv run ...` for run) - The **working directory** (pointing into your `integrations/` examples folder) - **Environment variables** (optional) — for example, `PORT` **Important rules for `run-dojo-everything.js`:** - The **ports must not collide** with any other integration. Pick the next highest available port number. - The `dojo` and `dojo-dev` entries in the same file need environment variables that point to your service's port, so the dojo knows where to reach your agent. - If your integration runs **multiple agents**, you can have multiple entries in run. See `a2a-middleware` for an example of this pattern. At this point, you should be able to spin up the dojo locally and see your integration working. ### Step 6: Add End-to-End Tests Every feature listed in your sidebar entry (in `menu.ts`) needs a corresponding end-to-end test. **Without tests, your PR will not be considered ready.** - **Create a test folder** for your integration inside `apps/dojo/e2e/tests/` (e.g., `apps/dojo/e2e/tests/myFrameworkTests/`). Each feature you support gets its own spec file inside this folder. - **Follow existing test patterns** — Look at how other integrations implement their tests. If other frameworks use shared helpers from `apps/dojo/e2e/featurePages/`, you should use `featurePages` too. However, some tests use framework-specific page objects in `apps/dojo/e2e/pages//`. If the same test for other frameworks lives in `pages/some-framework`, you'll need to copy it to `pages/my-framework` and adapt it for your integration. - **Run tests locally** before submitting your PR. From `apps/dojo/`, in one terminal: ```bash ./scripts/prep-dojo-everything.js --only dojo,my-framework ./scripts/run-dojo-everything.js --only dojo,my-framework ``` Then in a separate terminal, from `apps/dojo/e2e/`: ```bash pnpm install pnpm test tests/myFrameworkTests/ ``` ### Step 7: Add CI Configuration The end-to-end tests need to run in CI as well. Update the GitHub Actions workflow file at `.github/workflows/dojo-e2e.yml`: - **Add your integration to the test matrix** at the top of the workflow. The entry name must match the key you used in `agents.ts`. This tells CI which test path to use (e.g., `tests/myFrameworkTests`). - **Add a services section** that defines which services to build and run. The service names map back to the `prep-dojo` and `run-dojo` scripts. The CI workflow uses a `wait-on` command to check that services are responsive (via TCP/HTTP) before running tests. **Note:** Tests won't run by default on external PRs. The team will open a separate PR from within the repo to trigger CI, then merge the original contributor PR once tests pass. ### Step 8 (Optional): Update CODEOWNERS This step is only needed if you want to be added as a co-owner who can merge changes to your integration without core team review. If this applies to you, update the `.github/CODEOWNERS` file to add yourself alongside the team: ``` integrations/my-framework @ag-ui-protocol/copilotkit @your-github-username ``` For most contributors, this is not required — the core team already owns all paths by default. ### Quick Reference Checklist Use this checklist to verify your PR is complete before submitting: - [ ] Integration folder added under `integrations/` with language subfolder + examples - [ ] TypeScript client folder included (even for non-TS integrations) - [ ] `agents.ts` updated with integration entry and feature mapping (object key is the source of truth) - [ ] `menu.ts` updated with sidebar entry (`id` matches `agents.ts` key, `name` is human-readable) - [ ] `env.ts` updated with agent URL environment variable - [ ] Example code binds to `0.0.0.0` and respects `HOST`/`PORT` env vars - [ ] `prep-dojo-everything.js` and `run-dojo-everything.js` entries added (object keys match `agents.ts`) - [ ] Ports in `run-dojo-everything.js` do not collide with existing integrations - [ ] `dojo`/`dojo-dev` entries updated with env vars pointing to your service's port - [ ] End-to-end test spec files added for every supported feature - [ ] Tests pass locally - [ ] CI workflow matrix updated in `.github/workflows/dojo-e2e.yml` (entry name matches `agents.ts`) --- ## Contributing a Community SDK If you're adding a new language SDK (e.g., Go, Java, Kotlin, Ruby, Rust) rather than a framework integration, place it in the `sdks/community/` folder. The team will add you as a code owner for that SDK so you can push changes without needing core team sign-off. Documentation for community SDKs also lives inside that SDK folder. This is a separate process from adding an integration — see the steps above for framework integrations. --- ## Want to Contribute to the Docs? Docs are part of the codebase and super valuable—thanks for helping improve them! Here's how to contribute: 1. **Open an Issue First** - Open a [GitHub issue](https://github.com/ag-ui-protocol/ag-ui/issues) describing what you'd like to update or add. - Then comment and ask to be assigned. 2. **Submit a PR** - Once assigned, make your edits and open a pull request. - In the description, include: `Fixes #` (This links your PR to the issue and closes it automatically) - A maintainer will review it and merge if it looks good. That's it! Simple and appreciated. --- ## That's It! AG-UI is community-built, and every contribution helps shape where we go next. Big thanks for being part of it! ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2025 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # ag-ui Logo AG-UI: The Agent-User Interaction Protocol AG-UI is an open, lightweight, event-based protocol that standardizes how AI agents connect to user-facing applications. Built for simplicity and flexibility, it enables seamless integration between AI agents, real time user context, and user interfaces. ---
[![Version](https://img.shields.io/npm/v/@ag-ui/core?label=Version&color=6963ff&logo=npm&logoColor=white)](https://www.npmjs.com/package/@ag-ui/core) ![MIT](https://img.shields.io/github/license/copilotkit/copilotkit?color=%236963ff&label=License) ![Discord](https://img.shields.io/discord/1379082175625953370?logo=discord&logoColor=%23FFFFFF&label=Discord&color=%236963ff) Join our Discord →     Read the Docs →     Go to the AG-UI Dojo →     Follow us → 1600x680 ## 🚀 Getting Started Create a new AG-UI application in seconds: ```bash npx create-ag-ui-app my-agent-app ```

Useful Links:

- [The AG-UI Dojo](https://dojo.ag-ui.com/) - [Build AG-UI-powered applications(Quickstart)](https://docs.ag-ui.com/quickstart/applications) - [Build new AG-UI framework integrations (Quickstart)](https://go.copilotkit.ai/agui-contribute) - [Book a call to discuss an AG-UI integration with a new framework](https://calendly.com/markus-copilotkit/ag-ui) - [Join the Discord Community](https://discord.gg/Jd3FzfdJa8) ## What is AG-UI? AG-UI is an open, lightweight, event-based protocol for agent-human interaction, designed for simplicity & flexibility: - During agent executions, agent backends **emit events _compatible_ with one of AG-UI's ~16 standard event types** - Agent backends can **accept one of a few simple AG-UI compatible inputs** as arguments **AG-UI includes a flexible middleware layer** that ensures compatibility across diverse environments: - Works with **any event transport** (SSE, WebSockets, webhooks, etc.) - Allows for **loose event format matching**, enabling broad agent and app interoperability It also ships with a **reference HTTP implementation** and **default connector** to help teams get started fast. [Learn more about the specs →](https://go.copilotkit.ai/ag-ui-introduction) ## Why AG-UI? AG-UI was developed based on real-world requirements and practical experience building in-app agent interactions. ## Where does AGUI fit in the agentic protocol stack? AG-UI is complementary to the other 2 top agentic protocols - MCP gives agents tools - A2A allows agents to communicate with other agents - AG-UI brings agents into user-facing applications
The Agent Protocol Stack
## 🚀 Features - 💬 Real-time agentic chat with streaming - 🔄 Bi-directional state synchronization - 🧩 Generative UI and structured messages - 🧠 Real-time context enrichment - 🛠️ Frontend tool integration - 🧑‍💻 Human-in-the-loop collaboration ## 🛠 Supported Integrations AG-UI was born from CopilotKit's initial **partnership** with LangGraph and CrewAI - and brings the incredibly popular agent-user-interactivity infrastructure to the wider agentic ecosystem. **1st party** = the platforms that have AG‑UI built in and provide documentation for guidance. ## Frameworks | Framework | Status | AG-UI Resources | | ------------------------------------------------------------------ | ------------------------ | -------------------------------------------------------------------------------- | | Built-in Agent | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/direct-to-llm) | ### 🤝 Partnerships | Framework | Status | AG-UI Resources | | ---------- | ------- | ---------------- | | [LangGraph](https://www.langchain.com/langgraph) | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/langgraph/) 🎮 [Demos](https://dojo.ag-ui.com/langgraph-fastapi/feature/shared_state) | | [CrewAI](https://crewai.com/) | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/crewai-flows) 🎮 [Demos](https://dojo.ag-ui.com/crewai/feature/shared_state) | ### 🧩 1st Party | Framework | Status | AG-UI Resources | | ---------- | ------- | ---------------- | | [Microsoft Agent Framework](https://azure.microsoft.com/en-us/blog/introducing-microsoft-agent-framework/) | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/microsoft-agent-framework) 🎮 [Demos](https://dojo.ag-ui.com/microsoft-agent-framework-dotnet/feature/shared_state) | | [Google ADK](https://google.github.io/adk-docs/get-started/) | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/adk) 🎮 [Demos](https://dojo.ag-ui.com/adk-middleware/feature/shared_state?openCopilot=true) | | [AWS Strands Agents](https://github.com/strands-agents/sdk-python) | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/aws-strands) 🎮 [Demos](https://dojo.ag-ui.com/aws-strands/feature/shared_state) | | [Mastra](https://mastra.ai/) | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/mastra/) 🎮 [Demos](https://dojo.ag-ui.com/mastra/feature/tool_based_generative_ui) | | [Pydantic AI](https://github.com/pydantic/pydantic-ai) | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/pydantic-ai/) 🎮 [Demos](https://dojo.ag-ui.com/pydantic-ai/feature/shared_state) | | [Agno](https://github.com/agno-agi/agno) | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/agno/) 🎮 [Demos](https://dojo.ag-ui.com/agno/feature/tool_based_generative_ui) | | [LlamaIndex](https://github.com/run-llama/llama_index) | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/llamaindex/) 🎮 [Demos](https://dojo.ag-ui.com/llamaindex/feature/shared_state) | | [AG2](https://ag2.ai/) | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/ag2/) 🎮 [Demos](https://dojo.ag-ui.com/ag2/feature/shared_state) | | [AWS Bedrock Agents](https://aws.amazon.com/bedrock/agents/) | 🛠️ In Progress | – | ### 🌐 Community | Framework | Status | AG-UI Resources | | ---------- | ------- | ---------------- | | [Langroid](https://github.com/ag-ui-protocol/ag-ui/tree/main/integrations/langroid) | ✅ Supported | 🎮 [Demos](https://dojo.ag-ui.com/langroid/feature/shared_state) | | [OpenAI Agent SDK](https://openai.github.io/openai-agents-python/) | 🛠️ In Progress | – | | [Cloudflare Agents](https://developers.cloudflare.com/agents/) | 🛠️ In Progress | – | ## Agent Interaction Protocols | Protocols | Status | AG-UI Resources | Integrations | | ---------- | ------- | ---------------- | ------------- | | [A2A]() | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/a2a-protocol) | Partnership | ## Infrastructure / Deployment | Platform | Status | AG-UI Resources | Integrations | | ---------- | ------- | ---------------- | ------------- | | [Amazon Bedrock AgentCore](https://aws.amazon.com/bedrock/agentcore/) | ✅ Supported | ➡️ [Docs](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-agui.html) | 1st Party | ## Specification (standard) | Framework | Status | AG-UI Resources | | ---------- | ------- | ---------------- | | [Oracle Agent Spec](http://oracle.github.io/agent-spec/) | ✅ Supported | ➡️ [Docs](https://go.copilotkit.ai/copilotkit-oracle-docs) 🎮 [Demos](https://dojo.ag-ui.com/agent-spec-langgraph/feature/tool_based_generative_ui) | ## Generative UI | Framework | Status | AG-UI Resources | | ---------- | ------- | ---------------- | | [MCP Apps](https://blog.modelcontextprotocol.io/posts/2025-11-21-mcp-apps/) | ✅ Supported | ➡️ [Docs](https://docs.copilotkit.ai/generative-ui-specs/mcp-apps) 🎮 [Demos]() | ## SDKs | SDK | Status | AG-UI Resources | Integrations | | --- | ------- | ---------------- | ------------- | | [Kotlin]() | ✅ Supported | ➡️ [Getting Started](https://github.com/ag-ui-protocol/ag-ui/blob/main/docs/sdk/kotlin/overview.mdx) | Community | | [Golang]() | ✅ Supported | ➡️ [Getting Started](https://github.com/ag-ui-protocol/ag-ui/blob/main/docs/sdk/go/overview.mdx) | Community | | [Dart]() | ✅ Supported | ➡️ [Getting Started](https://github.com/ag-ui-protocol/ag-ui/tree/main/sdks/community/dart) | Community | | [Java]() | ✅ Supported | ➡️ [Getting Started](https://github.com/ag-ui-protocol/ag-ui/blob/main/docs/sdk/java/overview.mdx) | Community | | [Rust]() | ✅ Supported | ➡️ [Getting Started](https://github.com/ag-ui-protocol/ag-ui/tree/main/sdks/community/rust/crates/ag-ui-client) | Community | | [Ruby]() | ✅ Supported | ➡️ [Getting Started](https://github.com/ag-ui-protocol/ag-ui/tree/main/sdks/community/ruby) | Community | | [.NET]() | 🛠️ In Progress | ➡️ [PR](https://github.com/ag-ui-protocol/ag-ui/pull/38) | Community | | [Nim]() | 🛠️ In Progress | ➡️ [PR](https://github.com/ag-ui-protocol/ag-ui/pull/29) | Community | | [Flowise]() | 🛠️ In Progress | ➡️ [GitHub Source](https://github.com/ag-ui-protocol/ag-ui/issues/367) | Community | | [Langflow]() | 🛠️ In Progress | ➡️ [GitHub Source](https://github.com/ag-ui-protocol/ag-ui/issues/366) | Community | ## Clients | Client | Status | AG-UI Resources | Integrations | | --- | ------- | ---------------- | ------------- | | [CopilotKit](https://github.com/CopilotKit/CopilotKit) | ✅ Supported | ➡️ [Getting Started](https://docs.copilotkit.ai/direct-to-llm/guides/quickstart) | 1st Party | | [Terminal + Agent]() | ✅ Supported | ➡️ [Getting Started](https://docs.ag-ui.com/quickstart/clients) | Community | | [React Native]() | 🛠️ Help Wanted | ➡️ [GitHub Source](https://github.com/ag-ui-protocol/ag-ui/issues/510) | Community | [View all supported integrations →](https://docs.ag-ui.com/introduction#supported-integrations) ## Examples ### Hello World App Video: https://github.com/user-attachments/assets/18c03330-1ebc-4863-b2b8-cc6c3a4c7bae https://agui-demo.vercel.app/ ## The AG-UI Dojo (Building-Blocks Viewer) The AG-UI Dojo demonstrates AG-UI's core building blocks through simple, focused examples—each just 50-200 lines of code. View the source code for the Dojo and all framework integrations [here](https://github.com/ag-ui-protocol/ag-ui/tree/main/apps/dojo). https://github.com/user-attachments/assets/c298eea8-3f39-4a94-b968-7712429b0c49 ## 🙋🏽‍♂️ Contributing to AG-UI Check out the [Contributing guide](https://github.com/ag-ui-protocol/ag-ui/blob/main/CONTRIBUTING.md) - **[Bi-Weekely AG-UI Working Group](https://lu.ma/CopilotKit?k=c)** 📅 Follow the CopilotKit Luma Events Calendar ## Roadmap Check out the [AG-UI Roadmap](https://github.com/orgs/ag-ui-protocol/projects/1) to see what's being built and where you can jump in. ## 📄 License AG-UI is open source software [licensed as MIT](https://opensource.org/licenses/MIT). ================================================ FILE: apps/client-cli-example/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional stylelint cache .stylelintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variable files .env .env.* !.env.example # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next out # Nuxt.js build / generate output .nuxt dist .output # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # vuepress v2.x temp and cache directory .temp .cache # Sveltekit cache directory .svelte-kit/ # vitepress build output **/.vitepress/dist # vitepress cache directory **/.vitepress/cache # Docusaurus cache and generated files .docusaurus # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # Firebase cache directory .firebase/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v3 .pnp.* .yarn/* !.yarn/patches !.yarn/plugins !.yarn/releases !.yarn/sdks !.yarn/versions # Vite files vite.config.js.timestamp-* vite.config.ts.timestamp-* .vite/ ================================================ FILE: apps/client-cli-example/README.md ================================================ # AG-UI CLI Example A command-line chat interface demonstrating the AG-UI client with a Mastra agent. This example shows how to build an interactive CLI application that streams agent responses and tool calls in real-time. ## Features - Interactive chat loop with streaming responses - Real-time tool call visualization (weather and browser tools) - Message history persistence using LibSQL - Built with `@ag-ui/client` and `@ag-ui/mastra` ## Prerequisites - Node.js 22.13.0 or later - OpenAI API key ## Setup 1. Install dependencies from the repository root: ```bash pnpm install ``` 2. Set your OpenAI API key: ```bash export OPENAI_API_KEY=your_api_key_here ``` ## Usage Run the CLI: ```bash pnpm start ``` Try these example prompts: - "What's the weather in San Francisco?" - "Browse https://example.com" Press `Ctrl+D` to quit. ## How It Works This example uses: - **MastraAgent**: Wraps a Mastra agent with AG-UI protocol support - **Event Handlers**: Streams text deltas, tool calls, and results to the console - **Memory**: Persists conversation history in a local SQLite database ================================================ FILE: apps/client-cli-example/package.json ================================================ { "name": "client-cli-example", "version": "0.1.0", "private": true, "scripts": { "start": "tsx src/index.ts", "dev": "tsx --watch src/index.ts", "build": "tsc", "clean": "git clean -fdX --exclude=\"!.env\"" }, "dependencies": { "@ag-ui/client": "workspace:*", "@ag-ui/core": "workspace:*", "@ag-ui/mastra": "workspace:*", "@mastra/client-js": "^1.0.1", "@mastra/core": "^1.0.4", "@mastra/libsql": "^1.0.0", "@mastra/loggers": "^1.0.0", "@mastra/memory": "^1.0.0", "open": "^10.1.2", "zod": "^4.3.6" }, "devDependencies": { "@types/node": "^20", "tsx": "^4.7.0", "typescript": "^5" } } ================================================ FILE: apps/client-cli-example/src/agent.ts ================================================ import { Agent } from "@mastra/core/agent"; import { MastraAgent } from "@ag-ui/mastra"; import { Memory } from "@mastra/memory"; import { LibSQLStore } from "@mastra/libsql"; import { weatherTool } from "./tools/weather.tool"; import { browserTool } from "./tools/browser.tool"; export const agent = new MastraAgent({ resourceId: "cliExample", agent: new Agent({ id: "ag-ui-agent", name: "AG-UI Agent", instructions: ` You are a helpful assistant that runs a CLI application. When helping users get weather details for specific locations, respond: - Always ask for a location if none is provided. - If the location name isn’t in English, please translate it - If giving a location with multiple parts (e.g. "New York, NY"), use the most relevant part (e.g. "New York") - Include relevant details like humidity, wind conditions, and precipitation - Keep responses concise but informative Use the weatherTool to fetch current weather data. When helping users browse the web, always use a full URL, for example: "https://www.google.com" Use the browserTool to browse the web. `, model: "openai/gpt-4.1-mini", tools: { weatherTool, browserTool }, memory: new Memory({ storage: new LibSQLStore({ id: "mastra-cli-example-db", url: "file:./mastra.db", }), }), }), }); ================================================ FILE: apps/client-cli-example/src/index.ts ================================================ import * as readline from "readline"; import { randomUUID } from "@ag-ui/client"; import { agent } from "./agent"; const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); async function chatLoop() { console.log("🤖 AG-UI chat started! Type your messages and press Enter. Press Ctrl+D to quit.\n"); return new Promise((resolve) => { const promptUser = () => { rl.question("> ", async (input) => { if (input.trim() === "") { promptUser(); return; } console.log(""); rl.pause(); agent.messages.push({ id: randomUUID(), role: "user", content: input.trim(), }); try { await agent.runAgent( {}, { onTextMessageStartEvent() { process.stdout.write("🤖 AG-UI assistant: "); }, onTextMessageContentEvent({ event }) { process.stdout.write(event.delta); }, onTextMessageEndEvent() { console.log("\n"); }, onToolCallStartEvent({ event }) { console.log("🔧 Tool call:", event.toolCallName); }, onToolCallArgsEvent({ event }) { process.stdout.write(event.delta); }, onToolCallEndEvent() { console.log(""); }, onToolCallResultEvent({ event }) { if (event.content) { console.log("🔍 Tool call result:", event.content); } }, }, ); } catch (error) { console.error("❌ Error running agent:", error); } rl.resume(); promptUser(); }); }; rl.on("close", () => { console.log("\n👋 Goodbye!"); resolve(); }); promptUser(); }); } async function main() { await chatLoop(); } main().catch(console.error); ================================================ FILE: apps/client-cli-example/src/tools/browser.tool.ts ================================================ import { createTool } from "@mastra/core/tools"; import { z } from "zod"; import open from "open"; export const browserTool = createTool({ id: "browser", description: "Browse the web", inputSchema: z.object({ url: z.string().describe("URL to browse"), }), outputSchema: z.string(), execute: async (inputData) => { open(inputData.url); return `Browsed ${inputData.url}`; }, }); ================================================ FILE: apps/client-cli-example/src/tools/weather.tool.ts ================================================ import { createTool } from "@mastra/core/tools"; import { z } from "zod"; interface GeocodingResponse { results: { latitude: number; longitude: number; name: string; }[]; } interface WeatherResponse { current: { time: string; temperature_2m: number; apparent_temperature: number; relative_humidity_2m: number; wind_speed_10m: number; wind_gusts_10m: number; weather_code: number; }; } export const weatherTool = createTool({ id: "get-weather", description: "Get current weather for a location", inputSchema: z.object({ location: z.string().describe("City name"), }), outputSchema: z.object({ temperature: z.number(), feelsLike: z.number(), humidity: z.number(), windSpeed: z.number(), windGust: z.number(), conditions: z.string(), location: z.string(), }), execute: async (inputData) => { return await getWeather(inputData.location); }, }); const getWeather = async (location: string) => { const geocodingUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(location)}&count=1`; const geocodingResponse = await fetch(geocodingUrl); const geocodingData = (await geocodingResponse.json()) as GeocodingResponse; if (!geocodingData.results?.[0]) { throw new Error(`Location '${location}' not found`); } const { latitude, longitude, name } = geocodingData.results[0]; const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,wind_gusts_10m,weather_code`; const response = await fetch(weatherUrl); const data = (await response.json()) as WeatherResponse; return { temperature: data.current.temperature_2m, feelsLike: data.current.apparent_temperature, humidity: data.current.relative_humidity_2m, windSpeed: data.current.wind_speed_10m, windGust: data.current.wind_gusts_10m, conditions: getWeatherCondition(data.current.weather_code), location: name, }; }; function getWeatherCondition(code: number): string { const conditions: Record = { 0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast", 45: "Foggy", 48: "Depositing rime fog", 51: "Light drizzle", 53: "Moderate drizzle", 55: "Dense drizzle", 56: "Light freezing drizzle", 57: "Dense freezing drizzle", 61: "Slight rain", 63: "Moderate rain", 65: "Heavy rain", 66: "Light freezing rain", 67: "Heavy freezing rain", 71: "Slight snow fall", 73: "Moderate snow fall", 75: "Heavy snow fall", 77: "Snow grains", 80: "Slight rain showers", 81: "Moderate rain showers", 82: "Violent rain showers", 85: "Slight snow showers", 86: "Heavy snow showers", 95: "Thunderstorm", 96: "Thunderstorm with slight hail", 99: "Thunderstorm with heavy hail", }; return conditions[code] || "Unknown"; } ================================================ FILE: apps/client-cli-example/tsconfig.json ================================================ { "compilerOptions": { "target": "ES2020", "module": "commonjs", "lib": ["ES2020", "dom"], "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "moduleResolution": "node" }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } ================================================ FILE: apps/dojo/.gitignore ================================================ next-env.d.ts # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional stylelint cache .stylelintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variable files .env .env.* !.env.example # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next out # Nuxt.js build / generate output .nuxt dist .output # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # vuepress v2.x temp and cache directory .temp .cache # Sveltekit cache directory .svelte-kit/ # vitepress build output **/.vitepress/dist # vitepress cache directory **/.vitepress/cache # Docusaurus cache and generated files .docusaurus # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # Firebase cache directory .firebase/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v3 .pnp.* .yarn/* !.yarn/patches !.yarn/plugins !.yarn/releases !.yarn/sdks !.yarn/versions # Vite files vite.config.js.timestamp-* vite.config.ts.timestamp-* .vite/ # Mastra files .mastra ================================================ FILE: apps/dojo/LICENSE ================================================ Copyright (c) 2025 Tawkit Inc. Copyright (c) 2025 Markus Ecker Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: apps/dojo/README.md ================================================ # AG-UI Protocol Dojo A modern, interactive viewer for exploring CopilotKit agent demos with a clean, responsive UI and dark/light theme support. ## Overview The Demo Viewer provides a centralized interface for browsing, viewing, and exploring the source code of various CopilotKit agent demos. It features: - Clean, modern UI with dark/light theme support - Interactive demo previews - Source code exploration with syntax highlighting - Organized demo listing with tags and descriptions - LLM provider selection ## Development Setup To run the Demo Viewer locally for development, follow these steps: ### Install dependencies ```bash brew install protobuf ``` Note that running the dojo currently requires the use of `pnpm` (vs `yarn` or `npm`) do to how we handle workspace dependencies. ```bash curl -fsSL https://get.pnpm.io/install.sh | sh - ``` The first time you want to run, you need to build all of the dojos dependencies throught the repository. ``` # from the ag-ui repository root pnpm i pnpm build --filter=demo-viewer ``` ### Run the Demo Viewer There are 3 ways to run the demo viewer - Run just the demo viewer, and run the agent(s) separately - Run the dev script for the entire repo, and run the agent(s) separately - use the `dojo-everything` scripts #### Run just the demo viewer, and run the agent(s) separately. In one terminal, you can `cd` into the dojo directory and run `pnpm dev` to just run the dojo This will not capture updates to dependencies of the dojo In another terminal, you'll need to run any other agents you want to test separately, see "Run Agents" below. The dojo will start on port 3000 by default Note that some agents may run on colliding ports #### Run the dev script for the entire repo, and run the agent(s) separately In one terminal, you can run `pnpm dev` from the *repository root* This WILL automatically rebuild dependencies, for example if you change the mastra integration, it will automatically rebuild and be bundled into the dojo with HMR. In another terminal, you'll need to run any other agents you want to test separately, see "Run Agents" below. The dojo will start on port 3000 by default Note that some agents may run on colliding ports #### Run Agents Agent examples for the dojo are generally located in `integrations/{integrationName}/{language}/examples`. A readme there should explain what you need to do to run the example, but it's usually either `npm dev` for typescript packages, or `poetry install && poetry run dev` or `uv sync && uv run dev` for python servers. Note that some agents may run on colliding ports #### Use the `dojo-everything` scripts These are the easiest ways to run everything. They will automatically configure all of your ports to not be colliding, provide that information to the dojo, and spin up the dojo. ``` # In the apps/dojo directory ./scripts/prep-dojo-everything.js ./scripts/run-dojo-everything.js ``` The demo viewer will now run on port 9999. The one caveat here is that (for precompiled speed while running tests) this runs a production nextjs build, and that build has to be redone if you modify the dojo code at all (or any of the typescript integrations). You can look in the `run-dojo-everything.js` script and see which ports it runs agents at, and export those as environment variables, which can be found in `apps/dojo/src/env.ts`. Then you can run the dojo via `pnpm dev` at the repo root, to get live updates to typescript integrations and the dojo. There is not HMR on most of the python framework agent examples. To choose which agents or services the `run-dojo-everything.js` script runs you can use the `--only` flag, like this: `./scripts/run-dojo-everything.js --only adk-middleware,langgraph-fastapi`. The names for these IDs match what is in `src/agents.ts` as well as being findable in the run-dojo-everything script. . ### Adding a new integration Integrations should go in `integrations/{integrationID}`. There should always be a typescript folder that at least contains the client, and possibly a python (or other language) folder. To add it to the dojo, please make sure it gets added to - src/agents.ts - src/menu.ts - scripts/prep-dojo-everything.js - scripts/run-dojo-everything.js - e2e.yml - the `apps/dojo/e2e` folder, look in the tests folder of other frameworks, and you should be able to mostly dupiclate these. ================================================ FILE: apps/dojo/components.json ================================================ { "$schema": "https://ui.shadcn.com/schema.json", "style": "new-york", "rsc": true, "tsx": true, "tailwind": { "config": "tailwind.config.ts", "css": "src/app/globals.css", "baseColor": "neutral", "cssVariables": true, "prefix": "" }, "iconLibrary": "lucide", "aliases": { "components": "@/components", "utils": "@/lib/utils", "ui": "@/components/ui", "lib": "@/lib", "hooks": "@/hooks" }, "registries": {} } ================================================ FILE: apps/dojo/e2e/.gitignore ================================================ playwright-report/ test-results/ ================================================ FILE: apps/dojo/e2e/README.md ================================================ # CopilotKit Demo Smoke Tests This repository houses Playwright-based smoke tests that run on a 6-hour schedule to make sure CopilotKit demo apps remain live and functional. ## 🔧 Local development ```bash # Install deps npm install # Install browsers once npx playwright install --with-deps # Run the full suite npm test ``` Playwright HTML reports are saved to `./playwright-report`. ## ➕ Adding a new smoke test 1. Duplicate an existing file in `tests/` or create `tests/.spec.ts`. 2. Use Playwright's `test` API—keep the test short (<30 s). 3. Commit and push—GitHub Actions will pick it up on the next scheduled run. ## 🚦 CI / CD - `.github/workflows/scheduled-tests.yml` executes the suite every 6 hours and on manual trigger. - Failing runs surface in the Actions tab; the HTML report is uploaded as an artifact. - (Optional) Slack notifications can be wired by adding a step after the tests. - Slack alert on failure is baked into the workflow. Just add `SLACK_WEBHOOK_URL` (Incoming Webhook) in repo secrets. ================================================ FILE: apps/dojo/e2e/VIDEO_SETUP.md ================================================ # 📹 S3 Video Upload System This system automatically uploads videos of failed Playwright tests to S3 and embeds clickable links in Slack notifications. ## ✅ **Setup Complete Checklist** - [x] AWS infrastructure created (`setup-aws.sh`) - [x] Dependencies installed (`@aws-sdk/client-s3`, `json2md`) - [x] S3 video uploader created (`lib/upload-video.ts`) - [x] Custom reporter created (`reporters/s3-video-reporter.ts`) - [x] Playwright config updated (video recording enabled) - [x] Slack layout updated (video links embedded) - [x] GitHub Actions updated (AWS credentials) ## 🔧 **Required GitHub Secrets** Add these secrets to your repository: ``` AWS_ACCESS_KEY_ID=AKIA... AWS_SECRET_ACCESS_KEY=... AWS_S3_BUCKET_NAME=copilotkit-e2e-smoke-test-recordings-abc123 AWS_S3_REGION=us-east-1 SLACK_WEBHOOK_URL=https://hooks.slack.com/services/... ``` ## 🎯 **How It Works** ### **1. Video Recording** - Videos recorded only for **failed tests** (`retain-on-failure`) - 1280x720 resolution, WebM format - Stored temporarily in `test-results/` ### **2. S3 Upload Process** ``` Failed Test → Video Recorded → S3 Upload → Slack Notification ``` ### **3. S3 File Organization** ``` copilotkit-e2e-smoke-test-recordings-{random}/ └── github-runs/ └── {GITHUB_RUN_ID}/ └── cpk-demos-smoke-tests/ └── {SUITE_NAME}/ └── {TEST_NAME}/ └── video.webm ``` ### **4. Slack Integration** Videos appear as clickable links in categorized failure notifications: ``` 🤖 AI Response Issues (2 failures) • Human in the Loop Feature: Chat interaction steps → No AI response - Expected: /Travel Guide/i 📹 [Watch Video](https://bucket.s3.amazonaws.com/path/video.webm) 🔧 Action: Check API keys and AI service status ``` ## 🛠 **Local Development** ### **Test Video Upload Locally** ```bash # Set environment variables export AWS_S3_BUCKET_NAME="your-bucket-name" export AWS_S3_REGION="us-east-1" export AWS_ACCESS_KEY_ID="your-key" export AWS_SECRET_ACCESS_KEY="your-secret" # Run tests with video upload enabled CI=true pnpm exec playwright test --reporter=./reporters/s3-video-reporter.ts ``` ### **Disable Video Upload Locally** Videos are automatically disabled in local runs. To force enable: ```bash # Edit playwright.config.ts uploadVideos: true // In local reporter config ``` ## 📊 **Monitoring & Debugging** ### **Check Upload Status** - Videos upload logs appear in GitHub Actions output - Failed uploads are logged but don't fail the workflow - Video URLs written to `test-results/video-urls.json` ### **Common Issues** **❌ No videos in Slack** - Check AWS credentials in GitHub secrets - Verify S3 bucket permissions - Look for upload errors in Actions logs **❌ Videos not accessible** - Verify S3 bucket has public read access - Check bucket policy and CORS settings **❌ Upload timeouts** - Large video files may timeout - Check network connectivity to S3 - Consider video compression settings ## 🧹 **Maintenance** ### **Automatic Cleanup** - Videos automatically deleted after **30 days** - Lifecycle policy configured in S3 bucket - No manual cleanup required ### **Cost Management** - Only failed tests generate videos (~5-10 MB each) - 30-day retention keeps costs low - Monitor S3 usage in AWS console ## 🚀 **Next Steps** 1. **Run `setup-aws.sh`** to create infrastructure ✅ 2. **Add GitHub secrets** from script output ⏳ 3. **Test the system** by running a failing test ⏳ 4. **Check Slack notifications** for video links ⏳ ## 🔗 **File Structure** ``` cpk-demos-smoke-tests/ ├── lib/ │ └── upload-video.ts # S3 upload functionality ├── reporters/ │ └── s3-video-reporter.ts # Playwright reporter ├── .github/workflows/ │ └── scheduled-tests.yml # AWS credentials setup ├── playwright.config.ts # Video recording config ├── slack-layout.ts # Video links in notifications ├── setup-aws.sh # AWS infrastructure script └── VIDEO_SETUP.md # This file ``` ## 📹 **Video URL Format** ``` https://{bucket}.s3.{region}.amazonaws.com/github-runs/{run-id}/{project}/{suite}/{test}/video-{timestamp}.webm ``` Example: ``` https://copilotkit-e2e-recordings.s3.us-east-1.amazonaws.com/github-runs/1234567890/cpk-demos-smoke-tests/Human-in-the-Loop-Feature/Chat-interaction-steps/video-20240115-143022.webm ``` **🎉 Your failed test videos are now automatically uploaded to S3 and linked in Slack!** ================================================ FILE: apps/dojo/e2e/clean-reporter.cjs ================================================ function getTimestamp() { return (process.env.CI || process.env.VERBOSE) ? new Date().toLocaleTimeString('en-US', { hour12: false }) : ''; } function logStamp(...args) { console.log(getTimestamp(), ...args); } class CleanReporter { onBegin(config, suite) { console.log(`\n🎭 Running ${suite.allTests().length} tests...\n`); } onTestEnd(test, result) { const suiteName = test.parent?.title || "Unknown"; const testName = test.title; // Clean up suite name const cleanSuite = suiteName .replace(/Tests?$/i, "") .replace(/Page$/i, "") .replace(/([a-z])([A-Z])/g, "$1 $2") .trim(); if (result.status === "passed") { logStamp(`✅ PASS ${cleanSuite}: ${testName}`); return; } if (result.status === "skipped") { console.log(`⚠️ SKIP ${cleanSuite}: ${testName} (skipped)`); return; } // Handle all failure modes: "failed", "timedOut", "interrupted" const icon = result.status === "timedOut" ? "⏰ TIMEOUT" : "❌ FAIL"; logStamp(`${icon} ${cleanSuite}: ${testName}`); // Extract the most relevant error info const error = result.error || result.errors?.[0]; if (error) { let errorMsg = error.message || "Unknown error"; // Clean up common error patterns to make them more readable if (errorMsg.includes("None of the expected patterns matched")) { const patterns = errorMsg.match(/patterns matched[^:]*: ([^`]+)/); errorMsg = `AI response timeout - Expected: ${ patterns?.[1] || "AI response" }`; } else if ( errorMsg.includes("Timed out") && errorMsg.includes("toBeVisible") ) { const element = errorMsg.match(/locator\('([^']+)'\)/); errorMsg = `Element not found: ${element?.[1] || "UI element"}`; } else if (errorMsg.includes("Test timeout of")) { errorMsg = errorMsg.split("\n")[0]; } else if (errorMsg.includes("toBeGreaterThan")) { errorMsg = "Expected content not generated (count was 0)"; } // Show just the key error info console.log(`💥 ERROR: ${errorMsg.split("\n")[0]}`); // If it's an AI/API issue, make it clear if ( errorMsg.includes("AI") || errorMsg.includes("patterns") || errorMsg.includes("timeout") ) { console.log(` HINT: Likely cause: AI service down or API key issue`); } } // Surface diagnostic output from test-isolation-helper on failure. // This includes AI State Dump, NetworkError, PageError, and // BrowserConsole lines that would otherwise be hidden by this reporter. const diagnosticPrefixes = [ "[AI State Dump]", "[NetworkError]", "[PageError]", "[BrowserConsole]", "[Test Cleanup]", "[User]", "[Assistant]", ]; const stdout = (result.stdout || []) .map((chunk) => (typeof chunk === "string" ? chunk : chunk.toString("utf-8"))) .join(""); const diagnosticLines = stdout .split("\n") .filter((line) => diagnosticPrefixes.some((p) => line.includes(p))); if (diagnosticLines.length > 0) { console.log(" --- Diagnostics ---"); for (const line of diagnosticLines) { console.log(` ${line.trim()}`); } } console.log(""); // Extra spacing after failures } onEnd(result) { console.log("\n" + "=".repeat(60)); logStamp(`📊 TEST SUMMARY`); console.log("=".repeat(60)); if (!process.env.CI) { console.log( `Run 'pnpm exec playwright show-report' for detailed HTML report` ); } console.log("=".repeat(60) + "\n"); } } module.exports = CleanReporter; ================================================ FILE: apps/dojo/e2e/featurePages/AgenticChatPage.ts ================================================ import { Page, Locator, expect } from "@playwright/test"; import { CopilotSelectors } from "../utils/copilot-selectors"; import { sendChatMessage, awaitLLMResponseDone } from "../utils/copilot-actions"; import { DEFAULT_WELCOME_MESSAGE } from "../lib/constants"; export class AgenticChatPage { readonly page: Page; readonly openChatButton: Locator; readonly agentGreeting: Locator; readonly chatInput: Locator; readonly sendButton: Locator; readonly agentMessage: Locator; readonly userMessage: Locator; constructor(page: Page) { this.page = page; this.openChatButton = CopilotSelectors.chatToggle(page); this.agentGreeting = page .getByText(DEFAULT_WELCOME_MESSAGE); this.chatInput = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); this.agentMessage = CopilotSelectors.assistantMessages(page); this.userMessage = CopilotSelectors.userMessages(page); } async openChat() { try { await this.openChatButton.click({ timeout: 3000 }); } catch (error) { // Chat might already be open } } async sendMessage(message: string) { await sendChatMessage(this.page, message); await awaitLLMResponseDone(this.page); } async getGradientButtonByName(name: string | RegExp) { return this.page.getByRole("button", { name }); } async assertUserMessageVisible(text: string | RegExp) { await expect(this.userMessage.getByText(text)).toBeVisible(); } async assertAgentReplyVisible(expectedText: RegExp | RegExp[]) { const expectedTexts = Array.isArray(expectedText) ? expectedText : [expectedText]; let lastError: unknown = null; for (const pattern of expectedTexts) { try { const agentMessage = CopilotSelectors.assistantMessages(this.page).filter({ hasText: pattern }); await expect(agentMessage.last()).toBeVisible(); return; // At least one pattern matched, succeed } catch (error) { lastError = error; } } throw lastError; // No pattern matched } async assertAgentReplyContains(expectedText: string) { const agentMessage = CopilotSelectors.assistantMessages(this.page).last(); await expect(agentMessage).toContainText(expectedText); } async getAssistantMessageText(index: number): Promise { const message = this.agentMessage.nth(index); await expect(message).toBeVisible(); return (await message.textContent()) ?? ""; } async regenerateResponse(index: number) { const message = this.agentMessage.nth(index); await expect(message).toBeVisible(); // Hover over the message to reveal the regenerate button await message.hover(); const regenerateButton = message.getByTestId("copilot-regenerate-button"); try { await regenerateButton.click({ timeout: 3000 }); } catch { // If hover didn't reveal the button, force click await regenerateButton.click({ force: true }); } } async assertWeatherResponseStructure() { // The get_weather tool renders a deterministic component with data-testid="weather-info" const weatherInfo = this.page.getByTestId("weather-info"); await expect(weatherInfo.last()).toBeVisible(); await expect(weatherInfo.last()).toContainText("Temperature:"); await expect(weatherInfo.last()).toContainText("Humidity:"); await expect(weatherInfo.last()).toContainText("Wind Speed:"); await expect(weatherInfo.last()).toContainText("Conditions:"); } } ================================================ FILE: apps/dojo/e2e/featurePages/HumanInTheLoopPage.ts ================================================ import { Page, Locator, expect } from "@playwright/test"; import { CopilotSelectors } from "../utils/copilot-selectors"; import { sendAndAwaitResponse } from "../utils/copilot-actions"; import { DEFAULT_WELCOME_MESSAGE } from "../lib/constants"; export class HumanInTheLoopPage { readonly page: Page; readonly planTaskButton: Locator; readonly chatInput: Locator; readonly sendButton: Locator; readonly agentGreeting: Locator; readonly plan: Locator; readonly performStepsButton: Locator; readonly agentMessage: Locator; readonly userMessage: Locator; constructor(page: Page) { this.page = page; this.planTaskButton = page.getByRole("button", { name: "Human in the loop Plan a task", }); this.agentGreeting = page.getByText(DEFAULT_WELCOME_MESSAGE); this.chatInput = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); this.plan = page.getByTestId("select-steps"); this.performStepsButton = page.getByRole("button", { name: "Confirm" }); this.agentMessage = CopilotSelectors.assistantMessages(page); this.userMessage = CopilotSelectors.userMessages(page); } async openChat() { await expect(this.agentGreeting).toBeVisible(); } async sendMessage(message: string) { await sendAndAwaitResponse(this.page, message); } async selectItemsInPlanner() { await expect(this.plan).toBeVisible(); await this.plan.click(); } async getPlannerOnClick(name: string | RegExp) { return this.page.getByRole("button", { name }); } async uncheckItem(identifier: number | string): Promise { const plannerContainer = this.page.getByTestId("select-steps"); const items = plannerContainer.getByTestId("step-item"); let item; if (typeof identifier === "number") { item = items.nth(identifier); } else { item = items .filter({ has: this.page .getByTestId("step-text") .filter({ hasText: identifier }), }) .first(); } const stepTextElement = item.getByTestId("step-text"); const text = await stepTextElement.innerText(); await item.click(); return text; } async isStepItemUnchecked(target: number | string): Promise { const plannerContainer = this.page.getByTestId("select-steps"); const items = plannerContainer.getByTestId("step-item"); let item; if (typeof target === "number") { item = items.nth(target); } else { item = items .filter({ has: this.page.getByTestId("step-text").filter({ hasText: target }), }) .first(); } const checkbox = item.locator('input[type="checkbox"]'); return !(await checkbox.isChecked()); } async performSteps() { await this.performStepsButton.click(); await this.performStepsButton.waitFor({ state: "hidden" }); } async performStepsAndAwait() { const countBefore = await this.page .locator('[data-testid="copilot-assistant-message"]') .count(); await this.performStepsButton.click(); await this.performStepsButton.waitFor({ state: "hidden" }); await this.page.waitForFunction( (before) => document.querySelectorAll('[data-testid="copilot-assistant-message"]') .length > before, countBefore, { timeout: 30000 }, ); await this.page.waitForFunction( () => document.querySelector('[data-copilot-running="false"]') !== null, null, { timeout: 60000 }, ); } async assertAgentReplyVisible(expectedText: RegExp) { await expect( this.agentMessage.last().getByText(expectedText), ).toBeVisible(); } async assertUserMessageVisible(message: string) { await expect(this.page.getByText(message)).toBeVisible(); } } ================================================ FILE: apps/dojo/e2e/featurePages/SharedStatePage.ts ================================================ import { Page, Locator, expect } from '@playwright/test'; import { CopilotSelectors } from '../utils/copilot-selectors'; import { sendChatMessage, awaitLLMResponseDone } from '../utils/copilot-actions'; import { DEFAULT_WELCOME_MESSAGE } from '../lib/constants'; export class SharedStatePage { readonly page: Page; readonly chatInput: Locator; readonly sendButton: Locator; readonly agentGreeting: Locator; readonly agentMessage: Locator; readonly userMessage: Locator; readonly promptResponseLoader: Locator; readonly ingredientCards: Locator; readonly instructionsContainer: Locator; readonly addIngredient: Locator; constructor(page: Page) { this.page = page; this.agentGreeting = page.getByText(DEFAULT_WELCOME_MESSAGE); this.chatInput = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); this.promptResponseLoader = page.getByRole('button', { name: 'Please Wait...', disabled: true }); this.instructionsContainer = page.locator('.instructions-container'); this.addIngredient = page.getByRole('button', { name: '+ Add Ingredient' }); this.agentMessage = CopilotSelectors.assistantMessages(page); this.userMessage = CopilotSelectors.userMessages(page); this.ingredientCards = page.locator('.ingredient-card'); } async openChat() { await expect(this.agentGreeting).toBeVisible(); } async sendMessage(message: string) { await sendChatMessage(this.page, message); await awaitLLMResponseDone(this.page); } async loader() { // Wait for the LLM stream to finish using data-copilot-running await awaitLLMResponseDone(this.page); } async awaitIngredientCard(name: string) { // Use page.waitForFunction for case-insensitive matching on input values, // since CSS attribute selectors are case-sensitive await this.page.waitForFunction( (ingredientName) => { const inputs = document.querySelectorAll('.ingredient-card input.ingredient-name-input'); return Array.from(inputs).some( (input: HTMLInputElement) => input.value.toLowerCase().includes(ingredientName.toLowerCase()) ); }, name, { timeout: 15000 } ); } async addNewIngredient(placeholderText: string) { await this.addIngredient.click(); await expect(this.page.locator(`input[placeholder="${placeholderText}"]`)).toBeVisible(); } async getInstructionItems(containerLocator: Locator ) { const count = await containerLocator.locator('.instruction-item').count(); if (count <= 0) { throw new Error('No instruction items found in the container.'); } console.log(`✅ Found ${count} instruction items.`); return count; } async assertAgentReplyVisible(expectedText: RegExp) { await expect(this.agentMessage.getByText(expectedText)).toBeVisible(); } async assertUserMessageVisible(message: string) { await expect(this.page.getByText(message)).toBeVisible(); } } ================================================ FILE: apps/dojo/e2e/featurePages/ToolBaseGenUIPage.ts ================================================ import { Page, Locator, expect } from "@playwright/test"; import { CopilotSelectors } from "../utils/copilot-selectors"; import { sendChatMessage, awaitLLMResponseDone } from "../utils/copilot-actions"; import { DEFAULT_WELCOME_MESSAGE } from "../lib/constants"; export class ToolBaseGenUIPage { readonly page: Page; readonly haikuAgentIntro: Locator; readonly messageBox: Locator; readonly sendButton: Locator; readonly applyButton: Locator; readonly haikuBlock: Locator; readonly japaneseLines: Locator; readonly mainHaikuDisplay: Locator; constructor(page: Page) { this.page = page; this.haikuAgentIntro = page.getByText(DEFAULT_WELCOME_MESSAGE).first(); this.messageBox = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); this.haikuBlock = page.locator('[data-testid="haiku-card"]'); this.applyButton = page.getByRole("button", { name: "Apply" }); this.japaneseLines = page.locator('[data-testid="haiku-japanese-line"]'); this.mainHaikuDisplay = page.locator('[data-testid="haiku-carousel"]'); } async generateHaiku(message: string) { await expect(this.messageBox).toBeVisible(); await sendChatMessage(this.page, message); await awaitLLMResponseDone(this.page); } async checkGeneratedHaiku() { const cards = this.page.locator('[data-testid="haiku-card"]'); await expect(cards.last()).toBeVisible(); const mostRecentCard = cards.last(); await expect(mostRecentCard .locator('[data-testid="haiku-japanese-line"]') .first()).toBeVisible(); } async extractChatHaikuContent(page: Page): Promise { const allHaikuCards = page.locator('[data-testid="haiku-card"]'); await expect(allHaikuCards.first()).toBeVisible(); const cardCount = await allHaikuCards.count(); let chatHaikuContainer; let chatHaikuLines; for (let cardIndex = cardCount - 1; cardIndex >= 0; cardIndex--) { chatHaikuContainer = allHaikuCards.nth(cardIndex); chatHaikuLines = chatHaikuContainer.locator('[data-testid="haiku-japanese-line"]'); const linesCount = await chatHaikuLines.count(); if (linesCount > 0) { try { await expect(chatHaikuLines.first()).toBeVisible(); break; } catch (error) { continue; } } } if (!chatHaikuLines) { throw new Error("No haiku cards with visible lines found"); } const count = await chatHaikuLines.count(); const lines: string[] = []; for (let i = 0; i < count; i++) { const haikuLine = chatHaikuLines.nth(i); const japaneseText = await haikuLine.innerText(); lines.push(japaneseText); } const chatHaikuContent = lines.join("").replace(/\s/g, ""); return chatHaikuContent; } async extractMainDisplayHaikuContent(page: Page): Promise { const carousel = page.locator('[data-testid="haiku-carousel"]'); await expect(carousel).toBeVisible(); // Find the visible carousel item (the active slide) const carouselItems = carousel.locator('[data-testid^="carousel-item-"]'); const itemCount = await carouselItems.count(); let activeCard = null; // Find the visible/active carousel item for (let i = 0; i < itemCount; i++) { const item = carouselItems.nth(i); const isVisible = await item.isVisible(); if (isVisible) { activeCard = item.locator('[data-testid="haiku-card"]'); break; } } if (!activeCard) { // Fallback to first card if none found visible activeCard = carousel.locator('[data-testid="haiku-card"]').first(); } const mainDisplayLines = activeCard.locator('[data-testid="haiku-japanese-line"]'); const mainCount = await mainDisplayLines.count(); const lines: string[] = []; if (mainCount > 0) { for (let i = 0; i < mainCount; i++) { const haikuLine = mainDisplayLines.nth(i); const japaneseText = await haikuLine.innerText(); lines.push(japaneseText); } } const mainHaikuContent = lines.join("").replace(/\s/g, ""); return mainHaikuContent; } private async carouselIncludesHaiku( page: Page, chatHaikuContent: string, ): Promise { const carousel = page.locator('[data-testid="haiku-carousel"]'); if (!(await carousel.isVisible())) { return false; } const allCarouselCards = carousel.locator('[data-testid="haiku-card"]'); const cardCount = await allCarouselCards.count(); for (let i = 0; i < cardCount; i++) { const card = allCarouselCards.nth(i); const lines = card.locator('[data-testid="haiku-japanese-line"]'); const lineCount = await lines.count(); const cardLines: string[] = []; for (let j = 0; j < lineCount; j++) { const text = await lines.nth(j).innerText(); cardLines.push(text); } const cardContent = cardLines.join("").replace(/\s/g, ""); if (cardContent === chatHaikuContent) { return true; } } return false; } async checkHaikuDisplay(page: Page): Promise { const chatHaikuContent = await this.extractChatHaikuContent(page); await expect .poll( async () => this.carouselIncludesHaiku(page, chatHaikuContent), { timeout: 15000, intervals: [500, 1000, 2000] }, ) .toBe(true); } } ================================================ FILE: apps/dojo/e2e/featurePages/V1AgenticChatPage.ts ================================================ import { Page, Locator, expect } from "@playwright/test"; /** * Page object for v1 CopilotKit chat UI. * * V1 uses CSS class selectors (copilotKitInput, copilotKitAssistantMessage, etc.) * instead of the data-testid attributes used by v2. */ export class V1AgenticChatPage { readonly page: Page; readonly chatInput: Locator; readonly sendButton: Locator; readonly assistantMessages: Locator; readonly userMessages: Locator; constructor(page: Page) { this.page = page; this.chatInput = page.locator(".copilotKitInput textarea"); this.sendButton = page.locator( 'button[data-test-id="copilot-chat-ready"], button[data-test-id="copilot-chat-request-in-progress"]' ); this.assistantMessages = page.locator(".copilotKitAssistantMessage"); this.userMessages = page.locator(".copilotKitUserMessage"); } async waitForReady() { await expect(this.chatInput).toBeVisible(); } async sendMessage(message: string) { await this.chatInput.click(); await this.chatInput.fill(message); const sendBtn = this.page.locator( 'button[data-test-id="copilot-chat-ready"]' ); await expect(sendBtn).toBeEnabled(); await sendBtn.click(); // Wait for LLM to finish: in-progress → done await this.awaitLLMResponseDone(); } async awaitLLMResponseDone(timeout = 30_000) { // Wait for in-progress to start try { await this.page.waitForFunction( () => document.querySelector( 'button[data-copilotkit-in-progress="true"]' ) !== null, null, { timeout: 5000 } ); } catch { // May have already started and finished } // Wait for in-progress to end await this.page.waitForFunction( () => document.querySelector( 'button[data-copilotkit-in-progress="false"]' ) !== null || document.querySelector( 'button[data-test-id="copilot-chat-ready"]' ) !== null, null, { timeout } ); } async assertUserMessageVisible(text: string) { await expect(this.userMessages.getByText(text)).toBeVisible(); } async assertAgentReplyVisible(pattern: RegExp) { const message = this.assistantMessages.filter({ hasText: pattern }); await expect(message.last()).toBeVisible(); } } ================================================ FILE: apps/dojo/e2e/fixtures/openai/agentic-chat.json ================================================ { "fixtures": [ { "match": { "userMessage": "I am duaa" }, "response": { "content": "Hello duaa! How can I assist you today?" } }, { "match": { "userMessage": "background color to blue" }, "response": { "toolCalls": [ { "name": "change_background", "arguments": "{\"background\":\"blue\"}" } ] } }, { "match": { "userMessage": "background color to pink" }, "response": { "toolCalls": [ { "name": "change_background", "arguments": "{\"background\":\"pink\"}" } ] } }, { "match": { "userMessage": "stock price of AAPL" }, "response": { "content": "The current stock price of Apple Inc. (AAPL) is $150.25." } }, { "match": { "userMessage": "capital of France" }, "response": { "content": "The capital of France is Paris." } }, { "match": { "userMessage": "What was my first question" }, "response": { "content": "Your first question was about the capital of France." } }, { "match": { "userMessage": "favorite fruit is Mango" }, "response": { "content": "That's great! Mango is a wonderful tropical fruit known for its sweet, juicy flavor." } }, { "match": { "userMessage": "listening to Kaavish" }, "response": { "content": "Kaavish is a wonderful musical group known for their unique blend of Eastern and Western sounds!" } }, { "match": { "userMessage": "fact about Moon" }, "response": { "content": "The Moon is Earth's only natural satellite, orbiting at an average distance of about 384,400 km. It takes approximately 27.3 days to complete one orbit." } }, { "match": { "userMessage": "remind me what my favorite fruit" }, "response": { "content": "Your favorite fruit is Mango!" } }, { "match": { "userMessage": "counting down" }, "response": { "content": "counting down:\n10\n9\n8\n7\n6\n5\n4\n3\n2\n1\n\u2713" } }, { "match": { "userMessage": "tell me a joke" }, "response": { "content": "Why did the scarecrow win an award? Because he was outstanding in his field!" } }, { "match": { "userMessage": "say hello" }, "response": { "content": "Hello there! Nice to chat with you." } }, { "match": { "userMessage": "Hey there" }, "response": { "content": "Hello! How can I assist you today?" } }, { "match": { "userMessage": "Hi" }, "response": { "content": "Hello! How can I assist you today?" } }, { "match": { "userMessage": "my name is Alex" }, "response": { "content": "Hello Alex! Nice to meet you. How can I help you today?" } }, { "match": { "userMessage": "What is my name" }, "response": { "content": "Your name is Alex!" } } ] } ================================================ FILE: apps/dojo/e2e/fixtures/openai/agentic-gen-ui.json ================================================ { "fixtures": [ { "match": { "userMessage": "plan to make brownies" }, "response": { "toolCalls": [ { "name": "generate_task_steps_generative_ui", "arguments": "{\"steps\":[{\"description\":\"Gather ingredients: cocoa, butter, eggs, sugar, flour\",\"status\":\"pending\"},{\"description\":\"Melt butter and mix with cocoa and sugar\",\"status\":\"pending\"},{\"description\":\"Add eggs and flour, mix until smooth\",\"status\":\"pending\"},{\"description\":\"Pour into greased pan and bake at 350F for 25 minutes\",\"status\":\"pending\"}]}" } ] } }, { "match": { "userMessage": "Go to Mars" }, "response": { "toolCalls": [ { "name": "generate_task_steps_generative_ui", "arguments": "{\"steps\":[{\"description\":\"Design and build a spacecraft capable of Mars transit\",\"status\":\"pending\"},{\"description\":\"Assemble crew and run mission simulations\",\"status\":\"pending\"},{\"description\":\"Launch from Earth and navigate to Mars\",\"status\":\"pending\"},{\"description\":\"Land on Mars surface and establish base camp\",\"status\":\"pending\"}]}" } ] } }, { "match": { "userMessage": "help" }, "response": { "content": "Hello! I can help you with anything you need. Just tell me what you'd like to accomplish and I'll create a plan for you." } } ] } ================================================ FILE: apps/dojo/e2e/fixtures/openai/backend-tool-rendering.json ================================================ { "fixtures": [ { "match": { "userMessage": "Weather in San Francisco" }, "response": { "toolCalls": [ { "name": "get_weather", "arguments": "{\"location\":\"San Francisco\"}" } ] } }, { "match": { "userMessage": "Weather in New York" }, "response": { "toolCalls": [ { "name": "get_weather", "arguments": "{\"location\":\"New York\"}" } ] } }, { "match": { "userMessage": "weather" }, "response": { "toolCalls": [ { "name": "get_weather", "arguments": "{\"location\":\"San Francisco\"}" } ] } } ] } ================================================ FILE: apps/dojo/e2e/fixtures/openai/human-in-the-loop.json ================================================ { "fixtures": [ { "match": { "userMessage": "one step with eggs" }, "response": { "toolCalls": [ { "name": "generate_task_steps", "arguments": "{\"steps\":[{\"description\":\"Crack eggs into bowl\",\"status\":\"enabled\"},{\"description\":\"Preheat oven to 350F\",\"status\":\"enabled\"},{\"description\":\"Mix and bake for 25 min\",\"status\":\"enabled\"}]}" } ] } }, { "match": { "userMessage": "Does the planner include" }, "response": { "content": "No" } }, { "match": { "userMessage": "Start The Planning" }, "response": { "toolCalls": [ { "name": "generate_task_steps", "arguments": "{\"steps\":[{\"description\":\"Start The Planning\",\"status\":\"enabled\"},{\"description\":\"Design spacecraft\",\"status\":\"enabled\"},{\"description\":\"Launch mission\",\"status\":\"enabled\"}]}" } ] } }, { "match": { "userMessage": "trip to mars" }, "response": { "toolCalls": [ { "name": "generate_task_steps", "arguments": "{\"steps\":[{\"description\":\"Research mission requirements\",\"status\":\"enabled\"},{\"description\":\"Assemble crew\",\"status\":\"enabled\"},{\"description\":\"Build spacecraft\",\"status\":\"enabled\"},{\"description\":\"Launch from Earth\",\"status\":\"enabled\"},{\"description\":\"Land on Mars\",\"status\":\"enabled\"}]}" } ] } } ] } ================================================ FILE: apps/dojo/e2e/fixtures/openai/predictive-state.json ================================================ { "fixtures": [ { "match": { "userMessage": "dragon called Atlantis" }, "response": { "toolCalls": [ { "name": "write_document_local", "arguments": "{\"document\":\"Once upon a time, in a land far away, there lived a magnificent dragon named Atlantis. Atlantis was known throughout the realm for its shimmering scales that reflected the light of a thousand stars. The dragon Atlantis would soar above the mountains, breathing fire that lit up the night sky. Villagers would gather to watch Atlantis perform its aerial dances, marveling at the grace of this ancient creature.\"}" } ] } }, { "match": { "userMessage": "Change dragon name to Lola" }, "response": { "toolCalls": [ { "name": "write_document_local", "arguments": "{\"document\":\"Once upon a time, in a land far away, there lived a magnificent dragon named Lola. Lola was known throughout the realm for its shimmering scales that reflected the light of a thousand stars. The dragon Lola would soar above the mountains, breathing fire that lit up the night sky. Villagers would gather to watch Lola perform its aerial dances, marveling at the grace of this ancient creature.\"}" } ] } } ] } ================================================ FILE: apps/dojo/e2e/fixtures/openai/shared-state.json ================================================ { "fixtures": [ { "match": { "userMessage": "pasta recipe" }, "response": { "toolCalls": [ { "name": "updateWorkingMemory", "arguments": "{\"memory\":{\"recipe\":{\"skill_level\":\"Intermediate\",\"special_preferences\":[],\"cooking_time\":\"45 min\",\"ingredients\":[{\"icon\":\"🍝\",\"name\":\"Pasta\",\"amount\":\"400g\"},{\"icon\":\"🧂\",\"name\":\"Salt\",\"amount\":\"1 tsp\"},{\"icon\":\"🫒\",\"name\":\"Olive Oil\",\"amount\":\"4 tbsp\"},{\"icon\":\"🧄\",\"name\":\"Garlic\",\"amount\":\"6 cloves\"},{\"icon\":\"🍅\",\"name\":\"Tomatoes\",\"amount\":\"2 cups\"}],\"instructions\":[\"Boil water and cook pasta until al dente\",\"Slice garlic thinly and sauté in olive oil\",\"Dice tomatoes and add to the pan\",\"Season with salt to taste\",\"Toss pasta with the sauce and serve\"]}}}" } ] } }, { "match": { "userMessage": "the ingredients" }, "response": { "content": "Here are the ingredients:\n- Pasta: 400g\n- Salt: 1 tsp\n- Olive Oil: 4 tbsp\n- Garlic: 6 cloves\n- Tomatoes: 2 cups\n- Potatoes: 12\n- Carrots: 3\n- All-Purpose Flour: 2 cups" } } ] } ================================================ FILE: apps/dojo/e2e/fixtures/openai/subgraphs.json ================================================ { "fixtures": [] } ================================================ FILE: apps/dojo/e2e/fixtures/openai/tool-based-gen-ui.json ================================================ { "fixtures": [ { "match": { "userMessage": "I will always win" }, "response": { "toolCalls": [ { "name": "generate_haiku", "arguments": "{\"japanese\":[\"勝利の道を\",\"常に歩み続ける\",\"勝つ運命よ\"],\"english\":[\"On the path of victory\",\"I will always keep walking\",\"Destined to always win\"],\"image_name\":\"Mount_Fuji_Lake_Reflection_Cherry_Blossoms_Sakura_Spring.jpg\",\"gradient\":\"linear-gradient(135deg, #667eea 0%, #764ba2 100%)\"}" } ] } }, { "match": { "userMessage": "moon shines bright" }, "response": { "toolCalls": [ { "name": "generate_haiku", "arguments": "{\"japanese\":[\"月が輝く\",\"夜空に静かに浮かぶ\",\"光の詩よ\"],\"english\":[\"The bright moon shining\",\"Floating quietly in night sky\",\"A poem of light\"],\"image_name\":\"Cherry_Blossoms_Sakura_Night_View_City_Lights_Japan.jpg\",\"gradient\":\"linear-gradient(135deg, #0c3547 0%, #204f64 50%, #2d6187 100%)\"}" } ] } } ] } ================================================ FILE: apps/dojo/e2e/fixtures/openai/v1-chat.json ================================================ { "fixtures": [ { "match": { "userMessage": "Hello" }, "response": { "content": "Hello! How can I help you today?" } } ] } ================================================ FILE: apps/dojo/e2e/lib/constants.ts ================================================ /** * The default welcome message shown by CopilotChat/CopilotSidebar when no * custom `welcomeMessageText` label is provided. Update this single constant * if the default ever changes—all e2e page objects reference it. */ export const DEFAULT_WELCOME_MESSAGE = "How can I help you today?"; ================================================ FILE: apps/dojo/e2e/lib/mock-agent.ts ================================================ import { Page, Route } from "@playwright/test"; /** * Deterministic mock agent for Playwright e2e tests. * * Intercepts CopilotKit API calls at the browser level and returns * pre-defined SSE responses. This allows testing UI behavior (background * color changes, regenerate, shared state) without depending on live LLM * responses, eliminating the primary source of test flakiness. * * Usage: * const mock = new MockAgent(page); * mock.onMessage("background color to blue", * mock.toolCall("change_background", { background: "blue" }) * ); * await mock.install(); * // ... run test ... * await mock.uninstall(); */ // AG-UI event types used in SSE responses interface SSEEvent { type: string; [key: string]: unknown; } type ResponseSequence = SSEEvent[]; interface MessageHandler { pattern: string | RegExp; responses: ResponseSequence; once: boolean; used: boolean; } const ROUTE_PATTERN = /\/api\/copilotkit(next)?\/[^/]+/; export class MockAgent { private page: Page; private handlers: MessageHandler[] = []; private fallbackResponse: ResponseSequence | null = null; private installed = false; private routeHandler: ((route: Route) => Promise) | null = null; private runCounter = 0; private messageCounter = 0; private toolCallCounter = 0; constructor(page: Page) { this.page = page; } private nextRunId() { return `mock-run-${++this.runCounter}`; } private nextMessageId() { return `mock-msg-${++this.messageCounter}`; } private nextToolCallId() { return `mock-tc-${++this.toolCallCounter}`; } /** * Register a response for messages matching a pattern. */ onMessage( pattern: string | RegExp, responses: ResponseSequence, options: { once?: boolean } = {} ): this { this.handlers.push({ pattern, responses, once: options.once ?? false, used: false, }); return this; } /** * Set a fallback response for unmatched messages. */ onAnyMessage(responses: ResponseSequence): this { this.fallbackResponse = responses; return this; } /** * Install the route interceptor. Call before page.goto(). */ async install(): Promise { if (this.installed) return; this.routeHandler = async (route: Route) => { const request = route.request(); // Only intercept POST requests (SSE streams) if (request.method() !== "POST") { await route.continue(); return; } try { let body: string; try { body = request.postData() ?? ""; } catch (err) { console.warn("[MockAgent] Failed to read postData():", err instanceof Error ? err.message : err); body = ""; } // Find the user's last message in the request body. // If there's no user message (e.g. CopilotKit initialization request), // pass through to the real backend so the app can boot normally. const lastUserMessage = this.extractLastUserMessage(body); if (lastUserMessage === null) { await route.continue(); return; } const responses = this.findResponse(lastUserMessage); const sseBody = responses .map((event) => `data: ${JSON.stringify(event)}\n\n`) .join(""); await route.fulfill({ status: 200, headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", }, body: sseBody, }); } catch (err) { console.error("[MockAgent] Route handler error:", err instanceof Error ? err.message : err); await route.abort("failed").catch(() => {}); } }; await this.page.route(ROUTE_PATTERN, this.routeHandler); this.installed = true; } /** * Remove the route interceptor. */ async uninstall(): Promise { if (!this.installed || !this.routeHandler) return; await this.page.unroute(ROUTE_PATTERN, this.routeHandler); this.routeHandler = null; this.installed = false; } private extractLastUserMessage(body: string): string | null { try { const parsed = JSON.parse(body); // CopilotKit v2 format: { body: { messages: [...] } } const messages = parsed?.body?.messages ?? parsed?.messages ?? []; for (let i = messages.length - 1; i >= 0; i--) { if (messages[i]?.role === "user") { // Content can be a string or array of content parts const content = messages[i].content; if (typeof content === "string") return content; if (Array.isArray(content)) { const textPart = content.find( (p: { type: string; text?: string }) => p.type === "text" ); return textPart?.text ?? ""; } return ""; // user message exists but content shape is unrecognized } } } catch { // Not JSON or unexpected format } return null; // no user message found — likely an init request } private findResponse(userMessage: string): ResponseSequence { for (const handler of this.handlers) { if (handler.once && handler.used) continue; const matches = typeof handler.pattern === "string" ? userMessage.toLowerCase().includes(handler.pattern.toLowerCase()) : handler.pattern.test(userMessage); if (matches) { if (handler.once) handler.used = true; return handler.responses; } } if (this.fallbackResponse) { return this.fallbackResponse; } // Default: simple acknowledgment with stable IDs return [ { type: "RUN_STARTED", runId: "mock-run-default", threadId: "mock-thread" }, { type: "TEXT_MESSAGE_START", messageId: "mock-msg-default", role: "assistant" }, { type: "TEXT_MESSAGE_CONTENT", messageId: "mock-msg-default", delta: "I understand. How can I help?" }, { type: "TEXT_MESSAGE_END", messageId: "mock-msg-default" }, { type: "RUN_FINISHED", runId: "mock-run-default", threadId: "mock-thread" }, ]; } // ── Instance helpers for building response sequences ── /** * Build a text message response sequence. */ textMessage( text: string, options: { runId?: string; messageId?: string } = {} ): ResponseSequence { const runId = options.runId ?? this.nextRunId(); const messageId = options.messageId ?? this.nextMessageId(); const threadId = "mock-thread"; return [ { type: "RUN_STARTED", runId, threadId }, { type: "TEXT_MESSAGE_START", messageId, role: "assistant" }, { type: "TEXT_MESSAGE_CONTENT", messageId, delta: text }, { type: "TEXT_MESSAGE_END", messageId }, { type: "RUN_FINISHED", runId, threadId }, ]; } /** * Build a frontend tool call response sequence. * * For frontend tools (registered via useFrontendTool), CopilotKit uses a * multi-run pattern: * Run 1: Server sends TOOL_CALL events (no TOOL_CALL_RESULT) + RUN_FINISHED * Client: CopilotKit detects the unresolved tool call, executes the * frontend handler locally, then makes a follow-up request. * Run 2: Server responds with text (handled by fallback or another handler). * * IMPORTANT: Do NOT include TOOL_CALL_RESULT in the response — that tells * CopilotKit the tool was already executed server-side and it will skip * calling the frontend handler. Use { once: true } on the handler so the * follow-up request falls through to the fallback. */ toolCall( toolName: string, args: Record, options: { runId?: string; } = {} ): ResponseSequence { const runId = options.runId ?? this.nextRunId(); const toolParentMessageId = this.nextMessageId(); const toolCallId = this.nextToolCallId(); const threadId = "mock-thread"; return [ { type: "RUN_STARTED", runId, threadId }, { type: "TOOL_CALL_START", toolCallId, toolCallName: toolName, parentMessageId: toolParentMessageId, }, { type: "TOOL_CALL_ARGS", toolCallId, delta: JSON.stringify(args), }, { type: "TOOL_CALL_END", toolCallId }, { type: "RUN_FINISHED", runId, threadId }, ]; } /** * Concatenate multiple response sequences into one. */ static combine(...sequences: ResponseSequence[]): ResponseSequence { return sequences.flat(); } } ================================================ FILE: apps/dojo/e2e/lib/upload-video.ts ================================================ import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; import { readFileSync, existsSync } from "fs"; import { basename } from "path"; export interface VideoToUpload { videoPath: string; s3ObjectPath: string; testName: string; suiteName?: string; } export interface S3Config { bucketName: string; region: string; accessKeyId?: string; secretAccessKey?: string; } export class S3VideoUploader { private s3Client: S3Client; private config: S3Config; constructor(config: S3Config) { this.config = config; // Initialize S3 client with credentials from environment or passed config this.s3Client = new S3Client({ region: config.region, credentials: config.accessKeyId && config.secretAccessKey ? { accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey, } : undefined, // Use default credential chain if not provided }); } /** * Generate S3 object path for a video file */ generateS3Path( videoPath: string, testName: string, suiteName?: string ): string { const filename = basename(videoPath); const runId = process.env.GITHUB_RUN_ID || `local-${Date.now()}`; const projectName = process.env.GITHUB_REPOSITORY?.split("/")[1] || "cpk-demos-smoke-tests"; // Clean test names for file paths const cleanSuite = suiteName?.replace(/[^a-zA-Z0-9-_]/g, "-") || "unknown-suite"; const cleanTest = testName.replace(/[^a-zA-Z0-9-_]/g, "-"); return `github-runs/${runId}/${projectName}/${cleanSuite}/${cleanTest}/${filename}`; } /** * Generate public S3 URL for a given object path */ generatePublicUrl(s3ObjectPath: string): string { return `https://${this.config.bucketName}.s3.${this.config.region}.amazonaws.com/${s3ObjectPath}`; } /** * Upload a single video file to S3 */ async uploadVideo(video: VideoToUpload): Promise { try { // Check if file exists if (!existsSync(video.videoPath)) { throw new Error(`Video file not found: ${video.videoPath}`); } console.log( `📹 Uploading video: ${basename(video.videoPath)} for test: ${ video.testName }` ); // Read file content const fileContent = readFileSync(video.videoPath); // Upload to S3 const command = new PutObjectCommand({ Bucket: this.config.bucketName, Key: video.s3ObjectPath, Body: fileContent, ContentType: "video/webm", CacheControl: "public, max-age=86400", // Cache for 1 day Metadata: { "test-name": video.testName, "suite-name": video.suiteName || "unknown", "upload-time": new Date().toISOString(), }, }); await this.s3Client.send(command); const publicUrl = this.generatePublicUrl(video.s3ObjectPath); console.log(`✅ Video uploaded successfully: ${publicUrl}`); return publicUrl; } catch (error) { console.error(`❌ Failed to upload video ${video.videoPath}:`, error); throw error; } } /** * Upload multiple videos concurrently */ async uploadVideos( videos: VideoToUpload[] ): Promise<{ url: string; testName: string; suiteName?: string }[]> { if (videos.length === 0) { console.log("📹 No videos to upload"); return []; } console.log(`📹 Uploading ${videos.length} video(s) to S3...`); const uploadPromises = videos.map(async (video) => { try { const url = await this.uploadVideo(video); return { url, testName: video.testName, suiteName: video.suiteName, }; } catch (error) { console.error( `Failed to upload video for test ${video.testName}:`, error ); return null; } }); const results = await Promise.allSettled(uploadPromises); // Filter out failed uploads const successfulUploads = results .filter( ( result ): result is PromiseFulfilledResult<{ url: string; testName: string; suiteName?: string; } | null> => result.status === "fulfilled" && result.value !== null ) .map((result) => result.value!); const failedUploads = results.filter( (result) => result.status === "rejected" ).length; console.log(`✅ Successfully uploaded ${successfulUploads.length} videos`); if (failedUploads > 0) { console.warn(`⚠️ ${failedUploads} videos failed to upload`); } return successfulUploads; } } /** * Factory function to create uploader with environment variables */ export function createS3Uploader(): S3VideoUploader | null { const bucketName = process.env.AWS_S3_BUCKET_NAME; const region = process.env.AWS_S3_REGION || "us-east-1"; if (!bucketName) { console.warn("⚠️ AWS_S3_BUCKET_NAME not set, video upload disabled"); return null; } return new S3VideoUploader({ bucketName, region, accessKeyId: process.env.AWS_ACCESS_KEY_ID, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, }); } ================================================ FILE: apps/dojo/e2e/llmock-setup.ts ================================================ import { LLMock, type ChatMessage } from "@copilotkit/llmock"; import * as path from "node:path"; const MOCK_PORT = 5555; const FIXTURES_DIR = path.join(import.meta.dirname, "fixtures", "openai"); let mockServer: LLMock | null = null; export async function setupLLMock(): Promise { console.log("🔧 Starting LLMock server..."); // Small per-chunk latency prevents crew-ai's asyncio event loop from // getting congested by zero-latency streaming (real OpenAI has natural // network delays between chunks; LLMock needs to simulate this). mockServer = new LLMock({ port: MOCK_PORT, latency: 5 }); // Extract text from message content — handles both string and array-of-parts // (Strands SDK sends content as [{type: "text", text: "..."}]) const textOf = (content: ChatMessage["content"] | undefined): string => { if (typeof content === "string") return content; if (Array.isArray(content)) { return content .filter((p) => p.type === "text" && typeof p.text === "string") .map((p) => p.text!) .join(""); } return ""; }; // LangGraph HITL: the LangGraph agent registers tool `plan_execution_steps`, // not `generate_task_steps`. The JSON fixture returns `generate_task_steps` // which CopilotKit's useHumanInTheLoop() handles (wrong UI: Confirm/Reject). // LangGraph needs the correct tool name so chatNode routes to processStepsNode, // which calls interrupt() and triggers useLangGraphInterrupt() (correct UI: // Perform Steps). These predicate fixtures MUST come before loadFixtureFile. mockServer.addFixture({ match: { predicate: (req) => { const lastUser = req.messages.filter((m) => m.role === "user").pop(); const hasLangGraphTool = req.tools?.some( (t) => t.function.name === "plan_execution_steps", ); return ( !!hasLangGraphTool && textOf(lastUser?.content).includes("one step with eggs") ); }, }, response: { toolCalls: [ { name: "plan_execution_steps", arguments: JSON.stringify({ steps: [ { description: "Crack eggs into bowl", status: "enabled" }, { description: "Preheat oven to 350F", status: "enabled" }, { description: "Mix and bake for 25 min", status: "enabled" }, ], }), }, ], }, }); mockServer.addFixture({ match: { predicate: (req) => { const lastUser = req.messages.filter((m) => m.role === "user").pop(); const hasLangGraphTool = req.tools?.some( (t) => t.function.name === "plan_execution_steps", ); return ( !!hasLangGraphTool && textOf(lastUser?.content).includes("Start The Planning") ); }, }, response: { toolCalls: [ { name: "plan_execution_steps", arguments: JSON.stringify({ steps: [ { description: "Start The Planning", status: "enabled" }, { description: "Design spacecraft", status: "enabled" }, { description: "Launch mission", status: "enabled" }, ], }), }, ], }, }); // Claude Agent SDK HITL: same pattern as LangGraph above. The CLI registers // tools as mcp__ag_ui__generate_task_steps. The JSON fixture returns bare // generate_task_steps which the TS CLI resolves, but the Python CLI needs the // exact MCP-prefixed name. These predicate fixtures fire before the JSON ones. mockServer.addFixture({ match: { predicate: (req) => { const lastUser = req.messages.filter((m) => m.role === "user").pop(); const hasClaudeSdkTool = req.tools?.some( (t) => t.function.name.endsWith("__generate_task_steps"), ); return ( !!hasClaudeSdkTool && textOf(lastUser?.content).includes("one step with eggs") ); }, }, response: { toolCalls: [ { name: "mcp__ag_ui__generate_task_steps", arguments: JSON.stringify({ steps: [ { description: "Crack eggs into bowl", status: "enabled" }, { description: "Preheat oven to 350F", status: "enabled" }, { description: "Mix and bake for 25 min", status: "enabled" }, ], }), }, ], }, }); mockServer.addFixture({ match: { predicate: (req) => { const lastUser = req.messages.filter((m) => m.role === "user").pop(); const hasClaudeSdkTool = req.tools?.some( (t) => t.function.name.endsWith("__generate_task_steps"), ); return ( !!hasClaudeSdkTool && textOf(lastUser?.content).includes("Start The Planning") ); }, }, response: { toolCalls: [ { name: "mcp__ag_ui__generate_task_steps", arguments: JSON.stringify({ steps: [ { description: "Start The Planning", status: "enabled" }, { description: "Design spacecraft", status: "enabled" }, { description: "Launch mission", status: "enabled" }, ], }), }, ], }, }); // Load HITL fixtures — they share a "plan to make brownies" substring // with agentic-gen-ui fixtures, and first-match-wins. By loading HITL first, // "one step with eggs" matches HITL tests before "plan to make brownies" // matches the agenticGenUI fixture (which returns the wrong tool name). // NOTE: LangGraph and Claude SDK predicate fixtures above take priority // over these for requests containing their specific tool names. mockServer.loadFixtureFile(path.join(FIXTURES_DIR, "human-in-the-loop.json")); const sysContent = (msgs: ChatMessage[]) => msgs.find((m) => m.role === "system")?.content ?? ""; // Case-insensitive check for system prompt content — Python booleans are // True/False (capitalized) while JavaScript uses true/false (lowercase). const sysIncludes = (msgs: ChatMessage[], substr: string) => { const sys = typeof sysContent(msgs) === "string" ? (sysContent(msgs) as string) : ""; return sys.toLowerCase().includes(substr.toLowerCase()); }; const supervisorRoute = (nextAgent: string, answer: string) => ({ response: { toolCalls: [ { name: "supervisor_response", arguments: JSON.stringify({ answer, next_agent: nextAgent }), }, ], }, }); // Supervisor: no flights yet → route to flights_agent mockServer.addFixture({ match: { predicate: (req) => sysIncludes(req.messages, "Flights found: false"), }, ...supervisorRoute("flights_agent", "Let me find flights for you!"), }); // Supervisor: flights found, no hotels → route to hotels_agent mockServer.addFixture({ match: { predicate: (req) => sysIncludes(req.messages, "Flights found: true") && sysIncludes(req.messages, "Hotels found: false"), }, ...supervisorRoute( "hotels_agent", "Great choice! Now let me find hotels for you.", ), }); // Supervisor: flights + hotels done, experiences not yet → route to experiences_agent // NOTE: state.experiences has no default (undefined), so hasExperiences is always "true" // in the system prompt. We distinguish by checking if the experiences agent's // response text is already in the messages. mockServer.addFixture({ match: { predicate: (req) => { const experiencesDone = req.messages.some( (m) => m.role === "assistant" && textOf(m.content).includes("wonderful experiences"), ); return ( sysIncludes(req.messages, "Hotels found: true") && !experiencesDone ); }, }, ...supervisorRoute( "experiences_agent", "Excellent! Now let me find some experiences for you.", ), }); // Supervisor: all agents completed → route to complete mockServer.addFixture({ match: { predicate: (req) => { const experiencesDone = req.messages.some( (m) => m.role === "assistant" && textOf(m.content).includes("wonderful experiences"), ); return ( sysIncludes(req.messages, "Hotels found: true") && experiencesDone ); }, }, ...supervisorRoute("complete", "Your travel plan is all set!"), }); // Experiences agent's own ChatOpenAI call — returns generic text mockServer.addFixture({ match: { predicate: (req) => sysIncludes(req.messages, "You are the experiences agent"), }, response: { content: "I've found some wonderful experiences for your trip to San Francisco!", }, }); // Strands agentic gen UI: the Strands agent registers plan_task_steps, // not generate_task_steps_generative_ui. Predicate fixtures detect the // Strands tool name in the request and return the correct tool call. mockServer.addFixture({ match: { predicate: (req) => { const lastUser = req.messages.filter((m) => m.role === "user").pop(); const hasStrandsTool = req.tools?.some( (t) => t.function.name === "plan_task_steps", ); return ( !!hasStrandsTool && textOf(lastUser?.content).includes("plan to make brownies") ); }, }, response: { toolCalls: [ { name: "plan_task_steps", arguments: JSON.stringify({ task: "make brownies", context: "", steps: [ { description: "Gather ingredients", status: "pending" }, { description: "Melt butter and mix with cocoa", status: "pending", }, { description: "Add eggs and flour", status: "pending" }, { description: "Bake at 350F for 25 min", status: "pending" }, ], }), }, ], }, }); mockServer.addFixture({ match: { predicate: (req) => { const lastUser = req.messages.filter((m) => m.role === "user").pop(); const hasStrandsTool = req.tools?.some( (t) => t.function.name === "plan_task_steps", ); return ( !!hasStrandsTool && textOf(lastUser?.content).includes("Go to Mars") ); }, }, response: { toolCalls: [ { name: "plan_task_steps", arguments: JSON.stringify({ task: "Go to Mars", context: "", steps: [ { description: "Design spacecraft", status: "pending" }, { description: "Assemble crew", status: "pending" }, { description: "Launch from Earth", status: "pending" }, { description: "Land on Mars", status: "pending" }, ], }), }, ], }, }); // Shared state: ADK/Strands use generate_recipe (not updateWorkingMemory). // The JSON fixture in shared-state.json returns updateWorkingMemory which // only works for CopilotKit frameworks (Agno/LangGraph). These predicate // fixtures fire first for ADK and Strands (which both register generate_recipe). const recipeData = { title: "Pasta Aglio e Olio", skill_level: "Intermediate", special_preferences: [] as string[], cooking_time: "45 min", ingredients: [ { icon: "🍝", name: "Pasta", amount: "400g" }, { icon: "🧂", name: "Salt", amount: "1 tsp" }, { icon: "🫒", name: "Olive Oil", amount: "4 tbsp" }, { icon: "🧄", name: "Garlic", amount: "6 cloves" }, { icon: "🍅", name: "Tomatoes", amount: "2 cups" }, ], instructions: [ "Boil water and cook pasta until al dente", "Slice garlic thinly and sauté in olive oil", "Dice tomatoes and add to the pan", "Season with salt to taste", "Toss pasta with the sauce and serve", ], changes: "", }; // Strands/CrewAI/LangGraph: generate_recipe(recipe: Recipe) — nested {recipe: {...}} args. // These frameworks wrap recipe data under a "recipe" key. Discriminate from ADK // (flat args) via two signals: (1) tool schema has parameters.properties.recipe // (available in OpenAI-format requests), or (2) system prompt contains // "helpful recipe assistant" (Strands — whose Gemini SDK omits parameter // schemas from functionDeclarations). mockServer.addFixture({ match: { predicate: (req) => { const lastUser = req.messages.filter((m) => m.role === "user").pop(); const recipeTool = req.tools?.find( (t) => t.function.name === "generate_recipe", ); const hasNestedRecipeParam = !!( (recipeTool?.function.parameters as Record) ?.properties as Record )?.recipe; return ( !!recipeTool && (hasNestedRecipeParam || sysIncludes(req.messages, "helpful recipe assistant")) && textOf(lastUser?.content).includes("pasta recipe") ); }, }, response: { toolCalls: [ { name: "generate_recipe", arguments: JSON.stringify({ recipe: recipeData }), }, ], }, }); // ADK: generate_recipe(skill_level, title, ...) — flat argument format. // Falls through when neither tool schema nor system prompt indicates nested args. mockServer.addFixture({ match: { predicate: (req) => { const lastUser = req.messages.filter((m) => m.role === "user").pop(); const recipeTool = req.tools?.find( (t) => t.function.name === "generate_recipe", ); const hasNestedRecipeParam = !!( (recipeTool?.function.parameters as Record) ?.properties as Record )?.recipe; return ( !!recipeTool && !hasNestedRecipeParam && !sysIncludes(req.messages, "helpful recipe assistant") && textOf(lastUser?.content).includes("pasta recipe") ); }, }, response: { toolCalls: [ { name: "generate_recipe", arguments: JSON.stringify(recipeData) }, ], }, }); // Pydantic AI shared state: the agent registers display_recipe, // not updateWorkingMemory. The Recipe model differs from ADK/Strands // (no title/changes fields, StrEnum values for skill_level/cooking_time). // IMPORTANT: pydantic-ai's single_arg_name optimization means a tool with // one model-like parameter (e.g. display_recipe(recipe: Recipe)) uses the // model's schema directly as the tool JSON schema — so the arguments must // be the Recipe fields at the top level, NOT wrapped in {"recipe": {...}}. const pydanticRecipeData = { skill_level: "Intermediate", special_preferences: [] as string[], cooking_time: "45 min", ingredients: [ { icon: "🍝", name: "Pasta", amount: "400g" }, { icon: "🧂", name: "Salt", amount: "1 tsp" }, { icon: "🫒", name: "Olive Oil", amount: "4 tbsp" }, { icon: "🧄", name: "Garlic", amount: "6 cloves" }, { icon: "🍅", name: "Tomatoes", amount: "2 cups" }, ], instructions: [ "Boil water and cook pasta until al dente", "Slice garlic thinly and sauté in olive oil", "Dice tomatoes and add to the pan", "Season with salt to taste", "Toss pasta with the sauce and serve", ], }; mockServer.addFixture({ match: { predicate: (req) => { const lastUser = req.messages.filter((m) => m.role === "user").pop(); const hasPydanticTool = req.tools?.some( (t) => t.function.name === "display_recipe", ); return ( !!hasPydanticTool && textOf(lastUser?.content).includes("pasta recipe") ); }, }, response: { toolCalls: [ { name: "display_recipe", arguments: JSON.stringify(pydanticRecipeData), }, ], }, }); // Pydantic AI agentic gen UI: the agent registers create_plan, // not generate_task_steps_generative_ui. Predicate fixtures detect the // Pydantic AI tool name and return the correct tool call. mockServer.addFixture({ match: { predicate: (req) => { const lastUser = req.messages.filter((m) => m.role === "user").pop(); const hasPydanticTool = req.tools?.some( (t) => t.function.name === "create_plan", ); return ( !!hasPydanticTool && textOf(lastUser?.content).includes("plan to make brownies") ); }, }, response: { toolCalls: [ { name: "create_plan", arguments: JSON.stringify({ steps: [ "Gather ingredients", "Melt butter and mix with cocoa", "Add eggs and flour", "Bake at 350F for 25 min", ], }), }, ], }, }); mockServer.addFixture({ match: { predicate: (req) => { const lastUser = req.messages.filter((m) => m.role === "user").pop(); const hasPydanticTool = req.tools?.some( (t) => t.function.name === "create_plan", ); return ( !!hasPydanticTool && textOf(lastUser?.content).includes("Go to Mars") ); }, }, response: { toolCalls: [ { name: "create_plan", arguments: JSON.stringify({ steps: [ "Design spacecraft", "Assemble crew", "Launch from Earth", "Land on Mars", ], }), }, ], }, }); // Langroid agentic gen UI: Langroid embeds tool definitions in the system // message text (TOOL: create_plan) instead of using the OpenAI tools array. // Detect via system message content since req.tools will be empty. mockServer.addFixture({ match: { predicate: (req) => { const lastUser = req.messages.filter((m) => m.role === "user").pop(); const hasLangroidTool = sysIncludes(req.messages, "TOOL: create_plan"); return ( !!hasLangroidTool && textOf(lastUser?.content).includes("plan to make brownies") ); }, }, response: { toolCalls: [ { name: "create_plan", arguments: JSON.stringify({ request: "create_plan", steps: [ "Gather ingredients", "Melt butter and mix with cocoa", "Add eggs and flour", "Bake at 350F for 25 min", ], }), }, ], }, }); mockServer.addFixture({ match: { predicate: (req) => { const lastUser = req.messages.filter((m) => m.role === "user").pop(); const hasLangroidTool = sysIncludes(req.messages, "TOOL: create_plan"); return ( !!hasLangroidTool && textOf(lastUser?.content).includes("Go to Mars") ); }, }, response: { toolCalls: [ { name: "create_plan", arguments: JSON.stringify({ request: "create_plan", steps: [ "Design spacecraft", "Assemble crew", "Launch from Earth", "Land on Mars", ], }), }, ], }, }); // Langroid shared state: Langroid embeds generate_recipe in the system message. // The recipe arg is nested under "recipe" key like Strands/CrewAI/LangGraph. mockServer.addFixture({ match: { predicate: (req) => { const lastUser = req.messages.filter((m) => m.role === "user").pop(); const hasLangroidTool = sysIncludes( req.messages, "TOOL: generate_recipe", ); return ( !!hasLangroidTool && textOf(lastUser?.content).includes("pasta recipe") ); }, }, response: { toolCalls: [ { name: "generate_recipe", arguments: JSON.stringify({ request: "generate_recipe", recipe: recipeData, }), }, ], }, }); // LlamaIndex agentic gen UI: the agent registers run_task (a backend tool), // not generate_task_steps_generative_ui. The run_task tool takes a Task // model with steps: list[Step], where each Step has a description string. // Arguments are wrapped in {"task": {...}} since llama-index exposes the // function parameter name as the top-level key. mockServer.addFixture({ match: { predicate: (req) => { const lastUser = req.messages.filter((m) => m.role === "user").pop(); const hasLlamaIndexTool = req.tools?.some( (t) => t.function.name === "run_task", ); return ( !!hasLlamaIndexTool && textOf(lastUser?.content).includes("plan to make brownies") ); }, }, response: { toolCalls: [ { name: "run_task", arguments: JSON.stringify({ task: { steps: [ { description: "Gather ingredients" }, { description: "Melt butter and mix with cocoa" }, { description: "Add eggs and flour" }, { description: "Bake at 350F for 25 min" }, ], }, }), }, ], }, }); mockServer.addFixture({ match: { predicate: (req) => { const lastUser = req.messages.filter((m) => m.role === "user").pop(); const hasLlamaIndexTool = req.tools?.some( (t) => t.function.name === "run_task", ); return ( !!hasLlamaIndexTool && textOf(lastUser?.content).includes("Go to Mars") ); }, }, response: { toolCalls: [ { name: "run_task", arguments: JSON.stringify({ task: { steps: [ { description: "Design spacecraft" }, { description: "Assemble crew" }, { description: "Launch from Earth" }, { description: "Land on Mars" }, ], }, }), }, ], }, }); // LlamaIndex shared state: the agent registers update_recipe (a frontend // tool), not updateWorkingMemory. The Recipe model has skill_level, // special_preferences, cooking_time, ingredients, instructions (no title // or changes). Arguments are wrapped in {"recipe": {...}}. mockServer.addFixture({ match: { predicate: (req) => { const lastUser = req.messages.filter((m) => m.role === "user").pop(); const hasLlamaIndexTool = req.tools?.some( (t) => t.function.name === "update_recipe", ); return ( !!hasLlamaIndexTool && textOf(lastUser?.content).includes("pasta recipe") ); }, }, response: { toolCalls: [ { name: "update_recipe", arguments: JSON.stringify({ recipe: pydanticRecipeData, }), }, ], }, }); // Claude Agent SDK shared state: the adapter registers ag_ui_update_state // via an MCP server named "ag_ui", so the CLI sends the tool as // mcp__ag_ui__ag_ui_update_state. Match both bare and MCP-prefixed names. mockServer.addFixture({ match: { predicate: (req) => { const lastUser = req.messages.filter((m) => m.role === "user").pop(); const hasClaudeSdkTool = req.tools?.some( (t) => t.function.name === "ag_ui_update_state" || t.function.name.endsWith("__ag_ui_update_state"), ); return ( !!hasClaudeSdkTool && textOf(lastUser?.content).includes("pasta recipe") ); }, }, response: { toolCalls: [ { // Use MCP-prefixed name so the CLI can route it to the right tool. // The Python Claude SDK CLI requires exact name matching. name: "mcp__ag_ui__ag_ui_update_state", arguments: JSON.stringify({ state_updates: { recipe: recipeData } }), }, ], }, }); // Load all fixture JSON files from the fixtures directory // (HITL fixtures are duplicated but the earlier copies match first) mockServer.loadFixtureDir(FIXTURES_DIR); // Programmatic catch-all: when the last message is a tool result, // return a generic text acknowledgment. This must be added AFTER // fixture files so it appears last in the fixture list — but // fixture-file entries only match on userMessage (substring), and // a follow-up request after a tool call still has the same last // user message, so we need this predicate to fire FIRST. // Insert at position 0 so it's checked before file-based fixtures. // Prepend so it matches before substring-based fixtures on follow-up requests mockServer.prependFixture({ match: { predicate: (req) => { const last = req.messages[req.messages.length - 1]; return last?.role === "tool"; }, }, response: { content: "Done! I've completed that for you." }, }); // Universal catch-all: matches any request that wasn't handled above. // Appended LAST so specific fixtures always take priority. // Log unmatched requests for debugging fixture mismatches. mockServer.addFixture({ match: { predicate: (req) => { const lastUser = req.messages.filter((m) => m.role === "user").pop(); const userText = lastUser ? textOf(lastUser.content) : "(no user msg)"; const toolNames = req.tools?.map((t) => t.function.name).join(",") || "(no tools)"; const contentType = lastUser ? typeof lastUser.content : "N/A"; const contentSample = lastUser ? JSON.stringify(lastUser.content).slice(0, 120) : "N/A"; console.error( `[LLMock CATCH-ALL] model=${req.model} lastUser="${userText.slice(0, 80)}" tools=[${toolNames}] msgs=${req.messages.length} contentType=${contentType} content=${contentSample}`, ); return true; }, }, response: { content: "I understand. How can I help you with that?" }, }); // Log fixture counts for debugging const allFixtures = mockServer.getFixtures(); const predicateCount = allFixtures.filter((f) => f.match.predicate).length; const userMsgCount = allFixtures.filter((f) => f.match.userMessage).length; console.log( ` Fixture stats: ${allFixtures.length} total, ${predicateCount} predicate, ${userMsgCount} userMessage`, ); // Log the userMessage fixtures to verify they loaded allFixtures.forEach((f, i) => { if (f.match.userMessage) { console.log( ` [${i}] userMessage: "${String(f.match.userMessage).slice(0, 50)}"`, ); } }); const url = await mockServer.start(); console.log(`✅ LLMock server running at ${url}`); console.log(` Fixtures loaded from: ${FIXTURES_DIR}`); // Export the URL for child processes to use process.env.LLMOCK_URL = `${url}/v1`; } export async function teardownLLMock(): Promise { if (mockServer) { console.log("🧹 Stopping LLMock server..."); await mockServer.stop(); mockServer = null; console.log("✅ LLMock server stopped"); } } export function getMockServer(): LLMock | null { return mockServer; } ================================================ FILE: apps/dojo/e2e/package.json ================================================ { "name": "copilotkit-e2e", "version": "0.1.0", "private": true, "type": "module", "description": "Scheduled Playwright smoke tests for CopilotKit demo apps", "scripts": { "postinstall": "playwright install --with-deps", "test": "playwright test", "test:ui": "playwright test --ui", "report": "playwright show-report" }, "devDependencies": { "@playwright/test": "^1.43.1", "@slack/types": "^2.14.0", "@types/node": "^22.15.28", "playwright-slack-report": "^1.1.93" }, "dependencies": { "@aws-sdk/client-s3": "^3.600.0", "json2md": "^2.0.1" } } ================================================ FILE: apps/dojo/e2e/pages/a2aMiddlewarePages/A2AChatPage.ts ================================================ import { Page, Locator, expect } from '@playwright/test'; export class A2AChatPage { readonly page: Page; readonly mainChatTab: Locator; constructor(page: Page) { this.page = page; this.mainChatTab = page.getByRole('tab', {name: 'Main Chat' }); } async openChat() { await expect(this.mainChatTab).toBeVisible(); } } ================================================ FILE: apps/dojo/e2e/pages/adkMiddlewarePages/HumanInLoopPage.ts ================================================ import { Page, Locator, expect } from "@playwright/test"; import { CopilotSelectors } from "../../utils/copilot-selectors"; import { sendAndAwaitResponse } from "../../utils/copilot-actions"; import { DEFAULT_WELCOME_MESSAGE } from "../../lib/constants"; export class HumanInLoopPage { readonly page: Page; readonly planTaskButton: Locator; readonly chatInput: Locator; readonly sendButton: Locator; readonly agentGreeting: Locator; readonly plan: Locator; readonly performStepsButton: Locator; readonly agentMessage: Locator; readonly userMessage: Locator; constructor(page: Page) { this.page = page; this.planTaskButton = page.getByRole("button", { name: "Human in the loop Plan a task", }); this.agentGreeting = page.getByText(DEFAULT_WELCOME_MESSAGE); this.chatInput = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); this.plan = page.getByTestId("select-steps"); this.performStepsButton = page.getByRole("button", { name: "Confirm" }); this.agentMessage = CopilotSelectors.assistantMessages(page); this.userMessage = CopilotSelectors.userMessages(page); } async openChat() { await expect(this.agentGreeting).toBeVisible(); } async sendMessage(message: string) { await sendAndAwaitResponse(this.page, message); } async selectItemsInPlanner() { await expect(this.plan).toBeVisible(); await this.plan.click(); } async getPlannerOnClick(name: string | RegExp) { return this.page.getByRole("button", { name }); } async uncheckItem(identifier: number | string): Promise { const plannerContainer = this.page.getByTestId("select-steps"); const items = plannerContainer.getByTestId("step-item"); let item; if (typeof identifier === "number") { item = items.nth(identifier); } else { item = items .filter({ has: this.page .getByTestId("step-text") .filter({ hasText: identifier }), }) .first(); } const stepTextElement = item.getByTestId("step-text"); const text = await stepTextElement.innerText(); await item.click(); return text; } async isStepItemUnchecked(target: number | string): Promise { const plannerContainer = this.page.getByTestId("select-steps"); const items = plannerContainer.getByTestId("step-item"); let item; if (typeof target === "number") { item = items.nth(target); } else { item = items .filter({ has: this.page.getByTestId("step-text").filter({ hasText: target }), }) .first(); } const checkbox = item.locator('input[type="checkbox"]'); return !(await checkbox.isChecked()); } async performSteps() { await this.performStepsButton.click(); await this.performStepsButton.waitFor({ state: "hidden" }); } async performStepsAndAwait() { const countBefore = await this.page .locator('[data-testid="copilot-assistant-message"]') .count(); await this.performStepsButton.click(); await this.performStepsButton.waitFor({ state: "hidden" }); await this.page.waitForFunction( (before) => document.querySelectorAll('[data-testid="copilot-assistant-message"]') .length > before, countBefore, { timeout: 30000 }, ); await this.page.waitForFunction( () => document.querySelector('[data-copilot-running="false"]') !== null, null, { timeout: 60000 }, ); } async assertAgentReplyVisible(expectedText: RegExp) { await expect( this.agentMessage.last().getByText(expectedText), ).toBeVisible(); } async assertUserMessageVisible(message: string) { await expect(this.page.getByText(message)).toBeVisible(); } } ================================================ FILE: apps/dojo/e2e/pages/adkMiddlewarePages/PredictiveStateUpdatesPage.ts ================================================ import { Page, Locator, expect } from "@playwright/test"; import { CopilotSelectors } from "../../utils/copilot-selectors"; import { sendChatMessage, awaitLLMResponseDone, } from "../../utils/copilot-actions"; import { DEFAULT_WELCOME_MESSAGE } from "../../lib/constants"; export class PredictiveStateUpdatesPage { readonly page: Page; readonly chatInput: Locator; readonly sendButton: Locator; readonly agentGreeting: Locator; readonly agentResponsePrompt: Locator; readonly userApprovalModal: Locator; readonly approveButton: Locator; readonly acceptedButton: Locator; readonly confirmedChangesResponse: Locator; readonly rejectedChangesResponse: Locator; readonly agentMessage: Locator; readonly userMessage: Locator; readonly highlights: Locator; constructor(page: Page) { this.page = page; this.agentGreeting = page.getByText(DEFAULT_WELCOME_MESSAGE); this.chatInput = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); this.agentResponsePrompt = page.locator("div.tiptap.ProseMirror"); this.userApprovalModal = page.locator( '[data-testid="confirm-changes-modal"]', ); this.acceptedButton = page.getByText("✓ Accepted"); this.confirmedChangesResponse = page .locator('[data-testid="status-display"]', { hasText: "✓ Accepted" }) .last(); this.rejectedChangesResponse = page .locator('[data-testid="status-display"]', { hasText: "✗ Rejected" }) .last(); this.highlights = page.locator(".tiptap em"); this.agentMessage = CopilotSelectors.assistantMessages(page); this.userMessage = CopilotSelectors.userMessages(page); } async openChat() { await expect(this.agentGreeting).toBeVisible(); } async sendMessage(message: string) { await sendChatMessage(this.page, message); } async getPredictiveResponse() { await expect(this.agentResponsePrompt).toBeVisible(); await this.agentResponsePrompt.click(); } async getButton(page, buttonName) { return page.getByRole("button", { name: buttonName }).click(); } async getStatusLabelOfButton(page, statusText) { return page.getByText(statusText, { exact: true }); } async getUserApproval() { const modal = this.userApprovalModal.last(); const confirmBtn = modal.locator('[data-testid="confirm-button"]'); await expect(confirmBtn).toBeEnabled(); await confirmBtn.click(); await awaitLLMResponseDone(this.page); } async getUserRejection() { const modal = this.userApprovalModal.last(); const rejectBtn = modal.locator('[data-testid="reject-button"]'); await expect(rejectBtn).toBeEnabled(); await rejectBtn.click(); await awaitLLMResponseDone(this.page); } async verifyAgentResponse(dragonName) { const paragraphWithName = await this.page .locator(`div.tiptap >> text=${dragonName}`) .first(); const fullText = await paragraphWithName.textContent(); if (!fullText) { return null; } const match = fullText.match(new RegExp(dragonName, "i")); return match ? match[0] : null; } async verifyHighlightedText() { const highlightSelectors = [ ".tiptap em", ".tiptap s", "div.tiptap em", "div.tiptap s", ]; let count = 0; for (const selector of highlightSelectors) { count = await this.page.locator(selector).count(); if (count > 0) { break; } } if (count > 0) { expect(count).toBeGreaterThan(0); } else { const modal = this.page .locator('[data-testid="confirm-changes-modal"]') .last(); await expect(modal).toBeVisible(); } } } ================================================ FILE: apps/dojo/e2e/pages/agnoPages/HumanInLoopPage.ts ================================================ import { Page, Locator, expect } from "@playwright/test"; import { CopilotSelectors } from "../../utils/copilot-selectors"; import { sendAndAwaitResponse } from "../../utils/copilot-actions"; import { DEFAULT_WELCOME_MESSAGE } from "../../lib/constants"; export class HumanInLoopPage { readonly page: Page; readonly planTaskButton: Locator; readonly chatInput: Locator; readonly sendButton: Locator; readonly agentGreeting: Locator; readonly plan: Locator; readonly performStepsButton: Locator; readonly agentMessage: Locator; readonly userMessage: Locator; constructor(page: Page) { this.page = page; this.planTaskButton = page.getByRole("button", { name: "Human in the loop Plan a task", }); this.agentGreeting = page.getByText(DEFAULT_WELCOME_MESSAGE); this.chatInput = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); this.plan = page.getByTestId("select-steps"); this.performStepsButton = page.getByRole("button", { name: "Confirm" }); this.agentMessage = CopilotSelectors.assistantMessages(page); this.userMessage = CopilotSelectors.userMessages(page); } async openChat() { await expect(this.agentGreeting).toBeVisible(); } async sendMessage(message: string) { await sendAndAwaitResponse(this.page, message); } async selectItemsInPlanner() { await expect(this.plan).toBeVisible(); await this.plan.click(); } async getPlannerOnClick(name: string | RegExp) { return this.page.getByRole("button", { name }); } async uncheckItem(identifier: number | string): Promise { const plannerContainer = this.page.getByTestId("select-steps"); const items = plannerContainer.getByTestId("step-item"); let item; if (typeof identifier === "number") { item = items.nth(identifier); } else { item = items .filter({ has: this.page .getByTestId("step-text") .filter({ hasText: identifier }), }) .first(); } const stepTextElement = item.getByTestId("step-text"); const text = await stepTextElement.innerText(); await item.click(); return text; } async isStepItemUnchecked(target: number | string): Promise { const plannerContainer = this.page.getByTestId("select-steps"); const items = plannerContainer.getByTestId("step-item"); let item; if (typeof target === "number") { item = items.nth(target); } else { item = items .filter({ has: this.page.getByTestId("step-text").filter({ hasText: target }), }) .first(); } const checkbox = item.locator('input[type="checkbox"]'); return !(await checkbox.isChecked()); } async performSteps() { await this.performStepsButton.click(); await this.performStepsButton.waitFor({ state: "hidden" }); } async performStepsAndAwait() { const countBefore = await this.page .locator('[data-testid="copilot-assistant-message"]') .count(); await this.performStepsButton.click(); await this.performStepsButton.waitFor({ state: "hidden" }); await this.page.waitForFunction( (before) => document.querySelectorAll('[data-testid="copilot-assistant-message"]') .length > before, countBefore, { timeout: 30000 }, ); await this.page.waitForFunction( () => document.querySelector('[data-copilot-running="false"]') !== null, null, { timeout: 60000 }, ); } async assertAgentReplyVisible(expectedText: RegExp) { await expect( this.agentMessage.last().getByText(expectedText), ).toBeVisible(); } async assertUserMessageVisible(message: string) { await expect(this.page.getByText(message)).toBeVisible(); } } ================================================ FILE: apps/dojo/e2e/pages/awsStrandsPages/AgenticUIGenPage.ts ================================================ import { Page, Locator, expect } from '@playwright/test'; import { CopilotSelectors } from '../../utils/copilot-selectors'; import { sendChatMessage, awaitLLMResponseDone } from '../../utils/copilot-actions'; export class AgenticGenUIPage { readonly page: Page; readonly chatInput: Locator; readonly planTaskButton: Locator; readonly agentMessage: Locator; readonly userMessage: Locator; readonly agentGreeting: Locator; readonly agentPlannerContainer: Locator; readonly sendButton: Locator; constructor(page: Page) { this.page = page; this.planTaskButton = page.getByRole('button', { name: 'Agentic Generative UI' }); this.chatInput = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); this.agentMessage = CopilotSelectors.assistantMessages(page); this.userMessage = CopilotSelectors.userMessages(page); this.agentGreeting = page.getByText('This agent demonstrates'); this.agentPlannerContainer = page.getByTestId('task-progress'); } async plan() { const stepItems = this.agentPlannerContainer.getByTestId('task-step-text'); const count = await stepItems.count(); expect(count).toBeGreaterThan(0); for (let i = 0; i < count; i++) { const stepText = await stepItems.nth(i).textContent(); console.log(`Step ${i + 1}: ${stepText?.trim()}`); await expect(stepItems.nth(i)).toBeVisible(); } } async openChat() { await expect(this.planTaskButton).toBeVisible(); } async sendMessage(message: string) { await sendChatMessage(this.page, message); await awaitLLMResponseDone(this.page); } getPlannerButton(name: string | RegExp) { return this.page.getByRole('button', { name }); } async assertAgentReplyVisible(expectedText: RegExp) { await expect(this.agentMessage.last().getByText(expectedText)).toBeVisible(); } async getUserText(textOrRegex) { return await this.page.getByText(textOrRegex).isVisible(); } async assertUserMessageVisible(message: string) { await expect(this.userMessage.getByText(message)).toBeVisible(); } } ================================================ FILE: apps/dojo/e2e/pages/awsStrandsPages/HumanInLoopPage.ts ================================================ import { Page, Locator, expect } from "@playwright/test"; import { CopilotSelectors } from "../../utils/copilot-selectors"; import { sendAndAwaitResponse } from "../../utils/copilot-actions"; import { DEFAULT_WELCOME_MESSAGE } from "../../lib/constants"; export class HumanInLoopPage { readonly page: Page; readonly planTaskButton: Locator; readonly chatInput: Locator; readonly sendButton: Locator; readonly agentGreeting: Locator; readonly plan: Locator; readonly performStepsButton: Locator; readonly agentMessage: Locator; readonly userMessage: Locator; constructor(page: Page) { this.page = page; this.planTaskButton = page.getByRole("button", { name: "Human in the loop Plan a task", }); this.agentGreeting = page.getByText(DEFAULT_WELCOME_MESSAGE); this.chatInput = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); this.plan = page.getByTestId("select-steps"); this.performStepsButton = page.getByRole("button", { name: "Confirm" }); this.agentMessage = CopilotSelectors.assistantMessages(page); this.userMessage = CopilotSelectors.userMessages(page); } async openChat() { await expect(this.agentGreeting).toBeVisible(); } async sendMessage(message: string) { await sendAndAwaitResponse(this.page, message); } async selectItemsInPlanner() { await expect(this.plan).toBeVisible(); await this.plan.click(); } async getPlannerOnClick(name: string | RegExp) { return this.page.getByRole("button", { name }); } async uncheckItem(identifier: number | string): Promise { const plannerContainer = this.page.getByTestId("select-steps"); const items = plannerContainer.getByTestId("step-item"); let item; if (typeof identifier === "number") { item = items.nth(identifier); } else { item = items .filter({ has: this.page .getByTestId("step-text") .filter({ hasText: identifier }), }) .first(); } const stepTextElement = item.getByTestId("step-text"); const text = await stepTextElement.innerText(); await item.click(); return text; } async isStepItemUnchecked(target: number | string): Promise { const plannerContainer = this.page.getByTestId("select-steps"); const items = plannerContainer.getByTestId("step-item"); let item; if (typeof target === "number") { item = items.nth(target); } else { item = items .filter({ has: this.page.getByTestId("step-text").filter({ hasText: target }), }) .first(); } const checkbox = item.locator('input[type="checkbox"]'); return !(await checkbox.isChecked()); } async performSteps() { await this.performStepsButton.click(); await this.performStepsButton.waitFor({ state: "hidden" }); } async performStepsAndAwait() { const countBefore = await this.page .locator('[data-testid="copilot-assistant-message"]') .count(); await this.performStepsButton.click(); await this.performStepsButton.waitFor({ state: "hidden" }); await this.page.waitForFunction( (before) => document.querySelectorAll('[data-testid="copilot-assistant-message"]') .length > before, countBefore, { timeout: 30000 }, ); await this.page.waitForFunction( () => document.querySelector('[data-copilot-running="false"]') !== null, null, { timeout: 60000 }, ); } async assertAgentReplyVisible(expectedText: RegExp) { await expect( this.agentMessage.last().getByText(expectedText), ).toBeVisible(); } async assertUserMessageVisible(message: string) { await expect(this.page.getByText(message)).toBeVisible(); } } ================================================ FILE: apps/dojo/e2e/pages/crewAIPages/AgenticUIGenPage.ts ================================================ import { Page, Locator, expect } from '@playwright/test'; import { CopilotSelectors } from '../../utils/copilot-selectors'; import { sendChatMessage, awaitLLMResponseDone } from '../../utils/copilot-actions'; export class AgenticGenUIPage { readonly page: Page; readonly chatInput: Locator; readonly planTaskButton: Locator; readonly agentMessage: Locator; readonly userMessage: Locator; readonly agentGreeting: Locator; readonly agentPlannerContainer: Locator; readonly sendButton: Locator; constructor(page: Page) { this.page = page; this.planTaskButton = page.getByRole('button', { name: 'Agentic Generative UI' }); this.chatInput = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); this.agentMessage = CopilotSelectors.assistantMessages(page); this.userMessage = CopilotSelectors.userMessages(page); this.agentGreeting = page.getByText('This agent demonstrates'); this.agentPlannerContainer = page.getByTestId('task-progress'); } async plan() { const stepItems = this.agentPlannerContainer.getByTestId('task-step-text'); const count = await stepItems.count(); expect(count).toBeGreaterThan(0); for (let i = 0; i < count; i++) { const stepText = await stepItems.nth(i).textContent(); console.log(`Step ${i + 1}: ${stepText?.trim()}`); await expect(stepItems.nth(i)).toBeVisible(); } } async openChat() { await expect(this.planTaskButton).toBeVisible(); } async sendMessage(message: string) { await sendChatMessage(this.page, message); await awaitLLMResponseDone(this.page); } getPlannerButton(name: string | RegExp) { return this.page.getByRole('button', { name }); } async assertAgentReplyVisible(expectedText: RegExp) { await expect(this.agentMessage.last().getByText(expectedText)).toBeVisible(); } async getUserText(textOrRegex) { return await this.page.getByText(textOrRegex).isVisible(); } async assertUserMessageVisible(message: string) { await expect(this.userMessage.getByText(message)).toBeVisible(); } } ================================================ FILE: apps/dojo/e2e/pages/crewAIPages/HumanInLoopPage.ts ================================================ import { Page, Locator, expect } from "@playwright/test"; import { CopilotSelectors } from "../../utils/copilot-selectors"; import { sendAndAwaitResponse } from "../../utils/copilot-actions"; import { DEFAULT_WELCOME_MESSAGE } from "../../lib/constants"; export class HumanInLoopPage { readonly page: Page; readonly planTaskButton: Locator; readonly chatInput: Locator; readonly sendButton: Locator; readonly agentGreeting: Locator; readonly plan: Locator; readonly performStepsButton: Locator; readonly agentMessage: Locator; readonly userMessage: Locator; constructor(page: Page) { this.page = page; this.planTaskButton = page.getByRole("button", { name: "Human in the loop Plan a task", }); this.agentGreeting = page.getByText(DEFAULT_WELCOME_MESSAGE); this.chatInput = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); this.plan = page.getByTestId("select-steps"); this.performStepsButton = page.getByRole("button", { name: "Confirm" }); this.agentMessage = CopilotSelectors.assistantMessages(page); this.userMessage = CopilotSelectors.userMessages(page); } async openChat() { await expect(this.agentGreeting).toBeVisible(); } async sendMessage(message: string) { await sendAndAwaitResponse(this.page, message); } async selectItemsInPlanner() { await expect(this.plan).toBeVisible(); await this.plan.click(); } async getPlannerOnClick(name: string | RegExp) { return this.page.getByRole("button", { name }); } async uncheckItem(identifier: number | string): Promise { const plannerContainer = this.page.getByTestId("select-steps"); const items = plannerContainer.getByTestId("step-item"); let item; if (typeof identifier === "number") { item = items.nth(identifier); } else { item = items .filter({ has: this.page .getByTestId("step-text") .filter({ hasText: identifier }), }) .first(); } const stepTextElement = item.getByTestId("step-text"); const text = await stepTextElement.innerText(); await item.click(); return text; } async isStepItemUnchecked(target: number | string): Promise { const plannerContainer = this.page.getByTestId("select-steps"); const items = plannerContainer.getByTestId("step-item"); let item; if (typeof target === "number") { item = items.nth(target); } else { item = items .filter({ has: this.page.getByTestId("step-text").filter({ hasText: target }), }) .first(); } const checkbox = item.locator('input[type="checkbox"]'); return !(await checkbox.isChecked()); } async performSteps() { await this.performStepsButton.click(); await this.performStepsButton.waitFor({ state: "hidden" }); } async performStepsAndAwait() { const countBefore = await this.page .locator('[data-testid="copilot-assistant-message"]') .count(); await this.performStepsButton.click(); await this.performStepsButton.waitFor({ state: "hidden" }); await this.page.waitForFunction( (before) => document.querySelectorAll('[data-testid="copilot-assistant-message"]') .length > before, countBefore, { timeout: 30000 }, ); await this.page.waitForFunction( () => document.querySelector('[data-copilot-running="false"]') !== null, null, { timeout: 60000 }, ); } async assertAgentReplyVisible(expectedText: RegExp) { await expect( this.agentMessage.last().getByText(expectedText), ).toBeVisible(); } async assertUserMessageVisible(message: string) { await expect(this.page.getByText(message)).toBeVisible(); } } ================================================ FILE: apps/dojo/e2e/pages/crewAIPages/PredictiveStateUpdatesPage.ts ================================================ import { Page, Locator, expect } from '@playwright/test'; import { CopilotSelectors } from '../../utils/copilot-selectors'; import { sendChatMessage, awaitLLMResponseDone } from '../../utils/copilot-actions'; import { DEFAULT_WELCOME_MESSAGE } from '../../lib/constants'; export class PredictiveStateUpdatesPage { readonly page: Page; readonly chatInput: Locator; readonly sendButton: Locator; readonly agentGreeting: Locator; readonly agentResponsePrompt: Locator; readonly userApprovalModal: Locator; readonly approveButton: Locator; readonly acceptedButton: Locator; readonly confirmedChangesResponse: Locator; readonly rejectedChangesResponse: Locator; readonly agentMessage: Locator; readonly userMessage: Locator; readonly highlights: Locator; constructor(page: Page) { this.page = page; this.agentGreeting = page.getByText(DEFAULT_WELCOME_MESSAGE); this.chatInput = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); this.agentResponsePrompt = page.locator('div.tiptap.ProseMirror'); this.userApprovalModal = page.locator('[data-testid="confirm-changes-modal"]').last(); this.acceptedButton = page.getByText('✓ Accepted'); this.confirmedChangesResponse = CopilotSelectors.assistantMessages(page).last(); this.rejectedChangesResponse = CopilotSelectors.assistantMessages(page).last(); this.highlights = page.locator('.tiptap em'); this.agentMessage = CopilotSelectors.assistantMessages(page); this.userMessage = CopilotSelectors.userMessages(page); } async openChat() { await expect(this.agentGreeting).toBeVisible(); } async sendMessage(message: string) { await sendChatMessage(this.page, message); } async getPredictiveResponse() { await expect(this.agentResponsePrompt).toBeVisible(); await this.agentResponsePrompt.click(); } async getButton(page, buttonName) { return page.getByRole('button', { name: buttonName }).click(); } async getStatusLabelOfButton(page, statusText) { return page.getByText(statusText, { exact: true }); } async getUserApproval() { const confirmBtn = this.userApprovalModal.locator('[data-testid="confirm-button"]'); await expect(confirmBtn).toBeEnabled(); await confirmBtn.click(); await awaitLLMResponseDone(this.page); } async getUserRejection() { const rejectBtn = this.userApprovalModal.locator('[data-testid="reject-button"]'); await expect(rejectBtn).toBeEnabled(); await rejectBtn.click(); await awaitLLMResponseDone(this.page); } async verifyAgentResponse(dragonName) { const paragraphWithName = this.page.locator(`div.tiptap >> text=${dragonName}`).first(); await expect(paragraphWithName).toBeVisible(); const fullText = await paragraphWithName.textContent(); if (!fullText) { return null; } const match = fullText.match(new RegExp(dragonName, 'i')); return match ? match[0] : null; } async verifyHighlightedText(){ const highlightSelectors = [ '.tiptap em', '.tiptap s', 'div.tiptap em', 'div.tiptap s' ]; let count = 0; for (const selector of highlightSelectors) { count = await this.page.locator(selector).count(); if (count > 0) { break; } } if (count > 0) { expect(count).toBeGreaterThan(0); } else { const modal = this.page.locator('[data-testid="confirm-changes-modal"]').last(); await expect(modal).toBeVisible(); } } } ================================================ FILE: apps/dojo/e2e/pages/langGraphFastAPIPages/AgenticUIGenPage.ts ================================================ import { Page, Locator, expect } from '@playwright/test'; import { CopilotSelectors } from '../../utils/copilot-selectors'; import { sendChatMessage, awaitLLMResponseDone } from '../../utils/copilot-actions'; export class AgenticGenUIPage { readonly page: Page; readonly chatInput: Locator; readonly planTaskButton: Locator; readonly agentMessage: Locator; readonly userMessage: Locator; readonly agentGreeting: Locator; readonly agentPlannerContainer: Locator; readonly sendButton: Locator; constructor(page: Page) { this.page = page; this.planTaskButton = page.getByRole('button', { name: 'Agentic Generative UI' }); this.chatInput = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); this.agentMessage = CopilotSelectors.assistantMessages(page); this.userMessage = CopilotSelectors.userMessages(page); this.agentGreeting = page.getByText('This agent demonstrates'); this.agentPlannerContainer = page.getByTestId('task-progress'); } async plan() { const stepItems = this.agentPlannerContainer.getByTestId('task-step-text'); const count = await stepItems.count(); expect(count).toBeGreaterThan(0); for (let i = 0; i < count; i++) { const stepText = await stepItems.nth(i).textContent(); console.log(`Step ${i + 1}: ${stepText?.trim()}`); await expect(stepItems.nth(i)).toBeVisible(); } } async openChat() { await expect(this.planTaskButton).toBeVisible(); } async sendMessage(message: string) { await sendChatMessage(this.page, message); await awaitLLMResponseDone(this.page); } getPlannerButton(name: string | RegExp) { return this.page.getByRole('button', { name }); } async assertAgentReplyVisible(expectedText: RegExp) { await expect(this.agentMessage.last().getByText(expectedText)).toBeVisible(); } async getUserText(textOrRegex) { return await this.page.getByText(textOrRegex).isVisible(); } async assertUserMessageVisible(message: string) { await expect(this.userMessage.getByText(message)).toBeVisible(); } } ================================================ FILE: apps/dojo/e2e/pages/langGraphFastAPIPages/HumanInLoopPage.ts ================================================ import { Page, Locator, expect } from "@playwright/test"; import { CopilotSelectors } from "../../utils/copilot-selectors"; import { sendAndAwaitResponse } from "../../utils/copilot-actions"; export class HumanInLoopPage { readonly page: Page; readonly planTaskButton: Locator; readonly chatInput: Locator; readonly sendButton: Locator; readonly agentGreeting: Locator; readonly plan: Locator; readonly performStepsButton: Locator; readonly agentMessage: Locator; readonly userMessage: Locator; constructor(page: Page) { this.page = page; this.planTaskButton = page.getByRole("button", { name: "Human in the loop Plan a task", }); this.agentGreeting = page.getByText( "This agent demonstrates human-in-the-loop", ); this.chatInput = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); this.plan = page.getByTestId("select-steps"); this.performStepsButton = page.getByRole("button", { name: "✨Perform Steps", }); this.agentMessage = CopilotSelectors.assistantMessages(page); this.userMessage = CopilotSelectors.userMessages(page); } async openChat() { await this.planTaskButton.click(); } async sendMessage(message: string) { await sendAndAwaitResponse(this.page, message); } async selectItemsInPlanner() { await expect(this.plan).toBeVisible(); await this.plan.click(); } async getPlannerOnClick(name: string | RegExp) { return this.page.getByRole("button", { name }); } async uncheckItem(identifier: number | string): Promise { const plannerContainer = this.page.getByTestId("select-steps"); const items = plannerContainer.getByTestId("step-item"); let item; if (typeof identifier === "number") { item = items.nth(identifier); } else { item = items .filter({ has: this.page .getByTestId("step-text") .filter({ hasText: identifier }), }) .first(); } const stepTextElement = item.getByTestId("step-text"); const text = await stepTextElement.innerText(); await item.click(); return text; } async isStepItemUnchecked(target: number | string): Promise { const plannerContainer = this.page.getByTestId("select-steps"); const items = plannerContainer.getByTestId("step-item"); let item; if (typeof target === "number") { item = items.nth(target); } else { item = items .filter({ has: this.page.getByTestId("step-text").filter({ hasText: target }), }) .first(); } const checkbox = item.locator('input[type="checkbox"]'); return !(await checkbox.isChecked()); } async performSteps() { await this.performStepsButton.click(); await this.performStepsButton.waitFor({ state: "hidden" }); } async performStepsAndAwait() { const countBefore = await this.page .locator('[data-testid="copilot-assistant-message"]') .count(); await this.performStepsButton.click(); await this.performStepsButton.waitFor({ state: "hidden" }); await this.page.waitForFunction( (before) => document.querySelectorAll('[data-testid="copilot-assistant-message"]') .length > before, countBefore, { timeout: 30000 }, ); await this.page.waitForFunction( () => document.querySelector('[data-copilot-running="false"]') !== null, null, { timeout: 60000 }, ); } async assertAgentReplyVisible(expectedText: RegExp) { await expect( this.agentMessage.last().getByText(expectedText), ).toBeVisible(); } async assertUserMessageVisible(message: string) { await expect(this.page.getByText(message)).toBeVisible(); } } ================================================ FILE: apps/dojo/e2e/pages/langGraphFastAPIPages/PredictiveStateUpdatesPage.ts ================================================ import { Page, Locator, expect } from '@playwright/test'; import { CopilotSelectors } from '../../utils/copilot-selectors'; import { sendChatMessage, awaitLLMResponseDone } from '../../utils/copilot-actions'; import { DEFAULT_WELCOME_MESSAGE } from '../../lib/constants'; export class PredictiveStateUpdatesPage { readonly page: Page; readonly chatInput: Locator; readonly sendButton: Locator; readonly agentGreeting: Locator; readonly agentResponsePrompt: Locator; readonly userApprovalModal: Locator; readonly approveButton: Locator; readonly acceptedButton: Locator; readonly confirmedChangesResponse: Locator; readonly rejectedChangesResponse: Locator; readonly agentMessage: Locator; readonly userMessage: Locator; readonly highlights: Locator; constructor(page: Page) { this.page = page; this.agentGreeting = page.getByText(DEFAULT_WELCOME_MESSAGE); this.chatInput = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); this.agentResponsePrompt = page.locator('div.tiptap.ProseMirror'); this.userApprovalModal = page.locator('[data-testid="confirm-changes-modal"]').last(); this.approveButton = page.getByText('✓ Accepted'); this.acceptedButton = page.getByText('✓ Accepted'); this.confirmedChangesResponse = CopilotSelectors.assistantMessages(page).last(); this.rejectedChangesResponse = CopilotSelectors.assistantMessages(page).last(); this.highlights = page.locator('.tiptap em'); this.agentMessage = CopilotSelectors.assistantMessages(page); this.userMessage = CopilotSelectors.userMessages(page); } async openChat() { await expect(this.agentGreeting).toBeVisible(); } async sendMessage(message: string) { await sendChatMessage(this.page, message); } async getPredictiveResponse() { await expect(this.agentResponsePrompt).toBeVisible(); await this.agentResponsePrompt.click(); } async getButton(page, buttonName) { return page.getByRole('button', { name: buttonName }).click(); } async getStatusLabelOfButton(page, statusText) { return page.getByText(statusText, { exact: true }); } async getUserApproval() { const confirmBtn = this.userApprovalModal.locator('[data-testid="confirm-button"]'); await expect(confirmBtn).toBeEnabled(); await confirmBtn.click(); await awaitLLMResponseDone(this.page); } async getUserRejection() { const rejectBtn = this.userApprovalModal.locator('[data-testid="reject-button"]'); await expect(rejectBtn).toBeEnabled(); await rejectBtn.click(); await awaitLLMResponseDone(this.page); } async verifyAgentResponse(dragonName) { const paragraphWithName = this.page.locator(`div.tiptap >> text=${dragonName}`).first(); await expect(paragraphWithName).toBeVisible(); const fullText = await paragraphWithName.textContent(); if (!fullText) { return null; } const match = fullText.match(new RegExp(dragonName, 'i')); return match ? match[0] : null; } async verifyHighlightedText(){ const highlightSelectors = [ '.tiptap em', '.tiptap s', 'div.tiptap em', 'div.tiptap s' ]; let count = 0; for (const selector of highlightSelectors) { count = await this.page.locator(selector).count(); if (count > 0) { break; } } if (count > 0) { expect(count).toBeGreaterThan(0); } else { const modal = this.page.locator('[data-testid="confirm-changes-modal"]').last(); await expect(modal).toBeVisible(); } } } ================================================ FILE: apps/dojo/e2e/pages/langGraphFastAPIPages/SubgraphsPage.ts ================================================ export { SubgraphsPage } from '../langGraphPages/SubgraphsPage' ================================================ FILE: apps/dojo/e2e/pages/langGraphPages/AgenticUIGenPage.ts ================================================ import { Page, Locator, expect } from '@playwright/test'; import { CopilotSelectors } from '../../utils/copilot-selectors'; import { sendChatMessage, awaitLLMResponseDone } from '../../utils/copilot-actions'; import { DEFAULT_WELCOME_MESSAGE } from '../../lib/constants'; export class AgenticGenUIPage { readonly page: Page; readonly chatInput: Locator; readonly agentMessage: Locator; readonly userMessage: Locator; readonly agentGreeting: Locator; readonly agentPlannerContainer: Locator; readonly sendButton: Locator; constructor(page: Page) { this.page = page; this.chatInput = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); this.agentMessage = CopilotSelectors.assistantMessages(page); this.userMessage = CopilotSelectors.userMessages(page); this.agentGreeting = page.getByText(DEFAULT_WELCOME_MESSAGE); this.agentPlannerContainer = page.getByTestId('task-progress'); } async plan() { const stepItems = this.agentPlannerContainer.getByTestId('task-step-text'); const count = await stepItems.count(); expect(count).toBeGreaterThan(0); for (let i = 0; i < count; i++) { const stepText = await stepItems.nth(i).textContent(); console.log(`Step ${i + 1}: ${stepText?.trim()}`); await expect(stepItems.nth(i)).toBeVisible(); } } async openChat() { // V2 CopilotChat renders inline (no toggle button), so just wait for it to be ready await expect(this.agentGreeting).toBeVisible(); } async sendMessage(message: string) { await sendChatMessage(this.page, message); await awaitLLMResponseDone(this.page); } getPlannerButton(name: string | RegExp) { return this.page.getByRole('button', { name }); } async assertAgentReplyVisible(expectedText: RegExp) { await expect(this.agentMessage.last().getByText(expectedText)).toBeVisible(); } async getUserText(textOrRegex) { return await this.page.getByText(textOrRegex).isVisible(); } async assertUserMessageVisible(message: string) { await expect(this.userMessage.getByText(message)).toBeVisible(); } } ================================================ FILE: apps/dojo/e2e/pages/langGraphPages/HumanInLoopPage.ts ================================================ import { Page, Locator, expect } from "@playwright/test"; import { CopilotSelectors } from "../../utils/copilot-selectors"; import { sendAndAwaitResponse } from "../../utils/copilot-actions"; import { DEFAULT_WELCOME_MESSAGE } from "../../lib/constants"; export class HumanInLoopPage { readonly page: Page; readonly chatInput: Locator; readonly sendButton: Locator; readonly agentGreeting: Locator; readonly plan: Locator; readonly performStepsButton: Locator; readonly agentMessage: Locator; readonly userMessage: Locator; constructor(page: Page) { this.page = page; this.chatInput = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); // V2 CopilotChat renders inline with this welcome text this.agentGreeting = page.getByText(DEFAULT_WELCOME_MESSAGE); this.plan = page.getByTestId("select-steps"); this.performStepsButton = page.getByRole("button", { name: /Perform Steps/, }); this.agentMessage = CopilotSelectors.assistantMessages(page); this.userMessage = CopilotSelectors.userMessages(page); } async openChat() { // V2 CopilotChat renders inline (no toggle button), just wait for it to be ready await expect(this.agentGreeting).toBeVisible(); } async sendMessage(message: string) { await sendAndAwaitResponse(this.page, message); } async selectItemsInPlanner() { await expect(this.plan).toBeVisible(); await this.plan.click(); } async getPlannerOnClick(name: string | RegExp) { return this.page.getByRole("button", { name }); } async uncheckItem(identifier: number | string): Promise { const plannerContainer = this.page.getByTestId("select-steps"); const items = plannerContainer.getByTestId("step-item"); let item; if (typeof identifier === "number") { item = items.nth(identifier); } else { item = items .filter({ has: this.page .getByTestId("step-text") .filter({ hasText: identifier }), }) .first(); } const stepTextElement = item.getByTestId("step-text"); const text = await stepTextElement.innerText(); await item.click(); return text; } async isStepItemUnchecked(target: number | string): Promise { const plannerContainer = this.page.getByTestId("select-steps"); const items = plannerContainer.getByTestId("step-item"); let item; if (typeof target === "number") { item = items.nth(target); } else { item = items .filter({ has: this.page.getByTestId("step-text").filter({ hasText: target }), }) .first(); } const checkbox = item.locator('input[type="checkbox"]'); return !(await checkbox.isChecked()); } async performSteps() { await this.performStepsButton.click(); await this.performStepsButton.waitFor({ state: "hidden" }); } async performStepsAndAwait() { const countBefore = await this.page .locator('[data-testid="copilot-assistant-message"]') .count(); await this.performStepsButton.click(); await this.performStepsButton.waitFor({ state: "hidden" }); await this.page.waitForFunction( (before) => document.querySelectorAll('[data-testid="copilot-assistant-message"]') .length > before, countBefore, { timeout: 30000 }, ); await this.page.waitForFunction( () => document.querySelector('[data-copilot-running="false"]') !== null, null, { timeout: 60000 }, ); } async assertAgentReplyVisible(expectedText: RegExp) { await expect( this.agentMessage.last().getByText(expectedText), ).toBeVisible(); } async assertUserMessageVisible(message: string) { await expect(this.page.getByText(message)).toBeVisible(); } } ================================================ FILE: apps/dojo/e2e/pages/langGraphPages/PredictiveStateUpdatesPage.ts ================================================ import { Page, Locator, expect } from '@playwright/test'; import { CopilotSelectors } from '../../utils/copilot-selectors'; import { sendChatMessage, awaitLLMResponseDone } from '../../utils/copilot-actions'; import { DEFAULT_WELCOME_MESSAGE } from '../../lib/constants'; export class PredictiveStateUpdatesPage { readonly page: Page; readonly chatInput: Locator; readonly sendButton: Locator; readonly agentGreeting: Locator; readonly agentResponsePrompt: Locator; readonly userApprovalModal: Locator; readonly approveButton: Locator; readonly acceptedButton: Locator; readonly confirmedChangesResponse: Locator; readonly rejectedChangesResponse: Locator; readonly agentMessage: Locator; readonly userMessage: Locator; readonly highlights: Locator; constructor(page: Page) { this.page = page; this.agentGreeting = page.getByText(DEFAULT_WELCOME_MESSAGE); this.chatInput = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); this.agentResponsePrompt = page.locator('div.tiptap.ProseMirror'); this.userApprovalModal = page.locator('[data-testid="confirm-changes-modal"]').last(); this.approveButton = page.getByText('✓ Accepted'); this.acceptedButton = page.getByText('✓ Accepted'); this.confirmedChangesResponse = CopilotSelectors.assistantMessages(page).last(); this.rejectedChangesResponse = CopilotSelectors.assistantMessages(page).last(); this.highlights = page.locator('.tiptap em'); this.agentMessage = CopilotSelectors.assistantMessages(page); this.userMessage = CopilotSelectors.userMessages(page); } async openChat() { await expect(this.agentGreeting).toBeVisible(); } async sendMessage(message: string) { await sendChatMessage(this.page, message); } async getPredictiveResponse() { await expect(this.agentResponsePrompt).toBeVisible(); await this.agentResponsePrompt.click(); } async getButton(page, buttonName) { return page.getByRole('button', { name: buttonName }).click(); } async getStatusLabelOfButton(page, statusText) { return page.getByText(statusText, { exact: true }); } async getUserApproval() { const confirmBtn = this.userApprovalModal.locator('[data-testid="confirm-button"]'); await expect(confirmBtn).toBeEnabled(); await confirmBtn.click(); await awaitLLMResponseDone(this.page); } async getUserRejection() { const rejectBtn = this.userApprovalModal.locator('[data-testid="reject-button"]'); await expect(rejectBtn).toBeEnabled(); await rejectBtn.click(); await awaitLLMResponseDone(this.page); } async verifyAgentResponse(dragonName) { const paragraphWithName = this.page.locator(`div.tiptap >> text=${dragonName}`).first(); await expect(paragraphWithName).toBeVisible(); const fullText = await paragraphWithName.textContent(); if (!fullText) { return null; } const match = fullText.match(new RegExp(dragonName, 'i')); return match ? match[0] : null; } async verifyHighlightedText(){ const highlightSelectors = [ '.tiptap em', '.tiptap s', 'div.tiptap em', 'div.tiptap s' ]; let count = 0; for (const selector of highlightSelectors) { count = await this.page.locator(selector).count(); if (count > 0) { break; } } if (count > 0) { expect(count).toBeGreaterThan(0); } else { const modal = this.page.locator('[data-testid="confirm-changes-modal"]').last(); await expect(modal).toBeVisible(); } } } ================================================ FILE: apps/dojo/e2e/pages/langGraphPages/SubgraphsPage.ts ================================================ import { Page, Locator, expect } from '@playwright/test'; import { CopilotSelectors } from '../../utils/copilot-selectors'; import { sendChatMessage, awaitLLMResponseDone } from '../../utils/copilot-actions'; import { DEFAULT_WELCOME_MESSAGE } from '../../lib/constants'; export class SubgraphsPage { readonly page: Page; readonly chatInput: Locator; readonly sendButton: Locator; readonly agentGreeting: Locator; readonly agentMessage: Locator; readonly userMessage: Locator; // Flight-related elements readonly flightOptions: Locator; readonly klmFlightOption: Locator; readonly unitedFlightOption: Locator; readonly flightSelectionInterface: Locator; // Hotel-related elements readonly hotelOptions: Locator; readonly hotelZephyrOption: Locator; readonly ritzCarltonOption: Locator; readonly hotelZoeOption: Locator; readonly hotelSelectionInterface: Locator; // Itinerary and state elements readonly itineraryDisplay: Locator; readonly selectedFlight: Locator; readonly selectedHotel: Locator; readonly experienceRecommendations: Locator; // Subgraph activity indicators readonly activeAgent: Locator; readonly supervisorIndicator: Locator; readonly flightsAgentIndicator: Locator; readonly hotelsAgentIndicator: Locator; readonly experiencesAgentIndicator: Locator; constructor(page: Page) { this.page = page; this.agentGreeting = page.getByText(DEFAULT_WELCOME_MESSAGE); this.chatInput = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); this.agentMessage = CopilotSelectors.assistantMessages(page); this.userMessage = CopilotSelectors.userMessages(page); // Flight selection elements this.flightOptions = page.locator('[data-testid*="flight"], .flight-option'); this.klmFlightOption = page.getByText(/KLM.*\$650.*11h 30m/); this.unitedFlightOption = page.getByText(/United.*\$720.*12h 15m/); this.flightSelectionInterface = page.locator('[data-testid*="flight-select"], .flight-selection'); // Hotel selection elements this.hotelOptions = page.locator('[data-testid*="hotel"], .hotel-option'); this.hotelZephyrOption = page.getByText(/Hotel Zephyr.*Fisherman\'s Wharf.*\$280/); this.ritzCarltonOption = page.getByText(/Ritz-Carlton.*Nob Hill.*\$550/); this.hotelZoeOption = page.getByText(/Hotel Zoe.*Union Square.*\$320/); this.hotelSelectionInterface = page.locator('[data-testid*="hotel-select"], .hotel-selection'); // Itinerary elements this.itineraryDisplay = page.locator('[data-testid*="itinerary"], .itinerary'); this.selectedFlight = page.locator('[data-testid*="selected-flight"], .selected-flight'); this.selectedHotel = page.locator('[data-testid*="selected-hotel"], .selected-hotel'); this.experienceRecommendations = page.locator('[data-testid*="experience"], .experience'); // Agent activity indicators this.activeAgent = page.locator('[data-testid*="active-agent"], .active-agent'); this.supervisorIndicator = page.locator('[data-testid*="supervisor"], .supervisor-active'); this.flightsAgentIndicator = page.locator('[data-testid*="flights-agent"], .flights-agent-active'); this.hotelsAgentIndicator = page.locator('[data-testid*="hotels-agent"], .hotels-agent-active'); this.experiencesAgentIndicator = page.locator('[data-testid*="experiences-agent"], .experiences-agent-active'); } async openChat() { // V2 sidebar opens by default (chatDefaultOpen=true), so just wait for it await expect(this.agentGreeting).toBeVisible(); } async sendMessage(message: string) { await sendChatMessage(this.page, message); await awaitLLMResponseDone(this.page); } async selectFlight(airline: 'KLM' | 'United') { const flightOption = airline === 'KLM' ? this.klmFlightOption : this.unitedFlightOption; // Wait for flight options to be presented await expect(this.flightOptions.first()).toBeVisible(); // Click on the desired flight option await flightOption.click(); } async selectHotel(hotel: 'Zephyr' | 'Ritz-Carlton' | 'Zoe') { let hotelOption: Locator; switch (hotel) { case 'Zephyr': hotelOption = this.hotelZephyrOption; break; case 'Ritz-Carlton': hotelOption = this.ritzCarltonOption; break; case 'Zoe': hotelOption = this.hotelZoeOption; break; } // Wait for hotel options to be presented await expect(this.hotelOptions.first()).toBeVisible(); // Click on the desired hotel option await hotelOption.click(); } async waitForFlightsAgent() { await expect( this.page.getByText(/flight.*options|Amsterdam.*San Francisco|KLM|United/i).first() ).toBeVisible(); } async waitForHotelsAgent() { await expect( this.page.getByText(/hotel.*options|accommodation|Zephyr|Ritz-Carlton|Hotel Zoe/i).first() ).toBeVisible(); } async waitForExperiencesAgent() { await expect( this.page.getByText(/experience|activities|restaurant|Pier 39|Golden Gate|Swan Oyster|Tartine/i).first() ).toBeVisible(); } async verifyStaticFlightData() { await expect(this.page.getByText(/KLM.*\$650.*11h 30m/).first()).toBeVisible(); await expect(this.page.getByText(/United.*\$720.*12h 15m/).first()).toBeVisible(); } async verifyStaticHotelData() { await expect(this.page.getByText(/Hotel Zephyr.*\$280/).first()).toBeVisible(); await expect(this.page.getByText(/Ritz-Carlton.*\$550/).first()).toBeVisible(); await expect(this.page.getByText(/Hotel Zoe.*\$320/).first()).toBeVisible(); } async verifyStaticExperienceData() { await expect(this.page.getByText('No experiences planned yet')).not.toBeVisible({ timeout: 30000 }); await expect(this.page.locator('.activity-name').first()).toBeVisible(); const experienceContent = this.page.locator('.activity-name').first().or( this.page.getByText(/Pier 39|Golden Gate Bridge|Swan Oyster Depot|Tartine Bakery/i).first() ); await expect(experienceContent).toBeVisible(); } async verifyItineraryContainsFlight(airline: 'KLM' | 'United') { await expect(this.page.getByText(new RegExp(airline, 'i'))).toBeVisible(); } async verifyItineraryContainsHotel(hotel: 'Zephyr' | 'Ritz-Carlton' | 'Zoe') { const hotelName = hotel === 'Ritz-Carlton' ? 'Ritz-Carlton' : `Hotel ${hotel}`; await expect(this.page.getByText(new RegExp(hotelName, 'i'))).toBeVisible(); } async assertAgentReplyVisible(expectedText: RegExp) { await expect(this.agentMessage.last().getByText(expectedText)).toBeVisible(); } async assertUserMessageVisible(message: string) { await expect(this.page.getByText(message)).toBeVisible(); } async waitForSupervisorCoordination() { await expect( this.page.getByText(/supervisor|coordinate|specialist|routing/i).first() ).toBeVisible(); } async waitForAgentCompletion() { await expect( this.page.getByText(/complete|finished|planning.*done|itinerary.*ready/i).first() ).toBeVisible(); } } ================================================ FILE: apps/dojo/e2e/pages/langroidPages/AgenticUIGenPage.ts ================================================ import { Page, Locator, expect } from "@playwright/test"; import { CopilotSelectors } from "../../utils/copilot-selectors"; import { sendChatMessage, awaitLLMResponseDone, } from "../../utils/copilot-actions"; export class AgenticGenUIPage { readonly page: Page; readonly chatInput: Locator; readonly planTaskButton: Locator; readonly agentMessage: Locator; readonly userMessage: Locator; readonly agentGreeting: Locator; readonly agentPlannerContainer: Locator; readonly sendButton: Locator; constructor(page: Page) { this.page = page; this.planTaskButton = page.getByRole("button", { name: "Agentic Generative UI", }); this.chatInput = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); this.agentMessage = CopilotSelectors.assistantMessages(page); this.userMessage = CopilotSelectors.userMessages(page); this.agentGreeting = page.getByText("This agent demonstrates"); this.agentPlannerContainer = page.getByTestId("task-progress"); } async plan() { const stepItems = this.agentPlannerContainer.getByTestId("task-step-text"); const count = await stepItems.count(); expect(count).toBeGreaterThan(0); for (let i = 0; i < count; i++) { const stepText = await stepItems.nth(i).textContent(); console.log(`Step ${i + 1}: ${stepText?.trim()}`); await expect(stepItems.nth(i)).toBeVisible(); } } async openChat() { await expect(this.planTaskButton).toBeVisible(); } async sendMessage(message: string) { await sendChatMessage(this.page, message); await awaitLLMResponseDone(this.page); } getPlannerButton(name: string | RegExp) { return this.page.getByRole("button", { name }); } async assertAgentReplyVisible(expectedText: RegExp) { await expect( this.agentMessage.last().getByText(expectedText), ).toBeVisible(); } async getUserText(textOrRegex) { return await this.page.getByText(textOrRegex).isVisible(); } async assertUserMessageVisible(message: string) { await expect(this.userMessage.getByText(message)).toBeVisible(); } } ================================================ FILE: apps/dojo/e2e/pages/llamaIndexPages/AgenticUIGenPage.ts ================================================ import { Page, Locator, expect } from '@playwright/test'; import { CopilotSelectors } from '../../utils/copilot-selectors'; import { sendChatMessage, awaitLLMResponseDone } from '../../utils/copilot-actions'; export class AgenticGenUIPage { readonly page: Page; readonly chatInput: Locator; readonly planTaskButton: Locator; readonly agentMessage: Locator; readonly userMessage: Locator; readonly agentGreeting: Locator; readonly agentPlannerContainer: Locator; readonly sendButton: Locator; constructor(page: Page) { this.page = page; this.planTaskButton = page.getByRole('button', { name: 'Agentic Generative UI' }); this.chatInput = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); this.agentMessage = CopilotSelectors.assistantMessages(page); this.userMessage = CopilotSelectors.userMessages(page); this.agentGreeting = page.getByText('This agent demonstrates'); this.agentPlannerContainer = page.getByTestId('task-progress'); } async plan() { const stepItems = this.agentPlannerContainer.getByTestId('task-step-text'); const count = await stepItems.count(); expect(count).toBeGreaterThan(0); for (let i = 0; i < count; i++) { const stepText = await stepItems.nth(i).textContent(); console.log(`Step ${i + 1}: ${stepText?.trim()}`); await expect(stepItems.nth(i)).toBeVisible(); } } async openChat() { await expect(this.planTaskButton).toBeVisible(); } async sendMessage(message: string) { await sendChatMessage(this.page, message); await awaitLLMResponseDone(this.page); } getPlannerButton(name: string | RegExp) { return this.page.getByRole('button', { name }); } async assertAgentReplyVisible(expectedText: RegExp) { await expect(this.agentMessage.last().getByText(expectedText)).toBeVisible(); } async getUserText(textOrRegex) { return await this.page.getByText(textOrRegex).isVisible(); } async assertUserMessageVisible(message: string) { await expect(this.userMessage.getByText(message)).toBeVisible(); } } ================================================ FILE: apps/dojo/e2e/pages/llamaIndexPages/HumanInLoopPage.ts ================================================ import { Page, Locator, expect } from "@playwright/test"; import { CopilotSelectors } from "../../utils/copilot-selectors"; import { sendAndAwaitResponse } from "../../utils/copilot-actions"; import { DEFAULT_WELCOME_MESSAGE } from "../../lib/constants"; export class HumanInLoopPage { readonly page: Page; readonly planTaskButton: Locator; readonly chatInput: Locator; readonly sendButton: Locator; readonly agentGreeting: Locator; readonly plan: Locator; readonly performStepsButton: Locator; readonly agentMessage: Locator; readonly userMessage: Locator; constructor(page: Page) { this.page = page; this.planTaskButton = page.getByRole("button", { name: "Human in the loop Plan a task", }); this.agentGreeting = page.getByText(DEFAULT_WELCOME_MESSAGE); this.chatInput = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); this.plan = page.getByTestId("select-steps"); this.performStepsButton = page.getByRole("button", { name: "Confirm" }); this.agentMessage = CopilotSelectors.assistantMessages(page); this.userMessage = CopilotSelectors.userMessages(page); } async openChat() { await expect(this.agentGreeting).toBeVisible(); } async sendMessage(message: string) { await sendAndAwaitResponse(this.page, message); } async selectItemsInPlanner() { await expect(this.plan).toBeVisible(); await this.plan.click(); } async getPlannerOnClick(name: string | RegExp) { return this.page.getByRole("button", { name }); } async uncheckItem(identifier: number | string): Promise { const plannerContainer = this.page.getByTestId("select-steps"); const items = plannerContainer.getByTestId("step-item"); let item; if (typeof identifier === "number") { item = items.nth(identifier); } else { item = items .filter({ has: this.page .getByTestId("step-text") .filter({ hasText: identifier }), }) .first(); } const stepTextElement = item.getByTestId("step-text"); const text = await stepTextElement.innerText(); await item.click(); return text; } async isStepItemUnchecked(target: number | string): Promise { const plannerContainer = this.page.getByTestId("select-steps"); const items = plannerContainer.getByTestId("step-item"); let item; if (typeof target === "number") { item = items.nth(target); } else { item = items .filter({ has: this.page.getByTestId("step-text").filter({ hasText: target }), }) .first(); } const checkbox = item.locator('input[type="checkbox"]'); return !(await checkbox.isChecked()); } async performSteps() { await this.performStepsButton.click(); await this.performStepsButton.waitFor({ state: "hidden" }); } async performStepsAndAwait() { const countBefore = await this.page .locator('[data-testid="copilot-assistant-message"]') .count(); await this.performStepsButton.click(); await this.performStepsButton.waitFor({ state: "hidden" }); await this.page.waitForFunction( (before) => document.querySelectorAll('[data-testid="copilot-assistant-message"]') .length > before, countBefore, { timeout: 30000 }, ); await this.page.waitForFunction( () => document.querySelector('[data-copilot-running="false"]') !== null, null, { timeout: 60000 }, ); } async assertAgentReplyVisible(expectedText: RegExp) { await expect( this.agentMessage.last().getByText(expectedText), ).toBeVisible(); } async assertUserMessageVisible(message: string) { await expect(this.page.getByText(message)).toBeVisible(); } } ================================================ FILE: apps/dojo/e2e/pages/pydanticAIPages/AgenticUIGenPage.ts ================================================ import { Page, Locator, expect } from '@playwright/test'; import { CopilotSelectors } from '../../utils/copilot-selectors'; import { sendChatMessage, awaitLLMResponseDone } from '../../utils/copilot-actions'; export class AgenticGenUIPage { readonly page: Page; readonly chatInput: Locator; readonly planTaskButton: Locator; readonly agentMessage: Locator; readonly userMessage: Locator; readonly agentGreeting: Locator; readonly agentPlannerContainer: Locator; readonly sendButton: Locator; constructor(page: Page) { this.page = page; this.planTaskButton = page.getByRole('button', { name: 'Agentic Generative UI' }); this.chatInput = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); this.agentMessage = CopilotSelectors.assistantMessages(page); this.userMessage = CopilotSelectors.userMessages(page); this.agentGreeting = page.getByText('This agent demonstrates'); this.agentPlannerContainer = page.getByTestId('task-progress'); } async plan() { const stepItems = this.agentPlannerContainer.getByTestId('task-step-text'); const count = await stepItems.count(); expect(count).toBeGreaterThan(0); for (let i = 0; i < count; i++) { const stepText = await stepItems.nth(i).textContent(); console.log(`Step ${i + 1}: ${stepText?.trim()}`); await expect(stepItems.nth(i)).toBeVisible(); } } async openChat() { await expect(this.planTaskButton).toBeVisible(); } async sendMessage(message: string) { await sendChatMessage(this.page, message); await awaitLLMResponseDone(this.page); } getPlannerButton(name: string | RegExp) { return this.page.getByRole('button', { name }); } async assertAgentReplyVisible(expectedText: RegExp | RegExp[]) { const expectedTexts = Array.isArray(expectedText) ? expectedText : [expectedText]; let lastError: unknown = null; for (const pattern of expectedTexts) { try { const agentMessage = CopilotSelectors.assistantMessages(this.page).filter({ hasText: pattern }); await expect(agentMessage.last()).toBeVisible(); return; // At least one pattern matched, succeed } catch (error) { lastError = error; } } throw lastError; // No pattern matched } async getUserText(textOrRegex) { return await this.page.getByText(textOrRegex).isVisible(); } async assertUserMessageVisible(message: string) { await expect(this.userMessage.getByText(message)).toBeVisible(); } } ================================================ FILE: apps/dojo/e2e/pages/pydanticAIPages/HumanInLoopPage.ts ================================================ import { Page, Locator, expect } from "@playwright/test"; import { CopilotSelectors } from "../../utils/copilot-selectors"; import { sendAndAwaitResponse } from "../../utils/copilot-actions"; import { DEFAULT_WELCOME_MESSAGE } from "../../lib/constants"; export class HumanInLoopPage { readonly page: Page; readonly planTaskButton: Locator; readonly chatInput: Locator; readonly sendButton: Locator; readonly agentGreeting: Locator; readonly plan: Locator; readonly performStepsButton: Locator; readonly agentMessage: Locator; readonly userMessage: Locator; constructor(page: Page) { this.page = page; this.planTaskButton = page.getByRole("button", { name: "Human in the loop Plan a task", }); this.agentGreeting = page.getByText(DEFAULT_WELCOME_MESSAGE); this.chatInput = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); this.plan = page.getByTestId("select-steps"); this.performStepsButton = page.getByRole("button", { name: "Confirm" }); this.agentMessage = CopilotSelectors.assistantMessages(page); this.userMessage = CopilotSelectors.userMessages(page); } async openChat() { await expect(this.agentGreeting).toBeVisible(); } async sendMessage(message: string) { await sendAndAwaitResponse(this.page, message); } async selectItemsInPlanner() { await expect(this.plan).toBeVisible(); await this.plan.click(); } async getPlannerOnClick(name: string | RegExp) { return this.page.getByRole("button", { name }); } async uncheckItem(identifier: number | string): Promise { const plannerContainer = this.page.getByTestId("select-steps"); const items = plannerContainer.getByTestId("step-item"); let item; if (typeof identifier === "number") { item = items.nth(identifier); } else { item = items .filter({ has: this.page .getByTestId("step-text") .filter({ hasText: identifier }), }) .first(); } const stepTextElement = item.getByTestId("step-text"); const text = await stepTextElement.innerText(); await item.click(); return text; } async isStepItemUnchecked(target: number | string): Promise { const plannerContainer = this.page.getByTestId("select-steps"); const items = plannerContainer.getByTestId("step-item"); let item; if (typeof target === "number") { item = items.nth(target); } else { item = items .filter({ has: this.page.getByTestId("step-text").filter({ hasText: target }), }) .first(); } const checkbox = item.locator('input[type="checkbox"]'); return !(await checkbox.isChecked()); } async performSteps() { await this.performStepsButton.click(); await this.performStepsButton.waitFor({ state: "hidden" }); } async performStepsAndAwait() { const countBefore = await this.page .locator('[data-testid="copilot-assistant-message"]') .count(); await this.performStepsButton.click(); await this.performStepsButton.waitFor({ state: "hidden" }); await this.page.waitForFunction( (before) => document.querySelectorAll('[data-testid="copilot-assistant-message"]') .length > before, countBefore, { timeout: 30000 }, ); await this.page.waitForFunction( () => document.querySelector('[data-copilot-running="false"]') !== null, null, { timeout: 60000 }, ); } async assertAgentReplyVisible(expectedText: RegExp) { await expect( this.agentMessage.last().getByText(expectedText), ).toBeVisible(); } async assertUserMessageVisible(message: string) { await expect(this.page.getByText(message)).toBeVisible(); } } ================================================ FILE: apps/dojo/e2e/pages/pydanticAIPages/PredictiveStateUpdatesPage.ts ================================================ import { Page, Locator, expect } from '@playwright/test'; import { CopilotSelectors } from '../../utils/copilot-selectors'; import { sendChatMessage, awaitLLMResponseDone } from '../../utils/copilot-actions'; import { DEFAULT_WELCOME_MESSAGE } from '../../lib/constants'; export class PredictiveStateUpdatesPage { readonly page: Page; readonly chatInput: Locator; readonly sendButton: Locator; readonly agentGreeting: Locator; readonly agentResponsePrompt: Locator; readonly userApprovalModal: Locator; readonly approveButton: Locator; readonly acceptedButton: Locator; readonly confirmedChangesResponse: Locator; readonly rejectedChangesResponse: Locator; readonly agentMessage: Locator; readonly userMessage: Locator; readonly highlights: Locator; constructor(page: Page) { this.page = page; this.agentGreeting = page.getByText(DEFAULT_WELCOME_MESSAGE); this.chatInput = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); this.agentResponsePrompt = page.locator('div.tiptap.ProseMirror'); this.userApprovalModal = page.locator('div.bg-white.rounded.shadow-lg >> text=Confirm Changes'); this.acceptedButton = page.getByText('✓ Accepted'); this.confirmedChangesResponse = CopilotSelectors.assistantMessages(page).first(); this.rejectedChangesResponse = CopilotSelectors.assistantMessages(page).last(); this.highlights = page.locator('.tiptap em'); this.agentMessage = CopilotSelectors.assistantMessages(page); this.userMessage = CopilotSelectors.userMessages(page); } async openChat() { await expect(this.agentGreeting).toBeVisible(); } async sendMessage(message: string) { await sendChatMessage(this.page, message); } async getPredictiveResponse() { await expect(this.agentResponsePrompt).toBeVisible(); await this.agentResponsePrompt.click(); } async getButton(page, buttonName) { return page.getByRole('button', { name: buttonName }).click(); } async getStatusLabelOfButton(page, statusText) { return page.getByText(statusText, { exact: true }); } async getUserApproval() { const modal = this.userApprovalModal.last(); const confirmBtn = modal.getByRole('button', { name: 'Confirm' }); await expect(confirmBtn).toBeEnabled(); await confirmBtn.click(); await awaitLLMResponseDone(this.page); } async getUserRejection() { const modal = this.userApprovalModal.last(); const rejectBtn = modal.getByRole('button', { name: 'Reject' }); await expect(rejectBtn).toBeEnabled(); await rejectBtn.click(); await awaitLLMResponseDone(this.page); } async verifyAgentResponse(dragonName) { const paragraphWithName = await this.page.locator(`div.tiptap >> text=${dragonName}`).first(); const fullText = await paragraphWithName.textContent(); if (!fullText) { return null; } const match = fullText.match(new RegExp(dragonName, 'i')); return match ? match[0] : null; } async verifyHighlightedText(){ const highlightSelectors = [ '.tiptap em', '.tiptap s', 'div.tiptap em', 'div.tiptap s' ]; let count = 0; for (const selector of highlightSelectors) { count = await this.page.locator(selector).count(); if (count > 0) { break; } } if (count > 0) { expect(count).toBeGreaterThan(0); } else { const modal = this.page.locator('div.bg-white.rounded.shadow-lg').last(); await expect(modal).toBeVisible(); } } } ================================================ FILE: apps/dojo/e2e/pages/serverStarterAllFeaturesPages/AgenticUIGenPage.ts ================================================ import { Page, Locator, expect } from '@playwright/test'; import { CopilotSelectors } from '../../utils/copilot-selectors'; import { sendChatMessage, awaitLLMResponseDone } from '../../utils/copilot-actions'; export class AgenticGenUIPage { readonly page: Page; readonly chatInput: Locator; readonly planTaskButton: Locator; readonly agentMessage: Locator; readonly userMessage: Locator; readonly agentGreeting: Locator; readonly agentPlannerContainer: Locator; readonly sendButton: Locator; constructor(page: Page) { this.page = page; this.planTaskButton = page.getByRole('button', { name: 'Agentic Generative UI' }); this.chatInput = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); this.agentMessage = CopilotSelectors.assistantMessages(page); this.userMessage = CopilotSelectors.userMessages(page); this.agentGreeting = page.getByText('This agent demonstrates'); this.agentPlannerContainer = page.getByTestId('task-progress'); } async plan() { const stepItems = this.agentPlannerContainer.getByTestId('task-step-text'); const count = await stepItems.count(); expect(count).toBeGreaterThan(0); for (let i = 0; i < count; i++) { const stepText = await stepItems.nth(i).textContent(); console.log(`Step ${i + 1}: ${stepText?.trim()}`); await expect(stepItems.nth(i)).toBeVisible(); } } async openChat() { await expect(this.planTaskButton).toBeVisible(); } async sendMessage(message: string) { await sendChatMessage(this.page, message); await awaitLLMResponseDone(this.page); } getPlannerButton(name: string | RegExp) { return this.page.getByRole('button', { name }); } async assertAgentReplyVisible(expectedText: RegExp) { await expect(this.agentMessage.last().getByText(expectedText)).toBeVisible(); } async getUserText(textOrRegex) { return await this.page.getByText(textOrRegex).isVisible(); } async assertUserMessageVisible(message: string) { await expect(this.userMessage.getByText(message)).toBeVisible(); } } ================================================ FILE: apps/dojo/e2e/pages/serverStarterAllFeaturesPages/HumanInLoopPage.ts ================================================ import { Page, Locator, expect } from "@playwright/test"; import { CopilotSelectors } from "../../utils/copilot-selectors"; import { sendAndAwaitResponse } from "../../utils/copilot-actions"; import { DEFAULT_WELCOME_MESSAGE } from "../../lib/constants"; export class HumanInLoopPage { readonly page: Page; readonly planTaskButton: Locator; readonly chatInput: Locator; readonly sendButton: Locator; readonly agentGreeting: Locator; readonly plan: Locator; readonly performStepsButton: Locator; readonly agentMessage: Locator; readonly userMessage: Locator; constructor(page: Page) { this.page = page; this.planTaskButton = page.getByRole("button", { name: "Human in the loop Plan a task", }); this.agentGreeting = page.getByText(DEFAULT_WELCOME_MESSAGE); this.chatInput = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); this.plan = page.getByTestId("select-steps"); this.performStepsButton = page.getByRole("button", { name: "Confirm" }); this.agentMessage = CopilotSelectors.assistantMessages(page); this.userMessage = CopilotSelectors.userMessages(page); } async openChat() { await expect(this.agentGreeting).toBeVisible(); } async sendMessage(message: string) { await sendAndAwaitResponse(this.page, message); } async selectItemsInPlanner() { await expect(this.plan).toBeVisible(); await this.plan.click(); } async getPlannerOnClick(name: string | RegExp) { return this.page.getByRole("button", { name }); } async uncheckItem(identifier: number | string): Promise { const plannerContainer = this.page.getByTestId("select-steps"); const items = plannerContainer.getByTestId("step-item"); let item; if (typeof identifier === "number") { item = items.nth(identifier); } else { item = items .filter({ has: this.page .getByTestId("step-text") .filter({ hasText: identifier }), }) .first(); } const stepTextElement = item.getByTestId("step-text"); const text = await stepTextElement.innerText(); await item.click(); return text; } async isStepItemUnchecked(target: number | string): Promise { const plannerContainer = this.page.getByTestId("select-steps"); const items = plannerContainer.getByTestId("step-item"); let item; if (typeof target === "number") { item = items.nth(target); } else { item = items .filter({ has: this.page.getByTestId("step-text").filter({ hasText: target }), }) .first(); } const checkbox = item.locator('input[type="checkbox"]'); return !(await checkbox.isChecked()); } async performSteps() { await this.performStepsButton.click(); await this.performStepsButton.waitFor({ state: "hidden" }); } async performStepsAndAwait() { const countBefore = await this.page .locator('[data-testid="copilot-assistant-message"]') .count(); await this.performStepsButton.click(); await this.performStepsButton.waitFor({ state: "hidden" }); await this.page.waitForFunction( (before) => document.querySelectorAll('[data-testid="copilot-assistant-message"]') .length > before, countBefore, { timeout: 30000 }, ); await this.page.waitForFunction( () => document.querySelector('[data-copilot-running="false"]') !== null, null, { timeout: 60000 }, ); } async assertAgentReplyVisible(expectedText: RegExp) { await expect( this.agentMessage.last().getByText(expectedText), ).toBeVisible(); } async assertUserMessageVisible(message: string) { await expect(this.page.getByText(message)).toBeVisible(); } } ================================================ FILE: apps/dojo/e2e/pages/serverStarterAllFeaturesPages/PredictiveStateUpdatesPage.ts ================================================ import { Page, Locator, expect } from '@playwright/test'; import { CopilotSelectors } from '../../utils/copilot-selectors'; import { sendChatMessage, awaitLLMResponseDone } from '../../utils/copilot-actions'; import { DEFAULT_WELCOME_MESSAGE } from '../../lib/constants'; export class PredictiveStateUpdatesPage { readonly page: Page; readonly chatInput: Locator; readonly sendButton: Locator; readonly agentGreeting: Locator; readonly agentResponsePrompt: Locator; readonly userApprovalModal: Locator; readonly approveButton: Locator; readonly acceptedButton: Locator; readonly confirmedChangesResponse: Locator; readonly rejectedChangesResponse: Locator; readonly agentMessage: Locator; readonly userMessage: Locator; readonly highlights: Locator; constructor(page: Page) { this.page = page; this.agentGreeting = page.getByText(DEFAULT_WELCOME_MESSAGE); this.chatInput = CopilotSelectors.chatTextarea(page); this.sendButton = CopilotSelectors.sendButton(page); this.agentResponsePrompt = page.locator('div.tiptap.ProseMirror'); this.userApprovalModal = page.locator('[data-testid="confirm-changes-modal"]').last(); this.approveButton = page.getByText('✓ Accepted'); this.acceptedButton = page.getByText('✓ Accepted'); this.confirmedChangesResponse = CopilotSelectors.assistantMessages(page).last(); this.rejectedChangesResponse = CopilotSelectors.assistantMessages(page).last(); this.highlights = page.locator('.tiptap em'); this.agentMessage = CopilotSelectors.assistantMessages(page); this.userMessage = CopilotSelectors.userMessages(page); } async openChat() { await expect(this.agentGreeting).toBeVisible(); } async sendMessage(message: string) { await sendChatMessage(this.page, message); } async getPredictiveResponse() { await expect(this.agentResponsePrompt).toBeVisible(); await this.agentResponsePrompt.click(); } async getButton(page, buttonName) { return page.getByRole('button', { name: buttonName }).click(); } async getStatusLabelOfButton(page, statusText) { return page.getByText(statusText, { exact: true }); } async getUserApproval() { const confirmBtn = this.userApprovalModal.locator('[data-testid="confirm-button"]'); await expect(confirmBtn).toBeEnabled(); await confirmBtn.click(); await awaitLLMResponseDone(this.page); } async getUserRejection() { const rejectBtn = this.userApprovalModal.locator('[data-testid="reject-button"]'); await expect(rejectBtn).toBeEnabled(); await rejectBtn.click(); await awaitLLMResponseDone(this.page); } async verifyAgentResponse(dragonName) { const paragraphWithName = this.page.locator(`div.tiptap >> text=${dragonName}`).first(); await expect(paragraphWithName).toBeVisible(); const fullText = await paragraphWithName.textContent(); if (!fullText) { return null; } const match = fullText.match(new RegExp(dragonName, 'i')); return match ? match[0] : null; } async verifyHighlightedText(){ const highlightSelectors = [ '.tiptap em', '.tiptap s', 'div.tiptap em', 'div.tiptap s' ]; let count = 0; for (const selector of highlightSelectors) { count = await this.page.locator(selector).count(); if (count > 0) { break; } } if (count > 0) { expect(count).toBeGreaterThan(0); } else { const modal = this.page.locator('[data-testid="confirm-changes-modal"]').last(); await expect(modal).toBeVisible(); } } async getResponseContent() { const editor = this.page.locator('div.tiptap.ProseMirror'); const count = await editor.count(); if (count > 0) { const content = await editor.last().textContent(); if (content && content.trim().length > 0) { return content.trim(); } } return null; } } ================================================ FILE: apps/dojo/e2e/playwright.config.ts ================================================ import { defineConfig, devices, ReporterDescription } from "@playwright/test"; import { generateSimpleLayout } from "./slack-layout-simple"; function getReporters(): ReporterDescription[] { const videoReporter: ReporterDescription = [ "./reporters/s3-video-reporter.ts", { outputFile: "test-results/video-urls.json", uploadVideos: true, }, ]; const s3Reporter: ReporterDescription = [ "./node_modules/playwright-slack-report/dist/src/SlackReporter.js", { slackWebHookUrl: process.env.SLACK_WEBHOOK_URL, sendResults: "always", // always send results maxNumberOfFailuresToShow: 10, layout: generateSimpleLayout, // Use our simple layout }, ]; const githubReporter: ReporterDescription = ["github"]; const htmlReporter: ReporterDescription = ["html", { open: "never" }]; const cleanReporter: ReporterDescription = ["./clean-reporter.cjs"]; const addVideoAndSlack = process.env.SLACK_WEBHOOK_URL && process.env.AWS_S3_BUCKET_NAME; return [ process.env.CI ? githubReporter : undefined, addVideoAndSlack ? videoReporter : undefined, addVideoAndSlack ? s3Reporter : undefined, htmlReporter, cleanReporter, ].filter(Boolean) as ReporterDescription[]; } function getBaseUrl(): string { if (process.env.BASE_URL) { return new URL(process.env.BASE_URL).toString(); } console.error("BASE_URL is not set"); process.exit(1); } export default defineConfig({ globalSetup: "./test-isolation-setup.ts", globalTeardown: "./test-isolation-teardown.ts", timeout: 60_000, // 2x margin over typical <30s mock-backed test runtime testDir: "./tests", retries: process.env.CI ? 2 : 0, // Page rendering can be flaky in CI; 2 retries gives 3 total attempts // Make this sequential for now to avoid race conditions workers: process.env.CI ? undefined : undefined, fullyParallel: process.env.CI ? true : true, use: { headless: true, viewport: { width: 1280, height: 720 }, // Video recording for failed tests video: { mode: "retain-on-failure", // Only keep videos for failed tests size: { width: 1280, height: 720 }, }, navigationTimeout: 30_000, actionTimeout: 10_000, // Test isolation - ensure clean state between tests testIdAttribute: "data-testid", baseURL: getBaseUrl(), }, expect: { timeout: 30_000, // Mock-backed tests; 30s is generous }, // Test isolation between each test projects: [ { name: "chromium", use: { ...devices["Desktop Chrome"], // Force new context for each test to ensure isolation contextOptions: { // Clear all data between tests storageState: undefined, }, }, }, ], reporter: getReporters(), }); ================================================ FILE: apps/dojo/e2e/pnpm-workspace.yaml ================================================ packages: - '.' ================================================ FILE: apps/dojo/e2e/reporters/s3-video-reporter.ts ================================================ import { FullConfig, FullResult, Reporter, Suite, TestCase, TestResult, TestStep, } from "@playwright/test/reporter"; import { createS3Uploader, VideoToUpload } from "../lib/upload-video"; import { writeFileSync, existsSync } from "fs"; import { dirname } from "path"; import { mkdirSync } from "fs"; interface S3VideoReporterOptions { outputFile?: string; uploadVideos?: boolean; } interface VideoInfo { url: string; testName: string; suiteName?: string; videoPath?: string; // Store the file path for upload timestamp?: number; // For deduplication - keep most recent } // Global variable to store video URLs for other reporters to access export const uploadedVideos: VideoInfo[] = []; export default class S3VideoReporter implements Reporter { private options: S3VideoReporterOptions; private videos: VideoInfo[] = []; // Only final attempt videos constructor(options: S3VideoReporterOptions = {}) { this.options = { outputFile: options.outputFile || "test-results/video-urls.json", uploadVideos: options.uploadVideos !== false, // Default to true ...options, }; console.log( `📹 DEBUG: S3VideoReporter constructor called with options:`, options ); } onBegin(config: FullConfig, suite: Suite) { console.log(`📹 S3 Video Reporter initialized`); console.log(` Upload enabled: ${this.options.uploadVideos}`); console.log(` Output file: ${this.options.outputFile}`); } onTestEnd(test: TestCase, result: TestResult) { // Only process failed tests if (result.status !== "failed" && result.status !== "timedOut") { return; } console.log(`📹 Processing test attempt for: ${test.title}`); // Look for video attachments const videoAttachments = result.attachments.filter( (attachment) => attachment.name === "video" && attachment.path ); if (videoAttachments.length === 0) { console.log(`📹 No video attachments found for final attempt`); return; } console.log( `📹 Found ${videoAttachments.length} video(s) for failed test: ${test.title}` ); // Store video info for later upload videoAttachments.forEach((attachment) => { console.log( `📹 DEBUG: Processing attachment path=${attachment.path}, exists=${ attachment.path ? existsSync(attachment.path) : false }` ); if (attachment.path && existsSync(attachment.path)) { const videoInfo = { url: "", // Will be set after upload testName: test.title, suiteName: test.parent?.title, videoPath: attachment.path, // Store actual file path timestamp: Date.now(), // For deduplication }; this.videos.push(videoInfo); console.log(`📹 DEBUG: Added video info:`, videoInfo); console.log(`📹 DEBUG: Total videos now: ${this.videos.length}`); } else { console.log( `📹 DEBUG: Skipping attachment - path invalid or file doesn't exist` ); } }); } async onEnd(result: FullResult) { console.log(`📹 DEBUG: onEnd called`); console.log(`📹 DEBUG: uploadVideos=${this.options.uploadVideos}`); console.log(`📹 DEBUG: videos.length=${this.videos.length}`); console.log( `📹 DEBUG: videos=`, this.videos.map((v) => ({ testName: v.testName, hasPath: !!v.videoPath, pathExists: v.videoPath ? existsSync(v.videoPath) : false, })) ); if (!this.options.uploadVideos) { console.log("📹 Upload disabled in options"); return; } if (this.videos.length === 0) { console.log("📹 No videos collected"); return; } const uploader = createS3Uploader(); if (!uploader) { console.warn("⚠️ S3 uploader not configured, skipping video upload"); return; } try { // Deduplicate videos - keep only the most recent one for each test const videoMap = new Map(); this.videos.forEach((video) => { const existing = videoMap.get(video.testName); if (!existing || (video.timestamp || 0) > (existing.timestamp || 0)) { videoMap.set(video.testName, video); } }); const deduplicatedVideos = Array.from(videoMap.values()); console.log( `📹 Deduplicated ${this.videos.length} videos down to ${deduplicatedVideos.length} (keeping most recent per test)` ); // Use the deduplicated videos for upload const videosToUpload: VideoToUpload[] = deduplicatedVideos .filter((video) => video.videoPath && existsSync(video.videoPath)) .map((video) => { const s3ObjectPath = uploader.generateS3Path( video.videoPath!, video.testName, video.suiteName ); return { videoPath: video.videoPath!, s3ObjectPath, testName: video.testName, suiteName: video.suiteName, }; }); if (videosToUpload.length === 0) { console.log("📹 No video files found to upload"); return; } console.log( `📹 Preparing to upload ${videosToUpload.length} video(s)...` ); // Upload videos to S3 const uploadResults = await uploader.uploadVideos(videosToUpload); // Update our video info with URLs this.videos = uploadResults; // Store globally for other reporters uploadedVideos.splice(0); uploadedVideos.push(...this.videos); // Write video URLs to file for other processes await this.writeVideoUrls(); console.log( `✅ Successfully uploaded ${this.videos.length} videos to S3` ); } catch (error) { console.error("❌ Failed to upload videos:", error); } } private async writeVideoUrls() { if (!this.options.outputFile) return; const outputDir = dirname(this.options.outputFile); if (!existsSync(outputDir)) { mkdirSync(outputDir, { recursive: true }); } const videoData = { uploadTime: new Date().toISOString(), runId: process.env.GITHUB_RUN_ID || `local-${Date.now()}`, repository: process.env.GITHUB_REPOSITORY || "unknown", videos: this.videos, }; writeFileSync(this.options.outputFile, JSON.stringify(videoData, null, 2)); console.log(`📄 Video URLs written to: ${this.options.outputFile}`); } // Helper methods removed - we now collect videos directly in onTestEnd } /** * Get uploaded video URLs for use in other reporters */ export function getUploadedVideos(): VideoInfo[] { return [...uploadedVideos]; } /** * Get all uploaded videos */ export function getAllVideos(): VideoInfo[] { return [...uploadedVideos]; } ================================================ FILE: apps/dojo/e2e/setup-aws.sh ================================================ #!/bin/bash # AWS S3 Video Upload Setup Script # This script creates the necessary AWS infrastructure for Playwright video uploads set -e # Exit on any error # Configuration BUCKET_NAME="copilotkit-e2e-smoke-test-recordings-$(openssl rand -hex 4)" IAM_USER_NAME="copilotkit-e2e-smoke-test-uploader" POLICY_NAME="CopilotKitE2ESmokeTestVideoUploadPolicy" AWS_REGION="us-east-1" echo "🚀 Setting up AWS infrastructure for Playwright video uploads..." echo "Bucket name: $BUCKET_NAME" echo "IAM user: $IAM_USER_NAME" echo "Region: $AWS_REGION" echo "" # Check if AWS CLI is installed and configured if ! command -v aws &> /dev/null; then echo "❌ AWS CLI is not installed. Please install it first." exit 1 fi # Check if AWS credentials are configured if ! aws sts get-caller-identity &> /dev/null; then echo "❌ AWS credentials not configured. Run 'aws configure' first." exit 1 fi echo "✅ AWS CLI is configured" # Step 1: Create S3 Bucket echo "📦 Creating S3 bucket: $BUCKET_NAME" aws s3api create-bucket \ --bucket "$BUCKET_NAME" \ --region "$AWS_REGION" \ --create-bucket-configuration LocationConstraint="$AWS_REGION" 2>/dev/null || { # Handle us-east-1 special case (no LocationConstraint needed) if [ "$AWS_REGION" = "us-east-1" ]; then aws s3api create-bucket --bucket "$BUCKET_NAME" --region "$AWS_REGION" else echo "❌ Failed to create bucket" exit 1 fi } # Step 2: Configure bucket for public read access echo "🔓 Configuring bucket for public read access..." # Disable block public access aws s3api put-public-access-block \ --bucket "$BUCKET_NAME" \ --public-access-block-configuration \ "BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false" # Apply bucket policy for public read access aws s3api put-bucket-policy --bucket "$BUCKET_NAME" --policy "{ \"Version\": \"2012-10-17\", \"Statement\": [ { \"Sid\": \"PublicReadGetObject\", \"Effect\": \"Allow\", \"Principal\": \"*\", \"Action\": \"s3:GetObject\", \"Resource\": \"arn:aws:s3:::$BUCKET_NAME/*\" } ] }" # Step 3: Set up lifecycle policy for automatic cleanup (30 days) echo "🗂️ Setting up lifecycle policy for automatic cleanup..." aws s3api put-bucket-lifecycle-configuration \ --bucket "$BUCKET_NAME" \ --lifecycle-configuration '{ "Rules": [ { "ID": "DeleteOldVideos", "Status": "Enabled", "Filter": { "Prefix": "github-runs/" }, "Expiration": { "Days": 30 } } ] }' # Step 4: Create IAM policy for S3 upload permissions echo "👤 Creating IAM policy..." POLICY_ARN=$(aws iam create-policy \ --policy-name "$POLICY_NAME" \ --policy-document '{ "Version": "2012-10-17", "Statement": [ { "Sid": "S3VideoUploadPermissions", "Effect": "Allow", "Action": [ "s3:PutObject", "s3:PutObjectAcl", "s3:GetObject" ], "Resource": "arn:aws:s3:::'"$BUCKET_NAME"'/*" }, { "Sid": "S3ListBucketPermission", "Effect": "Allow", "Action": "s3:ListBucket", "Resource": "arn:aws:s3:::'"$BUCKET_NAME"'" } ] }' \ --query 'Policy.Arn' \ --output text) echo "✅ Created policy: $POLICY_ARN" # Step 5: Create IAM user echo "👤 Creating IAM user: $IAM_USER_NAME" aws iam create-user --user-name "$IAM_USER_NAME" || { echo "⚠️ User might already exist, continuing..." } # Step 6: Attach policy to user echo "🔗 Attaching policy to user..." aws iam attach-user-policy \ --user-name "$IAM_USER_NAME" \ --policy-arn "$POLICY_ARN" # Step 7: Create access keys echo "🔑 Creating access keys..." ACCESS_KEY_OUTPUT=$(aws iam create-access-key --user-name "$IAM_USER_NAME") ACCESS_KEY_ID=$(echo "$ACCESS_KEY_OUTPUT" | jq -r '.AccessKey.AccessKeyId') SECRET_ACCESS_KEY=$(echo "$ACCESS_KEY_OUTPUT" | jq -r '.AccessKey.SecretAccessKey') # No temporary files to clean up # Step 8: Test the setup echo "🧪 Testing S3 upload..." echo "test file" > /tmp/test-upload.txt aws s3 cp /tmp/test-upload.txt "s3://$BUCKET_NAME/test-upload.txt" \ --region "$AWS_REGION" # Test public access TEST_URL="https://$BUCKET_NAME.s3.$AWS_REGION.amazonaws.com/test-upload.txt" echo "🌐 Testing public access..." if curl -s -f "$TEST_URL" > /dev/null; then echo "✅ Public access working!" else echo "⚠️ Public access test failed, but bucket is created" fi # Clean up test file aws s3 rm "s3://$BUCKET_NAME/test-upload.txt" rm -f /tmp/test-upload.txt echo "" echo "🎉 AWS Setup Complete!" echo "====================" echo "" echo "📋 Add these to your GitHub repository secrets:" echo "AWS_ACCESS_KEY_ID: $ACCESS_KEY_ID" echo "AWS_SECRET_ACCESS_KEY: $SECRET_ACCESS_KEY" echo "" echo "📦 S3 Bucket Details:" echo "Bucket Name: $BUCKET_NAME" echo "Region: $AWS_REGION" echo "Public URL Pattern: https://$BUCKET_NAME.s3.$AWS_REGION.amazonaws.com/{path}" echo "" echo "🔄 Next Steps:" echo "1. Add the above secrets to your GitHub repository" echo "2. Update your Playwright configuration with the bucket name" echo "3. Run your tests to start uploading videos!" echo "" echo "💡 Videos will be automatically deleted after 30 days" echo "💡 Upload path format: github-runs/{RUN_ID}/{PROJECT}/{filename}.webm" ================================================ FILE: apps/dojo/e2e/slack-layout-simple.ts ================================================ import { Block, KnownBlock } from "@slack/types"; import { SummaryResults } from "playwright-slack-report/dist/src"; import { readFileSync, existsSync } from "fs"; interface VideoInfo { url: string; testName: string; } function getVideos(): VideoInfo[] { const videoFilePath = "test-results/video-urls.json"; if (!existsSync(videoFilePath)) { return []; } try { const videoData = JSON.parse(readFileSync(videoFilePath, "utf8")); return videoData.videos || []; } catch (error) { console.error("Failed to read videos:", error); return []; } } export function generateSimpleLayout( summaryResults: SummaryResults ): Array { const { passed, failed, skipped, tests } = summaryResults; // Summary const summary = { type: "section", text: { type: "mrkdwn", text: failed === 0 ? `✅ All ${passed} tests passed!` : `✅ ${passed} passed • ❌ ${failed} failed • ⏭ ${skipped} skipped`, }, }; if (failed === 0) { return [summary]; } // Get videos const videos = getVideos(); const videoMap = new Map(videos.map((v) => [v.testName, v.url])); // List failed tests const failedTests = tests.filter( (test) => test.status === "failed" || test.status === "timedOut" ); const failureLines = failedTests.map((test) => { const videoUrl = videoMap.get(test.name); const videoLink = videoUrl ? ` • <${videoUrl}|📹 Video>` : ""; return `• *${test.name}*${videoLink}`; }); const failures = { type: "section", text: { type: "mrkdwn", text: `*Failed Tests:*\n${failureLines.join("\n")}`, }, }; return [summary, failures]; } export default generateSimpleLayout; ================================================ FILE: apps/dojo/e2e/slack-layout.ts ================================================ import { Block, KnownBlock } from "@slack/types"; import { SummaryResults } from "playwright-slack-report/dist/src"; import { readFileSync, existsSync } from "fs"; interface VideoInfo { url: string; testName: string; suiteName?: string; category?: string; } function getVideosByCategory(): Map { const categoryMap = new Map(); // Read from the JSON file that S3 reporter creates const videoFilePath = "test-results/video-urls.json"; if (!existsSync(videoFilePath)) { console.log("📹 No video URLs file found yet"); return categoryMap; } try { const videoData = JSON.parse(readFileSync(videoFilePath, "utf8")); const videos: VideoInfo[] = videoData.videos || []; for (const video of videos) { const category = video.category || "❓ Other Issues"; if (!categoryMap.has(category)) { categoryMap.set(category, []); } categoryMap.get(category)!.push(video); } console.log(`📹 Loaded ${videos.length} videos from file for Slack`); } catch (error) { console.error("📹 Failed to read video URLs file:", error); } return categoryMap; } function getTestDisplayName(test: any): string { // Create a cleaner test name const suiteName = test.suiteName || test.file?.replace(/\.spec\.ts$/, "").replace(/Tests?/g, ""); const testName = test.name; // Remove redundant words and clean up const cleanSuite = suiteName ?.replace(/Tests?$/i, "") ?.replace(/Page$/i, "") ?.replace(/Spec$/i, "") ?.replace(/([a-z])([A-Z])/g, "$1 $2") // camelCase to spaces ?.trim(); return `${cleanSuite}: ${testName}`; } function categorizeAndCleanError(test: any): { category: string; cleanError: string; action: string; } { const error = test.error?.message || test.errors?.[0]?.message || "Unknown error"; // Debug logging to see what error data we're getting console.log(`🐛 DEBUG: Categorizing test "${test.name}"`); console.log( `🐛 DEBUG: Error object:`, JSON.stringify( { hasError: !!test.error, hasErrors: !!test.errors, errorMessage: test.error?.message, errorsLength: test.errors?.length, firstErrorMessage: test.errors?.[0]?.message, }, null, 2 ) ); // AI Response Timeouts if (error.includes("None of the expected patterns matched")) { const patterns = error.match(/patterns matched[^:]*: ([^`]+)/); return { category: "🤖 AI Response Issues", cleanError: `No AI response - Expected: ${ patterns?.[1] || "AI response" }`, action: "Check API keys and AI service status", }; } // Test timeout (usually AI-related in our suite) if ( error.includes("Test timeout") || test.name?.toLowerCase().includes("human") ) { return { category: "🤖 AI Response Issues", cleanError: "Test timeout waiting for AI response", action: "Check AI service availability and response times", }; } // UI Element Missing if (error.includes("Timed out") && error.includes("toBeVisible")) { const element = error.match(/locator\('([^']+)'\)/); return { category: "🎨 UI Issues", cleanError: `Element not found: ${element?.[1] || "UI element"}`, action: "Check if demo app is loading correctly", }; } // Content Generation Failures if (error.includes("toBeGreaterThan") && error.includes("0")) { return { category: "🎯 Content Generation", cleanError: "Expected AI content not generated (count was 0)", action: "AI generative features not working", }; } // Strict Mode Violations (multiple elements found) if (error.includes("strict mode violation")) { return { category: "🎯 Test Reliability", cleanError: "Multiple matching elements found", action: "Test selectors need to be more specific", }; } // CSS/Style Issues if (error.includes("toHaveCSS")) { return { category: "🎨 Styling Issues", cleanError: "Expected CSS styles not applied", action: "Check if dynamic styling is working", }; } // Default fallback return { category: "❓ Other Issues", cleanError: error.split("\n")[0]?.trim() || error, action: "Check logs for details", }; } export function generateCustomLayout( summaryResults: SummaryResults ): Array { const { passed, failed, skipped, tests } = summaryResults; const summary = { type: "section", text: { type: "mrkdwn", text: failed === 0 ? `✅ All ${passed} tests passed!` : `✅ ${passed} passed • ❌ ${failed} failed • ⏭ ${skipped} skipped`, }, }; // Only show failures if there are any const failures: Array = []; if (failed > 0) { const failedTests = tests.filter( (test) => test.status === "failed" || test.status === "timedOut" ); // Categorize failures const categorizedFailures = new Map< string, Array<{ test: any; cleanError: string; action: string }> >(); failedTests.forEach((test) => { const { category, cleanError, action } = categorizeAndCleanError(test); if (!categorizedFailures.has(category)) { categorizedFailures.set(category, []); } categorizedFailures.get(category)!.push({ test, cleanError, action }); }); // Get video URLs by category const videosByCategory = getVideosByCategory(); // Display failures by category for (const [category, categoryFailures] of categorizedFailures) { const failureLines = categoryFailures.map( ({ test, cleanError, action }) => { const testName = getTestDisplayName(test); // Look for videos for this test - search across ALL categories since // S3 reporter uses different categorization than Slack layout let testVideo: VideoInfo | undefined; for (const [_, videos] of videosByCategory) { testVideo = videos.find( (v) => v.testName === test.name || v.testName.includes(test.name) || test.name.includes(v.testName) ); if (testVideo) break; } const videoLink = testVideo ? `\n 📹 [Watch Video](${testVideo.url})` : ""; return `• **${testName}**\n → ${cleanError}${videoLink}`; } ); const uniqueActions = [...new Set(categoryFailures.map((f) => f.action))]; const actionText = uniqueActions.length === 1 ? `\n🔧 *Action:* ${uniqueActions[0]}` : `\n🔧 *Actions:* ${uniqueActions.join(", ")}`; failures.push({ type: "section", text: { type: "mrkdwn", text: `*${category}* (${categoryFailures.length} failure${ categoryFailures.length > 1 ? "s" : "" })\n${failureLines.join("\n\n")}${actionText}`, }, }); } // Add overall action summary if there are AI issues const hasAIIssues = categorizedFailures.has("🤖 AI Response Issues") || categorizedFailures.has("🎯 Content Generation"); if (hasAIIssues) { failures.push({ type: "context", elements: [ { type: "mrkdwn", text: "💡 *Most failures are AI-related.* Check API keys, service status, and rate limits.", }, ], }); } } return [summary, ...failures]; } export default generateCustomLayout; ================================================ FILE: apps/dojo/e2e/test-isolation-helper.ts ================================================ import { test as base, Page } from "@playwright/test"; import { awaitLLMResponseDone } from "./utils/copilot-actions"; /** * Dump the current state of assistant messages on the page. * Called automatically on test failure so CI logs show what the LLM * actually produced (or didn't produce) instead of just "Element not found". */ async function dumpPageAIState(page: Page) { try { const state = await page.evaluate(() => { // Use data-testid selectors (work with both V1 and V2 CopilotChat) const assistantMsgs = Array.from( document.querySelectorAll('[data-testid="copilot-assistant-message"]'), ); const userMsgs = Array.from( document.querySelectorAll('[data-testid="copilot-user-message"]'), ); const chatContainer = document.querySelector( '[data-testid="copilot-chat"]', ); const isRunning = chatContainer?.getAttribute("data-copilot-running"); // Check for HITL confirm modals const confirmModals = Array.from( document.querySelectorAll("div.bg-white.rounded.shadow-lg"), ); const confirmButtons = Array.from( document.querySelectorAll("button"), ).filter((b) => /confirm|reject|accept/i.test(b.textContent || "")); // Check for tiptap editor content const tiptapEditor = document.querySelector("div.tiptap.ProseMirror"); return { assistantMessages: assistantMsgs.map((el, i) => ({ index: i, text: el.textContent?.trim().slice(0, 200) || "(empty)", })), userMessages: userMsgs.map((el, i) => ({ index: i, text: el.textContent?.trim().slice(0, 200) || "(empty)", })), url: window.location.href, copilotRunning: isRunning, chatContainerFound: chatContainer !== null, confirmModals: confirmModals.length, confirmModalTexts: confirmModals.map( (m) => m.textContent?.trim().slice(0, 100) || "(empty)", ), confirmButtons: confirmButtons.map((b) => ({ text: b.textContent?.trim(), disabled: b.disabled, })), tiptapContent: tiptapEditor?.textContent?.trim().slice(0, 100) || "(none)", }; }); // Use console.log so clean-reporter surfaces diagnostic prefixes in CI output console.log("\n[AI State Dump] URL:", state.url); console.log( `[AI State Dump] Chat container: ${state.chatContainerFound ? "found" : "NOT FOUND"}, copilot-running: ${state.copilotRunning ?? "N/A"}`, ); console.log( `[AI State Dump] ${state.userMessages.length} user message(s), ${state.assistantMessages.length} assistant message(s)`, ); for (const msg of state.userMessages) { console.log(`[AI State Dump] User[${msg.index}]: ${msg.text}`); } for (const msg of state.assistantMessages) { console.log(`[AI State Dump] Assistant[${msg.index}]: ${msg.text}`); } if (state.assistantMessages.length === 0) { console.log(" [Assistant] (no messages — LLM may not have responded)"); } console.log( `[AI State Dump] Confirm modals: ${state.confirmModals}, buttons: ${JSON.stringify(state.confirmButtons)}`, ); console.log(`[AI State Dump] Tiptap editor: ${state.tiptapContent}`); if (state.confirmModals > 0) { for (const t of state.confirmModalTexts) { console.log(` [Modal] ${t}`); } } } catch { console.log( "[AI State Dump] Could not read page state (page may have navigated away)", ); } } /** * Dump LLMock journal entries on test failure so CI logs show what the mock * server received and returned. */ async function dumpLLMockJournal() { try { const res = await fetch("http://localhost:5555/v1/_requests?limit=20"); if (!res.ok) { console.log( `[LLMock Journal] Non-OK response: ${res.status} ${res.statusText}`, ); return; } const entries = (await res.json()) as Array<{ method: string; path: string; body: { model?: string; messages?: Array<{ role: string; content?: unknown }>; }; response: { status: number; fixture?: { match?: { userMessage?: string }; response?: unknown; } | null; }; }>; console.log(`\n[LLMock Journal] ${entries.length} request(s) recorded:`); for (const [i, entry] of entries.entries()) { const msgs = entry.body?.messages ?? []; const lastUser = [...msgs].reverse().find((m) => m.role === "user"); const lastUserText = typeof lastUser?.content === "string" ? lastUser.content.slice(0, 80) : "(non-string)"; const fixtureName = entry.response?.fixture?.match?.userMessage ?? "(predicate)"; console.log( ` [${i}] ${entry.method} ${entry.path} → ${entry.response?.status} | model=${entry.body?.model ?? "?"} msgs=${msgs.length} lastUser="${lastUserText}" fixture="${fixtureName}"`, ); } } catch { console.log( "[LLMock Journal] Could not fetch journal (server may be down)", ); } } // Extend base test with isolation setup and error monitoring export const test = base.extend<{}, {}>({ page: async ({ page }, use, testInfo) => { // Before each test - ensure clean state await page.context().clearCookies(); await page.context().clearPermissions(); // Monitor for app errors so failed backends surface immediately // instead of manifesting as opaque timeouts. const pageErrors: Error[] = []; const networkErrors: string[] = []; const agentPosts: string[] = []; page.on("pageerror", (error) => { console.error(`[PageError] ${error.message}`); pageErrors.push(error); }); // Log browser console errors (e.g. CopilotKit runtime logging API failures) page.on("console", (msg) => { if (msg.type() === "error") { console.error(`[BrowserConsole] ${msg.text()}`); } }); // Log ALL POST requests to agent backends (helps debug hung SSE streams) page.on("request", (request) => { if ( request.method() === "POST" && /copilotkit|agui|agent/i.test(request.url()) ) { const ts = new Date().toISOString().slice(11, 23); const msg = `[AgentPOST] ${ts} → ${request.url()}`; console.log(msg); agentPosts.push(msg); } }); // Log ALL responses from agent backends (including SSE stream starts) page.on("response", (response) => { if (/copilotkit|agui|agent/i.test(response.url())) { const ts = new Date().toISOString().slice(11, 23); if (response.status() >= 400) { const msg = `${response.status()} ${response.url()}`; console.error(`[NetworkError] ${msg}`); networkErrors.push(msg); } if (response.request().method() === "POST") { console.log( `[AgentResp] ${ts} ← ${response.status()} ${response.url()}`, ); } } }); await use(page); // On failure: dump what the LLM actually did so CI logs are actionable if (testInfo.status !== testInfo.expectedStatus) { await dumpPageAIState(page); await dumpLLMockJournal(); } // After each test - report collected errors if (pageErrors.length > 0) { console.warn( `[Test Cleanup] ${pageErrors.length} page error(s) during test:`, pageErrors.map((e) => e.message), ); } if (networkErrors.length > 0) { console.warn( `[Test Cleanup] ${networkErrors.length} network error(s) during test:`, networkErrors, ); } if ( testInfo.status !== testInfo.expectedStatus && agentPosts.length > 0 ) { console.log( `[Test Cleanup] ${agentPosts.length} agent POST(s) during test:`, ); for (const msg of agentPosts) console.log(` ${msg}`); } await page.context().clearCookies(); }, }); /** * Wait for the AI response to finish (SSE stream complete). * Delegates to awaitLLMResponseDone which uses the data-copilot-running attribute. */ export async function waitForAIResponse(page: Page, timeout: number = 15000) { await awaitLLMResponseDone(page, timeout); } /** * Wait for a specific number of assistant messages to exist with content. * More precise than waitForAIResponse when you know the expected message count. */ export async function waitForAssistantMessage( page: Page, options: { minMessages?: number; timeout?: number; stabilizationMs?: number; } = {}, ) { const { minMessages = 1, timeout = 30_000, stabilizationMs = 500 } = options; await page.waitForFunction( (min: number) => { const messages = document.querySelectorAll( '[data-testid="copilot-assistant-message"]', ); if (messages.length < min) return false; const lastMessage = messages[messages.length - 1]; return (lastMessage?.textContent?.trim().length ?? 0) > 0; }, minMessages, { timeout }, ); await page.waitForTimeout(stabilizationMs); } export { expect } from "@playwright/test"; ================================================ FILE: apps/dojo/e2e/test-isolation-setup.ts ================================================ import { chromium, FullConfig } from "@playwright/test"; import { setupLLMock } from "./llmock-setup"; async function globalSetup(config: FullConfig) { // Start the LLMock server before any tests run await setupLLMock(); console.log("🧹 Setting up test isolation..."); // Launch browser to clear any persistent state const browser = await chromium.launch(); const context = await browser.newContext(); // Clear all storage await context.clearCookies(); await context.clearPermissions(); // Try to clear cached data — requires navigating to a real page first // (about:blank doesn't allow localStorage access) const baseUrl = process.env.BASE_URL; if (baseUrl) { const page = await context.newPage(); try { await page.goto(baseUrl, { timeout: 10_000 }); await page.evaluate(() => { localStorage.clear(); sessionStorage.clear(); if (window.indexedDB) { indexedDB.deleteDatabase("test-db"); } }); } catch { // Page may not be ready yet — individual tests handle their own cleanup } } await browser.close(); console.log("✅ Test isolation setup complete"); } export default globalSetup; ================================================ FILE: apps/dojo/e2e/test-isolation-teardown.ts ================================================ import { teardownLLMock } from "./llmock-setup"; async function globalTeardown() { await teardownLLMock(); } export default globalTeardown; ================================================ FILE: apps/dojo/e2e/tests/a2aMiddlewareTests/a2aChatPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { A2AChatPage } from "../../pages/a2aMiddlewarePages/A2AChatPage"; // The a2a_chat page has a pre-existing rendering issue where the React tree // intermittently fails to hydrate on first load. This test doesn't involve AI // at all (just checks for a static tab bar), but needs in-page navigation // retries to work around the flaky page rendering — Playwright-level retries // use fresh pages which don't help since the issue is per-navigation. test.describe("A2A Chat Feature", () => { test("[A2A Middleware] Tab bar exists", async ({ page }) => { let lastError: unknown; for (let attempt = 0; attempt < 5; attempt++) { try { await page.goto("/a2a/feature/a2a_chat"); const chat = new A2AChatPage(page); await chat.openChat(); await expect(chat.mainChatTab).toBeVisible({ timeout: 15000 }); return; // success } catch (e) { lastError = e; await page.waitForTimeout(3000); } } throw lastError; }); }); ================================================ FILE: apps/dojo/e2e/tests/adkMiddlewareTests/agenticChatPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { AgenticChatPage } from "../../featurePages/AgenticChatPage"; test.describe("Agentic Chat Feature", () => { test("[ADK Middleware] Agentic Chat sends and receives a message", async ({ page, }) => { await page.goto("/adk-middleware/feature/agentic_chat"); const chat = new AgenticChatPage(page); await chat.openChat(); await expect(chat.agentGreeting).toBeVisible(); await chat.sendMessage("Hello, I am duaa."); await chat.assertUserMessageVisible("Hello, I am duaa."); await chat.assertAgentReplyVisible(/Hello duaa/i); }); test("[ADK Middleware] Agentic Chat changes background on message and reset", async ({ page, }) => { await page.goto("/adk-middleware/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(); // 1. Send message to change background to blue 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(); // 2. Change to pink 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("[ADK Middleware] Agentic Chat retains memory of user messages during a conversation", async ({ page, }) => { await page.goto("/adk-middleware/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("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!/); }); }); ================================================ FILE: apps/dojo/e2e/tests/adkMiddlewareTests/backendToolRenderingPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { awaitLLMResponseDone } from "../../utils/copilot-actions"; test("[ADK Middleware] Backend Tool Rendering displays weather cards", async ({ page, }) => { await page.goto("/adk-middleware/feature/backend_tool_rendering"); // Verify suggestion buttons are visible await expect( page.getByRole("button", { name: "Weather in San Francisco" }), ).toBeVisible({ timeout: 5000, }); // Click first suggestion and verify weather card appears await page.getByRole("button", { name: "Weather in San Francisco" }).click(); // Wait for either test ID or fallback to "Current Weather" text const weatherCard = page.getByTestId("weather-card"); const currentWeatherText = page.getByText("Current Weather"); // Try test ID first, fallback to text try { await expect(weatherCard).toBeVisible(); } catch (e) { // Fallback to checking for "Current Weather" text await expect(currentWeatherText.first()).toBeVisible(); } // Verify weather content is present (use flexible selectors) 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); // At least one of these should be true expect(hasHumidity || hasWind || hasCityName).toBeTruthy(); // Click second suggestion await page.getByRole("button", { name: "Weather in New York" }).click(); await awaitLLMResponseDone(page); // Verify at least one weather-related element is still visible const weatherElements = await page .getByText(/Weather|Humidity|Wind|Temperature/i) .count(); expect(weatherElements).toBeGreaterThan(0); }); ================================================ FILE: apps/dojo/e2e/tests/adkMiddlewareTests/humanInTheLoopPage.spec.ts ================================================ import { awaitLLMResponseDone } from "../../utils/copilot-actions"; import { test, expect } from "../../test-isolation-helper"; import { HumanInLoopPage } from "../../pages/adkMiddlewarePages/HumanInLoopPage"; test.describe("Human in the Loop Feature", () => { test("[ADK Middleware] should interact with the chat and perform steps", async ({ page, }) => { const humanInLoop = new HumanInLoopPage(page); await page.goto("/adk-middleware/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("[ADK Middleware] should interact with the chat using predefined prompts and perform steps", async ({ page, }) => { const humanInLoop = new HumanInLoopPage(page); await page.goto("/adk-middleware/feature/human_in_the_loop"); await humanInLoop.openChat(); // Click the predefined "Simple plan" suggestion button const simplePlanButton = page.getByRole("button", { name: "Simple plan" }); await expect(simplePlanButton).toBeVisible(); await simplePlanButton.click(); await awaitLLMResponseDone(page); await expect(humanInLoop.plan).toBeVisible(); // Uncheck the first step by index const uncheckedItem = await humanInLoop.uncheckItem(0); await humanInLoop.performStepsAndAwait(); await humanInLoop.sendMessage( `Does the planner include ${uncheckedItem}? ⚠️ Reply with only words 'Yes' or 'No' (no explanation, no punctuation).`, ); }); }); ================================================ FILE: apps/dojo/e2e/tests/adkMiddlewareTests/predictiveStateUpdatePage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { PredictiveStateUpdatesPage } from "../../pages/adkMiddlewarePages/PredictiveStateUpdatesPage"; test.describe("Predictive State Updates Feature", () => { test("[ADK Middleware] should interact with agent and approve asked changes", async ({ page, }) => { const predictiveStateUpdates = new PredictiveStateUpdatesPage(page); await page.goto("/adk-middleware/feature/predictive_state_updates"); await predictiveStateUpdates.openChat(); await predictiveStateUpdates.sendMessage( "Give me a story for a dragon called Atlantis in document", ); await page.waitForTimeout(2000); await predictiveStateUpdates.getPredictiveResponse(); await predictiveStateUpdates.getUserApproval(); await expect(predictiveStateUpdates.confirmedChangesResponse).toBeVisible(); const dragonName = await predictiveStateUpdates.verifyAgentResponse("Atlantis"); expect(dragonName).not.toBeNull(); // Send update to change the dragon name await predictiveStateUpdates.sendMessage("Change dragon name to Lola"); await page.waitForTimeout(2000); await predictiveStateUpdates.verifyHighlightedText(); await predictiveStateUpdates.getUserApproval(); await expect(predictiveStateUpdates.confirmedChangesResponse).toBeVisible(); const dragonNameNew = await predictiveStateUpdates.verifyAgentResponse("Lola"); expect(dragonNameNew).not.toBe(dragonName); }); test("[ADK Middleware] should interact with agent and reject asked changes", async ({ page, }) => { const predictiveStateUpdates = new PredictiveStateUpdatesPage(page); await page.goto("/adk-middleware/feature/predictive_state_updates"); await predictiveStateUpdates.openChat(); await predictiveStateUpdates.sendMessage( "Give me a story for a dragon called Atlantis in document", ); await page.waitForTimeout(2000); await predictiveStateUpdates.getPredictiveResponse(); await predictiveStateUpdates.getUserApproval(); await expect(predictiveStateUpdates.confirmedChangesResponse).toBeVisible(); const dragonName = await predictiveStateUpdates.verifyAgentResponse("Atlantis"); expect(dragonName).not.toBeNull(); // Send update to change the dragon name await predictiveStateUpdates.sendMessage("Change dragon name to Lola"); await page.waitForTimeout(2000); await predictiveStateUpdates.verifyHighlightedText(); await predictiveStateUpdates.getUserRejection(); await expect(predictiveStateUpdates.rejectedChangesResponse).toBeVisible(); const dragonNameAfterRejection = await predictiveStateUpdates.verifyAgentResponse("Atlantis"); expect(dragonNameAfterRejection).toBe(dragonName); expect(dragonNameAfterRejection).not.toBe("Lola"); }); }); ================================================ FILE: apps/dojo/e2e/tests/adkMiddlewareTests/sharedStatePage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { SharedStatePage } from "../../featurePages/SharedStatePage"; test.describe("Shared State Feature", () => { test("[ADK Middleware] should interact with the chat to get a recipe on prompt", async ({ page, }) => { const sharedStateAgent = new SharedStatePage(page); await page.goto("/adk-middleware/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"', ); await sharedStateAgent.loader(); await sharedStateAgent.awaitIngredientCard("Pasta"); await sharedStateAgent.getInstructionItems( sharedStateAgent.instructionsContainer, ); }); test("[ADK Middleware] should share state between UI and chat", async ({ page, }) => { const sharedStateAgent = new SharedStatePage(page); await page.goto("/adk-middleware/feature/shared_state"); await sharedStateAgent.openChat(); // Add new ingredient via UI await sharedStateAgent.addIngredient.click(); // Fill in the new ingredient details const newIngredientCard = page.locator(".ingredient-card").last(); await newIngredientCard.locator(".ingredient-name-input").fill("Potatoes"); await newIngredientCard.locator(".ingredient-amount-input").fill("12"); // Wait for UI to update await page.waitForTimeout(1000); // Ask chat for all ingredients await sharedStateAgent.sendMessage("Give me all the ingredients"); await sharedStateAgent.loader(); // Verify chat response includes both existing and new ingredients 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(); }); }); ================================================ FILE: apps/dojo/e2e/tests/adkMiddlewareTests/toolBasedGenUIPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { ToolBaseGenUIPage } from "../../featurePages/ToolBaseGenUIPage"; const pageURL = "/adk-middleware/feature/tool_based_generative_ui"; test.describe("Tool Based Generative UI Feature", () => { test("[ADK Middleware] 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("[ADK Middleware] 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(); // Wait for second haiku to be generated await genAIAgent.checkHaikuDisplay(page); // Now compare the second haiku }); }); ================================================ FILE: apps/dojo/e2e/tests/adkMiddlewareTests/v1AgenticChatPage.spec.ts ================================================ import { test } from "../../test-isolation-helper"; import { V1AgenticChatPage } from "../../featurePages/V1AgenticChatPage"; test("[V1] Google ADK sends and receives a message", async ({ page }) => { await page.goto("/adk-middleware/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\?/); }); ================================================ FILE: apps/dojo/e2e/tests/agnoTests/agenticChatPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { AgenticChatPage } from "../../featurePages/AgenticChatPage"; const appleAsk = "What is the current stock price of AAPL? Please respond in the format of 'The current stock price of Apple Inc. (AAPL) is {{price}}'"; test("[Agno] Agentic Chat sends and receives a greeting message", async ({ page, }) => { await page.goto("/agno/feature/agentic_chat"); const chat = new AgenticChatPage(page); await chat.openChat(); await expect(chat.agentGreeting).toBeVisible(); await chat.sendMessage("Hi"); await chat.assertUserMessageVisible("Hi"); await chat.assertAgentReplyVisible(/Hello! How can I assist you today\?/); }); test("[Agno] Agentic Chat provides stock price information", async ({ page, }) => { await page.goto("/agno/feature/agentic_chat"); const chat = new AgenticChatPage(page); await chat.openChat(); await expect(chat.agentGreeting).toBeVisible(); // Ask for AAPL stock price await chat.sendMessage(appleAsk); await chat.assertUserMessageVisible(appleAsk); // Check if the response contains the expected stock price information await chat.assertAgentReplyContains( "The current stock price of Apple Inc. (AAPL) is $150.25.", ); }); test("[Agno] Agentic Chat retains memory of previous questions", async ({ page, }) => { await page.goto("/agno/feature/agentic_chat"); const chat = new AgenticChatPage(page); await chat.openChat(); await expect(chat.agentGreeting).toBeVisible(); // First question — use a simple, deterministic question (no external API) await chat.sendMessage("What is the capital of France?"); await chat.assertUserMessageVisible("What is the capital of France?"); await chat.assertAgentReplyVisible(/The capital of France is Paris\./); // Ask about the first question to test memory await chat.sendMessage("What was my first question?"); await chat.assertUserMessageVisible("What was my first question?"); // Check if the agent remembers the first question about France await chat.assertAgentReplyVisible( /Your first question was about the capital of France\./, ); }); test("[Agno] Agentic Chat retains memory of user messages during a conversation", async ({ page, }) => { await page.goto("/agno/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!/); }); ================================================ FILE: apps/dojo/e2e/tests/agnoTests/backendToolRenderingPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { awaitLLMResponseDone } from "../../utils/copilot-actions"; test("[Agno] Backend Tool Rendering displays weather cards", async ({ page, }) => { await page.goto("/agno/feature/backend_tool_rendering"); // Verify suggestion buttons are visible await expect( page.getByRole("button", { name: "Weather in San Francisco" }), ).toBeVisible({ timeout: 5000, }); // Click first suggestion and verify weather card appears await page.getByRole("button", { name: "Weather in San Francisco" }).click(); // Wait for either test ID or fallback to "Current Weather" text const weatherCard = page.getByTestId("weather-card"); const currentWeatherText = page.getByText("Current Weather"); // Try test ID first, fallback to text try { await expect(weatherCard).toBeVisible(); } catch (e) { // Fallback to checking for "Current Weather" text await expect(currentWeatherText.first()).toBeVisible(); } // Verify weather content is present (use flexible selectors) 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); // At least one of these should be true expect(hasHumidity || hasWind || hasCityName).toBeTruthy(); // Click second suggestion await page.getByRole("button", { name: "Weather in New York" }).click(); await awaitLLMResponseDone(page); // Verify at least one weather-related element is still visible const weatherElements = await page .getByText(/Weather|Humidity|Wind|Temperature/i) .count(); expect(weatherElements).toBeGreaterThan(0); }); ================================================ FILE: apps/dojo/e2e/tests/agnoTests/humanInTheLoopPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { HumanInLoopPage } from "../../pages/agnoPages/HumanInLoopPage"; test.describe("Human in the Loop Feature", () => { test("[Agno] should interact with the chat and perform steps", async ({ page, }) => { const humanInLoop = new HumanInLoopPage(page); await page.goto("/agno/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("[Agno] should interact with the chat using predefined prompts and perform steps", async ({ page, }) => { const humanInLoop = new HumanInLoopPage(page); await page.goto("/agno/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).`, ); }); }); ================================================ FILE: apps/dojo/e2e/tests/agnoTests/toolBasedGenUIPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { ToolBaseGenUIPage } from "../../featurePages/ToolBaseGenUIPage"; const pageURL = "/agno/feature/tool_based_generative_ui"; test("[Agno] 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("[Agno] 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(); // Wait for second haiku to be generated await genAIAgent.checkHaikuDisplay(page); // Now compare the second haiku }); ================================================ FILE: apps/dojo/e2e/tests/agnoTests/v1AgenticChatPage.spec.ts ================================================ import { test } from "../../test-isolation-helper"; import { V1AgenticChatPage } from "../../featurePages/V1AgenticChatPage"; test("[V1] Agno sends and receives a message", async ({ page }) => { await page.goto("/agno/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\?/); }); ================================================ FILE: apps/dojo/e2e/tests/awsStrandsTests/agenticChatPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { AgenticChatPage } from "../../featurePages/AgenticChatPage"; test("[Strands] Agentic Chat sends and receives a message", async ({ page, }) => { await page.goto("/aws-strands/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("[Strands] Agentic Chat changes background on message and reset", async ({ page, }) => { await page.goto("/aws-strands/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(); // 1. Send message to change background to blue 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(); // 2. Change to pink 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("[Strands] Agentic Chat retains memory of user messages during a conversation", async ({ page, }) => { await page.goto("/aws-strands/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!/); }); ================================================ FILE: apps/dojo/e2e/tests/awsStrandsTests/agenticGenUI.spec.ts ================================================ 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("[Strands] should interact with the chat to get a planner on prompt", async ({ page, }) => { const genUIAgent = new AgenticGenUIPage(page); await page.goto("/aws-strands/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("[Strands] should interact with the chat using predefined prompts and perform steps", async ({ page, }) => { const genUIAgent = new AgenticGenUIPage(page); await page.goto("/aws-strands/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); }); }); ================================================ FILE: apps/dojo/e2e/tests/awsStrandsTests/backendToolRenderingPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; test("[Strands] Backend Tool Rendering displays weather cards", async ({ page, }) => { // Set shorter default timeout for this test test.setTimeout(30000); // 30 seconds total await page.goto("/aws-strands/feature/backend_tool_rendering"); // Verify suggestion buttons are visible await expect( page.getByRole("button", { name: "Weather in San Francisco" }), ).toBeVisible({ timeout: 5000, }); // Click first suggestion and verify weather card appears await page.getByRole("button", { name: "Weather in San Francisco" }).click(); // Wait for either test ID or fallback to "Current Weather" text const weatherCard = page.getByTestId("weather-card"); const currentWeatherText = page.getByText("Current Weather"); // Try test ID first, fallback to text try { await expect(weatherCard).toBeVisible(); } catch (e) { // Fallback to checking for "Current Weather" text await expect(currentWeatherText.first()).toBeVisible(); } // Verify weather content is present (use flexible selectors) 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); // At least one of these should be true expect(hasHumidity || hasWind || hasCityName).toBeTruthy(); // Click second suggestion await page.getByRole("button", { name: "Weather in New York" }).click(); await page.waitForTimeout(2000); // Verify at least one weather-related element is still visible const weatherElements = await page .getByText(/Weather|Humidity|Wind|Temperature/i) .count(); expect(weatherElements).toBeGreaterThan(0); }); ================================================ FILE: apps/dojo/e2e/tests/awsStrandsTests/humanInTheLoopPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { HumanInLoopPage } from "../../pages/awsStrandsPages/HumanInLoopPage"; test.describe("Human in the Loop Feature", () => { test("[Strands] should interact with the chat and perform steps", async ({ page, }) => { const humanInLoop = new HumanInLoopPage(page); await page.goto("/aws-strands/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("[Strands] should interact with the chat using predefined prompts and perform steps", async ({ page, }) => { const humanInLoop = new HumanInLoopPage(page); await page.goto("/aws-strands/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).`, ); }); }); ================================================ FILE: apps/dojo/e2e/tests/awsStrandsTests/sharedStatePage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { SharedStatePage } from "../../featurePages/SharedStatePage"; test.describe("Shared State Feature", () => { test("[Strands] should interact with the chat to get a recipe on prompt", async ({ page, }) => { const sharedStateAgent = new SharedStatePage(page); await page.goto("/aws-strands/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("[Strands] should share state between UI and chat", async ({ page }) => { const sharedStateAgent = new SharedStatePage(page); await page.goto("/aws-strands/feature/shared_state"); await sharedStateAgent.openChat(); // Add new ingredient via UI await sharedStateAgent.addIngredient.click(); // Fill in the new ingredient details const newIngredientCard = page.locator(".ingredient-card").last(); await newIngredientCard.locator(".ingredient-name-input").fill("Potatoes"); await newIngredientCard.locator(".ingredient-amount-input").fill("12"); // Wait for UI to update await page.waitForTimeout(1000); // Ask chat for all ingredients await sharedStateAgent.sendMessage("Please list all of the ingredients"); await sharedStateAgent.loader(); // Verify chat response includes both existing and new ingredients 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(); }); }); ================================================ FILE: apps/dojo/e2e/tests/awsStrandsTests/v1AgenticChatPage.spec.ts ================================================ import { test } from "../../test-isolation-helper"; import { V1AgenticChatPage } from "../../featurePages/V1AgenticChatPage"; test("[V1] AWS Strands sends and receives a message", async ({ page }) => { await page.goto("/aws-strands/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\?/); }); ================================================ FILE: apps/dojo/e2e/tests/claudeAgentSdkPythonTests/agenticChatPage.spec.ts ================================================ import { test } from "../../test-isolation-helper"; import { AgenticChatPage } from "../../featurePages/AgenticChatPage"; test("[Claude Agent SDK Python] Agentic Chat sends and receives a greeting message", async ({ page, }) => { await page.goto("/claude-agent-sdk-python/feature/agentic_chat"); const chat = new AgenticChatPage(page); await chat.openChat(); await chat.sendMessage("Hi"); await chat.assertUserMessageVisible("Hi"); await chat.assertAgentReplyVisible(/Hello|Hi|hey/i); }); test("[Claude Agent SDK Python] Agentic Chat retains memory of previous questions", async ({ page, }) => { test.slow(); await page.goto("/claude-agent-sdk-python/feature/agentic_chat"); const chat = new AgenticChatPage(page); await chat.openChat(); await chat.sendMessage("Hi, my name is Alex"); await chat.assertUserMessageVisible("Hi, my name is Alex"); await chat.assertAgentReplyVisible(/Hello|Hi|Alex/i); await chat.sendMessage("What is my name?"); await chat.assertUserMessageVisible("What is my name?"); await chat.assertAgentReplyVisible(/Alex/i); }); test("[Claude Agent SDK Python] Agentic Chat retains memory of user messages during a conversation", async ({ page, }) => { test.slow(); await page.goto("/claude-agent-sdk-python/feature/agentic_chat"); const chat = new AgenticChatPage(page); await chat.openChat(); await chat.sendMessage("Hey there"); await chat.assertUserMessageVisible("Hey there"); const favFruit = "Mango"; await chat.sendMessage(`My favorite fruit is ${favFruit}`); await chat.assertUserMessageVisible(`My favorite fruit is ${favFruit}`); await chat.assertAgentReplyVisible(new RegExp(favFruit, "i")); await chat.sendMessage("and I love listening to Kaavish"); await chat.assertUserMessageVisible("and I love listening to Kaavish"); await chat.assertAgentReplyVisible(/Kaavish/i); await chat.sendMessage("tell me an interesting fact about Moon"); await chat.assertUserMessageVisible("tell me an interesting fact about Moon"); await chat.assertAgentReplyVisible(/Moon/i); 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(new RegExp(favFruit, "i")); }); ================================================ FILE: apps/dojo/e2e/tests/claudeAgentSdkPythonTests/backendToolRenderingPage.spec.ts ================================================ import { test, expect } from "@playwright/test"; test("[Claude Agent SDK Python] Backend Tool Rendering displays weather cards", async ({ page, }) => { test.setTimeout(30000); await page.goto("/claude-agent-sdk-python/feature/backend_tool_rendering"); // Verify suggestion buttons are visible await expect( page.getByRole("button", { name: "Weather in San Francisco" }) ).toBeVisible({ timeout: 5000 }); // Click first suggestion and verify weather card appears await page .getByRole("button", { name: "Weather in San Francisco" }) .click(); // Wait for either test ID or fallback to "Current Weather" text const weatherCard = page.getByTestId("weather-card"); const currentWeatherText = page.getByText("Current Weather"); try { await expect(weatherCard).toBeVisible({ timeout: 10000 }); } catch (e) { await expect(currentWeatherText.first()).toBeVisible({ timeout: 10000 }); } // Verify weather content is present (use flexible selectors) 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); // At least one of these should be true expect(hasHumidity || hasWind || hasCityName).toBeTruthy(); // Click second suggestion await page.getByRole("button", { name: "Weather in New York" }).click(); await page.waitForTimeout(2000); // Verify at least one weather-related element is still visible const weatherElements = await page .getByText(/Weather|Humidity|Wind|Temperature/i) .count(); expect(weatherElements).toBeGreaterThan(0); }); ================================================ FILE: apps/dojo/e2e/tests/claudeAgentSdkPythonTests/humanInTheLoopPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { awaitLLMResponseDone } from "../../utils/copilot-actions"; import { HumanInTheLoopPage } from "../../featurePages/HumanInTheLoopPage"; test.describe("Human in the Loop Feature", () => { test("[Claude Agent SDK Python] should interact with the chat and perform steps", async ({ page, }) => { test.slow(); const humanInLoop = new HumanInTheLoopPage(page); await page.goto("/claude-agent-sdk-python/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.performSteps(); await awaitLLMResponseDone(page); await humanInLoop.sendMessage( `Does the planner include ${itemText}? ⚠️ Reply with only words 'Yes' or 'No' (no explanation, no punctuation).` ); }); test("[Claude Agent SDK Python] should interact with the chat using predefined prompts and perform steps", async ({ page, }) => { test.slow(); const humanInLoop = new HumanInTheLoopPage(page); await page.goto("/claude-agent-sdk-python/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.performSteps(); await awaitLLMResponseDone(page); await humanInLoop.sendMessage( `Does the planner include ${uncheckedItem}? ⚠️ Reply with only words 'Yes' or 'No' (no explanation, no punctuation).` ); }); }); ================================================ FILE: apps/dojo/e2e/tests/claudeAgentSdkPythonTests/sharedStatePage.spec.ts ================================================ import { test, expect } from "@playwright/test"; import { SharedStatePage } from "../../featurePages/SharedStatePage"; test.describe("Shared State Feature", () => { test("[Claude Agent SDK Python] should interact with the chat to get a recipe on prompt", async ({ page, }) => { test.slow(); // Claude Agent SDK responses go through CLI subprocess const sharedStateAgent = new SharedStatePage(page); await page.goto("/claude-agent-sdk-python/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"' ); await sharedStateAgent.loader(); // Use longer timeout than SharedStatePage.awaitIngredientCard default (15s) // Claude Agent SDK responses go through a CLI subprocess and are slower await page.waitForFunction( (ingredientName) => { const inputs = document.querySelectorAll('.ingredient-card input.ingredient-name-input'); return Array.from(inputs).some( (input: HTMLInputElement) => input.value.toLowerCase().includes(ingredientName.toLowerCase()) ); }, "Pasta", { timeout: 60000 } ); await sharedStateAgent.getInstructionItems( sharedStateAgent.instructionsContainer ); }); test("[Claude Agent SDK Python] should share state between UI and chat", async ({ page, }) => { test.slow(); // Claude Agent SDK responses go through CLI subprocess const sharedStateAgent = new SharedStatePage(page); await page.goto("/claude-agent-sdk-python/feature/shared_state"); await sharedStateAgent.openChat(); // Add new ingredient via UI await sharedStateAgent.addIngredient.click(); // Fill in the new ingredient details const newIngredientCard = page.locator(".ingredient-card").last(); await newIngredientCard.locator(".ingredient-name-input").fill("Potatoes"); await newIngredientCard.locator(".ingredient-amount-input").fill("12"); // Wait for UI to update await page.waitForTimeout(1000); // Ask chat for all ingredients await sharedStateAgent.sendMessage("Give me all the ingredients"); await sharedStateAgent.loader(); // Verify chat response includes the new ingredient await expect( sharedStateAgent.agentMessage.getByText(/Potatoes/) ).toBeVisible({ timeout: 30000 }); }); }); ================================================ FILE: apps/dojo/e2e/tests/claudeAgentSdkPythonTests/toolBasedGenUIPage.spec.ts ================================================ import { test, expect } from "@playwright/test"; import { ToolBaseGenUIPage } from "../../featurePages/ToolBaseGenUIPage"; const pageURL = "/claude-agent-sdk-python/feature/tool_based_generative_ui"; test.describe("Tool Based Generative UI Feature", () => { test("[Claude Agent SDK Python] 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("[Claude Agent SDK Python] 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); }); }); ================================================ FILE: apps/dojo/e2e/tests/claudeAgentSdkTypescriptTests/agenticChatPage.spec.ts ================================================ import { test } from "../../test-isolation-helper"; import { AgenticChatPage } from "../../featurePages/AgenticChatPage"; test("[Claude Agent SDK TypeScript] Agentic Chat sends and receives a greeting message", async ({ page, }) => { await page.goto("/claude-agent-sdk-typescript/feature/agentic_chat"); const chat = new AgenticChatPage(page); await chat.openChat(); await chat.sendMessage("Hi"); await chat.assertUserMessageVisible("Hi"); await chat.assertAgentReplyVisible(/Hello|Hi|hey/i); }); test("[Claude Agent SDK TypeScript] Agentic Chat retains memory of previous questions", async ({ page, }) => { test.slow(); await page.goto("/claude-agent-sdk-typescript/feature/agentic_chat"); const chat = new AgenticChatPage(page); await chat.openChat(); await chat.sendMessage("Hi, my name is Alex"); await chat.assertUserMessageVisible("Hi, my name is Alex"); await chat.assertAgentReplyVisible(/Hello|Hi|Alex/i); await chat.sendMessage("What is my name?"); await chat.assertUserMessageVisible("What is my name?"); await chat.assertAgentReplyVisible(/Alex/i); }); test("[Claude Agent SDK TypeScript] Agentic Chat retains memory of user messages during a conversation", async ({ page, }) => { test.slow(); await page.goto("/claude-agent-sdk-typescript/feature/agentic_chat"); const chat = new AgenticChatPage(page); await chat.openChat(); await chat.sendMessage("Hey there"); await chat.assertUserMessageVisible("Hey there"); const favFruit = "Mango"; await chat.sendMessage(`My favorite fruit is ${favFruit}`); await chat.assertUserMessageVisible(`My favorite fruit is ${favFruit}`); await chat.assertAgentReplyVisible(new RegExp(favFruit, "i")); await chat.sendMessage("and I love listening to Kaavish"); await chat.assertUserMessageVisible("and I love listening to Kaavish"); await chat.assertAgentReplyVisible(/Kaavish/i); await chat.sendMessage("tell me an interesting fact about Moon"); await chat.assertUserMessageVisible("tell me an interesting fact about Moon"); await chat.assertAgentReplyVisible(/Moon/i); 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(new RegExp(favFruit, "i")); }); ================================================ FILE: apps/dojo/e2e/tests/claudeAgentSdkTypescriptTests/backendToolRenderingPage.spec.ts ================================================ import { test, expect } from "@playwright/test"; test("[Claude Agent SDK TypeScript] Backend Tool Rendering displays weather cards", async ({ page, }) => { test.setTimeout(30000); await page.goto( "/claude-agent-sdk-typescript/feature/backend_tool_rendering" ); // Verify suggestion buttons are visible await expect( page.getByRole("button", { name: "Weather in San Francisco" }) ).toBeVisible({ timeout: 5000 }); // Click first suggestion and verify weather card appears await page .getByRole("button", { name: "Weather in San Francisco" }) .click(); // Wait for either test ID or fallback to "Current Weather" text const weatherCard = page.getByTestId("weather-card"); const currentWeatherText = page.getByText("Current Weather"); try { await expect(weatherCard).toBeVisible({ timeout: 10000 }); } catch (e) { await expect(currentWeatherText.first()).toBeVisible({ timeout: 10000 }); } // Verify weather content is present (use flexible selectors) 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); // At least one of these should be true expect(hasHumidity || hasWind || hasCityName).toBeTruthy(); // Click second suggestion await page.getByRole("button", { name: "Weather in New York" }).click(); await page.waitForTimeout(2000); // Verify at least one weather-related element is still visible const weatherElements = await page .getByText(/Weather|Humidity|Wind|Temperature/i) .count(); expect(weatherElements).toBeGreaterThan(0); }); ================================================ FILE: apps/dojo/e2e/tests/claudeAgentSdkTypescriptTests/humanInTheLoopPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { awaitLLMResponseDone } from "../../utils/copilot-actions"; import { HumanInTheLoopPage } from "../../featurePages/HumanInTheLoopPage"; test.describe("Human in the Loop Feature", () => { test("[Claude Agent SDK TypeScript] should interact with the chat and perform steps", async ({ page, }) => { test.slow(); const humanInLoop = new HumanInTheLoopPage(page); await page.goto("/claude-agent-sdk-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.performSteps(); await awaitLLMResponseDone(page); await humanInLoop.sendMessage( `Does the planner include ${itemText}? ⚠️ Reply with only words 'Yes' or 'No' (no explanation, no punctuation).` ); }); test("[Claude Agent SDK TypeScript] should interact with the chat using predefined prompts and perform steps", async ({ page, }) => { test.slow(); const humanInLoop = new HumanInTheLoopPage(page); await page.goto("/claude-agent-sdk-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.performSteps(); await awaitLLMResponseDone(page); await humanInLoop.sendMessage( `Does the planner include ${uncheckedItem}? ⚠️ Reply with only words 'Yes' or 'No' (no explanation, no punctuation).` ); }); }); ================================================ FILE: apps/dojo/e2e/tests/claudeAgentSdkTypescriptTests/sharedStatePage.spec.ts ================================================ import { test, expect } from "@playwright/test"; import { SharedStatePage } from "../../featurePages/SharedStatePage"; test.describe("Shared State Feature", () => { test("[Claude Agent SDK TypeScript] should interact with the chat to get a recipe on prompt", async ({ page, }) => { test.slow(); // Claude Agent SDK responses go through CLI subprocess const sharedStateAgent = new SharedStatePage(page); await page.goto("/claude-agent-sdk-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"' ); await sharedStateAgent.loader(); // Use longer timeout than SharedStatePage.awaitIngredientCard default (15s) // Claude Agent SDK responses go through a CLI subprocess and are slower await page.waitForFunction( (ingredientName) => { const inputs = document.querySelectorAll('.ingredient-card input.ingredient-name-input'); return Array.from(inputs).some( (input: HTMLInputElement) => input.value.toLowerCase().includes(ingredientName.toLowerCase()) ); }, "Pasta", { timeout: 60000 } ); await sharedStateAgent.getInstructionItems( sharedStateAgent.instructionsContainer ); }); test("[Claude Agent SDK TypeScript] should share state between UI and chat", async ({ page, }) => { test.slow(); // Claude Agent SDK responses go through CLI subprocess const sharedStateAgent = new SharedStatePage(page); await page.goto("/claude-agent-sdk-typescript/feature/shared_state"); await sharedStateAgent.openChat(); // Add new ingredient via UI await sharedStateAgent.addIngredient.click(); // Fill in the new ingredient details const newIngredientCard = page.locator(".ingredient-card").last(); await newIngredientCard.locator(".ingredient-name-input").fill("Potatoes"); await newIngredientCard.locator(".ingredient-amount-input").fill("12"); // Wait for UI to update await page.waitForTimeout(1000); // Ask chat for all ingredients await sharedStateAgent.sendMessage("Give me all the ingredients"); await sharedStateAgent.loader(); // Verify chat response includes the new ingredient await expect( sharedStateAgent.agentMessage.getByText(/Potatoes/) ).toBeVisible({ timeout: 30000 }); }); }); ================================================ FILE: apps/dojo/e2e/tests/claudeAgentSdkTypescriptTests/toolBasedGenUIPage.spec.ts ================================================ import { test, expect } from "@playwright/test"; import { ToolBaseGenUIPage } from "../../featurePages/ToolBaseGenUIPage"; const pageURL = "/claude-agent-sdk-typescript/feature/tool_based_generative_ui"; test.describe("Tool Based Generative UI Feature", () => { test("[Claude Agent SDK TypeScript] 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("[Claude Agent SDK TypeScript] 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); }); }); ================================================ FILE: apps/dojo/e2e/tests/crewAITests/agenticChatPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { AgenticChatPage } from "../../featurePages/AgenticChatPage"; test("[CrewAI] Agentic Chat sends and receives a message", async ({ page }) => { await page.goto("/crewai/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/i); }); test("[CrewAI] Agentic Chat changes background on message and reset", async ({ page, }) => { await page.goto("/crewai/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(); // 1. Send message to change background to blue 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(); // 2. Change to pink 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("[CrewAI] Agentic Chat retains memory of user messages during a conversation", async ({ page, }) => { await page.goto("/crewai/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(/how can I assist you/i); const favFruit = "Mango"; await chat.sendMessage(`My favorite fruit is ${favFruit}`); await chat.assertUserMessageVisible(`My favorite fruit is ${favFruit}`); await chat.assertAgentReplyVisible(new RegExp(favFruit, "i")); await chat.sendMessage("and I love listening to Kaavish"); await chat.assertUserMessageVisible("and I love listening to Kaavish"); await chat.assertAgentReplyVisible(/Kaavish/i); await chat.sendMessage("tell me an interesting fact about Moon"); await chat.assertUserMessageVisible("tell me an interesting fact about Moon"); await chat.assertAgentReplyVisible(/Moon/i); 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(new RegExp(favFruit, "i")); }); ================================================ FILE: apps/dojo/e2e/tests/crewAITests/agenticGenUI.spec.ts ================================================ import { awaitLLMResponseDone } from "../../utils/copilot-actions"; import { test, expect } from "../../test-isolation-helper"; import { AgenticGenUIPage } from "../../pages/crewAIPages/AgenticUIGenPage"; test.fixme("[CrewAI] Agentic Gen UI", () => { // Flaky test("[CrewAI] should interact with the chat to get a planner on prompt", async ({ page, }) => { const genUIAgent = new AgenticGenUIPage(page); await page.goto("/crewai/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); }); // Flaky test.fixme( "[CrewAI] should interact with the chat using predefined prompts and perform steps", async ({ page }) => { const genUIAgent = new AgenticGenUIPage(page); await page.goto("/crewai/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); }, ); }); ================================================ FILE: apps/dojo/e2e/tests/crewAITests/humanInTheLoopPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { HumanInLoopPage } from "../../pages/crewAIPages/HumanInLoopPage"; test.describe("Human in the Loop Feature", () => { test("[CrewAI] should interact with the chat and perform steps", async ({ page, }) => { const humanInLoop = new HumanInLoopPage(page); await page.goto("/crewai/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}? \u26a0\ufe0f Reply with only words 'Yes' or 'No' (no explanation, no punctuation).`, ); }); test("[CrewAI] should interact with the chat using predefined prompts and perform steps", async ({ page, }) => { const humanInLoop = new HumanInLoopPage(page); await page.goto("/crewai/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}? \u26a0\ufe0f Reply with only words 'Yes' or 'No' (no explanation, no punctuation).`, ); }); }); ================================================ FILE: apps/dojo/e2e/tests/crewAITests/predictiveStateUpdatePage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { PredictiveStateUpdatesPage } from "../../pages/crewAIPages/PredictiveStateUpdatesPage"; test.describe("Predictive Status Updates Feature", () => { test.slow(); // Multi-step AI flow through crew-ai: needs extra time test("[CrewAI] should interact with agent and approve asked changes", async ({ page, }) => { const predictiveStateUpdates = new PredictiveStateUpdatesPage(page); await page.goto("/crewai/feature/predictive_state_updates"); await predictiveStateUpdates.openChat(); await predictiveStateUpdates.sendMessage( "Give me a story for a dragon called Atlantis in document", ); await predictiveStateUpdates.getPredictiveResponse(); await predictiveStateUpdates.getUserApproval(); await expect(predictiveStateUpdates.confirmedChangesResponse).toBeVisible(); const dragonName = await predictiveStateUpdates.verifyAgentResponse("Atlantis"); expect(dragonName).not.toBeNull(); // Send update to change the dragon name await predictiveStateUpdates.sendMessage("Change dragon name to Lola"); await predictiveStateUpdates.verifyHighlightedText(); await predictiveStateUpdates.getUserApproval(); await expect(predictiveStateUpdates.confirmedChangesResponse).toBeVisible(); const dragonNameNew = await predictiveStateUpdates.verifyAgentResponse("Lola"); expect(dragonNameNew).not.toBe(dragonName); }); test("[CrewAI] should interact with agent and reject asked changes", async ({ page, }) => { const predictiveStateUpdates = new PredictiveStateUpdatesPage(page); await page.goto("/crewai/feature/predictive_state_updates"); await predictiveStateUpdates.openChat(); await predictiveStateUpdates.sendMessage( "Give me a story for a dragon called Atlantis in document", ); await predictiveStateUpdates.getPredictiveResponse(); await predictiveStateUpdates.getUserApproval(); await expect(predictiveStateUpdates.confirmedChangesResponse).toBeVisible(); const dragonName = await predictiveStateUpdates.verifyAgentResponse("Atlantis"); expect(dragonName).not.toBeNull(); // Send update to change the dragon name await predictiveStateUpdates.sendMessage("Change dragon name to Lola"); await predictiveStateUpdates.verifyHighlightedText(); await predictiveStateUpdates.getUserRejection(); await expect(predictiveStateUpdates.rejectedChangesResponse).toBeVisible(); const dragonNameAfterRejection = await predictiveStateUpdates.verifyAgentResponse("Atlantis"); expect(dragonNameAfterRejection).toBe(dragonName); expect(dragonNameAfterRejection).not.toBe("Lola"); }); }); ================================================ FILE: apps/dojo/e2e/tests/crewAITests/sharedStatePage.spec.ts ================================================ import { test, expect } from "@playwright/test"; import { SharedStatePage } from "../../featurePages/SharedStatePage"; test.describe("Shared State Feature", () => { test("[CrewAI] should interact with the chat to get a recipe on prompt", async ({ page, }) => { const sharedStateAgent = new SharedStatePage(page); // Update URL to new domain await page.goto( "/crewai/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"'); await sharedStateAgent.loader(); await sharedStateAgent.awaitIngredientCard('Pasta'); await sharedStateAgent.getInstructionItems( sharedStateAgent.instructionsContainer ); }); test("[CrewAI] should share state between UI and chat", async ({ page, }) => { const sharedStateAgent = new SharedStatePage(page); await page.goto( "/crewai/feature/shared_state" ); await sharedStateAgent.openChat(); // Add new ingredient via UI await sharedStateAgent.addIngredient.click(); // Fill in the new ingredient details const newIngredientCard = page.locator('.ingredient-card').last(); await newIngredientCard.locator('.ingredient-name-input').fill('Potatoes'); await newIngredientCard.locator('.ingredient-amount-input').fill('12'); // Wait for UI to update await page.waitForTimeout(1000); // Ask chat for all ingredients await sharedStateAgent.sendMessage("Give me all the ingredients"); await sharedStateAgent.loader(); // Verify chat response includes both existing and new ingredients 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(); }); }); ================================================ FILE: apps/dojo/e2e/tests/crewAITests/toolBasedGenUIPage.spec.ts ================================================ import { test, expect } from "@playwright/test"; import { ToolBaseGenUIPage } from "../../featurePages/ToolBaseGenUIPage"; const pageURL = "/crewai/feature/tool_based_generative_ui"; test('[CrewAI] 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('[CrewAI] 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(); // Wait for second haiku to be generated await genAIAgent.checkHaikuDisplay(page); // Now compare the second haiku }); ================================================ FILE: apps/dojo/e2e/tests/crewAITests/v1AgenticChatPage.spec.ts ================================================ import { test } from "../../test-isolation-helper"; import { V1AgenticChatPage } from "../../featurePages/V1AgenticChatPage"; test("[V1] CrewAI sends and receives a message", async ({ page }) => { await page.goto("/crewai/feature/v1_agentic_chat"); const chat = new V1AgenticChatPage(page); await chat.sendMessage("Hi"); await chat.assertUserMessageVisible("Hi"); await chat.assertAgentReplyVisible(/Hello|Hi|hey|help|assist/i); }); ================================================ FILE: apps/dojo/e2e/tests/langchainTests/agenticChatPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { AgenticChatPage } from "../../featurePages/AgenticChatPage"; test("[LangChain] Agentic Chat sends and receives a message", async ({ page, }) => { await page.goto("/langchain/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/i); }); test("[LangChain] Agentic Chat changes background on message and reset", async ({ page, }) => { await page.goto("/langchain/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(); // 1. Send message to change background to blue 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(); // 2. Change to pink 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("[LangChain] Agentic Chat retains memory of user messages during a conversation", async ({ page, }) => { await page.goto("/langchain/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(/how can I assist you/i); const favFruit = "Mango"; await chat.sendMessage(`My favorite fruit is ${favFruit}`); await chat.assertUserMessageVisible(`My favorite fruit is ${favFruit}`); await chat.assertAgentReplyVisible(new RegExp(favFruit, "i")); await chat.sendMessage("and I love listening to Kaavish"); await chat.assertUserMessageVisible("and I love listening to Kaavish"); await chat.assertAgentReplyVisible(/Kaavish/i); await chat.sendMessage("tell me an interesting fact about Moon"); await chat.assertUserMessageVisible("tell me an interesting fact about Moon"); await chat.assertAgentReplyVisible(/Moon/i); 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(new RegExp(favFruit, "i")); }); ================================================ FILE: apps/dojo/e2e/tests/langchainTests/toolBasedGenUIPage.spec.ts ================================================ import { test, expect } from "@playwright/test"; import { ToolBaseGenUIPage } from "../../featurePages/ToolBaseGenUIPage"; const pageURL = "/langchain/feature/tool_based_generative_ui"; test('[LangChain] 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('[LangChain] 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); }); ================================================ FILE: apps/dojo/e2e/tests/langgraphFastAPITests/agenticChatPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { AgenticChatPage } from "../../featurePages/AgenticChatPage"; test("[LangGraph FastAPI] Agentic Chat sends and receives a message", async ({ page, }) => { await page.goto("/langgraph-fastapi/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|Hi|Hey|Greetings|nice to meet|welcome/i, ); }); test("[LangGraph FastAPI] Agentic Chat changes background on message and reset", async ({ page, }) => { await page.goto("/langgraph-fastapi/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(); // 1. Send message to change background to blue 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(); // 2. Change to pink 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); const backgroundAfterPink = await getBackground(); // Verify it also differs from initial (not a reset) expect(backgroundAfterPink).not.toBe(initialBackground); }); test("[LangGraph FastAPI] Agentic Chat retains memory of user messages during a conversation", async ({ page, }) => { await page.goto("/langgraph-fastapi/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( /how can I|help|assist|what can I do|what would you like/i, ); const favFruit = "Mango"; await chat.sendMessage(`My favorite fruit is ${favFruit}`); await chat.assertUserMessageVisible(`My favorite fruit is ${favFruit}`); await chat.assertAgentReplyVisible(new RegExp(favFruit, "i")); await chat.sendMessage("and I love listening to Kaavish"); await chat.assertUserMessageVisible("and I love listening to Kaavish"); await chat.assertAgentReplyVisible(/Kaavish/i); await chat.sendMessage("tell me an interesting fact about Moon"); await chat.assertUserMessageVisible("tell me an interesting fact about Moon"); await chat.assertAgentReplyVisible(/Moon/i); 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(new RegExp(favFruit, "i")); }); // Skip: CopilotChat v2 does not wire up onRegenerate to assistant messages, // so the regenerate button is not rendered. Requires framework-level change. test.skip("[LangGraph FastAPI] Agentic Chat regenerates a response", async ({ page, }) => { await page.goto("/langgraph-fastapi/feature/agentic_chat"); const chat = new AgenticChatPage(page); await chat.openChat(); await expect(chat.agentGreeting).toBeVisible(); // Send messages using page object (now uses sendChatMessage + awaitLLMResponseDone) await chat.sendMessage("tell me a joke"); // Greeting is not a copilot-assistant-message, so joke reply is at index 0 const jokeIndex = 0; await chat.getAssistantMessageText(jokeIndex); // Send a filler so the joke is not the last message await chat.sendMessage("say hello"); // Regenerate the joke response await chat.regenerateResponse(jokeIndex); await page.waitForFunction( () => document.querySelector('[data-copilot-running="false"]') !== null, null, { timeout: 15000 }, ); const newJoke = await chat.getAssistantMessageText(jokeIndex); expect(newJoke.length).toBeGreaterThan(0); }); ================================================ FILE: apps/dojo/e2e/tests/langgraphFastAPITests/agenticGenUI.spec.ts ================================================ import { awaitLLMResponseDone } from "../../utils/copilot-actions"; import { test, expect } from "../../test-isolation-helper"; import { AgenticGenUIPage } from "../../pages/langGraphFastAPIPages/AgenticUIGenPage"; test.describe("Agent Generative UI Feature", () => { test("[LangGraph FastAPI] should interact with the chat to get a planner on prompt", async ({ page, }) => { const genUIAgent = new AgenticGenUIPage(page); await page.goto("/langgraph-fastapi/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("[LangGraph FastAPI] should interact with the chat using predefined prompts and perform steps", async ({ page, }) => { const genUIAgent = new AgenticGenUIPage(page); await page.goto("/langgraph-fastapi/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); }); }); ================================================ FILE: apps/dojo/e2e/tests/langgraphFastAPITests/backendToolRenderingPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { awaitLLMResponseDone } from "../../utils/copilot-actions"; test("[LanggraphFastAPI] Backend Tool Rendering displays weather cards", async ({ page, }) => { await page.goto("/langgraph-fastapi/feature/backend_tool_rendering"); // Verify suggestion buttons are visible await expect( page.getByRole("button", { name: "Weather in San Francisco" }), ).toBeVisible({ timeout: 5000, }); // Click first suggestion and verify weather card appears await page.getByRole("button", { name: "Weather in San Francisco" }).click(); // Wait for either test ID or fallback to "Current Weather" text const weatherCard = page.getByTestId("weather-card"); const currentWeatherText = page.getByText("Current Weather"); // Try test ID first, fallback to text try { await expect(weatherCard).toBeVisible(); } catch (e) { // Fallback to checking for "Current Weather" text await expect(currentWeatherText.first()).toBeVisible(); } // Verify weather content is present (use flexible selectors) 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); // At least one of these should be true expect(hasHumidity || hasWind || hasCityName).toBeTruthy(); // Click second suggestion await page.getByRole("button", { name: "Weather in New York" }).click(); await awaitLLMResponseDone(page); // Verify at least one weather-related element is still visible const weatherElements = await page .getByText(/Weather|Humidity|Wind|Temperature/i) .count(); expect(weatherElements).toBeGreaterThan(0); }); ================================================ FILE: apps/dojo/e2e/tests/langgraphFastAPITests/humanInTheLoopPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { HumanInLoopPage } from "../../pages/langGraphFastAPIPages/HumanInLoopPage"; test.describe("Human in the Loop Feature", () => { test("[LangGraph FastAPI] should interact with the chat and perform steps", async ({ page, }) => { const humanInLoop = new HumanInLoopPage(page); await page.goto("/langgraph-fastapi/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}? \u26a0\ufe0f Reply with only words 'Yes' or 'No' (no explanation, no punctuation).`, ); }); test("[LangGraph FastAPI] should interact with the chat using predefined prompts and perform steps", async ({ page, }) => { const humanInLoop = new HumanInLoopPage(page); await page.goto("/langgraph-fastapi/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}? \u26a0\ufe0f Reply with only words 'Yes' or 'No' (no explanation, no punctuation).`, ); }); }); ================================================ FILE: apps/dojo/e2e/tests/langgraphFastAPITests/predictiveStateUpdatePage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { PredictiveStateUpdatesPage } from "../../pages/langGraphFastAPIPages/PredictiveStateUpdatesPage"; test.describe("Predictive Status Updates Feature", () => { test("[LangGraph FastAPI] should interact with agent and approve asked changes", async ({ page, }) => { const predictiveStateUpdates = new PredictiveStateUpdatesPage(page); await page.goto("/langgraph-fastapi/feature/predictive_state_updates"); await predictiveStateUpdates.openChat(); await predictiveStateUpdates.sendMessage( "Give me a story for a dragon called Atlantis in document", ); await predictiveStateUpdates.getPredictiveResponse(); await predictiveStateUpdates.getUserApproval(); await expect(predictiveStateUpdates.confirmedChangesResponse).toBeVisible(); const dragonName = await predictiveStateUpdates.verifyAgentResponse("Atlantis"); expect(dragonName).not.toBeNull(); await predictiveStateUpdates.sendMessage("Change dragon name to Lola"); await predictiveStateUpdates.verifyHighlightedText(); await predictiveStateUpdates.getUserApproval(); await expect(predictiveStateUpdates.confirmedChangesResponse).toBeVisible(); const dragonNameNew = await predictiveStateUpdates.verifyAgentResponse("Lola"); expect(dragonNameNew).not.toBe(dragonName); }); test("[LangGraph FastAPI] should interact with agent and reject asked changes", async ({ page, }) => { const predictiveStateUpdates = new PredictiveStateUpdatesPage(page); await page.goto("/langgraph-fastapi/feature/predictive_state_updates"); await predictiveStateUpdates.openChat(); await predictiveStateUpdates.sendMessage( "Give me a story for a dragon called Atlantis in document", ); await predictiveStateUpdates.getPredictiveResponse(); await predictiveStateUpdates.getUserApproval(); await expect(predictiveStateUpdates.confirmedChangesResponse).toBeVisible(); const dragonName = await predictiveStateUpdates.verifyAgentResponse("Atlantis"); expect(dragonName).not.toBeNull(); await predictiveStateUpdates.sendMessage("Change dragon name to Lola"); await predictiveStateUpdates.verifyHighlightedText(); await predictiveStateUpdates.getUserRejection(); await expect(predictiveStateUpdates.rejectedChangesResponse).toBeVisible(); const dragonNameAfterRejection = await predictiveStateUpdates.verifyAgentResponse("Atlantis"); expect(dragonNameAfterRejection).toBe(dragonName); expect(dragonNameAfterRejection).not.toBe("Lola"); }); }); ================================================ FILE: apps/dojo/e2e/tests/langgraphFastAPITests/sharedStatePage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { SharedStatePage } from "../../featurePages/SharedStatePage"; test.describe("Shared State Feature", () => { test("[LangGraph FastAPI] should interact with the chat to get a recipe on prompt", async ({ page, }) => { const sharedStateAgent = new SharedStatePage(page); // Update URL to new domain await page.goto("/langgraph-fastapi/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"', ); await sharedStateAgent.loader(); await sharedStateAgent.awaitIngredientCard("Pasta"); await sharedStateAgent.getInstructionItems( sharedStateAgent.instructionsContainer, ); }); test("[LangGraph FastAPI] should share state between UI and chat", async ({ page, }) => { const sharedStateAgent = new SharedStatePage(page); await page.goto("/langgraph-fastapi/feature/shared_state"); await sharedStateAgent.openChat(); // Add new ingredient via UI await sharedStateAgent.addIngredient.click(); // Fill in the new ingredient details const newIngredientCard = page.locator(".ingredient-card").last(); await newIngredientCard.locator(".ingredient-name-input").fill("Potatoes"); await newIngredientCard.locator(".ingredient-amount-input").fill("12"); // Wait for UI to update await page.waitForTimeout(1000); // Ask chat for all ingredients await sharedStateAgent.sendMessage( "Give me all the ingredients, also list them in your message", ); await sharedStateAgent.loader(); // Verify chat response includes both existing and new ingredients await expect( sharedStateAgent.agentMessage.getByText(/Potatoes/i), ).toBeVisible(); await expect(sharedStateAgent.agentMessage.getByText(/12/)).toBeVisible(); await expect( sharedStateAgent.agentMessage.getByText(/Carrots/i), ).toBeVisible(); await expect( sharedStateAgent.agentMessage.getByText( /All-Purpose Flour|all.purpose flour/i, ), ).toBeVisible(); }); }); ================================================ FILE: apps/dojo/e2e/tests/langgraphFastAPITests/subgraphsPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { SubgraphsPage } from "../../pages/langGraphPages/SubgraphsPage"; test.describe("Subgraphs Travel Agent Feature", () => { test("[LangGraph] should complete full travel planning flow with feature validation", async ({ page, }) => { const subgraphsPage = new SubgraphsPage(page); await page.goto("/langgraph-fastapi/feature/subgraphs"); await subgraphsPage.openChat(); // Initiate travel planning await subgraphsPage.sendMessage("Help me plan a trip to San Francisco"); // FEATURE TEST: Wait for supervisor coordination await subgraphsPage.waitForSupervisorCoordination(); await expect(subgraphsPage.supervisorIndicator) .toBeVisible({ timeout: 10000 }) .catch(() => { console.log( "Supervisor indicator not found, verifying through content", ); }); // FEATURE TEST: Flights Agent - verify agent indicator becomes active await subgraphsPage.waitForFlightsAgent(); await expect(subgraphsPage.flightsAgentIndicator) .toBeVisible({ timeout: 10000 }) .catch(() => { console.log( "Flights agent indicator not found, checking content instead", ); }); await subgraphsPage.verifyStaticFlightData(); // FEATURE TEST: Test interrupt pause behavior - flow shouldn't auto-proceed // await expect(page.getByText(/hotel.*options|accommodation|Zephyr|Ritz-Carlton|Hotel Zoe/i)).not.toBeVisible(); // Select KLM flight through interrupt await subgraphsPage.selectFlight("KLM"); // FEATURE TEST: Verify immediate state update after selection await expect(subgraphsPage.selectedFlight) .toContainText("KLM") .catch(async () => { await expect(page.getByText(/KLM/i)).toBeVisible({ timeout: 2000 }); }); // FEATURE TEST: Hotels Agent - verify agent indicator switches await subgraphsPage.waitForHotelsAgent(); await expect(subgraphsPage.hotelsAgentIndicator) .toBeVisible({ timeout: 10000 }) .catch(() => { console.log( "Hotels agent indicator not found, checking content instead", ); }); await subgraphsPage.verifyStaticHotelData(); // FEATURE TEST: Test interrupt pause behavior again // Select Hotel Zoe through interrupt await subgraphsPage.selectHotel("Zoe"); // FEATURE TEST: Verify hotel selection immediately updates state await expect(subgraphsPage.selectedHotel) .toContainText("Zoe") .catch(async () => { await expect(page.getByText(/Hotel Zoe|Zoe/i)).toBeVisible({ timeout: 2000, }); }); // FEATURE TEST: Experiences Agent - verify agent indicator becomes active await subgraphsPage.waitForExperiencesAgent(); await expect(subgraphsPage.experiencesAgentIndicator) .toBeVisible({ timeout: 10000 }) .catch(() => { console.log( "Experiences agent indicator not found, checking content instead", ); }); await subgraphsPage.verifyStaticExperienceData(); }); test("[LangGraph] should handle different selections and demonstrate supervisor routing patterns", async ({ page, }) => { const subgraphsPage = new SubgraphsPage(page); await page.goto("/langgraph-fastapi/feature/subgraphs"); await subgraphsPage.openChat(); await subgraphsPage.sendMessage( "I want to visit San Francisco from Amsterdam", ); // FEATURE TEST: Wait for supervisor coordination await subgraphsPage.waitForSupervisorCoordination(); await expect(subgraphsPage.supervisorIndicator) .toBeVisible({ timeout: 10000 }) .catch(() => { console.log( "Supervisor indicator not found, verifying through content", ); }); // FEATURE TEST: Flights Agent - verify agent indicator becomes active await subgraphsPage.waitForFlightsAgent(); await expect(subgraphsPage.flightsAgentIndicator) .toBeVisible({ timeout: 10000 }) .catch(() => { console.log( "Flights agent indicator not found, checking content instead", ); }); await subgraphsPage.verifyStaticFlightData(); // FEATURE TEST: Test different selection - United instead of KLM await subgraphsPage.selectFlight("United"); // FEATURE TEST: Verify immediate state update after selection await expect(subgraphsPage.selectedFlight) .toContainText("United") .catch(async () => { await expect(page.getByText(/United/i)).toBeVisible({ timeout: 2000 }); }); // FEATURE TEST: Hotels Agent - verify agent indicator switches await subgraphsPage.waitForHotelsAgent(); await expect(subgraphsPage.hotelsAgentIndicator) .toBeVisible({ timeout: 10000 }) .catch(() => { console.log( "Hotels agent indicator not found, checking content instead", ); }); await subgraphsPage.verifyStaticHotelData(); // FEATURE TEST: Test interrupt pause behavior again // FEATURE TEST: Test different hotel selection - Ritz-Carlton await subgraphsPage.selectHotel("Ritz-Carlton"); // FEATURE TEST: Verify hotel selection immediately updates state await expect(subgraphsPage.selectedHotel) .toContainText("Ritz-Carlton") .catch(async () => { await expect(page.getByText(/Ritz-Carlton/i)).toBeVisible({ timeout: 2000, }); }); // FEATURE TEST: Experiences Agent - verify agent indicator becomes active await subgraphsPage.waitForExperiencesAgent(); await expect(subgraphsPage.experiencesAgentIndicator) .toBeVisible({ timeout: 10000 }) .catch(() => { console.log( "Experiences agent indicator not found, checking content instead", ); }); // FEATURE TEST: Verify subgraph streaming detection - experiences agent is active await expect(subgraphsPage.experiencesAgentIndicator) .toHaveClass(/active/) .catch(() => { console.log("Experiences agent not active, checking content instead"); }); // FEATURE TEST: Verify complete state persistence across all agents await expect(subgraphsPage.selectedFlight).toContainText("United"); // Flight selection persisted await expect(subgraphsPage.selectedHotel).toContainText("Ritz-Carlton"); // Hotel selection persisted await subgraphsPage.verifyStaticExperienceData(); // Experiences provided based on selections }); }); ================================================ FILE: apps/dojo/e2e/tests/langgraphFastAPITests/toolBasedGenUIPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { ToolBaseGenUIPage } from "../../featurePages/ToolBaseGenUIPage"; const pageURL = "/langgraph-fastapi/feature/tool_based_generative_ui"; test("[LangGraph FastAPI] 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("[LangGraph FastAPI] 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); }); ================================================ FILE: apps/dojo/e2e/tests/langgraphFastAPITests/v1AgenticChatPage.spec.ts ================================================ import { test } from "../../test-isolation-helper"; import { V1AgenticChatPage } from "../../featurePages/V1AgenticChatPage"; test("[V1] LangGraph FastAPI sends and receives a message", async ({ page, }) => { await page.goto("/langgraph-fastapi/feature/v1_agentic_chat"); const chat = new V1AgenticChatPage(page); await chat.sendMessage("Hi"); await chat.assertUserMessageVisible("Hi"); await chat.assertAgentReplyVisible(/Hello|Hi|hey|help|assist/i); }); ================================================ FILE: apps/dojo/e2e/tests/langgraphPythonTests/agenticChatPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { AgenticChatPage } from "../../featurePages/AgenticChatPage"; test("[LangGraph] Agentic Chat sends and receives a message", async ({ page, }) => { await page.goto("/langgraph/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|Hi|Hey|Greetings|nice to meet|welcome/i, ); }); test("[LangGraph] Agentic Chat changes background on message and reset", async ({ page, }) => { await page.goto("/langgraph/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(); // 1. Send message to change background to blue 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(); // 2. Change to pink 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); const backgroundAfterPink = await getBackground(); // Verify it also differs from initial (not a reset) expect(backgroundAfterPink).not.toBe(initialBackground); }); test("[LangGraph] Agentic Chat retains memory of user messages during a conversation", async ({ page, }) => { await page.goto("/langgraph/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( /how can I|help|assist|what can I do|what would you like/i, ); const favFruit = "Mango"; await chat.sendMessage(`My favorite fruit is ${favFruit}`); await chat.assertUserMessageVisible(`My favorite fruit is ${favFruit}`); await chat.assertAgentReplyVisible(new RegExp(favFruit, "i")); await chat.sendMessage("and I love listening to Kaavish"); await chat.assertUserMessageVisible("and I love listening to Kaavish"); await chat.assertAgentReplyVisible(/Kaavish/i); await chat.sendMessage("tell me an interesting fact about Moon"); await chat.assertUserMessageVisible("tell me an interesting fact about Moon"); await chat.assertAgentReplyVisible(/Moon/i); 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(new RegExp(favFruit, "i")); }); // Skip: CopilotChat v2 does not wire up onRegenerate to assistant messages, // so the regenerate button is not rendered. Requires framework-level change. test.skip("[LangGraph] Agentic Chat regenerates a response", async ({ page, }) => { await page.goto("/langgraph/feature/agentic_chat"); const chat = new AgenticChatPage(page); await chat.openChat(); await expect(chat.agentGreeting).toBeVisible(); // Send messages using page object (now uses sendChatMessage + awaitLLMResponseDone) await chat.sendMessage("tell me a joke"); // Greeting is not a copilot-assistant-message, so joke reply is at index 0 const jokeIndex = 0; await chat.getAssistantMessageText(jokeIndex); // Send a filler so the joke is not the last message await chat.sendMessage("say hello"); // Regenerate the joke response await chat.regenerateResponse(jokeIndex); await page.waitForFunction( () => document.querySelector('[data-copilot-running="false"]') !== null, null, { timeout: 15000 }, ); const newJoke = await chat.getAssistantMessageText(jokeIndex); expect(newJoke.length).toBeGreaterThan(0); }); ================================================ FILE: apps/dojo/e2e/tests/langgraphPythonTests/agenticGenUI.spec.ts ================================================ import { awaitLLMResponseDone } from "../../utils/copilot-actions"; import { test, expect } from "../../test-isolation-helper"; import { AgenticGenUIPage } from "../../pages/langGraphPages/AgenticUIGenPage"; test.describe("Agent Generative UI Feature", () => { test("[LangGraph] should interact with the chat to get a planner on prompt", async ({ page, }) => { const genUIAgent = new AgenticGenUIPage(page); await page.goto("/langgraph/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("[LangGraph] should interact with the chat using predefined prompts and perform steps", async ({ page, }) => { const genUIAgent = new AgenticGenUIPage(page); await page.goto("/langgraph/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); }); }); ================================================ FILE: apps/dojo/e2e/tests/langgraphPythonTests/backendToolRenderingPage.spec.ts ================================================ import { test, expect } from "@playwright/test"; test("[LanggraphPython] Backend Tool Rendering displays weather cards", async ({ page }) => { // Set shorter default timeout for this test test.setTimeout(30000); // 30 seconds total await page.goto("/langgraph/feature/backend_tool_rendering"); // Verify suggestion buttons are visible await expect(page.getByRole("button", { name: "Weather in San Francisco" })).toBeVisible({ timeout: 5000, }); // Click first suggestion and verify weather card appears await page.getByRole("button", { name: "Weather in San Francisco" }).click(); // Wait for either test ID or fallback to "Current Weather" text const weatherCard = page.getByTestId("weather-card"); const currentWeatherText = page.getByText("Current Weather"); // Try test ID first, fallback to text try { await expect(weatherCard).toBeVisible(); } catch (e) { // Fallback to checking for "Current Weather" text await expect(currentWeatherText.first()).toBeVisible(); } // Verify weather content is present (use flexible selectors) 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); // At least one of these should be true expect(hasHumidity || hasWind || hasCityName).toBeTruthy(); // Click second suggestion await page.getByRole("button", { name: "Weather in New York" }).click(); await page.waitForTimeout(2000); // Verify at least one weather-related element is still visible const weatherElements = await page.getByText(/Weather|Humidity|Wind|Temperature/i).count(); expect(weatherElements).toBeGreaterThan(0); }); ================================================ FILE: apps/dojo/e2e/tests/langgraphPythonTests/humanInTheLoopPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { HumanInLoopPage } from "../../pages/langGraphPages/HumanInLoopPage"; test.describe("Human in the Loop Feature", () => { test("[LangGraph] should interact with the chat and perform steps", async ({ page, }) => { const humanInLoop = new HumanInLoopPage(page); await page.goto("/langgraph/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}? \u26a0\ufe0f Reply with only words 'Yes' or 'No' (no explanation, no punctuation).`, ); }); test("should interact with the chat using predefined prompts and perform steps", async ({ page, }) => { const humanInLoop = new HumanInLoopPage(page); await page.goto("/langgraph/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}? \u26a0\ufe0f Reply with only words 'Yes' or 'No' (no explanation, no punctuation).`, ); }); }); ================================================ FILE: apps/dojo/e2e/tests/langgraphPythonTests/predictiveStateUpdatePage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { PredictiveStateUpdatesPage } from "../../pages/langGraphPages/PredictiveStateUpdatesPage"; test.describe("Predictive Status Updates Feature", () => { test("[LangGraph] should interact with agent and approve asked changes", async ({ page, }) => { const predictiveStateUpdates = new PredictiveStateUpdatesPage(page); await page.goto("/langgraph/feature/predictive_state_updates"); await predictiveStateUpdates.openChat(); await predictiveStateUpdates.sendMessage( "Give me a story for a dragon called Atlantis in document", ); await predictiveStateUpdates.getPredictiveResponse(); await predictiveStateUpdates.getUserApproval(); await expect(predictiveStateUpdates.confirmedChangesResponse).toBeVisible(); const dragonName = await predictiveStateUpdates.verifyAgentResponse("Atlantis"); expect(dragonName).not.toBeNull(); await predictiveStateUpdates.sendMessage("Change dragon name to Lola"); await predictiveStateUpdates.verifyHighlightedText(); await predictiveStateUpdates.getUserApproval(); await expect(predictiveStateUpdates.confirmedChangesResponse).toBeVisible(); const dragonNameNew = await predictiveStateUpdates.verifyAgentResponse("Lola"); expect(dragonNameNew).not.toBe(dragonName); }); test("[LangGraph] should interact with agent and reject asked changes", async ({ page, }) => { const predictiveStateUpdates = new PredictiveStateUpdatesPage(page); await page.goto("/langgraph/feature/predictive_state_updates"); await predictiveStateUpdates.openChat(); await predictiveStateUpdates.sendMessage( "Give me a story for a dragon called Atlantis in document", ); await predictiveStateUpdates.getPredictiveResponse(); await predictiveStateUpdates.getUserApproval(); await expect(predictiveStateUpdates.confirmedChangesResponse).toBeVisible(); const dragonName = await predictiveStateUpdates.verifyAgentResponse("Atlantis"); expect(dragonName).not.toBeNull(); await predictiveStateUpdates.sendMessage("Change dragon name to Lola"); await predictiveStateUpdates.verifyHighlightedText(); await predictiveStateUpdates.getUserRejection(); await expect(predictiveStateUpdates.rejectedChangesResponse).toBeVisible(); const dragonNameAfterRejection = await predictiveStateUpdates.verifyAgentResponse("Atlantis"); expect(dragonNameAfterRejection).toBe(dragonName); expect(dragonNameAfterRejection).not.toBe("Lola"); }); }); ================================================ FILE: apps/dojo/e2e/tests/langgraphPythonTests/sharedStatePage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { SharedStatePage } from "../../featurePages/SharedStatePage"; test.describe("Shared State Feature", () => { test("[LangGraph] should interact with the chat to get a recipe on prompt", async ({ page, }) => { const sharedStateAgent = new SharedStatePage(page); // Update URL to new domain await page.goto("/langgraph/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"', ); await sharedStateAgent.loader(); await sharedStateAgent.awaitIngredientCard("Pasta"); await sharedStateAgent.getInstructionItems( sharedStateAgent.instructionsContainer, ); }); test("[LangGraph] should share state between UI and chat", async ({ page, }) => { const sharedStateAgent = new SharedStatePage(page); await page.goto("/langgraph/feature/shared_state"); await sharedStateAgent.openChat(); // Add new ingredient via UI await sharedStateAgent.addIngredient.click(); // Fill in the new ingredient details const newIngredientCard = page.locator(".ingredient-card").last(); await newIngredientCard.locator(".ingredient-name-input").fill("Potatoes"); await newIngredientCard.locator(".ingredient-amount-input").fill("12"); // Wait for UI to update await page.waitForTimeout(1000); // Ask chat for all ingredients await sharedStateAgent.sendMessage("Give me all the ingredients"); await sharedStateAgent.loader(); // Verify chat response includes both existing and new ingredients await expect( sharedStateAgent.agentMessage.getByText(/Potatoes/i), ).toBeVisible(); await expect(sharedStateAgent.agentMessage.getByText(/12/)).toBeVisible(); await expect( sharedStateAgent.agentMessage.getByText(/Carrots/i), ).toBeVisible(); await expect( sharedStateAgent.agentMessage.getByText( /All-Purpose Flour|all.purpose flour/i, ), ).toBeVisible(); }); }); ================================================ FILE: apps/dojo/e2e/tests/langgraphPythonTests/subgraphsPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { SubgraphsPage } from "../../pages/langGraphPages/SubgraphsPage"; test.describe("Subgraphs Travel Agent Feature", () => { test("[LangGraph] should complete full travel planning flow with feature validation", async ({ page, }) => { const subgraphsPage = new SubgraphsPage(page); await page.goto("/langgraph/feature/subgraphs"); await subgraphsPage.openChat(); // Initiate travel planning await subgraphsPage.sendMessage("Help me plan a trip to San Francisco"); // FEATURE TEST: Wait for supervisor coordination await subgraphsPage.waitForSupervisorCoordination(); await expect(subgraphsPage.supervisorIndicator) .toBeVisible({ timeout: 10000 }) .catch(() => { console.log( "Supervisor indicator not found, verifying through content", ); }); // FEATURE TEST: Flights Agent - verify agent indicator becomes active await subgraphsPage.waitForFlightsAgent(); await expect(subgraphsPage.flightsAgentIndicator) .toBeVisible({ timeout: 10000 }) .catch(() => { console.log( "Flights agent indicator not found, checking content instead", ); }); await subgraphsPage.verifyStaticFlightData(); // FEATURE TEST: Test interrupt pause behavior - flow shouldn't auto-proceed // await expect(page.getByText(/hotel.*options|accommodation|Zephyr|Ritz-Carlton|Hotel Zoe/i)).not.toBeVisible(); // Select KLM flight through interrupt await subgraphsPage.selectFlight("KLM"); // FEATURE TEST: Verify immediate state update after selection await expect(subgraphsPage.selectedFlight) .toContainText("KLM") .catch(async () => { await expect(page.getByText(/KLM/i)).toBeVisible({ timeout: 2000 }); }); // FEATURE TEST: Hotels Agent - verify agent indicator switches await subgraphsPage.waitForHotelsAgent(); await expect(subgraphsPage.hotelsAgentIndicator) .toBeVisible({ timeout: 10000 }) .catch(() => { console.log( "Hotels agent indicator not found, checking content instead", ); }); await subgraphsPage.verifyStaticHotelData(); // FEATURE TEST: Test interrupt pause behavior again // Select Hotel Zoe through interrupt await subgraphsPage.selectHotel("Zoe"); // FEATURE TEST: Verify hotel selection immediately updates state await expect(subgraphsPage.selectedHotel) .toContainText("Zoe") .catch(async () => { await expect(page.getByText(/Hotel Zoe|Zoe/i)).toBeVisible({ timeout: 2000, }); }); // FEATURE TEST: Experiences Agent - verify agent indicator becomes active await subgraphsPage.waitForExperiencesAgent(); await expect(subgraphsPage.experiencesAgentIndicator) .toBeVisible({ timeout: 10000 }) .catch(() => { console.log( "Experiences agent indicator not found, checking content instead", ); }); await subgraphsPage.verifyStaticExperienceData(); }); test("[LangGraph] should handle different selections and demonstrate supervisor routing patterns", async ({ page, }) => { const subgraphsPage = new SubgraphsPage(page); await page.goto("/langgraph/feature/subgraphs"); await subgraphsPage.openChat(); await subgraphsPage.sendMessage( "I want to visit San Francisco from Amsterdam", ); // FEATURE TEST: Wait for supervisor coordination await subgraphsPage.waitForSupervisorCoordination(); await expect(subgraphsPage.supervisorIndicator) .toBeVisible({ timeout: 10000 }) .catch(() => { console.log( "Supervisor indicator not found, verifying through content", ); }); // FEATURE TEST: Flights Agent - verify agent indicator becomes active await subgraphsPage.waitForFlightsAgent(); await expect(subgraphsPage.flightsAgentIndicator) .toBeVisible({ timeout: 10000 }) .catch(() => { console.log( "Flights agent indicator not found, checking content instead", ); }); await subgraphsPage.verifyStaticFlightData(); // FEATURE TEST: Test different selection - United instead of KLM await subgraphsPage.selectFlight("United"); // FEATURE TEST: Verify immediate state update after selection await expect(subgraphsPage.selectedFlight) .toContainText("United") .catch(async () => { await expect(page.getByText(/United/i)).toBeVisible({ timeout: 2000 }); }); // FEATURE TEST: Hotels Agent - verify agent indicator switches await subgraphsPage.waitForHotelsAgent(); await expect(subgraphsPage.hotelsAgentIndicator) .toBeVisible({ timeout: 10000 }) .catch(() => { console.log( "Hotels agent indicator not found, checking content instead", ); }); // FEATURE TEST: Test different hotel selection - Ritz-Carlton await subgraphsPage.selectHotel("Ritz-Carlton"); // FEATURE TEST: Verify hotel selection immediately updates state await expect(subgraphsPage.selectedHotel) .toContainText("Ritz-Carlton") .catch(async () => { await expect(page.getByText(/Ritz-Carlton/i)).toBeVisible({ timeout: 2000, }); }); // FEATURE TEST: Experiences Agent - verify agent indicator becomes active await subgraphsPage.waitForExperiencesAgent(); await expect(subgraphsPage.experiencesAgentIndicator) .toBeVisible({ timeout: 10000 }) .catch(() => { console.log( "Experiences agent indicator not found, checking content instead", ); }); // FEATURE TEST: Verify subgraph streaming detection - experiences agent is active await expect(subgraphsPage.experiencesAgentIndicator) .toHaveClass(/active/) .catch(() => { console.log("Experiences agent not active, checking content instead"); }); // FEATURE TEST: Verify complete state persistence across all agents await expect(subgraphsPage.selectedFlight).toContainText("United"); // Flight selection persisted await expect(subgraphsPage.selectedHotel).toContainText("Ritz-Carlton"); // Hotel selection persisted await subgraphsPage.verifyStaticExperienceData(); // Experiences provided based on selections }); }); ================================================ FILE: apps/dojo/e2e/tests/langgraphPythonTests/toolBasedGenUIPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { ToolBaseGenUIPage } from "../../featurePages/ToolBaseGenUIPage"; const pageURL = "/langgraph/feature/tool_based_generative_ui"; test("[LangGraph] 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("[LangGraph] 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); }); ================================================ FILE: apps/dojo/e2e/tests/langgraphPythonTests/v1AgenticChatPage.spec.ts ================================================ import { test } from "../../test-isolation-helper"; import { V1AgenticChatPage } from "../../featurePages/V1AgenticChatPage"; test("[V1] LangGraph Python sends and receives a message", async ({ page }) => { await page.goto("/langgraph/feature/v1_agentic_chat"); const chat = new V1AgenticChatPage(page); await chat.sendMessage("Hi"); await chat.assertUserMessageVisible("Hi"); await chat.assertAgentReplyVisible(/Hello|Hi|hey|help|assist/i); }); ================================================ FILE: apps/dojo/e2e/tests/langgraphTypescriptTests/agenticChatDeterministic.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { AgenticChatPage } from "../../featurePages/AgenticChatPage"; import { MockAgent } from "../../lib/mock-agent"; /** * Deterministic versions of the flaky agentic chat tests. * * These tests verify UI behavior (background color changes, regenerate) * using a mock agent that returns predetermined SSE responses instead of * hitting a live LLM. This eliminates flakiness from: * - LLM not calling the expected tool * - LLM responding too slowly * - LLM producing identical output on regenerate * * The live-LLM versions of these tests still exist in agenticChatPage.spec.ts * and test the full integration path. These deterministic tests verify the * UI correctly responds to agent events. */ test.describe("Deterministic Agentic Chat", () => { test("[LangGraph] Background color changes via tool call", async ({ page, }) => { const mock = new MockAgent(page); // Configure deterministic responses for color change requests. // { once: true } is required because CopilotKit uses a multi-run pattern // for frontend tools: Run 1 delivers the tool call events, CopilotKit // executes the handler locally, then makes a follow-up request with the // same user message. The follow-up falls through to the fallback. mock.onMessage( "background color to blue", mock.toolCall("change_background", { background: "blue" }), { once: true } ); mock.onMessage( "background color to pink", mock.toolCall("change_background", { background: "pink" }), { once: true } ); // Fallback handles CopilotKit's follow-up requests after tool execution mock.onAnyMessage( mock.textMessage("Done! I've changed the background color for you.") ); await mock.install(); await page.goto("/langgraph-typescript/feature/agentic_chat"); const chat = new AgenticChatPage(page); await chat.openChat(); // Get initial background const backgroundContainer = page.locator( '[data-testid="background-container"]' ); const initialBackground = await backgroundContainer.evaluate( (el) => getComputedStyle(el).backgroundColor ); // Send blue color change request await chat.sendMessage("Hi change the background color to blue"); await chat.assertUserMessageVisible( "Hi change the background color to blue" ); // Wait for tool call to be processed and background to update await expect .poll( async () => { const current = await backgroundContainer.evaluate( (el) => getComputedStyle(el).backgroundColor ); return current !== initialBackground; }, { message: "Background color should change after tool call", timeout: 30_000, intervals: [500, 1000, 2000, 3000], } ) .toBeTruthy(); const blueBackground = await backgroundContainer.evaluate( (el) => getComputedStyle(el).backgroundColor ); // Send pink color change request await chat.sendMessage("Hi change the background color to pink"); await chat.assertUserMessageVisible( "Hi change the background color to pink" ); await expect .poll( async () => { const current = await backgroundContainer.evaluate( (el) => getComputedStyle(el).backgroundColor ); return current !== blueBackground; }, { message: "Background color should change from blue to pink", timeout: 30_000, intervals: [500, 1000, 2000, 3000], } ) .toBeTruthy(); const pinkBackground = await backgroundContainer.evaluate( (el) => getComputedStyle(el).backgroundColor ); expect(pinkBackground).not.toBe(initialBackground); await mock.uninstall(); }); // CopilotChat v2 does not wire up onRegenerate to assistant messages, // so the regenerate button is not rendered. test.skip("[LangGraph] Regenerate produces a new response", async ({ page }) => { const mock = new MockAgent(page); const jokes = [ "Why did the scarecrow win an award? Because he was outstanding in his field!", "What do you call a bear with no teeth? A gummy bear!", ]; // First greeting mock.onMessage(/hello/i, mock.textMessage("Hello! How can I help you today?")); // Joke request — returns first joke mock.onMessage(/joke/i, mock.textMessage(jokes[0]!), { once: true }); // Name request mock.onMessage(/name/i, mock.textMessage("How about the name Alexander?")); // Fallback for regeneration — returns a different joke mock.onAnyMessage(mock.textMessage(jokes[1]!)); await mock.install(); await page.goto("/langgraph-typescript/feature/agentic_chat"); const chat = new AgenticChatPage(page); await chat.openChat(); await expect(chat.agentGreeting).toBeVisible(); // Send first message await chat.sendMessage("Hello agent"); await page.waitForTimeout(3000); // Ask for a joke await chat.sendMessage("tell me a joke"); await page.waitForTimeout(3000); const originalJoke = await chat.getAssistantMessageText(2); expect(originalJoke.length).toBeGreaterThan(0); // Send another message await chat.sendMessage("provide a random person's name"); await page.waitForTimeout(3000); // Regenerate the joke await chat.regenerateResponse(2); await page.waitForTimeout(3000); // With mock agent, the regenerated response should be the fallback // (different joke), proving the regenerate mechanism works. // Unlike live-LLM tests, we CAN assert the text differs because // the mock returns deterministic, distinct responses. const newJoke = await chat.getAssistantMessageText(2); expect(newJoke.length).toBeGreaterThan(0); expect(newJoke).not.toBe(originalJoke); await mock.uninstall(); }); }); ================================================ FILE: apps/dojo/e2e/tests/langgraphTypescriptTests/agenticChatPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { AgenticChatPage } from "../../featurePages/AgenticChatPage"; import { sendChatMessage, awaitLLMResponseDone, } from "../../utils/copilot-actions"; test("[LangGraph] Agentic Chat sends and receives a message", async ({ page, }) => { await page.goto("/langgraph-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! How can I assist you today\?/, ); }); test("[LangGraph] Agentic Chat changes background on message and reset", async ({ page, }) => { await page.goto("/langgraph-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(); // 1. Send message to change background to blue 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(); // 2. Change to pink 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("[LangGraph] Agentic Chat retains memory of user messages during a conversation", async ({ page, }) => { await page.goto("/langgraph-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(/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!/); }); // v2 doesn't support regenerating messages yet, so skipping this test for now test.skip("[LangGraph Typescript] Agentic Chat regenerates a response", async ({ page, }) => { await page.goto("/langgraph-typescript/feature/agentic_chat"); const chat = new AgenticChatPage(page); await chat.openChat(); await expect(chat.agentGreeting).toBeVisible(); // Use sendChatMessage + awaitLLMResponseDone to save time budget // vs sendAndAwaitResponse (avoids double-waiting on assistant message count). // greeting=0, joke reply=1, filler reply=2 await sendChatMessage(page, "tell me a joke"); await awaitLLMResponseDone(page); const originalJoke = await chat.getAssistantMessageText(1); // Send a filler so the joke is not the last message await sendChatMessage(page, "say hello"); await awaitLLMResponseDone(page); // Regenerate the joke response (index 1) await chat.regenerateResponse(1); await page.waitForFunction( () => document.querySelector('[data-copilot-running="false"]') !== null, null, { timeout: 15000 }, ); const newJoke = await chat.getAssistantMessageText(1); expect(newJoke.length).toBeGreaterThan(0); }); ================================================ FILE: apps/dojo/e2e/tests/langgraphTypescriptTests/agenticGenUI.spec.ts ================================================ import { awaitLLMResponseDone } from "../../utils/copilot-actions"; import { test, expect } from "../../test-isolation-helper"; import { AgenticGenUIPage } from "../../pages/langGraphPages/AgenticUIGenPage"; test.describe("Agent Generative UI Feature", () => { test("[LangGraph] should interact with the chat to get a planner on prompt", async ({ page, }) => { const genUIAgent = new AgenticGenUIPage(page); await page.goto("/langgraph-typescript/feature/agentic_generative_ui"); await genUIAgent.openChat(); await genUIAgent.sendMessage("Hi"); await genUIAgent.assertAgentReplyVisible( /Hello! How can I assist you today\?/, ); await genUIAgent.sendMessage("Give me a plan to make brownies"); await expect(genUIAgent.agentPlannerContainer).toBeVisible(); await genUIAgent.plan(); await awaitLLMResponseDone(page); }); test("[LangGraph] should interact with the chat using predefined prompts and perform steps", async ({ page, }) => { const genUIAgent = new AgenticGenUIPage(page); await page.goto("/langgraph-typescript/feature/agentic_generative_ui"); await genUIAgent.openChat(); await genUIAgent.sendMessage("Hi"); await genUIAgent.assertAgentReplyVisible( /Hello! How can I assist you today\?/, ); await genUIAgent.sendMessage("Go to Mars"); await expect(genUIAgent.agentPlannerContainer).toBeVisible(); await genUIAgent.plan(); await awaitLLMResponseDone(page); }); }); ================================================ FILE: apps/dojo/e2e/tests/langgraphTypescriptTests/humanInTheLoopPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { HumanInLoopPage } from "../../pages/langGraphPages/HumanInLoopPage"; test.describe("Human in the Loop Feature", () => { test("[LangGraph] should interact with the chat and perform steps", async ({ page, }) => { const humanInLoop = new HumanInLoopPage(page); await page.goto("/langgraph-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("should interact with the chat using predefined prompts and perform steps", async ({ page, }) => { const humanInLoop = new HumanInLoopPage(page); await page.goto("/langgraph-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).`, ); }); }); ================================================ FILE: apps/dojo/e2e/tests/langgraphTypescriptTests/predictiveStateUpdatePage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { PredictiveStateUpdatesPage } from "../../pages/langGraphPages/PredictiveStateUpdatesPage"; test.describe("Predictive Status Updates Feature", () => { test("[LangGraph] should interact with agent and approve asked changes", async ({ page, }) => { const predictiveStateUpdates = new PredictiveStateUpdatesPage(page); await page.goto("/langgraph-typescript/feature/predictive_state_updates"); await predictiveStateUpdates.openChat(); await page.waitForTimeout(2000); await predictiveStateUpdates.sendMessage( "Give me a story for a dragon called Atlantis in document", ); await page.waitForTimeout(2000); await predictiveStateUpdates.getPredictiveResponse(); await predictiveStateUpdates.getUserApproval(); await expect(predictiveStateUpdates.confirmedChangesResponse).toBeVisible(); const dragonName = await predictiveStateUpdates.verifyAgentResponse("Atlantis"); expect(dragonName).not.toBeNull(); await page.waitForTimeout(3000); await predictiveStateUpdates.sendMessage("Change dragon name to Lola"); await page.waitForTimeout(2000); await predictiveStateUpdates.verifyHighlightedText(); await predictiveStateUpdates.getUserApproval(); await expect(predictiveStateUpdates.confirmedChangesResponse).toBeVisible(); const dragonNameNew = await predictiveStateUpdates.verifyAgentResponse("Lola"); expect(dragonNameNew).not.toBe(dragonName); }); test("[LangGraph] should interact with agent and reject asked changes", async ({ page, }) => { const predictiveStateUpdates = new PredictiveStateUpdatesPage(page); await page.goto("/langgraph-typescript/feature/predictive_state_updates"); await predictiveStateUpdates.openChat(); await page.waitForTimeout(2000); await predictiveStateUpdates.sendMessage( "Give me a story for a dragon called Atlantis in document", ); await page.waitForTimeout(2000); await predictiveStateUpdates.getPredictiveResponse(); await predictiveStateUpdates.getUserApproval(); await expect(predictiveStateUpdates.confirmedChangesResponse).toBeVisible(); const dragonName = await predictiveStateUpdates.verifyAgentResponse("Atlantis"); expect(dragonName).not.toBeNull(); await page.waitForTimeout(3000); await predictiveStateUpdates.sendMessage("Change dragon name to Lola"); await page.waitForTimeout(2000); await predictiveStateUpdates.verifyHighlightedText(); await predictiveStateUpdates.getUserRejection(); await expect(predictiveStateUpdates.rejectedChangesResponse).toBeVisible(); const dragonNameAfterRejection = await predictiveStateUpdates.verifyAgentResponse("Atlantis"); expect(dragonNameAfterRejection).toBe(dragonName); expect(dragonNameAfterRejection).not.toBe("Lola"); }); }); ================================================ FILE: apps/dojo/e2e/tests/langgraphTypescriptTests/sharedStatePage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { SharedStatePage } from "../../featurePages/SharedStatePage"; test.describe("Shared State Feature", () => { test("[LangGraph] should interact with the chat to get a recipe on prompt", async ({ page, }) => { const sharedStateAgent = new SharedStatePage(page); // Update URL to new domain await page.goto("/langgraph-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"', ); await sharedStateAgent.loader(); await sharedStateAgent.awaitIngredientCard("Pasta"); await sharedStateAgent.getInstructionItems( sharedStateAgent.instructionsContainer, ); }); test("[LangGraph] should share state between UI and chat", async ({ page, }) => { const sharedStateAgent = new SharedStatePage(page); await page.goto("/langgraph-typescript/feature/shared_state"); await sharedStateAgent.openChat(); // Add new ingredient via UI await sharedStateAgent.addIngredient.click(); // Fill in the new ingredient details const newIngredientCard = page.locator(".ingredient-card").last(); await newIngredientCard.locator(".ingredient-name-input").fill("Potatoes"); await newIngredientCard.locator(".ingredient-amount-input").fill("12"); // Wait for UI to update await page.waitForTimeout(1000); // Ask chat for all ingredients await sharedStateAgent.sendMessage("Give me all the ingredients"); await sharedStateAgent.loader(); // Verify chat response includes both existing and new ingredients 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(); }); }); ================================================ FILE: apps/dojo/e2e/tests/langgraphTypescriptTests/subgraphsPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { SubgraphsPage } from "../../pages/langGraphPages/SubgraphsPage"; test.describe("Subgraphs Travel Agent Feature", () => { test("[LangGraph] should complete full travel planning flow with feature validation", async ({ page, }) => { const subgraphsPage = new SubgraphsPage(page); await page.goto("/langgraph-typescript/feature/subgraphs"); await subgraphsPage.openChat(); // Initiate travel planning await subgraphsPage.sendMessage("Help me plan a trip to San Francisco"); // FEATURE TEST: Wait for supervisor coordination await subgraphsPage.waitForSupervisorCoordination(); await expect(subgraphsPage.supervisorIndicator) .toBeVisible({ timeout: 10000 }) .catch(() => { console.log( "Supervisor indicator not found, verifying through content", ); }); // FEATURE TEST: Flights Agent - verify agent indicator becomes active await subgraphsPage.waitForFlightsAgent(); await expect(subgraphsPage.flightsAgentIndicator) .toBeVisible({ timeout: 10000 }) .catch(() => { console.log( "Flights agent indicator not found, checking content instead", ); }); await subgraphsPage.verifyStaticFlightData(); // FEATURE TEST: Test interrupt pause behavior - flow shouldn't auto-proceed await page.waitForTimeout(3000); // await expect(page.getByText(/hotel.*options|accommodation|Zephyr|Ritz-Carlton|Hotel Zoe/i)).not.toBeVisible(); // Select KLM flight through interrupt await subgraphsPage.selectFlight("KLM"); // FEATURE TEST: Verify immediate state update after selection await expect(subgraphsPage.selectedFlight) .toContainText("KLM") .catch(async () => { await expect(page.getByText(/KLM/i)).toBeVisible({ timeout: 2000 }); }); // FEATURE TEST: Hotels Agent - verify agent indicator switches await subgraphsPage.waitForHotelsAgent(); await expect(subgraphsPage.hotelsAgentIndicator) .toBeVisible({ timeout: 10000 }) .catch(() => { console.log( "Hotels agent indicator not found, checking content instead", ); }); await subgraphsPage.verifyStaticHotelData(); // FEATURE TEST: Test interrupt pause behavior again await page.waitForTimeout(3000); // Select Hotel Zoe through interrupt await subgraphsPage.selectHotel("Zoe"); // FEATURE TEST: Verify hotel selection immediately updates state await expect(subgraphsPage.selectedHotel) .toContainText("Zoe") .catch(async () => { await expect(page.getByText(/Hotel Zoe|Zoe/i)).toBeVisible({ timeout: 2000, }); }); // FEATURE TEST: Experiences Agent - verify agent indicator becomes active await subgraphsPage.waitForExperiencesAgent(); await expect(subgraphsPage.experiencesAgentIndicator) .toBeVisible({ timeout: 10000 }) .catch(() => { console.log( "Experiences agent indicator not found, checking content instead", ); }); await subgraphsPage.verifyStaticExperienceData(); }); test("[LangGraph] should handle different selections and demonstrate supervisor routing patterns", async ({ page, }) => { const subgraphsPage = new SubgraphsPage(page); await page.goto("/langgraph-typescript/feature/subgraphs"); await subgraphsPage.openChat(); await subgraphsPage.sendMessage( "I want to visit San Francisco from Amsterdam", ); // FEATURE TEST: Wait for supervisor coordination await subgraphsPage.waitForSupervisorCoordination(); await expect(subgraphsPage.supervisorIndicator) .toBeVisible({ timeout: 10000 }) .catch(() => { console.log( "Supervisor indicator not found, verifying through content", ); }); // FEATURE TEST: Flights Agent - verify agent indicator becomes active await subgraphsPage.waitForFlightsAgent(); await expect(subgraphsPage.flightsAgentIndicator) .toBeVisible({ timeout: 10000 }) .catch(() => { console.log( "Flights agent indicator not found, checking content instead", ); }); await subgraphsPage.verifyStaticFlightData(); // FEATURE TEST: Test different selection - United instead of KLM await subgraphsPage.selectFlight("United"); // FEATURE TEST: Verify immediate state update after selection await expect(subgraphsPage.selectedFlight) .toContainText("United") .catch(async () => { await expect(page.getByText(/United/i)).toBeVisible({ timeout: 2000 }); }); // FEATURE TEST: Hotels Agent - verify agent indicator switches await subgraphsPage.waitForHotelsAgent(); await expect(subgraphsPage.hotelsAgentIndicator) .toBeVisible({ timeout: 10000 }) .catch(() => { console.log( "Hotels agent indicator not found, checking content instead", ); }); // FEATURE TEST: Test different hotel selection - Ritz-Carlton await subgraphsPage.selectHotel("Ritz-Carlton"); // FEATURE TEST: Verify hotel selection immediately updates state await expect(subgraphsPage.selectedHotel) .toContainText("Ritz-Carlton") .catch(async () => { await expect(page.getByText(/Ritz-Carlton/i)).toBeVisible({ timeout: 2000, }); }); // FEATURE TEST: Experiences Agent - verify agent indicator becomes active await subgraphsPage.waitForExperiencesAgent(); await expect(subgraphsPage.experiencesAgentIndicator) .toBeVisible({ timeout: 10000 }) .catch(() => { console.log( "Experiences agent indicator not found, checking content instead", ); }); // FEATURE TEST: Verify subgraph streaming detection - experiences agent is active await expect(subgraphsPage.experiencesAgentIndicator) .toHaveClass(/active/) .catch(() => { console.log("Experiences agent not active, checking content instead"); }); // FEATURE TEST: Verify complete state persistence across all agents await expect(subgraphsPage.selectedFlight).toContainText("United"); // Flight selection persisted await expect(subgraphsPage.selectedHotel).toContainText("Ritz-Carlton"); // Hotel selection persisted await subgraphsPage.verifyStaticExperienceData(); // Experiences provided based on selections }); }); ================================================ FILE: apps/dojo/e2e/tests/langgraphTypescriptTests/toolBasedGenUIPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { ToolBaseGenUIPage } from "../../featurePages/ToolBaseGenUIPage"; const pageURL = "/langgraph-typescript/feature/tool_based_generative_ui"; test("[LangGraph] 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("[LangGraph] 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); }); ================================================ FILE: apps/dojo/e2e/tests/langgraphTypescriptTests/v1AgenticChatPage.spec.ts ================================================ import { test } from "../../test-isolation-helper"; import { V1AgenticChatPage } from "../../featurePages/V1AgenticChatPage"; test("[V1] LangGraph TypeScript sends and receives a message", async ({ page, }) => { await page.goto("/langgraph-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\?/); }); ================================================ FILE: apps/dojo/e2e/tests/langroidTests/agenticChatPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { AgenticChatPage } from "../../featurePages/AgenticChatPage"; test("[Langroid] Agentic Chat sends and receives a message", async ({ page, }) => { await page.goto("/langroid/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/i); }); test("[Langroid] Agentic Chat changes background on message and reset", async ({ page, }) => { await page.goto("/langroid/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(); // 1. Send message to change background to blue 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(); // 2. Change to pink 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("[Langroid] Agentic Chat retains memory of user messages during a conversation", async ({ page, }) => { await page.goto("/langroid/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([/assist you/i, /help you/i]); const favFruit = "Mango"; await chat.sendMessage(`My favorite fruit is ${favFruit}`); await chat.assertUserMessageVisible(`My favorite fruit is ${favFruit}`); await chat.assertAgentReplyVisible(new RegExp(favFruit, "i")); await chat.sendMessage("and I love listening to Kaavish"); await chat.assertUserMessageVisible("and I love listening to Kaavish"); await chat.assertAgentReplyVisible(/Kaavish/i); await chat.sendMessage("tell me an interesting fact about Moon"); await chat.assertUserMessageVisible("tell me an interesting fact about Moon"); await chat.assertAgentReplyVisible(/Moon/i); 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(new RegExp(favFruit, "i")); }); ================================================ FILE: apps/dojo/e2e/tests/langroidTests/agenticGenUI.spec.ts ================================================ import { awaitLLMResponseDone } from "../../utils/copilot-actions"; import { test, expect } from "../../test-isolation-helper"; import { AgenticGenUIPage } from "../../pages/langroidPages/AgenticUIGenPage"; test.describe("Agent Generative UI Feature", () => { // Fails. Issue with integration or something. test("[Langroid] should interact with the chat to get a planner on prompt", async ({ page, }) => { const genUIAgent = new AgenticGenUIPage(page); await page.goto("/langroid/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 using your tools", ); await expect(genUIAgent.agentPlannerContainer).toBeVisible(); await genUIAgent.plan(); await awaitLLMResponseDone(page); }); // Fails. Issue with integration or something. test("[Langroid] should interact with the chat using predefined prompts and perform steps", async ({ page, }) => { const genUIAgent = new AgenticGenUIPage(page); await page.goto("/langroid/feature/agentic_generative_ui"); await genUIAgent.openChat(); await genUIAgent.sendMessage("Hi"); await genUIAgent.assertAgentReplyVisible(/Hello/); await genUIAgent.sendMessage("Go to Mars using your tools"); await expect(genUIAgent.agentPlannerContainer).toBeVisible(); await genUIAgent.plan(); await awaitLLMResponseDone(page); }); }); ================================================ FILE: apps/dojo/e2e/tests/langroidTests/backendToolRenderingPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; test("[Langroid] Backend Tool Rendering displays weather cards", async ({ page }) => { // Set shorter default timeout for this test test.setTimeout(30000); // 30 seconds total await page.goto("/langroid/feature/backend_tool_rendering"); // Verify suggestion buttons are visible await expect(page.getByRole("button", { name: "Weather in San Francisco" })).toBeVisible({ timeout: 5000, }); // Click first suggestion and verify weather card appears await page.getByRole("button", { name: "Weather in San Francisco" }).click(); // Wait for either test ID or fallback to "Current Weather" text const weatherCard = page.getByTestId("weather-card"); const currentWeatherText = page.getByText("Current Weather"); // Try test ID first, fallback to text try { await expect(weatherCard).toBeVisible(); } catch (e) { // Fallback to checking for "Current Weather" text await expect(currentWeatherText.first()).toBeVisible(); } // Verify weather content is present (use flexible selectors) 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); // At least one of these should be true expect(hasHumidity || hasWind || hasCityName).toBeTruthy(); // Click second suggestion await page.getByRole("button", { name: "Weather in New York" }).click(); await page.waitForTimeout(2000); // Verify at least one weather-related element is still visible const weatherElements = await page.getByText(/Weather|Humidity|Wind|Temperature/i).count(); expect(weatherElements).toBeGreaterThan(0); }); ================================================ FILE: apps/dojo/e2e/tests/langroidTests/sharedStatePage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { SharedStatePage } from "../../featurePages/SharedStatePage"; test.describe("Shared State Feature", () => { test("[Langroid] should interact with the chat to get a recipe on prompt", async ({ page, }) => { const sharedStateAgent = new SharedStatePage(page); // Update URL to new domain await page.goto("/langroid/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"', ); await sharedStateAgent.loader(); await sharedStateAgent.awaitIngredientCard("Pasta"); await sharedStateAgent.getInstructionItems( sharedStateAgent.instructionsContainer, ); }); test("[Langroid] should share state between UI and chat", async ({ page, }) => { const sharedStateAgent = new SharedStatePage(page); await page.goto("/langroid/feature/shared_state"); await sharedStateAgent.openChat(); // Add new ingredient via UI await sharedStateAgent.addIngredient.click(); // Fill in the new ingredient details const newIngredientCard = page.locator(".ingredient-card").last(); await newIngredientCard.locator(".ingredient-name-input").fill("Potatoes"); await newIngredientCard.locator(".ingredient-amount-input").fill("12"); // Wait for UI to update await page.waitForTimeout(1000); // Ask chat for all ingredients await sharedStateAgent.sendMessage("Give me all the ingredients"); await sharedStateAgent.loader(); // Verify chat response includes both existing and new ingredients 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(); }); }); ================================================ FILE: apps/dojo/e2e/tests/llamaIndexTests/agenticChatPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { AgenticChatPage } from "../../featurePages/AgenticChatPage"; test("[LlamaIndex] Agentic Chat sends and receives a message", async ({ page, }) => { await page.goto("/llama-index/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/i); }); test("[LlamaIndex] Agentic Chat changes background on message and reset", async ({ page, }) => { await page.goto("/llama-index/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(); // 1. Send message to change background to blue 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(); // 2. Change to pink 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("[LlamaIndex] Agentic Chat retains memory of user messages during a conversation", async ({ page, }) => { await page.goto("/llama-index/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([/assist you/i, /help you/i]); const favFruit = "Mango"; await chat.sendMessage(`My favorite fruit is ${favFruit}`); await chat.assertUserMessageVisible(`My favorite fruit is ${favFruit}`); await chat.assertAgentReplyVisible(new RegExp(favFruit, "i")); await chat.sendMessage("and I love listening to Kaavish"); await chat.assertUserMessageVisible("and I love listening to Kaavish"); await chat.assertAgentReplyVisible(/Kaavish/i); await chat.sendMessage("tell me an interesting fact about Moon"); await chat.assertUserMessageVisible("tell me an interesting fact about Moon"); await chat.assertAgentReplyVisible(/Moon/i); 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(new RegExp(favFruit, "i")); }); ================================================ FILE: apps/dojo/e2e/tests/llamaIndexTests/agenticGenUI.spec.ts ================================================ import { awaitLLMResponseDone } from "../../utils/copilot-actions"; import { test, expect } from "../../test-isolation-helper"; import { AgenticGenUIPage } from "../../pages/llamaIndexPages/AgenticUIGenPage"; test.describe("Agent Generative UI Feature", () => { // Fails. Issue with integration or something. test("[LlamaIndex] should interact with the chat to get a planner on prompt", async ({ page, }) => { const genUIAgent = new AgenticGenUIPage(page); await page.goto("/llama-index/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 using your tools", ); await expect(genUIAgent.agentPlannerContainer).toBeVisible(); await genUIAgent.plan(); await awaitLLMResponseDone(page); }); // Fails. Issue with integration or something. test("[LlamaIndex] should interact with the chat using predefined prompts and perform steps", async ({ page, }) => { const genUIAgent = new AgenticGenUIPage(page); await page.goto("/llama-index/feature/agentic_generative_ui"); await genUIAgent.openChat(); await genUIAgent.sendMessage("Hi"); await genUIAgent.assertAgentReplyVisible(/Hello/); await genUIAgent.sendMessage("Go to Mars using your tools"); await expect(genUIAgent.agentPlannerContainer).toBeVisible(); await genUIAgent.plan(); await awaitLLMResponseDone(page); }); }); ================================================ FILE: apps/dojo/e2e/tests/llamaIndexTests/backendToolRenderingPage.spec.ts ================================================ import { test, expect } from "@playwright/test"; test("[LlamaIndex] Backend Tool Rendering displays weather cards", async ({ page }) => { // Set shorter default timeout for this test test.setTimeout(30000); // 30 seconds total await page.goto("/llama-index/feature/backend_tool_rendering"); // Verify suggestion buttons are visible await expect(page.getByRole("button", { name: "Weather in San Francisco" })).toBeVisible({ timeout: 5000, }); // Click first suggestion and verify weather card appears await page.getByRole("button", { name: "Weather in San Francisco" }).click(); // Wait for either test ID or fallback to "Current Weather" text const weatherCard = page.getByTestId("weather-card"); const currentWeatherText = page.getByText("Current Weather"); // Try test ID first, fallback to text try { await expect(weatherCard).toBeVisible(); } catch (e) { // Fallback to checking for "Current Weather" text await expect(currentWeatherText.first()).toBeVisible(); } // Verify weather content is present (use flexible selectors) 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); // At least one of these should be true expect(hasHumidity || hasWind || hasCityName).toBeTruthy(); // Click second suggestion await page.getByRole("button", { name: "Weather in New York" }).click(); await page.waitForTimeout(2000); // Verify at least one weather-related element is still visible const weatherElements = await page.getByText(/Weather|Humidity|Wind|Temperature/i).count(); expect(weatherElements).toBeGreaterThan(0); }); ================================================ FILE: apps/dojo/e2e/tests/llamaIndexTests/humanInTheLoopPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { HumanInLoopPage } from "../../pages/llamaIndexPages/HumanInLoopPage"; test.describe("Human in the Loop Feature", () => { test("[LlamaIndex] should interact with the chat and perform steps", async ({ page, }) => { const humanInLoop = new HumanInLoopPage(page); await page.goto("/llama-index/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}? \u26a0\ufe0f Reply with only words 'Yes' or 'No' (no explanation, no punctuation).`, ); }); test("[LlamaIndex] should interact with the chat using predefined prompts and perform steps", async ({ page, }) => { const humanInLoop = new HumanInLoopPage(page); await page.goto("/llama-index/feature/human_in_the_loop"); await humanInLoop.openChat(); await humanInLoop.sendMessage("Hi"); await humanInLoop.sendMessage( "Plan a mission to Mars with multiple steps and 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}? \u26a0\ufe0f Reply with only words 'Yes' or 'No' (no explanation, no punctuation).`, ); }); }); ================================================ FILE: apps/dojo/e2e/tests/llamaIndexTests/sharedStatePage.spec.ts ================================================ import { test, expect } from "@playwright/test"; import { SharedStatePage } from "../../featurePages/SharedStatePage"; test.describe("Shared State Feature", () => { test("[LlamaIndex] should interact with the chat to get a recipe on prompt", async ({ page, }) => { const sharedStateAgent = new SharedStatePage(page); // Update URL to new domain await page.goto( "/llama-index/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"'); await sharedStateAgent.loader(); await sharedStateAgent.awaitIngredientCard('Pasta'); await sharedStateAgent.getInstructionItems( sharedStateAgent.instructionsContainer ); }); test("[LlamaIndex] should share state between UI and chat", async ({ page, }) => { const sharedStateAgent = new SharedStatePage(page); await page.goto( "/llama-index/feature/shared_state" ); await sharedStateAgent.openChat(); // Add new ingredient via UI await sharedStateAgent.addIngredient.click(); // Fill in the new ingredient details const newIngredientCard = page.locator('.ingredient-card').last(); await newIngredientCard.locator('.ingredient-name-input').fill('Potatoes'); await newIngredientCard.locator('.ingredient-amount-input').fill('12'); // Wait for UI to update await page.waitForTimeout(1000); // Ask chat for all ingredients await sharedStateAgent.sendMessage("Give me all the ingredients"); await sharedStateAgent.loader(); // Verify chat response includes both existing and new ingredients 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(); }); }); ================================================ FILE: apps/dojo/e2e/tests/llamaIndexTests/v1AgenticChatPage.spec.ts ================================================ import { test } from "../../test-isolation-helper"; import { V1AgenticChatPage } from "../../featurePages/V1AgenticChatPage"; test("[V1] LlamaIndex sends and receives a message", async ({ page }) => { await page.goto("/llama-index/feature/v1_agentic_chat"); const chat = new V1AgenticChatPage(page); await chat.sendMessage("Hi"); await chat.assertUserMessageVisible("Hi"); await chat.assertAgentReplyVisible(/Hello|Hi|hey|help|assist/i); }); ================================================ FILE: apps/dojo/e2e/tests/mastraAgentLocalTests/agenticChatPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { AgenticChatPage } from "../../featurePages/AgenticChatPage"; test("[MastraAgentLocal] Agentic Chat sends and receives a message", async ({ page, }) => { await page.goto("/mastra-agent-local/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! How can I assist you today\?/, ); }); test("[MastraAgentLocal] Agentic Chat changes background on message and reset", async ({ page, }) => { await page.goto("/mastra-agent-local/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(); // 1. Send message to change background to blue 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(); // 2. Change to pink 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); const backgroundAfterPink = await getBackground(); // Verify it also differs from initial (not a reset) expect(backgroundAfterPink).not.toBe(initialBackground); }); test("[MastraAgentLocal] Agentic Chat retains memory of user messages during a conversation", async ({ page, }) => { await page.goto("/mastra-agent-local/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(/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!/); }); ================================================ FILE: apps/dojo/e2e/tests/mastraAgentLocalTests/backendToolRenderingPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; test("[MastraAgentLocal] Backend Tool Rendering displays weather cards", async ({ page, }) => { // Set shorter default timeout for this test test.setTimeout(30000); // 30 seconds total await page.goto("/mastra-agent-local/feature/backend_tool_rendering"); // Verify suggestion buttons are visible await expect( page.getByRole("button", { name: "Weather in San Francisco" }), ).toBeVisible({ timeout: 5000, }); // Click first suggestion and verify weather card appears await page.getByRole("button", { name: "Weather in San Francisco" }).click(); // Wait for either test ID or fallback to "Current Weather" text const weatherCard = page.getByTestId("weather-card"); const currentWeatherText = page.getByText("Current Weather"); // Try test ID first, fallback to text try { await expect(weatherCard).toBeVisible(); } catch (e) { // Fallback to checking for "Current Weather" text await expect(currentWeatherText.first()).toBeVisible(); } // Verify weather content is present (use flexible selectors) 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); // At least one of these should be true expect(hasHumidity || hasWind || hasCityName).toBeTruthy(); // Click second suggestion await page.getByRole("button", { name: "Weather in New York" }).click(); await page.waitForTimeout(2000); // Verify at least one weather-related element is still visible const weatherElements = await page .getByText(/Weather|Humidity|Wind|Temperature/i) .count(); expect(weatherElements).toBeGreaterThan(0); }); ================================================ FILE: apps/dojo/e2e/tests/mastraAgentLocalTests/humanInTheLoopPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { HumanInTheLoopPage } from "../../featurePages/HumanInTheLoopPage"; test.describe("Human in the Loop Feature", () => { test("[Mastra Agent Local] should interact with the chat and perform steps", async ({ page, }) => { const humanInLoop = new HumanInTheLoopPage(page); await page.goto("/mastra-agent-local/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("[Mastra Agent Local] should interact with the chat using predefined prompts and perform steps", async ({ page, }) => { const humanInLoop = new HumanInTheLoopPage(page); await page.goto("/mastra-agent-local/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).`, ); }); }); ================================================ FILE: apps/dojo/e2e/tests/mastraAgentLocalTests/sharedStatePage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { SharedStatePage } from "../../featurePages/SharedStatePage"; test.describe("Shared State Feature", () => { test("[MastraAgentLocal] should interact with the chat to get a recipe on prompt", async ({ page, }) => { const sharedStateAgent = new SharedStatePage(page); // Update URL to new domain await page.goto("/mastra-agent-local/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"', ); await sharedStateAgent.loader(); await sharedStateAgent.awaitIngredientCard("Pasta"); await sharedStateAgent.getInstructionItems( sharedStateAgent.instructionsContainer, ); }); test("[MastraAgentLocal] should share state between UI and chat", async ({ page, }) => { const sharedStateAgent = new SharedStatePage(page); await page.goto("/mastra-agent-local/feature/shared_state"); await sharedStateAgent.openChat(); // Add new ingredient via UI await sharedStateAgent.addIngredient.click(); // Fill in the new ingredient details const newIngredientCard = page.locator(".ingredient-card").last(); await newIngredientCard.locator(".ingredient-name-input").fill("Potatoes"); await newIngredientCard.locator(".ingredient-amount-input").fill("12"); // Wait for UI to update await page.waitForTimeout(1000); // Ask chat for all ingredients await sharedStateAgent.sendMessage("Give me all the ingredients"); await sharedStateAgent.loader(); // Verify chat response includes both existing and new ingredients 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(); }); }); ================================================ FILE: apps/dojo/e2e/tests/mastraAgentLocalTests/toolBasedGenUIPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { ToolBaseGenUIPage } from "../../featurePages/ToolBaseGenUIPage"; const pageURL = "/mastra-agent-local/feature/tool_based_generative_ui"; test("[Mastra Agent Local] 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("[Mastra Agent Local] 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); }); ================================================ FILE: apps/dojo/e2e/tests/mastraAgentLocalTests/v1AgenticChatPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { V1AgenticChatPage } from "../../featurePages/V1AgenticChatPage"; test("[V1] Mastra Agent Local sends and receives a message", async ({ page, }) => { await page.goto("/mastra-agent-local/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\?/); }); ================================================ FILE: apps/dojo/e2e/tests/mastraTests/agenticChatPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { AgenticChatPage } from "../../featurePages/AgenticChatPage"; import { sendChatMessage, awaitLLMResponseDone, } from "../../utils/copilot-actions"; test("[Mastra] Agentic Chat sends and receives a greeting message", async ({ page, }) => { await page.goto("/mastra/feature/agentic_chat"); const chat = new AgenticChatPage(page); await chat.openChat(); await expect(chat.agentGreeting).toBeVisible(); await chat.sendMessage("Hi"); await chat.assertUserMessageVisible("Hi"); await chat.assertAgentReplyVisible(/Hello|Hi|hey/i); }); test("[Mastra] Agentic Chat provides weather information", async ({ page }) => { await page.goto("/mastra/feature/agentic_chat"); const chat = new AgenticChatPage(page); await chat.openChat(); await expect(chat.agentGreeting).toBeVisible(); // Ask for Islamabad weather — use sendChatMessage to avoid // sendAndAwaitResponse timeout when the weather tool call is slow await sendChatMessage(page, "What is the weather in Islamabad"); await chat.assertUserMessageVisible("What is the weather in Islamabad"); // The weather-info component renders deterministically; wait for it await chat.assertWeatherResponseStructure(); }); test("[Mastra] Agentic Chat retains memory of previous questions", async ({ page, }) => { await page.goto("/mastra/feature/agentic_chat"); const chat = new AgenticChatPage(page); await chat.openChat(); await expect(chat.agentGreeting).toBeVisible(); // First question about weather — sendChatMessage avoids the // sendAndAwaitResponse timeout when the weather tool is slow await sendChatMessage(page, "What is the weather in Islamabad"); await chat.assertUserMessageVisible("What is the weather in Islamabad"); await chat.assertWeatherResponseStructure(); // Ensure stream is done before sending next message await awaitLLMResponseDone(page); // Ask about the first question to test memory await chat.sendMessage("What was my first question"); await chat.assertUserMessageVisible("What was my first question"); // Check if the agent remembers the first question about weather await chat.assertAgentReplyVisible(/weather|Islamabad/i); }); test("[Mastra] Agentic Chat retains memory of user messages during a conversation", async ({ page, }) => { await page.goto("/mastra/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(/how can I assist you/i); const favFruit = "Mango"; await chat.sendMessage(`My favorite fruit is ${favFruit}`); await chat.assertUserMessageVisible(`My favorite fruit is ${favFruit}`); await chat.assertAgentReplyVisible(new RegExp(favFruit, "i")); await chat.sendMessage("and I love listening to Kaavish"); await chat.assertUserMessageVisible("and I love listening to Kaavish"); await chat.assertAgentReplyVisible(/Kaavish/i); await chat.sendMessage("tell me an interesting fact about Moon"); await chat.assertUserMessageVisible("tell me an interesting fact about Moon"); await chat.assertAgentReplyVisible(/Moon/i); 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(new RegExp(favFruit, "i")); }); ================================================ FILE: apps/dojo/e2e/tests/mastraTests/backendToolRenderingPage.spec.ts ================================================ import { test, expect } from "@playwright/test"; test("[Mastra] Backend Tool Rendering displays weather cards", async ({ page }) => { // Set shorter default timeout for this test test.setTimeout(30000); // 30 seconds total await page.goto("/mastra/feature/backend_tool_rendering"); // Verify suggestion buttons are visible await expect(page.getByRole("button", { name: "Weather in San Francisco" })).toBeVisible({ timeout: 5000, }); // Click first suggestion and verify weather card appears await page.getByRole("button", { name: "Weather in San Francisco" }).click(); // Wait for either test ID or fallback to "Current Weather" text const weatherCard = page.getByTestId("weather-card"); const currentWeatherText = page.getByText("Current Weather"); // Try test ID first, fallback to text try { await expect(weatherCard).toBeVisible(); } catch (e) { // Fallback to checking for "Current Weather" text await expect(currentWeatherText.first()).toBeVisible(); } // Verify weather content is present (use flexible selectors) 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); // At least one of these should be true expect(hasHumidity || hasWind || hasCityName).toBeTruthy(); // Click second suggestion await page.getByRole("button", { name: "Weather in New York" }).click(); await page.waitForTimeout(2000); // Verify at least one weather-related element is still visible const weatherElements = await page.getByText(/Weather|Humidity|Wind|Temperature/i).count(); expect(weatherElements).toBeGreaterThan(0); }); ================================================ FILE: apps/dojo/e2e/tests/mastraTests/humanInTheLoopPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { HumanInTheLoopPage } from "../../featurePages/HumanInTheLoopPage"; test.describe("Human in the Loop Feature", () => { test("[Mastra] should interact with the chat and perform steps", async ({ page, }) => { const humanInLoop = new HumanInTheLoopPage(page); await page.goto("/mastra/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}? \u26a0\ufe0f Reply with only words 'Yes' or 'No' (no explanation, no punctuation).`, ); }); test("[Mastra] should interact with the chat using predefined prompts and perform steps", async ({ page, }) => { const humanInLoop = new HumanInTheLoopPage(page); await page.goto("/mastra/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}? \u26a0\ufe0f Reply with only words 'Yes' or 'No' (no explanation, no punctuation).`, ); }); }); ================================================ FILE: apps/dojo/e2e/tests/mastraTests/toolBasedGenUIPage.spec.ts ================================================ import { test, expect } from "@playwright/test"; import { ToolBaseGenUIPage } from "../../featurePages/ToolBaseGenUIPage"; const pageURL = "/mastra/feature/tool_based_generative_ui"; test("[Mastra] 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 infra issue, not an integration issue test.fixme( "[Mastra] 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(); // Wait for second haiku to be generated await genAIAgent.checkHaikuDisplay(page); // Now compare the second haiku }, ); ================================================ FILE: apps/dojo/e2e/tests/mastraTests/v1AgenticChatPage.spec.ts ================================================ import { test } from "../../test-isolation-helper"; import { V1AgenticChatPage } from "../../featurePages/V1AgenticChatPage"; test("[V1] Mastra sends and receives a message", async ({ page }) => { await page.goto("/mastra/feature/v1_agentic_chat"); const chat = new V1AgenticChatPage(page); await chat.sendMessage("Hi"); await chat.assertUserMessageVisible("Hi"); await chat.assertAgentReplyVisible(/Hello|Hi|hey|help|assist/i); }); ================================================ FILE: apps/dojo/e2e/tests/middlewareStarterTests/agenticChatPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { AgenticChatPage } from "../../featurePages/AgenticChatPage"; test("[Middleware Starter] Testing Agentic Chat", async ({ page }) => { await page.goto("/middleware-starter/feature/agentic_chat"); const chat = new AgenticChatPage(page); await chat.openChat(); await expect(chat.agentGreeting).toBeVisible(); await chat.sendMessage("Hey there"); await chat.assertUserMessageVisible("Hey there"); await chat.assertAgentReplyVisible(/Hello world!/i); }); ================================================ FILE: apps/dojo/e2e/tests/middlewareStarterTests/v1AgenticChatPage.spec.ts ================================================ import { test } from "../../test-isolation-helper"; import { V1AgenticChatPage } from "../../featurePages/V1AgenticChatPage"; test("[V1] Middleware Starter sends and receives a message", async ({ page, }) => { await page.goto("/middleware-starter/feature/v1_agentic_chat"); const chat = new V1AgenticChatPage(page); await chat.sendMessage("Hi"); await chat.assertUserMessageVisible("Hi"); await chat.assertAgentReplyVisible(/Hello|Hi|hey|help|assist/i); }); ================================================ FILE: apps/dojo/e2e/tests/pydanticAITests/agenticChatPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { AgenticChatPage } from "../../featurePages/AgenticChatPage"; test("[PydanticAI] Agentic Chat sends and receives a message", async ({ page, }) => { await page.goto("/pydantic-ai/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/i); }); test("[PydanticAI] Agentic Chat changes background on message and reset", async ({ page, }) => { await page.goto("/pydantic-ai/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(); // 1. Send message to change background to blue 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(); // 2. Change to pink 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("[PydanticAI] Agentic Chat retains memory of user messages during a conversation", async ({ page, }) => { await page.goto("/pydantic-ai/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(/how can I assist you/i); const favFruit = "Mango"; await chat.sendMessage(`My favorite fruit is ${favFruit}`); await chat.assertUserMessageVisible(`My favorite fruit is ${favFruit}`); await chat.assertAgentReplyVisible(new RegExp(favFruit, "i")); await chat.sendMessage("and I love listening to Kaavish"); await chat.assertUserMessageVisible("and I love listening to Kaavish"); await chat.assertAgentReplyVisible(/Kaavish/i); await chat.sendMessage("tell me an interesting fact about Moon"); await chat.assertUserMessageVisible("tell me an interesting fact about Moon"); await chat.assertAgentReplyVisible(/Moon/i); 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(new RegExp(favFruit, "i")); }); ================================================ FILE: apps/dojo/e2e/tests/pydanticAITests/agenticGenUI.spec.ts ================================================ import { awaitLLMResponseDone } from "../../utils/copilot-actions"; import { test, expect } from "../../test-isolation-helper"; import { AgenticGenUIPage } from "../../pages/pydanticAIPages/AgenticUIGenPage"; test.describe("Agent Generative UI Feature", () => { test("[PydanticAI] should interact with the chat to get a planner on prompt", async ({ page, }) => { const genUIAgent = new AgenticGenUIPage(page); await page.goto("/pydantic-ai/feature/agentic_generative_ui"); await genUIAgent.openChat(); await genUIAgent.sendMessage("Hi"); await genUIAgent.assertAgentReplyVisible([/Hello/, /Hi/]); await genUIAgent.sendMessage("give me a plan to make brownies"); await expect(genUIAgent.agentPlannerContainer).toBeVisible(); await genUIAgent.plan(); await awaitLLMResponseDone(page); }); test("[PydanticAI] should interact with the chat using predefined prompts and perform steps", async ({ page, }) => { const genUIAgent = new AgenticGenUIPage(page); await page.goto("/pydantic-ai/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); }); }); ================================================ FILE: apps/dojo/e2e/tests/pydanticAITests/backendToolRenderingPage.spec.ts ================================================ import { test, expect } from "@playwright/test"; test("[PydanticAI] Backend Tool Rendering displays weather cards", async ({ page }) => { // Set shorter default timeout for this test test.setTimeout(30000); // 30 seconds total await page.goto("/pydantic-ai/feature/backend_tool_rendering"); // Verify suggestion buttons are visible await expect(page.getByRole("button", { name: "Weather in San Francisco" })).toBeVisible({ timeout: 5000, }); // Click first suggestion and verify weather card appears await page.getByRole("button", { name: "Weather in San Francisco" }).click(); // Wait for either test ID or fallback to "Current Weather" text const weatherCard = page.getByTestId("weather-card"); const currentWeatherText = page.getByText("Current Weather"); // Try test ID first, fallback to text try { await expect(weatherCard).toBeVisible(); } catch (e) { // Fallback to checking for "Current Weather" text await expect(currentWeatherText.first()).toBeVisible(); } // Verify weather content is present (use flexible selectors) 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); // At least one of these should be true expect(hasHumidity || hasWind || hasCityName).toBeTruthy(); // Click second suggestion await page.getByRole("button", { name: "Weather in New York" }).click(); await page.waitForTimeout(2000); // Verify at least one weather-related element is still visible const weatherElements = await page.getByText(/Weather|Humidity|Wind|Temperature/i).count(); expect(weatherElements).toBeGreaterThan(0); }); ================================================ FILE: apps/dojo/e2e/tests/pydanticAITests/humanInTheLoopPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { HumanInLoopPage } from "../../pages/pydanticAIPages/HumanInLoopPage"; test.describe("Human in the Loop Feature", () => { test("[PydanticAI] should interact with the chat and perform steps", async ({ page, }) => { const humanInLoop = new HumanInLoopPage(page); await page.goto("/pydantic-ai/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("[PydanticAI] should interact with the chat using predefined prompts and perform steps", async ({ page, }) => { const humanInLoop = new HumanInLoopPage(page); await page.goto("/pydantic-ai/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).`, ); }); }); ================================================ FILE: apps/dojo/e2e/tests/pydanticAITests/predictiveStateUpdatePage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { PredictiveStateUpdatesPage } from "../../pages/pydanticAIPages/PredictiveStateUpdatesPage"; test.describe("Predictive Status Updates Feature", () => { // Fails on a production build. test.fixme( "[PydanticAI] should interact with agent and approve asked changes", async ({ page }) => { const predictiveStateUpdates = new PredictiveStateUpdatesPage(page); // Update URL to new domain await page.goto("/pydantic-ai/feature/predictive_state_updates"); await predictiveStateUpdates.openChat(); await predictiveStateUpdates.sendMessage( "Give me a story for a dragon called Atlantis in document", ); await page.waitForTimeout(2000); await predictiveStateUpdates.getPredictiveResponse(); await predictiveStateUpdates.getUserApproval(); await expect( predictiveStateUpdates.confirmedChangesResponse, ).toBeVisible(); const dragonName = await predictiveStateUpdates.verifyAgentResponse("Atlantis"); expect(dragonName).not.toBeNull(); // Send update to change the dragon name await predictiveStateUpdates.sendMessage("Change dragon name to Lola"); await page.waitForTimeout(2000); await predictiveStateUpdates.verifyHighlightedText(); await predictiveStateUpdates.getUserApproval(); await expect( predictiveStateUpdates.confirmedChangesResponse.nth(1), ).toBeVisible(); const dragonNameNew = await predictiveStateUpdates.verifyAgentResponse("Lola"); expect(dragonNameNew).not.toBe(dragonName); }, ); // Skipped while the above test is failing, the entire feature is temporarily disabled test.skip("[PydanticAI] should interact with agent and reject asked changes", async ({ page, }) => { const predictiveStateUpdates = new PredictiveStateUpdatesPage(page); // Update URL to new domain await page.goto("/pydantic-ai/feature/predictive_state_updates"); await predictiveStateUpdates.openChat(); await predictiveStateUpdates.sendMessage( "Give me a story for a dragon called called Atlantis in document", ); await predictiveStateUpdates.getPredictiveResponse(); await predictiveStateUpdates.getUserApproval(); await expect(predictiveStateUpdates.confirmedChangesResponse).toBeVisible(); const dragonName = await predictiveStateUpdates.verifyAgentResponse("Atlantis"); expect(dragonName).not.toBeNull(); // Send update to change the dragon name await predictiveStateUpdates.sendMessage("Change dragon name to Lola"); await page.waitForTimeout(2000); await predictiveStateUpdates.verifyHighlightedText(); await predictiveStateUpdates.getUserRejection(); await expect(predictiveStateUpdates.rejectedChangesResponse).toBeVisible(); const dragonNameAfterRejection = await predictiveStateUpdates.verifyAgentResponse("Atlantis"); expect(dragonNameAfterRejection).toBe(dragonName); expect(dragonNameAfterRejection).not.toBe("Lola"); }); }); ================================================ FILE: apps/dojo/e2e/tests/pydanticAITests/sharedStatePage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { SharedStatePage } from "../../featurePages/SharedStatePage"; test.describe("Shared State Feature", () => { test("[PydanticAI] should interact with the chat to get a recipe on prompt", async ({ page, }) => { const sharedStateAgent = new SharedStatePage(page); await page.goto("/pydantic-ai/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"', ); await sharedStateAgent.awaitIngredientCard("Pasta"); await sharedStateAgent.getInstructionItems( sharedStateAgent.instructionsContainer, ); }); test("[PydanticAI] should share state between UI and chat", async ({ page, }) => { const sharedStateAgent = new SharedStatePage(page); await page.goto("/pydantic-ai/feature/shared_state"); await sharedStateAgent.openChat(); // Add new ingredient via UI await sharedStateAgent.addIngredient.click(); // Fill in the new ingredient details const newIngredientCard = page.locator(".ingredient-card").last(); await newIngredientCard.locator(".ingredient-name-input").fill("Potatoes"); await newIngredientCard.locator(".ingredient-amount-input").fill("12"); // Wait for UI input to settle await expect( newIngredientCard.locator(".ingredient-name-input"), ).toHaveValue("Potatoes"); // Ask chat for all ingredients await sharedStateAgent.sendMessage("Give me all the ingredients"); // Verify chat response includes the UI-added ingredient await expect( sharedStateAgent.agentMessage.getByText(/Potatoes/), ).toBeVisible(); }); }); ================================================ FILE: apps/dojo/e2e/tests/pydanticAITests/toolBasedGenUIPage.spec.ts ================================================ import { test, expect } from "@playwright/test"; import { ToolBaseGenUIPage } from "../../featurePages/ToolBaseGenUIPage"; const pageURL = "/pydantic-ai/feature/tool_based_generative_ui"; test('[PydanticAI] 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('[PydanticAI] 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(); // Wait for second haiku to be generated await genAIAgent.checkHaikuDisplay(page); // Now compare the second haiku }); ================================================ FILE: apps/dojo/e2e/tests/pydanticAITests/v1AgenticChatPage.spec.ts ================================================ import { test } from "../../test-isolation-helper"; import { V1AgenticChatPage } from "../../featurePages/V1AgenticChatPage"; test("[V1] Pydantic AI sends and receives a message", async ({ page }) => { await page.goto("/pydantic-ai/feature/v1_agentic_chat"); const chat = new V1AgenticChatPage(page); await chat.sendMessage("Hi"); await chat.assertUserMessageVisible("Hi"); await chat.assertAgentReplyVisible(/Hello|Hi|hey|help|assist/i); }); ================================================ FILE: apps/dojo/e2e/tests/serverStarterAllFeaturesTests/agenticChatPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { AgenticChatPage } from "../../featurePages/AgenticChatPage"; import { sendChatMessage } from "../../utils/copilot-actions"; test("[Server Starter all features] Agentic Chat displays countdown from 10 to 1 with tick mark", async ({ page, }) => { await page.goto("/server-starter-all-features/feature/agentic_chat"); const chat = new AgenticChatPage(page); await chat.openChat(); await expect(chat.agentGreeting).toBeVisible(); // Use sendChatMessage to avoid sendAndAwaitResponse timeout; // the countdown assertion below handles the waiting with its own timeout. await sendChatMessage(page, "Hey there"); await chat.assertUserMessageVisible("Hey there"); // v2 CopilotKit uses data-testid="copilot-assistant-message" with data-message-id const countdownMessage = page .getByTestId("copilot-assistant-message") .filter({ hasText: "counting down:" }); await expect(countdownMessage).toBeVisible({ timeout: 30000 }); // Wait for countdown to complete by checking for the tick mark await expect(countdownMessage).toContainText("\u2713", { timeout: 15000 }); const countdownText = await countdownMessage.textContent(); expect(countdownText).toContain("counting down:"); expect(countdownText).toMatch( /counting down:\s*10\s+9\s+8\s+7\s+6\s+5\s+4\s+3\s+2\s+1\s+\u2713/, ); }); ================================================ FILE: apps/dojo/e2e/tests/serverStarterAllFeaturesTests/agenticGenUI.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { AgenticGenUIPage } from "../../pages/serverStarterAllFeaturesPages/AgenticUIGenPage"; test.describe("Agent Generative UI Feature", () => { // Temporarily disabled because the agent planner UI element is currently missing in CI runs. test.skip("[Server Starter all features] should interact with the chat to get a planner on prompt", async ({ page, }) => { const genUIAgent = new AgenticGenUIPage(page); await page.goto( "/server-starter-all-features/feature/agentic_generative_ui", ); await genUIAgent.openChat(); await genUIAgent.sendMessage("Hi"); await expect(genUIAgent.agentPlannerContainer).toBeVisible(); await genUIAgent.plan(); await expect(genUIAgent.agentPlannerContainer).toBeVisible({ timeout: 8000, }); }); }); ================================================ FILE: apps/dojo/e2e/tests/serverStarterAllFeaturesTests/backendToolRenderingPage.spec.ts ================================================ import { test, expect } from "@playwright/test"; test("[ServerStarterAllFeatures] Backend Tool Rendering displays weather cards", async ({ page, }) => { // Set longer timeout for this test since server-starter-all-features can be slower test.setTimeout(60000); // 60 seconds total await page.goto("/server-starter-all-features/feature/backend_tool_rendering"); // Wait for page to load - be more lenient with timeout await page.waitForLoadState("networkidle", { timeout: 15000 }).catch(() => {}); // Verify suggestion buttons are visible with longer timeout await expect(page.getByRole("button", { name: "Weather in San Francisco" })).toBeVisible({ timeout: 15000, }); // Click first suggestion and verify weather card appears await page.getByRole("button", { name: "Weather in San Francisco" }).click(); // Wait longer for weather card to appear (backend processing time) const weatherCard = page.getByTestId("weather-card"); const currentWeatherText = page.getByText("Current Weather"); // Try test ID first with longer timeout, fallback to text let weatherVisible = false; try { await expect(weatherCard).toBeVisible({ timeout: 20000 }); weatherVisible = true; } catch (e) { // Fallback to checking for "Current Weather" text try { await expect(currentWeatherText.first()).toBeVisible({ timeout: 20000 }); weatherVisible = true; } catch (e2) { // Last resort - check for any weather-related content const weatherContent = await page.getByText(/Humidity|Wind|Temperature/i).count(); weatherVisible = weatherContent > 0; } } expect(weatherVisible).toBeTruthy(); // Verify weather content is present (use flexible selectors) await page.waitForTimeout(1000); // Give elements time to render 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); // At least one of these should be true expect(hasHumidity || hasWind || hasCityName).toBeTruthy(); // Click second suggestion await page.getByRole("button", { name: "Weather in New York" }).click(); await page.waitForTimeout(3000); // Longer wait for backend to process // Verify at least one weather-related element is still visible const weatherElements = await page.getByText(/Weather|Humidity|Wind|Temperature/i).count(); expect(weatherElements).toBeGreaterThan(0); }); ================================================ FILE: apps/dojo/e2e/tests/serverStarterAllFeaturesTests/humanInTheLoopPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { HumanInLoopPage } from "../../pages/serverStarterAllFeaturesPages/HumanInLoopPage"; test.describe("Human in the Loop Feature", () => { test(" [Server Starter all features] should interact with the chat using predefined prompts and perform steps", async ({ page, }) => { const humanInLoop = new HumanInLoopPage(page); await page.goto("/server-starter-all-features/feature/human_in_the_loop"); await humanInLoop.openChat(); await humanInLoop.sendMessage("Hi"); await expect(humanInLoop.plan).toBeVisible(); await humanInLoop.performStepsAndAwait(); }); }); ================================================ FILE: apps/dojo/e2e/tests/serverStarterAllFeaturesTests/predictiveStateUpdatePage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { PredictiveStateUpdatesPage } from "../../pages/serverStarterAllFeaturesPages/PredictiveStateUpdatesPage"; test.describe("Predictive Status Updates Feature", () => { // The server-starter-all backend is a mock that streams write_document_local // + confirm_changes tool calls. The confirm_changes HiTL modal works, but the // predictive state mechanism (PredictState custom event -> editor content) does // not populate the TipTap editor in the current framework version. These tests // verify the HiTL confirm/reject flow works end-to-end. test("[Server Starter all features] should interact with agent and approve asked changes", async ({ page, }) => { const predictiveStateUpdates = new PredictiveStateUpdatesPage(page); await page.goto( "/server-starter-all-features/feature/predictive_state_updates", ); await predictiveStateUpdates.openChat(); await predictiveStateUpdates.sendMessage("Write a story"); // The mock backend sends confirm_changes tool call -> HiTL modal appears await predictiveStateUpdates.getPredictiveResponse(); await predictiveStateUpdates.getUserApproval(); // After approval the agent responds with a confirmation message await expect(predictiveStateUpdates.confirmedChangesResponse).toBeVisible(); // Send a follow-up message - triggers another round of tool calls await predictiveStateUpdates.sendMessage("Update the story"); await predictiveStateUpdates.verifyHighlightedText(); await predictiveStateUpdates.getUserApproval(); await expect(predictiveStateUpdates.confirmedChangesResponse).toBeVisible(); }); test("[Server Starter all features] should interact with agent and reject asked changes", async ({ page, }) => { const predictiveStateUpdates = new PredictiveStateUpdatesPage(page); await page.goto( "/server-starter-all-features/feature/predictive_state_updates", ); await predictiveStateUpdates.openChat(); await predictiveStateUpdates.sendMessage("Write a story"); // First round: approve to establish baseline await predictiveStateUpdates.getPredictiveResponse(); await predictiveStateUpdates.getUserApproval(); await expect(predictiveStateUpdates.confirmedChangesResponse).toBeVisible(); // Second round: reject the changes await predictiveStateUpdates.sendMessage("Update the story"); await predictiveStateUpdates.verifyHighlightedText(); await predictiveStateUpdates.getUserRejection(); await expect(predictiveStateUpdates.rejectedChangesResponse).toBeVisible(); }); }); ================================================ FILE: apps/dojo/e2e/tests/serverStarterAllFeaturesTests/sharedStatePage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { SharedStatePage } from "../../featurePages/SharedStatePage"; import { sendChatMessage } from "../../utils/copilot-actions"; test.describe("Shared State Feature", () => { test("[Server Starter all features] should interact with the chat to get a recipe on prompt", async ({ page, }) => { const sharedStateAgent = new SharedStatePage(page); await page.goto("/server-starter-all-features/feature/shared_state"); await sharedStateAgent.openChat(); // Use sendChatMessage to avoid sendAndAwaitResponse timeout; // loader() and awaitIngredientCard handle the waiting. await sendChatMessage( page, 'Please give me a pasta recipe of your choosing, but one of the ingredients should be "Pasta"', ); await sharedStateAgent.loader(); await sharedStateAgent.awaitIngredientCard("Salt"); await sharedStateAgent.getInstructionItems( sharedStateAgent.instructionsContainer, ); }); test("[Server Starter all features] should share state between UI and chat", async ({ page, }) => { const sharedStateAgent = new SharedStatePage(page); await page.goto("/server-starter-all-features/feature/shared_state"); await sharedStateAgent.openChat(); // Add new ingredient via UI await sharedStateAgent.addIngredient.click(); // Fill in the new ingredient details const newIngredientCard = page.locator(".ingredient-card").last(); await newIngredientCard.locator(".ingredient-name-input").fill("Potatoes"); await newIngredientCard.locator(".ingredient-amount-input").fill("12"); // Wait for UI to update await page.waitForTimeout(1000); // Use sendChatMessage to avoid sendAndAwaitResponse timeout; // loader() and awaitIngredientCard handle the waiting. await sendChatMessage(page, "Give me all the ingredients"); await sharedStateAgent.loader(); // Verify hardcoded ingredients await sharedStateAgent.awaitIngredientCard("chicken breast"); await sharedStateAgent.awaitIngredientCard("chili powder"); await sharedStateAgent.awaitIngredientCard("Salt"); await sharedStateAgent.awaitIngredientCard("Lettuce leaves"); expect( await sharedStateAgent.getInstructionItems( sharedStateAgent.instructionsContainer, ), ).toBe(3); }); }); ================================================ FILE: apps/dojo/e2e/tests/serverStarterAllFeaturesTests/toolBasedGenUIPage.spec.ts ================================================ import { test, expect } from "@playwright/test"; import { ToolBaseGenUIPage } from "../../featurePages/ToolBaseGenUIPage"; const pageURL = "/server-starter-all-features/feature/tool_based_generative_ui"; test('[Server Starter all features] 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('[Server Starter all features] 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); }); ================================================ FILE: apps/dojo/e2e/tests/serverStarterAllFeaturesTests/v1AgenticChatPage.spec.ts ================================================ import { test } from "../../test-isolation-helper"; import { V1AgenticChatPage } from "../../featurePages/V1AgenticChatPage"; test("[V1] Server Starter All Features sends and receives a message", async ({ page, }) => { await page.goto("/server-starter-all-features/feature/v1_agentic_chat"); const chat = new V1AgenticChatPage(page); await chat.sendMessage("Hi"); await chat.assertUserMessageVisible("Hi"); await chat.assertAgentReplyVisible(/Hello|Hi|hey|help|assist/i); }); ================================================ FILE: apps/dojo/e2e/tests/serverStarterTests/agenticChatPage.spec.ts ================================================ import { test, expect } from "../../test-isolation-helper"; import { AgenticChatPage } from "../../featurePages/AgenticChatPage"; test("[Server Starter] Testing Agentic Chat", async ({ page }) => { await page.goto("/server-starter/feature/agentic_chat"); const chat = new AgenticChatPage(page); await chat.openChat(); await expect(chat.agentGreeting).toBeVisible(); await chat.sendMessage("Hey there"); await chat.assertUserMessageVisible("Hey there"); await chat.assertAgentReplyVisible(/Hello world!/i); }); ================================================ FILE: apps/dojo/e2e/tests/serverStarterTests/v1AgenticChatPage.spec.ts ================================================ import { test } from "../../test-isolation-helper"; import { V1AgenticChatPage } from "../../featurePages/V1AgenticChatPage"; test("[V1] Server Starter sends and receives a message", async ({ page }) => { await page.goto("/server-starter/feature/v1_agentic_chat"); const chat = new V1AgenticChatPage(page); await chat.sendMessage("Hi"); await chat.assertUserMessageVisible("Hi"); await chat.assertAgentReplyVisible(/Hello|Hi|hey|help|assist/i); }); ================================================ FILE: apps/dojo/e2e/utils/aiWaitHelpers.ts ================================================ import { expect, Locator, Page } from "@playwright/test"; import { awaitLLMResponseDone } from "./copilot-actions"; /** * Wait for AI assistant messages with extended timeout and retry logic. */ export async function waitForAIResponse( locator: Locator, pattern: RegExp, timeoutMs: number = 30_000 ) { await expect(locator.getByText(pattern)).toBeVisible({ timeout: timeoutMs }); } /** * Wait for AI-generated content to appear. */ export async function waitForAIContent( locator: Locator, timeoutMs: number = 30_000 ) { await expect(locator).toBeVisible({ timeout: timeoutMs }); } /** * Wait for AI form interactions to be ready. */ export async function waitForAIFormReady( locator: Locator, timeoutMs: number = 30_000 ) { await expect(locator).toBeVisible({ timeout: timeoutMs }); await expect(locator).toBeEnabled({ timeout: timeoutMs }); await expect(locator).toBeEditable({ timeout: timeoutMs }); } /** * Wait for AI dialog/modal to appear. */ export async function waitForAIDialog( locator: Locator, timeoutMs: number = 30_000 ) { await expect(locator).toBeVisible({ timeout: timeoutMs }); } /** * Wait for the LLM to finish, then check for any matching pattern. * No more polling loop — waits for stream to end, then asserts. */ export async function waitForAIPatterns( page: Page, patterns: RegExp[], timeoutMs: number = 30_000 ): Promise { // Wait for the LLM stream to complete first await awaitLLMResponseDone(page, timeoutMs); // Then check for patterns immediately for (const pattern of patterns) { try { const element = page.locator("body").getByText(pattern); if ((await element.count()) > 0) { await expect(element.first()).toBeVisible({ timeout: 5000 }); return; } } catch { // Continue to next pattern } } throw new Error( `None of the expected patterns matched after LLM response: ${patterns .map((p) => p.toString()) .join(", ")}` ); } ================================================ FILE: apps/dojo/e2e/utils/copilot-actions.ts ================================================ import { Page, expect } from "@playwright/test"; import { CopilotSelectors } from "./copilot-selectors"; /** Default timeout for waiting for LLM response to finish (SSE stream done) */ const LLM_RESPONSE_TIMEOUT = 60_000; /** Default timeout for finding a DOM element after response */ const ELEMENT_TIMEOUT = 10_000; /** * Wait for the LLM SSE stream to finish. * Uses the `data-copilot-running` attribute on the chat container — * no arbitrary timeouts or loading-indicator polling needed. */ export async function awaitLLMResponseDone( page: Page, timeout = LLM_RESPONSE_TIMEOUT, ) { // First wait briefly for the stream to start try { await page.waitForFunction( () => document.querySelector('[data-copilot-running="true"]') !== null, null, { timeout: 3000 }, ); } catch { // May have already started and finished, continue } // Then wait for the stream to finish await page.waitForFunction( () => document.querySelector('[data-copilot-running="false"]') !== null, null, { timeout }, ); } /** * Type a message into the chat input and click send. * Replaces the duplicated sendMessage pattern across all page objects. */ export async function sendChatMessage(page: Page, message: string) { const input = CopilotSelectors.chatTextarea(page); await input.click(); await input.fill(message); const sendButton = CopilotSelectors.sendButton(page); await expect(sendButton).toBeVisible(); await expect(sendButton).toBeEnabled(); await sendButton.click(); } /** * Send a message and wait for the LLM to finish responding. * * Uses assistant message counting to avoid a race condition in multi-turn * conversations where `data-copilot-running="false"` from the previous * response is still present when we start checking. */ export async function sendAndAwaitResponse( page: Page, message: string, timeout = LLM_RESPONSE_TIMEOUT, ) { // Snapshot assistant message count before sending so we can detect // when the agent starts responding to THIS message. const countBefore = await page .locator('[data-testid="copilot-assistant-message"]') .count(); await sendChatMessage(page, message); // Wait for a NEW assistant message to appear, proving the agent // started responding to our message (not a stale previous response). await page.waitForFunction( (before) => document.querySelectorAll('[data-testid="copilot-assistant-message"]') .length > before, countBefore, { timeout }, ); // Now wait for the stream to finish — at this point the running state // belongs to the current response, not a stale one. await page.waitForFunction( () => document.querySelector('[data-copilot-running="false"]') !== null, null, { timeout }, ); } /** * Assert that the last assistant message contains the expected text. */ export async function assertAssistantReply( page: Page, expected: RegExp | string, timeout = ELEMENT_TIMEOUT, ) { const messages = CopilotSelectors.assistantMessages(page); const last = messages.last(); await expect(last).toBeVisible({ timeout }); if (typeof expected === "string") { await expect(last).toContainText(expected, { timeout }); } else { await expect(last.getByText(expected)).toBeVisible({ timeout }); } } /** * Assert that a user message is visible in the chat. */ export async function assertUserMessage( page: Page, text: string | RegExp, timeout = ELEMENT_TIMEOUT, ) { const messages = CopilotSelectors.userMessages(page); await expect(messages.getByText(text)).toBeVisible({ timeout }); } /** * Open the chat by clicking the toggle button. * Silently succeeds if the chat is already open. */ export async function openChat(page: Page) { try { const toggle = CopilotSelectors.chatToggle(page); await toggle.click({ timeout: 3000 }); } catch { // Chat may already be open } } /** * Hover over an assistant message to reveal the toolbar, then click regenerate. */ export async function regenerateResponse(page: Page, messageIndex: number) { const message = CopilotSelectors.assistantMessages(page).nth(messageIndex); await expect(message).toBeVisible({ timeout: ELEMENT_TIMEOUT }); await message.hover(); const regenerate = message.getByTestId("copilot-regenerate-button"); try { await regenerate.click({ timeout: 3000 }); } catch { await regenerate.click({ force: true }); } } ================================================ FILE: apps/dojo/e2e/utils/copilot-selectors.ts ================================================ import { Page } from "@playwright/test"; /** * Centralized CopilotKit element selectors using data-testid attributes. * Import these instead of defining fragile CSS/role selectors in page objects. */ export const CopilotSelectors = { /** Main chat container — also carries data-copilot-running attribute */ chat: (page: Page) => page.getByTestId("copilot-chat"), /** Chat text input (textarea) */ chatTextarea: (page: Page) => page.getByTestId("copilot-chat-textarea"), /** Send / Stop button */ sendButton: (page: Page) => page.getByTestId("copilot-send-button"), /** All assistant messages */ assistantMessages: (page: Page) => page.getByTestId("copilot-assistant-message"), /** All user messages */ userMessages: (page: Page) => page.getByTestId("copilot-user-message"), /** Message list container */ messageList: (page: Page) => page.getByTestId("copilot-message-list"), /** Loading cursor (AI thinking indicator) */ loadingCursor: (page: Page) => page.getByTestId("copilot-loading-cursor"), /** Regenerate button on assistant messages */ regenerateButton: (page: Page) => page.getByTestId("copilot-regenerate-button"), /** Chat toggle (open/close) button */ chatToggle: (page: Page) => page.getByTestId("copilot-chat-toggle"), /** Sidebar container */ sidebar: (page: Page) => page.getByTestId("copilot-sidebar"), /** Popup dialog */ popup: (page: Page) => page.getByTestId("copilot-popup"), /** Suggestion pills container */ suggestions: (page: Page) => page.getByTestId("copilot-suggestions"), /** Individual suggestion pills */ suggestion: (page: Page) => page.getByTestId("copilot-suggestion"), /** Modal header */ modalHeader: (page: Page) => page.getByTestId("copilot-modal-header"), /** Modal close button */ closeButton: (page: Page) => page.getByTestId("copilot-close-button"), /** Welcome screen */ welcomeScreen: (page: Page) => page.getByTestId("copilot-welcome-screen"), /** Scroll to bottom button */ scrollToBottom: (page: Page) => page.getByTestId("copilot-scroll-to-bottom"), /** Input pill container */ chatInput: (page: Page) => page.getByTestId("copilot-chat-input"), /** Slash commands menu */ slashMenu: (page: Page) => page.getByTestId("copilot-slash-menu"), } as const; ================================================ FILE: apps/dojo/eslint.config.mjs ================================================ import nextCoreWebVitals from "eslint-config-next/core-web-vitals"; import nextTypescript from "eslint-config-next/typescript"; import next from "eslint-config-next"; const eslintConfig = [ ...nextCoreWebVitals, ...nextTypescript, ...next, ...compat.config({ rules: { "@typescript-eslint/no-unused-vars": "off", }, }), { ignores: [ "node_modules/**", ".next/**", "out/**", "build/**", "next-env.d.ts", ], }, ]; export default eslintConfig; ================================================ FILE: apps/dojo/next.config.ts ================================================ import type { NextConfig } from "next"; import createMDX from "@next/mdx"; const withMDX = createMDX({ extension: /\.mdx?$/, options: { // If you use remark-gfm, you'll need to use next.config.mjs // as the package is ESM only // https://github.com/remarkjs/remark-gfm#install remarkPlugins: [], rehypePlugins: [], // If you use `MDXProvider`, uncomment the following line. providerImportSource: "@mdx-js/react", }, }); const nextConfig: NextConfig = { /* config options here */ // Configure pageExtensions to include md and mdx pageExtensions: ["ts", "tsx", "js", "jsx", "md", "mdx"], webpack: (config, { isServer }) => { // Ignore the demo files during build config.module.rules.push({ test: /agent\/demo\/crew_enterprise\/ui\/.*\.(ts|tsx|js|jsx)$/, loader: "ignore-loader", }); return config; }, serverExternalPackages: ["@mastra/libsql", "@copilotkit/runtime"], }; // Merge MDX config with Next.js config export default withMDX(nextConfig); ================================================ FILE: apps/dojo/package.json ================================================ { "name": "demo-viewer", "version": "0.1.0", "private": true, "scripts": { "dev": "npm run generate-content-json && next dev", "build": "next build", "start": "npm run generate-content-json && next start", "lint": "eslint .", "mastra:dev": "mastra dev", "generate-content-json": "npx tsx scripts/generate-content-json.ts", "run-everything": "./scripts/prep-dojo-everything.js && ./scripts/run-dojo-everything.js" }, "dependencies": { "@a2a-js/sdk": "0.2.5", "@a2ui/lit": "^0.8.1", "@ag-ui/a2a": "workspace:*", "@ag-ui/a2a-middleware": "workspace:*", "@ag-ui/a2ui-middleware": "workspace:*", "@ag-ui/adk": "workspace:*", "@ag-ui/ag2": "workspace:*", "@ag-ui/agno": "workspace:*", "@ag-ui/aws-strands": "workspace:*", "@ag-ui/claude-agent-sdk": "workspace:*", "@ag-ui/crewai": "workspace:*", "@ag-ui/langchain": "workspace:*", "@ag-ui/langroid": "workspace:*", "@ag-ui/langgraph": "workspace:*", "@ag-ui/llamaindex": "workspace:*", "@ag-ui/mastra": "workspace:*", "@ag-ui/middleware-starter": "workspace:*", "@ag-ui/pydantic-ai": "workspace:*", "@ag-ui/server-starter": "workspace:*", "@ag-ui/server-starter-all-features": "workspace:*", "@ag-ui/spring-ai": "workspace:*", "@ag-ui/vercel-ai-sdk": "workspace:*", "@ai-sdk/openai": "^3.0.36", "@anthropic-ai/claude-agent-sdk": "^0.2.58", "@copilotkit/a2ui-renderer": "0.0.0-mme-ag-ui-0-0-46-20260227141603", "@copilotkit/react-core": "0.0.0-mme-ag-ui-0-0-46-20260227141603", "@copilotkit/react-ui": "0.0.0-mme-ag-ui-0-0-46-20260227141603", "@copilotkit/runtime": "0.0.0-mme-ag-ui-0-0-46-20260227141603", "@copilotkit/runtime-client-gql": "0.0.0-mme-ag-ui-0-0-46-20260227141603", "@copilotkit/shared": "0.0.0-mme-ag-ui-0-0-46-20260227141603", "@langchain/openai": "1.0.0", "@mastra/client-js": "^1.0.1", "@mastra/core": "^1.0.4", "@mastra/dynamodb": "^1.0.0", "@mastra/libsql": "^1.0.0", "@mastra/loggers": "^1.0.0", "@mastra/memory": "^1.0.0", "@mdx-js/loader": "^3.1.0", "@mdx-js/mdx": "^3.1.0", "@mdx-js/react": "^3.1.0", "@monaco-editor/react": "^4.7.0", "@next/mdx": "^16.0.7", "@phosphor-icons/react": "^2.1.10", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.3", "@tiptap/extension-color": "^2.11.5", "@tiptap/extension-placeholder": "^2.11.5", "@tiptap/pm": "^2.11.5", "@tiptap/react": "^2.11.5", "@tiptap/starter-kit": "^2.11.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dedent": "^1.7.0", "diff": "^7.0.0", "embla-carousel-react": "^8.6.0", "fast-json-patch": "^3.1.1", "hono": "^4.11.4", "lucide-react": "^0.477.0", "markdown-it": "^14.1.0", "markdown-it-ins": "^4.0.0", "next": "16.0.10", "next-themes": "^0.4.6", "openai": "^4.98.0", "react": "^19.2.1", "react-dom": "^19.2.1", "rxjs": "7.8.1", "streamdown": "^1.6.10", "tailwind-merge": "^3.3.0", "tailwindcss-animate": "^1.0.7", "untruncate-json": "^0.0.1", "uuid": "^11.1.0", "zod": "^3.25.75" }, "peerDependencies": { "@ag-ui/client": "workspace:*", "@ag-ui/core": "workspace:*", "@ag-ui/encoder": "workspace:*", "@ag-ui/proto": "workspace:*" }, "devDependencies": { "@copilotkit/llmock": "^1.1.1", "@shadcn/ui": "^0.0.4", "@tailwindcss/postcss": "^4", "@tailwindcss/typography": "^0.5.16", "@types/diff": "^7.0.1", "@types/markdown-it": "^14.1.2", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "concurrently": "^9.2.0", "eslint": "^9", "eslint-config-next": "16.0.7", "mastra": "^1.0.1", "tailwindcss": "^4", "tsx": "^4.7.0", "typescript": "^5", "wait-port": "^1.1.0" } } ================================================ FILE: apps/dojo/postcss.config.mjs ================================================ const config = { plugins: ["@tailwindcss/postcss"], }; export default config; ================================================ FILE: apps/dojo/scripts/generate-content-json.ts ================================================ import fs from "fs"; import path from "path"; import { menuIntegrations } from "../src/menu"; // Map menuIntegrations to the format needed for content generation const agentConfigs = menuIntegrations.map((integration) => ({ id: integration.id, agentKeys: [...integration.features], })); const featureFiles = ["page.tsx", "style.css", "README.mdx"]; async function getFile(_filePath: string | undefined, _fileName?: string) { if (!_filePath) { console.warn(`File path is undefined, skipping.`); return {}; } const fileName = _fileName ?? path.basename(_filePath); const filePath = _fileName ? path.join(_filePath, fileName) : _filePath; // Check if it's a remote URL const isRemoteUrl = _filePath.startsWith("http://") || _filePath.startsWith("https://"); let content: string; try { if (isRemoteUrl) { // Convert GitHub URLs to raw URLs for direct file access let fetchUrl = _filePath; if (_filePath.includes("github.com") && _filePath.includes("/blob/")) { fetchUrl = _filePath .replace("github.com", "raw.githubusercontent.com") .replace("/blob/", "/"); } // Fetch remote file content console.log(`Fetching remote file: ${fetchUrl}`); const response = await fetch(fetchUrl); if (!response.ok) { console.warn( `Failed to fetch remote file: ${fetchUrl}, status: ${response.status}`, ); return {}; } content = await response.text(); } else { // Handle local file if (!fs.existsSync(filePath)) { console.warn(`File not found: ${filePath}, skipping.`); return {}; } content = fs.readFileSync(filePath, "utf8"); } const extension = fileName.split(".").pop(); let language = extension; if (extension === "py") language = "python"; else if (extension === "cs") language = "csharp"; else if (extension === "css") language = "css"; else if (extension === "md" || extension === "mdx") language = "markdown"; else if (extension === "tsx") language = "typescript"; else if (extension === "js") language = "javascript"; else if (extension === "json") language = "json"; else if (extension === "yaml" || extension === "yml") language = "yaml"; else if (extension === "toml") language = "toml"; return { name: fileName, content, language, type: "file", }; } catch (error) { console.error(`Error reading file ${filePath}:`, error); return {}; } } const FEATURE_BASE = path.join(__dirname, "../src/app/[integrationId]/feature"); function resolveFeatureDir(featureId: string): string { const v1Path = path.join(FEATURE_BASE, "(v1)", featureId); if (fs.existsSync(v1Path)) return v1Path; return path.join(FEATURE_BASE, "(v2)", featureId); } async function getFeatureFrontendFiles(featureId: string) { const featurePath = resolveFeatureDir(featureId); const retrievedFiles = []; for (const fileName of featureFiles) { retrievedFiles.push(await getFile(featurePath, fileName)); } return retrievedFiles; } const integrationsFolderPath = "../../../integrations"; const middlewaresFolderPath = "../../../middlewares"; const agentFilesMapper: Record< string, (agentKeys: string[]) => Record > = { "middleware-starter": () => ({ agentic_chat: [ path.join( __dirname, middlewaresFolderPath, `/middleware-starter/src/index.ts`, ), ], }), "pydantic-ai": (agentKeys: string[]) => { return agentKeys.reduce( (acc, agentId) => ({ ...acc, [agentId]: [ path.join( __dirname, integrationsFolderPath, `/pydantic-ai/python/examples/server/api/${agentId}.py`, ), ], }), {}, ); }, "server-starter": () => ({ agentic_chat: [ path.join( __dirname, integrationsFolderPath, `/server-starter/python/examples/example_server/__init__.py`, ), ], }), "server-starter-all-features": (agentKeys: string[]) => { return agentKeys.reduce( (acc, agentId) => ({ ...acc, [agentId]: [ path.join( __dirname, integrationsFolderPath, `/server-starter-all-features/python/examples/example_server/${agentId}.py`, ), ], }), {}, ); }, mastra: () => ({ agentic_chat: [ path.join( __dirname, integrationsFolderPath, `/mastra/typescript/examples/src/mastra/agents/agentic-chat.ts`, ), ], backend_tool_rendering: [ path.join( __dirname, integrationsFolderPath, `/mastra/typescript/examples/src/mastra/agents/backend-tool-rendering.ts`, ), ], human_in_the_loop: [ path.join( __dirname, integrationsFolderPath, `/mastra/typescript/examples/src/mastra/agents/human-in-the-loop.ts`, ), ], tool_based_generative_ui: [ path.join( __dirname, integrationsFolderPath, `/mastra/typescript/examples/src/mastra/agents/tool-based-generative-ui.ts`, ), ], }), "mastra-agent-local": () => ({ agentic_chat: [path.join(__dirname, "../src/mastra/agents/agentic-chat.ts")], human_in_the_loop: [path.join(__dirname, "../src/mastra/agents/human-in-the-loop.ts")], backend_tool_rendering: [path.join(__dirname, "../src/mastra/agents/backend-tool-rendering.ts")], shared_state: [path.join(__dirname, "../src/mastra/agents/shared-state.ts")], tool_based_generative_ui: [path.join(__dirname, "../src/mastra/agents/tool-based-generative-ui.ts")], }), "vercel-ai-sdk": () => ({ agentic_chat: [ path.join( __dirname, integrationsFolderPath, `/vercel-ai-sdk/src/index.ts`, ), ], }), langgraph: (agentKeys: string[]) => { return agentKeys.reduce( (acc, agentId) => ({ ...acc, [agentId]: [ path.join( __dirname, integrationsFolderPath, `/langgraph/python/examples/agents/${agentId}/agent.py`, ), path.join( __dirname, integrationsFolderPath, `/langgraph/typescript/examples/src/agents/${agentId}/agent.ts`, ), ], }), {}, ); }, "langgraph-typescript": (agentKeys: string[]) => { return agentKeys.reduce( (acc, agentId) => ({ ...acc, [agentId]: [ path.join( __dirname, integrationsFolderPath, `/langgraph/python/examples/agents/${agentId}/agent.py`, ), path.join( __dirname, integrationsFolderPath, `/langgraph/typescript/examples/src/agents/${agentId}/agent.ts`, ), ], }), {}, ); }, "langgraph-fastapi": (agentKeys: string[]) => { return agentKeys.reduce( (acc, agentId) => ({ ...acc, [agentId]: [ path.join( __dirname, integrationsFolderPath, `/langgraph/python/examples/agents/${agentId}/agent.py`, ), ], }), {}, ); }, "spring-ai": () => ({}), ag2: (agentKeys: string[]) => { return agentKeys.reduce( (acc, agentId) => ({ ...acc, [agentId]: [ path.join( __dirname, integrationsFolderPath, `/ag2/python/examples/server/api/${agentId}.py`, ), ], }), {}, ); }, agno: (agentKeys: string[]) => { return agentKeys.reduce( (acc, agentId) => ({ ...acc, [agentId]: [ path.join( __dirname, integrationsFolderPath, `/agno/python/examples/server/api/${agentId}.py`, ), ], }), {}, ); }, "llama-index": (agentKeys: string[]) => { return agentKeys.reduce( (acc, agentId) => ({ ...acc, [agentId]: [ path.join( __dirname, integrationsFolderPath, `/llama-index/python/examples/server/routers/${agentId}.py`, ), ], }), {}, ); }, crewai: (agentKeys: string[]) => { return agentKeys.reduce( (acc, agentId) => ({ ...acc, [agentId]: [ path.join( __dirname, integrationsFolderPath, `/crew-ai/python/ag_ui_crewai/examples/${agentId}.py`, ), ], }), {}, ); }, "adk-middleware": (agentKeys: string[]) => { return agentKeys.reduce( (acc, agentId) => ({ ...acc, [agentId]: [ path.join( __dirname, integrationsFolderPath, `/adk-middleware/python/examples/server/api/${agentId}.py`, ), ], }), {}, ); }, "aws-strands": (agentKeys: string[]) => { return agentKeys.reduce( (acc, agentId) => ({ ...acc, [agentId]: [ path.join( __dirname, integrationsFolderPath, `/aws-strands/python/examples/server/api/${agentId}.py`, ) ], }), {}, ); }, "microsoft-agent-framework-python": (agentKeys: string[]) => { return agentKeys.reduce( (acc, agentId) => ({ ...acc, [agentId]: [ path.join( __dirname, integrationsFolderPath, `/microsoft-agent-framework/python/examples/agents/dojo.py`, ), ], }), {}, ); }, "microsoft-agent-framework-dotnet": (agentKeys: string[]) => { return agentKeys.reduce( (acc, agentId) => ({ ...acc, [agentId]: [ path.join( __dirname, integrationsFolderPath, `/microsoft-agent-framework/dotnet/examples/AGUIDojoServer/ChatClientAgentFactory.cs`, ), path.join( __dirname, integrationsFolderPath, `/microsoft-agent-framework/dotnet/examples/AGUIDojoServer/SharedStateAgent.cs`, ), path.join( __dirname, integrationsFolderPath, `/microsoft-agent-framework/dotnet/examples/AGUIDojoServer/Program.cs`, ), ], }), {}, ); }, "agent-spec-langgraph": (agentKeys: string[]) => { return agentKeys.reduce( (acc, agentId) => ({ ...acc, [agentId]: [ path.join( __dirname, integrationsFolderPath, `/agent-spec/python/examples/server/api/${agentId}.py`, ), ], }), {}, ); }, "agent-spec-wayflow": (agentKeys: string[]) => { return agentKeys.reduce( (acc, agentId) => ({ ...acc, [agentId]: [ path.join( __dirname, integrationsFolderPath, `/agent-spec/python/examples/server/api/${agentId}.py`, ), ], }), {}, ); }, // A2A integrations use runtime-configured agents without per-feature source files "a2a-basic": () => ({}), "a2a": () => ({}), // Built-in agent with A2UI middleware - uses dedicated API route "builtin": () => ({}), "claude-agent-sdk-python": (agentKeys: string[]) => { return agentKeys.reduce( (acc, agentId) => ({ ...acc, [agentId]: [ path.join( __dirname, integrationsFolderPath, `/claude-agent-sdk/python/examples/agents/${agentId}.py`, ), ], }), {}, ); }, "claude-agent-sdk-typescript": (agentKeys: string[]) => { return agentKeys.reduce( (acc, agentId) => ({ ...acc, [agentId]: [ path.join( __dirname, integrationsFolderPath, `/claude-agent-sdk/typescript/examples/${agentId}.ts`, ), ], }), {}, ); }, "langroid": (agentKeys: string[]) => { return agentKeys.reduce( (acc, agentId) => ({ ...acc, [agentId]: [ path.join( __dirname, integrationsFolderPath, `/langroid/python/examples/server/api/${agentId}.py`, ), ], }), {}, ); }, }; async function runGenerateContent() { const result = {}; for (const agentConfig of agentConfigs) { // Use the parsed agent keys instead of executing the agents function const agentsPerFeatures = agentConfig.agentKeys; const agentFilePaths = agentFilesMapper[agentConfig.id]?.( agentConfig.agentKeys, ); console.log(agentConfig.id, agentFilePaths); if (!agentFilePaths) { continue; } // If agentsPerFeatures is empty but we have agentFilePaths, use the keys from agentFilePaths // This handles cases like Mastra where agents are dynamically discovered const featureIds = agentsPerFeatures.length > 0 ? agentsPerFeatures : Object.keys(agentFilePaths); // Per feature, assign all the frontend files like page.tsx as well as all agent files for (const featureId of featureIds) { const agentFilePathsForFeature = agentFilePaths[featureId] ?? []; const allFiles = [ // Get all frontend files for the feature ...(await getFeatureFrontendFiles(featureId)), // Get the agent (python/TS) file ...(await Promise.all( agentFilePathsForFeature.map(async (f) => await getFile(f)), )), ]; // Filter out empty objects (files that weren't found) // @ts-expect-error -- redundant error about indexing of a new object. result[`${agentConfig.id}::${featureId}`] = allFiles.filter( (file) => Object.keys(file).length > 0 ); } } return result; } /** * Validates that all integration IDs in menuIntegrations have corresponding * entries in agentFilesMapper. Returns true if valid, false otherwise. */ function validateAgentFilesMapper(): boolean { const menuIntegrationIds = menuIntegrations.map((integration) => integration.id); const mapperKeys = new Set(Object.keys(agentFilesMapper)); const missingEntries = menuIntegrationIds.filter((id) => !mapperKeys.has(id)); if (missingEntries.length > 0) { console.error("❌ Missing agentFilesMapper entries for the following integration IDs:"); console.error(""); for (const id of missingEntries) { console.error(` - ${id}`); } console.error(""); console.error("Please add entries for these IDs in:"); console.error(" apps/dojo/scripts/generate-content-json.ts (agentFilesMapper object)"); console.error(""); console.error("Then run `(p)npm run generate-content-json` in the apps/dojo folder."); console.error(""); return false; } return true; } /** * Validates that all feature folders have a README.mdx file. * Returns true if valid, false otherwise. */ function validateFeatureReadmes(): boolean { // Get all unique features across all integrations const allFeatures = new Set(); for (const integration of menuIntegrations) { for (const feature of integration.features) { allFeatures.add(feature); } } const missingReadmes: Array<{ feature: string; integrations: string[] }> = []; for (const feature of allFeatures) { const readmePath = path.join(resolveFeatureDir(feature), "README.mdx"); if (!fs.existsSync(readmePath)) { // Find which integrations use this feature const integrationsUsingFeature = menuIntegrations .filter((i) => (i.features as string[]).includes(feature)) .map((i) => i.id); missingReadmes.push({ feature, integrations: integrationsUsingFeature, }); } } if (missingReadmes.length > 0) { console.error("❌ Missing README.mdx files for the following features:"); console.error(""); for (const { feature, integrations } of missingReadmes) { console.error(` - ${feature}`); console.error(` Used by: ${integrations.join(", ")}`); console.error(` Missing: ${path.relative(path.join(__dirname, ".."), path.join(resolveFeatureDir(feature), "README.mdx"))}`); } console.error(""); console.error("Please create README.mdx files for these features."); console.error("See apps/dojo/src/app/[integrationId]/feature/agentic_chat/README.mdx for an example."); console.error(""); return false; } return true; } (async () => { // Validate that all menuIntegrations have agentFilesMapper entries if (!validateAgentFilesMapper()) { process.exit(1); } // Validate that all features have README.mdx files if (!validateFeatureReadmes()) { process.exit(1); } const result = await runGenerateContent(); fs.writeFileSync( path.join(__dirname, "../src/files.json"), JSON.stringify(result, null, 2), ); console.log("Successfully generated src/files.json"); })(); ================================================ FILE: apps/dojo/scripts/link-cpk.js ================================================ #!/usr/bin/env node const fs = require("fs"); const { execSync } = require("child_process"); const path = require("path"); const cpkPath = process.argv[2] || "./CopilotKit/packages"; if (!fs.existsSync(cpkPath)) { console.error(`CopilotKit packages path ${cpkPath} does not exist`); process.exit(1); } // Detect whether the CopilotKit repo uses the old v1/v2 split or the new flat structure. const hasV1Subdir = fs.existsSync(path.join(cpkPath, "v1")); const hasV2Subdir = fs.existsSync(path.join(cpkPath, "v2")); const isOldStructure = hasV1Subdir && hasV2Subdir; const namespaceDirs = {}; if (isOldStructure) { // Old CopilotKit structure: v1/ and v2/ subdirs namespaceDirs["@copilotkit/"] = path.join(cpkPath, "v1"); namespaceDirs["@copilotkitnext/"] = path.join(cpkPath, "v2"); } else { // New flat structure (CopilotKit PR #3409): all packages under @copilotkit/ namespaceDirs["@copilotkit/"] = cpkPath; } const gitRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf-8", cwd: __dirname, }).trim(); const dojoDir = path.join(gitRoot, "apps/dojo"); function linkCopilotKit() { const pkgPath = path.join(dojoDir, "package.json"); const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); let success = true; for (const [prefix, pkgDir] of Object.entries(namespaceDirs)) { const relative = `./${path.relative(dojoDir, pkgDir)}`; const packages = Object.keys(pkg.dependencies).filter((dep) => dep.startsWith(prefix), ); packages.forEach((packageName) => { const folderName = packageName.replace(prefix, ""); if (!fs.existsSync(path.join(pkgDir, folderName))) { console.error( `Package ${packageName} does not exist in ${pkgDir}`, ); success = false; return; } pkg.dependencies[packageName] = path.join(relative, folderName); }); } if (!success) { console.error("One or more packages do not exist in the CopilotKit repo!"); process.exit(1); } fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2)); // Summary for (const [prefix, pkgDir] of Object.entries(namespaceDirs)) { const count = Object.keys(pkg.dependencies).filter((d) => d.startsWith(prefix), ).length; console.log(`Linked ${count} ${prefix}* packages from ${pkgDir}`); } } linkCopilotKit(); ================================================ FILE: apps/dojo/scripts/prep-dojo-everything.js ================================================ #!/usr/bin/env node const { execSync } = require("child_process"); const path = require("path"); const concurrently = require("concurrently"); // Parse command line arguments const args = process.argv.slice(2); const showHelp = args.includes("--help") || args.includes("-h"); const dryRun = args.includes("--dry-run"); // selection controls function parseList(flag) { const idx = args.indexOf(flag); if (idx !== -1 && args[idx + 1]) { return args[idx + 1] .split(",") .map((s) => s.trim()) .filter(Boolean); } return null; } const onlyList = parseList("--only") || parseList("--include"); const excludeList = parseList("--exclude") || []; if (showHelp) { console.log(` Usage: node prep-dojo-everything.js [options] Options: --dry-run Show what would be installed without actually running --only list Comma-separated services to include (defaults to all) --exclude list Comma-separated services to exclude --help, -h Show this help message Examples: node prep-dojo-everything.js node prep-dojo-everything.js --dry-run node prep-dojo-everything.js --only dojo,agno node prep-dojo-everything.js --exclude crew-ai,mastra `); process.exit(0); } const gitRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim(); const integrationsRoot = path.join(gitRoot, "integrations"); const middlewaresRoot = path.join(gitRoot, "middlewares"); // Define all prep targets keyed by a stable id const ALL_TARGETS = { "server-starter": { command: "uv sync", name: "Server Starter", cwd: path.join(integrationsRoot, "server-starter/python/examples"), }, "server-starter-all": { command: "uv sync", name: "Server AF", cwd: path.join(integrationsRoot, "server-starter-all-features/python/examples"), }, ag2: { command: "uv sync", name: "AG2", cwd: path.join(integrationsRoot, "ag2/python/examples"), }, agno: { command: "uv sync", name: "Agno", cwd: path.join(integrationsRoot, "agno/python/examples"), }, "crew-ai": { command: "poetry install", name: "CrewAI", cwd: path.join(integrationsRoot, "crew-ai/python"), }, 'langroid': { command: 'uv sync', name: 'Langroid', cwd: path.join(integrationsRoot, 'langroid/python/examples'), }, "langgraph-fastapi": { command: "uv sync", name: "LG FastAPI", cwd: path.join(integrationsRoot, "langgraph/python/examples"), }, "langgraph-platform-typescript": { command: "pnpm install", name: "LG Platform TS", cwd: path.join(integrationsRoot, "langgraph/typescript/examples"), }, "llama-index": { command: "uv sync", name: "Llama Index", cwd: path.join(integrationsRoot, "llama-index/python/examples"), }, mastra: { command: "pnpm install --no-frozen-lockfile", name: "Mastra", cwd: path.join(integrationsRoot, "mastra/typescript/examples"), }, "pydantic-ai": { command: "uv sync", name: "Pydantic AI", cwd: path.join(integrationsRoot, "pydantic-ai/python/examples"), }, "aws-strands": { command: "poetry install", name: "AWS Strands", cwd: path.join(integrationsRoot, "aws-strands/python/examples"), }, "adk-middleware": { command: "uv sync", name: "ADK Middleware", cwd: path.join(integrationsRoot, "adk-middleware/python/examples"), }, "a2a-middleware": { command: "uv sync", name: "A2A Middleware", cwd: path.join(middlewaresRoot, "a2a-middleware/examples"), }, dojo: { command: "pnpm install --no-frozen-lockfile && npx nx run demo-viewer:build", name: "Dojo", cwd: gitRoot, }, "dojo-dev": { command: "pnpm install --no-frozen-lockfile && npx nx run-many -t build --exclude=demo-viewer", name: "Dojo (dev)", cwd: gitRoot, }, "claude-agent-sdk-python": { command: "uv sync", name: "Claude Agent SDK (Python)", cwd: path.join(integrationsRoot, "claude-agent-sdk/python/examples"), }, "claude-agent-sdk-typescript": { command: "pnpm install", name: "Claude Agent SDK (TypeScript)", cwd: path.join(integrationsRoot, "claude-agent-sdk/typescript"), }, "microsoft-agent-framework-python": { command: "uv sync", name: "Microsoft Agent Framework (Python)", cwd: path.join(integrationsRoot, "microsoft-agent-framework/python/examples"), }, "microsoft-agent-framework-dotnet": { command: "dotnet restore AGUIDojoServer/AGUIDojoServer.csproj && dotnet build AGUIDojoServer/AGUIDojoServer.csproj", name: "Microsoft Agent Framework (.NET)", cwd: path.join(integrationsRoot, "microsoft-agent-framework/dotnet/examples"), }, }; function printDryRunServices(procs) { console.log("Dry run - would install dependencies for the following services:"); procs.forEach((proc) => { console.log(` - ${proc.name} (${proc.cwd})`); console.log(` Command: ${proc.command}`); console.log(""); }); process.exit(0); } async function main() { // determine selection let selectedKeys = Object.keys(ALL_TARGETS); if (onlyList && onlyList.length) { selectedKeys = onlyList; } if (excludeList && excludeList.length) { selectedKeys = selectedKeys.filter((k) => !excludeList.includes(k)); } if (selectedKeys.includes("dojo") && selectedKeys.includes("dojo-dev")) { selectedKeys= selectedKeys.filter(x => x != "dojo-dev"); } // Build procs list, warning on unknown keys const procs = []; for (const key of selectedKeys) { const target = ALL_TARGETS[key]; if (!target) { console.warn(`Skipping unknown service: ${key}`); continue; } procs.push(target); } if (dryRun) { printDryRunServices(procs); } // Separate pnpm targets from others to avoid concurrent install races. // Multiple pnpm installs within the same workspace race on the shared // node_modules/.pnpm/ directory, causing ENOENT errors. const pnpmProcs = []; const otherProcs = []; for (const proc of procs) { if (proc.command.startsWith("pnpm")) { pnpmProcs.push(proc); } else { otherProcs.push(proc); } } // Run pnpm targets sequentially to avoid races on shared node_modules for (const proc of pnpmProcs) { console.log(`\n=== [${proc.name}] ${proc.command} ===`); try { execSync(proc.command, { cwd: proc.cwd, stdio: "inherit" }); } catch (err) { console.error(`[${proc.name}] Failed: ${err.message}`); process.exit(1); } } // Run remaining targets concurrently (uv, poetry, dotnet — all independent) if (otherProcs.length === 0) { process.exit(0); } const { result } = concurrently(otherProcs); result .then(() => process.exit(0)) .catch((err) => { console.error(err); process.exit(1); }); } main(); ================================================ FILE: apps/dojo/scripts/run-dojo-everything.js ================================================ #!/usr/bin/env node const { execSync } = require("child_process"); const path = require("path"); const concurrently = require("concurrently"); // Pinned: @langchain/langgraph-api@1.1.14 regressed schema extraction, causing // worker timeouts on CI runners. Re-evaluate when a newer version fixes the issue. const LANGGRAPH_CLI_VERSION = '1.1.13'; // Parse command line arguments const args = process.argv.slice(2); const showHelp = args.includes("--help") || args.includes("-h"); const dryRun = args.includes("--dry-run"); function parseList(flag) { const idx = args.indexOf(flag); if (idx !== -1 && args[idx + 1]) { return args[idx + 1] .split(",") .map((s) => s.trim()) .filter(Boolean); } return null; } const onlyList = parseList("--only") || parseList("--include"); const excludeList = parseList("--exclude") || []; if (showHelp) { console.log(` Usage: node run-dojo-everything.js [options] Options: --dry-run Show what would be started without actually running --only list Comma-separated services to include (defaults to all) --exclude list Comma-separated services to exclude --help, -h Show this help message Examples: node run-dojo-everything.js node run-dojo-everything.js --dry-run node run-dojo-everything.js --only dojo,server-starter node run-dojo-everything.js --exclude crew-ai,mastra `); process.exit(0); } const gitRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf-8", }).trim(); const integrationsRoot = path.join(gitRoot, "integrations"); const middlewaresRoot = path.join(gitRoot, "middlewares"); // Define all runnable services keyed by a stable id const ALL_SERVICES = { 'server-starter': [{ command: 'uv run dev', name: 'Server Starter', cwd: path.join(integrationsRoot, 'server-starter/python/examples'), env: { PORT: 8000 }, }], 'server-starter-all': [{ command: 'uv run dev', name: 'Server AF', cwd: path.join(integrationsRoot, 'server-starter-all-features/python/examples'), env: { PORT: 8001 }, }], 'ag2': [{ command: 'uv run dev', name: 'AG2', cwd: path.join(integrationsRoot, 'ag2/python/examples'), env: { PORT: 8018 }, }], 'agno': [{ command: 'uv run dev', name: 'Agno', cwd: path.join(integrationsRoot, 'agno/python/examples'), env: { PORT: 8002 }, }], 'crew-ai': [{ command: 'poetry run dev', name: 'CrewAI', cwd: path.join(integrationsRoot, 'crew-ai/python'), env: { PORT: 8003 }, }], 'langgraph-fastapi': [{ command: 'uv run dev', name: 'LG FastAPI', cwd: path.join(integrationsRoot, 'langgraph/python/examples'), env: { PORT: 8004 }, }], 'langgraph-platform-python': [{ command: `pnpx @langchain/langgraph-cli@${LANGGRAPH_CLI_VERSION} dev --no-browser --host 127.0.0.1 --port 8005`, name: 'LG Platform Py', cwd: path.join(integrationsRoot, 'langgraph/python/examples'), env: { PORT: 8005 }, }], 'langgraph-platform-typescript': [{ command: `pnpx @langchain/langgraph-cli@${LANGGRAPH_CLI_VERSION} dev --no-browser --host 127.0.0.1 --port 8006`, name: 'LG Platform TS', cwd: path.join(integrationsRoot, 'langgraph/typescript/examples'), env: { PORT: 8006 }, }], 'langroid': [{ command: 'uv run dev', name: 'Langroid', cwd: path.join(integrationsRoot, 'langroid/python/examples'), env: { PORT: 8021 }, }], 'llama-index': [{ command: 'uv run dev', name: 'Llama Index', cwd: path.join(integrationsRoot, 'llama-index/python/examples'), env: { PORT: 8007 }, }], 'mastra': [{ command: 'npm run dev', name: 'Mastra', cwd: path.join(integrationsRoot, 'mastra/typescript/examples'), env: { PORT: 8008, OPENAI_API_KEY: process.env.OPENAI_API_KEY || 'test-key', ...(!process.env.OPENAI_API_KEY && { OPENAI_BASE_URL: 'http://localhost:5555/v1' }), }, }], 'pydantic-ai': [{ command: 'uv run dev', name: 'Pydantic AI', cwd: path.join(integrationsRoot, 'pydantic-ai/python/examples'), env: { PORT: 8009 }, }], 'aws-strands': [{ command: 'poetry run dev', name: 'AWS Strands', cwd: path.join(integrationsRoot, 'aws-strands/python/examples'), env: { PORT: 8017 }, }], 'adk-middleware': [{ command: 'uv run dev', name: 'ADK Middleware', cwd: path.join(integrationsRoot, 'adk-middleware/python/examples'), env: { PORT: 8010 }, }], 'a2a-middleware': [{ command: 'uv run buildings_management.py', name: 'A2A Middleware: Buildings Management', cwd: path.join(middlewaresRoot, "a2a-middleware/examples"), env: { PORT: 8011 }, }, { command: 'uv run finance.py', name: 'A2A Middleware: Finance', cwd: path.join(middlewaresRoot, "a2a-middleware/examples"), env: { PORT: 8012 }, }, { command: 'uv run it.py', name: 'A2A Middleware: IT', cwd: path.join(middlewaresRoot, "a2a-middleware/examples"), env: { PORT: 8013 }, }, { command: 'uv run orchestrator.py', name: 'A2A Middleware: Orchestrator', cwd: path.join(middlewaresRoot, "a2a-middleware/examples"), env: { PORT: 8014 }, }], 'claude-agent-sdk-python': [{ command: 'uv run dev', name: 'Claude Agent SDK (Python)', cwd: path.join(integrationsRoot, 'claude-agent-sdk/python/examples'), env: { PORT: 8019, ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || 'sk-ant-api03-test-key-for-llmock-000000000000000000000000000000000000000000000000-000000000000AA', ...(!process.env.ANTHROPIC_API_KEY && { ANTHROPIC_BASE_URL: 'http://localhost:5555' }), }, }], 'claude-agent-sdk-typescript': [{ command: 'npx tsx examples/server.ts', name: 'Claude Agent SDK (TypeScript)', cwd: path.join(integrationsRoot, 'claude-agent-sdk/typescript'), env: { PORT: 8020, ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || 'sk-ant-api03-test-key-for-llmock-000000000000000000000000000000000000000000000000-000000000000AA', ...(!process.env.ANTHROPIC_API_KEY && { ANTHROPIC_BASE_URL: 'http://localhost:5555' }), }, }], 'microsoft-agent-framework-python': [{ command: 'uv run dev', name: 'Microsoft Agent Framework (Python)', cwd: path.join(integrationsRoot, 'microsoft-agent-framework/python/examples'), env: { PORT: 8015 }, }], 'microsoft-agent-framework-dotnet': [{ command: 'dotnet run --project AGUIDojoServer/AGUIDojoServer.csproj --urls "http://localhost:8889" --no-build', name: 'Microsoft Agent Framework (.NET)', cwd: path.join(integrationsRoot, 'microsoft-agent-framework/dotnet/examples'), env: { PORT: 8016 }, }], 'dojo': [{ command: 'pnpm run start', name: 'Dojo', cwd: path.join(gitRoot, 'apps/dojo'), env: { PORT: 9999, AG2_URL: 'http://localhost:8018', SERVER_STARTER_URL: 'http://localhost:8000', SERVER_STARTER_ALL_FEATURES_URL: 'http://localhost:8001', AGNO_URL: 'http://localhost:8002', CREW_AI_URL: 'http://localhost:8003', LANGGRAPH_FAST_API_URL: 'http://localhost:8004', LANGGRAPH_PYTHON_URL: 'http://localhost:8005', LANGGRAPH_TYPESCRIPT_URL: 'http://localhost:8006', LLAMA_INDEX_URL: 'http://localhost:8007', MASTRA_URL: 'http://localhost:8008', PYDANTIC_AI_URL: 'http://localhost:8009', ADK_MIDDLEWARE_URL: 'http://localhost:8010', A2A_MIDDLEWARE_BUILDINGS_MANAGEMENT_URL: 'http://localhost:8011', A2A_MIDDLEWARE_FINANCE_URL: 'http://localhost:8012', A2A_MIDDLEWARE_IT_URL: 'http://localhost:8013', A2A_MIDDLEWARE_ORCHESTRATOR_URL: 'http://localhost:8014', AWS_STRANDS_URL: 'http://localhost:8017', CLAUDE_AGENT_SDK_PYTHON_URL: 'http://localhost:8019', CLAUDE_AGENT_SDK_TYPESCRIPT_URL: 'http://localhost:8020', LANGROID_URL: 'http://localhost:8021', NEXT_PUBLIC_CUSTOM_DOMAIN_TITLE: 'cpkdojo.local___CopilotKit Feature Viewer', }, }], 'dojo-dev': [{ command: 'pnpm run dev --filter=demo-viewer...', name: 'Dojo (dev)', cwd: gitRoot, env: { PORT: 9999, AG2_URL: 'http://localhost:8018', SERVER_STARTER_URL: 'http://localhost:8000', SERVER_STARTER_ALL_FEATURES_URL: 'http://localhost:8001', AGNO_URL: 'http://localhost:8002', CREW_AI_URL: 'http://localhost:8003', LANGGRAPH_FAST_API_URL: 'http://localhost:8004', LANGGRAPH_PYTHON_URL: 'http://localhost:8005', LANGGRAPH_TYPESCRIPT_URL: 'http://localhost:8006', LLAMA_INDEX_URL: 'http://localhost:8007', MASTRA_URL: 'http://localhost:8008', PYDANTIC_AI_URL: 'http://localhost:8009', ADK_MIDDLEWARE_URL: 'http://localhost:8010', A2A_MIDDLEWARE_BUILDINGS_MANAGEMENT_URL: 'http://localhost:8011', A2A_MIDDLEWARE_FINANCE_URL: 'http://localhost:8012', A2A_MIDDLEWARE_IT_URL: 'http://localhost:8013', A2A_MIDDLEWARE_ORCHESTRATOR_URL: 'http://localhost:8014', AWS_STRANDS_URL: 'http://localhost:8017', CLAUDE_AGENT_SDK_PYTHON_URL: 'http://localhost:8019', CLAUDE_AGENT_SDK_TYPESCRIPT_URL: 'http://localhost:8020', LANGROID_URL: 'http://localhost:8021', NEXT_PUBLIC_CUSTOM_DOMAIN_TITLE: 'cpkdojo.local___CopilotKit Feature Viewer', }, }], }; function printDryRunServices(procs) { console.log("Dry run - would start the following services:"); procs.forEach((proc) => { console.log(` - ${proc.name} (${proc.cwd})`); console.log(` Command: ${proc.command}`); console.log(` Environment variables:`); if (proc.env) { Object.entries(proc.env).forEach(([key, value]) => { console.log(` ${key}: ${value}`); }); } else { console.log(" No environment variables specified."); } console.log(""); }); process.exit(0); } async function main() { // determine selection let selectedKeys = Object.keys(ALL_SERVICES); if (onlyList && onlyList.length) { selectedKeys = onlyList; } if (excludeList && excludeList.length) { selectedKeys = selectedKeys.filter((k) => !excludeList.includes(k)); } if (selectedKeys.includes("dojo") && selectedKeys.includes("dojo-dev")) { selectedKeys = selectedKeys.filter((x) => x != "dojo-dev"); } // LLMock: inject OPENAI_BASE_URL, OPENAI_API_BASE, and OPENAI_API_KEY // defaults so all framework agents route OpenAI API calls to the mock server // when running. OPENAI_API_BASE is the legacy env var used by llama-index // (via resolve_openai_credentials) and litellm (used by crew-ai). const openaiEnvDefaults = { OPENAI_BASE_URL: process.env.OPENAI_BASE_URL || 'http://localhost:5555/v1', OPENAI_API_BASE: process.env.OPENAI_API_BASE || 'http://localhost:5555/v1', OPENAI_API_KEY: process.env.OPENAI_API_KEY || 'sk-mock', }; // LLMock: inject GOOGLE_GEMINI_BASE_URL so ADK middleware agents (which keep // their native Gemini model strings) route to the mock server via the genai // client's built-in env var support. No /v1 suffix — the genai client appends // the full /v1beta/models/{model}:generateContent path itself. const geminiEnvDefaults = { GOOGLE_GEMINI_BASE_URL: process.env.GOOGLE_GEMINI_BASE_URL || 'http://localhost:5555', GOOGLE_API_KEY: process.env.GOOGLE_API_KEY || 'fake-gemini-key', }; // Build processes, warn for unknown keys const procs = []; for (const key of selectedKeys) { const svcs = ALL_SERVICES[key]; if (!svcs || svcs.length === 0) { console.warn(`Skipping unknown service: ${key}`); continue; } for (const svc of svcs) { svc.env = { ...openaiEnvDefaults, ...geminiEnvDefaults, ...svc.env }; } procs.push(...svcs); } if (dryRun) { printDryRunServices(procs); } console.log("Starting services: ", procs.map((p) => p.name).join(", ")); const { result } = concurrently(procs, { killOthersOn: ["failure", "success"], }); result .then(() => process.exit(0)) .catch((err) => { console.error(err); process.exit(1); }); } main(); ================================================ FILE: apps/dojo/src/agents.ts ================================================ import "server-only"; import type { AbstractAgent } from "@ag-ui/client"; import type { AgentsMap } from "./types/agents"; import { mapAgents } from "./utils/agents"; import { MiddlewareStarterAgent } from "@ag-ui/middleware-starter"; import { ServerStarterAgent } from "@ag-ui/server-starter"; import { ServerStarterAllFeaturesAgent } from "@ag-ui/server-starter-all-features"; import { MastraClient } from "@mastra/client-js"; import { MastraAgent } from "@ag-ui/mastra"; // import { VercelAISDKAgent } from "@ag-ui/vercel-ai-sdk"; // import { openai } from "@ai-sdk/openai"; import { LangGraphAgent, LangGraphHttpAgent } from "@ag-ui/langgraph"; import { AgnoAgent } from "@ag-ui/agno"; import { LlamaIndexAgent } from "@ag-ui/llamaindex"; import { CrewAIAgent } from "@ag-ui/crewai"; import getEnvVars from "./env"; import { mastra } from "./mastra"; import { PydanticAIAgent } from "@ag-ui/pydantic-ai"; import { ADKAgent } from "@ag-ui/adk"; import { SpringAiAgent } from "@ag-ui/spring-ai"; import { HttpAgent } from "@ag-ui/client"; import { A2AMiddlewareAgent } from "@ag-ui/a2a-middleware"; import { AWSStrandsAgent } from "@ag-ui/aws-strands"; import { A2AAgent } from "@ag-ui/a2a"; import { A2AClient } from "@a2a-js/sdk/client"; import { LangChainAgent } from "@ag-ui/langchain"; import { BuiltInAgent } from "@copilotkit/runtime/v2"; import { A2UIMiddleware, A2UI_PROMPT } from "@ag-ui/a2ui-middleware"; import { Ag2Agent } from "@ag-ui/ag2"; import { LangroidHttpAgent } from "@ag-ui/langroid"; const envVars = getEnvVars(); export const agentsIntegrations = { "middleware-starter": async () => ({ agentic_chat: new MiddlewareStarterAgent(), }), "pydantic-ai": async () => mapAgents( (path) => new PydanticAIAgent({ url: `${envVars.pydanticAIUrl}/${path}` }), { agentic_chat: "agentic_chat", agentic_generative_ui: "agentic_generative_ui", human_in_the_loop: "human_in_the_loop", // TODO: Re-enable this once production builds no longer break // predictive_state_updates: "predictive_state_updates", shared_state: "shared_state", tool_based_generative_ui: "tool_based_generative_ui", backend_tool_rendering: "backend_tool_rendering", } ), "server-starter": async () => ({ agentic_chat: new ServerStarterAgent({ url: envVars.serverStarterUrl }), }), "adk-middleware": async () => mapAgents( (path) => new ADKAgent({ url: `${envVars.adkMiddlewareUrl}/${path}` }), { agentic_chat: "chat", agentic_generative_ui: "adk-agentic-generative-ui", tool_based_generative_ui: "adk-tool-based-generative-ui", human_in_the_loop: "adk-human-in-loop-agent", backend_tool_rendering: "backend_tool_rendering", shared_state: "adk-shared-state-agent", predictive_state_updates: "adk-predictive-state-agent", } ), "server-starter-all-features": async () => mapAgents( (path) => new ServerStarterAllFeaturesAgent({ url: `${envVars.serverStarterAllFeaturesUrl}/${path}` }), { agentic_chat: "agentic_chat", // TODO: Add agent for agentic_chat_reasoning backend_tool_rendering: "backend_tool_rendering", human_in_the_loop: "human_in_the_loop", agentic_generative_ui: "agentic_generative_ui", tool_based_generative_ui: "tool_based_generative_ui", shared_state: "shared_state", predictive_state_updates: "predictive_state_updates", } ), mastra: async () => { const mastraClient = new MastraClient({ baseUrl: envVars.mastraUrl, }); return MastraAgent.getRemoteAgents({ // Cast needed: pnpm may resolve separate @mastra/client-js installations // for dojo vs @ag-ui/mastra, causing nominal type mismatch on private fields mastraClient: mastraClient as any, resourceId: "mastra-agent-remote" }) as Promise>; }, "mastra-agent-local": async () => { return MastraAgent.getLocalAgents({ // Cast needed: pnpm may resolve separate @mastra/core installations // for dojo vs @ag-ui/mastra, causing nominal type mismatch on private fields mastra: mastra as any, resourceId: "mastra-agent-local" }) as Record<"agentic_chat" | "backend_tool_rendering" | "human_in_the_loop" | "shared_state" | "tool_based_generative_ui", AbstractAgent>; }, // Disabled until we can support Vercel AI SDK v5 // "vercel-ai-sdk": async () => ({ // agentic_chat: new VercelAISDKAgent({ model: openai("gpt-4o") }), // }), langgraph: async () => ({ ...mapAgents( (graphId) => { return new LangGraphAgent({ deploymentUrl: envVars.langgraphPythonUrl, graphId }) }, { agentic_chat: "agentic_chat", backend_tool_rendering: "backend_tool_rendering", agentic_generative_ui: "agentic_generative_ui", human_in_the_loop: "human_in_the_loop", predictive_state_updates: "predictive_state_updates", shared_state: "shared_state", tool_based_generative_ui: "tool_based_generative_ui", subgraphs: "subgraphs", } ), // Uses LangGraphHttpAgent instead of LangGraphAgent agentic_chat_reasoning: new LangGraphHttpAgent({ url: `${envVars.langgraphPythonUrl}/agent/agentic_chat_reasoning`, }), // A2UI Chat with middleware a2ui_chat: (() => { const agent = new LangGraphAgent({ deploymentUrl: envVars.langgraphPythonUrl, graphId: "a2ui_chat" }); agent.use(new A2UIMiddleware({ injectA2UITool: true })); return agent; })(), }), "langgraph-fastapi": async () => ({ ...mapAgents( (path) => new LangGraphHttpAgent({ url: `${envVars.langgraphFastApiUrl}/agent/${path}` }), { agentic_chat: "agentic_chat", backend_tool_rendering: "backend_tool_rendering", agentic_generative_ui: "agentic_generative_ui", human_in_the_loop: "human_in_the_loop", predictive_state_updates: "predictive_state_updates", shared_state: "shared_state", tool_based_generative_ui: "tool_based_generative_ui", subgraphs: "subgraphs", } ), // A2UI Chat with middleware - uses backend tool auto-detection (no injected tool) a2ui_chat: (() => { const agent = new LangGraphHttpAgent({ url: `${envVars.langgraphFastApiUrl}/agent/a2ui_chat` }); agent.use(new A2UIMiddleware()); return agent; })(), // A2UI Chat with middleware - uses injected frontend tool a2ui_chat_inject: (() => { const agent = new LangGraphHttpAgent({ url: `${envVars.langgraphFastApiUrl}/agent/a2ui_chat` }); agent.use(new A2UIMiddleware({ injectA2UITool: true })); return agent; })(), }), "langgraph-typescript": async () => mapAgents( (graphId) => { return new LangGraphAgent({ deploymentUrl: envVars.langgraphTypescriptUrl, graphId }) }, { agentic_chat: "agentic_chat", // TODO: Add agent for backend_tool_rendering agentic_generative_ui: "agentic_generative_ui", human_in_the_loop: "human_in_the_loop", predictive_state_updates: "predictive_state_updates", shared_state: "shared_state", tool_based_generative_ui: "tool_based_generative_ui", subgraphs: "subgraphs", } ), // TODO: @ranst91 Enable `langchain` integration in apps/dojo/src/menu.ts once ready langchain: async () => { const agent = new LangChainAgent({ chainFn: async ({ messages, tools, threadId }) => { const { ChatOpenAI } = await import("@langchain/openai"); const chatOpenAI = new ChatOpenAI({ model: "gpt-4o" }); const model = chatOpenAI.bindTools(tools, { strict: true, }); return model.stream(messages, { tools, metadata: { conversation_id: threadId } }); }, }); return { agentic_chat: agent, tool_based_generative_ui: agent, }; }, agno: async () => mapAgents( (path) => new AgnoAgent({ url: `${envVars.agnoUrl}/${path}/agui` }), { agentic_chat: "agentic_chat", tool_based_generative_ui: "tool_based_generative_ui", backend_tool_rendering: "backend_tool_rendering", human_in_the_loop: "human_in_the_loop", } ), "spring-ai": async () => mapAgents( (path) => new SpringAiAgent({ url: `${envVars.springAiUrl}/${path}/agui` }), { agentic_chat: "agentic_chat", shared_state: "shared_state", tool_based_generative_ui: "tool_based_generative_ui", human_in_the_loop: "human_in_the_loop", agentic_generative_ui: "agentic_generative_ui", } ), "llama-index": async () => mapAgents( (path) => new LlamaIndexAgent({ url: `${envVars.llamaIndexUrl}/${path}/run` }), { agentic_chat: "agentic_chat", human_in_the_loop: "human_in_the_loop", agentic_generative_ui: "agentic_generative_ui", shared_state: "shared_state", backend_tool_rendering: "backend_tool_rendering", } ), crewai: async () => mapAgents( (path) => new CrewAIAgent({ url: `${envVars.crewAiUrl}/${path}` }), { agentic_chat: "agentic_chat", // TODO: Add agent for backend_tool_rendering // backend_tool_rendering: "backend_tool_rendering", human_in_the_loop: "human_in_the_loop", tool_based_generative_ui: "tool_based_generative_ui", agentic_generative_ui: "agentic_generative_ui", shared_state: "shared_state", predictive_state_updates: "predictive_state_updates", } ), "agent-spec-langgraph": async () => mapAgents( (path) => { const agent = new HttpAgent({ url: `${envVars.agentSpecUrl}/langgraph/${path}`, }); if (path === "a2ui_chat") { agent.use(new A2UIMiddleware({ injectA2UITool: true })); } return agent; }, { agentic_chat: "agentic_chat", backend_tool_rendering: "backend_tool_rendering", human_in_the_loop: "human_in_the_loop", tool_based_generative_ui: "tool_based_generative_ui", a2ui_chat: "a2ui_chat", } ), "agent-spec-wayflow": async () => mapAgents( (path) => { const agent = new HttpAgent({ url: `${envVars.agentSpecUrl}/wayflow/${path}`, }); if (path === "a2ui_chat") { agent.use(new A2UIMiddleware({ injectA2UITool: true })); } return agent; }, { agentic_chat: "agentic_chat", backend_tool_rendering: "backend_tool_rendering", tool_based_generative_ui: "tool_based_generative_ui", human_in_the_loop: "human_in_the_loop", a2ui_chat: "a2ui_chat", } ), "microsoft-agent-framework-python": async () => mapAgents( (path) => new HttpAgent({ url: `${envVars.agentFrameworkPythonUrl}/${path}` }), { agentic_chat: "agentic_chat", backend_tool_rendering: "backend_tool_rendering", human_in_the_loop: "human_in_the_loop", agentic_generative_ui: "agentic_generative_ui", shared_state: "shared_state", tool_based_generative_ui: "tool_based_generative_ui", predictive_state_updates: "predictive_state_updates", } ), "a2a-basic": async () => { const a2aClient = new A2AClient(envVars.a2aUrl); return { vnext_chat: new A2AAgent({ description: "Direct A2A agent", a2aClient, debug: process.env.NODE_ENV !== "production", }), }; }, "microsoft-agent-framework-dotnet": async () => mapAgents( (path) => new HttpAgent({ url: `${envVars.agentFrameworkDotnetUrl}/${path}` }), { agentic_chat: "agentic_chat", backend_tool_rendering: "backend_tool_rendering", human_in_the_loop: "human_in_the_loop", agentic_generative_ui: "agentic_generative_ui", shared_state: "shared_state", tool_based_generative_ui: "tool_based_generative_ui", predictive_state_updates: "predictive_state_updates", } ), a2a: async () => { // A2A agents: building management, finance, it agents const agentUrls = [ envVars.a2aMiddlewareBuildingsManagementUrl, envVars.a2aMiddlewareFinanceUrl, envVars.a2aMiddlewareItUrl, ]; // AGUI orchestration/routing agent const orchestrationAgent = new HttpAgent({ url: envVars.a2aMiddlewareOrchestratorUrl, }); return { a2a_chat: new A2AMiddlewareAgent({ description: "Middleware that connects to remote A2A agents", agentUrls, orchestrationAgent, instructions: ` You are an HR agent. You are responsible for hiring employees and other typical HR tasks. It's very important to contact all the departments necessary to complete the task. For example, to hire an employee, you must contact all 3 departments: Finance, IT and Buildings Management. Help the Buildings Management department to find a table. You can make tool calls on behalf of other agents. DO NOT FORGET TO COMMUNICATE BACK TO THE RELEVANT AGENT IF MAKING A TOOL CALL ON BEHALF OF ANOTHER AGENT!!! When choosing a seat with the buildings management agent, You MUST use the \`pickTable\` tool to have the user pick a seat. The buildings management agent will then use the \`pickSeat\` tool to pick a seat. `, }), }; }, "aws-strands": async () => ({ // Different URL pattern (hyphens) and one has debug:true, so not using mapAgents ...mapAgents( (path) => new AWSStrandsAgent({ url: `${envVars.awsStrandsUrl}/${path}/` }), { agentic_chat: "agentic-chat", backend_tool_rendering: "backend-tool-rendering", agentic_generative_ui: "agentic-generative-ui", shared_state: "shared-state", } ), human_in_the_loop: new AWSStrandsAgent({ url: `${envVars.awsStrandsUrl}/human-in-the-loop`, debug: true }), }), // Built-in Agent with A2UI support builtin: async () => { const systemPrompt = `You are a helpful assistant that can render rich UI surfaces using the A2UI protocol. When the user asks for visual content (cards, forms, lists, buttons, etc.), use the send_a2ui_json_to_client tool to render A2UI surfaces. ${A2UI_PROMPT}`; const builtInAgent = new BuiltInAgent({ model: "openai/gpt-4o", prompt: systemPrompt, }); builtInAgent.use(new A2UIMiddleware({ injectA2UITool: true })); return { a2ui_chat: builtInAgent as unknown as AbstractAgent, }; }, "ag2": async () => mapAgents( (path) => new Ag2Agent({ url: `${envVars.ag2Url}/${path}` }), { agentic_chat: "agentic_chat", backend_tool_rendering: "backend_tool_rendering", human_in_the_loop: "human_in_the_loop", agentic_generative_ui: "agentic_generative_ui", shared_state: "shared_state", tool_based_generative_ui: "tool_based_generative_ui", } ), "claude-agent-sdk-python": async () => mapAgents( (path) => new HttpAgent({ url: `${envVars.claudeAgentSdkPythonUrl}/${path}` }), { agentic_chat: "agentic_chat", backend_tool_rendering: "backend_tool_rendering", shared_state: "shared_state", human_in_the_loop: "human_in_the_loop", tool_based_generative_ui: "tool_based_generative_ui", } ), "claude-agent-sdk-typescript": async () => mapAgents( (path) => new HttpAgent({ url: `${envVars.claudeAgentSdkTypescriptUrl}/${path}` }), { agentic_chat: "agentic_chat", backend_tool_rendering: "backend_tool_rendering", shared_state: "shared_state", human_in_the_loop: "human_in_the_loop", tool_based_generative_ui: "tool_based_generative_ui", } ), langroid: async () => mapAgents( (path) => new LangroidHttpAgent({ url: `${envVars.langroidUrl}/${path}/` }), { agentic_chat: "agentic_chat", backend_tool_rendering: "backend_tool_rendering", agentic_generative_ui: "agentic_generative_ui", shared_state: "shared_state", } ), } satisfies AgentsMap; ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v1)/v1_agentic_chat/README.mdx ================================================ # 🤖 V1 Agentic Chat ## What This Demo Shows This demo verifies **CopilotKit v1 API compatibility**. It uses the original v1 components (`CopilotKit` provider and `CopilotChat`) to ensure that v1 APIs continue to work correctly against the current runtime. 1. **V1 Provider**: Uses `CopilotKit` from `@copilotkit/react-core` with the `agent` prop for agent selection 2. **V1 Chat UI**: Uses `CopilotChat` from `@copilotkit/react-ui` with v1 styling 3. **Same Backend**: Connects to the same runtime endpoint as v2, validating backward compatibility ## How to Interact This is a standard chat interface — type a message and the agent will respond conversationally, just like the v2 agentic chat demo. ## ✨ V1 Compatibility **What's happening technically:** - The v1 `CopilotKit` provider connects to the same `/api/copilotkit/[integration]` endpoint - The v1 chat UI renders with v1 CSS classes (`.copilotKitInput`, `.copilotKitAssistantMessage`, etc.) - The agent selected via the `agent` prop maps to the same `agentic_chat` backend agent - This ensures that applications built with v1 APIs continue to function after runtime upgrades ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v1)/v1_agentic_chat/page.tsx ================================================ "use client"; import React from "react"; import { CopilotKit } from "@copilotkit/react-core"; import { CopilotChat } from "@copilotkit/react-ui"; import "@copilotkit/react-ui/styles.css"; interface V1AgenticChatProps { params: Promise<{ integrationId: string; }>; } const V1AgenticChat: React.FC = ({ params }) => { const { integrationId } = React.use(params); return (
); }; export default V1AgenticChat; ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v2)/a2a_chat/README.mdx ================================================ # 🤖 A2A Chat ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v2)/a2a_chat/a2a_chat.tsx ================================================ "use client"; import React, { useEffect, useState } from "react"; import "@copilotkit/react-core/v2/styles.css"; import "./style.css"; import { useAgent, UseAgentUpdate, useRenderTool, useHumanInTheLoop, useConfigureSuggestions, CopilotChat, } from "@copilotkit/react-core/v2"; import { z } from "zod"; import dedent from "dedent"; import { CopilotKit } from "@copilotkit/react-core"; interface A2AChatProps { params: Promise<{ integrationId: string; }>; onNotification?: () => void; } const A2AChat: React.FC = ({ params, onNotification }) => { const { integrationId } = React.use(params); return ( ); }; interface A2AChatState { a2aMessages: { name: string; to: string; message: string }[]; } interface Seat { seatNumber: number; status: "available" | "occupied"; name?: string; } interface Table { name: string; seats: Seat[]; } const MaybeMessageToA2A = ({ status, args }: { status: string; args: { agentName: string; task: string }; result?: string }) => { switch (status) { case "executing": case "complete": return ; case "inProgress": default: return null; } }; const MaybeMessageFromA2A = ({ status, args, result }: { status: string; args: { agentName: string; task: string }; result?: string }) => { switch (status) { case "complete": return ; case "executing": case "inProgress": default: return null; } }; interface MessageProps { from: string; to: string; message: string; color: "blue" | "green"; } const Message = ({ from, to, message, color }: MessageProps) => { const colorClass = color === "blue" ? "bg-blue-100 text-blue-700" : "bg-green-100 text-green-700"; return (
{from} {to}
{message}
); }; const Chat = ({ onNotification }: { onNotification?: () => void }) => { useConfigureSuggestions({ suggestions: [ { title: "Find a desk", message: "Help me find a desk near my teammates.", }, { title: "Check availability", message: "What desks are available right now?", }, ], available: "always", }); const { agent } = useAgent({ agentId: "a2a_chat", updates: [UseAgentUpdate.OnMessagesChanged, UseAgentUpdate.OnRunStatusChanged], }); const isLoading = agent.isRunning; const visibleMessages = agent.messages; useEffect(() => { if ( visibleMessages?.length > 0 && (!isLoading || (visibleMessages?.[visibleMessages.length - 1] as unknown as { name: string }).name === "pickTable") ) { console.log("onNotification"); onNotification?.(); } }, [isLoading, visibleMessages, onNotification]); useRenderTool({ agentId: "a2a_chat", name: "send_message_to_a2a_agent", parameters: z.object({ agentName: z.string().describe("The name of the A2A agent to send the message to"), task: z.string().describe("The message to send to the A2A agent"), }), render: (props: any) => { return ( <> ); }, }); const [selectedSeat, setSelectedSeat] = useState<{ tableIndex: number; seatNumber: number; } | null>(null); const [isConfirmed, setIsConfirmed] = useState(false); useHumanInTheLoop( { name: "pickTable", description: dedent(` Lets the use pick a table from available tables. The result will be the selected table. Wait for the user to respond via this tool, don't keep talking to them after calling it until it has resolved. Don't call this tool twice in a row or I'll turn you off! Returns: A json object with the following properties: - tableName: (string): The name of the table that was selected - seatNumber: (number): The number of the seat that was selected `), // Cast needed: pnpm may resolve separate Zod installations for dojo vs CopilotKit parameters: z.object({ tables: z.array( z.object({ name: z.string().describe("The name of the table"), seats: z.array( z.object({ seatNumber: z.number().describe("The number of the seat"), status: z.enum(["available", "occupied"]).describe("The status of the seat"), name: z.string().optional().describe("The name of the person occupying the seat"), }), ), }), ).describe(`A JSON encoded array of tables. This is an example of the format: [{ "name": "Table 1", "seats": [{ "seatNumber": 1, "status": "available" }, { "seatNumber": 2, "status": "occupied", "name": "Alice" }] }, { "name": "Table 2", "seats": [{ "seatNumber": 1, "status": "available" }, { "seatNumber": 2, "status": "available" }] }, { "name": "Table 3", "seats": [{ "seatNumber": 1, "status": "occupied", "name": "Bob" }, { "seatNumber": 2, "status": "available" }] }]`), }) as any, render({ args, respond }: { args: { tables?: Table[] }; respond?: (result: unknown) => Promise }) { const availableSeats = args.tables?.reduce( (total: number, table: Table) => total + (table.seats?.filter((seat: Seat) => seat.status === "available").length || 0), 0, ) || 0; const teamMembers = args.tables?.flatMap( (table: Table) => table.seats ?.filter((seat: Seat) => seat.status === "occupied" && seat.name) .map((seat: Seat) => ({ name: seat.name ?? "", table: table.name, seat: seat.seatNumber, })) || [], ) || []; const handleSeatClick = (tableIndex: number, seatNumber: number, status: string) => { if (status === "available") { setSelectedSeat({ tableIndex, seatNumber }); setIsConfirmed(false); // Reset confirmation when selecting a new seat } }; return (
{/* Header */}

Desk Picker - Engineering Team

{availableSeats} seats available • {teamMembers.length} teammates nearby

{/* Legend */}
Available
Occupied
Your Team
Selected
{/* Tables Grid */}
{args.tables?.map((table: Table, tableIndex: number) => (

{table.name}

{table.seats?.map((seat: Seat, seatIndex: number) => { const isSelected = selectedSeat?.tableIndex === tableIndex && selectedSeat?.seatNumber === seat.seatNumber; const isTeamMember = seat.status === "occupied" && seat.name; return ( ); })}
))}
{/* Selection Display */} {selectedSeat && (

Selected: {args.tables?.[selectedSeat.tableIndex]?.name} - Seat{" "} {selectedSeat.seatNumber}

)}
); }, }, [selectedSeat, isConfirmed], ); return (
); }; export default A2AChat; ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v2)/a2a_chat/page.tsx ================================================ "use client"; import React, { useState, useEffect, useCallback, useRef } from "react"; import { Plus, MessageSquare, Users, Settings } from "lucide-react"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import A2AChat from "./a2a_chat"; interface PageProps { params: Promise<{ integrationId: string; }>; } function Page({ params }: PageProps) { const [activeTab, setActiveTab] = useState("chat-1"); const [tabs, setTabs] = useState([{ id: "chat-1", label: "Main Chat", icon: MessageSquare }]); const [chatInstances, setChatInstances] = useState>({}); const [tabNotifications, setTabNotifications] = useState>({}); const activeTabRef = useRef(activeTab); // Function to add notification badge to a specific tab const addNotification = useCallback( (tabId: string) => { // Only add notification if the tab is not currently active console.log("addNotification", tabId, activeTabRef.current); if (tabId !== activeTabRef.current) { setTabNotifications((prev) => ({ ...prev, [tabId]: true, })); } }, [activeTabRef.current], ); // Clear notification when tab becomes active const handleTabChange = useCallback((tabId: string) => { activeTabRef.current = tabId; setActiveTab(tabId); // Clear notification for the newly active tab setTabNotifications((prev) => ({ ...prev, [tabId]: false, })); }, []); // Initialize chat instances when tabs change useEffect(() => { const newInstances = { ...chatInstances }; tabs.forEach((tab) => { if (!newInstances[tab.id]) { newInstances[tab.id] = ( addNotification(tab.id)} /> ); } }); setChatInstances(newInstances); }, [tabs, params, addNotification]); const handleAddTab = () => { const newTab = { id: `chat-${Date.now()}`, label: `Chat ${tabs.length + 1}`, icon: MessageSquare, }; setTabs([...tabs, newTab]); activeTabRef.current = newTab.id; setActiveTab(newTab.id); }; return (
{/* Beautiful Tab Bar */}
{tabs.map((tab) => { const IconComponent = tab.icon; const hasNotification = tabNotifications[tab.id]; return ( {tab.label} {/* Notification Badge */} {hasNotification && (
)} ); })} {/* Plus Button Tab */} {/* Settings Button */}
{/* Tab Contents - All chat instances stay mounted */}
{tabs.map((tab) => (
{/* Chat Background Decoration */}
{/* Chat Content */}
{chatInstances[tab.id]}
))}
); } export default Page; ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v2)/a2a_chat/style.css ================================================ .copilotKitInput { border-bottom-left-radius: 0.75rem; border-bottom-right-radius: 0.75rem; border-top-left-radius: 0.75rem; border-top-right-radius: 0.75rem; border: 1px solid var(--copilot-kit-separator-color) !important; } .copilotKitChat { background-color: transparent !important; } .copilotKitMessages { background-color: transparent !important; } .copilotKitInputContainer { background-color: transparent !important; } .poweredBy { background-color: transparent !important; } ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v2)/a2ui_chat/README.mdx ================================================ # A2UI Chat Chat with rich A2UI surface rendering using CopilotKit's BuiltInAgent and A2UIMiddleware. ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v2)/a2ui_chat/page.tsx ================================================ "use client"; import React, { useState } from "react"; import "@copilotkit/react-core/v2/styles.css"; import "./style.css"; import { CopilotChat, CopilotKitProvider, useConfigureSuggestions, } from "@copilotkit/react-core/v2"; import { createA2UIMessageRenderer } from "@copilotkit/a2ui-renderer"; import { theme } from "./theme"; export const dynamic = "force-dynamic"; const activityRenderers = [createA2UIMessageRenderer({ theme })]; interface PageProps { params: Promise<{ integrationId: string; }>; } function Chat({ agentId }: { agentId: string }) { useConfigureSuggestions({ suggestions: [ { title: "Tell a story", message: "Tell me a short story with rich formatting.", }, { title: "Create a list", message: "Create a structured list of the top 5 programming languages.", }, ], available: "always", }); return ; } export default function Page({ params }: PageProps) { const { integrationId } = React.use(params); const showToggle = integrationId === "langgraph-fastapi"; const [injectTool, setInjectTool] = useState(false); const agentId = injectTool && showToggle ? "a2ui_chat_inject" : "a2ui_chat"; return (
{showToggle && (
{injectTool ? "(frontend tool injection)" : "(backend auto-detection)"}
)}
); } ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v2)/a2ui_chat/style.css ================================================ /* Fix for messages being hidden behind the absolutely-positioned input */ .a2ui-chat-container [class*="overflow-y-scroll"] { padding-bottom: 120px !important; } /* * Default A2UI color palette. * * These CSS custom properties are required by the A2UI structural utility * classes (color-bgc-*, color-c-*, color-bc-*). The renderer does not bundle * a palette — the host application must provide one. * * Palette values match the Material Design 3 purple theme used by the * CopilotKit A2UI renderer. */ .a2ui-surface { /* Font */ font-family: "Google Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; /* Neutral */ --n-100: #ffffff; --n-99: #fcfcfc; --n-98: #f9f9f9; --n-95: #f1f1f1; --n-90: #e2e2e2; --n-80: #c6c6c6; --n-70: #ababab; --n-60: #919191; --n-50: #777777; --n-40: #5e5e5e; --n-35: #525252; --n-30: #474747; --n-25: #3b3b3b; --n-20: #303030; --n-15: #262626; --n-10: #1b1b1b; --n-5: #111111; --n-0: #000000; /* Primary */ --p-100: var(--a2ui-card-bg, #ffffff); --p-99: #fffbff; --p-98: #fcf8ff; --p-95: #f2efff; --p-90: #e1e0ff; --p-80: #c0c1ff; --p-70: #a0a3ff; --p-60: #8487ea; --p-50: #6a6dcd; --p-40: #5154b3; --p-35: #4447a6; --p-30: #383b99; --p-25: #2c2e8d; --p-20: #202182; --p-15: #131178; --p-10: #06006c; --p-5: #03004d; --p-0: #000000; /* Secondary */ --s-100: #ffffff; --s-99: #fffbff; --s-98: #fcf8ff; --s-95: #f2efff; --s-90: #e2e0f9; --s-80: #c6c4dd; --s-70: #aaa9c1; --s-60: #8f8fa5; --s-50: #75758b; --s-40: #5d5c72; --s-35: #515165; --s-30: #454559; --s-25: #393a4d; --s-20: #2e2f42; --s-15: #242437; --s-10: #191a2c; --s-5: #0f0f21; --s-0: #000000; /* Tertiary */ --t-100: #ffffff; --t-99: #fffbff; --t-98: #fff8f9; --t-95: #ffecf4; --t-90: #ffd8ec; --t-80: #e9b9d3; --t-70: #cc9eb8; --t-60: #af849d; --t-50: #946b83; --t-40: #79536a; --t-35: #6c475d; --t-30: #5f3c51; --t-25: #523146; --t-20: #46263a; --t-15: #3a1b2f; --t-10: #2e1125; --t-5: #22071a; --t-0: #000000; /* Neutral Variant */ --nv-100: #ffffff; --nv-99: #fffbff; --nv-98: #fcf8ff; --nv-95: #f2effa; --nv-90: #e4e1ec; --nv-80: #c8c5d0; --nv-70: #acaab4; --nv-60: #918f9a; --nv-50: #777680; --nv-40: #5e5d67; --nv-35: #52515b; --nv-30: #46464f; --nv-25: #3b3b43; --nv-20: #303038; --nv-15: #25252d; --nv-10: #1b1b23; --nv-5: #101018; --nv-0: #000000; /* Error */ --e-100: #ffffff; --e-99: #fffbff; --e-98: #fff8f7; --e-95: #ffedea; --e-90: #ffdad6; --e-80: #ffb4ab; --e-70: #ff897d; --e-60: #ff5449; --e-50: #de3730; --e-40: #ba1a1a; --e-35: #a80710; --e-30: #93000a; --e-25: #7e0007; --e-20: #690005; --e-15: #540003; --e-10: #410002; --e-5: #2d0001; --e-0: #000000; /* Dojo-specific */ --primary: #137fec; --text-color: #fff; --background-light: #f6f7f8; --background-dark: #101922; --border-color: oklch(from var(--background-light) l c h / calc(alpha * 0.15)); --elevated-background-light: oklch(from var(--background-light) l c h / calc(alpha * 0.05)); --bb-grid-size: 4px; --bb-grid-size-2: calc(var(--bb-grid-size) * 2); --bb-grid-size-3: calc(var(--bb-grid-size) * 3); --bb-grid-size-4: calc(var(--bb-grid-size) * 4); --bb-grid-size-5: calc(var(--bb-grid-size) * 5); --bb-grid-size-6: calc(var(--bb-grid-size) * 6); --bb-grid-size-7: calc(var(--bb-grid-size) * 7); --bb-grid-size-8: calc(var(--bb-grid-size) * 8); --bb-grid-size-9: calc(var(--bb-grid-size) * 9); --bb-grid-size-10: calc(var(--bb-grid-size) * 10); --bb-grid-size-11: calc(var(--bb-grid-size) * 11); --bb-grid-size-12: calc(var(--bb-grid-size) * 12); --bb-grid-size-13: calc(var(--bb-grid-size) * 13); --bb-grid-size-14: calc(var(--bb-grid-size) * 14); --bb-grid-size-15: calc(var(--bb-grid-size) * 15); --bb-grid-size-16: calc(var(--bb-grid-size) * 16); } ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v2)/a2ui_chat/theme.ts ================================================ import { v0_8 } from "@a2ui/lit"; /** Elements */ const a = { "typography-f-sf": true, "typography-fs-n": true, "typography-w-500": true, "layout-as-n": true, "layout-dis-iflx": true, "layout-al-c": true, }; const audio = { "layout-w-100": true, }; const body = { "typography-f-s": true, "typography-fs-n": true, "typography-w-400": true, "layout-mt-0": true, "layout-mb-2": true, "typography-sz-bm": true, "color-c-n10": true, }; const button = { "typography-f-sf": true, "typography-fs-n": true, "typography-w-500": true, "layout-pt-3": true, "layout-pb-3": true, "layout-pl-5": true, "layout-pr-5": true, "layout-mb-1": true, "border-br-16": true, "border-bw-0": true, "border-c-n70": true, "border-bs-s": true, "color-bgc-s30": true, "color-c-n100": true, "behavior-ho-80": true, }; const heading = { "typography-f-sf": true, "typography-fs-n": true, "typography-w-500": true, "layout-mt-0": true, "layout-mb-2": true, "color-c-n10": true, }; const h1 = { ...heading, "typography-sz-tl": true, }; const h2 = { ...heading, "typography-sz-tm": true, }; const h3 = { ...heading, "typography-sz-ts": true, }; const h4 = { ...heading, "typography-sz-bl": true, }; const h5 = { ...heading, "typography-sz-bm": true, }; const iframe = { "behavior-sw-n": true, }; const input = { "typography-f-sf": true, "typography-fs-n": true, "typography-w-400": true, "layout-pl-4": true, "layout-pr-4": true, "layout-pt-2": true, "layout-pb-2": true, "border-br-6": true, "border-bw-1": true, "color-bc-s70": true, "border-bs-s": true, "layout-as-n": true, "color-c-n10": true, }; const p = { "typography-f-s": true, "typography-fs-n": true, "typography-w-400": true, "layout-m-0": true, "typography-sz-bm": true, "layout-as-n": true, "color-c-n10": true, }; const orderedList = { "typography-f-s": true, "typography-fs-n": true, "typography-w-400": true, "layout-m-0": true, "typography-sz-bm": true, "layout-as-n": true, }; const unorderedList = { "typography-f-s": true, "typography-fs-n": true, "typography-w-400": true, "layout-m-0": true, "typography-sz-bm": true, "layout-as-n": true, }; const listItem = { "typography-f-s": true, "typography-fs-n": true, "typography-w-400": true, "layout-m-0": true, "typography-sz-bm": true, "layout-as-n": true, }; const pre = { "typography-f-c": true, "typography-fs-n": true, "typography-w-400": true, "typography-sz-bm": true, "typography-ws-p": true, "layout-as-n": true, }; const textarea = { ...input, "layout-r-none": true, "layout-fs-c": true, }; const video = { "layout-el-cv": true, }; const aLight = v0_8.Styles.merge(a, { "color-c-n5": true }); const inputLight = v0_8.Styles.merge(input, { "color-c-n5": true }); const textareaLight = v0_8.Styles.merge(textarea, { "color-c-n5": true }); const buttonLight = v0_8.Styles.merge(button, { "color-c-n100": true }); const h1Light = v0_8.Styles.merge(h1, { "color-c-n5": true }); const h2Light = v0_8.Styles.merge(h2, { "color-c-n5": true }); const h3Light = v0_8.Styles.merge(h3, { "color-c-n5": true }); const h4Light = v0_8.Styles.merge(h4, { "color-c-n5": true }); const h5Light = v0_8.Styles.merge(h5, { "color-c-n5": true }); const bodyLight = v0_8.Styles.merge(body, { "color-c-n5": true }); const pLight = v0_8.Styles.merge(p, { "color-c-n35": true }); const preLight = v0_8.Styles.merge(pre, { "color-c-n35": true }); const orderedListLight = v0_8.Styles.merge(orderedList, { "color-c-n35": true, }); const unorderedListLight = v0_8.Styles.merge(unorderedList, { "color-c-n35": true, }); const listItemLight = v0_8.Styles.merge(listItem, { "color-c-n35": true, }); export const theme: v0_8.Types.Theme = { additionalStyles: { Button: { "--n-35": "var(--n-100)", }, }, components: { AudioPlayer: {}, Button: { "layout-pt-2": true, "layout-pb-2": true, "layout-pl-3": true, "layout-pr-3": true, "border-br-12": true, "border-bw-0": true, "border-bs-s": true, "color-bgc-p30": true, "color-c-n100": true, "behavior-ho-70": true, }, Card: { "border-br-9": true, "color-bgc-p100": true, "layout-p-4": true }, CheckBox: { element: { "layout-m-0": true, "layout-mr-2": true, "layout-p-2": true, "border-br-12": true, "border-bw-1": true, "border-bs-s": true, "color-bgc-p100": true, "color-bc-p60": true, "color-c-n30": true, "color-c-p30": true, }, label: { "color-c-p30": true, "typography-f-sf": true, "typography-v-r": true, "typography-w-400": true, "layout-flx-1": true, "typography-sz-ll": true, }, container: { "layout-dsp-iflex": true, "layout-al-c": true, }, }, Column: { "layout-g-2": true, }, DateTimeInput: { container: { "typography-sz-bm": true, "layout-w-100": true, "layout-g-2": true, "layout-dsp-flexhor": true, "layout-al-c": true, }, label: { "layout-flx-0": true, }, element: { "layout-pt-2": true, "layout-pb-2": true, "layout-pl-3": true, "layout-pr-3": true, "border-br-12": true, "border-bw-1": true, "border-bs-s": true, "color-bgc-p100": true, "color-bc-p60": true, "color-c-n30": true, "color-c-p30": true, }, }, Divider: {}, Image: { all: { "border-br-5": true, "layout-el-cv": true, "layout-w-100": true, "layout-h-100": true, }, avatar: {}, header: {}, icon: {}, largeFeature: {}, mediumFeature: {}, smallFeature: {}, }, Icon: {}, List: { "layout-g-4": true, "layout-p-2": true, }, Modal: { backdrop: { "color-bbgc-p60_20": true }, element: { "border-br-2": true, "color-bgc-p100": true, "layout-p-4": true, "border-bw-1": true, "border-bs-s": true, "color-bc-p80": true, }, }, MultipleChoice: { container: {}, label: {}, element: {}, }, Row: { "layout-g-4": true, }, Slider: { container: {}, label: {}, element: {}, }, Tabs: { container: {}, controls: { all: {}, selected: {} }, element: {}, }, Text: { all: { "layout-w-100": true, "layout-g-2": true, "color-c-p30": true, }, h1: { "typography-f-sf": true, "typography-v-r": true, "typography-w-400": true, "layout-m-0": true, "layout-p-0": true, "typography-sz-tl": true, }, h2: { "typography-f-sf": true, "typography-v-r": true, "typography-w-400": true, "layout-m-0": true, "layout-p-0": true, "typography-sz-tm": true, }, h3: { "typography-f-sf": true, "typography-v-r": true, "typography-w-400": true, "layout-m-0": true, "layout-p-0": true, "typography-sz-ts": true, }, h4: { "typography-f-sf": true, "typography-v-r": true, "typography-w-400": true, "layout-m-0": true, "layout-p-0": true, "typography-sz-bl": true, }, h5: { "typography-f-sf": true, "typography-v-r": true, "typography-w-400": true, "layout-m-0": true, "layout-p-0": true, "typography-sz-bm": true, }, body: {}, caption: {}, }, TextField: { container: { "typography-sz-bm": true, "layout-w-100": true, "layout-g-2": true, "layout-dsp-flexhor": true, "layout-al-c": true, }, label: { "layout-flx-0": true, }, element: { "typography-sz-bm": true, "layout-pt-2": true, "layout-pb-2": true, "layout-pl-3": true, "layout-pr-3": true, "border-br-12": true, "border-bw-1": true, "border-bs-s": true, "color-bgc-p100": true, "color-bc-p60": true, "color-c-n30": true, "color-c-p30": true, }, }, Video: { "border-br-5": true, "layout-el-cv": true, }, }, elements: { a: aLight, audio, body: bodyLight, button: buttonLight, h1: h1Light, h2: h2Light, h3: h3Light, h4: h4Light, h5: h5Light, iframe, input: inputLight, p: pLight, pre: preLight, textarea: textareaLight, video, }, markdown: { p: [...Object.keys(pLight)], h1: [...Object.keys(h1Light)], h2: [...Object.keys(h2Light)], h3: [...Object.keys(h3Light)], h4: [...Object.keys(h4Light)], h5: [...Object.keys(h5Light)], ul: [...Object.keys(unorderedListLight)], ol: [...Object.keys(orderedListLight)], li: [...Object.keys(listItemLight)], a: [...Object.keys(aLight)], strong: [], em: [], }, }; ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v2)/agentic_chat/README.mdx ================================================ # 🤖 Agentic Chat with Frontend Tools ## What This Demo Shows This demo showcases CopilotKit's **agentic chat** capabilities with **frontend tool integration**: 1. **Natural Conversation**: Chat with your Copilot in a familiar chat interface 2. **Frontend Tool Execution**: The Copilot can directly interacts with your UI by calling frontend functions 3. **Seamless Integration**: Tools defined in the frontend and automatically discovered and made available to the agent ## How to Interact Try asking your Copilot to: - "Can you change the background color to something more vibrant?" - "Make the background a blue to purple gradient" - "Set the background to a sunset-themed gradient" - "Change it back to a simple light color" You can also chat about other topics - the agent will respond conversationally while having the ability to use your UI tools when appropriate. ## ✨ Frontend Tool Integration in Action **What's happening technically:** - The React component defines a frontend function using `useCopilotAction` - CopilotKit automatically exposes this function to the agent - When you make a request, the agent determines whether to use the tool - The agent calls the function with the appropriate parameters - The UI immediately updates in response **What you'll see in this demo:** - The Copilot understands requests to change the background - It generates CSS values for colors and gradients - When it calls the tool, the background changes instantly - The agent provides a conversational response about the changes it made This technique of exposing frontend functions to your Copilot can be extended to any UI manipulation you want to enable, from theme changes to data filtering, navigation, or complex UI state management! ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v2)/agentic_chat/page.tsx ================================================ "use client"; import React, { useState } from "react"; import "@copilotkit/react-core/v2/styles.css"; import { useFrontendTool, useRenderTool, useAgentContext, useConfigureSuggestions, CopilotChat, } from "@copilotkit/react-core/v2"; import { z } from "zod"; import { CopilotKit } from "@copilotkit/react-core"; interface AgenticChatProps { params: Promise<{ integrationId: string; }>; } const AgenticChat: React.FC = ({ params }) => { const { integrationId } = React.use(params); return ( ); }; const Chat = () => { const [background, setBackground] = useState("--copilot-kit-background-color"); useAgentContext({ description: 'Name of the user', value: 'Bob' }); useFrontendTool({ name: "change_background", description: "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", parameters: z.object({ background: z.string().describe("The background. Prefer gradients. Only use when asked."), }) , handler: async ({ background }: { background: string }) => { setBackground(background); return { status: "success", message: `Background changed to ${background}`, }; }, }); useRenderTool({ name: "get_weather", parameters: z.object({ location: z.string(), }) , render: ({ args, result, status }: any) => { if (status !== "complete") { return
Loading weather...
; } return (
Weather in {result?.city || args.location}
Temperature: {result?.temperature}°C
Humidity: {result?.humidity}%
Wind Speed: {result?.windSpeed ?? result?.wind_speed} mph
Conditions: {result?.conditions}
); }, }); useConfigureSuggestions({ suggestions: [ { title: "Change background", message: "Change the background to something new.", }, { title: "Generate sonnet", message: "Write a short sonnet about AI.", }, ], available: "always", }); return (
); }; export default AgenticChat; ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v2)/agentic_chat_reasoning/README.mdx ================================================ # 🤖 Agentic Chat with Reasoning ## What This Demo Shows This demo showcases CopilotKit's **agentic chat** capabilities with **frontend tool integration**: 1. **Natural Conversation**: Chat with your Copilot in a familiar chat interface 2. **Frontend Tool Execution**: The Copilot can directly interacts with your UI by calling frontend functions 3. **Seamless Integration**: Tools defined in the frontend and automatically discovered and made available to the agent ## How to Interact Try asking your Copilot to: - "Can you change the background color to something more vibrant?" - "Make the background a blue to purple gradient" - "Set the background to a sunset-themed gradient" - "Change it back to a simple light color" You can also chat about other topics - the agent will respond conversationally while having the ability to use your UI tools when appropriate. ## ✨ Frontend Tool Integration in Action **What's happening technically:** - The React component defines a frontend function using `useCopilotAction` - CopilotKit automatically exposes this function to the agent - When you make a request, the agent determines whether to use the tool - The agent calls the function with the appropriate parameters - The UI immediately updates in response **What you'll see in this demo:** - The Copilot understands requests to change the background - It generates CSS values for colors and gradients - When it calls the tool, the background changes instantly - The agent provides a conversational response about the changes it made This technique of exposing frontend functions to your Copilot can be extended to any UI manipulation you want to enable, from theme changes to data filtering, navigation, or complex UI state management! ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v2)/agentic_chat_reasoning/page.tsx ================================================ "use client"; import React, { useState } from "react"; import "@copilotkit/react-core/v2/styles.css"; import "./style.css"; import { useAgent, UseAgentUpdate, useFrontendTool, useConfigureSuggestions, CopilotChat, } from "@copilotkit/react-core/v2"; import { z } from "zod"; import { ChevronDown } from "lucide-react"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { CopilotKit } from "@copilotkit/react-core"; interface AgenticChatProps { params: Promise<{ integrationId: string; }>; } const AgenticChat: React.FC = ({ params }) => { const { integrationId } = React.use(params); return ( ); }; interface AgentState { model: string; } const Chat = () => { const [background, setBackground] = useState("--copilot-kit-background-color"); const { agent } = useAgent({ agentId: "agentic_chat_reasoning", updates: [UseAgentUpdate.OnStateChanged], }); const agentState = agent.state as AgentState | undefined; // Initialize model if not set const selectedModel = agentState?.model || "OpenAI"; const handleModelChange = (model: string) => { agent.setState({ model }); }; useConfigureSuggestions({ suggestions: [ { title: "Change background", message: "Change the background to something new.", }, { title: "Generate sonnet", message: "Write a short sonnet about AI.", }, ], available: "always", }); useFrontendTool({ agentId: "agentic_chat_reasoning", name: "change_background", description: "Change the background color of the chat. Can be anything that the CSS background attribute accepts. Regular colors, linear of radial gradients etc.", parameters: z.object({ background: z.string().describe("The background. Prefer gradients."), }) , handler: async ({ background }: { background: string }) => { setBackground(background); }, }); return (
{/* Reasoning Model Dropdown */}
Reasoning Model: Select Model handleModelChange("OpenAI")}> OpenAI handleModelChange("Anthropic")}> Anthropic handleModelChange("Gemini")}> Gemini
{/* Chat Container */}
); }; export default AgenticChat; ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v2)/agentic_chat_reasoning/style.css ================================================ .copilotKitInput { border-bottom-left-radius: 0.75rem; border-bottom-right-radius: 0.75rem; border-top-left-radius: 0.75rem; border-top-right-radius: 0.75rem; border: 1px solid var(--copilot-kit-separator-color) !important; } .copilotKitChat { background-color: #fff !important; } ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v2)/agentic_generative_ui/README.mdx ================================================ # 🚀 Agentic Generative UI Task Executor ## What This Demo Shows This demo showcases CopilotKit's **agentic generative UI** capabilities: 1. **Real-time Status Updates**: The Copilot provides live feedback as it works through complex tasks 2. **Long-running Task Execution**: See how agents can handle extended processes with continuous feedback 3. **Dynamic UI Generation**: The interface updates in real-time to reflect the agent's progress ## How to Interact Simply ask your Copilot to perform any moderately complex task: - "Make me a sandwich" - "Plan a vacation to Japan" - "Create a weekly workout routine" The Copilot will break down the task into steps and begin "executing" them, providing real-time status updates as it progresses. ## ✨ Agentic Generative UI in Action **What's happening technically:** - The agent analyzes your request and creates a detailed execution plan - Each step is processed sequentially with realistic timing - Status updates are streamed to the frontend using CopilotKit's streaming capabilities - The UI dynamically renders these updates without page refreshes - The entire flow is managed by the agent, requiring no manual intervention **What you'll see in this demo:** - The Copilot breaks your task into logical steps - A status indicator shows the current progress - Each step is highlighted as it's being executed - Detailed status messages explain what's happening at each moment - Upon completion, you receive a summary of the task execution This pattern of providing real-time progress for long-running tasks is perfect for scenarios where users benefit from transparency into complex processes - from data analysis to content creation, system configurations, or multi-stage workflows! ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v2)/agentic_generative_ui/page.tsx ================================================ "use client"; import React from "react"; import "@copilotkit/react-core/v2/styles.css"; import "./style.css"; import { useAgent, UseAgentUpdate, useConfigureSuggestions, CopilotChat, } from "@copilotkit/react-core/v2"; import { useTheme } from "next-themes"; import { CopilotKit } from "@copilotkit/react-core"; interface AgenticGenerativeUIProps { params: Promise<{ integrationId: string; }>; } const AgenticGenerativeUI: React.FC = ({ params }) => { const { integrationId } = React.use(params); return ( ); }; interface AgentState { steps: { description: string; status: "pending" | "completed"; }[]; } const Chat = () => { const { theme } = useTheme(); const { agent } = useAgent({ agentId: "agentic_generative_ui", updates: [UseAgentUpdate.OnStateChanged], }); const agentState = agent.state as AgentState | undefined; useConfigureSuggestions({ suggestions: [ { title: "Simple plan", message: "Please build a plan to go to mars in 5 steps.", }, { title: "Complex plan", message: "Please build a plan to go to make pizza in 10 steps.", }, ], available: "always", }); const steps = agentState?.steps; return (
(
{messageElements} {steps && steps.length > 0 && (
)} {interruptElement}
), }} />
); }; function TaskProgress({ steps, theme }: { steps: AgentState["steps"]; theme?: string }) { const completedCount = steps.filter((step) => step.status === "completed").length; const progressPercentage = (completedCount / steps.length) * 100; return (
{/* Header */}

Task Progress

{completedCount}/{steps.length} Complete
{/* Progress Bar */}
{/* Steps */}
{steps.map((step, index) => { const isCompleted = step.status === "completed"; const isCurrentPending = step.status === "pending" && index === steps.findIndex((s) => s.status === "pending"); const isFuturePending = step.status === "pending" && !isCurrentPending; return (
{/* Connector Line */} {index < steps.length - 1 && (
)} {/* Status Icon */}
{isCompleted ? ( ) : isCurrentPending ? ( ) : ( )}
{/* Step Content */}
{step.description}
{isCurrentPending && (
Processing...
)}
{/* Animated Background for Current Step */} {isCurrentPending && (
)}
); })}
{/* Decorative Elements */}
); } // Enhanced Icons function CheckIcon() { return ( ); } function SpinnerIcon() { return ( ); } function ClockIcon({ theme }: { theme?: string }) { return ( ); } export default AgenticGenerativeUI; ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v2)/agentic_generative_ui/style.css ================================================ .copilotKitInput { border-bottom-left-radius: 0.75rem; border-bottom-right-radius: 0.75rem; border-top-left-radius: 0.75rem; border-top-right-radius: 0.75rem; border: 1px solid var(--copilot-kit-separator-color) !important; } .copilotKitChat { background-color: #fff !important; } ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v2)/backend_tool_rendering/README.mdx ================================================ # 🤖 Agentic Chat with Frontend Tools ## What This Demo Shows This demo showcases CopilotKit's **agentic chat** capabilities with **frontend tool integration**: 1. **Natural Conversation**: Chat with your Copilot in a familiar chat interface 2. **Frontend Tool Execution**: The Copilot can directly interacts with your UI by calling frontend functions 3. **Seamless Integration**: Tools defined in the frontend and automatically discovered and made available to the agent ## How to Interact Try asking your Copilot to: - "Can you change the background color to something more vibrant?" - "Make the background a blue to purple gradient" - "Set the background to a sunset-themed gradient" - "Change it back to a simple light color" You can also chat about other topics - the agent will respond conversationally while having the ability to use your UI tools when appropriate. ## ✨ Frontend Tool Integration in Action **What's happening technically:** - The React component defines a frontend function using `useCopilotAction` - CopilotKit automatically exposes this function to the agent - When you make a request, the agent determines whether to use the tool - The agent calls the function with the appropriate parameters - The UI immediately updates in response **What you'll see in this demo:** - The Copilot understands requests to change the background - It generates CSS values for colors and gradients - When it calls the tool, the background changes instantly - The agent provides a conversational response about the changes it made This technique of exposing frontend functions to your Copilot can be extended to any UI manipulation you want to enable, from theme changes to data filtering, navigation, or complex UI state management! ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v2)/backend_tool_rendering/page.tsx ================================================ "use client"; import React from "react"; import "@copilotkit/react-core/v2/styles.css"; import "./style.css"; import { useRenderTool, useConfigureSuggestions, CopilotChat, } from "@copilotkit/react-core/v2"; import { z } from "zod"; import { CopilotKit } from "@copilotkit/react-core"; interface AgenticChatProps { params: Promise<{ integrationId: string; }>; } const AgenticChat: React.FC = ({ params }) => { const { integrationId } = React.use(params); return ( ); }; const Chat = () => { useRenderTool({ name: "get_weather", parameters: z.object({ location: z.string(), }) , render: ({ args, result, status }: any) => { if (status !== "complete") { return (
⚙️ Retrieving weather...
); } const weatherResult: WeatherToolResult = { temperature: result?.temperature || 0, conditions: result?.conditions || "clear", humidity: result?.humidity || 0, windSpeed: result?.wind_speed || 0, feelsLike: result?.feels_like || result?.temperature || 0, }; const themeColor = getThemeColor(weatherResult.conditions); return ( ); }, }); useConfigureSuggestions({ suggestions: [ { title: "Weather in San Francisco", message: "What's the weather like in San Francisco?", }, { title: "Weather in New York", message: "Tell me about the weather in New York.", }, { title: "Weather in Tokyo", message: "How's the weather in Tokyo today?", }, ], available: "always", }); return (
); }; interface WeatherToolResult { temperature: number; conditions: string; humidity: number; windSpeed: number; feelsLike: number; } function getThemeColor(conditions: string): string { const conditionLower = conditions.toLowerCase(); if (conditionLower.includes("clear") || conditionLower.includes("sunny")) { return "#667eea"; } if (conditionLower.includes("rain") || conditionLower.includes("storm")) { return "#4A5568"; } if (conditionLower.includes("cloud")) { return "#718096"; } if (conditionLower.includes("snow")) { return "#63B3ED"; } return "#764ba2"; } function WeatherCard({ location, themeColor, result, status, }: { location?: string; themeColor: string; result: WeatherToolResult; status: "inProgress" | "executing" | "complete"; }) { return (

{location}

Current Weather

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

Humidity

{result.humidity}%

Wind

{result.windSpeed} mph

Feels Like

{result.feelsLike}°

); } function WeatherIcon({ conditions }: { conditions: string }) { if (!conditions) return null; if (conditions.toLowerCase().includes("clear") || conditions.toLowerCase().includes("sunny")) { return ; } if ( conditions.toLowerCase().includes("rain") || conditions.toLowerCase().includes("drizzle") || conditions.toLowerCase().includes("snow") || conditions.toLowerCase().includes("thunderstorm") ) { return ; } if ( conditions.toLowerCase().includes("fog") || conditions.toLowerCase().includes("cloud") || conditions.toLowerCase().includes("overcast") ) { return ; } return ; } // Simple sun icon for the weather card function SunIcon() { return ( ); } function RainIcon() { return ( {/* Cloud */} {/* Rain drops */} ); } function CloudIcon() { return ( ); } export default AgenticChat; ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v2)/backend_tool_rendering/style.css ================================================ .copilotKitInput { border-bottom-left-radius: 0.75rem; border-bottom-right-radius: 0.75rem; border-top-left-radius: 0.75rem; border-top-right-radius: 0.75rem; border: 1px solid var(--copilot-kit-separator-color) !important; } .copilotKitChat { background-color: #fff !important; } ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v2)/human_in_the_loop/README.mdx ================================================ # 🤝 Human-in-the-Loop Task Planner ## What This Demo Shows This demo showcases CopilotKit's **human-in-the-loop** capabilities: 1. **Collaborative Planning**: The Copilot generates task steps and lets you decide which ones to perform 2. **Interactive Decision Making**: Select or deselect steps to customize the execution plan 3. **Adaptive Responses**: The Copilot adapts its execution based on your choices, even handling missing steps ## How to Interact Try these steps to experience the demo: 1. Ask your Copilot to help with a task, such as: - "Make me a sandwich" - "Plan a weekend trip" - "Organize a birthday party" - "Start a garden" 2. Review the suggested steps provided by your Copilot 3. Select or deselect steps using the checkboxes to customize the plan - Try removing essential steps to see how the Copilot adapts! 4. Click "Execute Plan" to see the outcome based on your selections ## ✨ Human-in-the-Loop Magic in Action **What's happening technically:** - The agent analyzes your request and breaks it down into logical steps - These steps are presented to you through a dynamic UI component - Your selections are captured as user input - The agent considers your choices when executing the plan - The agent adapts to missing steps with creative problem-solving **What you'll see in this demo:** - The Copilot provides a detailed, step-by-step plan for your task - You have complete control over which steps to include - If you remove essential steps, the Copilot provides entertaining and creative workarounds - The final execution reflects your choices, showing how human input shapes the outcome - Each response is tailored to your specific selections This human-in-the-loop pattern creates a powerful collaborative experience where both human judgment and AI capabilities work together to achieve better results than either could alone! ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v2)/human_in_the_loop/page.tsx ================================================ "use client"; import React, { useState, useEffect } from "react"; import "@copilotkit/react-core/v2/styles.css"; import { useHumanInTheLoop, useConfigureSuggestions, CopilotChat, CopilotChatConfigurationProvider, } from "@copilotkit/react-core/v2"; import { CopilotKit, useLangGraphInterrupt } from "@copilotkit/react-core"; import { z } from "zod"; import { useTheme } from "next-themes"; interface HumanInTheLoopProps { params: Promise<{ integrationId: string; }>; } const HumanInTheLoop: React.FC = ({ params }) => { const { integrationId } = React.use(params); return ( ); }; interface Step { description: string; status: "disabled" | "enabled" | "executing"; } // Shared UI Components const StepContainer = ({ theme, children }: { theme?: string; children: React.ReactNode }) => (
{children}
); const StepHeader = ({ theme, enabledCount, totalCount, status, showStatus = false, }: { theme?: string; enabledCount: number; totalCount: number; status?: string; showStatus?: boolean; }) => (

Select Steps

{enabledCount}/{totalCount} Selected
{showStatus && (
{status === "executing" ? "Ready" : "Waiting"}
)}
0 ? (enabledCount / totalCount) * 100 : 0}%` }} />
); const StepItem = ({ step, theme, status, onToggle, disabled = false, }: { step: { description: string; status: string }; theme?: string; status?: string; onToggle: () => void; disabled?: boolean; }) => (
); const ActionButton = ({ variant, theme, disabled, onClick, children, }: { variant: "primary" | "secondary" | "success" | "danger"; theme?: string; disabled?: boolean; onClick: () => void; children: React.ReactNode; }) => { const baseClasses = "px-6 py-3 rounded-lg font-semibold transition-all duration-200"; const enabledClasses = "hover:scale-105 shadow-md hover:shadow-lg"; const disabledClasses = "opacity-50 cursor-not-allowed"; const variantClasses = { primary: "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", secondary: theme === "dark" ? "bg-slate-700 hover:bg-slate-600 text-white border border-slate-600 hover:border-slate-500" : "bg-gray-100 hover:bg-gray-200 text-gray-800 border border-gray-300 hover:border-gray-400", success: "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", danger: "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", }; return ( ); }; const DecorativeElements = ({ theme, variant = "default", }: { theme?: string; variant?: "default" | "success" | "danger"; }) => ( <>
); const InterruptHumanInTheLoop: React.FC<{ event: { value: { steps: Step[] } }; resolve: (value: string) => void; }> = ({ event, resolve }) => { const { theme } = useTheme(); // Parse and initialize steps data let initialSteps: Step[] = []; if (event.value && event.value.steps && Array.isArray(event.value.steps)) { initialSteps = event.value.steps.map((step: any) => ({ description: typeof step === "string" ? step : step.description || "", status: typeof step === "object" && step.status ? step.status : "enabled", })); } const [localSteps, setLocalSteps] = useState(initialSteps); const enabledCount = localSteps.filter((step) => step.status === "enabled").length; const handleStepToggle = (index: number) => { setLocalSteps((prevSteps) => prevSteps.map((step, i) => i === index ? { ...step, status: step.status === "enabled" ? "disabled" : "enabled" } : step, ), ); }; const handlePerformSteps = () => { const selectedSteps = localSteps .filter((step) => step.status === "enabled") .map((step) => step.description); resolve("The user selected the following steps: " + selectedSteps.join(", ")); }; return (
{localSteps.map((step, index) => ( handleStepToggle(index)} /> ))}
Perform Steps {enabledCount}
); }; const Chat = ({ integrationId }: { integrationId: string }) => { return ( ); }; const ChatContent = () => { useConfigureSuggestions({ suggestions: [ { title: "Simple plan", message: "Please plan a trip to mars in 5 steps." }, { title: "Complex plan", message: "Please plan a pasta dish in 10 steps." }, ], available: "always", }); // Langgraph uses it's own hook to handle human-in-the-loop interactions via langgraph interrupts, // This hook won't do anything for other integrations. useLangGraphInterrupt({ render: ({ event, resolve }) => , }); useHumanInTheLoop({ agentId: "human_in_the_loop", name: "generate_task_steps", description: "Generates a list of steps for the user to perform", parameters: z.object({ steps: z.array( z.object({ description: z.string(), status: z.enum(["enabled", "disabled", "executing"]), }), ), }) , // Note: In v1, `available` was used to disable this for langgraph integrations. // In v2, availability is handled at the agent/backend level. render: ({ args, respond, status }: any) => { return ; }, }); return (
); }; const StepsFeedback = ({ args, respond, status }: { args: any; respond: any; status: any }) => { const { theme } = useTheme(); const [localSteps, setLocalSteps] = useState([]); const [accepted, setAccepted] = useState(null); useEffect(() => { if (status === "executing" && localSteps.length === 0 && Array.isArray(args?.steps) && args.steps.length > 0) { setLocalSteps(args.steps); } }, [status, args?.steps, localSteps]); if (!Array.isArray(args?.steps) || args.steps.length === 0) { return <>; } const steps = Array.isArray(localSteps) && localSteps.length > 0 ? localSteps : args.steps; const enabledCount = steps.filter((step: any) => step.status === "enabled").length; const handleStepToggle = (index: number) => { setLocalSteps((prevSteps) => prevSteps.map((step, i) => i === index ? { ...step, status: step.status === "enabled" ? "disabled" : "enabled" } : step, ), ); }; const handleReject = () => { if (respond) { setAccepted(false); respond({ accepted: false }); } }; const handleConfirm = () => { if (respond) { setAccepted(true); respond({ accepted: true, steps: localSteps.filter((step) => step.status === "enabled") }); } }; return (
{steps.map((step: any, index: any) => ( handleStepToggle(index)} disabled={status !== "executing"} /> ))}
{/* Action Buttons - Different logic from InterruptHumanInTheLoop */} {accepted === null && (
Reject Confirm {enabledCount}
)} {/* Result State - Unique to StepsFeedback */} {accepted !== null && (
{accepted ? "✓" : "✗"} {accepted ? "Accepted" : "Rejected"}
)}
); }; export default HumanInTheLoop; ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v2)/predictive_state_updates/README.mdx ================================================ # 📝 Predictive State Updates Document Editor ## What This Demo Shows This demo showcases CopilotKit's **predictive state updates** for real-time document collaboration: 1. **Live Document Editing**: Watch as your Copilot makes changes to a document in real-time 2. **Diff Visualization**: See exactly what's being changed as it happens 3. **Streaming Updates**: Changes are displayed character-by-character as the Copilot works ## How to Interact Try these interactions with the collaborative document editor: - "Fix the grammar and typos in this document" - "Make this text more professional" - "Add a section about [topic]" - "Summarize this content in bullet points" - "Change the tone to be more casual" Watch as the Copilot processes your request and edits the document in real-time right before your eyes. ## ✨ Predictive State Updates in Action **What's happening technically:** - The document state is shared between your UI and the Copilot - As the Copilot generates content, changes are streamed to the UI - Each modification is visualized with additions and deletions - The UI renders these changes progressively, without waiting for completion - All edits are tracked and displayed in a visually intuitive way **What you'll see in this demo:** - Text changes are highlighted in different colors (green for additions, red for deletions) - The document updates character-by-character, creating a typing-like effect - You can see the Copilot's thought process as it refines the content - The final document seamlessly incorporates all changes - The experience feels collaborative, as if someone is editing alongside you This pattern of real-time collaborative editing with diff visualization is perfect for document editors, code review tools, content creation platforms, or any application where users benefit from seeing exactly how content is being transformed! ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v2)/predictive_state_updates/page.tsx ================================================ "use client"; import "@copilotkit/react-core/v2/styles.css"; import "./style.css"; import MarkdownIt from "markdown-it"; import React from "react"; import { diffWords } from "diff"; import { useEditor, EditorContent } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; import { useEffect, useState, useRef } from "react"; import { useAgent, UseAgentUpdate, useHumanInTheLoop, useConfigureSuggestions, CopilotChat, CopilotSidebar, } from "@copilotkit/react-core/v2"; import { z } from "zod"; import { useMobileView } from "@/utils/use-mobile-view"; import { useMobileChat } from "@/utils/use-mobile-chat"; import { useURLParams } from "@/contexts/url-params-context"; import { CopilotKit } from "@copilotkit/react-core"; const extensions = [StarterKit]; interface PredictiveStateUpdatesProps { params: Promise<{ integrationId: string; }>; } export default function PredictiveStateUpdates({ params }: PredictiveStateUpdatesProps) { const { integrationId } = React.use(params); const { isMobile } = useMobileView(); const { chatDefaultOpen } = useURLParams(); const defaultChatHeight = 50; const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } = useMobileChat(defaultChatHeight); const chatTitle = "AI Document Editor"; const chatDescription = "Ask me to create or edit a document"; return (
{isMobile ? ( <> {/* Chat Toggle Button */}
{ if (!isChatOpen) { setChatHeight(defaultChatHeight); // Reset to good default when opening } setIsChatOpen(!isChatOpen); }} >
{chatTitle}
{chatDescription}
{/* Pull-Up Chat Container */}
{/* Drag Handle Bar */}
{/* Chat Header */}

{chatTitle}

{/* Chat Content - Flexible container for messages and input */}
{/* Backdrop */} {isChatOpen && (
setIsChatOpen(false)} /> )} ) : ( )}
); } interface AgentState { document: string; } const DocumentEditor = () => { const editor = useEditor({ extensions, immediatelyRender: false, editorProps: { attributes: { class: "min-h-screen p-10" }, }, }); const [placeholderVisible, setPlaceholderVisible] = useState(false); const [currentDocument, setCurrentDocument] = useState(""); useConfigureSuggestions({ suggestions: [ { title: "Write a pirate story", message: "Please write a story about a pirate named Candy Beard.", }, { title: "Write a mermaid story", message: "Please write a story about a mermaid named Luna.", }, { title: "Add character", message: "Please add a character named Courage." }, ], available: "always", }); const { agent } = useAgent({ agentId: "predictive_state_updates", updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged], }); const agentState = agent.state as AgentState | undefined; const setAgentState = (s: AgentState) => agent.setState(s); const isLoading = agent.isRunning; // Track when a run transitions from running to not running (replaces nodeName == "end") const wasRunning = useRef(false); useEffect(() => { if (isLoading) { setCurrentDocument(editor?.getText() || ""); } editor?.setEditable(!isLoading); }, [isLoading]); useEffect(() => { if (wasRunning.current && !isLoading) { // Run just finished - set the text one final time if (currentDocument.trim().length > 0 && currentDocument !== agentState?.document) { const newDocument = agentState?.document || ""; const diff = diffPartialText(currentDocument, newDocument, true); const markdown = fromMarkdown(diff); editor?.commands.setContent(markdown); } } wasRunning.current = isLoading; }, [isLoading]); useEffect(() => { if (isLoading) { if (currentDocument.trim().length > 0) { const newDocument = agentState?.document || ""; const diff = diffPartialText(currentDocument, newDocument); const markdown = fromMarkdown(diff); editor?.commands.setContent(markdown); } else { const markdown = fromMarkdown(agentState?.document || ""); editor?.commands.setContent(markdown); } } }, [agentState?.document]); const text = editor?.getText() || ""; useEffect(() => { setPlaceholderVisible(text.length === 0); if (!isLoading) { setCurrentDocument(text); setAgentState({ document: text, }); } }, [text]); // TODO(steve): Remove this when all agents have been updated to use write_document tool. useHumanInTheLoop( { agentId: "predictive_state_updates", name: "confirm_changes", render: ({ args, respond, status }) => ( { editor?.commands.setContent(fromMarkdown(currentDocument)); setAgentState({ document: currentDocument }); }} onConfirm={() => { editor?.commands.setContent(fromMarkdown(agentState?.document || "")); setCurrentDocument(agentState?.document || ""); setAgentState({ document: agentState?.document || "" }); }} /> ), }, [agentState?.document], ); // Action to write the document. useHumanInTheLoop( { agentId: "predictive_state_updates", name: "write_document", description: `Present the proposed changes to the user for review`, parameters: z.object({ document: z.string().describe("The full updated document in markdown format"), }) , render({ args, status, respond }: { args: { document?: string }; status: string; respond?: (result: unknown) => Promise }) { if (status === "executing") { return ( { editor?.commands.setContent(fromMarkdown(currentDocument)); setAgentState({ document: currentDocument }); }} onConfirm={() => { editor?.commands.setContent(fromMarkdown(agentState?.document || "")); setCurrentDocument(agentState?.document || ""); setAgentState({ document: agentState?.document || "" }); }} /> ); } return <>; }, }, [agentState?.document], ); return (
{placeholderVisible && (
Write whatever you want here in Markdown format...
)}
); }; interface ConfirmChangesProps { args: any; respond: any; status: any; onReject: () => void; onConfirm: () => void; } function ConfirmChanges({ args, respond, status, onReject, onConfirm }: ConfirmChangesProps) { const [accepted, setAccepted] = useState(null); return (

Confirm Changes

Do you want to accept the changes?

{accepted === null && (
)} {accepted !== null && (
{accepted ? "✓ Accepted" : "✗ Rejected"}
)}
); } function fromMarkdown(text: string) { const md = new MarkdownIt({ typographer: true, html: true, }); return md.render(text); } function diffPartialText(oldText: string, newText: string, isComplete: boolean = false) { let oldTextToCompare = oldText; if (oldText.length > newText.length && !isComplete) { // make oldText shorter oldTextToCompare = oldText.slice(0, newText.length); } const changes = diffWords(oldTextToCompare, newText); let result = ""; changes.forEach((part) => { if (part.added) { result += `${part.value}`; } else if (part.removed) { result += `${part.value}`; } else { result += part.value; } }); if (oldText.length > newText.length && !isComplete) { result += oldText.slice(newText.length); } return result; } function isAlpha(text: string) { return /[a-zA-Z\u00C0-\u017F]/.test(text.trim()); } ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v2)/predictive_state_updates/style.css ================================================ /* Basic editor styles */ .tiptap-container { height: 100vh; /* Full viewport height */ width: 100vw; /* Full viewport width */ display: flex; flex-direction: column; } .tiptap { flex: 1; /* Take up remaining space */ overflow: auto; /* Allow scrolling if content overflows */ } .tiptap :first-child { margin-top: 0; } /* List styles */ .tiptap ul, .tiptap ol { padding: 0 1rem; margin: 1.25rem 1rem 1.25rem 0.4rem; } .tiptap ul li p, .tiptap ol li p { margin-top: 0.25em; margin-bottom: 0.25em; } /* Heading styles */ .tiptap h1, .tiptap h2, .tiptap h3, .tiptap h4, .tiptap h5, .tiptap h6 { line-height: 1.1; margin-top: 2.5rem; text-wrap: pretty; font-weight: bold; } .tiptap h1, .tiptap h2, .tiptap h3, .tiptap h4, .tiptap h5, .tiptap h6 { margin-top: 3.5rem; margin-bottom: 1.5rem; } .tiptap p { margin-bottom: 1rem; } .tiptap h1 { font-size: 1.4rem; } .tiptap h2 { font-size: 1.2rem; } .tiptap h3 { font-size: 1.1rem; } .tiptap h4, .tiptap h5, .tiptap h6 { font-size: 1rem; } /* Code and preformatted text styles */ .tiptap code { background-color: var(--purple-light); border-radius: 0.4rem; color: var(--black); font-size: 0.85rem; padding: 0.25em 0.3em; } .tiptap pre { background: var(--black); border-radius: 0.5rem; color: var(--white); font-family: "JetBrainsMono", monospace; margin: 1.5rem 0; padding: 0.75rem 1rem; } .tiptap pre code { background: none; color: inherit; font-size: 0.8rem; padding: 0; } .tiptap blockquote { border-left: 3px solid var(--gray-3); margin: 1.5rem 0; padding-left: 1rem; } .tiptap hr { border: none; border-top: 1px solid var(--gray-2); margin: 2rem 0; } .tiptap s { background-color: #f9818150; padding: 2px; font-weight: bold; color: rgba(0, 0, 0, 0.7); } .tiptap em { background-color: #b2f2bb; padding: 2px; font-weight: bold; font-style: normal; } .copilotKitWindow { box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); } ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v2)/shared_state/README.mdx ================================================ # 🍳 Shared State Recipe Creator ## What This Demo Shows This demo showcases CopilotKit's **shared state** functionality - a powerful feature that enables bidirectional data flow between: 1. **Frontend → Agent**: UI controls update the agent's context in real-time 2. **Agent → Frontend**: The Copilot's recipe creations instantly update the UI components It's like having a cooking buddy who not only listens to what you want but also updates your recipe card as you chat - no refresh needed! ✨ ## How to Interact Mix and match any of these parameters (or none at all - it's up to you!): - **Skill Level**: Beginner to expert 👨‍🍳 - **Cooking Time**: Quick meals or slow cooking ⏱️ - **Special Preferences**: Dietary needs, flavor profiles, health goals 🥗 - **Ingredients**: Items you want to include 🧅🥩🍄 - **Instructions**: Any specific steps Then chat with your Copilot chef with prompts like: - "I'm a beginner cook. Can you make me a quick dinner?" - "I need something spicy with chicken that takes under 30 minutes!" ## ✨ Shared State Magic in Action **What's happening technically:** - The UI and Copilot agent share the same state object (**Agent State = UI State**) - Changes from either side automatically update the other - Neither side needs to manually request updates from the other **What you'll see in this demo:** - Set cooking time to 20 minutes in the UI and watch the Copilot immediately respect your time constraint - Add ingredients through the UI and see them appear in your recipe - When the Copilot suggests new ingredients, watch them automatically appear in the UI ingredients list - Change your skill level and see how the Copilot adapts its instructions in real-time This synchronized state creates a seamless experience where the agent always has your current preferences, and any updates to the recipe are instantly reflected in both places. This shared state pattern can be applied to any application where you want your UI and Copilot to work together in perfect harmony! ================================================ FILE: apps/dojo/src/app/[integrationId]/feature/(v2)/shared_state/page.tsx ================================================ "use client"; import { useAgent, UseAgentUpdate, useCopilotKit, useConfigureSuggestions, CopilotChat, CopilotSidebar, } from "@copilotkit/react-core/v2"; import React, { useState, useEffect, useRef } from "react"; import "@copilotkit/react-core/v2/styles.css"; import "./style.css"; import { useMobileView } from "@/utils/use-mobile-view"; import { useMobileChat } from "@/utils/use-mobile-chat"; import { useURLParams } from "@/contexts/url-params-context"; import { CopilotKit } from "@copilotkit/react-core"; interface SharedStateProps { params: Promise<{ integrationId: string; }>; } export default function SharedState({ params }: SharedStateProps) { const { integrationId } = React.use(params); const { isMobile } = useMobileView(); const { chatDefaultOpen } = useURLParams(); const defaultChatHeight = 50; const { isChatOpen, setChatHeight, setIsChatOpen, isDragging, chatHeight, handleDragStart } = useMobileChat(defaultChatHeight); const chatTitle = "AI Recipe Assistant"; const chatDescription = "Ask me to craft recipes"; return (
{isMobile ? ( <> {/* Chat Toggle Button */}
{ if (!isChatOpen) { setChatHeight(defaultChatHeight); // Reset to good default when opening } setIsChatOpen(!isChatOpen); }} >
{chatTitle}
{chatDescription}
{/* Pull-Up Chat Container */}
{/* Drag Handle Bar */}
{/* Chat Header */}

{chatTitle}

{/* Chat Content - Flexible container for messages and input */}
{/* Backdrop */} {isChatOpen && (
setIsChatOpen(false)} /> )} ) : ( )}
); } enum SkillLevel { BEGINNER = "Beginner", INTERMEDIATE = "Intermediate", ADVANCED = "Advanced", } enum CookingTime { FiveMin = "5 min", FifteenMin = "15 min", ThirtyMin = "30 min", FortyFiveMin = "45 min", SixtyPlusMin = "60+ min", } const cookingTimeValues = [ { label: CookingTime.FiveMin, value: 0 }, { label: CookingTime.FifteenMin, value: 1 }, { label: CookingTime.ThirtyMin, value: 2 }, { label: CookingTime.FortyFiveMin, value: 3 }, { label: CookingTime.SixtyPlusMin, value: 4 }, ]; enum SpecialPreferences { HighProtein = "High Protein", LowCarb = "Low Carb", Spicy = "Spicy", BudgetFriendly = "Budget-Friendly", OnePotMeal = "One-Pot Meal", Vegetarian = "Vegetarian", Vegan = "Vegan", } interface Ingredient { icon: string; name: string; amount: string; } interface Recipe { title: string; skill_level: SkillLevel; cooking_time: CookingTime; special_preferences: string[]; ingredients: Ingredient[]; instructions: string[]; } interface RecipeAgentState { recipe: Recipe; } const INITIAL_STATE: RecipeAgentState = { recipe: { title: "Make Your Recipe", skill_level: SkillLevel.INTERMEDIATE, cooking_time: CookingTime.FortyFiveMin, special_preferences: [], ingredients: [ { icon: "🥕", name: "Carrots", amount: "3 large, grated" }, { icon: "🌾", name: "All-Purpose Flour", amount: "2 cups" }, ], instructions: ["Preheat oven to 350°F (175°C)"], }, }; function Recipe() { const { isMobile } = useMobileView(); const { agent } = useAgent({ agentId: "shared_state", updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged], }); const { copilotkit } = useCopilotKit(); useConfigureSuggestions({ suggestions: [ { title: "Create Italian recipe", message: "Create a delicious Italian pasta recipe.", }, { title: "Make it healthier", message: "Make the recipe healthier with more vegetables.", }, { title: "Suggest variations", message: "Suggest some creative variations of this recipe.", }, ], available: "always", }); const agentState = agent.state as RecipeAgentState | undefined; const setAgentState = (s: RecipeAgentState) => agent.setState(s); const isLoading = agent.isRunning; // Set initial state on mount useEffect(() => { if (!agentState?.recipe) { setAgentState(INITIAL_STATE); } }, []); const [recipe, setRecipe] = useState(INITIAL_STATE.recipe); const [editingInstructionIndex, setEditingInstructionIndex] = useState(null); const newInstructionRef = useRef(null); const updateRecipe = (partialRecipe: Partial) => { setAgentState({ ...(agentState || INITIAL_STATE), recipe: { ...recipe, ...partialRecipe, }, }); setRecipe({ ...recipe, ...partialRecipe, }); }; const newRecipeState = { ...recipe }; const newChangedKeys = []; const changedKeysRef = useRef([]); for (const key in recipe) { if ( agentState && agentState.recipe && (agentState.recipe as any)[key] !== undefined && (agentState.recipe as any)[key] !== null ) { let agentValue = (agentState.recipe as any)[key]; const recipeValue = (recipe as any)[key]; // Check if agentValue is a string and replace \n with actual newlines if (typeof agentValue === "string") { agentValue = agentValue.replace(/\\n/g, "\n"); } if (JSON.stringify(agentValue) !== JSON.stringify(recipeValue)) { (newRecipeState as any)[key] = agentValue; newChangedKeys.push(key); } } } if (newChangedKeys.length > 0) { changedKeysRef.current = newChangedKeys; } else if (!isLoading) { changedKeysRef.current = []; } useEffect(() => { setRecipe(newRecipeState); }, [JSON.stringify(newRecipeState)]); const handleTitleChange = (event: React.ChangeEvent) => { updateRecipe({ title: event.target.value, }); }; const handleSkillLevelChange = (event: React.ChangeEvent) => { updateRecipe({ skill_level: event.target.value as SkillLevel, }); }; const handleDietaryChange = (preference: string, checked: boolean) => { if (checked) { updateRecipe({ special_preferences: [...recipe.special_preferences, preference], }); } else { updateRecipe({ special_preferences: recipe.special_preferences.filter((p) => p !== preference), }); } }; const handleCookingTimeChange = (event: React.ChangeEvent) => { updateRecipe({ cooking_time: cookingTimeValues[Number(event.target.value)].label, }); }; const addIngredient = () => { // Pick a random food emoji from our valid list updateRecipe({ ingredients: [...recipe.ingredients, { icon: "🍴", name: "", amount: "" }], }); }; const updateIngredient = (index: number, field: keyof Ingredient, value: string) => { const updatedIngredients = [...recipe.ingredients]; updatedIngredients[index] = { ...updatedIngredients[index], [field]: value, }; updateRecipe({ ingredients: updatedIngredients }); }; const removeIngredient = (index: number) => { const updatedIngredients = [...recipe.ingredients]; updatedIngredients.splice(index, 1); updateRecipe({ ingredients: updatedIngredients }); }; const addInstruction = () => { const newIndex = recipe.instructions.length; updateRecipe({ instructions: [...recipe.instructions, ""], }); // Set the new instruction as the editing one setEditingInstructionIndex(newIndex); // Focus the new instruction after render setTimeout(() => { const textareas = document.querySelectorAll(".instructions-container textarea"); const newTextarea = textareas[textareas.length - 1] as HTMLTextAreaElement; if (newTextarea) { newTextarea.focus(); } }, 50); }; const updateInstruction = (index: number, value: string) => { const updatedInstructions = [...recipe.instructions]; updatedInstructions[index] = value; updateRecipe({ instructions: updatedInstructions }); }; const removeInstruction = (index: number) => { const updatedInstructions = [...recipe.instructions]; updatedInstructions.splice(index, 1); updateRecipe({ instructions: updatedInstructions }); }; // Simplified icon handler that defaults to a fork/knife for any problematic icons const getProperIcon = (icon: string | undefined): string => { // If icon is undefined return the default if (!icon) { return "🍴"; } return icon; }; return (
{/* Recipe Title */}
🕒
🏆
{/* Dietary Preferences */}
{changedKeysRef.current.includes("special_preferences") && }

Dietary Preferences

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

Ingredients

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

Instructions

{recipe.instructions.map((instruction, index) => (
{/* Number Circle */}
{index + 1}
{/* Vertical Line */} {index < recipe.instructions.length - 1 &&
} {/* Instruction Content */}
setEditingInstructionIndex(index)} >