From 6eeab82e3c40df9b6a38f3eaa2b98ef356036a90 Mon Sep 17 00:00:00 2001 From: Austin Merrick Date: Thu, 18 Jun 2026 09:35:51 -0700 Subject: [PATCH 01/12] fix(skills): correct copilotkit-upgrade v2 package paths and API accuracy The copilotkit-upgrade skill was the one skill skipped by the v2 migration. It routed migrators to packages that do not exist and documented many v2 APIs incorrectly, so following it produced non-installable / non-compiling code. This is a full accuracy pass, verified against the live source barrels. Package paths (phantom -> real /v2 subpaths): - `@copilotkit/react` (phantom) -> `@copilotkit/react-core/v2` - `@copilotkit/agent` (phantom) -> `@copilotkit/runtime/v2` (`BuiltInAgent`) - bare `@copilotkit/runtime` for v2-only APIs -> `@copilotkit/runtime/v2` - `createCopilotEndpointExpress` (phantom) -> `createCopilotExpressHandler` (/v2/express) - `LangGraphAgent` -> `@copilotkit/runtime/langgraph` (no separate @ag-ui install) API accuracy: - `available` is a boolean (default true; false hides), not "enabled"|"disabled" - `useRenderToolCall()` / `useRenderActivityMessage()` are zero-arg, return a render function; renderers match by tool name, not call id - `LangGraphAgent` config field is `deploymentUrl`, not `url` (compile error) - `BuiltInAgent` model strings use slash form + real ids (openai/gpt-4o, anthropic/claude-sonnet-4.5, google/gemini-2.5-pro) - `GroqAdapter` / `BedrockAdapter` -> custom `AbstractAgent` (no such provider; vertex is Google, not AWS Bedrock) - `createCopilotEndpoint` / `createCopilotEndpointExpress` flagged @deprecated; canonical: `createCopilotHonoHandler` / `createCopilotExpressHandler` - `TextMessage` is not a v2 type; use the `Message` union / per-role types - `mcpApps.servers` entries are flat `{ type, url }`, not `{ transport: {...} }` - Next.js App Router needs `export const POST/GET = app.fetch` (not `export default`) - `useCopilotKit` returns `{ copilotkit, executingToolCallIds }` from `/v2/context` - `useDefaultTool` -> split: `useDefaultRenderTool` (render) + `useFrontendTool` (handler) - `LangGraphHttpAgent` is a distinct class, not merged into `LangGraphAgent` - `guardrails_c` is active for CopilotCloud topic restrictions on the compat provider (not a delete-me no-op); no effect on the v2 AG-UI runtime - `CopilotKitProps` extends `Omit` - tool-call render props include `toolCallId`; reconciled `ToolCallStatus` and `useInterrupt` render-prop wording; v1 Next.js handler is `copilotRuntimeNextJSAppRouterEndpoint` (not `copilotKitEndpoint`) - fixed a self-contradictory sentence in the Overview Also rewrites sources.md, which was generated against a non-existent `packages/v1/*` / `packages/v2/*` layout (the root cause of the phantom names), and corrects its file-path citations to the real flat layout. --- skills/copilotkit-upgrade/SKILL.md | 66 ++++---- .../references/breaking-changes.md | 74 +++++---- .../references/deprecation-map.md | 126 +++++++------- .../references/v1-to-v2-migration.md | 155 ++++++++++-------- skills/copilotkit-upgrade/sources.md | 68 ++++---- 5 files changed, 265 insertions(+), 224 deletions(-) diff --git a/skills/copilotkit-upgrade/SKILL.md b/skills/copilotkit-upgrade/SKILL.md index 9dca7877d20..8d1a6f5719d 100644 --- a/skills/copilotkit-upgrade/SKILL.md +++ b/skills/copilotkit-upgrade/SKILL.md @@ -1,7 +1,7 @@ --- name: copilotkit-upgrade description: "Use when migrating a CopilotKit v1 application to v2 -- updating package imports, replacing deprecated hooks and components, switching from GraphQL runtime to AG-UI protocol runtime, and resolving breaking API changes." -version: 1.0.0 +version: 1.0.1 --- # CopilotKit v1 to v2 Migration Skill @@ -15,7 +15,7 @@ This plugin includes an MCP server (`copilotkit-docs`) that provides `search-doc ## Overview -CopilotKit v2 is a ground-up rewrite built on the AG-UI protocol (`@ag-ui/client` / `@ag-ui/core`). Users continue to install and import `@copilotkit/*` packages -- the v2 changes are exposed through the same package names with updated APIs (new hook names, component names, runtime configuration). The `@copilotkit/*` namespace is an internal implementation detail that users never interact with. +CopilotKit v2 is a ground-up rewrite built on the AG-UI protocol (`@ag-ui/client` / `@ag-ui/core`). Users continue to install and import `@copilotkit/*` packages -- the v2 changes are exposed through the same package names (under their `/v2` subpaths) with updated APIs (new hook names, component names, runtime configuration). The underlying `@ag-ui/*` packages are an internal implementation detail re-exported through `@copilotkit/react-core/v2`, so users never need to install them directly. ## Migration Workflow @@ -41,9 +41,10 @@ Key hooks and components to find and replace: | ---------------------------------- | ---------------------------------------------------- | | `useCopilotAction` | `useFrontendTool` | | `useCopilotReadable` | `useAgentContext` | -| `useCopilotChat` | `useAgent` + `useSuggestions` | +| `useCopilotChat` | `useAgent` | | `useCoAgent` | `useAgent` | -| `useCoAgentStateRender` | `useRenderToolCall` / `useRenderActivityMessage` | +| `useCoAgentStateRender` | `useRenderTool` / `useRenderActivityMessage` | +| `useCopilotContext` | `useCopilotKit` (from `@copilotkit/react-core/v2/context`) | | `useLangGraphInterrupt` | `useInterrupt` | | `useCopilotChatSuggestions` | `useConfigureSuggestions` + `useSuggestions` | | `useCopilotAdditionalInstructions` | `useAgentContext` | @@ -57,16 +58,16 @@ Refer to `references/v1-to-v2-migration.md` for detailed before/after code examp ### 4. Update Package Dependencies -The `@copilotkit/*` package names stay the same. Update to the latest v2 versions: +The `@copilotkit/*` package names stay the same. v2 does **not** introduce new package names -- the v2 APIs ship from the **`/v2` subpath** of the existing packages (`@copilotkit/react-core/v2`, `@copilotkit/runtime/v2`). There is no `@copilotkit/react` or `@copilotkit/agent` package. Update to the latest v2 versions: ``` -@copilotkit/react-core -> @copilotkit/react (consolidated into one package) -@copilotkit/react-ui -> @copilotkit/react (consolidated into one package) +@copilotkit/react-core -> @copilotkit/react-core (v2 symbols under the /v2 subpath) +@copilotkit/react-ui -> chat components move to @copilotkit/react-core/v2; react-ui contributes only styles in v2 @copilotkit/react-textarea -> removed (no v2 equivalent) -@copilotkit/runtime -> @copilotkit/runtime (same package, new agent-based API) -@copilotkit/runtime-client-gql -> removed (replaced by AG-UI protocol, re-exported from @copilotkit/react) +@copilotkit/runtime -> @copilotkit/runtime (v2 symbols under the /v2 subpath) +@copilotkit/runtime-client-gql -> removed (replaced by AG-UI protocol; @ag-ui/client types are re-exported from @copilotkit/react-core/v2) @copilotkit/shared -> @copilotkit/shared (same package) -@copilotkit/sdk-js -> @copilotkit/agent (new package for agent definitions) +@copilotkit/sdk-js -> removed (BuiltInAgent now ships from @copilotkit/runtime/v2) ``` ### 5. Update Runtime Configuration @@ -78,20 +79,25 @@ The v1 `CopilotRuntime` accepted service adapters (OpenAI, Anthropic, LangChain, ```ts import { CopilotRuntime, OpenAIAdapter } from "@copilotkit/runtime"; const runtime = new CopilotRuntime({ actions: [...] }); -// used with copilotKitEndpoint() for Next.js, Express, etc. +// used with framework handlers like copilotRuntimeNextJSAppRouterEndpoint() (Next.js), etc. ``` **v2 pattern** (agents + Hono endpoint): ```ts -import { CopilotRuntime, createCopilotEndpoint } from "@copilotkit/runtime"; -import { BuiltInAgent } from "@copilotkit/agent"; +import { + CopilotRuntime, + BuiltInAgent, + createCopilotHonoHandler, +} from "@copilotkit/runtime/v2"; const runtime = new CopilotRuntime({ - agents: { myAgent: new BuiltInAgent({ model: "openai:gpt-4o" }) }, + agents: { myAgent: new BuiltInAgent({ model: "openai/gpt-4o" }) }, }); -const app = createCopilotEndpoint({ runtime, basePath: "/api/copilotkit" }); +const app = createCopilotHonoHandler({ runtime, basePath: "/api/copilotkit" }); ``` +> Use `createCopilotHonoHandler` (from `@copilotkit/runtime/v2`) as the canonical Hono endpoint factory. `createCopilotEndpoint` is a **deprecated** alias for it -- avoid it in new code. For Express, use `createCopilotExpressHandler` from `@copilotkit/runtime/v2/express` (`createCopilotEndpointExpress` is its deprecated alias). + ### 6. Update Provider The provider component keeps the name `CopilotKit` -- only the import path changes. The package root (`@copilotkit/react-core`) is the legacy v1 provider; the `/v2` subpath is the migration target. @@ -121,18 +127,18 @@ import { CopilotKit } from "@copilotkit/react-core/v2"; ## Quick Reference -| Concept | v1 | v2 | -| -------------------- | ----------------------------------------------- | -------------------------------------------------------------------------- | -| Package scope | `@copilotkit/*` | `@copilotkit/*` (same scope, updated APIs) | -| Protocol | GraphQL | AG-UI (SSE) | -| Provider component | `CopilotKit` (from `@copilotkit/react-core`) | `CopilotKit` (from `@copilotkit/react-core/v2`) | -| Define frontend tool | `useCopilotAction` | `useFrontendTool` | -| Share app state | `useCopilotReadable` | `useAgentContext` | -| Agent interaction | `useCoAgent` | `useAgent` | -| Handle interrupts | `useLangGraphInterrupt` | `useInterrupt` | -| Render tool calls | `useCopilotAction({ render })` | `useRenderToolCall` | -| Chat suggestions | `useCopilotChatSuggestions` | `useConfigureSuggestions` | -| Runtime class | `CopilotRuntime` (adapters) | `CopilotRuntime` (agents) | -| Endpoint setup | `copilotKitEndpoint()` | `createCopilotEndpoint()` | -| Agent definition | `LangGraphAgent` endpoint | `AbstractAgent` / `BuiltInAgent` | -| Chat components | `CopilotChat`, `CopilotPopup`, `CopilotSidebar` | `CopilotChat`, `CopilotPopup`, `CopilotSidebar` (from `@copilotkit/react`) | +| Concept | v1 | v2 | +| -------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------- | +| Package scope | `@copilotkit/*` | `@copilotkit/*` (same scope, updated APIs) | +| Protocol | GraphQL | AG-UI (SSE) | +| Provider component | `CopilotKit` (from `@copilotkit/react-core`) | `CopilotKit` (from `@copilotkit/react-core/v2`) | +| Define frontend tool | `useCopilotAction` | `useFrontendTool` | +| Share app state | `useCopilotReadable` | `useAgentContext` | +| Agent interaction | `useCoAgent` | `useAgent` | +| Handle interrupts | `useLangGraphInterrupt` | `useInterrupt` | +| Render tool calls | `useCopilotAction({ render })` | `useFrontendTool({ render })` or `useRenderTool` (render-only) | +| Chat suggestions | `useCopilotChatSuggestions` | `useConfigureSuggestions` | +| Runtime class | `CopilotRuntime` (adapters) | `CopilotRuntime` (agents, from `@copilotkit/runtime/v2`) | +| Endpoint setup | `copilotRuntimeNextJSAppRouterEndpoint()` | `createCopilotHonoHandler()` (`createCopilotEndpoint` is a deprecated alias) | +| Agent definition | `LangGraphAgent` endpoint | `AbstractAgent` / `BuiltInAgent` (from `@copilotkit/runtime/v2`) | +| Chat components | `CopilotChat`, `CopilotPopup`, `CopilotSidebar` | `CopilotChat`, `CopilotPopup`, `CopilotSidebar` (from `@copilotkit/react-core/v2`) | diff --git a/skills/copilotkit-upgrade/references/breaking-changes.md b/skills/copilotkit-upgrade/references/breaking-changes.md index 7382364ba97..e069e9473a0 100644 --- a/skills/copilotkit-upgrade/references/breaking-changes.md +++ b/skills/copilotkit-upgrade/references/breaking-changes.md @@ -10,21 +10,21 @@ v1 split React functionality across three packages: - `@copilotkit/react-ui` -- chat components (CopilotChat, CopilotPopup, CopilotSidebar) - `@copilotkit/react-textarea` -- CopilotTextarea component -v2 consolidates everything into a single package: +v2 consolidates the React surface under the **`/v2` subpath of the same `@copilotkit/react-core` package** (there is no `@copilotkit/react` package): -- `@copilotkit/react` -- provider, hooks, types, chat components, AG-UI re-exports +- `@copilotkit/react-core/v2` -- provider, hooks, types, chat components, and AG-UI re-exports (`@copilotkit/react-core/v2/styles.css` for styles) -### New package scope +### Same package names, new subpath -The v2 API is exposed through the same `@copilotkit/*` packages. No package name changes are required when upgrading. +The v2 API is exposed through the same `@copilotkit/*` packages -- no package name changes are required when upgrading. The v2 symbols live under the `/v2` subpath (`@copilotkit/react-core/v2`, `@copilotkit/runtime/v2`, `@copilotkit/runtime/v2/express`). ### Removed packages -| Package | Status | -| -------------------------------- | ------------------------------------------------------------------ | -| `@copilotkit/react-textarea` | Removed. No v2 equivalent. | -| `@copilotkit/runtime-client-gql` | Replaced by `@ag-ui/client` (re-exported from `@copilotkit/react`) | -| `@copilotkit/sdk-js` | Replaced by `@copilotkit/agent` | +| Package | Status | +| -------------------------------- | -------------------------------------------------------------------------------- | +| `@copilotkit/react-textarea` | No v2 equivalent. The v1 package stays installable; remove it only after migrating off `CopilotTextarea`. | +| `@copilotkit/runtime-client-gql` | Replaced by `@ag-ui/client` (re-exported from `@copilotkit/react-core/v2`) | +| `@copilotkit/sdk-js` | Removed. `BuiltInAgent` and agent definitions ship from `@copilotkit/runtime/v2` | --- @@ -45,7 +45,7 @@ The most fundamental breaking change is the protocol layer. v1 used a GraphQL-ba ### Component import path change -The provider keeps the name `CopilotKit`; the import path changes from the package root (`@copilotkit/react-core`, legacy v1) to the `/v2` subpath (`@copilotkit/react-core/v2`). The `/v2` subpath also exports a `CopilotKitProvider` component -- do **not** migrate to it. It is a functionality subset of `CopilotKit`, which is the compatibility bridge across v1 and v2 (its `CopilotKitProps` extends `CopilotKitProviderProps`, so every `CopilotKitProvider` prop works on it). +The provider keeps the name `CopilotKit`; the import path changes from the package root (`@copilotkit/react-core`, legacy v1) to the `/v2` subpath (`@copilotkit/react-core/v2`). The `/v2` subpath also exports a `CopilotKitProvider` component -- do **not** migrate to it. It is a functionality subset of `CopilotKit`, which is the compatibility bridge across v1 and v2 (its `CopilotKitProps` extends `Omit` with a narrowed `children` type, so every non-`children` `CopilotKitProvider` prop works on it). ### Props changes @@ -56,7 +56,7 @@ The provider keeps the name `CopilotKit`; the import path changes from the packa | `publicApiKey` | Kept (deprecated) | `publicLicenseKey` is the canonical name | | `properties` | Kept | Same behavior | | `agents` | Removed | Use `selfManagedAgents` or `agents__unsafe_dev_only` | -| `guardrails_c` | Removed | -- | +| `guardrails_c` | Kept (CopilotCloud only) | Marked `@internal`/defunct in source, but still wired into the legacy CopilotCloud `restrictToTopic` config when a cloud key (`publicApiKey`) is set; has no effect on the v2 AG-UI runtime path | | `children` | Kept | Same behavior | | -- | Added: `credentials` | `RequestCredentials` for fetch (e.g., `"include"` for cookies) | | -- | Added: `selfManagedAgents` | `Record` for client-side agents | @@ -66,7 +66,7 @@ The provider keeps the name `CopilotKit`; the import path changes from the packa ### Context hook rename -`useCopilotContext` is replaced by `useCopilotKit` which returns `{ copilotkit: CopilotKitCoreReact }`. +`useCopilotContext` is replaced by `useCopilotKit` (imported from `@copilotkit/react-core/v2/context`), which returns `{ copilotkit: CopilotKitCoreReact, executingToolCallIds: ReadonlySet }`. --- @@ -103,21 +103,23 @@ handler: async (args) => { ... } // args is typed from the Zod schema **Render props change:** ```ts -// v1 render status: "inProgress" | "executing" | "complete" +// v1 render status: the string literals "inProgress" | "executing" | "complete" // v1 uses `respond()` callback for interactive actions -// v2 render status: ToolCallStatus.InProgress | ToolCallStatus.Executing | ToolCallStatus.Complete -// v2 render props: { name, args, status, result } +// v2 render status: the `ToolCallStatus` enum (ToolCallStatus.InProgress | .Executing | .Complete). +// Its values ARE those same strings, so `status === "inProgress"` still works; +// prefer comparing against the enum members. +// v2 render props: { name, toolCallId, args, status, result } ``` **Availability change:** ```ts // v1 -disabled: true; // or available: "disabled" +disabled: true; -// v2 -available: "disabled" | "enabled"; +// v2 — `available` is a boolean (defaults to true; set false to hide the tool) +available: false; ``` ### useCopilotReadable -> useAgentContext @@ -162,7 +164,7 @@ AbstractAgent; // AG-UI agent instance with run(), stop(), etc. - `agentName` -> `agentId` - `nodeName` -> removed (use `enabled` predicate to filter) -- `render` props change: receives `InterruptRenderProps` instead of v1's `{ event, resolve }` +- `render` props change: v2 receives `InterruptRenderProps` = `{ event, resolve, result }` (still includes `event`/`resolve`, adds `result`) - New `renderInChat` prop (default `true`) controls whether interrupt renders inside CopilotChat - New `handler` prop for programmatic handling before rendering - New `enabled` predicate prop for filtering interrupts @@ -197,14 +199,14 @@ All service adapters are removed from the runtime: | Removed Adapter | v2 Alternative | | --------------------------- | ------------------------------------------------------------------- | -| `OpenAIAdapter` | Use `BuiltInAgent({ model: "openai:gpt-4o" })` | -| `AnthropicAdapter` | Use `BuiltInAgent({ model: "anthropic:claude-sonnet-4-20250514" })` | -| `GoogleGenerativeAIAdapter` | Use `BuiltInAgent({ model: "google:gemini-pro" })` | +| `OpenAIAdapter` | Use `BuiltInAgent({ model: "openai/gpt-4o" })` | +| `AnthropicAdapter` | Use `BuiltInAgent({ model: "anthropic/claude-sonnet-4.5" })` | +| `GoogleGenerativeAIAdapter` | Use `BuiltInAgent({ model: "google/gemini-2.5-pro" })` | | `LangChainAdapter` | Use a custom `AbstractAgent` implementation | -| `GroqAdapter` | Use `BuiltInAgent` with Groq-compatible model string | +| `GroqAdapter` | Use a custom `AbstractAgent` (pass a Groq `LanguageModel` instance) | | `UnifyAdapter` | Use a custom `AbstractAgent` implementation | | `OpenAIAssistantAdapter` | Use a custom `AbstractAgent` implementation | -| `BedrockAdapter` | Use `BuiltInAgent({ model: "vertex:..." })` or custom agent | +| `BedrockAdapter` | Use a custom `AbstractAgent` implementation | | `OllamaAdapter` | Use a custom `AbstractAgent` implementation | | `EmptyAdapter` | Not needed | @@ -239,13 +241,13 @@ new CopilotRuntime({ v1 had built-in integrations for Next.js (App Router, Pages Router), Express, NestJS, and Node HTTP. v2 uses Hono as the standard HTTP layer: -| v1 Integration | v2 Replacement | -| ----------------------------------------- | -------------------------------------------------- | -| `copilotRuntimeNextJSAppRouterEndpoint` | `createCopilotEndpoint` (Hono, works with Next.js) | -| `copilotRuntimeNextJSPagesRouterEndpoint` | `createCopilotEndpoint` (Hono) | -| `CopilotRuntimeNodeExpressEndpoint` | `createCopilotEndpointExpress` | -| `CopilotRuntimeNestEndpoint` | Use Hono adapter or Express endpoint | -| `CopilotRuntimeNodeHttpEndpoint` | Use Hono or Express endpoint | +| v1 Integration | v2 Replacement | +| ----------------------------------------- | ---------------------------------------------------------------- | +| `copilotRuntimeNextJSAppRouterEndpoint` | `createCopilotHonoHandler` (Hono, works with Next.js) | +| `copilotRuntimeNextJSPagesRouterEndpoint` | `createCopilotHonoHandler` (Hono) | +| `CopilotRuntimeNodeExpressEndpoint` | `createCopilotExpressHandler` (`@copilotkit/runtime/v2/express`) | +| `CopilotRuntimeNestEndpoint` | Use Hono adapter or Express endpoint | +| `CopilotRuntimeNodeHttpEndpoint` | Use Hono or Express endpoint | ### Endpoint configuration @@ -259,10 +261,10 @@ export const POST = copilotRuntimeNextJSAppRouterEndpoint({ endpoint: "/api/copilotkit", }); -// v2 -import { createCopilotEndpoint } from "@copilotkit/runtime"; +// v2 -- use createCopilotHonoHandler (createCopilotEndpoint is a deprecated alias) +import { createCopilotHonoHandler } from "@copilotkit/runtime/v2"; -const app = createCopilotEndpoint({ +const app = createCopilotHonoHandler({ runtime, basePath: "/api/copilotkit", cors: { @@ -290,12 +292,12 @@ new CopilotRuntime({ }); // v2 (direct agent instance) -import { LangGraphAgent } from "@ag-ui/langgraph"; +import { LangGraphAgent } from "@copilotkit/runtime/langgraph"; new CopilotRuntime({ agents: { myAgent: new LangGraphAgent({ - url: "http://localhost:8000", + deploymentUrl: "http://localhost:8000", graphId: "my-graph", }), }, diff --git a/skills/copilotkit-upgrade/references/deprecation-map.md b/skills/copilotkit-upgrade/references/deprecation-map.md index aafb5613222..c48096371c3 100644 --- a/skills/copilotkit-upgrade/references/deprecation-map.md +++ b/skills/copilotkit-upgrade/references/deprecation-map.md @@ -4,89 +4,89 @@ Complete mapping of every deprecated v1 API to its v2 replacement. ## Hooks -| v1 Hook | v1 Package | v2 Replacement | v2 Package | Status | -| ---------------------------------- | ------------------------ | ------------------------------------------------ | ------------------- | ------------------------------------ | -| `useCopilotAction` | `@copilotkit/react-core` | `useFrontendTool` | `@copilotkit/react` | Renamed + new parameter format (Zod) | -| `useCopilotReadable` | `@copilotkit/react-core` | `useAgentContext` | `@copilotkit/react` | Renamed, `parentId` removed | -| `useCopilotChat` | `@copilotkit/react-core` | `useAgent` | `@copilotkit/react` | Replaced (different API) | -| `useCoAgent` | `@copilotkit/react-core` | `useAgent` | `@copilotkit/react` | Renamed, different return type | -| `useCoAgentStateRender` | `@copilotkit/react-core` | `useRenderToolCall` / `useRenderActivityMessage` | `@copilotkit/react` | Split into two hooks | -| `useLangGraphInterrupt` | `@copilotkit/react-core` | `useInterrupt` | `@copilotkit/react` | Renamed + new API | -| `useCopilotChatSuggestions` | `@copilotkit/react-core` | `useConfigureSuggestions` + `useSuggestions` | `@copilotkit/react` | Split into two hooks | -| `useCopilotAdditionalInstructions` | `@copilotkit/react-core` | `useAgentContext` | `@copilotkit/react` | Use description/value context | -| `useMakeCopilotDocumentReadable` | `@copilotkit/react-core` | `useAgentContext` | `@copilotkit/react` | Pass content directly | -| `useCopilotRuntimeClient` | `@copilotkit/react-core` | `useCopilotKit` | `@copilotkit/react` | Access core via provider context | -| `useCopilotContext` | `@copilotkit/react-core` | `useCopilotKit` | `@copilotkit/react` | Returns `{ copilotkit }` | -| `useCopilotMessagesContext` | `@copilotkit/react-core` | -- | -- | Removed (use agent event stream) | -| `useCoAgentStateRenders` | `@copilotkit/react-core` | -- | -- | Removed (context no longer needed) | -| `useCopilotChatInternal` | `@copilotkit/react-core` | -- | -- | Internal, removed | -| `useCopilotChatHeadless_c` | `@copilotkit/react-core` | -- | -- | Internal, removed | -| `useCopilotAuthenticatedAction_c` | `@copilotkit/react-core` | -- | -- | Internal, removed | -| `useFrontendTool` | `@copilotkit/react-core` | `useFrontendTool` | `@copilotkit/react` | Same name, import path changes | -| `useHumanInTheLoop` | `@copilotkit/react-core` | `useHumanInTheLoop` | `@copilotkit/react` | Same name, import path changes | -| `useRenderToolCall` | `@copilotkit/react-core` | `useRenderToolCall` | `@copilotkit/react` | Same name, import path changes | -| `useDefaultTool` | `@copilotkit/react-core` | `useDefaultRenderTool` | `@copilotkit/react` | Renamed | -| `useLazyToolRenderer` | `@copilotkit/react-core` | -- | -- | Removed | -| `useChatContext` (react-ui) | `@copilotkit/react-ui` | `useCopilotChatConfiguration` | `@copilotkit/react` | Renamed | +| v1 Hook | v1 Package | v2 Replacement | v2 Package | Status | +| ---------------------------------- | ------------------------ | ------------------------------------------------ | ----------------------------------- | ------------------------------------ | +| `useCopilotAction` | `@copilotkit/react-core` | `useFrontendTool` | `@copilotkit/react-core/v2` | Renamed + new parameter format (Zod) | +| `useCopilotReadable` | `@copilotkit/react-core` | `useAgentContext` | `@copilotkit/react-core/v2` | Renamed, `parentId` removed | +| `useCopilotChat` | `@copilotkit/react-core` | `useAgent` | `@copilotkit/react-core/v2` | Replaced (different API) | +| `useCoAgent` | `@copilotkit/react-core` | `useAgent` | `@copilotkit/react-core/v2` | Renamed, different return type | +| `useCoAgentStateRender` | `@copilotkit/react-core` | `useRenderTool` / `useRenderActivityMessage` | `@copilotkit/react-core/v2` | Split: render-by-name + activity rendering | +| `useLangGraphInterrupt` | `@copilotkit/react-core` | `useInterrupt` | `@copilotkit/react-core/v2` | Renamed + new API | +| `useCopilotChatSuggestions` | `@copilotkit/react-core` | `useConfigureSuggestions` + `useSuggestions` | `@copilotkit/react-core/v2` | Split into two hooks | +| `useCopilotAdditionalInstructions` | `@copilotkit/react-core` | `useAgentContext` | `@copilotkit/react-core/v2` | Use description/value context | +| `useMakeCopilotDocumentReadable` | `@copilotkit/react-core` | `useAgentContext` | `@copilotkit/react-core/v2` | Pass content directly | +| `useCopilotRuntimeClient` | `@copilotkit/react-core` | `useCopilotKit` | `@copilotkit/react-core/v2/context` | Access core via provider context | +| `useCopilotContext` | `@copilotkit/react-core` | `useCopilotKit` | `@copilotkit/react-core/v2/context` | Returns `{ copilotkit, executingToolCallIds }` | +| `useCopilotMessagesContext` | `@copilotkit/react-core` | -- | -- | Removed (use agent event stream) | +| `useCoAgentStateRenders` | `@copilotkit/react-core` | -- | -- | Removed (context no longer needed) | +| `useCopilotChatInternal` | `@copilotkit/react-core` | -- | -- | Internal, removed | +| `useCopilotChatHeadless_c` | `@copilotkit/react-core` | -- | -- | Internal, removed | +| `useCopilotAuthenticatedAction_c` | `@copilotkit/react-core` | -- | -- | Internal, removed | +| `useFrontendTool` | `@copilotkit/react-core` | `useFrontendTool` | `@copilotkit/react-core/v2` | Same name, import path changes | +| `useHumanInTheLoop` | `@copilotkit/react-core` | `useHumanInTheLoop` | `@copilotkit/react-core/v2` | Same name, import path changes | +| `useRenderToolCall` | `@copilotkit/react-core` | `useRenderToolCall` | `@copilotkit/react-core/v2` | Same name, import path changes | +| `useDefaultTool` | `@copilotkit/react-core` | `useDefaultRenderTool` (render) / `useFrontendTool` (handler) | `@copilotkit/react-core/v2` | Split: v1's catch-all had a handler; v2 `useDefaultRenderTool` is render-only | +| `useLazyToolRenderer` | `@copilotkit/react-core` | -- | -- | Removed | +| `useChatContext` (react-ui) | `@copilotkit/react-ui` | `useCopilotChatConfiguration` | `@copilotkit/react-core/v2` | Renamed | ## Components | v1 Component | v1 Package | v2 Replacement | v2 Package | Status | | ----------------------------- | ---------------------------- | ----------------------------- | --------------------------- | ------------------------------------ | | `CopilotKit` | `@copilotkit/react-core` | `CopilotKit` | `@copilotkit/react-core/v2` | Same name, new import path | -| `CopilotChat` | `@copilotkit/react-ui` | `CopilotChat` | `@copilotkit/react` | Same name, new package | -| `CopilotPopup` | `@copilotkit/react-ui` | `CopilotPopup` | `@copilotkit/react` | Same name, new package | -| `CopilotSidebar` | `@copilotkit/react-ui` | `CopilotSidebar` | `@copilotkit/react` | Same name, new package | +| `CopilotChat` | `@copilotkit/react-ui` | `CopilotChat` | `@copilotkit/react-core/v2` | Same name, new package | +| `CopilotPopup` | `@copilotkit/react-ui` | `CopilotPopup` | `@copilotkit/react-core/v2` | Same name, new package | +| `CopilotSidebar` | `@copilotkit/react-ui` | `CopilotSidebar` | `@copilotkit/react-core/v2` | Same name, new package | | `CopilotTextarea` | `@copilotkit/react-textarea` | -- | -- | **Removed** | -| `CopilotDevConsole` | `@copilotkit/react-ui` | `CopilotKitInspector` | `@copilotkit/react` | Renamed | -| `Markdown` | `@copilotkit/react-ui` | -- | -- | Removed (use A2UI renderer) | -| `AssistantMessage` | `@copilotkit/react-ui` | `CopilotChatAssistantMessage` | `@copilotkit/react` | Renamed | -| `UserMessage` | `@copilotkit/react-ui` | `CopilotChatUserMessage` | `@copilotkit/react` | Renamed | +| `CopilotDevConsole` | `@copilotkit/react-ui` | `CopilotKitInspector` | `@copilotkit/react-core/v2` | Renamed | +| `Markdown` | `@copilotkit/react-ui` | -- | -- | Removed -- v2 chat components render markdown internally | +| `AssistantMessage` | `@copilotkit/react-ui` | `CopilotChatAssistantMessage` | `@copilotkit/react-core/v2` | Renamed | +| `UserMessage` | `@copilotkit/react-ui` | `CopilotChatUserMessage` | `@copilotkit/react-core/v2` | Renamed | | `ImageRenderer` | `@copilotkit/react-ui` | -- | -- | Removed | -| `RenderSuggestionsList` | `@copilotkit/react-ui` | `CopilotChatSuggestionView` | `@copilotkit/react` | Renamed | -| `RenderSuggestion` | `@copilotkit/react-ui` | `CopilotChatSuggestionPill` | `@copilotkit/react` | Renamed | +| `RenderSuggestionsList` | `@copilotkit/react-ui` | `CopilotChatSuggestionView` | `@copilotkit/react-core/v2` | Renamed | +| `RenderSuggestion` | `@copilotkit/react-ui` | `CopilotChatSuggestionPill` | `@copilotkit/react-core/v2` | Renamed | | `CoAgentStateRendersProvider` | `@copilotkit/react-core` | -- | -- | Removed (no v2 equivalent) | -| `ThreadsProvider` | `@copilotkit/react-core` | -- | -- | Removed (threads managed by runtime) | +| `ThreadsProvider` | `@copilotkit/react-core` | `useThreads` | `@copilotkit/react-core/v2` | Provider removed; use the `useThreads` hook for client-side thread management | > **Note:** `@copilotkit/react-core/v2` also exports a `CopilotKitProvider` component. Do not migrate to it -- it is a functionality subset of `CopilotKit` (from `/v2`), which is the compatibility bridge across v1 and v2. ## Runtime Classes -| v1 Class/Function | v1 Package | v2 Replacement | v2 Package | Status | -| ------------------------------ | --------------------- | ------------------------------------------ | --------------------- | ------------------------------------ | -| `CopilotRuntime` | `@copilotkit/runtime` | `CopilotRuntime` | `@copilotkit/runtime` | Same name, different constructor API | -| `OpenAIAdapter` | `@copilotkit/runtime` | `BuiltInAgent({ model: "openai:..." })` | `@copilotkit/agent` | **Removed** | -| `AnthropicAdapter` | `@copilotkit/runtime` | `BuiltInAgent({ model: "anthropic:..." })` | `@copilotkit/agent` | **Removed** | -| `GoogleGenerativeAIAdapter` | `@copilotkit/runtime` | `BuiltInAgent({ model: "google:..." })` | `@copilotkit/agent` | **Removed** | -| `LangChainAdapter` | `@copilotkit/runtime` | Custom `AbstractAgent` | -- | **Removed** | -| `GroqAdapter` | `@copilotkit/runtime` | `BuiltInAgent` with Groq model | `@copilotkit/agent` | **Removed** | -| `UnifyAdapter` | `@copilotkit/runtime` | Custom `AbstractAgent` | -- | **Removed** | -| `OpenAIAssistantAdapter` | `@copilotkit/runtime` | Custom `AbstractAgent` | -- | **Removed** | -| `BedrockAdapter` | `@copilotkit/runtime` | `BuiltInAgent({ model: "vertex:..." })` | `@copilotkit/agent` | **Removed** | -| `OllamaAdapter` (experimental) | `@copilotkit/runtime` | Custom `AbstractAgent` | -- | **Removed** | -| `EmptyAdapter` | `@copilotkit/runtime` | -- | -- | **Removed** | -| `RemoteChain` | `@copilotkit/runtime` | -- | -- | **Removed** | -| `LangGraphAgent` | `@copilotkit/runtime` | `LangGraphAgent` | `@ag-ui/langgraph` | Moved to AG-UI package | -| `LangGraphHttpAgent` | `@copilotkit/runtime` | `LangGraphAgent` | `@ag-ui/langgraph` | Moved + renamed | +| v1 Class/Function | v1 Package | v2 Replacement | v2 Package | Status | +| ------------------------------ | --------------------- | ------------------------------------------ | ------------------------ | ------------------------------------ | +| `CopilotRuntime` | `@copilotkit/runtime` | `CopilotRuntime` | `@copilotkit/runtime/v2` | Same name, different constructor API | +| `OpenAIAdapter` | `@copilotkit/runtime` | `BuiltInAgent({ model: "openai/..." })` | `@copilotkit/runtime/v2` | **Removed** | +| `AnthropicAdapter` | `@copilotkit/runtime` | `BuiltInAgent({ model: "anthropic/..." })` | `@copilotkit/runtime/v2` | **Removed** | +| `GoogleGenerativeAIAdapter` | `@copilotkit/runtime` | `BuiltInAgent({ model: "google/..." })` | `@copilotkit/runtime/v2` | **Removed** | +| `LangChainAdapter` | `@copilotkit/runtime` | Custom `AbstractAgent` | -- | **Removed** | +| `GroqAdapter` | `@copilotkit/runtime` | Custom `AbstractAgent` (Groq `LanguageModel`) | -- | **Removed** | +| `UnifyAdapter` | `@copilotkit/runtime` | Custom `AbstractAgent` | -- | **Removed** | +| `OpenAIAssistantAdapter` | `@copilotkit/runtime` | Custom `AbstractAgent` | -- | **Removed** | +| `BedrockAdapter` | `@copilotkit/runtime` | Custom `AbstractAgent` | -- | **Removed** | +| `OllamaAdapter` (experimental) | `@copilotkit/runtime` | Custom `AbstractAgent` | -- | **Removed** | +| `EmptyAdapter` | `@copilotkit/runtime` | -- | -- | **Removed** | +| `RemoteChain` | `@copilotkit/runtime` | -- | -- | **Removed** | +| `LangGraphAgent` | `@copilotkit/runtime` | `LangGraphAgent` | `@copilotkit/runtime/langgraph` | Moved to the `/langgraph` subpath | +| `LangGraphHttpAgent` | `@copilotkit/runtime` | `LangGraphHttpAgent` | `@copilotkit/runtime/langgraph` | Distinct class (not merged into `LangGraphAgent`); moved to the `/langgraph` subpath | ## Runtime Framework Integrations -| v1 Function | v1 Package | v2 Replacement | v2 Package | Status | -| ----------------------------------------- | --------------------- | ------------------------------ | --------------------- | ---------------------- | -| `copilotRuntimeNextJSAppRouterEndpoint` | `@copilotkit/runtime` | `createCopilotEndpoint` | `@copilotkit/runtime` | **Removed** (use Hono) | -| `copilotRuntimeNextJSPagesRouterEndpoint` | `@copilotkit/runtime` | `createCopilotEndpoint` | `@copilotkit/runtime` | **Removed** (use Hono) | -| `CopilotRuntimeNodeExpressEndpoint` | `@copilotkit/runtime` | `createCopilotEndpointExpress` | `@copilotkit/runtime` | Renamed | -| `CopilotRuntimeNestEndpoint` | `@copilotkit/runtime` | `createCopilotEndpoint` | `@copilotkit/runtime` | **Removed** (use Hono) | -| `CopilotRuntimeNodeHttpEndpoint` | `@copilotkit/runtime` | `createCopilotEndpoint` | `@copilotkit/runtime` | **Removed** (use Hono) | +| v1 Function | v1 Package | v2 Replacement | v2 Package | Status | +| ----------------------------------------- | --------------------- | ----------------------------- | -------------------------------- | ----------------------------------------------------------- | +| `copilotRuntimeNextJSAppRouterEndpoint` | `@copilotkit/runtime` | `createCopilotHonoHandler` | `@copilotkit/runtime/v2` | **Removed** (use Hono; `createCopilotEndpoint` is a deprecated alias) | +| `copilotRuntimeNextJSPagesRouterEndpoint` | `@copilotkit/runtime` | `createCopilotHonoHandler` | `@copilotkit/runtime/v2` | **Removed** (use Hono; `createCopilotEndpoint` is a deprecated alias) | +| `CopilotRuntimeNodeExpressEndpoint` | `@copilotkit/runtime` | `createCopilotExpressHandler` | `@copilotkit/runtime/v2/express` | Renamed | +| `CopilotRuntimeNestEndpoint` | `@copilotkit/runtime` | `createCopilotHonoHandler` | `@copilotkit/runtime/v2` | **Removed** (use Hono) | +| `CopilotRuntimeNodeHttpEndpoint` | `@copilotkit/runtime` | `createCopilotHonoHandler` | `@copilotkit/runtime/v2` | **Removed** (use Hono) | ## Types | v1 Type | v1 Package | v2 Replacement | v2 Package | Status | | ------------------------------------ | ------------------------ | -------------------------------- | ---------------------------- | -------------------------------------------------------------- | -| `CopilotKitProps` | `@copilotkit/react-core` | `CopilotKitProps` | `@copilotkit/react-core/v2` | Same name, new import path (extends `CopilotKitProviderProps`) | -| `CopilotContextParams` | `@copilotkit/react-core` | `CopilotKitContextValue` | `@copilotkit/react` | Renamed | -| `FrontendAction` | `@copilotkit/react-core` | `ReactFrontendTool` | `@copilotkit/react` | Renamed + restructured | -| `ActionRenderProps` | `@copilotkit/react-core` | `ReactToolCallRenderer` | `@copilotkit/react` | Renamed + restructured | +| `CopilotKitProps` | `@copilotkit/react-core` | `CopilotKitProps` | `@copilotkit/react-core/v2` | Same name, new import path (extends `Omit`) | +| `CopilotContextParams` | `@copilotkit/react-core` | `CopilotKitContextValue` | `@copilotkit/react-core/v2` | Renamed | +| `FrontendAction` | `@copilotkit/react-core` | `ReactFrontendTool` | `@copilotkit/react-core/v2` | Renamed + restructured | +| `ActionRenderProps` | `@copilotkit/react-core` | `ReactToolCallRenderer` | `@copilotkit/react-core/v2` | Renamed + restructured | | `DocumentPointer` | `@copilotkit/react-core` | -- | -- | **Removed** | | `SystemMessageFunction` | `@copilotkit/react-core` | -- | -- | **Removed** | | `CopilotChatSuggestionConfiguration` | `@copilotkit/react-core` | `Suggestion` | `@copilotkit/core` | Renamed | @@ -103,9 +103,9 @@ These were already deprecated within v1 itself: | Location | Deprecated API | Replacement | | ---------------------- | ------------------------------------------------------------ | ---------------------------------------------------- | -| `FrontendAction` | `disabled` | `available: "disabled"` | +| `FrontendAction` | `disabled` | `available: false` (boolean; defaults to `true`) | | `ActionRenderProps` | `respond()` | Use `respond` (same, just documented differently) | -| `CopilotKitProps` | `guardrails_c` | Removed in v2 | +| `CopilotKitProps` | `guardrails_c` | `@internal`/defunct in source but still populates the legacy CopilotCloud `restrictToTopic` config when a cloud key is set; no effect on the v2 AG-UI runtime | | `CopilotRuntime` | `onBeforeRequest` / `onAfterRequest` | `beforeRequestMiddleware` / `afterRequestMiddleware` | | `useCopilotChat` | `visibleMessages` | Use AG-UI message stream | | `useCopilotChat` | `appendMessage` | Use `sendMessage` or agent API | diff --git a/skills/copilotkit-upgrade/references/v1-to-v2-migration.md b/skills/copilotkit-upgrade/references/v1-to-v2-migration.md index cfc8ac3b7a8..f9bc83efcd6 100644 --- a/skills/copilotkit-upgrade/references/v1-to-v2-migration.md +++ b/skills/copilotkit-upgrade/references/v1-to-v2-migration.md @@ -4,37 +4,40 @@ ### Step 1: Replace Dependencies -Remove v1 packages and install v2 equivalents: +The `@copilotkit/*` package names are unchanged in v2 -- the v2 APIs ship from the **`/v2` subpath** of the same packages. There is **no** `@copilotkit/react` or `@copilotkit/agent` package; do not install them. You only need to remove the packages that no longer have a v2 surface and upgrade the rest to their latest versions: ```bash -# Remove v1 -npm uninstall @copilotkit/react-core @copilotkit/react-ui @copilotkit/react-textarea \ - @copilotkit/runtime @copilotkit/runtime-client-gql @copilotkit/shared @copilotkit/sdk-js +# Remove packages with no v2 surface (uninstall @copilotkit/react-textarea only +# AFTER you have migrated off CopilotTextarea -- the v1 package still exists and +# stays installable for backward compatibility) +npm uninstall @copilotkit/runtime-client-gql @copilotkit/sdk-js -# Install v2 -npm install @copilotkit/react @copilotkit/runtime @copilotkit/agent @copilotkit/shared +# Upgrade the packages you keep to their latest (v2) versions. +# `hono` is only needed for the Hono endpoint (createCopilotHonoHandler); `zod` is +# the peer dep used for tool parameter schemas. +npm install @copilotkit/react-core@latest @copilotkit/runtime@latest @copilotkit/shared@latest zod ``` **Package mapping:** -| v1 Package | v2 Package | Notes | -| -------------------------------- | --------------------- | ---------------------------------------------- | -| `@copilotkit/react-core` | `@copilotkit/react` | Hooks, provider, types merged into one package | -| `@copilotkit/react-ui` | `@copilotkit/react` | Chat components merged into same package | -| `@copilotkit/react-textarea` | -- | Removed entirely, no v2 equivalent | -| `@copilotkit/runtime` | `@copilotkit/runtime` | New agent-based architecture | -| `@copilotkit/runtime-client-gql` | `@ag-ui/client` | Re-exported by `@copilotkit/react` | -| `@copilotkit/shared` | `@copilotkit/shared` | Utility types and constants | -| `@copilotkit/sdk-js` | `@copilotkit/agent` | Agent definitions (BuiltInAgent, etc.) | +| v1 Package | v2 Package | Notes | +| -------------------------------- | --------------------------- | ----------------------------------------------------------------------------------------- | +| `@copilotkit/react-core` | `@copilotkit/react-core/v2` | Same package; v2 hooks, provider, types, and chat components live under the `/v2` subpath | +| `@copilotkit/react-ui` | `@copilotkit/react-core/v2` | Chat components moved into `react-core/v2`; `react-ui` contributes only styles in v2 | +| `@copilotkit/react-textarea` | -- | No v2 equivalent; the v1 `@copilotkit/react-textarea@1.x` package stays installable -- drop it only after migrating off `CopilotTextarea` | +| `@copilotkit/runtime` | `@copilotkit/runtime/v2` | Same package; v2 runtime/agents live under the `/v2` subpath | +| `@copilotkit/runtime-client-gql` | `@ag-ui/client` | Re-exported by `@copilotkit/react-core/v2` | +| `@copilotkit/shared` | `@copilotkit/shared` | Utility types and constants | +| `@copilotkit/sdk-js` | `@copilotkit/runtime/v2` | `BuiltInAgent` and agent definitions now ship from `runtime/v2` | ### Step 2: Update All Imports Find-and-replace import paths across your codebase: ``` -@copilotkit/react-core -> @copilotkit/react -@copilotkit/react-ui -> @copilotkit/react -@copilotkit/runtime -> @copilotkit/runtime +@copilotkit/react-core -> @copilotkit/react-core/v2 +@copilotkit/react-ui -> @copilotkit/react-core/v2 (plus import "@copilotkit/react-core/v2/styles.css") +@copilotkit/runtime -> @copilotkit/runtime/v2 (Express helpers: @copilotkit/runtime/v2/express) @copilotkit/shared -> @copilotkit/shared ``` @@ -75,7 +78,7 @@ function App() { **Key differences:** - Same component name; the import path changes from the package root (legacy v1) to the `/v2` subpath -- The props type keeps the name `CopilotKitProps` (also exported from `/v2`); it extends `CopilotKitProviderProps`, so every `CopilotKitProvider` prop also works on it +- The props type keeps the name `CopilotKitProps` (also exported from `/v2`); it extends `Omit` (with a narrowed `children` type), so every non-`children` `CopilotKitProvider` prop also works on it - v2 adds `credentials`, `selfManagedAgents`, `renderToolCalls`, `renderActivityMessages` props - v2 removes `agents` prop (use `selfManagedAgents` or `agents__unsafe_dev_only` for local dev) @@ -118,7 +121,7 @@ useCopilotAction({ **v2:** ```tsx -import { useFrontendTool } from "@copilotkit/react"; +import { useFrontendTool } from "@copilotkit/react-core/v2"; import { z } from "zod"; useFrontendTool({ @@ -144,8 +147,8 @@ useFrontendTool({ - Hook renamed from `useCopilotAction` to `useFrontendTool` - Parameters use Zod schemas instead of the v1 parameter descriptor array (`{ name, type, required }`) - The `handler` receives the full args object directly (not destructured from `{ arg1, arg2 }`) -- The `render` prop works similarly but uses `ToolCallStatus` enum (`"inProgress"`, `"executing"`, `"complete"`) -- v2 adds `available` prop (`"enabled"` | `"disabled"`) replacing the v1 `disabled` boolean +- The `render` prop works similarly but `status` is now a `ToolCallStatus` enum member (`ToolCallStatus.InProgress` / `.Executing` / `.Complete`, whose values are `"inProgress"` / `"executing"` / `"complete"`) +- v2 replaces the v1 `disabled` boolean with `available` -- also a boolean (defaults to `true`; set `false` to hide the tool) - v2 adds `agentId` prop to scope a tool to a specific agent ### useCopilotReadable -> useAgentContext @@ -168,7 +171,7 @@ function EmployeeList({ employees }) { **v2:** ```tsx -import { useAgentContext } from "@copilotkit/react"; +import { useAgentContext } from "@copilotkit/react-core/v2"; function EmployeeList({ employees }) { useAgentContext({ @@ -199,7 +202,7 @@ useMakeCopilotDocumentReadable(documentPointer, ["category1"]); **v2:** ```tsx -import { useAgentContext } from "@copilotkit/react"; +import { useAgentContext } from "@copilotkit/react-core/v2"; useAgentContext({ description: "Document content for category1", @@ -211,6 +214,7 @@ useAgentContext({ - No direct `DocumentPointer` equivalent in v2; pass document content directly via `useAgentContext` - Categories are not supported; use the `description` field to provide context +- v1 returned a document `id` (used for `parentId` chaining); `useAgentContext` returns nothing, and hierarchical context via `parentId` is gone -- flatten instead ### useCoAgent -> useAgent @@ -231,7 +235,7 @@ const { name, state, setState, running, start, stop, run } = **v2:** ```tsx -import { useAgent } from "@copilotkit/react"; +import { useAgent } from "@copilotkit/react-core/v2"; const agent = useAgent({ agentId: "my-agent" }); @@ -247,7 +251,7 @@ const agent = useAgent({ agentId: "my-agent" }); - v2 returns an `AbstractAgent` instance instead of a destructured state object - The `run` / `start` / `stop` API surface differs -- v2 uses AG-UI protocol methods -### useCoAgentStateRender -> useRenderToolCall / useRenderActivityMessage +### useCoAgentStateRender -> useRenderTool / useRenderActivityMessage **v1:** @@ -266,27 +270,24 @@ useCoAgentStateRender({ **v2:** ```tsx -import { useRenderToolCall } from "@copilotkit/react"; +import { useRenderTool } from "@copilotkit/react-core/v2"; -// For tool call rendering: -useRenderToolCall({ - toolCall, - toolMessage, -}); - -// Or for activity messages: -import { useRenderActivityMessage } from "@copilotkit/react"; - -useRenderActivityMessage({ - // renders activity/progress messages from the agent +// Register a render-only renderer for a tool BY NAME (the declarative successor): +useRenderTool({ + name: "basic_agent", + render: ({ name, args, status, result }) => ( + + ), }); ``` **Key differences:** -- `useCoAgentStateRender` is split into `useRenderToolCall` (for tool execution UI) and `useRenderActivityMessage` (for progress/activity rendering) -- v2 does not have the `nodeName` filtering -- tool rendering is keyed by tool call ID -- v2 uses AG-UI `ToolCall` and `ToolMessage` types instead of the v1 agent state shape +- The idiomatic successor is `useRenderTool({ name, render })` -- a declarative, render-only registration keyed by tool **name** (no handler). If you also need execution behavior, use `useFrontendTool`, which accepts both a `handler` and a `render`. +- For agent progress/activity (not tool calls), use `useRenderActivityMessage()` -- a zero-arg hook returning `{ renderActivityMessage, findRenderer }`. +- `useRenderToolCall()` is the lower-level imperative hook (zero-arg, returns a `renderToolCall({ toolCall, toolMessage })` function) used internally by the chat views; prefer `useRenderTool` for migration. +- v2 has no `nodeName` filtering; renderers match by tool name (with an `agentId` tie-break and a `"*"` wildcard fallback). +- v2 uses AG-UI `ToolCall` / `ToolMessage` types instead of the v1 agent state shape. ### useLangGraphInterrupt -> useInterrupt @@ -312,7 +313,7 @@ useLangGraphInterrupt({ **v2:** ```tsx -import { useInterrupt } from "@copilotkit/react"; +import { useInterrupt } from "@copilotkit/react-core/v2"; const interruptElement = useInterrupt({ renderInChat: false, // false = you render it yourself; true = renders in CopilotChat @@ -343,7 +344,7 @@ return
{interruptElement}
; - v2 adds optional `handler` for programmatic interrupt handling before rendering - v2 returns a `React.ReactElement | null` when `renderInChat: false` -### useCopilotChat -> useAgent + useSuggestions +### useCopilotChat -> useAgent **v1:** @@ -362,7 +363,7 @@ await appendMessage( **v2:** ```tsx -import { useAgent } from "@copilotkit/react"; +import { useAgent } from "@copilotkit/react-core/v2"; const agent = useAgent({ agentId: "my-agent" }); @@ -392,7 +393,10 @@ useCopilotChatSuggestions({ **v2:** ```tsx -import { useConfigureSuggestions, useSuggestions } from "@copilotkit/react"; +import { + useConfigureSuggestions, + useSuggestions, +} from "@copilotkit/react-core/v2"; // Configure suggestion generation: useConfigureSuggestions({ @@ -427,7 +431,7 @@ useCopilotAdditionalInstructions({ **v2:** ```tsx -import { useAgentContext } from "@copilotkit/react"; +import { useAgentContext } from "@copilotkit/react-core/v2"; useAgentContext({ description: "Additional instructions for the agent", @@ -446,7 +450,7 @@ import { useHumanInTheLoop } from "@copilotkit/react-core"; **v2:** ```tsx -import { useHumanInTheLoop } from "@copilotkit/react"; +import { useHumanInTheLoop } from "@copilotkit/react-core/v2"; ``` The API is similar -- registers a tool that pauses for user input via a render function with a `respond` callback. @@ -463,7 +467,7 @@ import { OpenAIAdapter, GoogleGenerativeAIAdapter, } from "@copilotkit/runtime"; -import { copilotKitEndpoint } from "@copilotkit/runtime"; // Next.js App Router +import { copilotRuntimeNextJSAppRouterEndpoint } from "@copilotkit/runtime"; // Next.js App Router const serviceAdapter = new OpenAIAdapter({ model: "gpt-4o" }); const runtime = new CopilotRuntime({ @@ -481,41 +485,54 @@ const runtime = new CopilotRuntime({ }); // Next.js App Router -export const POST = copilotKitEndpoint(runtime, serviceAdapter); +export const POST = copilotRuntimeNextJSAppRouterEndpoint({ + runtime, + serviceAdapter, + endpoint: "/api/copilotkit", +}); ``` ### v2: AG-UI Agent Pattern ```ts -import { CopilotRuntime, createCopilotEndpoint } from "@copilotkit/runtime"; -import { BuiltInAgent } from "@copilotkit/agent"; -import { LangGraphAgent } from "@ag-ui/langgraph"; +import { + CopilotRuntime, + BuiltInAgent, + createCopilotHonoHandler, +} from "@copilotkit/runtime/v2"; +import { LangGraphAgent } from "@copilotkit/runtime/langgraph"; const runtime = new CopilotRuntime({ agents: { default: new BuiltInAgent({ - model: "openai:gpt-4o", + model: "openai/gpt-4o", // Frontend tools are registered client-side via useFrontendTool }), myLangGraphAgent: new LangGraphAgent({ - url: "http://localhost:8000", + deploymentUrl: "http://localhost:8000", graphId: "my-graph", }), }, // Optional middleware a2ui: {}, // A2UI middleware config mcpApps: { - // MCP Apps middleware - servers: [{ transport: { type: "sse", url: "http://localhost:3001/sse" } }], + // MCP Apps middleware -- each server config is flat (type + url at the top level) + servers: [{ type: "sse", url: "http://localhost:3001/sse" }], }, }); -// Hono-based endpoint (works with Next.js, Express, standalone) -const app = createCopilotEndpoint({ +// Hono-based endpoint. `createCopilotEndpoint` is a deprecated alias for +// `createCopilotHonoHandler`. For Express, use `createCopilotExpressHandler` +// from "@copilotkit/runtime/v2/express". +const app = createCopilotHonoHandler({ runtime, basePath: "/api/copilotkit", }); +// Standalone Hono: `export default app`. +// Next.js App Router (app/api/copilotkit/route.ts): export the fetch handler instead: +// export const POST = app.fetch; +// export const GET = app.fetch; export default app; ``` @@ -524,7 +541,7 @@ export default app; - No more service adapters (`OpenAIAdapter`, `LangChainAdapter`, etc.) -- model selection is done inside agents - No more `actions` array on the runtime -- frontend tools are registered via `useFrontendTool`, backend tools via agent configuration - No more `remoteEndpoints` -- agents are passed directly as `AbstractAgent` instances -- Endpoint setup uses `createCopilotEndpoint` (Hono) or `createCopilotEndpointExpress` (Express) instead of framework-specific integrations +- Endpoint setup uses `createCopilotHonoHandler` (Hono, from `@copilotkit/runtime/v2`; also exported as the deprecated alias `createCopilotEndpoint`) or `createCopilotExpressHandler` (Express, from `@copilotkit/runtime/v2/express`) instead of framework-specific integrations - v2 runtime supports SSE mode and Intelligence mode (durable threads with realtime events) ### v2 Runtime Modes @@ -536,7 +553,7 @@ const runtime = new CopilotRuntime({ }); // Intelligence Mode (durable threads, realtime, requires CopilotKitIntelligence) -import { CopilotKitIntelligence } from "@copilotkit/runtime"; +import { CopilotKitIntelligence } from "@copilotkit/runtime/v2"; const runtime = new CopilotRuntime({ agents: { default: myAgent }, @@ -552,7 +569,7 @@ const runtime = new CopilotRuntime({ ## Chat Component Migration -Chat components have the same names but move to `@copilotkit/react`: +Chat components have the same names but move to `@copilotkit/react-core/v2`: **v1:** @@ -567,7 +584,11 @@ import { **v2:** ```tsx -import { CopilotChat, CopilotPopup, CopilotSidebar } from "@copilotkit/react"; +import { + CopilotChat, + CopilotPopup, + CopilotSidebar, +} from "@copilotkit/react-core/v2"; ``` v2 adds new chat sub-components for granular customization: @@ -602,14 +623,16 @@ v1 used GraphQL-based message types from `@copilotkit/runtime-client-gql`: import { TextMessage, MessageRole } from "@copilotkit/runtime-client-gql"; ``` -v2 uses AG-UI protocol types from `@ag-ui/client` (re-exported by `@copilotkit/react`): +v2 uses AG-UI protocol types from `@ag-ui/client` (re-exported by `@copilotkit/react-core/v2`): ```tsx import { - Message, - TextMessage, + Message, // the message union + AssistantMessage, // per-role message types (UserMessage, SystemMessage, ToolMessage, ...) ToolCall, ToolMessage, EventType, -} from "@copilotkit/react"; // re-exports from @ag-ui/client +} from "@copilotkit/react-core/v2"; // re-exports from @ag-ui/client ``` + +> v2 has no standalone `TextMessage` type (that was the v1 GraphQL name). Use the `Message` union or the per-role types (`AssistantMessage` / `UserMessage` / ...); streamed text arrives via events such as `TextMessageChunkEvent`. diff --git a/skills/copilotkit-upgrade/sources.md b/skills/copilotkit-upgrade/sources.md index 276a2d9bb69..43440d8de55 100644 --- a/skills/copilotkit-upgrade/sources.md +++ b/skills/copilotkit-upgrade/sources.md @@ -1,41 +1,51 @@ # Sources Files and directories read from CopilotKit/CopilotKit to generate this skill's references. -Generated: 2026-03-28 +Generated: 2026-06-18 (regenerated against live `main`) + +> **Package layout note:** the repo is a flat monorepo -- every package lives directly +> under `packages/` (there is no `packages/v1/` or `packages/v2/` directory). +> v2 ships from the **`/v2` subpath of the same packages**: v2 React is +> `@copilotkit/react-core/v2` (source: `packages/react-core/src/v2/`) and v2 runtime is +> `@copilotkit/runtime/v2` (source: `packages/runtime/src/v2/`). There is no +> `@copilotkit/react` or `@copilotkit/agent` package -- `BuiltInAgent` and the v2 agent +> definitions are re-exported from `@copilotkit/runtime/v2` +> (source: `packages/runtime/src/agent/`). ## v1-to-v2-migration.md -- packages/v1/react-core/src/index.ts (v1 hook exports: useCopilotAction, useCopilotReadable, useCoAgent, useLangGraphInterrupt, useCopilotChat, useCopilotChatSuggestions, useCopilotAdditionalInstructions, useMakeCopilotDocumentReadable, CopilotKit provider) -- packages/v1/react-ui/src/index.ts (v1 component exports: CopilotChat, CopilotPopup, CopilotSidebar) -- packages/v1/react-textarea/src/index.ts (CopilotTextarea export, confirmed removed in v2) -- packages/v1/runtime/src/index.ts (v1 runtime exports: CopilotRuntime, OpenAIAdapter, AnthropicAdapter, GoogleGenerativeAIAdapter, LangChainAdapter, copilotRuntimeNextJSAppRouterEndpoint, copilotKitEndpoint) -- packages/v1/runtime-client-gql/src/index.ts (v1 GraphQL types: TextMessage, MessageRole, ActionExecutionMessage, ResultMessage) -- packages/v2/react/src/index.ts (v2 hook exports: useFrontendTool, useAgentContext, useAgent, useInterrupt, useSuggestions, useConfigureSuggestions, useRenderToolCall, useRenderActivityMessage, CopilotKit compat provider -- the recommended migration target; CopilotKitProvider is also exported but is a functionality subset) -- packages/v2/runtime/src/index.ts (v2 runtime exports: CopilotRuntime, createCopilotEndpoint, createCopilotEndpointExpress, CopilotKitIntelligence) -- packages/v2/agent/src/index.ts (v2 agent exports: BuiltInAgent, defineTool) -- packages/v2/core/src/ (CopilotKitCore, AG-UI event types, AbstractAgent interface) +- packages/react-core/src/index.tsx (v1 hook exports: useCopilotAction, useCopilotReadable, useCoAgent, useLangGraphInterrupt, useCopilotChat, useCopilotChatSuggestions, useCopilotAdditionalInstructions, useMakeCopilotDocumentReadable, CopilotKit provider) +- packages/react-ui/src/components/chat/index.tsx (v1 component exports: CopilotChat, CopilotPopup, CopilotSidebar; CSS-only in v2) +- packages/react-textarea/src/index.tsx (CopilotTextarea export, confirmed removed in v2) +- packages/runtime/src/index.ts (v1 runtime exports: CopilotRuntime, OpenAIAdapter, AnthropicAdapter, GoogleGenerativeAIAdapter, LangChainAdapter, copilotRuntimeNextJSAppRouterEndpoint, copilotKitEndpoint) +- packages/runtime-client-gql/src/index.ts (v1 GraphQL types: TextMessage, MessageRole, ActionExecutionMessage, ResultMessage) +- packages/react-core/src/v2/index.ts (v2 React exports under `@copilotkit/react-core/v2`: useFrontendTool, useAgentContext, useAgent, useInterrupt, useSuggestions, useConfigureSuggestions, useRenderToolCall, useRenderActivityMessage, useHumanInTheLoop, CopilotKit compat provider -- the recommended migration target; CopilotKitProvider is also exported but is a functionality subset; chat components CopilotChat/CopilotPopup/CopilotSidebar; re-exports `@copilotkit/core` and `@ag-ui/client`) +- packages/runtime/src/v2/index.ts (v2 runtime exports under `@copilotkit/runtime/v2`: CopilotRuntime, createCopilotHonoHandler [deprecated alias createCopilotEndpoint], CopilotKitIntelligence, InMemoryAgentRunner) +- packages/runtime/src/v2/runtime/endpoints/express.ts (Express endpoint helper `createCopilotExpressHandler`, exported from `@copilotkit/runtime/v2/express`) +- packages/runtime/src/agent/index.ts (v2 agent exports re-exported from `@copilotkit/runtime/v2`: BuiltInAgent, defineTool) +- packages/core/src/ (CopilotKitCore, AG-UI event types, AbstractAgent interface; re-exported by `@copilotkit/react-core/v2`) ## breaking-changes.md -- packages/v1/react-core/src/ (v1 provider props: CopilotKitProps, parameter descriptor format, FrontendAction type, ActionRenderProps) -- packages/v1/runtime/src/ (v1 service adapters, CopilotRuntime constructor with actions/remoteEndpoints, framework integration functions) -- packages/v1/shared/src/ (v1 Parameter type definition) -- packages/v2/react/src/ (v2 provider props: CopilotKitProviderProps, Zod parameter schemas, useFrontendTool available prop) -- packages/v2/runtime/src/ (v2 CopilotRuntime constructor with agents/middleware, createCopilotEndpoint Hono-based) -- packages/v2/core/src/ (AG-UI event types, message types replacing GraphQL types) -- packages/v2/agent/src/ (BuiltInAgent replacing all service adapters) +- packages/react-core/src/ (v1 provider props: CopilotKitProps, parameter descriptor format, FrontendAction type, ActionRenderProps) +- packages/runtime/src/ (v1 service adapters, CopilotRuntime constructor with actions/remoteEndpoints, framework integration functions) +- packages/shared/src/ (v1 Parameter type definition) +- packages/react-core/src/v2/ (v2 provider props: CopilotKitProviderProps, Zod parameter schemas, useFrontendTool available prop) +- packages/runtime/src/v2/ (v2 CopilotRuntime constructor with agents/middleware, createCopilotHonoHandler Hono-based) +- packages/core/src/ (AG-UI event types, message types replacing GraphQL types) +- packages/runtime/src/agent/ (BuiltInAgent replacing all service adapters) ## deprecation-map.md -- packages/v1/react-core/src/index.ts (all v1 hook and component exports) -- packages/v1/react-ui/src/index.ts (all v1 UI component exports) -- packages/v1/react-textarea/src/index.ts (CopilotTextarea export) -- packages/v1/runtime/src/index.ts (all v1 runtime class and function exports) -- packages/v1/runtime-client-gql/src/index.ts (v1 GraphQL client exports) -- packages/v1/shared/src/index.ts (v1 shared type exports) -- packages/v1/sdk-js/src/index.ts (v1 SDK exports) -- packages/v2/react/src/index.ts (all v2 hook and component exports) -- packages/v2/runtime/src/index.ts (all v2 runtime exports) -- packages/v2/agent/src/index.ts (v2 agent exports) -- packages/v2/core/src/index.ts (v2 core exports) -- packages/v2/shared/src/index.ts (v2 shared type exports) +- packages/react-core/src/index.tsx (all v1 hook and component exports) +- packages/react-ui/src/components/chat/index.tsx (all v1 UI component exports) +- packages/react-textarea/src/index.tsx (CopilotTextarea export) +- packages/runtime/src/index.ts (all v1 runtime class and function exports) +- packages/runtime-client-gql/src/index.ts (v1 GraphQL client exports) +- packages/shared/src/index.ts (v1 shared type exports) +- packages/sdk-js/src/index.ts (v1 SDK exports) +- packages/react-core/src/v2/index.ts (all v2 React hook and component exports) +- packages/runtime/src/v2/index.ts (all v2 runtime exports) +- packages/runtime/src/agent/index.ts (v2 agent exports) +- packages/core/src/index.ts (v2 core exports) +- packages/shared/src/index.ts (v2 shared type exports) From fd4c99156701f94afc716c209faa3ede69bfd588 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 18:20:16 +0000 Subject: [PATCH 02/12] style: auto-fix formatting --- skills/copilotkit-upgrade/SKILL.md | 26 +-- .../references/breaking-changes.md | 36 ++-- .../references/deprecation-map.md | 166 +++++++++--------- .../references/v1-to-v2-migration.md | 16 +- 4 files changed, 122 insertions(+), 122 deletions(-) diff --git a/skills/copilotkit-upgrade/SKILL.md b/skills/copilotkit-upgrade/SKILL.md index 8d1a6f5719d..a74dc357ff8 100644 --- a/skills/copilotkit-upgrade/SKILL.md +++ b/skills/copilotkit-upgrade/SKILL.md @@ -37,20 +37,20 @@ Scan the codebase for all v1 imports and API usage: Key hooks and components to find and replace: -| v1 API | v2 Replacement | -| ---------------------------------- | ---------------------------------------------------- | -| `useCopilotAction` | `useFrontendTool` | -| `useCopilotReadable` | `useAgentContext` | -| `useCopilotChat` | `useAgent` | -| `useCoAgent` | `useAgent` | -| `useCoAgentStateRender` | `useRenderTool` / `useRenderActivityMessage` | +| v1 API | v2 Replacement | +| ---------------------------------- | ---------------------------------------------------------- | +| `useCopilotAction` | `useFrontendTool` | +| `useCopilotReadable` | `useAgentContext` | +| `useCopilotChat` | `useAgent` | +| `useCoAgent` | `useAgent` | +| `useCoAgentStateRender` | `useRenderTool` / `useRenderActivityMessage` | | `useCopilotContext` | `useCopilotKit` (from `@copilotkit/react-core/v2/context`) | -| `useLangGraphInterrupt` | `useInterrupt` | -| `useCopilotChatSuggestions` | `useConfigureSuggestions` + `useSuggestions` | -| `useCopilotAdditionalInstructions` | `useAgentContext` | -| `useMakeCopilotDocumentReadable` | `useAgentContext` | -| `CopilotKit` (root import) | `CopilotKit` (from `@copilotkit/react-core/v2`) | -| `CopilotTextarea` | Removed -- use standard textarea + `useFrontendTool` | +| `useLangGraphInterrupt` | `useInterrupt` | +| `useCopilotChatSuggestions` | `useConfigureSuggestions` + `useSuggestions` | +| `useCopilotAdditionalInstructions` | `useAgentContext` | +| `useMakeCopilotDocumentReadable` | `useAgentContext` | +| `CopilotKit` (root import) | `CopilotKit` (from `@copilotkit/react-core/v2`) | +| `CopilotTextarea` | Removed -- use standard textarea + `useFrontendTool` | ### 3. Map to v2 Equivalents diff --git a/skills/copilotkit-upgrade/references/breaking-changes.md b/skills/copilotkit-upgrade/references/breaking-changes.md index e069e9473a0..6d5abb33a0a 100644 --- a/skills/copilotkit-upgrade/references/breaking-changes.md +++ b/skills/copilotkit-upgrade/references/breaking-changes.md @@ -20,11 +20,11 @@ The v2 API is exposed through the same `@copilotkit/*` packages -- no package na ### Removed packages -| Package | Status | -| -------------------------------- | -------------------------------------------------------------------------------- | +| Package | Status | +| -------------------------------- | --------------------------------------------------------------------------------------------------------- | | `@copilotkit/react-textarea` | No v2 equivalent. The v1 package stays installable; remove it only after migrating off `CopilotTextarea`. | -| `@copilotkit/runtime-client-gql` | Replaced by `@ag-ui/client` (re-exported from `@copilotkit/react-core/v2`) | -| `@copilotkit/sdk-js` | Removed. `BuiltInAgent` and agent definitions ship from `@copilotkit/runtime/v2` | +| `@copilotkit/runtime-client-gql` | Replaced by `@ag-ui/client` (re-exported from `@copilotkit/react-core/v2`) | +| `@copilotkit/sdk-js` | Removed. `BuiltInAgent` and agent definitions ship from `@copilotkit/runtime/v2` | --- @@ -49,20 +49,20 @@ The provider keeps the name `CopilotKit`; the import path changes from the packa ### Props changes -| v1 Prop | v2 Status | Notes | -| -------------- | ------------------------------- | -------------------------------------------------------------- | -| `runtimeUrl` | Kept | Same behavior | -| `headers` | Kept | Same behavior | -| `publicApiKey` | Kept (deprecated) | `publicLicenseKey` is the canonical name | -| `properties` | Kept | Same behavior | -| `agents` | Removed | Use `selfManagedAgents` or `agents__unsafe_dev_only` | +| v1 Prop | v2 Status | Notes | +| -------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `runtimeUrl` | Kept | Same behavior | +| `headers` | Kept | Same behavior | +| `publicApiKey` | Kept (deprecated) | `publicLicenseKey` is the canonical name | +| `properties` | Kept | Same behavior | +| `agents` | Removed | Use `selfManagedAgents` or `agents__unsafe_dev_only` | | `guardrails_c` | Kept (CopilotCloud only) | Marked `@internal`/defunct in source, but still wired into the legacy CopilotCloud `restrictToTopic` config when a cloud key (`publicApiKey`) is set; has no effect on the v2 AG-UI runtime path | -| `children` | Kept | Same behavior | -| -- | Added: `credentials` | `RequestCredentials` for fetch (e.g., `"include"` for cookies) | -| -- | Added: `selfManagedAgents` | `Record` for client-side agents | -| -- | Added: `renderToolCalls` | `ReactToolCallRenderer[]` for provider-level tool renderers | -| -- | Added: `renderActivityMessages` | `ReactActivityMessageRenderer[]` for activity renderers | -| -- | Added: `useSingleEndpoint` | Boolean to use single-route endpoint mode | +| `children` | Kept | Same behavior | +| -- | Added: `credentials` | `RequestCredentials` for fetch (e.g., `"include"` for cookies) | +| -- | Added: `selfManagedAgents` | `Record` for client-side agents | +| -- | Added: `renderToolCalls` | `ReactToolCallRenderer[]` for provider-level tool renderers | +| -- | Added: `renderActivityMessages` | `ReactActivityMessageRenderer[]` for activity renderers | +| -- | Added: `useSingleEndpoint` | Boolean to use single-route endpoint mode | ### Context hook rename @@ -201,7 +201,7 @@ All service adapters are removed from the runtime: | --------------------------- | ------------------------------------------------------------------- | | `OpenAIAdapter` | Use `BuiltInAgent({ model: "openai/gpt-4o" })` | | `AnthropicAdapter` | Use `BuiltInAgent({ model: "anthropic/claude-sonnet-4.5" })` | -| `GoogleGenerativeAIAdapter` | Use `BuiltInAgent({ model: "google/gemini-2.5-pro" })` | +| `GoogleGenerativeAIAdapter` | Use `BuiltInAgent({ model: "google/gemini-2.5-pro" })` | | `LangChainAdapter` | Use a custom `AbstractAgent` implementation | | `GroqAdapter` | Use a custom `AbstractAgent` (pass a Groq `LanguageModel` instance) | | `UnifyAdapter` | Use a custom `AbstractAgent` implementation | diff --git a/skills/copilotkit-upgrade/references/deprecation-map.md b/skills/copilotkit-upgrade/references/deprecation-map.md index c48096371c3..9d55dd66995 100644 --- a/skills/copilotkit-upgrade/references/deprecation-map.md +++ b/skills/copilotkit-upgrade/references/deprecation-map.md @@ -4,111 +4,111 @@ Complete mapping of every deprecated v1 API to its v2 replacement. ## Hooks -| v1 Hook | v1 Package | v2 Replacement | v2 Package | Status | -| ---------------------------------- | ------------------------ | ------------------------------------------------ | ----------------------------------- | ------------------------------------ | -| `useCopilotAction` | `@copilotkit/react-core` | `useFrontendTool` | `@copilotkit/react-core/v2` | Renamed + new parameter format (Zod) | -| `useCopilotReadable` | `@copilotkit/react-core` | `useAgentContext` | `@copilotkit/react-core/v2` | Renamed, `parentId` removed | -| `useCopilotChat` | `@copilotkit/react-core` | `useAgent` | `@copilotkit/react-core/v2` | Replaced (different API) | -| `useCoAgent` | `@copilotkit/react-core` | `useAgent` | `@copilotkit/react-core/v2` | Renamed, different return type | -| `useCoAgentStateRender` | `@copilotkit/react-core` | `useRenderTool` / `useRenderActivityMessage` | `@copilotkit/react-core/v2` | Split: render-by-name + activity rendering | -| `useLangGraphInterrupt` | `@copilotkit/react-core` | `useInterrupt` | `@copilotkit/react-core/v2` | Renamed + new API | -| `useCopilotChatSuggestions` | `@copilotkit/react-core` | `useConfigureSuggestions` + `useSuggestions` | `@copilotkit/react-core/v2` | Split into two hooks | -| `useCopilotAdditionalInstructions` | `@copilotkit/react-core` | `useAgentContext` | `@copilotkit/react-core/v2` | Use description/value context | -| `useMakeCopilotDocumentReadable` | `@copilotkit/react-core` | `useAgentContext` | `@copilotkit/react-core/v2` | Pass content directly | -| `useCopilotRuntimeClient` | `@copilotkit/react-core` | `useCopilotKit` | `@copilotkit/react-core/v2/context` | Access core via provider context | -| `useCopilotContext` | `@copilotkit/react-core` | `useCopilotKit` | `@copilotkit/react-core/v2/context` | Returns `{ copilotkit, executingToolCallIds }` | -| `useCopilotMessagesContext` | `@copilotkit/react-core` | -- | -- | Removed (use agent event stream) | -| `useCoAgentStateRenders` | `@copilotkit/react-core` | -- | -- | Removed (context no longer needed) | -| `useCopilotChatInternal` | `@copilotkit/react-core` | -- | -- | Internal, removed | -| `useCopilotChatHeadless_c` | `@copilotkit/react-core` | -- | -- | Internal, removed | -| `useCopilotAuthenticatedAction_c` | `@copilotkit/react-core` | -- | -- | Internal, removed | -| `useFrontendTool` | `@copilotkit/react-core` | `useFrontendTool` | `@copilotkit/react-core/v2` | Same name, import path changes | -| `useHumanInTheLoop` | `@copilotkit/react-core` | `useHumanInTheLoop` | `@copilotkit/react-core/v2` | Same name, import path changes | -| `useRenderToolCall` | `@copilotkit/react-core` | `useRenderToolCall` | `@copilotkit/react-core/v2` | Same name, import path changes | +| v1 Hook | v1 Package | v2 Replacement | v2 Package | Status | +| ---------------------------------- | ------------------------ | ------------------------------------------------------------- | ----------------------------------- | ----------------------------------------------------------------------------- | +| `useCopilotAction` | `@copilotkit/react-core` | `useFrontendTool` | `@copilotkit/react-core/v2` | Renamed + new parameter format (Zod) | +| `useCopilotReadable` | `@copilotkit/react-core` | `useAgentContext` | `@copilotkit/react-core/v2` | Renamed, `parentId` removed | +| `useCopilotChat` | `@copilotkit/react-core` | `useAgent` | `@copilotkit/react-core/v2` | Replaced (different API) | +| `useCoAgent` | `@copilotkit/react-core` | `useAgent` | `@copilotkit/react-core/v2` | Renamed, different return type | +| `useCoAgentStateRender` | `@copilotkit/react-core` | `useRenderTool` / `useRenderActivityMessage` | `@copilotkit/react-core/v2` | Split: render-by-name + activity rendering | +| `useLangGraphInterrupt` | `@copilotkit/react-core` | `useInterrupt` | `@copilotkit/react-core/v2` | Renamed + new API | +| `useCopilotChatSuggestions` | `@copilotkit/react-core` | `useConfigureSuggestions` + `useSuggestions` | `@copilotkit/react-core/v2` | Split into two hooks | +| `useCopilotAdditionalInstructions` | `@copilotkit/react-core` | `useAgentContext` | `@copilotkit/react-core/v2` | Use description/value context | +| `useMakeCopilotDocumentReadable` | `@copilotkit/react-core` | `useAgentContext` | `@copilotkit/react-core/v2` | Pass content directly | +| `useCopilotRuntimeClient` | `@copilotkit/react-core` | `useCopilotKit` | `@copilotkit/react-core/v2/context` | Access core via provider context | +| `useCopilotContext` | `@copilotkit/react-core` | `useCopilotKit` | `@copilotkit/react-core/v2/context` | Returns `{ copilotkit, executingToolCallIds }` | +| `useCopilotMessagesContext` | `@copilotkit/react-core` | -- | -- | Removed (use agent event stream) | +| `useCoAgentStateRenders` | `@copilotkit/react-core` | -- | -- | Removed (context no longer needed) | +| `useCopilotChatInternal` | `@copilotkit/react-core` | -- | -- | Internal, removed | +| `useCopilotChatHeadless_c` | `@copilotkit/react-core` | -- | -- | Internal, removed | +| `useCopilotAuthenticatedAction_c` | `@copilotkit/react-core` | -- | -- | Internal, removed | +| `useFrontendTool` | `@copilotkit/react-core` | `useFrontendTool` | `@copilotkit/react-core/v2` | Same name, import path changes | +| `useHumanInTheLoop` | `@copilotkit/react-core` | `useHumanInTheLoop` | `@copilotkit/react-core/v2` | Same name, import path changes | +| `useRenderToolCall` | `@copilotkit/react-core` | `useRenderToolCall` | `@copilotkit/react-core/v2` | Same name, import path changes | | `useDefaultTool` | `@copilotkit/react-core` | `useDefaultRenderTool` (render) / `useFrontendTool` (handler) | `@copilotkit/react-core/v2` | Split: v1's catch-all had a handler; v2 `useDefaultRenderTool` is render-only | -| `useLazyToolRenderer` | `@copilotkit/react-core` | -- | -- | Removed | -| `useChatContext` (react-ui) | `@copilotkit/react-ui` | `useCopilotChatConfiguration` | `@copilotkit/react-core/v2` | Renamed | +| `useLazyToolRenderer` | `@copilotkit/react-core` | -- | -- | Removed | +| `useChatContext` (react-ui) | `@copilotkit/react-ui` | `useCopilotChatConfiguration` | `@copilotkit/react-core/v2` | Renamed | ## Components -| v1 Component | v1 Package | v2 Replacement | v2 Package | Status | -| ----------------------------- | ---------------------------- | ----------------------------- | --------------------------- | ------------------------------------ | -| `CopilotKit` | `@copilotkit/react-core` | `CopilotKit` | `@copilotkit/react-core/v2` | Same name, new import path | -| `CopilotChat` | `@copilotkit/react-ui` | `CopilotChat` | `@copilotkit/react-core/v2` | Same name, new package | -| `CopilotPopup` | `@copilotkit/react-ui` | `CopilotPopup` | `@copilotkit/react-core/v2` | Same name, new package | -| `CopilotSidebar` | `@copilotkit/react-ui` | `CopilotSidebar` | `@copilotkit/react-core/v2` | Same name, new package | -| `CopilotTextarea` | `@copilotkit/react-textarea` | -- | -- | **Removed** | -| `CopilotDevConsole` | `@copilotkit/react-ui` | `CopilotKitInspector` | `@copilotkit/react-core/v2` | Renamed | -| `Markdown` | `@copilotkit/react-ui` | -- | -- | Removed -- v2 chat components render markdown internally | -| `AssistantMessage` | `@copilotkit/react-ui` | `CopilotChatAssistantMessage` | `@copilotkit/react-core/v2` | Renamed | -| `UserMessage` | `@copilotkit/react-ui` | `CopilotChatUserMessage` | `@copilotkit/react-core/v2` | Renamed | -| `ImageRenderer` | `@copilotkit/react-ui` | -- | -- | Removed | -| `RenderSuggestionsList` | `@copilotkit/react-ui` | `CopilotChatSuggestionView` | `@copilotkit/react-core/v2` | Renamed | -| `RenderSuggestion` | `@copilotkit/react-ui` | `CopilotChatSuggestionPill` | `@copilotkit/react-core/v2` | Renamed | -| `CoAgentStateRendersProvider` | `@copilotkit/react-core` | -- | -- | Removed (no v2 equivalent) | +| v1 Component | v1 Package | v2 Replacement | v2 Package | Status | +| ----------------------------- | ---------------------------- | ----------------------------- | --------------------------- | ----------------------------------------------------------------------------- | +| `CopilotKit` | `@copilotkit/react-core` | `CopilotKit` | `@copilotkit/react-core/v2` | Same name, new import path | +| `CopilotChat` | `@copilotkit/react-ui` | `CopilotChat` | `@copilotkit/react-core/v2` | Same name, new package | +| `CopilotPopup` | `@copilotkit/react-ui` | `CopilotPopup` | `@copilotkit/react-core/v2` | Same name, new package | +| `CopilotSidebar` | `@copilotkit/react-ui` | `CopilotSidebar` | `@copilotkit/react-core/v2` | Same name, new package | +| `CopilotTextarea` | `@copilotkit/react-textarea` | -- | -- | **Removed** | +| `CopilotDevConsole` | `@copilotkit/react-ui` | `CopilotKitInspector` | `@copilotkit/react-core/v2` | Renamed | +| `Markdown` | `@copilotkit/react-ui` | -- | -- | Removed -- v2 chat components render markdown internally | +| `AssistantMessage` | `@copilotkit/react-ui` | `CopilotChatAssistantMessage` | `@copilotkit/react-core/v2` | Renamed | +| `UserMessage` | `@copilotkit/react-ui` | `CopilotChatUserMessage` | `@copilotkit/react-core/v2` | Renamed | +| `ImageRenderer` | `@copilotkit/react-ui` | -- | -- | Removed | +| `RenderSuggestionsList` | `@copilotkit/react-ui` | `CopilotChatSuggestionView` | `@copilotkit/react-core/v2` | Renamed | +| `RenderSuggestion` | `@copilotkit/react-ui` | `CopilotChatSuggestionPill` | `@copilotkit/react-core/v2` | Renamed | +| `CoAgentStateRendersProvider` | `@copilotkit/react-core` | -- | -- | Removed (no v2 equivalent) | | `ThreadsProvider` | `@copilotkit/react-core` | `useThreads` | `@copilotkit/react-core/v2` | Provider removed; use the `useThreads` hook for client-side thread management | > **Note:** `@copilotkit/react-core/v2` also exports a `CopilotKitProvider` component. Do not migrate to it -- it is a functionality subset of `CopilotKit` (from `/v2`), which is the compatibility bridge across v1 and v2. ## Runtime Classes -| v1 Class/Function | v1 Package | v2 Replacement | v2 Package | Status | -| ------------------------------ | --------------------- | ------------------------------------------ | ------------------------ | ------------------------------------ | -| `CopilotRuntime` | `@copilotkit/runtime` | `CopilotRuntime` | `@copilotkit/runtime/v2` | Same name, different constructor API | -| `OpenAIAdapter` | `@copilotkit/runtime` | `BuiltInAgent({ model: "openai/..." })` | `@copilotkit/runtime/v2` | **Removed** | -| `AnthropicAdapter` | `@copilotkit/runtime` | `BuiltInAgent({ model: "anthropic/..." })` | `@copilotkit/runtime/v2` | **Removed** | -| `GoogleGenerativeAIAdapter` | `@copilotkit/runtime` | `BuiltInAgent({ model: "google/..." })` | `@copilotkit/runtime/v2` | **Removed** | -| `LangChainAdapter` | `@copilotkit/runtime` | Custom `AbstractAgent` | -- | **Removed** | -| `GroqAdapter` | `@copilotkit/runtime` | Custom `AbstractAgent` (Groq `LanguageModel`) | -- | **Removed** | -| `UnifyAdapter` | `@copilotkit/runtime` | Custom `AbstractAgent` | -- | **Removed** | -| `OpenAIAssistantAdapter` | `@copilotkit/runtime` | Custom `AbstractAgent` | -- | **Removed** | -| `BedrockAdapter` | `@copilotkit/runtime` | Custom `AbstractAgent` | -- | **Removed** | -| `OllamaAdapter` (experimental) | `@copilotkit/runtime` | Custom `AbstractAgent` | -- | **Removed** | -| `EmptyAdapter` | `@copilotkit/runtime` | -- | -- | **Removed** | -| `RemoteChain` | `@copilotkit/runtime` | -- | -- | **Removed** | -| `LangGraphAgent` | `@copilotkit/runtime` | `LangGraphAgent` | `@copilotkit/runtime/langgraph` | Moved to the `/langgraph` subpath | -| `LangGraphHttpAgent` | `@copilotkit/runtime` | `LangGraphHttpAgent` | `@copilotkit/runtime/langgraph` | Distinct class (not merged into `LangGraphAgent`); moved to the `/langgraph` subpath | +| v1 Class/Function | v1 Package | v2 Replacement | v2 Package | Status | +| ------------------------------ | --------------------- | --------------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------ | +| `CopilotRuntime` | `@copilotkit/runtime` | `CopilotRuntime` | `@copilotkit/runtime/v2` | Same name, different constructor API | +| `OpenAIAdapter` | `@copilotkit/runtime` | `BuiltInAgent({ model: "openai/..." })` | `@copilotkit/runtime/v2` | **Removed** | +| `AnthropicAdapter` | `@copilotkit/runtime` | `BuiltInAgent({ model: "anthropic/..." })` | `@copilotkit/runtime/v2` | **Removed** | +| `GoogleGenerativeAIAdapter` | `@copilotkit/runtime` | `BuiltInAgent({ model: "google/..." })` | `@copilotkit/runtime/v2` | **Removed** | +| `LangChainAdapter` | `@copilotkit/runtime` | Custom `AbstractAgent` | -- | **Removed** | +| `GroqAdapter` | `@copilotkit/runtime` | Custom `AbstractAgent` (Groq `LanguageModel`) | -- | **Removed** | +| `UnifyAdapter` | `@copilotkit/runtime` | Custom `AbstractAgent` | -- | **Removed** | +| `OpenAIAssistantAdapter` | `@copilotkit/runtime` | Custom `AbstractAgent` | -- | **Removed** | +| `BedrockAdapter` | `@copilotkit/runtime` | Custom `AbstractAgent` | -- | **Removed** | +| `OllamaAdapter` (experimental) | `@copilotkit/runtime` | Custom `AbstractAgent` | -- | **Removed** | +| `EmptyAdapter` | `@copilotkit/runtime` | -- | -- | **Removed** | +| `RemoteChain` | `@copilotkit/runtime` | -- | -- | **Removed** | +| `LangGraphAgent` | `@copilotkit/runtime` | `LangGraphAgent` | `@copilotkit/runtime/langgraph` | Moved to the `/langgraph` subpath | +| `LangGraphHttpAgent` | `@copilotkit/runtime` | `LangGraphHttpAgent` | `@copilotkit/runtime/langgraph` | Distinct class (not merged into `LangGraphAgent`); moved to the `/langgraph` subpath | ## Runtime Framework Integrations -| v1 Function | v1 Package | v2 Replacement | v2 Package | Status | -| ----------------------------------------- | --------------------- | ----------------------------- | -------------------------------- | ----------------------------------------------------------- | +| v1 Function | v1 Package | v2 Replacement | v2 Package | Status | +| ----------------------------------------- | --------------------- | ----------------------------- | -------------------------------- | --------------------------------------------------------------------- | | `copilotRuntimeNextJSAppRouterEndpoint` | `@copilotkit/runtime` | `createCopilotHonoHandler` | `@copilotkit/runtime/v2` | **Removed** (use Hono; `createCopilotEndpoint` is a deprecated alias) | | `copilotRuntimeNextJSPagesRouterEndpoint` | `@copilotkit/runtime` | `createCopilotHonoHandler` | `@copilotkit/runtime/v2` | **Removed** (use Hono; `createCopilotEndpoint` is a deprecated alias) | -| `CopilotRuntimeNodeExpressEndpoint` | `@copilotkit/runtime` | `createCopilotExpressHandler` | `@copilotkit/runtime/v2/express` | Renamed | -| `CopilotRuntimeNestEndpoint` | `@copilotkit/runtime` | `createCopilotHonoHandler` | `@copilotkit/runtime/v2` | **Removed** (use Hono) | -| `CopilotRuntimeNodeHttpEndpoint` | `@copilotkit/runtime` | `createCopilotHonoHandler` | `@copilotkit/runtime/v2` | **Removed** (use Hono) | +| `CopilotRuntimeNodeExpressEndpoint` | `@copilotkit/runtime` | `createCopilotExpressHandler` | `@copilotkit/runtime/v2/express` | Renamed | +| `CopilotRuntimeNestEndpoint` | `@copilotkit/runtime` | `createCopilotHonoHandler` | `@copilotkit/runtime/v2` | **Removed** (use Hono) | +| `CopilotRuntimeNodeHttpEndpoint` | `@copilotkit/runtime` | `createCopilotHonoHandler` | `@copilotkit/runtime/v2` | **Removed** (use Hono) | ## Types -| v1 Type | v1 Package | v2 Replacement | v2 Package | Status | -| ------------------------------------ | ------------------------ | -------------------------------- | ---------------------------- | -------------------------------------------------------------- | +| v1 Type | v1 Package | v2 Replacement | v2 Package | Status | +| ------------------------------------ | ------------------------ | -------------------------------- | ---------------------------- | -------------------------------------------------------------------------------- | | `CopilotKitProps` | `@copilotkit/react-core` | `CopilotKitProps` | `@copilotkit/react-core/v2` | Same name, new import path (extends `Omit`) | -| `CopilotContextParams` | `@copilotkit/react-core` | `CopilotKitContextValue` | `@copilotkit/react-core/v2` | Renamed | -| `FrontendAction` | `@copilotkit/react-core` | `ReactFrontendTool` | `@copilotkit/react-core/v2` | Renamed + restructured | -| `ActionRenderProps` | `@copilotkit/react-core` | `ReactToolCallRenderer` | `@copilotkit/react-core/v2` | Renamed + restructured | -| `DocumentPointer` | `@copilotkit/react-core` | -- | -- | **Removed** | -| `SystemMessageFunction` | `@copilotkit/react-core` | -- | -- | **Removed** | -| `CopilotChatSuggestionConfiguration` | `@copilotkit/react-core` | `Suggestion` | `@copilotkit/core` | Renamed | -| `Parameter` | `@copilotkit/shared` | Zod schemas / `StandardSchemaV1` | `zod` / `@copilotkit/shared` | Replaced with schema-based | -| `CopilotServiceAdapter` | `@copilotkit/runtime` | `AbstractAgent` | `@ag-ui/client` | Replaced | -| `TextMessageEvents` | `@copilotkit/runtime` | -- | -- | **Removed** (@deprecated) | -| `ToolCallEvents` | `@copilotkit/runtime` | -- | -- | **Removed** (@deprecated) | -| `CustomEventNames` | `@copilotkit/runtime` | -- | -- | **Removed** (@deprecated) | -| `PredictStateTool` | `@copilotkit/runtime` | -- | -- | **Removed** (@deprecated) | +| `CopilotContextParams` | `@copilotkit/react-core` | `CopilotKitContextValue` | `@copilotkit/react-core/v2` | Renamed | +| `FrontendAction` | `@copilotkit/react-core` | `ReactFrontendTool` | `@copilotkit/react-core/v2` | Renamed + restructured | +| `ActionRenderProps` | `@copilotkit/react-core` | `ReactToolCallRenderer` | `@copilotkit/react-core/v2` | Renamed + restructured | +| `DocumentPointer` | `@copilotkit/react-core` | -- | -- | **Removed** | +| `SystemMessageFunction` | `@copilotkit/react-core` | -- | -- | **Removed** | +| `CopilotChatSuggestionConfiguration` | `@copilotkit/react-core` | `Suggestion` | `@copilotkit/core` | Renamed | +| `Parameter` | `@copilotkit/shared` | Zod schemas / `StandardSchemaV1` | `zod` / `@copilotkit/shared` | Replaced with schema-based | +| `CopilotServiceAdapter` | `@copilotkit/runtime` | `AbstractAgent` | `@ag-ui/client` | Replaced | +| `TextMessageEvents` | `@copilotkit/runtime` | -- | -- | **Removed** (@deprecated) | +| `ToolCallEvents` | `@copilotkit/runtime` | -- | -- | **Removed** (@deprecated) | +| `CustomEventNames` | `@copilotkit/runtime` | -- | -- | **Removed** (@deprecated) | +| `PredictStateTool` | `@copilotkit/runtime` | -- | -- | **Removed** (@deprecated) | ## v1 Props Marked @deprecated Within v1 These were already deprecated within v1 itself: -| Location | Deprecated API | Replacement | -| ---------------------- | ------------------------------------------------------------ | ---------------------------------------------------- | -| `FrontendAction` | `disabled` | `available: false` (boolean; defaults to `true`) | -| `ActionRenderProps` | `respond()` | Use `respond` (same, just documented differently) | +| Location | Deprecated API | Replacement | +| ---------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `FrontendAction` | `disabled` | `available: false` (boolean; defaults to `true`) | +| `ActionRenderProps` | `respond()` | Use `respond` (same, just documented differently) | | `CopilotKitProps` | `guardrails_c` | `@internal`/defunct in source but still populates the legacy CopilotCloud `restrictToTopic` config when a cloud key is set; no effect on the v2 AG-UI runtime | -| `CopilotRuntime` | `onBeforeRequest` / `onAfterRequest` | `beforeRequestMiddleware` / `afterRequestMiddleware` | -| `useCopilotChat` | `visibleMessages` | Use AG-UI message stream | -| `useCopilotChat` | `appendMessage` | Use `sendMessage` or agent API | -| Chat component props | `AssistantMessage` / `UserMessage` / `Messages` render props | `RenderMessage` | -| `useA2UIStore` | `useA2UIStore` | `useA2UIContext` | -| `useA2UIStoreSelector` | `useA2UIStoreSelector` | `useA2UIContext` | +| `CopilotRuntime` | `onBeforeRequest` / `onAfterRequest` | `beforeRequestMiddleware` / `afterRequestMiddleware` | +| `useCopilotChat` | `visibleMessages` | Use AG-UI message stream | +| `useCopilotChat` | `appendMessage` | Use `sendMessage` or agent API | +| Chat component props | `AssistantMessage` / `UserMessage` / `Messages` render props | `RenderMessage` | +| `useA2UIStore` | `useA2UIStore` | `useA2UIContext` | +| `useA2UIStoreSelector` | `useA2UIStoreSelector` | `useA2UIContext` | diff --git a/skills/copilotkit-upgrade/references/v1-to-v2-migration.md b/skills/copilotkit-upgrade/references/v1-to-v2-migration.md index f9bc83efcd6..84be12ea1af 100644 --- a/skills/copilotkit-upgrade/references/v1-to-v2-migration.md +++ b/skills/copilotkit-upgrade/references/v1-to-v2-migration.md @@ -20,15 +20,15 @@ npm install @copilotkit/react-core@latest @copilotkit/runtime@latest @copilotkit **Package mapping:** -| v1 Package | v2 Package | Notes | -| -------------------------------- | --------------------------- | ----------------------------------------------------------------------------------------- | -| `@copilotkit/react-core` | `@copilotkit/react-core/v2` | Same package; v2 hooks, provider, types, and chat components live under the `/v2` subpath | -| `@copilotkit/react-ui` | `@copilotkit/react-core/v2` | Chat components moved into `react-core/v2`; `react-ui` contributes only styles in v2 | +| v1 Package | v2 Package | Notes | +| -------------------------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `@copilotkit/react-core` | `@copilotkit/react-core/v2` | Same package; v2 hooks, provider, types, and chat components live under the `/v2` subpath | +| `@copilotkit/react-ui` | `@copilotkit/react-core/v2` | Chat components moved into `react-core/v2`; `react-ui` contributes only styles in v2 | | `@copilotkit/react-textarea` | -- | No v2 equivalent; the v1 `@copilotkit/react-textarea@1.x` package stays installable -- drop it only after migrating off `CopilotTextarea` | -| `@copilotkit/runtime` | `@copilotkit/runtime/v2` | Same package; v2 runtime/agents live under the `/v2` subpath | -| `@copilotkit/runtime-client-gql` | `@ag-ui/client` | Re-exported by `@copilotkit/react-core/v2` | -| `@copilotkit/shared` | `@copilotkit/shared` | Utility types and constants | -| `@copilotkit/sdk-js` | `@copilotkit/runtime/v2` | `BuiltInAgent` and agent definitions now ship from `runtime/v2` | +| `@copilotkit/runtime` | `@copilotkit/runtime/v2` | Same package; v2 runtime/agents live under the `/v2` subpath | +| `@copilotkit/runtime-client-gql` | `@ag-ui/client` | Re-exported by `@copilotkit/react-core/v2` | +| `@copilotkit/shared` | `@copilotkit/shared` | Utility types and constants | +| `@copilotkit/sdk-js` | `@copilotkit/runtime/v2` | `BuiltInAgent` and agent definitions now ship from `runtime/v2` | ### Step 2: Update All Imports From 0f9009cca1e245de9076ebc669506d8909370bfb Mon Sep 17 00:00:00 2001 From: Austin Merrick Date: Mon, 22 Jun 2026 10:43:43 -0700 Subject: [PATCH 03/12] docs(teams): rewrite Microsoft Teams guide for @copilotkit/bot-teams The previous guide documented an older API (createTeamsAgentBot, a local "Teams DevTools" bridge) that shipped through copilotkitnext. Rewrite it for the new createBot + teams() PlatformAdapter, mirroring the Slack guide: M365 Agents Playground quickstart, interactive Adaptive Cards, a human-approval gate, splitting the bot from its agent over AG-UI, and Azure sideloading into real Teams. Also refresh the frontend picker summary (Playground, not DevTools). --- .../src/content/docs/frontends/teams.mdx | 626 +++++++++--------- .../shell-docs/src/lib/frontend-options.ts | 2 +- 2 files changed, 302 insertions(+), 326 deletions(-) diff --git a/showcase/shell-docs/src/content/docs/frontends/teams.mdx b/showcase/shell-docs/src/content/docs/frontends/teams.mdx index 8062b394eed..a929d8dd2cc 100644 --- a/showcase/shell-docs/src/content/docs/frontends/teams.mdx +++ b/showcase/shell-docs/src/content/docs/frontends/teams.mdx @@ -1,313 +1,322 @@ --- title: Microsoft Teams -description: Build a Microsoft Teams bot with CopilotKit and Copilot Runtime from an empty Node project. +description: Build an AI Microsoft Teams bot with CopilotKit using createBot, the Teams adapter, agent runs with thread.runAgent, and interactive JSX messages rendered as Adaptive Cards. hideTOC: true earlyAccess: teams --- -import { Accordions, Accordion } from "fumadocs-ui/components/accordion"; -import { Callout } from "fumadocs-ui/components/callout"; -import { Step, Steps } from "fumadocs-ui/components/steps"; -import { Tab, Tabs } from "fumadocs-ui/components/tabs"; - -## What you will build - -By the end of this guide, you will have a small Node app that can answer Microsoft Teams messages -through Copilot Runtime. You will test it locally first with Teams DevTools, then optionally expose -the same bot through Azure Bot Service and sideload it into Microsoft Teams. - -`@copilotkit/bot-teams` handles the Teams-specific work: receiving Teams activities, rendering -Adaptive Cards, streaming updates, and running the local DevTools bridge. Your app provides the -Copilot Runtime, agent configuration, Microsoft credentials, tunnel, and Teams manifest. - -| Process | Port | Purpose | -| --- | --- | --- | -| Copilot Runtime | 8200 | Runs a `BuiltInAgent` and exposes `/api/copilotkit` | -| Teams bot | 3978 | Receives Teams activities at `POST /api/messages` | -| Teams DevTools | 3979 | Local browser UI for testing Teams messages without a Teams account | +This guide takes you from zero to a Microsoft Teams bot you can chat with, then adds an interactive Adaptive Card and a human-approval gate. You write handlers in TypeScript, the agent's replies stream into the conversation, and rich messages are JSX that the adapter renders to Adaptive Cards (Teams' message-UI format). You can run the whole thing locally in the **M365 Agents Playground** with no Microsoft account, then sideload it into real Teams when you're ready. ## Prerequisites -For local development: - -- [Node.js](https://nodejs.org/en/download) 22 or newer -- npm 10 or newer -- an [OpenAI API key](https://platform.openai.com/api-keys) +- Node.js 20+ +- An OpenAI API key (or Anthropic/Google, or any model the [built-in agent](/build-with-agents) supports) +- For local testing, nothing else. The M365 Agents Playground runs over `npx`, with no account needed. +- For real Teams (the last step): a Microsoft 365 tenant that allows [custom app upload](https://learn.microsoft.com/microsoftteams/platform/concepts/deploy-and-publish/apps-upload), plus access to create an [Entra app registration](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app) and an [Azure Bot resource](https://learn.microsoft.com/azure/bot-service/bot-service-quickstart-registration) -For sideloading into Microsoft Teams: - -- the [`devtunnel` CLI](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started) -- a Microsoft 365 tenant that allows [uploading custom apps](https://learn.microsoft.com/microsoftteams/platform/concepts/deploy-and-publish/apps-upload) -- access to create a [Microsoft Entra app registration](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app) -- access to create an [Azure Bot resource](https://learn.microsoft.com/azure/bot-service/bot-service-quickstart-registration) - -## Build and verify locally +## Getting started - - -### Create the Node project - -Start with a new Node project and install Copilot Runtime, the core package, and the Teams bridge: - - - - - ```bash - mkdir copilotkit-teams-bot - cd copilotkit-teams-bot - npm init -y - npm install @copilotkit/bot-teams@0.0.1 @copilotkit/core@1.59.1 @copilotkit/runtime - npm install -D tsx typescript @types/node - ``` - - - - - ```bash - mkdir copilotkit-teams-bot - cd copilotkit-teams-bot - pnpm init - pnpm add @copilotkit/bot-teams@0.0.1 @copilotkit/core@1.59.1 @copilotkit/runtime - pnpm add -D tsx typescript @types/node - ``` - - - - - ```bash - mkdir copilotkit-teams-bot - cd copilotkit-teams-bot - yarn init -y - yarn config set nodeLinker node-modules - yarn add @copilotkit/bot-teams@0.0.1 @copilotkit/core@1.59.1 @copilotkit/runtime - yarn add -D tsx typescript @types/node - ``` - - - - - - The Teams bridge is early access. `@copilotkit/bot-teams@0.0.1` expects - `@copilotkit/core@1.59.1`, so the quickstart pins that pair to keep - `CopilotKitCore` types aligned. - - - - - - -### Add TypeScript - -Create `tsconfig.json`: - -```json title="tsconfig.json" -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true - } -} -``` - - - - - -### Add Copilot Runtime and the Teams bot - -Create `bot.cts`. This single file starts both Copilot Runtime and the Teams-facing bot: - -```ts title="bot.cts" -import { createServer } from "node:http"; -import { CopilotKitCore } from "@copilotkit/core"; -import { createTeamsAgentBot } from "@copilotkit/bot-teams/bot"; -import { BuiltInAgent, CopilotRuntime } from "@copilotkit/runtime/v2"; -import { createCopilotNodeListener } from "@copilotkit/runtime/v2/node"; - -const agentId = "assistant"; -const runtimePort = Number(process.env.RUNTIME_PORT ?? 8200); -const botPort = Number(process.env.PORT ?? 3978); -const runtimeUrl = `http://localhost:${runtimePort}/api/copilotkit`; - -const runtime = new CopilotRuntime({ - agents: { - [agentId]: new BuiltInAgent({ - model: process.env.OPENAI_MODEL ?? "openai:gpt-5-mini", - prompt: - "You are a helpful Microsoft Teams assistant. Keep replies concise and useful.", - }), - }, -}); - -createServer( - createCopilotNodeListener({ - runtime, - basePath: "/api/copilotkit", - cors: true, - }), -).listen(runtimePort, () => { - console.log(`Copilot Runtime listening at ${runtimeUrl}`); -}); - -const core = new CopilotKitCore({ runtimeUrl }); -core.setDefaultThrottleMs(1000); -core.registerProxiedAgent({ - agentId, - runtimeAgentId: agentId, -}); - -async function main() { - const bot = createTeamsAgentBot({ - core, - agentId, - approvalTimeoutMs: 5 * 60 * 1000, - reviewerName: "Reviewer", - }); - - await bot.start(botPort); - console.log(`Teams bot listening at http://localhost:${botPort}/api/messages`); - console.log(`Teams DevTools available at http://localhost:${botPort + 1}/devtools`); -} - -main().catch((error) => { - console.error(error); - process.exit(1); -}); -``` - -Your project should now look like this: - -```txt -copilotkit-teams-bot/ - package.json - tsconfig.json - bot.cts -``` - - - - - -### Run the bot + + ### Scaffold the project + + ```bash + mkdir my-teams-bot && cd my-teams-bot + npm init -y && npm pkg set type=module + ``` + + Install the bot packages, plus `@copilotkit/runtime` for the in-process agent and `tsx` to run TypeScript directly: + + + + ```bash + npm install @copilotkit/bot @copilotkit/bot-ui @copilotkit/bot-teams @copilotkit/runtime + npm install -D tsx typescript @types/node + ``` + + + ```bash + pnpm add @copilotkit/bot @copilotkit/bot-ui @copilotkit/bot-teams @copilotkit/runtime + pnpm add -D tsx typescript @types/node + ``` + + + ```bash + yarn add @copilotkit/bot @copilotkit/bot-ui @copilotkit/bot-teams @copilotkit/runtime + yarn add -D tsx typescript @types/node + ``` + + + + Then create a `tsconfig.json` that points the JSX factory at `@copilotkit/bot-ui`. That is what makes `` / ` + + + , + ); + return; + } + await thread.runAgent(); + }); + ``` + + Send a message containing "deploy" and click the buttons. Your handler code never leaves your process. Teams only sees an opaque action id carried in the card's `Action.Submit` data. + + + The default action store is **in-memory**: after a process restart, clicks on old buttons are acknowledged but ignored. For buttons that survive restarts, plug a durable store into `createBot({ actionStore })` (see the [ActionStore contract](/reference/bot/types/ActionStore)). + + + + ### Gate an action on human approval + + The same buttons can **block the agent** until a human decides. A tool calls `await thread.awaitChoice()`, which posts the card and suspends the run until a click resolves it. It's the Teams equivalent of React's `useHumanInTheLoop`. Because the bot acks the Teams turn immediately and runs the agent out-of-turn, the approval can land minutes later and still resume the agent: + + ```tsx title="bot.tsx" + import { defineBotTool } from "@copilotkit/bot"; + import { Message, Header, Actions, Button } from "@copilotkit/bot-ui"; + import { z } from "zod"; + + const confirmSend = defineBotTool({ + name: "confirm_send", + description: + "Ask the user to approve before sending. BLOCKS until they click; returns {confirmed}.", + parameters: z.object({ summary: z.string() }), + async handler({ summary }, { thread }) { + const choice = await thread.awaitChoice<{ confirmed?: boolean }>( // [!code highlight] + +
{`📣 ${summary}?`}
+ + + + +
, + ); + return choice?.confirmed ? "Approved, proceed." : "Declined, stop."; + }, + }); + + const bot = createBot({ + adapters: [teams({ port: 3978 })], + agent: makeAgent, // as above + tools: [confirmSend], // [!code highlight] + }); + ``` + + The agent calls `confirm_send` before the consequential action, the card posts, and the run waits for Approve/Reject. The [full example](https://github.com/CopilotKit/CopilotKit/tree/main/examples/teams) wires this into a `send_announcement` flow and updates the card in place (✅/🚫) the moment it's clicked. + + + Pending `awaitChoice` waiters live in memory, so they don't survive a process restart. Durable waiters are a planned follow-up. + +
-Set your OpenAI key and start the bot: - - - - - ```bash - export OPENAI_API_KEY=sk-... - npx tsx bot.cts - ``` - - - - - ```powershell - $env:OPENAI_API_KEY="sk-..." - npx tsx bot.cts - ``` +
- - +## Split the bot and the agent -Open `http://localhost:3979/devtools` and send: +The bot and its agent talk over [AG-UI](https://docs.ag-ui.com), an open protocol for communication between agents and frontends, so they don't have to share a process. The production shape is two services joined by a URL: move the `CopilotSseRuntime` block into its own process (or point at any existing AG-UI endpoint, such as a deployed CopilotKit runtime or a LangGraph server) and point the bot at it via env: -```txt -Hello from Teams +```tsx title="bot.tsx" +agent: (threadId) => { + const agent = new SanitizingHttpAgent({ url: process.env.AGENT_URL! }); // [!code highlight] + agent.threadId = threadId; + return agent; +}, ``` - - If you receive a response card in DevTools, Copilot Runtime and the Teams bot are working - locally. - - - - +[`SanitizingHttpAgent`](https://github.com/CopilotKit/CopilotKit/tree/main/packages/bot-teams) is an `HttpAgent` that tolerates the event streams real agent backends emit. Use it instead of the stock `HttpAgent` when connecting to a remote runtime. ## Sideload into Microsoft Teams -The local DevTools path does not require Microsoft credentials. The real Teams client does. Keep the -same `bot.cts`; add a tunnel, a Microsoft app registration, an Azure Bot resource, and a Teams app -manifest. +The Playground needs no credentials. The real Teams client does. Keep the same `bot.tsx`, then add a public HTTPS endpoint, a Microsoft app registration, an Azure Bot resource, and a Teams app manifest. -### Create a public tunnel +### Give the bot a public HTTPS endpoint -In a separate terminal, expose the bot port: +Real Teams reaches your bot over the internet, so it needs a public HTTPS messaging endpoint. The bot is a plain Node HTTP server with one route (`POST /api/messages`) and a `/healthz` probe, so you have two ways to get one. -```bash -devtunnel create copilotkit-teams-local -a -devtunnel port create copilotkit-teams-local -p 3978 -devtunnel host copilotkit-teams-local -``` +**Deploy it** (recommended for anything past a quick test). It runs on any host that gives you a public HTTPS URL, whether that's a container platform, a PaaS, or a VM behind a reverse proxy. Bind to the port the host provides (the adapter reads `PORT`), and keep the agent runtime reachable from the bot, either in-process as written here or as a second service (see [Split the bot and the agent](#split-the-bot-and-the-agent)). The [`examples/teams`](https://github.com/CopilotKit/CopilotKit/tree/main/examples/teams) project ships a Dockerfile so the whole thing is a one-step container build. -Copy the HTTPS tunnel URL. Your bot messaging endpoint will be: +**Tunnel to localhost** for quick testing against real Teams without deploying. Any tunneler works; Microsoft's [`devtunnel` CLI](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started) is one option: -```txt -https:///api/messages +```bash +devtunnel create copilotkit-teams -a +devtunnel port create copilotkit-teams -p 3978 +devtunnel host copilotkit-teams ``` +Either way you end up with a public host. Your bot's messaging endpoint is `https:///api/messages`, which you'll plug into the Azure Bot resource and the manifest below. + ### Create Microsoft credentials -In Microsoft Entra ID: - -1. Create an app registration. -2. Copy the Application (client) ID. This is your Teams bot ID. -3. Copy the Directory (tenant) ID. -4. Create a client secret and copy its value. - -In Azure Bot Service: +In **Microsoft Entra ID**, create an app registration and copy its **Application (client) ID** (this is your Teams bot id), its **Directory (tenant) ID**, and a new **client secret** value. -1. Create a bot resource that uses the app registration from Entra ID. -2. Set the messaging endpoint to `https:///api/messages`. -3. Enable the Microsoft Teams channel. +In **Azure Bot Service**, create a bot resource that uses that app registration, set its messaging endpoint to `https:///api/messages`, and enable the **Microsoft Teams** channel. -Run the bot with those credentials: +Then run the bot with those credentials. The Teams adapter reads them from the `clientId` / `clientSecret` / `tenantId` environment variables (the names the M365 Agents SDK uses), or you can pass them to `teams({ clientId, clientSecret, tenantId, port })`: - - - - ```bash - export OPENAI_API_KEY=sk-... - export CLIENT_ID= - export CLIENT_SECRET= - export TENANT_ID= - npx tsx bot.cts - ``` - - - - - ```powershell - $env:OPENAI_API_KEY="sk-..." - $env:CLIENT_ID="" - $env:CLIENT_SECRET="" - $env:TENANT_ID="" - npx tsx bot.cts - ``` +```bash +export OPENAI_API_KEY=sk-... +export clientId= +export clientSecret= +export tenantId= +npx tsx bot.tsx +``` - - + + With credentials set, the bot acks each Teams turn immediately and runs the agent on a detached context, so a HITL approval can resume the agent minutes later. In the anonymous Playground there's no app id, so the run uses the inbound turn instead, which localhost holds open across the wait. + -### Create the Teams app package +### Build the Teams app package -Create `appPackage/manifest.json`: +A Teams app package is a zip of a manifest plus two icons. Create `appPackage/manifest.json`: ```json title="appPackage/manifest.json" { @@ -321,96 +330,63 @@ Create `appPackage/manifest.json`: "privacyUrl": "https://example.com/privacy", "termsOfUseUrl": "https://example.com/terms" }, - "name": { - "short": "CopilotKit Bot", - "full": "CopilotKit Teams Bot" - }, + "name": { "short": "CopilotKit Bot", "full": "CopilotKit Teams Bot" }, "description": { "short": "A CopilotKit assistant for Microsoft Teams.", "full": "A Microsoft Teams bot powered by CopilotKit." }, - "icons": { - "outline": "outline.png", - "color": "color.png" - }, + "icons": { "color": "color.png", "outline": "outline.png" }, "accentColor": "#5B5FC7", "bots": [ { "botId": "", - "scopes": ["personal"], + "scopes": ["personal", "team", "groupChat"], "supportsFiles": false, "isNotificationOnly": false } ], - "validDomains": [""] + "validDomains": [""] } ``` -Replace: - -- `` with a new UUID for this Teams app package -- `` with the Entra application client ID -- `` with the tunnel host only, without `https://` - -Add the required Teams icons: - -- `appPackage/color.png`: 192 x 192 -- `appPackage/outline.png`: 32 x 32, transparent background - -```txt -appPackage/ - manifest.json - color.png - outline.png -``` - -Package the manifest: +Replace `` with a fresh UUID, `` with the Entra client id, and `` with the host only (no `https://`). Add the two required icons, `color.png` (192×192) and `outline.png` (32×32, transparent), then zip them together: ```bash cd appPackage -zip -r appPackage.local.zip manifest.json color.png outline.png +zip -r appPackage.zip manifest.json color.png outline.png ``` + + The [`examples/teams`](https://github.com/CopilotKit/CopilotKit/tree/main/examples/teams) project ships a `pnpm package` script that validates the manifest, injects your app id from env, generates placeholder icons, and builds the zip. + + ### Upload and test in Teams -In Microsoft Teams: +In Microsoft Teams: open **Apps → Manage your apps → Upload a custom app**, choose `appPackage/appPackage.zip`, then open a personal chat with the bot and send `Hello`. You should get the same reply you saw in the Playground. -1. Go to **Apps**. -2. Select **Manage your apps**. -3. Select **Upload a custom app**. -4. Choose `appPackage/appPackage.local.zip`. -5. Open a personal chat with the bot and send `Hello`. - -You should receive the same response you saw in DevTools. + + + - **Teams says the bot can't be reached.** Confirm the Azure Bot messaging endpoint is exactly `https:///api/messages`, and that your deployment (or tunnel) is up and reachable over HTTPS. + - **Auth errors.** Confirm `clientId` / `clientSecret` / `tenantId` match the Entra app registration used by the Azure Bot resource. + - **"Upload a custom app" is missing.** Your tenant doesn't allow sideloading for your account; ask a tenant admin to enable custom app upload. + + -## Troubleshooting - - - - Confirm `OPENAI_API_KEY` is set in the terminal running `npx tsx bot.cts`, and check that - `http://localhost:8200/api/copilotkit/info` returns the `assistant` agent. - - - Confirm the Azure Bot messaging endpoint is exactly `https:///api/messages`, - and that `devtunnel host copilotkit-teams-local` is still running. - - - Confirm `CLIENT_ID`, `CLIENT_SECRET`, and `TENANT_ID` match the Entra app registration used by - the Azure Bot resource. - - - Your Microsoft 365 tenant does not allow sideloading for your account. Ask a tenant admin to - enable custom app upload for Teams. - - - Stop the process using that port, or set `PORT` and `RUNTIME_PORT` before running - `npx tsx bot.cts`. - - +## Known limitations (v1) + +- **In-memory state:** conversation history and pending HITL approvals are in-memory, so they don't survive a process restart. Swap in a durable `ConversationStore` / `ActionStore` for production. +- **Streamed by message edit:** replies post once and edit in place as tokens arrive, rather than native Teams token streaming (a planned enhancement). +- **No slash commands, file upload, or directory lookup yet:** drive everything through `onMessage` plus tools. +- **Replies only:** the bot answers turns it's part of (DMs and `@`-mentions); it doesn't post proactively on its own. + +## Next steps + +- **API reference:** the [Bots reference](/reference/bot), covering [createBot](/reference/bot/functions/createBot), the [Thread API](/reference/bot/classes/Thread), [tools](/reference/bot/functions/defineBotTool), and the [component vocabulary](/reference/bot/components/Message) +- **Full example:** the [Teams demo bot](https://github.com/CopilotKit/CopilotKit/tree/main/examples/teams), a `BuiltInAgent` with agent-rendered Adaptive Cards, a human-in-the-loop approval gate, and a deployable app package diff --git a/showcase/shell-docs/src/lib/frontend-options.ts b/showcase/shell-docs/src/lib/frontend-options.ts index bc055f868e9..9848bf720de 100644 --- a/showcase/shell-docs/src/lib/frontend-options.ts +++ b/showcase/shell-docs/src/lib/frontend-options.ts @@ -50,7 +50,7 @@ export const FRONTEND_OPTIONS: readonly FrontendOption[] = [ id: "teams", name: "Teams", icon: "teams", - summary: "Microsoft Teams bot quickstart and local DevTools setup.", + summary: "Microsoft Teams bot quickstart with the M365 Agents Playground.", }, ] as const; From 7a309fc210b55434fce8ecc57f6f4e0e746e0a22 Mon Sep 17 00:00:00 2001 From: Austin Merrick Date: Thu, 25 Jun 2026 12:56:04 -0700 Subject: [PATCH 04/12] feat(bot-teams): Microsoft Teams PlatformAdapter with Adaptive Cards, streamed replies, and HITL Adds the @copilotkit/bot-teams package: a PlatformAdapter that bridges the CopilotKit runtime to Microsoft Teams. Renders agent output as Adaptive Cards (including native components via bot-ui), streams replies with a typing-indicator heartbeat, supports human-in-the-loop confirmations, and handles inbound files via the Graph API with channel file support. --- packages/bot-teams/ARCHITECTURE.md | 131 ++++ packages/bot-teams/README.md | 141 ++++ packages/bot-teams/package.json | 67 ++ packages/bot-teams/src/adapter.test.ts | 245 +++++++ packages/bot-teams/src/adapter.ts | 672 ++++++++++++++++++ .../bot-teams/src/conversation-store.test.ts | 84 +++ packages/bot-teams/src/conversation-store.ts | 109 +++ packages/bot-teams/src/download-files.test.ts | 125 ++++ packages/bot-teams/src/download-files.ts | 268 +++++++ packages/bot-teams/src/event-renderer.test.ts | 112 +++ packages/bot-teams/src/event-renderer.ts | 162 +++++ packages/bot-teams/src/graph-files.test.ts | 134 ++++ packages/bot-teams/src/graph-files.ts | 199 ++++++ packages/bot-teams/src/index.ts | 41 ++ packages/bot-teams/src/interaction.test.ts | 54 ++ packages/bot-teams/src/interaction.ts | 53 ++ packages/bot-teams/src/listener.test.ts | 49 ++ packages/bot-teams/src/listener.ts | 84 +++ packages/bot-teams/src/message-stream.test.ts | 53 ++ packages/bot-teams/src/message-stream.ts | 106 +++ .../src/render/adaptive-card.test.ts | 253 +++++++ .../bot-teams/src/render/adaptive-card.ts | 423 +++++++++++ .../bot-teams/src/render/auto-close.test.ts | 57 ++ packages/bot-teams/src/render/auto-close.ts | 163 +++++ packages/bot-teams/src/render/budget.test.ts | 28 + packages/bot-teams/src/render/budget.ts | 46 ++ .../bot-teams/src/render/markdown.test.ts | 69 ++ packages/bot-teams/src/render/markdown.ts | 134 ++++ .../bot-teams/src/sanitizing-http-agent.ts | 67 ++ packages/bot-teams/src/types.ts | 55 ++ packages/bot-teams/tsconfig.check.json | 10 + packages/bot-teams/tsconfig.json | 10 + packages/bot-teams/vitest.config.ts | 7 + packages/bot-ui/src/components.test.tsx | 27 + packages/bot-ui/src/components.tsx | 29 + pnpm-lock.yaml | 412 +++++++++-- pnpm-workspace.yaml | 1 + 37 files changed, 4635 insertions(+), 45 deletions(-) create mode 100644 packages/bot-teams/ARCHITECTURE.md create mode 100644 packages/bot-teams/README.md create mode 100644 packages/bot-teams/package.json create mode 100644 packages/bot-teams/src/adapter.test.ts create mode 100644 packages/bot-teams/src/adapter.ts create mode 100644 packages/bot-teams/src/conversation-store.test.ts create mode 100644 packages/bot-teams/src/conversation-store.ts create mode 100644 packages/bot-teams/src/download-files.test.ts create mode 100644 packages/bot-teams/src/download-files.ts create mode 100644 packages/bot-teams/src/event-renderer.test.ts create mode 100644 packages/bot-teams/src/event-renderer.ts create mode 100644 packages/bot-teams/src/graph-files.test.ts create mode 100644 packages/bot-teams/src/graph-files.ts create mode 100644 packages/bot-teams/src/index.ts create mode 100644 packages/bot-teams/src/interaction.test.ts create mode 100644 packages/bot-teams/src/interaction.ts create mode 100644 packages/bot-teams/src/listener.test.ts create mode 100644 packages/bot-teams/src/listener.ts create mode 100644 packages/bot-teams/src/message-stream.test.ts create mode 100644 packages/bot-teams/src/message-stream.ts create mode 100644 packages/bot-teams/src/render/adaptive-card.test.ts create mode 100644 packages/bot-teams/src/render/adaptive-card.ts create mode 100644 packages/bot-teams/src/render/auto-close.test.ts create mode 100644 packages/bot-teams/src/render/auto-close.ts create mode 100644 packages/bot-teams/src/render/budget.test.ts create mode 100644 packages/bot-teams/src/render/budget.ts create mode 100644 packages/bot-teams/src/render/markdown.test.ts create mode 100644 packages/bot-teams/src/render/markdown.ts create mode 100644 packages/bot-teams/src/sanitizing-http-agent.ts create mode 100644 packages/bot-teams/src/types.ts create mode 100644 packages/bot-teams/tsconfig.check.json create mode 100644 packages/bot-teams/tsconfig.json create mode 100644 packages/bot-teams/vitest.config.ts diff --git a/packages/bot-teams/ARCHITECTURE.md b/packages/bot-teams/ARCHITECTURE.md new file mode 100644 index 00000000000..3f32b52ef7e --- /dev/null +++ b/packages/bot-teams/ARCHITECTURE.md @@ -0,0 +1,131 @@ +# Architecture + +`@copilotkit/bot-teams` is a concrete `PlatformAdapter` for +[`@copilotkit/bot`](../bot): it plugs Microsoft Teams into the +platform-agnostic bot engine, exactly as [`@copilotkit/bot-slack`](../bot-slack) +does for Slack. You write the bot once (handlers, JSX, tools, context) and this +package translates between the engine and Teams via the **Microsoft 365 Agents +SDK** (`@microsoft/agents-hosting`). + +## Design goals + +- **The agent is ignorant of Teams.** Tool/handler code uses the engine's + platform-agnostic surface (`thread.post`, `thread.stream`, + `thread.awaitChoice`, bot-ui JSX). Nothing Teams-specific leaks up. +- **Teams mechanics are contained.** Adaptive Card rendering, streamed-by-edit + updates, card-action decoding, and proactive auth all live behind the + `PlatformAdapter` boundary. +- **Failure isolation.** One bad turn (e.g. a Bot Connector error) is logged and + contained, so it never crashes the process or takes down other conversations. + +## The boundary: `PlatformAdapter` + +`TeamsAdapter` (in `adapter.ts`) implements the engine's `PlatformAdapter`: +ingress normalization, egress (`post` / `update` / `delete` / streamed edits), +IR→native rendering, capability flags, and the conversation store. `teams(opts)` +is the thin factory most callers use. + +## Request lifecycle + +``` +Teams ──HTTP──▶ POST /api/messages (listener.ts, express) + │ CloudAdapter.process ── authenticates, builds TurnContext + ▼ + handleActivity (adapter.ts) + │ message? → sink.onTurn(...) → engine runs handlers / agent + │ card submit? → sink.onInteraction(...) → engine resolves awaitChoice + ▼ + egress: render IR → Adaptive Card | Markdown text, sent on a + TurnContext (proactive when credentialed; see below) +``` + +### Ingress + +`createTeamsServer` (`listener.ts`) stands up `POST /api/messages` (+ a +`/healthz` liveness probe) and hands each inbound activity to +`CloudAdapter.process`, which authenticates the request and invokes +`handleActivity`. The `process` promise is `.catch`-contained so a failed turn +returns 500 instead of crashing the process. + +### Proactive vs in-turn (the credentialed split) + +How the bot replies depends on whether it has Microsoft credentials: + +- **Credentialed (real Teams):** ingress acks the inbound turn immediately and + runs the work on a **detached `continueConversation` context** authenticated + by the app id. This lets an `awaitChoice` suspend outlive the ~15s Teams turn + window (an approval can land minutes later), and (critically) it is the + _authenticated_ context. The inbound turn's own connector client is created + with an **anonymous identity**, so using it for outbound calls + (`sendActivity`/`updateActivity`) is rejected `401`. **Both** ordinary replies + **and** card interactions therefore run on the proactive context. +- **Anonymous (local M365 Agents Playground):** `continueConversation` needs an + app id we don't have, so work runs on the inbound turn context. localhost + holds that connection open across an `awaitChoice` suspend, and the Playground + doesn't enforce connector auth, so the anonymous context is fine there. + +### Run / render + +`createRunRenderer` (`event-renderer.ts`) subscribes to the agent's AG-UI event +stream and bridges it to Teams: each text message is **streamed by edit**. It posts +once (after a typing indicator), then `updateActivity` edits it as the buffer grows, +throttled and serialised by `TeamsMessageStream` (`message-stream.ts`). Mid-stream +buffers are balanced by `autoCloseOpenMarkdown` (`render/auto-close.ts`) so an +in-flight `**`/code-fence never renders broken; the finalized message commits the +agent's exact (balanced) text. Tool calls and interrupts are captured for the +run-loop to read after `runAgent` resolves. + +### Rendering + +`render(ir)` chooses the surface: a reply that collapses to plain text +(`isPlainText`) is sent as a normal **Markdown** text activity (a bare `Echo: hi` +shouldn't be a card); anything structured/interactive becomes an **Adaptive Card +1.5** attachment (`render/adaptive-card.ts`). Both renderers clamp to +`TEAMS_LIMITS` (`render/budget.ts`) to stay within Teams' payload ceilings. + +### HITL & interrupts + +A tool handler that calls `await thread.awaitChoice()` posts an approval +Adaptive Card and suspends the run. The card's buttons are `Action.Submit`s +carrying an opaque `ckActionId` + tiny value in their `data`. The click arrives +as a Message activity; `parseCardAction` / `decodeInteraction` (`interaction.ts`) +recognise it and route it to `sink.onInteraction`, which resolves the waiter and +runs the button's `onClick` (e.g. editing the card in place). Ingress and +interaction decoding derive the conversation key from one shared helper +(`conversationKeyOf`) so the waiter always resolves. + +### Conversation store + +Teams does not hand the bot a queryable transcript (unlike Slack's +`conversations.history`), so `TeamsConversationStore` (`conversation-store.ts`) +keeps an **in-memory** transcript per conversation and seeds each agent run with +it. It implements the engine's `ConversationStore` interface, so a durable +backend can be swapped in for production (today the store and any pending +`awaitChoice` waiters do not survive a restart). + +## SDK files at a glance + +| File | Role | +| -------------------------- | -------------------------------------------------------------------- | +| `adapter.ts` | `PlatformAdapter`: ingress, egress, proactive auth, rendering | +| `listener.ts` | express server: `POST /api/messages` + `/healthz`, error containment | +| `event-renderer.ts` | AG-UI → streamed-by-edit + tool/interrupt capture | +| `message-stream.ts` | throttled, serialised post-then-edit state machine | +| `render/adaptive-card.ts` | bot-ui IR → Adaptive Card 1.5 (+ HITL action ids) | +| `render/markdown.ts` | bot-ui IR → Markdown (plain-text path) | +| `render/auto-close.ts` | balances mid-stream markdown for clean edits | +| `render/budget.ts` | per-element limits, truncation/clamping | +| `interaction.ts` | decode `Action.Submit` → engine `InteractionEvent` | +| `conversation-store.ts` | in-memory transcript (pluggable for durability) | +| `sanitizing-http-agent.ts` | `HttpAgent` tolerant of `@ag-ui/langgraph` event quirks | + +## What's intentionally _not_ done yet + +The architecture leaves room for each; none is required for the core loop: + +- **Native token streaming:** replies stream by post-then-edit, not via the + SDK's `StreamingResponse` (`queueTextChunk`/`endStream`). +- **Durable conversation store + HITL waiters:** in-memory today. +- **File upload/download** and **Microsoft Graph user lookup:** not wired. + +These mirror the deferred items in the README's roadmap. diff --git a/packages/bot-teams/README.md b/packages/bot-teams/README.md new file mode 100644 index 00000000000..0eb7bc233e0 --- /dev/null +++ b/packages/bot-teams/README.md @@ -0,0 +1,141 @@ +# @copilotkit/bot-teams + +The **Microsoft Teams platform adapter** for [`@copilotkit/bot`](../bot). It's a +concrete `PlatformAdapter` that plugs Teams into the platform-agnostic bot +engine, exactly like [`@copilotkit/bot-slack`](../bot-slack) does for Slack. You +write your bot once with `createBot` (handlers, JSX, tools, context) and run it +on Teams by adding this adapter. + +It is built on the **Microsoft 365 Agents SDK** (`@microsoft/agents-hosting`), +the successor to the Bot Framework SDK. + +## Install + +```sh +pnpm add @copilotkit/bot @copilotkit/bot-ui @copilotkit/bot-teams +``` + +## Quickstart + +```ts +import { createBot } from "@copilotkit/bot"; +import { teams } from "@copilotkit/bot-teams"; + +const bot = createBot({ + adapters: [teams({ port: 3978 })], +}); + +bot.onMessage(({ thread, message }) => thread.post(`Echo: ${message.text}`)); + +await bot.start(); // POST /api/messages now listening on :3978 +``` + +Then point the **Microsoft 365 Agents Playground** at it. No Microsoft +credentials are required for local development: + +```sh +npx @microsoft/m365agentsplayground # opens http://localhost:56150 +``` + +The Playground connects to `http://127.0.0.1:3978/api/messages` and gives you a +Teams-like chat UI to test against. See [`examples/teams`](../../examples/teams) +for a complete, runnable echo bot, and the +[Microsoft Teams guide](../../showcase/shell-docs/src/content/docs/frontends/teams.mdx) +for sideloading into real Teams via Azure Bot Service. + +## How it maps onto the `PlatformAdapter` contract + +- **Ingress:** a `CloudAdapter` receives Teams activities at + `POST /api/messages` (stood up by an Express server). Each `message` activity + is normalized into `sink.onTurn(...)`. Uploaded files ride along as + attachments: `buildFileContentParts` downloads them (a `file.download.info` + URL, or a `data:`/https media URL) and hands the agent multimodal content + parts — CSV/JSON/text as decoded text, images and PDFs as binary. That's what + makes "upload a CSV → get a chart" work. Note Teams only delivers uploaded + files to a bot in **1:1 (personal) chat** (requires `supportsFiles: true` in + the app manifest); in a channel or group chat Teams does NOT send the file to + the bot at all, so chart-from-data there means pasting the data inline. +- **Egress:** structured/interactive UI is rendered to an **Adaptive Card** + (1.5) and sent as an attachment; a reply that collapses to plain text is sent + as a normal text activity (a bare `Echo: hi` shouldn't be a card). Both go out + on the live `TurnContext` _within the originating turn_. The engine awaits the + whole turn handler, so a reply (or a full `runAgent()` loop) completes before + the HTTP response closes. (Out-of-turn / proactive sends fall back to + `CloudAdapter.continueConversation` via the captured conversation reference.) +- **Files out:** `postFile` posts a file to the conversation. An image (e.g. a + rendered chart PNG) is sent as an inline attachment via a `data:` URI, so it + renders directly in the thread — the bot-slack `postFile` parallel. +- **Streaming:** text replies stream **by message edit** (Teams' baseline + model). It posts the first content, then `updateActivity` edits the same + message as the buffer grows (throttled and serialised; see + `TeamsMessageStream`), after a typing indicator. Native token streaming is a + later enhancement. +- **Agent runs:** `createRunRenderer` bridges AG-UI events to Teams. Each text + message is streamed by edit, and tool calls plus interrupts are captured for + the run loop. +- **History:** Teams does not hand the bot a queryable transcript, so an + in-memory `TeamsConversationStore` keeps one per conversation and seeds each + agent run with it. Swap in a durable `ConversationStore` for production. + +## Options + +```ts +teams({ + port: 3978, // POST /api/messages port (Playground default) + clientId, // Microsoft app id; omit for anonymous local dev + clientSecret, // omit for anonymous local dev + tenantId, // omit for multi-tenant / anonymous + interruptEventNames, // custom-event names treated as agent interrupts +}); +``` + +Credentials also resolve from the `clientId` / `clientSecret` / `tenantId` +environment variables (the names the M365 Agents SDK reads). + +## Status & roadmap + +Implemented: message ingress; **Adaptive Card rendering** of the bot-ui +vocabulary (`
`, `
`/``, ``, ``, +``, ``/`
as a native Table with a header row", () => { + const card = renderAdaptiveCard([ + el( + "table", + [el("row", [el("cell", [text("a1")]), el("cell", [text("b1")])])], + { + columns: [{ header: "A" }, { header: "B", align: "right" }], + }, + ), + ]); + const table = card.body[0] as Record; + expect(table.type).toBe("Table"); + expect(table.firstRowAsHeader).toBe(true); + const rows = table.rows as Array>; + expect(rows).toHaveLength(2); // header + 1 data row + expect(rows[0]!.type).toBe("TableRow"); + }); + + it("clamps top-level actions to the Teams ceiling", () => { + const buttons = Array.from({ length: 10 }, (_, i) => + el("button", [text(`b${i}`)], { onClick: { id: `ck:${i}` } }), + ); + const card = renderAdaptiveCard([el("actions", buttons)]); + expect(card.actions).toHaveLength(6); + }); + + it("skips unknown intrinsics without throwing", () => { + const card = renderAdaptiveCard([el("mystery", [text("x")])]); + expect(card.body).toHaveLength(0); + }); + + it("renders a vertical-bar with title and axis titles", () => { + const card = renderAdaptiveCard([ + chart({ + title: "Tickets", + xAxisTitle: "Month", + yAxisTitle: "Count", + data: [ + { label: "Jan", value: 3 }, + { label: "Feb", value: 7 }, + ], + }), + ]); + expect(card.body[0]).toMatchObject({ + type: "Chart.VerticalBar", + title: "Tickets", + showTitle: true, + showBarValues: true, + maxWidth: "520px", + xAxisTitle: "Month", + yAxisTitle: "Count", + data: [ + { x: "Jan", y: 3 }, + { x: "Feb", y: 7 }, + ], + }); + }); + + it("defaults an absent chart type to a vertical bar", () => { + const card = renderAdaptiveCard([ + chart({ title: "T", data: [{ label: "a", value: 1 }] }), + ]); + expect((card.body[0] as Record).type).toBe( + "Chart.VerticalBar", + ); + }); + + it("maps pie/donut to legend+value slices (no axes)", () => { + const data = [ + { label: "Open", value: 4 }, + { label: "Closed", value: 6 }, + ]; + const pie = renderAdaptiveCard([chart({ type: "pie", data })]); + expect(pie.body[0]).toMatchObject({ + type: "Chart.Pie", + data: [ + { legend: "Open", value: 4 }, + { legend: "Closed", value: 6 }, + ], + }); + expect((pie.body[0] as Record).xAxisTitle).toBeUndefined(); + const donut = renderAdaptiveCard([chart({ type: "donut", data })]); + expect((donut.body[0] as Record).type).toBe("Chart.Donut"); + }); + + it("maps line to a single legended series of values", () => { + const card = renderAdaptiveCard([ + chart({ + type: "line", + title: "Revenue", + data: [{ label: "Q1", value: 100 }], + }), + ]); + expect(card.body[0]).toMatchObject({ + type: "Chart.Line", + data: [{ legend: "Revenue", values: [{ x: "Q1", y: 100 }] }], + }); + }); + + it("maps horizontalBar to an x/y series", () => { + const card = renderAdaptiveCard([ + chart({ type: "horizontalBar", data: [{ label: "A", value: 5 }] }), + ]); + expect(card.body[0]).toMatchObject({ + type: "Chart.HorizontalBar", + data: [{ x: "A", y: 5 }], + }); + }); + + it("clamps chart data points to the Teams ceiling", () => { + const data = Array.from({ length: 80 }, (_, i) => ({ + label: `p${i}`, + value: i, + })); + const card = renderAdaptiveCard([chart({ data })]); + const points = (card.body[0] as { data: unknown[] }).data; + expect(points).toHaveLength(50); + }); +}); + +describe("isPlainText", () => { + it("is true for text-only trees", () => { + expect(isPlainText([text("hi")])).toBe(true); + expect(isPlainText([el("message", [el("section", [text("hi")])])])).toBe( + true, + ); + }); + + it("is false once any rich element appears", () => { + expect(isPlainText([el("header", [text("hi")])])).toBe(false); + expect(isPlainText([el("actions", [el("button", [text("x")])])])).toBe( + false, + ); + expect(isPlainText([el("message", [chart({ data: [] })])])).toBe(false); + }); +}); + +describe("collectPlainText", () => { + it("joins block text depth-first", () => { + const ir = [el("message", [el("section", [text("a")]), text("b")])]; + expect(collectPlainText(ir)).toBe("ab"); + }); +}); diff --git a/packages/bot-teams/src/render/adaptive-card.ts b/packages/bot-teams/src/render/adaptive-card.ts new file mode 100644 index 00000000000..5e1f7c51b9c --- /dev/null +++ b/packages/bot-teams/src/render/adaptive-card.ts @@ -0,0 +1,423 @@ +import type { BotNode } from "@copilotkit/bot-ui"; +import { TEAMS_LIMITS, truncateText, clampArray } from "./budget.js"; + +/** Teams attachment content type for an Adaptive Card. */ +export const ADAPTIVE_CARD_CONTENT_TYPE = + "application/vnd.microsoft.card.adaptive"; + +/** A minimally-typed Adaptive Card (1.5). Elements/actions are open bags: the + * schema is large and we only emit a curated subset. */ +export interface AdaptiveCard { + type: "AdaptiveCard"; + $schema: string; + version: string; + body: CardElement[]; + actions?: CardAction[]; +} +type CardElement = Record; +type CardAction = Record; + +const SCHEMA = "http://adaptivecards.io/schemas/adaptive-card.json"; +const VERSION = "1.5"; + +/** + * Render a cross-platform component IR tree (already expanded by `renderToIR` + * and pre-bound by the action registry, so event props are `{ id }`) into a + * Teams **Adaptive Card** (1.5). + * + * Structural nodes map to body elements (`
`→bold `TextBlock`, + * `
`/``→wrapped `TextBlock`, ``→`FactSet`, + * `
`→native `Table`, ``→`Image`). Interactive nodes split by + * Adaptive Card shape: `
` → a native Adaptive Cards `Table` (1.5). */ +function renderTable(node: BotNode): CardElement { + const props = node.props ?? {}; + const cell = (text: string, header = false): Record => ({ + type: "TableCell", + items: [ + { + type: "TextBlock", + text: truncateText(text, TEAMS_LIMITS.cellText), + wrap: true, + ...(header ? { weight: "Bolder" } : {}), + }, + ], + }); + + const columnsProp = props.columns as + | { header: string; align?: "left" | "center" | "right" }[] + | undefined; + const columns = columnsProp + ? clampArray(columnsProp, TEAMS_LIMITS.tableColumns).items + : undefined; + + const rows: Record[] = []; + if (columns && columns.length > 0) { + rows.push({ + type: "TableRow", + cells: columns.map((c) => cell(c.header, true)), + }); + } + const rowNodes = childNodes(node).filter((c) => c.type === "row"); + const { items: dataRows } = clampArray(rowNodes, TEAMS_LIMITS.tableRows); + for (const rowNode of dataRows) { + const cells = childNodes(rowNode).filter((c) => c.type === "cell"); + rows.push({ + type: "TableRow", + cells: cells.map((c) => cell(collectText(c))), + }); + } + + const table: CardElement = { + type: "Table", + columns: (columns ?? inferColumns(rowNodes)).map((c) => ({ + width: 1, + ...(typeof c === "object" && "align" in c && c.align + ? { horizontalCellContentAlignment: capitalize(c.align) } + : {}), + })), + rows, + firstRowAsHeader: !!(columns && columns.length > 0), + gridStyle: "default", + }; + return table; +} + +/** + * A `` → a native Teams chart element (`Chart.VerticalBar` / + * `Chart.HorizontalBar` / `Chart.Line` / `Chart.Pie` / `Chart.Donut`). These + * are a Teams host extension: they render in Teams clients whose app manifest + * opts into chart support; other Adaptive Card hosts ignore the unknown + * element. Data points clamp and labels/title truncate to the budget. + */ +function renderChart(node: BotNode): CardElement { + const props = node.props ?? {}; + const type = String(props.type ?? "verticalBar"); + const title = + props.title != null && String(props.title).length > 0 + ? truncateText(String(props.title), TEAMS_LIMITS.chartTitle) + : undefined; + + const rawData = Array.isArray(props.data) + ? (props.data as { label?: unknown; value?: unknown }[]) + : []; + const { items } = clampArray(rawData, TEAMS_LIMITS.chartDataPoints); + const points = items.map((p) => ({ + label: truncateText(String(p?.label ?? ""), TEAMS_LIMITS.chartLabel), + value: Number.isFinite(Number(p?.value)) ? Number(p?.value) : 0, + })); + + // Fields shared by every chart kind. `showTitle` is meaningless without a + // title; `maxWidth` keeps the chart from stretching the whole card. + const common: CardElement = { maxWidth: "520px" }; + if (title !== undefined) { + common.title = title; + common.showTitle = true; + } + // Axis titles apply to the cartesian charts (bar/line), not pie/donut. + const withAxes = (el: CardElement): CardElement => { + if (props.xAxisTitle != null) el.xAxisTitle = String(props.xAxisTitle); + if (props.yAxisTitle != null) el.yAxisTitle = String(props.yAxisTitle); + return el; + }; + const xy = points.map((p) => ({ x: p.label, y: p.value })); + const slices = points.map((p) => ({ legend: p.label, value: p.value })); + + switch (type) { + case "horizontalBar": + return withAxes({ ...common, type: "Chart.HorizontalBar", data: xy }); + case "line": + return withAxes({ + ...common, + type: "Chart.Line", + data: [{ legend: title ?? "", values: xy }], + }); + case "pie": + return { ...common, type: "Chart.Pie", data: slices }; + case "donut": + return { ...common, type: "Chart.Donut", data: slices }; + default: + // verticalBar — also the fallback for any unrecognized type. + return withAxes({ + ...common, + type: "Chart.VerticalBar", + showBarValues: true, + data: xy, + }); + } +} + +/** When no explicit `columns` are given, size the grid to the widest row. */ +function inferColumns(rowNodes: BotNode[]): { align?: undefined }[] { + let widest = 0; + for (const r of rowNodes) { + const n = childNodes(r).filter((c) => c.type === "cell").length; + if (n > widest) widest = n; + } + return Array.from( + { length: Math.min(widest, TEAMS_LIMITS.tableColumns) }, + () => ({}), + ); +} + +function capitalize(s: string): string { + return s.charAt(0).toUpperCase() + s.slice(1); +} + +/** Extract `{ id }` stamped onto an event prop by the action registry, if present. */ +function idFromHandler(handler: unknown): string | undefined { + if (handler && typeof handler === "object" && "id" in handler) { + const id = (handler as { id?: unknown }).id; + if (typeof id === "string") return id; + } + return undefined; +} + +/** The expanded `children` of an IR node as a `BotNode[]` (empty if none). */ +function childNodes(node: BotNode): BotNode[] { + const children = node.props?.children; + if (Array.isArray(children)) return children as BotNode[]; + if ( + children && + typeof children === "object" && + "type" in (children as object) + ) { + return [children as BotNode]; + } + return []; +} + +/** Concatenate the `value` of all descendant `text` nodes (depth-first). */ +function collectText(node: BotNode): string { + if (typeof node.type === "string" && node.type === "text") { + return String(node.props?.value ?? ""); + } + let acc = ""; + for (const child of childNodes(node)) acc += collectText(child); + return acc; +} + +/** + * Does this IR collapse to plain text (no structural or interactive elements)? + * Such replies are sent as a normal Teams text activity rather than wrapped in + * an Adaptive Card. A bare `Echo: hi` shouldn't render as a card. + */ +export function isPlainText(ir: BotNode[]): boolean { + const RICH = new Set([ + "header", + "fields", + "field", + "table", + "row", + "cell", + "chart", + "image", + "actions", + "button", + "select", + "input", + "divider", + "context", + ]); + const visit = (node: BotNode): boolean => { + if (typeof node.type === "string" && RICH.has(node.type)) return false; + return childNodes(node).every(visit); + }; + return ir.every(visit); +} + +/** Plain-text projection of an IR tree (depth-first text, blocks joined). */ +export function collectPlainText(ir: BotNode[]): string { + return ir + .map((n) => collectText(n)) + .filter((s) => s.length > 0) + .join("\n\n") + .trim(); +} diff --git a/packages/bot-teams/src/render/auto-close.test.ts b/packages/bot-teams/src/render/auto-close.test.ts new file mode 100644 index 00000000000..425c11b4538 --- /dev/null +++ b/packages/bot-teams/src/render/auto-close.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect } from "vitest"; +import { autoCloseOpenMarkdown as ac } from "./auto-close.js"; + +describe("autoCloseOpenMarkdown", () => { + it("leaves empty and already-balanced text untouched (no synthetic closers)", () => { + expect(ac("")).toBe(""); + expect(ac("plain text")).toBe("plain text"); + expect(ac("**bold** and _italic_")).toBe("**bold** and _italic_"); + expect(ac("```js\nconst x = 1;\n```")).toBe("```js\nconst x = 1;\n```"); + }); + + it("closes an unclosed bold/italic/strike that has content", () => { + expect(ac("**bold")).toBe("**bold**"); + expect(ac("_italic")).toBe("_italic_"); + expect(ac("~~strike")).toBe("~~strike~~"); + }); + + it("closes nested markers innermost-first", () => { + expect(ac("**bold _italic")).toBe("**bold _italic_**"); + }); + + it("inserts closers before trailing whitespace", () => { + expect(ac("**bold ")).toBe("**bold** "); + }); + + it("does NOT close a marker with no content after it (avoids transient ****)", () => { + expect(ac("hello **")).toBe("hello **"); + expect(ac("text _")).toBe("text _"); + }); + + it("closes an open inline code span with content, but not a bare opener", () => { + expect(ac("run `npm test")).toBe("run `npm test`"); + expect(ac("a paired `span` then more")).toBe("a paired `span` then more"); + }); + + it("closes an open fence only once there's code past the language line", () => { + expect(ac("```js")).toBe("```js"); // still on the language line + expect(ac("```js\n")).toBe("```js\n"); // language line, no code yet + expect(ac("```js\nconst x = 1;")).toBe("```js\nconst x = 1;\n```"); + expect(ac("```\nplain code")).toBe("```\nplain code\n```"); + }); + + it("does not corrupt digits adjacent to balanced regions (sentinel safety)", () => { + // Regression guard: the placeholder sentinels must be real PUA codepoints, + // not empty strings, otherwise the restore regex would eat bare digits. + expect(ac("```\ncode\n```\n12345 items")).toBe( + "```\ncode\n```\n12345 items", + ); + expect(ac("see `x` then 2024 and 99")).toBe("see `x` then 2024 and 99"); + }); + + it("is idempotent once the real closer arrives", () => { + // mid-stream we'd have closed it; the finalized balanced text adds nothing. + expect(ac("**done**")).toBe("**done**"); + expect(ac(ac("**partial"))).toBe(ac("**partial")); + }); +}); diff --git a/packages/bot-teams/src/render/auto-close.ts b/packages/bot-teams/src/render/auto-close.ts new file mode 100644 index 00000000000..805b92d5d97 --- /dev/null +++ b/packages/bot-teams/src/render/auto-close.ts @@ -0,0 +1,163 @@ +/** + * Streaming-polish layer for Teams' post-then-edit streaming. + * + * While an agent is mid-stream the buffer is usually an *unfinished* markdown + * document: an open code fence, an unclosed `**`, etc. Teams renders message + * text as markdown, so editing the message with unbalanced text makes the rest + * of the bubble render broken (a stray ``` turns the remainder into a code + * block until the closer arrives, a dangling `**` bolds the rest, and so on). + * + * `autoCloseOpenMarkdown` returns a balanced copy of the buffer by appending the + * minimum set of closers needed for well-formed display. When the agent later + * emits the real closer the buffer balances on its own and this adds nothing, + * so the *committed* (finalized) message has no synthetic closers. + * + * Handled, in priority order: + * 1. Unclosed fenced code blocks ``` (most severe: leaks code styling) + * 2. Unclosed inline code + * 3. Unclosed bold `**` / `__`, italic `*` / `_`, strike `~~` + * + * Heuristics: + * - A marker with no content after it (trailing `**`) is NOT closed. The + * agent may just be opening it; closing would flash a transient `****`. + * - Markers inside code/fence regions don't count. + * - Closers are emitted innermost-first so the structure nests correctly. + */ + +// Private-use codepoints that will never appear in agent markdown, used to +// stash already-balanced regions while we scan for the dangling opener. +const SENTINEL_FENCE = "\uE000"; +const SENTINEL_INLINE = "\uE001"; + +export function autoCloseOpenMarkdown(text: string): string { + if (!text) return text; + + // ── 1. Fenced code blocks ────────────────────────────────────────── + const fences: string[] = []; + let work = text.replace(/```[\s\S]*?```/g, (m) => { + fences.push(m); + return `${SENTINEL_FENCE}${fences.length - 1}${SENTINEL_FENCE}`; + }); + + const openFenceIdx = work.indexOf("```"); + let openFenceTail = ""; + if (openFenceIdx >= 0) { + openFenceTail = work.slice(openFenceIdx); + work = work.slice(0, openFenceIdx); + } + + // ── 2. Inline code regions ───────────────────────────────────────── + const inlines: string[] = []; + work = work.replace(/`[^`\n]+`/g, (m) => { + inlines.push(m); + return `${SENTINEL_INLINE}${inlines.length - 1}${SENTINEL_INLINE}`; + }); + + const openBacktickIdx = work.indexOf("`"); + let openBacktickTail = ""; + if (openBacktickIdx >= 0) { + openBacktickTail = work.slice(openBacktickIdx); + work = work.slice(0, openBacktickIdx); + } + + // ── 3. Bold / italic / strike via stack scan ────────────────────── + const stack = scanBracketStack(work); + const closers = stack.slice().toReversed().join(""); + + // ── 4. Reassemble ───────────────────────────────────────────────── + // Insert closers BEFORE trailing whitespace so "**bold " → "**bold**". + let output = work; + if (closers) { + const trail = output.match(/\s*$/)?.[0] ?? ""; + output = output.slice(0, output.length - trail.length) + closers + trail; + } + + if (openBacktickTail) { + output += openBacktickTail; + if (hasContentAfterMarker(openBacktickTail, "`")) output += "`"; + } + + output = output.replace( + new RegExp(`${SENTINEL_INLINE}(\\d+)${SENTINEL_INLINE}`, "g"), + (_, idx) => inlines[Number(idx)] ?? "", + ); + + if (openFenceTail) { + output += openFenceTail; + if (hasFenceCodeContent(openFenceTail)) { + output += openFenceTail.endsWith("\n") ? "```" : "\n```"; + } + } + + output = output.replace( + new RegExp(`${SENTINEL_FENCE}(\\d+)${SENTINEL_FENCE}`, "g"), + (_, idx) => fences[Number(idx)] ?? "", + ); + + return output; +} + +/** + * Walk `text` and return the unbalanced bracket stack. Recognises (longest + * first) `**`, `__`, `~~`, then `*`, `_`, so `**` is one bold marker, not two + * italics. A trailing marker with no content after it is dropped (closing it + * would flash a transient `****`). + */ +function scanBracketStack(text: string): string[] { + const stack: string[] = []; + const tryToggle = (m: string) => { + if (stack[stack.length - 1] === m) stack.pop(); + else stack.push(m); + }; + + let i = 0; + while (i < text.length) { + if (text.startsWith("**", i)) { + tryToggle("**"); + i += 2; + } else if (text.startsWith("__", i)) { + tryToggle("__"); + i += 2; + } else if (text.startsWith("~~", i)) { + tryToggle("~~"); + i += 2; + } else if (text[i] === "*") { + tryToggle("*"); + i += 1; + } else if (text[i] === "_") { + tryToggle("_"); + i += 1; + } else { + i += 1; + } + } + + while (stack.length > 0) { + const last = stack[stack.length - 1]!; + const lastIdx = text.lastIndexOf(last); + if (lastIdx < 0) break; + const after = text.slice(lastIdx + last.length); + if (/^\s*$/.test(after)) stack.pop(); + else break; + } + + return stack; +} + +function hasContentAfterMarker(tail: string, marker: string): boolean { + if (!tail.startsWith(marker)) return false; + return /\S/.test(tail.slice(marker.length)); +} + +/** + * True if a ```` ```?\n ```` buffer has actual code past the + * optional language line (the first newline separates the language tag from the + * code body). A buffer still on the language line has nothing to close yet. + */ +function hasFenceCodeContent(tail: string): boolean { + if (!tail.startsWith("```")) return false; + const after = tail.slice(3); + const nl = after.indexOf("\n"); + if (nl < 0) return false; + return /\S/.test(after.slice(nl + 1)); +} diff --git a/packages/bot-teams/src/render/budget.test.ts b/packages/bot-teams/src/render/budget.test.ts new file mode 100644 index 00000000000..35b68202bea --- /dev/null +++ b/packages/bot-teams/src/render/budget.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from "vitest"; +import { truncateText, clampArray, TEAMS_LIMITS } from "./budget.js"; + +describe("truncateText", () => { + it("leaves short text unchanged", () => { + expect(truncateText("hi", 10)).toBe("hi"); + }); + it("truncates with an ellipsis and never exceeds max", () => { + const out = truncateText("abcdef", 4); + expect(out).toBe("abc…"); + expect(out.length).toBe(4); + }); +}); + +describe("clampArray", () => { + it("keeps everything under the cap", () => { + expect(clampArray([1, 2], 5)).toEqual({ items: [1, 2], overflow: 0 }); + }); + it("clamps and reports overflow", () => { + expect(clampArray([1, 2, 3, 4], 2)).toEqual({ items: [1, 2], overflow: 2 }); + }); +}); + +describe("TEAMS_LIMITS", () => { + it("caps top-level actions to a Teams-friendly count", () => { + expect(TEAMS_LIMITS.actions).toBe(6); + }); +}); diff --git a/packages/bot-teams/src/render/budget.ts b/packages/bot-teams/src/render/budget.ts new file mode 100644 index 00000000000..5144d167341 --- /dev/null +++ b/packages/bot-teams/src/render/budget.ts @@ -0,0 +1,46 @@ +/** + * Adaptive Card payload limits for Teams. + * + * Teams caps an Adaptive Card attachment at ~28 KB of JSON and renders only a + * handful of top-level actions comfortably. These ceilings keep a rendered card + * inside those bounds; the renderer clamps collections and truncates text to + * them rather than emitting an oversized card Teams would reject. + */ +export const TEAMS_LIMITS = { + /** Top-level body elements (TextBlocks, FactSets, Tables, etc.) per card. */ + bodyElements: 100, + /** Top-level `Action.Submit`s. Teams shows ~6 before overflowing. */ + actions: 6, + /** Characters of text in a single TextBlock. */ + textBlock: 12000, + factTitle: 200, + factValue: 2000, + factsPerSet: 50, + buttonText: 256, + tableColumns: 12, + tableRows: 100, + cellText: 2000, + choices: 100, + choiceLabel: 256, + /** Data points (categories / slices) in a single chart. */ + chartDataPoints: 50, + /** Characters of a chart title or a data point's label. */ + chartTitle: 200, + chartLabel: 200, +} as const; + +/** Truncate to `max` chars, appending an ellipsis if the input was longer. Never returns >max. */ +export function truncateText(text: string, max: number): string { + if (text.length <= max) return text; + if (max <= 1) return text.slice(0, max); + return text.slice(0, max - 1) + "…"; +} + +/** Clamp an array to `max` items; return the kept items plus how many overflowed. */ +export function clampArray( + items: readonly T[], + max: number, +): { items: T[]; overflow: number } { + if (items.length <= max) return { items: [...items], overflow: 0 }; + return { items: items.slice(0, max), overflow: items.length - max }; +} diff --git a/packages/bot-teams/src/render/markdown.test.ts b/packages/bot-teams/src/render/markdown.test.ts new file mode 100644 index 00000000000..c897d9ad556 --- /dev/null +++ b/packages/bot-teams/src/render/markdown.test.ts @@ -0,0 +1,69 @@ +import { describe, it, expect } from "vitest"; +import type { BotNode } from "@copilotkit/bot-ui"; +import { renderTeamsMarkdown } from "./markdown.js"; + +const text = (value: string): BotNode => ({ type: "text", props: { value } }); + +describe("renderTeamsMarkdown", () => { + it("renders a bare text node", () => { + expect(renderTeamsMarkdown([text("Echo: hi")])).toBe("Echo: hi"); + }); + + it("renders a header as bold", () => { + const ir: BotNode[] = [ + { type: "header", props: { children: [text("Title")] } }, + ]; + expect(renderTeamsMarkdown(ir)).toBe("**Title**"); + }); + + it("renders a divider as a rule", () => { + expect(renderTeamsMarkdown([{ type: "divider", props: {} }])).toBe("---"); + }); + + it("joins a message container's children with blank lines", () => { + const ir: BotNode[] = [ + { + type: "message", + props: { + children: [ + { type: "header", props: { children: [text("Status")] } }, + { type: "section", props: { children: [text("All good.")] } }, + ], + }, + }, + ]; + expect(renderTeamsMarkdown(ir)).toBe("**Status**\n\nAll good."); + }); + + it("renders context children as emphasized lines", () => { + const ir: BotNode[] = [ + { type: "context", props: { children: [text("fyi")] } }, + ]; + expect(renderTeamsMarkdown(ir)).toBe("_fyi_"); + }); + + it("returns an empty string for an empty tree", () => { + expect(renderTeamsMarkdown([])).toBe(""); + }); + + it("renders a
as a GFM pipe-table fallback", () => { + const cell = (v: string): BotNode => ({ + type: "cell", + props: { children: [text(v)] }, + }); + const ir: BotNode[] = [ + { + type: "table", + props: { + columns: [{ header: "Name" }, { header: "Count", align: "right" }], + children: [ + { type: "row", props: { children: [cell("Bugs"), cell("3")] } }, + ], + }, + }, + ]; + expect(renderTeamsMarkdown(ir)).toBe( + "| Name | Count |\n| --- | ---: |\n| Bugs | 3 |", + ); + }); +}); diff --git a/packages/bot-teams/src/render/markdown.ts b/packages/bot-teams/src/render/markdown.ts new file mode 100644 index 00000000000..2f816b64382 --- /dev/null +++ b/packages/bot-teams/src/render/markdown.ts @@ -0,0 +1,134 @@ +import type { BotNode } from "@copilotkit/bot-ui"; + +/** + * Render the bot-ui IR tree to a Teams message string. + * + * Teams message activities render Markdown, so the structural vocabulary maps + * cleanly onto it: a `
` becomes a bold line, `
`/`` + * pass their text through, `` becomes a rule, and so on. This is the + * thin, text-first renderer that covers plain replies and the common card + * shapes; richer interactive surfaces (buttons, inputs) will render to + * Adaptive Cards in a follow-up (see the package README). + */ +export function renderTeamsMarkdown(ir: BotNode[]): string { + return ir + .map((node) => renderNode(node)) + .filter((s) => s.length > 0) + .join("\n\n") + .trim(); +} + +function renderNode(node: BotNode): string { + if (typeof node.type !== "string") { + // Components are expanded to intrinsic nodes before render(); a stray + // function/symbol node carries no renderable text. + return collectText(node); + } + + switch (node.type) { + case "text": + return String(node.props.value ?? ""); + case "header": + return `**${collectText(node)}**`; + case "divider": + return "---"; + case "context": + // Supplementary, lower-emphasis text. + return collectText(node) + .split("\n") + .map((line) => (line ? `_${line}_` : line)) + .join("\n"); + case "field": + return collectText(node); + case "fields": + return childNodes(node) + .map((c) => renderNode(c)) + .filter(Boolean) + .join("\n"); + case "button": + // No interactive surface yet, so render the label so the intent is visible. + return `\`${collectText(node)}\``; + case "table": + return renderTable(node); + case "message": + case "section": + case "markdown": + case "actions": + default: + // Containers and unknown nodes: render children, falling back to any + // direct text. + return renderChildren(node) || collectText(node); + } +} + +/** + * Markdown-table fallback for `
` (the Adaptive Card renderer emits a + * native Table; this is the text-surface fallback). Teams renders GFM pipe + * tables in message text. + */ +function renderTable(node: BotNode): string { + const columns = node.props?.columns as + | { header: string; align?: "left" | "center" | "right" }[] + | undefined; + const rowNodes = childNodes(node).filter((c) => c.type === "row"); + const dataRows = rowNodes.map((r) => + childNodes(r) + .filter((c) => c.type === "cell") + .map((c) => collectText(c).replace(/\|/g, "\\|")), + ); + + const width = + columns?.length ?? dataRows.reduce((m, r) => Math.max(m, r.length), 0); + if (width === 0) return ""; + + const headers = columns + ? columns.map((c) => c.header) + : Array.from({ length: width }, () => " "); + const sep = (columns ?? Array.from({ length: width })).map((c) => { + const align = (c as { align?: string } | undefined)?.align; + if (align === "center") return ":---:"; + if (align === "right") return "---:"; + return "---"; + }); + + const line = (cells: string[]): string => + `| ${Array.from({ length: width }, (_, i) => cells[i] ?? "").join(" | ")} |`; + + return [line(headers), `| ${sep.join(" | ")} |`, ...dataRows.map(line)].join( + "\n", + ); +} + +function renderChildren(node: BotNode): string { + const kids = childNodes(node); + if (kids.length === 0) return ""; + return kids + .map((c) => renderNode(c)) + .filter((s) => s.length > 0) + .join("\n\n"); +} + +/** Normalize a node's `children` prop to an array of child nodes. */ +function childNodes(node: BotNode): BotNode[] { + const children = node.props?.children; + if (Array.isArray(children)) return children as BotNode[]; + if (children && typeof children === "object" && "type" in children) { + return [children as BotNode]; + } + return []; +} + +/** Depth-first collection of a node's descendant text. */ +function collectText(node: BotNode): string { + const out: string[] = []; + const visit = (n: BotNode): void => { + if (typeof n.type === "string" && n.type === "text") { + const value = n.props?.value; + if (value != null) out.push(String(value)); + return; + } + for (const child of childNodes(n)) visit(child); + }; + visit(node); + return out.join(" ").replace(/\s+/g, " ").trim(); +} diff --git a/packages/bot-teams/src/sanitizing-http-agent.ts b/packages/bot-teams/src/sanitizing-http-agent.ts new file mode 100644 index 00000000000..8b6eeb27f5a --- /dev/null +++ b/packages/bot-teams/src/sanitizing-http-agent.ts @@ -0,0 +1,67 @@ +import { HttpAgent, parseSSEStream, runHttpRequest } from "@ag-ui/client"; +import type { BaseEvent, RunAgentInput } from "@ag-ui/core"; +import { map } from "rxjs"; +import type { Observable } from "rxjs"; + +/** + * An `HttpAgent` that tolerates the AG-UI event streams real agents emit. + * + * `@ag-ui/client`'s stock transform re-validates every streamed event against a + * strict Zod schema. Some events that `@ag-ui/langgraph` legitimately emits fail + * it (notably a `TOOL_CALL_START` whose `parentMessageId` is `null`: "Expected + * string, received null"), and a single rejected event aborts the entire run. + * That breaks LangGraph interrupts / human-in-the-loop on Teams, where the tool + * call that triggers the interrupt carries exactly that shape. + * + * The bridge talks to a trusted runtime, so rather than re-validate its output + * we use the same SSE parse the stock path wraps (`parseSSEStream`) and coerce + * the known nullable-string fields. This deliberately drops the stock + * transform's *entire* strict Zod re-validation step (not just the offending + * field). This is acceptable only because the runtime is trusted and `runHttpRequest` + * still throws on transport/HTTP errors. The first coercion logs a one-time + * breadcrumb so the workaround is visible in production. Revert to the stock + * transform (`transformHttpEventStream`) once upstream makes the fields + * nullable. + * + * Use it in place of `HttpAgent` when pointing the Teams bot at a LangGraph + * (or other AG-UI) agent: + * + * ```ts + * import { SanitizingHttpAgent } from "@copilotkit/bot-teams"; + * const agent = new SanitizingHttpAgent({ url: process.env.AGENT_URL! }); + * ``` + */ +export class SanitizingHttpAgent extends HttpAgent { + run(input: RunAgentInput): Observable { + return parseSSEStream( + runHttpRequest(() => this.fetch(this.url, this.requestInit(input))), + this.debugLogger, + ).pipe(map((event: unknown) => coerceNullStrings(event) as BaseEvent)); + } +} + +/** One-time breadcrumb so the workaround's use is visible in production. */ +let coercionWarned = false; + +/** + * Coerce known nullable-string event fields to `""`. Targeted on purpose: we + * only touch fields where a `null` is known to come through from + * `@ag-ui/langgraph` and would otherwise trip a downstream string check. + */ +function coerceNullStrings(event: unknown): unknown { + if (!event || typeof event !== "object") return event; + const e = event as Record; + if (e["parentMessageId"] === null) { + e["parentMessageId"] = ""; + if (!coercionWarned) { + coercionWarned = true; + console.warn( + '[SanitizingHttpAgent] coerced a null `parentMessageId` to "" and ' + + "bypassed @ag-ui/client strict event re-validation for this stream " + + "(known @ag-ui/langgraph quirk). Remove this agent once upstream " + + "makes the field nullable.", + ); + } + } + return e; +} diff --git a/packages/bot-teams/src/types.ts b/packages/bot-teams/src/types.ts new file mode 100644 index 00000000000..92b4b4e1033 --- /dev/null +++ b/packages/bot-teams/src/types.ts @@ -0,0 +1,55 @@ +import type { TurnContext } from "@microsoft/agents-hosting"; +import type { ConversationReference } from "@microsoft/agents-activity"; +import type { FileDeliveryConfig } from "./download-files.js"; + +/** + * Stable per-conversation key. Teams gives every conversation (1:1 chat, group + * chat, or channel) a durable `conversation.id`, which is exactly the grain we + * want: one agent session per conversation. + */ +export type ConversationKey = string; + +/** + * Where a reply goes. + * + * Replies sent *inside* the originating turn use the live {@link TurnContext} + * (the simplest path, and the one the M365 Agents Playground exercises). The + * {@link ConversationReference} is captured alongside it so the same target can + * later drive proactive (out-of-turn) sends via + * `CloudAdapter.continueConversation`. + */ +export interface TeamsReplyTarget { + conversationKey: ConversationKey; + reference: Partial; + /** Live turn context, present while replying within the originating activity. */ + context?: TurnContext; +} + +export interface TeamsAdapterOptions { + /** + * Port for the bot's `POST /api/messages` endpoint. Defaults to `3978`, the + * endpoint the M365 Agents Playground connects to. + */ + port?: number; + /** + * Microsoft app (client) id. Omit for anonymous local development with the + * M365 Agents Playground; required to talk to real Teams via Azure Bot + * Service. Falls back to the `clientId` env var. + */ + clientId?: string; + /** Microsoft client secret. Omit for anonymous local dev. Falls back to `clientSecret`. */ + clientSecret?: string; + /** Microsoft tenant (directory) id. Omit for multi-tenant / anonymous. Falls back to `tenantId`. */ + tenantId?: string; + /** + * Custom-event names treated as interrupts by the run renderer (captured for + * an `onInterrupt` handler). Defaults to `on_interrupt`, the name + * LangGraph's AG-UI adapter emits. + */ + interruptEventNames?: ReadonlySet; + /** + * Tunables for inbound file handling (size/count caps applied when a user + * uploads files). Defaults are sane; override only to widen or tighten them. + */ + files?: FileDeliveryConfig; +} diff --git a/packages/bot-teams/tsconfig.check.json b/packages/bot-teams/tsconfig.check.json new file mode 100644 index 00000000000..c4cbc877d12 --- /dev/null +++ b/packages/bot-teams/tsconfig.check.json @@ -0,0 +1,10 @@ +{ + "extends": "@copilotkit/typescript-config/base.json", + "compilerOptions": { + "noEmit": true, + "lib": ["es2023", "dom"], + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/bot-teams/tsconfig.json b/packages/bot-teams/tsconfig.json new file mode 100644 index 00000000000..2555b7388da --- /dev/null +++ b/packages/bot-teams/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@copilotkit/typescript-config/base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "lib": ["es2023"] + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules", "examples"] +} diff --git a/packages/bot-teams/vitest.config.ts b/packages/bot-teams/vitest.config.ts new file mode 100644 index 00000000000..ae847ff6d9c --- /dev/null +++ b/packages/bot-teams/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/**/*.test.ts"], + }, +}); diff --git a/packages/bot-ui/src/components.test.tsx b/packages/bot-ui/src/components.test.tsx index 344814e0402..5888ae9428b 100644 --- a/packages/bot-ui/src/components.test.tsx +++ b/packages/bot-ui/src/components.test.tsx @@ -13,6 +13,7 @@ import { Cell, Image, Select, + Chart, } from "./components.js"; /** @@ -36,6 +37,10 @@ const __typeGuards = () => { ; // @ts-expect-error Select.options is required
({ header }))}> + {table.rows.map((row, i) => ( + + {row.map((cell, j) => ( + {cell} + ))} + + ))} +
+ ) : null} + , + ); + return "Displayed the card to the user. Give a one-line confirmation; do not restate the card's contents."; + }, +}); + +const bot = createBot({ + adapters: [teams({ port })], + agent: (threadId: string) => { + const agent = new SanitizingHttpAgent({ url: runtimeAgentUrl }); + agent.threadId = threadId; + return agent; + }, + tools: [showCard, renderChartTool, ...hitlTools], +}); + +// Run the agent on every message. It streams text by edit and renders Adaptive +// Cards on its own via the show_card tool. Uploaded files (e.g. a CSV) are +// recorded into the conversation transcript by the adapter — including their +// decoded contents — so `runAgent()` picks them up from the seeded history with +// no extra wiring, and they persist for follow-up turns. +bot.onMessage(async ({ thread, message }) => { + // A bare file upload with no accompanying text should still do something + // useful. The adapter only sets `contentParts` when it actually read file + // content, so this nudges the agent to act on a dropped-in CSV instead of + // running on an empty prompt and asking "what would you like me to do?". + const hasFile = (message.contentParts?.length ?? 0) > 0; + if (hasFile && message.text.trim().length === 0) { + await thread.runAgent({ + prompt: + "I uploaded a file with no other instructions. If it contains " + + "tabular or numeric data, chart it with render_chart (pick a sensible " + + "chart type); otherwise give me a short summary of what's in it.", + }); + return; + } + await thread.runAgent(); +}); + +await bot.start(); + +console.log( + `Teams demo bot listening at http://localhost:${port}/api/messages`, +); +console.log( + 'Run `pnpm playground`, then ask for a "summary" or "status" to see an ' + + "auto-rendered card, upload a CSV and ask for a chart to see render_chart, " + + 'or "announce X to the team" to see the HITL approval.', +); + +// Stop the bot cleanly on exit. +const shutdown = async (signal: string): Promise => { + console.log(`\nReceived ${signal}, stopping…`); + await bot.stop().catch(() => {}); + process.exit(0); +}; +process.on("SIGINT", () => void shutdown("SIGINT")); +process.on("SIGTERM", () => void shutdown("SIGTERM")); diff --git a/examples/teams/app/tools/render-chart.tsx b/examples/teams/app/tools/render-chart.tsx new file mode 100644 index 00000000000..112709efbf8 --- /dev/null +++ b/examples/teams/app/tools/render-chart.tsx @@ -0,0 +1,68 @@ +/** + * `render_chart` — the agent describes the data it wants to visualize and we + * render it as a **native Teams chart** (an Adaptive Card chart element), no + * image generation involved. This is the "upload a CSV → get a chart" payoff: + * the agent parses the data (the CSV arrives as readable text via the adapter's + * inbound-file handling), then calls this with a simple `{label, value}` series. + * + * Native charts render right inside the card, so there's no headless browser, + * no Chromium, and no PNG upload, so the bot stays a pure Node service. + */ +import { z } from "zod"; +import { defineBotTool } from "@copilotkit/bot"; +import { Message, Chart } from "@copilotkit/bot-ui"; + +const schema = z.object({ + chartType: z + .enum(["verticalBar", "horizontalBar", "line", "pie", "donut"]) + .optional() + .describe( + "Chart kind. Defaults to 'verticalBar'. Use 'line' for trends over " + + "time, 'pie'/'donut' for parts of a whole, 'horizontalBar' for ranked " + + "categories with long labels.", + ), + title: z.string().describe("Short title shown above the chart."), + xAxisTitle: z + .string() + .optional() + .describe("X-axis label (bar/line charts only)."), + yAxisTitle: z + .string() + .optional() + .describe("Y-axis label (bar/line charts only)."), + data: z + .array( + z.object({ + label: z + .string() + .describe("Category / x value, e.g. '2026-01' or 'Sev1'."), + value: z.number().describe("Numeric value for this category."), + }), + ) + .min(1) + .describe("The data to plot — one entry per category."), +}); + +export const renderChartTool = defineBotTool({ + name: "render_chart", + description: + "Render a native chart in the conversation. Provide a chartType, a title, " + + "and a `data` array of {label, value} points (inline the actual numbers). " + + "Use this to visualize data — e.g. after analyzing an uploaded CSV. The " + + "chart renders inline in Teams.", + parameters: schema, + async handler({ chartType, title, xAxisTitle, yAxisTitle, data }, ctx) { + await ctx.thread.post( + + + , + ); + return "Rendered and posted the chart. Give a one-line confirmation; do not restate the chart's data in prose."; + }, +}); diff --git a/examples/teams/appPackage/README.md b/examples/teams/appPackage/README.md new file mode 100644 index 00000000000..fb784aa96fb --- /dev/null +++ b/examples/teams/appPackage/README.md @@ -0,0 +1,38 @@ +# Teams app package + +The Teams app manifest + icons you sideload into Microsoft Teams to install the +bot. Build the `.zip` with one command, then upload it in **Teams → Apps → +Manage your apps → Upload a custom app**. + +## Build it + +From `examples/teams`: + +```sh +pnpm package +``` + +This validates everything Teams needs and writes `appPackage/appPackage.zip`: + +- **Bot id:** read from `MICROSOFT_APP_ID` / `CLIENT_ID` / `clientId` (env or + `examples/teams/.env`) and injected into `manifest.json`'s `bots[0].botId`, so + the committed manifest stays a placeholder and you never hardcode your id. Must + be the **Application (client) ID** (a GUID) of the Entra app bound to your + Azure Bot. +- **Icons:** `color.png` (192×192) and `outline.png` (32×32). Auto-generated as + CopilotKit-purple placeholders if missing; drop in your own PNGs of those exact + sizes to brand it. +- **Manifest:** checked for valid JSON and the required bot fields. + +If something's missing the script tells you exactly what and how to fix it. + +## What's here + +- `manifest.json`: the app manifest template (`botId` is a placeholder; the + build injects the real one). Edit `developer` / `name` / `description` to taste. +- `color.png` / `outline.png`: placeholder icons (regenerated if deleted). +- `package.mjs`: the dependency-free build script (`pnpm package`). +- `appPackage.zip`: the build output (gitignored). + +The bot only **replies** once your hosted endpoint is set as the Azure Bot +**messaging endpoint**. Installing the package just registers the bot in Teams. diff --git a/examples/teams/appPackage/color.png b/examples/teams/appPackage/color.png new file mode 100644 index 0000000000000000000000000000000000000000..25a443a246ad02e39095313317ba6b29e4c98efc GIT binary patch literal 450 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE4M+yv$zf+;VC?jCaSW-r_4cwMF9QR|0fU+% z1@Xv)rhYlo4Ji|^GRL=aNHFj)urV+ examples/teams/appPackage/appPackage.zip + */ +import { readFileSync, writeFileSync, existsSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +import { deflateSync } from "node:zlib"; + +const here = dirname(fileURLToPath(import.meta.url)); +const p = (name) => join(here, name); + +const PURPLE = [91, 95, 199, 255]; // #5B5FC7 + +const GUID_RE = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +const fail = (msg, hint) => { + console.error(`\n❌ ${msg}`); + if (hint) console.error(` ${hint}`); + process.exit(1); +}; + +// ---------------------------------------------------------------- CRC-32 +const CRC_TABLE = (() => { + const t = new Uint32Array(256); + for (let n = 0; n < 256; n++) { + let c = n; + for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; + t[n] = c >>> 0; + } + return t; +})(); +function crc32(buf) { + let c = 0xffffffff; + for (let i = 0; i < buf.length; i++) + c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8); + return (c ^ 0xffffffff) >>> 0; +} + +// ------------------------------------------------------------ PNG writer +function pngChunk(type, data) { + const len = Buffer.alloc(4); + len.writeUInt32BE(data.length, 0); + const typeBuf = Buffer.from(type, "ascii"); + const body = Buffer.concat([typeBuf, data]); + const crc = Buffer.alloc(4); + crc.writeUInt32BE(crc32(body), 0); + return Buffer.concat([len, body, crc]); +} +/** Write an 8-bit RGBA PNG. `pixel(x,y) -> [r,g,b,a]`. */ +function writePng(path, w, h, pixel) { + const raw = Buffer.alloc((w * 4 + 1) * h); + let o = 0; + for (let y = 0; y < h; y++) { + raw[o++] = 0; // filter: none + for (let x = 0; x < w; x++) { + const [r, g, b, a] = pixel(x, y); + raw[o++] = r; + raw[o++] = g; + raw[o++] = b; + raw[o++] = a; + } + } + const ihdr = Buffer.alloc(13); + ihdr.writeUInt32BE(w, 0); + ihdr.writeUInt32BE(h, 4); + ihdr[8] = 8; // bit depth + ihdr[9] = 6; // color type RGBA + const png = Buffer.concat([ + Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), + pngChunk("IHDR", ihdr), + pngChunk("IDAT", deflateSync(raw, { level: 9 })), + pngChunk("IEND", Buffer.alloc(0)), + ]); + writeFileSync(path, png); +} +/** Read a PNG's pixel dimensions from its IHDR (no decode). */ +function pngDimensions(buf) { + const sig = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; + if (buf.length < 24 || !sig.every((b, i) => buf[i] === b)) return undefined; + return { w: buf.readUInt32BE(16), h: buf.readUInt32BE(20) }; +} + +// -------------------------------------------------------------- ZIP (stored) +function zip(entries) { + // Fixed 1980-01-01 timestamp (deterministic output). + const dosTime = 0; + const dosDate = 0x21; + const locals = []; + const centrals = []; + let offset = 0; + for (const { name, data } of entries) { + const nameBuf = Buffer.from(name, "utf8"); + const crc = crc32(data); + const local = Buffer.alloc(30); + local.writeUInt32LE(0x04034b50, 0); + local.writeUInt16LE(20, 4); // version needed + local.writeUInt16LE(0, 6); // flags + local.writeUInt16LE(0, 8); // method: stored + local.writeUInt16LE(dosTime, 10); + local.writeUInt16LE(dosDate, 12); + local.writeUInt32LE(crc, 14); + local.writeUInt32LE(data.length, 18); // compressed size + local.writeUInt32LE(data.length, 22); // uncompressed size + local.writeUInt16LE(nameBuf.length, 26); + local.writeUInt16LE(0, 28); + locals.push(local, nameBuf, data); + + const central = Buffer.alloc(46); + central.writeUInt32LE(0x02014b50, 0); + central.writeUInt16LE(20, 4); + central.writeUInt16LE(20, 6); + central.writeUInt16LE(0, 8); + central.writeUInt16LE(0, 10); + central.writeUInt16LE(dosTime, 12); + central.writeUInt16LE(dosDate, 14); + central.writeUInt32LE(crc, 16); + central.writeUInt32LE(data.length, 20); + central.writeUInt32LE(data.length, 24); + central.writeUInt16LE(nameBuf.length, 28); + central.writeUInt32LE(offset, 42); + centrals.push(central, nameBuf); + + offset += local.length + nameBuf.length + data.length; + } + const centralStart = offset; + const centralBuf = Buffer.concat(centrals); + const eocd = Buffer.alloc(22); + eocd.writeUInt32LE(0x06054b50, 0); + eocd.writeUInt16LE(entries.length, 8); + eocd.writeUInt16LE(entries.length, 10); + eocd.writeUInt32LE(centralBuf.length, 12); + eocd.writeUInt32LE(centralStart, 16); + return Buffer.concat([...locals, centralBuf, eocd]); +} + +// ----------------------------------------------------------- env resolution +function parseEnv(path) { + const out = {}; + if (!existsSync(path)) return out; + for (const line of readFileSync(path, "utf8").split("\n")) { + const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/); + if (m && !line.trimStart().startsWith("#")) out[m[1]] = m[2].trim(); + } + return out; +} + +function main() { + const env = { ...parseEnv(p("../.env")), ...process.env }; + const manifestPath = p("manifest.json"); + + if (!existsSync(manifestPath)) + fail("manifest.json not found next to this script."); + let manifest; + try { + manifest = JSON.parse(readFileSync(manifestPath, "utf8")); + } catch (e) { + return fail(`manifest.json is not valid JSON: ${e.message}`); + } + if (!manifest.bots?.[0]) { + fail( + "manifest.json has no bots[0] entry.", + "A Teams bot manifest needs a `bots` array.", + ); + } + + // Resolve the Microsoft App (bot) id: env wins; else a real id already in the + // manifest; else fail with guidance. + const placeholder = "REPLACE_WITH_MICROSOFT_APP_ID"; + const fromEnv = env.MICROSOFT_APP_ID || env.CLIENT_ID || env.clientId || ""; + const fromManifest = + manifest.bots[0].botId !== placeholder ? manifest.bots[0].botId : ""; + const botId = (fromEnv || fromManifest).trim(); + + if (!botId) { + fail( + "No Microsoft App id found.", + "Set MICROSOFT_APP_ID (or clientId) in examples/teams/.env. It's the\n" + + " Application (client) ID of the Entra app bound to your Azure Bot.", + ); + } + if (!GUID_RE.test(botId)) { + fail( + `Microsoft App id "${botId}" is not a GUID.`, + "Use the Application (client) ID (a GUID), not the secret or its id.", + ); + } + manifest.bots[0].botId = botId; + // RSC: webApplicationInfo.id must be the same Entra app (client) id so Teams + // can grant the resource-specific ChannelMessage.Read.Group permission to it. + if (manifest.webApplicationInfo) manifest.webApplicationInfo.id = botId; + + // Icons: validate, auto-generating CopilotKit-purple placeholders if missing. + const requireIcon = (name, size, make) => { + const path = p(name); + if (!existsSync(path)) { + make(path); + console.log(`• generated placeholder ${name} (${size}×${size})`); + return; + } + const dims = pngDimensions(readFileSync(path)); + if (!dims) fail(`${name} is not a valid PNG.`); + if (dims.w !== size || dims.h !== size) { + fail( + `${name} must be ${size}×${size}, found ${dims.w}×${dims.h}.`, + "Replace it with a correctly-sized PNG, or delete it to auto-generate one.", + ); + } + }; + requireIcon("color.png", 192, (path) => + writePng(path, 192, 192, () => PURPLE), + ); + requireIcon("outline.png", 32, (path) => { + const cx = 15.5; + const cy = 15.5; + const r = 14; + writePng(path, 32, 32, (x, y) => + (x - cx) ** 2 + (y - cy) ** 2 <= r * r + ? [255, 255, 255, 255] + : [0, 0, 0, 0], + ); + }); + + // Build the zip with the resolved manifest (manifest.json at the root). + const entries = [ + { + name: "manifest.json", + data: Buffer.from(JSON.stringify(manifest, null, 2) + "\n", "utf8"), + }, + { name: "color.png", data: readFileSync(p("color.png")) }, + { name: "outline.png", data: readFileSync(p("outline.png")) }, + ]; + const outPath = p("appPackage.zip"); + writeFileSync(outPath, zip(entries)); + + console.log(`\n✅ Built appPackage.zip (botId ${botId})`); + console.log( + " Upload in Teams → Apps → Manage your apps → Upload a custom app.", + ); + console.log(` ${outPath}\n`); +} + +main(); diff --git a/examples/teams/package.json b/examples/teams/package.json new file mode 100644 index 00000000000..8a7ad6c48ac --- /dev/null +++ b/examples/teams/package.json @@ -0,0 +1,32 @@ +{ + "name": "teams-example", + "version": "0.0.1", + "private": true, + "description": "Runnable demo for @copilotkit/bot-teams: streamed replies, Adaptive Cards, and a human-in-the-loop approval gate, testable in the Microsoft 365 Agents Playground with no Microsoft credentials.", + "license": "MIT", + "type": "module", + "scripts": { + "dev": "tsx watch app/index.tsx", + "start": "tsx app/index.tsx", + "build": "pnpm exec nx run-many -t build -p @copilotkit/bot @copilotkit/bot-teams @copilotkit/bot-ui @copilotkit/runtime", + "playground": "agentsplayground", + "package": "node appPackage/package.mjs", + "check-types": "tsc --noEmit -p tsconfig.json", + "test": "vitest run" + }, + "dependencies": { + "@copilotkit/bot": "workspace:*", + "@copilotkit/bot-teams": "workspace:*", + "@copilotkit/bot-ui": "workspace:*", + "@copilotkit/runtime": "workspace:*", + "zod": "^3.25.76" + }, + "devDependencies": { + "@microsoft/m365agentsplayground": "^0.2.27", + "@types/node": "^22.10.0", + "dotenv": "^16.4.5", + "tsx": "^4.19.2", + "typescript": "^5.6.3", + "vitest": "^4.1.3" + } +} diff --git a/examples/teams/scripts/verify-graph-channel.ts b/examples/teams/scripts/verify-graph-channel.ts new file mode 100644 index 00000000000..29c85a04694 --- /dev/null +++ b/examples/teams/scripts/verify-graph-channel.ts @@ -0,0 +1,145 @@ +/** + * Standalone verification of the app-only Graph channel-file chain — the exact + * sequence `@copilotkit/bot-teams` runs in a channel, WITHOUT the bot, a tunnel, + * an Azure Bot resource, or Chromium. Use it to prove the permission model in a + * tenant where YOU are the admin (e.g. a free Microsoft 365 Developer sandbox) + * before asking your real org's admin to consent. + * + * Setup (in the sandbox, where you're Global Admin): + * 1. Entra → App registrations → New registration (single tenant is fine). + * 2. API permissions → add Microsoft Graph APPLICATION permissions + * - ChannelMessage.Read.All (read the channel message) + * - Files.Read.All (download the SharePoint file) + * → Grant admin consent (you can — you're the admin). + * 3. Certificates & secrets → new client secret. + * 4. Create a team + channel, upload a CSV to the channel. + * 5. Get the team + channel ids: open the channel → ••• → "Get link to + * channel". The link has groupId= and the channel id is the + * "19:....@thread.tacv2" segment (URL-decode %3a → :, %40 → @). + * + * Run: + * CLIENT_ID=... CLIENT_SECRET=... TENANT_ID=... \ + * TEAM_ID=... CHANNEL_ID='19:xxxx@thread.tacv2' \ + * pnpm --filter teams-example exec tsx scripts/verify-graph-channel.ts + */ +const GRAPH = "https://graph.microsoft.com/v1.0"; + +function need(name: string): string { + const v = process.env[name]; + if (!v) { + console.error(`Missing env var ${name} (see the header of this file).`); + process.exit(1); + } + return v; +} + +function shareIdFor(url: string): string { + const b64 = Buffer.from(url, "utf8").toString("base64"); + return "u!" + b64.replace(/=+$/, "").replace(/\//g, "_").replace(/\+/g, "-"); +} + +async function main(): Promise { + const clientId = need("CLIENT_ID"); + const clientSecret = need("CLIENT_SECRET"); + const tenantId = need("TENANT_ID"); + const teamId = need("TEAM_ID"); + const channelId = need("CHANNEL_ID"); + + // Step 1 — app-only token (client credentials), exactly like the bot. + console.log("[1/3] Acquiring app-only Graph token…"); + const tokenRes = await fetch( + `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, + { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + client_id: clientId, + client_secret: clientSecret, + scope: "https://graph.microsoft.com/.default", + grant_type: "client_credentials", + }), + }, + ); + if (!tokenRes.ok) { + console.error( + ` ✗ token failed (HTTP ${tokenRes.status}): ${await tokenRes.text()}`, + ); + process.exit(1); + } + const token = ((await tokenRes.json()) as { access_token: string }) + .access_token; + console.log(" ✓ got a token"); + + // Step 2 — read the channel's recent messages, find file attachments. + console.log( + "[2/3] Reading channel messages (needs ChannelMessage.Read.All)…", + ); + const msgsRes = await fetch( + `${GRAPH}/teams/${teamId}/channels/${encodeURIComponent(channelId)}/messages?$top=20`, + { headers: { Authorization: `Bearer ${token}` } }, + ); + if (!msgsRes.ok) { + console.error( + ` ✗ read messages failed (HTTP ${msgsRes.status}): ${await msgsRes.text()}`, + ); + console.error( + " → ChannelMessage.Read.All probably isn't consented yet.", + ); + process.exit(1); + } + const msgs = (await msgsRes.json()) as { + value?: Array<{ + id?: string; + attachments?: Array<{ + contentType?: string; + contentUrl?: string; + name?: string; + }>; + }>; + }; + const files = (msgs.value ?? []) + .flatMap((m) => m.attachments ?? []) + .filter((a) => a.contentType === "reference" && a.contentUrl); + console.log( + ` ✓ read ${msgs.value?.length ?? 0} messages; found ${files.length} file attachment(s)`, + ); + if (files.length === 0) { + console.error( + " → No file attachments in the last 20 messages. Upload a CSV to this channel and rerun.", + ); + process.exit(1); + } + for (const f of files) console.log(` • ${f.name} ${f.contentUrl}`); + + // Step 3 — download each file from SharePoint via /shares. + console.log("[3/3] Downloading from SharePoint (needs Files.Read.All)…"); + for (const f of files) { + const dlRes = await fetch( + `${GRAPH}/shares/${shareIdFor(f.contentUrl!)}/driveItem/content`, + { headers: { Authorization: `Bearer ${token}` }, redirect: "follow" }, + ); + if (!dlRes.ok) { + console.error( + ` ✗ download "${f.name}" failed (HTTP ${dlRes.status}): ${await dlRes.text()}`, + ); + console.error(" → Files.Read.All probably isn't consented yet."); + process.exit(1); + } + const bytes = Buffer.from(await dlRes.arrayBuffer()); + const preview = bytes.toString("utf8").slice(0, 120).replace(/\n/g, "\\n"); + console.log( + ` ✓ "${f.name}" — ${bytes.byteLength} bytes; starts: "${preview}"`, + ); + } + + console.log( + "\n✅ Verified: app-only token → read channel message → download file all work.\n" + + " This is exactly what the bot does; granting the same two permissions in\n" + + " your real tenant makes the channel CSV→chart flow work there too.", + ); +} + +main().catch((err) => { + console.error("Unexpected failure:", err); + process.exit(1); +}); diff --git a/examples/teams/tsconfig.json b/examples/teams/tsconfig.json new file mode 100644 index 00000000000..889068cb833 --- /dev/null +++ b/examples/teams/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "isolatedModules": true, + "module": "NodeNext", + "moduleDetection": "force", + "moduleResolution": "NodeNext", + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2022", + "noEmit": true, + "lib": ["es2022", "dom"], + "types": ["node"], + "jsx": "react-jsx", + "jsxImportSource": "@copilotkit/bot-ui" + }, + "include": ["app/**/*.ts", "app/**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/examples/teams/vitest.config.ts b/examples/teams/vitest.config.ts new file mode 100644 index 00000000000..2d73ffdd564 --- /dev/null +++ b/examples/teams/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + esbuild: { + jsx: "automatic", + jsxImportSource: "@copilotkit/bot-ui", + }, + test: { + include: ["app/**/*.test.ts", "app/**/*.test.tsx"], + }, +}); From 27dcf4a4049ad0446d1fd543649f2657c107c2e8 Mon Sep 17 00:00:00 2001 From: Jordan Ritter Date: Thu, 25 Jun 2026 13:22:31 -0700 Subject: [PATCH 06/12] fix(showcase): promote strands-typescript to production (dual-env SSOT) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The showcase-strands-typescript integration was staging-only: it had no production Railway serviceInstance, so the prod D6 dashboard column showed a uniform false-red (every cell errorClass=goto-error, backendUrl="") — the probe navigated a bare relative path because the harness had no prod health record / backendUrl to discover. Provisions the prod serviceInstance (8a50728e-6119-43c4-b59c-d9535b6717a4, domain showcase-strands-typescript-production.up.railway.app, healthcheck /api/health, image pinned to the GHCR @sha256 digest, OPENAI_BASE_URL at prod aimock) and brings the SSOT to the dual-env showcase-strands shape: - railway-envs.ts: add the prod env entry with the real instanceId, gateValidated:true, drop gateIgnore, remove the legacyJsonCompat prod-domain placeholder. - railway-envs.generated.json: regenerated (prod instanceId/domain, probe.prod true, prod healthcheck; moved into the promote closure, tier 2). - railway-envs.golden.json: regenerated to include the new prod (service,env) pair (intentional behavior change, not a refactor regression). - showcase_promote.yml: dropdown regenerated to list strands-typescript. - verify-railway-image-refs.test.ts / redeploy-env.test.ts: update the gateValidated/scope counts (39->40 gate targets, prod default 38->39) and the now-stale staging-only comments. RED->GREEN (live prod): BEFORE /api/health 404, prod PocketBase health:strands-typescript totalItems:0, the 3 named D6 cells all errorClass=goto-error backendUrl="". AFTER /api/health 200, prod PocketBase health:strands-typescript present (status:200, valid url), verify-railway-image-refs OK 80 instances. --- .github/workflows/showcase_promote.yml | 1 + .../fixtures/railway-envs.golden.json | 7 +++ .../verify-railway-image-refs.test.ts | 46 ++++++++----------- showcase/scripts/railway-envs.generated.json | 17 +++---- showcase/scripts/railway-envs.ts | 38 ++++++--------- showcase/scripts/redeploy-env.test.ts | 29 ++++++------ 6 files changed, 65 insertions(+), 73 deletions(-) diff --git a/.github/workflows/showcase_promote.yml b/.github/workflows/showcase_promote.yml index 7a271051182..6cc98e3899f 100644 --- a/.github/workflows/showcase_promote.yml +++ b/.github/workflows/showcase_promote.yml @@ -72,6 +72,7 @@ on: - starter-pydantic-ai - starter-strands-python - strands + - strands-typescript - webhooks # <<< END GENERATED service options digest: diff --git a/showcase/scripts/__tests__/fixtures/railway-envs.golden.json b/showcase/scripts/__tests__/fixtures/railway-envs.golden.json index 3706a9623e1..3dacdcd3c03 100644 --- a/showcase/scripts/__tests__/fixtures/railway-envs.golden.json +++ b/showcase/scripts/__tests__/fixtures/railway-envs.golden.json @@ -425,6 +425,13 @@ } }, "showcase-strands-typescript": { + "prod": { + "instanceId": "8a50728e-6119-43c4-b59c-d9535b6717a4", + "domain": "showcase-strands-typescript-production.up.railway.app", + "probe": true, + "driver": "agent", + "repoName": "showcase-strands-typescript" + }, "staging": { "instanceId": "3f917b9f-c3f0-4d8b-96ca-7f455e06b5ba", "domain": "showcase-strands-typescript-staging.up.railway.app", diff --git a/showcase/scripts/__tests__/verify-railway-image-refs.test.ts b/showcase/scripts/__tests__/verify-railway-image-refs.test.ts index e1e405a98d0..c161ab869f4 100644 --- a/showcase/scripts/__tests__/verify-railway-image-refs.test.ts +++ b/showcase/scripts/__tests__/verify-railway-image-refs.test.ts @@ -22,21 +22,17 @@ import type { ServiceEntry } from "../railway-envs"; describe("ServiceEntry gateIgnore field", () => { it("is optional on the type and defaults to falsy when unset", () => { // Every real SSOT entry has gateIgnore unset (undefined / falsy), - // EXCEPT two deliberately gateIgnore:true entries: + // EXCEPT one deliberately gateIgnore:true entry: // - the staging-only `harness-workers` pool-fleet worker (no public // domain, does not fit the symmetric dual-env shape the gate - // validates); - // - the staging-only `showcase-strands-typescript` integration (prod - // not yet provisioned, so it omits the prod env and is gate-ignored - // until promoted dual-env). - // See their SSOT entries in railway-envs.ts for the rationale. - // S2: the 12 starter- services are NO LONGER gate-ignored — they - // are fully gate-managed (gateValidated, no gateIgnore), so they fall - // into the default-falsy branch below exactly like every showcase-* agent. - const GATE_IGNORED = new Set([ - "harness-workers", - "showcase-strands-typescript", - ]); + // validates). + // See its SSOT entry in railway-envs.ts for the rationale. + // `showcase-strands-typescript` is now provisioned dual-env + // (gateValidated:true, no gateIgnore), so it falls into the default-falsy + // branch below. S2: the 12 starter- services are likewise NO LONGER + // gate-ignored — they are fully gate-managed (gateValidated, no + // gateIgnore), exactly like every showcase-* agent. + const GATE_IGNORED = new Set(["harness-workers"]); const isGateIgnored = (name: string): boolean => GATE_IGNORED.has(name); for (const [name, entry] of Object.entries(SERVICES)) { const gi = (entry as ServiceEntry).gateIgnore; @@ -268,16 +264,13 @@ describe("WS-C: all gate-managed services gateValidated, with correct overrides" }); it("marks every gate-managed service gateValidated (no Phase-2 holdouts)", () => { - // Intentional gateValidated:false entries (both gateIgnore:true): the - // staging-only `harness-workers` (no public domain) and the staging-only - // `showcase-strands-typescript` (prod not yet provisioned). S2 brought - // the 12 starter- services UNDER the gate (gateValidated:true), so - // they are no longer holdouts — every OTHER service, starters included, + // Intentional gateValidated:false entry (gateIgnore:true): the + // staging-only `harness-workers` (no public domain). S2 brought the 12 + // starter- services UNDER the gate (gateValidated:true), so they + // are no longer holdouts; `showcase-strands-typescript` is now provisioned + // in prod and gateValidated too — every OTHER service, starters included, // must be gateValidated:true. - const GATE_IGNORED = new Set([ - "harness-workers", - "showcase-strands-typescript", - ]); + const GATE_IGNORED = new Set(["harness-workers"]); const isGateIgnored = (name: string): boolean => GATE_IGNORED.has(name); const unvalidated = Object.entries(SERVICES) .filter(([name, entry]) => !entry.gateValidated && !isGateIgnored(name)) @@ -298,17 +291,18 @@ describe("WS-C: all gate-managed services gateValidated, with correct overrides" }); } - it("findMissingServices treats all 39 gateValidated services as targets (27 showcase/infra + 12 starters)", () => { + it("findMissingServices treats all 40 gateValidated services as targets (28 showcase/infra + 12 starters)", () => { // With nothing "present", every gateValidated service should appear in // the missing set. After S2 brought the 12 starter- services under - // the gate (gateValidated:true, dual-env), that means all 39 — the 27 + // the gate (gateValidated:true, dual-env), and showcase-strands-typescript + // was provisioned in prod (gateValidated:true), that means all 40 — the 28 // showcase/infra gateValidated services plus the 12 starters. (The // gateIgnore entry harness-workers is NOT gateValidated and so is not // required here.) const missingProd = findMissingServices("prod", new Set()); const missingStaging = findMissingServices("staging", new Set()); - expect(missingProd).toHaveLength(39); - expect(missingStaging).toHaveLength(39); + expect(missingProd).toHaveLength(40); + expect(missingStaging).toHaveLength(40); // The 12 starters are now demanded in BOTH envs. expect(missingProd).toContain("starter-adk"); expect(missingStaging).toContain("starter-mastra"); diff --git a/showcase/scripts/railway-envs.generated.json b/showcase/scripts/railway-envs.generated.json index 183aa6ed931..fd2c6b264b7 100644 --- a/showcase/scripts/railway-envs.generated.json +++ b/showcase/scripts/railway-envs.generated.json @@ -778,18 +778,18 @@ { "name": "showcase-strands-typescript", "serviceId": "d6f47c8c-a0a1-4dbe-991c-50f8463fd68d", - "prodInstanceId": "d6f47c8c-a0a1-4dbe-991c-50f8463fd68d", + "prodInstanceId": "8a50728e-6119-43c4-b59c-d9535b6717a4", "stagingInstanceId": "3f917b9f-c3f0-4d8b-96ca-7f455e06b5ba", "ciBuilt": true, - "gateValidated": false, + "gateValidated": true, "dispatchName": "strands-typescript", "domains": { "staging": "showcase-strands-typescript-staging.up.railway.app", - "prod": "showcase-strands-typescript-staging.up.railway.app" + "prod": "showcase-strands-typescript-production.up.railway.app" }, "probe": { "staging": true, - "prod": false, + "prod": true, "driver": "agent" }, "promoteTier": 2, @@ -801,6 +801,7 @@ } ], "healthcheckPath": { + "prod": "/api/health", "staging": "/api/health" } }, @@ -1299,6 +1300,10 @@ "name": "showcase-strands", "tier": 2 }, + { + "name": "showcase-strands-typescript", + "tier": 2 + }, { "name": "starter-adk", "tier": 2 @@ -1352,10 +1357,6 @@ { "name": "harness-workers", "reason": "no \"prod\" environment in the SSOT — cannot be promoted (it exists in: staging)." - }, - { - "name": "showcase-strands-typescript", - "reason": "no \"prod\" environment in the SSOT — cannot be promoted (it exists in: staging)." } ] } diff --git a/showcase/scripts/railway-envs.ts b/showcase/scripts/railway-envs.ts index 5802a028f85..3a9315863c3 100644 --- a/showcase/scripts/railway-envs.ts +++ b/showcase/scripts/railway-envs.ts @@ -1150,23 +1150,16 @@ export const SERVICES: Record< }, }, }, - // STAGING-ONLY (for now). The TypeScript sibling of `showcase-strands` - // ships to staging first; its prod instance is intentionally NOT yet - // provisioned. Under the env-map schema the entry declares only `staging` - // (no prod key, no placeholder ID). gateIgnore skips BOTH image-ref-gate - // directions so a prod-less, not-yet-pinned service does not trip - // findMissingServices / findUntrackedServices; gateValidated is therefore - // false. ciBuilt:true keeps it in the staging redeploy scope so a - // main-merge build of its image bounces the staging instance. When prod is - // later provisioned + promoted via showcase_promote.yml, convert this to - // the dual-env `showcase-strands` shape: add a `prod` env entry with its - // real serviceInstance ID, flip gateValidated:true, drop gateIgnore, and - // remove the legacyJsonCompat prod-domain placeholder below. + // The TypeScript sibling of `showcase-strands`. Now provisioned in BOTH + // staging and prod (dual-env `showcase-strands` shape): the prod + // serviceInstance was created + deployed + health-verified, so the entry + // declares a real `prod` env, gateValidated:true (verify-railway-image-refs + // validates both drift directions), gateIgnore dropped, and the + // legacyJsonCompat prod-domain placeholder removed. "showcase-strands-typescript": { serviceId: "d6f47c8c-a0a1-4dbe-991c-50f8463fd68d", ciBuilt: true, - gateValidated: false, - gateIgnore: true, + gateValidated: true, dispatchName: "strands-typescript", probeDriver: "agent", // Tier-2 leaf (default). Runtime dep: the agent routes its LLM traffic @@ -1176,6 +1169,12 @@ export const SERVICES: Record< runtimeDeps: ["aimock"], serviceRefs: [{ key: "OPENAI_BASE_URL", target: "aimock" }], environments: { + prod: { + instanceId: "8a50728e-6119-43c4-b59c-d9535b6717a4", + healthcheckPath: "/api/health", + domain: "showcase-strands-typescript-production.up.railway.app", + probe: true, + }, staging: { instanceId: "3f917b9f-c3f0-4d8b-96ca-7f455e06b5ba", healthcheckPath: "/api/health", @@ -1183,17 +1182,6 @@ export const SERVICES: Record< probe: true, }, }, - // Ruby/jq JSON-shape compat (see ServiceEntry.legacyJsonCompat). The - // emitter fills the absent prod env's prodInstanceId from serviceId and - // the missing prod domain from this borrowed staging host so the - // generated JSON keeps its legacy {prod,staging} shape. Neither is read - // by any TS accessor (no prod env => never dereferenced; probe.prod is - // emitted false). - legacyJsonCompat: { - domains: { - prod: "showcase-strands-typescript-staging.up.railway.app", - }, - }, }, // ───────────────────────── starter-* container fleet ───────────────────── // The 12 starter-template containers (ghcr.io/copilotkit/starter-), diff --git a/showcase/scripts/redeploy-env.test.ts b/showcase/scripts/redeploy-env.test.ts index e0f0125201c..9b8fcd71ba7 100644 --- a/showcase/scripts/redeploy-env.test.ts +++ b/showcase/scripts/redeploy-env.test.ts @@ -68,17 +68,17 @@ describe("runRedeploy", () => { }); expect(result.exitCode).toBe(0); - // 39 CI-built (27 showcase/infra incl. the staging-only - // showcase-strands-typescript, + 12 starters) + harness-workers - // (imageOf consumer of showcase-harness) = 40. All 39 declare staging, - // so the env-aware default scope keeps every one of them. + // 39 CI-built (27 showcase/infra incl. showcase-strands-typescript, + // now dual-env, + 12 starters) + harness-workers (imageOf consumer of + // showcase-harness) = 40. All 39 declare staging, so the env-aware + // default scope keeps every one of them. expect(result.attempted).toBe(40); expect(result.succeeded).toBe(40); expect(redeploy).toHaveBeenCalledTimes(40); // pocketbase is now CI-built, so it IS in the default redeploy scope. expect(seenNames).toContain("pocketbase"); - // The staging-only TypeScript Strands integration declares staging, so - // it IS in the staging default scope (but NOT prod — see below). + // The TypeScript Strands integration declares staging, so it IS in the + // staging default scope (and now prod too — see below). expect(seenNames).toContain("showcase-strands-typescript"); // S2: starters are CI-built, so they JOIN the default redeploy scope. expect(seenNames).toContain("starter-adk"); @@ -142,14 +142,14 @@ describe("runRedeploy", () => { ]); }); - it("default prod scope excludes staging-only services (harness-workers + showcase-strands-typescript)", async () => { + it("default prod scope excludes staging-only services (harness-workers) but includes dual-env showcase-strands-typescript", async () => { // The default scope is env-aware: a service with no prod env never joins // the prod scope. harness-workers (imageOf consumer, staging-only) is - // excluded by the env-aware imageOf expansion; showcase-strands-typescript - // (ciBuilt, staging-only — prod not yet provisioned) is excluded by the - // env-aware base-scope filter. The prod default = the 38 CI-built services - // that declare prod (26 showcase/infra + 12 starters), neither staging-only - // service. + // excluded by the env-aware imageOf expansion. showcase-strands-typescript + // is now provisioned dual-env, so it DOES join the prod default scope. The + // prod default = the 39 CI-built services that declare prod (27 + // showcase/infra incl. showcase-strands-typescript + 12 starters); only + // staging-only harness-workers is excluded. const seenNames: string[] = []; const redeploy = vi.fn(async (serviceId: string) => { const name = Object.entries(SERVICES).find( @@ -163,9 +163,10 @@ describe("runRedeploy", () => { redeploy, appendSummary, }); - expect(result.attempted).toBe(38); + expect(result.attempted).toBe(39); expect(seenNames).not.toContain("harness-workers"); - expect(seenNames).not.toContain("showcase-strands-typescript"); + // showcase-strands-typescript is now dual-env, so it joins the prod scope. + expect(seenNames).toContain("showcase-strands-typescript"); // S2: starters ARE in the default prod scope (CI-built, dual-env). expect(seenNames).toContain("starter-adk"); }); From ca920066aca9473768b92ae5ec8585904c39e8da Mon Sep 17 00:00:00 2001 From: Austin Merrick Date: Mon, 8 Jun 2026 12:33:17 -0700 Subject: [PATCH 07/12] build(core): raise build heap ceiling to 8GB via cross-env The @copilotkit/core build (tsdown with dts generation) has a working set of about 3.45GB. Node's default old-space ceiling is roughly 4GB and CI pins NODE_OPTIONS to 4096, leaving almost no headroom. Under nx run-many the build competes for CPU with sibling builds, GC falls behind, and the process tips over the heap limit. nx flags it as a flaky task and pre-commit/CI builds fail intermittently with a V8 heap OOM. Bake the ceiling into the core build script with cross-env so every invocation path gets consistent headroom: local lefthook hooks, nx run-many, CI, and direct pnpm build, on every platform including Windows. 8192 matches the value already used by the e2e workflow and gives roughly 2x headroom over the working set. Scoped to core only; it is the single package that OOMs. --- packages/core/package.json | 3 ++- pnpm-lock.yaml | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 52f75a70304..c4dac36a7b2 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -23,7 +23,7 @@ "access": "public" }, "scripts": { - "build": "tsdown", + "build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 tsdown", "size": "size-limit", "compat-check": "es-check es2022 --module 'dist/**/!(*.umd).{mjs,cjs,js}' && es-check es2018 'dist/**/*.umd.js'", "dev": "tsdown --watch", @@ -49,6 +49,7 @@ "@valibot/to-json-schema": "^1.5.0", "@vitest/coverage-v8": "^3.2.4", "arktype": "^2.1.29", + "cross-env": "^10.1.0", "tsdown": "^0.20.3", "typescript": "5.8.2", "valibot": "^1.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d67e8e798bf..10db4f22aa6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2581,6 +2581,9 @@ importers: arktype: specifier: ^2.1.29 version: 2.1.29 + cross-env: + specifier: ^10.1.0 + version: 10.1.0 tsdown: specifier: ^0.20.3 version: 0.20.3(@arethetypeswrong/core@0.18.2)(publint@0.3.17)(synckit@0.11.12)(typescript@5.8.2) @@ -6331,6 +6334,9 @@ packages: resolution: {integrity: sha512-CsFmA3u3c2QoLDTfEpGr4t25fjMU31nyvse7IzWTvb0ZycuPjMjb0fjlheh+PbhBYb9YLugnT2uY6Mwcg1o+Zg==} engines: {node: '>=18.0.0'} + '@epic-web/invariant@1.0.0': + resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} + '@esbuild/aix-ppc64@0.27.3': resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} engines: {node: '>=18'} @@ -15244,6 +15250,11 @@ packages: resolution: {integrity: sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g==} engines: {node: '>=18.0'} + cross-env@10.1.0: + resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==} + engines: {node: '>=20'} + hasBin: true + cross-env@7.0.3: resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} @@ -30635,6 +30646,8 @@ snapshots: '@whatwg-node/promise-helpers': 1.3.2 tslib: 2.8.1 + '@epic-web/invariant@1.0.0': {} + '@esbuild/aix-ppc64@0.27.3': optional: true @@ -41810,6 +41823,11 @@ snapshots: croner@9.1.0: {} + cross-env@10.1.0: + dependencies: + '@epic-web/invariant': 1.0.0 + cross-spawn: 7.0.6 + cross-env@7.0.3: dependencies: cross-spawn: 7.0.6 From 4396f71ab08de43aff4811da6207090ffef24a34 Mon Sep 17 00:00:00 2001 From: Jordan Ritter Date: Thu, 25 Jun 2026 14:06:04 -0700 Subject: [PATCH 08/12] docs(showcase): document promoting a staging-only integration to production MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a "Promoting a Staging-Only Integration to Production" section to showcase/RAILWAY.md — the staging-first -> promote-later procedure that was undocumented and caused the strands-typescript D6 false-red (PR #5705). The procedure previously survived only as a comment inside the SSOT (railway-envs.ts); there was no human-facing SOP. The new section is a start-to-finish checklist grounded in the PR #5705 worked example: - When it applies (gateValidated:false, gateIgnore:true, staging-only env map, legacyJsonCompat prod placeholder). - The critical gotcha up front: the promote pipeline (showcase_promote.yml / bin/railway promote) only moves digests to a prod service that ALREADY exists; it does NOT provision a new prod serviceInstance. Until that instance exists, D6 false-reds the whole column (404 -> empty backendUrl -> goto-error on every cell). - Ordered steps: provision the prod serviceInstance out-of-band (environmentStageChanges + environmentPatchCommitStaged, mirroring a peer prod TS service); edit the SSOT (add prod env block, gateValidated:true, drop gateIgnore, remove legacyJsonCompat); regenerate derived artifacts (emit-railway-envs-json.ts, golden fixture, sync-promote-service-options.ts) and run the gate (verify-railway-image-refs.ts + vitest); prod secrets via the prod env var set / aimock (no inline secrets); verify GREEN (/api/health 200, prod PocketBase health record, D6 flips on the next hourly :40 tick). Cross-links INTEGRATION-CHECKLIST.md §B (single-shot bring-up) both ways. Leaves a precise TODO that §B.3 still names the stale showcase_deploy.yml for the build matrix (the RAILWAY.md references were already corrected on main). --- showcase/INTEGRATION-CHECKLIST.md | 8 ++ showcase/RAILWAY.md | 130 ++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) diff --git a/showcase/INTEGRATION-CHECKLIST.md b/showcase/INTEGRATION-CHECKLIST.md index b5085617e6a..8691f9f4083 100644 --- a/showcase/INTEGRATION-CHECKLIST.md +++ b/showcase/INTEGRATION-CHECKLIST.md @@ -118,6 +118,14 @@ When running `migrate-integration-examples.ts` or reasoning about drift, remembe ## B. External Setup (after the package is ready) +This section is the **single-shot** bring-up: provision the prod Railway +service immediately, then go live. If instead you ship the integration +**staging-only first** and defer prod ("promote later"), follow +[`./RAILWAY.md`](./RAILWAY.md) → "Promoting a Staging-Only Integration to +Production" for the provision-prod-instance + SSOT-gate-flip + promote path +(and note: the promote pipeline does NOT provision a new prod service — D6 +false-reds the whole column until the prod instance exists). + ### 1. Railway Service - [ ] Create service in the **CopilotKit Showcase** Railway project, **US-West** region diff --git a/showcase/RAILWAY.md b/showcase/RAILWAY.md index 707ab76e90f..d470105ca4e 100644 --- a/showcase/RAILWAY.md +++ b/showcase/RAILWAY.md @@ -73,6 +73,136 @@ primary deploy path. 4. **Git-based services**: auto-updates only apply to image-sourced services. Skip step 1 for git-deploy services. +## Promoting a Staging-Only Integration to Production + +### When this applies + +You added an integration **staging-only on purpose** — it ships to staging +first and its prod instance is deferred to "promote later." In the SSOT +(`showcase/scripts/railway-envs.ts`) such an entry looks like the +`showcase-strands-typescript` block did before [PR #5705](https://github.com/CopilotKit/CopilotKit/pull/5705): + +- `gateValidated: false`, +- `gateIgnore: true`, +- an `environments:` map containing **only `staging`** (no `prod` block, so no + prod `serviceInstance` ID exists), +- a `legacyJsonCompat.domains.prod` placeholder pointing at the **borrowed + staging host**, purely to keep the generated JSON's legacy `{prod, staging}` + shape (it is never dereferenced by any TS accessor). + +This is the worked example to follow — `showcase-strands-typescript` was +promoted exactly this way in PR #5705. + +### The critical gotcha (read this first) + +**The promote pipeline only promotes image digests to a prod service that +ALREADY exists — it does NOT provision a new prod `serviceInstance`.** Both the +promote workflow (`showcase_promote.yml`, "Showcase: Promote (staging → prod)") +and `bin/railway promote` move the staging-tested `@sha256` digest onto an +existing prod instance; neither has a "create the prod service" step (there is +no provisioning subcommand in `bin/railway`). So a staging-only integration +will **never** appear in prod just by running promote. + +Until the prod `serviceInstance` exists, **D6 false-reds the entire column**: +the harness has no `health:` record for prod, so the per-cell probe is +handed an empty `backendUrl`, Playwright calls `page.goto("/demos/…")` on a +bare relative path, and Chromium rejects it as an invalid URL — +`errorClass=goto-error` on *every* cell (column-wide, uniform `fail_count`). +The fix is not a code fix; it is provisioning the missing prod instance and +flipping the SSOT gate. + +### Ordered checklist + +1. **Provision the prod Railway `serviceInstance`.** This is out-of-band (no + `bin/railway` subcommand covers it; see [`./bin/README.md`](./bin/README.md), + which defers "new-service provisioning" to this doc). Use the GraphQL + staged-change primitive, mirroring a peer prod TypeScript showcase service + (PR #5705 mirrored `showcase-claude-sdk-typescript`): + - `environmentStageChanges(production, …)` — stage a `services.` block + copied from the peer: `source.image` (pinned `@sha256` digest with + `autoUpdates.minor`), `networking.serviceDomains.`, + `build.builder RAILPACK`, and a `deploy` block (reused GHCR + `registryCredentials`, runtime V2, `healthcheckPath: /api/health`, + `multiRegionConfig`). + - `environmentPatchCommitStaged(production, )` — commit the staged + change; this **materializes** the prod `serviceInstance` (in PR #5705, + `8a50728e-6119-43c4-b59c-d9535b6717a4`). + - Deploy it (`serviceInstanceDeployV2`) and poll the deployment to + `SUCCESS`. + +2. **Edit the SSOT (`showcase/scripts/railway-envs.ts`)** — convert the entry + to the dual-env `showcase-strands` shape: + - add a `prod` env block under `environments:` with the **real** + `instanceId`, `healthcheckPath: "/api/health"`, the prod `domain`, and + `probe: true`; + - set `gateValidated: true` (per the `gateValidated` doc in that file, new + SSOT services MUST land `gateValidated: true`; `gateIgnore` is only for + "deliberately-untracked third-party / domainless / single-env services" — + a prod-promoted demo is none of those); + - **remove** `gateIgnore: true`; + - **remove** the `legacyJsonCompat` prod-domain placeholder (the borrowed + staging host); + - update the leading comment to reflect the dual-env state. + + See the PR #5705 diff on this file for the exact before/after. + +3. **Regenerate the derived artifacts and run the gate:** + - `npx tsx showcase/scripts/emit-railway-envs-json.ts` — regenerate + `railway-envs.generated.json` (CI verifies with `--check`). + - Regenerate the golden fixture + `showcase/scripts/__tests__/fixtures/railway-envs.golden.json` so the new + prod `(service, env)` pair is captured — this is an **intentional** + behavior change, not a refactor regression + (`railway-envs.golden.test.ts` is a behavior-preservation guard). + - `npx tsx showcase/scripts/sync-promote-service-options.ts` — regenerate + the `showcase_promote.yml` workflow_dispatch dropdown so the slug becomes + a promote target (CI verifies with `--check`). + - `npx tsx showcase/scripts/verify-railway-image-refs.ts` — run the image-ref + gate; with `gateValidated: true` it now validates the prod pin too. + - Run the scripts test suite (`pnpm exec vitest run` from `showcase/`), + including `verify-railway-image-refs.test.ts` and `redeploy-env.test.ts`, + whose gate-target / redeploy-scope counts and "staging-only" comments + change when the entry flips dual-env. + +4. **Secrets.** A prod TypeScript integration gets its provider keys + (`OPENAI_API_KEY` / `ANTHROPIC_API_KEY`, and `OPENAI_BASE_URL` for + aimock-routed agents) from the **prod env's variable set**, mirroring the + peer prod service's config — set them on the new prod instance, never inline + a secret value in the SSOT or in a commit. If the agent routes 100% to + aimock (the `serviceRefs: [{ key: "OPENAI_BASE_URL", target: "aimock" }]` + case), `OPENAI_BASE_URL` points at the **prod** aimock origin and the + `OPENAI_API_KEY` is the non-secret `sk-aim…` aimock placeholder — so no real + prod secret is sourced. The `OPENAI_BASE_URL` service-ref is asserted + prod→prod by the promote preflight (never copied across envs). + +5. **Verify GREEN.** After the prod instance is up: + - prod `/api/health` returns **200** + (`https://showcase--production.up.railway.app/api/health`); + - the prod PocketBase `health` collection gains a `health:` record + (`dimension="health"`, `status:200`, a real prod `url`); + - the D6 column flips on the prod harness's **next hourly + `d6-all-pills-e2e` tick** (runs at `:40`). The probe needs the harness to + have discovered the new prod health record first, so expect up to ~1 hour + of lag — the column stays red until the next tick even though the service + is healthy. Don't panic about that lag; confirm health (200 + the + PocketBase record) as the discriminating GREEN signal, then let the tick + clear the cells. + +Once promoted, run the digest promote itself the normal way — +`showcase_promote.yml` (now listing the slug) or `bin/railway promote`; see +[`./bin/README.md`](./bin/README.md) "Worked example: promote staging → +production". + +> **Related:** for the *single-shot* "create prod service → go live" +> bring-up (where prod is provisioned immediately, with no staging-first +> phase), see [`./INTEGRATION-CHECKLIST.md`](./INTEGRATION-CHECKLIST.md) §B. +> This section is the **staging-first → promote-later** counterpart. +> +> TODO: `INTEGRATION-CHECKLIST.md` §B.3 still names `showcase_deploy.yml` as +> the build/push workflow to edit; the build/push matrix has since moved to +> `showcase_build.yml` ("Build & Push"), with `showcase_deploy.yml` now the +> staging verify gate. Correct §B.3 in a follow-up. + ## Environment IDs - Project: `` From e9fce5b8bb906753de321daa1c2283aaf1264930 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 25 Jun 2026 21:07:23 +0000 Subject: [PATCH 09/12] style: auto-fix formatting --- showcase/RAILWAY.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/showcase/RAILWAY.md b/showcase/RAILWAY.md index d470105ca4e..3e1fcf050b0 100644 --- a/showcase/RAILWAY.md +++ b/showcase/RAILWAY.md @@ -107,7 +107,7 @@ Until the prod `serviceInstance` exists, **D6 false-reds the entire column**: the harness has no `health:` record for prod, so the per-cell probe is handed an empty `backendUrl`, Playwright calls `page.goto("/demos/…")` on a bare relative path, and Chromium rejects it as an invalid URL — -`errorClass=goto-error` on *every* cell (column-wide, uniform `fail_count`). +`errorClass=goto-error` on _every_ cell (column-wide, uniform `fail_count`). The fix is not a code fix; it is provisioning the missing prod instance and flipping the SSOT gate. @@ -193,7 +193,7 @@ Once promoted, run the digest promote itself the normal way — [`./bin/README.md`](./bin/README.md) "Worked example: promote staging → production". -> **Related:** for the *single-shot* "create prod service → go live" +> **Related:** for the _single-shot_ "create prod service → go live" > bring-up (where prod is provisioned immediately, with no staging-first > phase), see [`./INTEGRATION-CHECKLIST.md`](./INTEGRATION-CHECKLIST.md) §B. > This section is the **staging-first → promote-later** counterpart. From a1b1792ef08fd69c3f8a196acef7412070cd4d5b Mon Sep 17 00:00:00 2001 From: Tyler Slaton Date: Thu, 25 Jun 2026 15:30:05 -0700 Subject: [PATCH 10/12] Add bot-teams to release scopes --- .github/workflows/canary.yml | 1 + .github/workflows/publish-release.yml | 1 + .github/workflows/stable-release.yml | 1 + release.config.json | 5 +++++ scripts/release/lib/config.ts | 4 +++- 5 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index e7ceeb1a537..461d88b599d 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -39,6 +39,7 @@ on: - bot - bot-discord - bot-slack + - bot-teams - bot-telegram - bot-whatsapp suffix: diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 179a86c9e3c..743b5e43e7f 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -42,6 +42,7 @@ on: - bot - bot-discord - bot-slack + - bot-teams - bot-telegram - bot-whatsapp mode: diff --git a/.github/workflows/stable-release.yml b/.github/workflows/stable-release.yml index 5d1d5f5ea38..dfa543fb2a1 100644 --- a/.github/workflows/stable-release.yml +++ b/.github/workflows/stable-release.yml @@ -13,6 +13,7 @@ on: - bot - bot-discord - bot-slack + - bot-teams - bot-telegram - bot-whatsapp bump: diff --git a/release.config.json b/release.config.json index 9f23ea2227a..07fe3a77f7b 100644 --- a/release.config.json +++ b/release.config.json @@ -47,6 +47,11 @@ "versionSource": "@copilotkit/bot-slack", "sharedVersion": false }, + "bot-teams": { + "packages": ["@copilotkit/bot-teams"], + "versionSource": "@copilotkit/bot-teams", + "sharedVersion": false + }, "bot-telegram": { "packages": ["@copilotkit/bot-telegram"], "versionSource": "@copilotkit/bot-telegram", diff --git a/scripts/release/lib/config.ts b/scripts/release/lib/config.ts index e7b39d1b962..6b86eddcc91 100644 --- a/scripts/release/lib/config.ts +++ b/scripts/release/lib/config.ts @@ -13,7 +13,9 @@ export type ReleaseScope = | "bot" | "bot-discord" | "bot-slack" - | "bot-telegram"; + | "bot-teams" + | "bot-telegram" + | "bot-whatsapp"; export interface ScopeConfig { packages: string[]; From 5f9c2404d1db25979af38d3b20999654d6dfe6fb Mon Sep 17 00:00:00 2001 From: Tyler Slaton Date: Thu, 25 Jun 2026 15:35:45 -0700 Subject: [PATCH 11/12] chore: format banking showcase --- examples/showcases/banking/docs/DESIGN.md | 48 +++---- .../docs/teach-mode/LEARNING-TRACK-PLAN.md | 130 ++++++++++-------- .../src/app/api/v1/exceptions/route.ts | 5 +- .../src/components/change-pin-dialog.tsx | 7 +- .../src/components/chat/chat-inbox.tsx | 12 +- .../src/components/chat/chat-panel-header.tsx | 3 +- .../src/components/credit-card-details.tsx | 4 +- .../src/components/statistics-chart.tsx | 4 +- .../banking/src/components/ui/button.tsx | 6 +- .../banking/src/components/ui/card.tsx | 6 +- .../src/components/ui/dropdown-menu.tsx | 5 +- .../banking/src/components/ui/select.tsx | 5 +- examples/showcases/banking/tsconfig.json | 14 +- 13 files changed, 129 insertions(+), 120 deletions(-) diff --git a/examples/showcases/banking/docs/DESIGN.md b/examples/showcases/banking/docs/DESIGN.md index 39c18779e19..6de4f09b560 100644 --- a/examples/showcases/banking/docs/DESIGN.md +++ b/examples/showcases/banking/docs/DESIGN.md @@ -36,33 +36,33 @@ colors (`brand`, `brand-violet`, `brand-indigo`, `brand-soft`, `surface`, `surface-muted`, `canvas`, `ink`, `ink-muted`, `positive`, `positive-soft`, `negative`, `negative-soft`, `hairline`). -| Token | Light | Dark | Role | -| ------------------ | -------------------- | --------------------- | -------------------------------------- | -| `--canvas` | `255 60% 97%` | `252 30% 7%` | App background (lavender / deep indigo)| -| `--surface` | `0 0% 100%` | `252 24% 11%` | Cards, sidebar, menus | -| `--surface-muted` | `252 40% 98%` | `252 22% 14%` | Row hover, inset blocks | -| `--ink` | `252 30% 14%` | `250 30% 96%` | Primary text / headings | -| `--ink-muted` | `250 12% 46%` | `250 12% 66%` | Secondary / label text | -| `--hairline` | `252 30% 92%` | `252 20% 22%` | Borders / dividers | -| `--brand`/`-violet`| `252 83% 67%` | (shared) | Primary violet | -| `--brand-indigo` | `248 84% 60%` | (shared) | Gradient end / heading color | -| `--brand-soft` | `252 90% 96%` | `252 50% 18%` | Lilac chips, hover wash, avatar bg | -| `--positive` | `152 62% 40%` | `152 56% 50%` | Income / available credit | -| `--positive-soft` | `152 70% 95%` | `152 40% 16%` | Income chip background | -| `--negative` | `349 78% 56%` | `349 80% 64%` | Expense / destructive | -| `--negative-soft` | `349 90% 96%` | `349 40% 18%` | Expense chip background | +| Token | Light | Dark | Role | +| ------------------- | ------------- | ------------- | --------------------------------------- | +| `--canvas` | `255 60% 97%` | `252 30% 7%` | App background (lavender / deep indigo) | +| `--surface` | `0 0% 100%` | `252 24% 11%` | Cards, sidebar, menus | +| `--surface-muted` | `252 40% 98%` | `252 22% 14%` | Row hover, inset blocks | +| `--ink` | `252 30% 14%` | `250 30% 96%` | Primary text / headings | +| `--ink-muted` | `250 12% 46%` | `250 12% 66%` | Secondary / label text | +| `--hairline` | `252 30% 92%` | `252 20% 22%` | Borders / dividers | +| `--brand`/`-violet` | `252 83% 67%` | (shared) | Primary violet | +| `--brand-indigo` | `248 84% 60%` | (shared) | Gradient end / heading color | +| `--brand-soft` | `252 90% 96%` | `252 50% 18%` | Lilac chips, hover wash, avatar bg | +| `--positive` | `152 62% 40%` | `152 56% 50%` | Income / available credit | +| `--positive-soft` | `152 70% 95%` | `152 40% 16%` | Income chip background | +| `--negative` | `349 78% 56%` | `349 80% 64%` | Expense / destructive | +| `--negative-soft` | `349 90% 96%` | `349 40% 18%` | Expense chip background | ## Radius, shadow, font scale -| Token | Value | -| ---------------- | ---------------------------------------------- | -| `--radius` (lg) | `1.375rem` (~22px) — card baseline | -| `radius-xl/2xl` | `+6px` / `+12px` — sidebar, hero panels | -| `radius-md/sm` | `-6px` / `-10px` | -| `--shadow-soft` | resting card shadow (low, violet-tinted) | -| `--shadow-lift` | hover / floating panels / menus | -| `--shadow-glow` | violet glow under gradient CTAs | -| `--font-sans` | `var(--font-inter), ui-sans-serif, system-ui…` | +| Token | Value | +| --------------- | ---------------------------------------------- | +| `--radius` (lg) | `1.375rem` (~22px) — card baseline | +| `radius-xl/2xl` | `+6px` / `+12px` — sidebar, hero panels | +| `radius-md/sm` | `-6px` / `-10px` | +| `--shadow-soft` | resting card shadow (low, violet-tinted) | +| `--shadow-lift` | hover / floating panels / menus | +| `--shadow-glow` | violet glow under gradient CTAs | +| `--font-sans` | `var(--font-inter), ui-sans-serif, system-ui…` | ## Helper classes (in `@layer components`) diff --git a/examples/showcases/banking/docs/teach-mode/LEARNING-TRACK-PLAN.md b/examples/showcases/banking/docs/teach-mode/LEARNING-TRACK-PLAN.md index fa13dcc2bd5..56f38559157 100644 --- a/examples/showcases/banking/docs/teach-mode/LEARNING-TRACK-PLAN.md +++ b/examples/showcases/banking/docs/teach-mode/LEARNING-TRACK-PLAN.md @@ -9,6 +9,7 @@ > **Owner:** jerel@copilotkit.ai · **Date:** 2026-06-05 · **Status:** Draft for review > > **Repos in play (read both):** +> > - **Banking demo (canonical, OSS):** `CopilotKit/examples/showcases/banking` — Next.js App > Router, CopilotKit v2 hooks, `workspace:*` packages (react-core 1.59.2, **no recording > hook**). This is where the gate/unlock/framing/UX live and verify today. @@ -52,20 +53,20 @@ backend (FOR-147)**; then **pin the hook + swap the import (FOR-146)**; then the ## 1. The loop, end to end — concretely, against THIS demo -The banking entities are: `transaction` (the gated write is *approve*), `expense-policy` +The banking entities are: `transaction` (the gated write is _approve_), `expense-policy` (the limit that blocks it), `policy-exception` (the unlock record), and a `policy-exception-code` catalogue (justifying vs decoy vs invalid). -| Step | What happens | THIS demo's concrete entities + file | -|------|--------------|--------------------------------------| -| **1. Agent A tries the obvious write** | A fresh agent is asked to approve an over-limit transaction. It calls the approve write. | The agent prompt in `src/app/api/copilotkit/[[...slug]]/route.ts` (`bankingAgent`) lists the tools but withholds the unlock recipe. The approve path is the `showAndApproveTransactions` HITL in `src/app/page.tsx` → `changeTransactionStatus` (`src/app/actions.ts`) → `PUT /api/v1/transactions/[id]`. | -| **2. Gate fails, symptom-only** | The PUT returns **422 `OVER_POLICY_LIMIT`**, message `" policy limit exceeded"`. It names the *problem* (over limit), never the *fix* (policy exception). | `src/app/api/v1/transactions/[id]/route.ts` (the `patch.status === "approved" && !isWithinPolicyLimit && !hasApprovedException` branch). Rules in `src/lib/store.ts`: `isWithinPolicyLimit` / `hasApprovedException` / `canApprove`. Seed: `t-1` (Google Ads, −5000, Marketing limit 5000/spent 500) ⇒ over limit. | -| **3. Agent stops (framing holds)** | Per the **ACTION DISCIPLINE** clause, the agent does not improvise. It reports the failure and asks the human how to proceed. It does NOT fire a distractor (`sendSpendAlert` / `requestCardReplacement` / `flagForReview`). | Prompt + distractor tools in the runtime route and `src/app/page.tsx` (the three `useFrontendTool` no-op distractors). This is the **control**: pre-learning, a correctly-framed agent cannot pass. | -| **4. Human demonstrates the unlock** | A human opens a policy exception under a **justifying** code (e.g. `EXC-BOARD-APPROVED`), finalizes it (auto-approves + links `activeExceptionId`), then re-approves — now **201**. | Today: `PolicyExceptionModal` (`src/components/policy-exception-modal.tsx`) opened from the over-limit row in `src/components/transactions-list.tsx`. Catalogue: `src/app/api/v1/policy-exception-codes.ts` (`JUSTIFYING_EXCEPTION_CODES` = BOARD-APPROVED / CONTRACTUAL-COMMITMENT / EMERGENCY-SPEND; decoys = WILL-REIMBURSE / ONE-TIME). REST: `exceptions/route.ts` + `exceptions/[id]/finalize/route.ts`. **This plan moves that flow inline into the chat — see §3.2.** | -| **5. Each mutation is recorded on the thread** | After `open()` and after `finalize()`, the UI calls `recordUserAction({title, description, previousData, newData, metadata})`. `previousData` carries the gated flags (`approvePermitted: false`); `newData` the unlocked effect (flipped flags + the linking exception id); `metadata` the `transactionId`. | Two existing calls in `policy-exception-modal.tsx` (`policy_exception.opened`, `policy_exception.finalized`) + two in `transactions-list.tsx` (`transaction.approved`, `transaction.denied`). The import is the no-op shim `@/lib/record-user-action` **today** — see Blocker 2a. | -| **6. Events stream to the gateway** | With the real hook + Intelligence runtime, every AG-UI event of the run (including the recorded user actions) streams over the Phoenix WebSocket to the Intelligence gateway, scoped to the current user + thread. | The env-gated `CopilotKitIntelligence({apiUrl,wsUrl,apiKey})` branch of `createRuntime()` in the runtime route; `identifyUser` maps `properties.userRole` → a stable `northwind-` id so threads + knowledge are scoped consistently. Reference: `cpk-intelligence-banking/demos/e-commerce/bff/.../main.ts`. | -| **7. Distilled into `/knowledge`** | The `sl-worker` sweeps the recorded actions and an LLM writer distills a reusable procedure: *"to approve an over-policy-limit transaction, open a policy exception under a justifying code (board-approved / contractual / emergency), finalize it, then approve."* It lands in `/knowledge` (shared per org+project). | `apps/sl-worker` in the Intelligence repo (gated on `SL_ENABLED=true`); writes `cpki.knowledge_base_files` exposed to agents as `/knowledge`. | -| **8. Agent B (fresh) learns + succeeds** | In a NEW thread with no memory of the human, the agent is asked the same over-limit approval. It greps `/knowledge` (via the `copilotkit_knowledge_base_shell` tool), discovers the procedure, files a *justifying* exception, finalizes it, approves → **201** — no human help, nothing added to the prompt. | Same `bankingAgent` prompt (still recipe-free) reading `/knowledge`. This is the **proof of learning** — see §5. | +| Step | What happens | THIS demo's concrete entities + file | +| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **1. Agent A tries the obvious write** | A fresh agent is asked to approve an over-limit transaction. It calls the approve write. | The agent prompt in `src/app/api/copilotkit/[[...slug]]/route.ts` (`bankingAgent`) lists the tools but withholds the unlock recipe. The approve path is the `showAndApproveTransactions` HITL in `src/app/page.tsx` → `changeTransactionStatus` (`src/app/actions.ts`) → `PUT /api/v1/transactions/[id]`. | +| **2. Gate fails, symptom-only** | The PUT returns **422 `OVER_POLICY_LIMIT`**, message `" policy limit exceeded"`. It names the _problem_ (over limit), never the _fix_ (policy exception). | `src/app/api/v1/transactions/[id]/route.ts` (the `patch.status === "approved" && !isWithinPolicyLimit && !hasApprovedException` branch). Rules in `src/lib/store.ts`: `isWithinPolicyLimit` / `hasApprovedException` / `canApprove`. Seed: `t-1` (Google Ads, −5000, Marketing limit 5000/spent 500) ⇒ over limit. | +| **3. Agent stops (framing holds)** | Per the **ACTION DISCIPLINE** clause, the agent does not improvise. It reports the failure and asks the human how to proceed. It does NOT fire a distractor (`sendSpendAlert` / `requestCardReplacement` / `flagForReview`). | Prompt + distractor tools in the runtime route and `src/app/page.tsx` (the three `useFrontendTool` no-op distractors). This is the **control**: pre-learning, a correctly-framed agent cannot pass. | +| **4. Human demonstrates the unlock** | A human opens a policy exception under a **justifying** code (e.g. `EXC-BOARD-APPROVED`), finalizes it (auto-approves + links `activeExceptionId`), then re-approves — now **201**. | Today: `PolicyExceptionModal` (`src/components/policy-exception-modal.tsx`) opened from the over-limit row in `src/components/transactions-list.tsx`. Catalogue: `src/app/api/v1/policy-exception-codes.ts` (`JUSTIFYING_EXCEPTION_CODES` = BOARD-APPROVED / CONTRACTUAL-COMMITMENT / EMERGENCY-SPEND; decoys = WILL-REIMBURSE / ONE-TIME). REST: `exceptions/route.ts` + `exceptions/[id]/finalize/route.ts`. **This plan moves that flow inline into the chat — see §3.2.** | +| **5. Each mutation is recorded on the thread** | After `open()` and after `finalize()`, the UI calls `recordUserAction({title, description, previousData, newData, metadata})`. `previousData` carries the gated flags (`approvePermitted: false`); `newData` the unlocked effect (flipped flags + the linking exception id); `metadata` the `transactionId`. | Two existing calls in `policy-exception-modal.tsx` (`policy_exception.opened`, `policy_exception.finalized`) + two in `transactions-list.tsx` (`transaction.approved`, `transaction.denied`). The import is the no-op shim `@/lib/record-user-action` **today** — see Blocker 2a. | +| **6. Events stream to the gateway** | With the real hook + Intelligence runtime, every AG-UI event of the run (including the recorded user actions) streams over the Phoenix WebSocket to the Intelligence gateway, scoped to the current user + thread. | The env-gated `CopilotKitIntelligence({apiUrl,wsUrl,apiKey})` branch of `createRuntime()` in the runtime route; `identifyUser` maps `properties.userRole` → a stable `northwind-` id so threads + knowledge are scoped consistently. Reference: `cpk-intelligence-banking/demos/e-commerce/bff/.../main.ts`. | +| **7. Distilled into `/knowledge`** | The `sl-worker` sweeps the recorded actions and an LLM writer distills a reusable procedure: _"to approve an over-policy-limit transaction, open a policy exception under a justifying code (board-approved / contractual / emergency), finalize it, then approve."_ It lands in `/knowledge` (shared per org+project). | `apps/sl-worker` in the Intelligence repo (gated on `SL_ENABLED=true`); writes `cpki.knowledge_base_files` exposed to agents as `/knowledge`. | +| **8. Agent B (fresh) learns + succeeds** | In a NEW thread with no memory of the human, the agent is asked the same over-limit approval. It greps `/knowledge` (via the `copilotkit_knowledge_base_shell` tool), discovers the procedure, files a _justifying_ exception, finalizes it, approves → **201** — no human help, nothing added to the prompt. | Same `bankingAgent` prompt (still recipe-free) reading `/knowledge`. This is the **proof of learning** — see §5. | The contrast in step 5 (`previousData` gated flags vs `newData` unlocked flags) is the signal the distiller turns into the procedure — which is why the flag names must stay stable across @@ -78,6 +79,7 @@ the distiller turns into the procedure — which is why the flag names must stay ### 2a. The recording hook (FOR-146) — pin the build, swap one import **Current state (verified).** + - Banking demo `package.json` pins CopilotKit packages as `workspace:*`; the installed `@copilotkit/react-core` is **1.59.2**, whose `v2` hooks index exports `useFrontendTool` / `useHumanInTheLoop` / `useAgent` / `useThreads` / `useComponent` / @@ -100,11 +102,11 @@ the distiller turns into the procedure — which is why the flag names must stay `e103a19` pins (at minimum `@copilotkit/react-core`, plus `@copilotkit/core`, `@copilotkit/runtime`, `@copilotkit/shared` to keep the runtime route's `CopilotKitIntelligence` / `BuiltInAgent` imports on the same line). Re-install. - *Cost:* the demo leaves the OSS monorepo `workspace:*` graph; lockfile churn. Best when + _Cost:_ the demo leaves the OSS monorepo `workspace:*` graph; lockfile churn. Best when the demo is being **vendored into the Intelligence repo** (where these pins already exist — see §6). - **(B) Land the recording hook into the OSS `react-core/v2` build** the demo's - `workspace:*` already resolves, then bump. *Cost:* a real OSS change; out of scope for + `workspace:*` already resolves, then bump. _Cost:_ a real OSS change; out of scope for this demo plan but the cleaner long-term home. Treat as a CopilotKit-core ticket. 2. **The one-line import swap** at each of the **four** call sites. Change ONLY the import; every `recordUserAction({...})` body and the `UserActionRecord` type stay identical: @@ -145,16 +147,16 @@ const intelligenceEnabled = Boolean( // missing → new CopilotRuntime({ agents:{default:bankingAgent}, runner: new InMemoryAgentRunner() }) ``` -So no route code changes to *turn on* the backend — it is purely a deploy + env exercise. +So no route code changes to _turn on_ the backend — it is purely a deploy + env exercise. What each var points at: -| Env var | Points at | Local-dev value | Notes | -|---------|-----------|-----------------|-------| -| `INTELLIGENCE_API_URL` | `apps/app-api` HTTP — the `/knowledge` + threads + user_actions store | `http://localhost:7050` (`APP_API_PORT` default in `scripts/local-dev.sh`) | The durable backend the gateway writes to and `/knowledge` is read from. | -| `INTELLIGENCE_GATEWAY_WS_URL` | `apps/realtime-gateway` Phoenix WebSocket — where AG-UI run events (incl. recorded actions) stream | `ws://localhost:7053` (`REALTIME_GATEWAY_PORT` default) | The live ingestion seam. | -| `INTELLIGENCE_API_KEY` | the org/project key (scopes threads + `/knowledge`) | the seeded `cpk_…` key for `casa-de-erlang` (e-commerce uses `cpk_sPRVSEED_seed0privat0longtoken00`) | Must belong to an org whose `cpki.users` includes the demo identities `identifyUser` mints (`northwind-`), or those users must be seeded. | -| `COPILOTKIT_LICENSE_TOKEN` | optional, read automatically by the runtime | — | Only if the build requires it. | -| `SL_ENABLED` | gate on the **`sl-worker`** distillation sweep (Intelligence side) | `true` | Without it the worker won't distill — recording streams but `/knowledge` never fills. | +| Env var | Points at | Local-dev value | Notes | +| ----------------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| `INTELLIGENCE_API_URL` | `apps/app-api` HTTP — the `/knowledge` + threads + user_actions store | `http://localhost:7050` (`APP_API_PORT` default in `scripts/local-dev.sh`) | The durable backend the gateway writes to and `/knowledge` is read from. | +| `INTELLIGENCE_GATEWAY_WS_URL` | `apps/realtime-gateway` Phoenix WebSocket — where AG-UI run events (incl. recorded actions) stream | `ws://localhost:7053` (`REALTIME_GATEWAY_PORT` default) | The live ingestion seam. | +| `INTELLIGENCE_API_KEY` | the org/project key (scopes threads + `/knowledge`) | the seeded `cpk_…` key for `casa-de-erlang` (e-commerce uses `cpk_sPRVSEED_seed0privat0longtoken00`) | Must belong to an org whose `cpki.users` includes the demo identities `identifyUser` mints (`northwind-`), or those users must be seeded. | +| `COPILOTKIT_LICENSE_TOKEN` | optional, read automatically by the runtime | — | Only if the build requires it. | +| `SL_ENABLED` | gate on the **`sl-worker`** distillation sweep (Intelligence side) | `true` | Without it the worker won't distill — recording streams but `/knowledge` never fills. | **Standing it up — two paths.** @@ -177,7 +179,7 @@ What each var points at: distilled procedure appearing in `/knowledge` (verifiable by grepping `/knowledge` via the agent or inspecting `cpki.knowledge_base_files`). -**Ordering.** 2a and 2b are independent to *build* but both required for the live loop. Do +**Ordering.** 2a and 2b are independent to _build_ but both required for the live loop. Do 2b's standup in parallel with the UX shell; 2a is the final flip. The cleanest single move is §6 (vendor into the Intelligence repo), which clears 2a and 2b together because the pins and services already live there. @@ -209,11 +211,15 @@ useConfigureSuggestions({ suggestions: [ { title: "Approve the $5,000 Marketing transaction", - message: "Approve the $5,000 Google Ads transaction on the Marketing policy.", + message: + "Approve the $5,000 Google Ads transaction on the Marketing policy.", }, { title: "View transactions", message: "Show me my recent transactions" }, { title: "Add a card", message: "Add a new credit card" }, - { title: "Assign a policy", message: "Assign a spending policy to one of my cards" }, + { + title: "Assign a policy", + message: "Assign a spending policy to one of my cards", + }, ], }); ``` @@ -230,11 +236,12 @@ useConfigureSuggestions({ ### 3.2 Inline HITL — approve/deny + file-exception rendered IN the chat **Goal.** The human's whole demonstration — see the over-limit symptom, approve/deny, and -*file a policy exception* — happens **inline in the chat as a tool-call card**, not in a +_file a policy exception_ — happens **inline in the chat as a tool-call card**, not in a separate page modal. That's what makes the recorded demonstration feel like "the agent watched me do it right here." **What exists today.** + - The approve/deny inline card is **already** an inline HITL: `showAndApproveTransactions` (`src/app/page.tsx`, ~line 405) renders `` inside the chat via `useHumanInTheLoop`'s `render`. That list shows the **over-limit symptom** @@ -256,21 +263,21 @@ watched me do it right here." - **File exception** (calls `openPolicyException` → `finalizePolicyException`, the same REST callers threaded from `useCreditCards` in `actions.ts`), - and on success a confirmation + the approve affordance. - It carries the **same two `recordUserAction` calls** (`policy_exception.opened` → - `policy_exception.finalized`) verbatim from the modal — the recording payloads do not - change. + It carries the **same two `recordUserAction` calls** (`policy_exception.opened` → + `policy_exception.finalized`) verbatim from the modal — the recording payloads do not + change. 2. **A new inline HITL tool `fileAndApproveOverLimit`** (a `useHumanInTheLoop` in `src/app/page.tsx`) whose `render` mounts `PolicyExceptionInline`. Description stays **neutral** (does not name the exception path or which codes justify — preserves the - learning invariant), e.g. *"Resolve a blocked over-limit approval. Requires human - approval."* This becomes the inline surface the human uses to teach, and later the surface + learning invariant), e.g. _"Resolve a blocked over-limit approval. Requires human + approval."_ This becomes the inline surface the human uses to teach, and later the surface the learned agent's `openPolicyException` / `finalizePolicyException` calls render into. 3. **Reuse vs replace.** - **Reuse** `approval-buttons.tsx` unchanged for approve/deny. - **Reuse** the existing `openPolicyException` / `finalizePolicyException` HITL tools (`src/app/page.tsx`, ~lines 492 / 549) — they already render inline approve cards; the learned agent drives the unlock through these. `PolicyExceptionInline` is the - *human-initiated* twin. + _human-initiated_ twin. - **Replace** the page-modal entrypoint: drop the `setExceptionTxnId` → `` branch in `transactions-list.tsx` in favor of rendering `PolicyExceptionInline` within the chat card. Keep `policy-exception-modal.tsx` only if a non-chat entry is still wanted; @@ -293,6 +300,7 @@ exposing `isRecording` + `beginRecording()` / `endRecording()` (or a ref-counted `withRecording()` wrapper). Wire it around the **`recordUserAction` calls**: each call site calls `beginRecording()` immediately before firing the record(s) and `endRecording()` when the demonstration step settles. Concretely: + - In `PolicyExceptionInline` (§3.2): `beginRecording()` at the start of `handleSubmit`, and `endRecording()` after the second (`finalized`) record resolves (or in `finally`). - In `transactions-list.tsx` approve/deny: wrap the `recordUserAction(...)` call the same way. @@ -305,6 +313,7 @@ demonstration step settles. Concretely: > (FOR-148) and stays correct once the real hook streams (FOR-146). **Visual treatment.** + - An **edge vignette**: a full-viewport overlay with `box-shadow: inset 0 0 0 …` / `radial-gradient` mask so color concentrates at the **edges** and fades to transparent in the center (content stays unobscured). @@ -338,15 +347,15 @@ wrapping in `policy-exception-inline.tsx` and `transactions-list.tsx`. ## 4. Buildable NOW vs blocked — the explicit split -| Piece | Ticket | Needs the real hook? | Needs the Intelligence backend? | Buildable today? | -|-------|--------|----------------------|---------------------------------|------------------| -| Suggested prompt (§3.1) | FOR-148 | No | No | **Yes** — pure `useConfigureSuggestions` copy. | -| Inline HITL card (§3.2) | FOR-148 | No (renders the demonstration UI; recording payloads already present via the shim) | No | **Yes** — renders + drives REST unlock; records via the shim. | -| Recording vignette (§3.3) | FOR-148 | No (reads a local `recording` flag set around the record calls) | No | **Yes** — flag is true even against the no-op shim. | -| Recording actually streams (role #3 beyond no-op) | FOR-146 | **Yes** (pin `e103a19` + import swap) | Indirectly (events have somewhere to go) | No — blocked on 2a. | -| Distill → `/knowledge` (role #5) | FOR-147 | — | **Yes** (`app-api` + gateway + `sl-worker`, `SL_ENABLED=true`) | No — blocked on 2b. | -| Fresh-agent learns + succeeds | FOR-149 | Yes | Yes | No — needs 2a + 2b. | -| Fresh-agent verification harness | FOR-145 | The script half works today (§5); the learning half needs 2a+2b | Partial | The REST proof: **yes**. The learning proof: blocked. | +| Piece | Ticket | Needs the real hook? | Needs the Intelligence backend? | Buildable today? | +| ------------------------------------------------- | ------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------- | +| Suggested prompt (§3.1) | FOR-148 | No | No | **Yes** — pure `useConfigureSuggestions` copy. | +| Inline HITL card (§3.2) | FOR-148 | No (renders the demonstration UI; recording payloads already present via the shim) | No | **Yes** — renders + drives REST unlock; records via the shim. | +| Recording vignette (§3.3) | FOR-148 | No (reads a local `recording` flag set around the record calls) | No | **Yes** — flag is true even against the no-op shim. | +| Recording actually streams (role #3 beyond no-op) | FOR-146 | **Yes** (pin `e103a19` + import swap) | Indirectly (events have somewhere to go) | No — blocked on 2a. | +| Distill → `/knowledge` (role #5) | FOR-147 | — | **Yes** (`app-api` + gateway + `sl-worker`, `SL_ENABLED=true`) | No — blocked on 2b. | +| Fresh-agent learns + succeeds | FOR-149 | Yes | Yes | No — needs 2a + 2b. | +| Fresh-agent verification harness | FOR-145 | The script half works today (§5); the learning half needs 2a+2b | Partial | The REST proof: **yes**. The learning proof: blocked. | **Why the UX is safe to build first.** The three UX pieces only depend on (a) the v2 chat + HITL APIs the demo already uses, and (b) a local `recording` boolean. The no-op shim already @@ -385,6 +394,7 @@ BASE_URL=http://localhost:3000 ./verify-teachable-gate.sh # next dev defaults ``` It asserts, in order: + - **A. GATE** — `PUT /api/v1/transactions/t-1 {"status":"approved"}` → **422 `OVER_POLICY_LIMIT`**, and the body does **not** mention the exception/unlock path (symptom-only invariant). @@ -396,7 +406,7 @@ It asserts, in order: enumerate the catalogue (non-enumeration invariant). This proves the gate is real and the unlock is discriminating — i.e. there is genuinely -*something to learn* — without any Intelligence backend. It is the control that the demo isn't +_something to learn_ — without any Intelligence backend. It is the control that the demo isn't faked. Re-run from a fresh server to reseed (in-memory store). ### 5.2 The fresh-agent proof (activates after 2a + 2b) — roles #3 + #5 — FOR-149 @@ -404,19 +414,19 @@ faked. Re-run from a fresh server to reseed (in-memory store). This is the proof the loop **learned**, not that REST works. Requires the real hook (2a) and the env-gated `CopilotKitIntelligence` backend with `SL_ENABLED=true` (2b). -1. **Baseline (control).** Fresh thread, ask: *"Approve the $5,000 Google Ads transaction on - the Marketing policy."* With the recipe-free prompt + ACTION DISCIPLINE intact, the agent +1. **Baseline (control).** Fresh thread, ask: _"Approve the $5,000 Google Ads transaction on + the Marketing policy."_ With the recipe-free prompt + ACTION DISCIPLINE intact, the agent hits the gate, has no procedure, and **reports the failure** instead of firing a distractor. - *This failure is the control — record it.* + _This failure is the control — record it._ 2. **Human teaches.** Open the inline policy-exception card (§3.2), pick a **justifying** code, file + finalize. Each step fires `recordUserAction(...)` on the current thread — now a real stream (2a), and the vignette (§3.3) confirms recording is live on screen. 3. **Distill.** The `sl-worker` (2b, `SL_ENABLED=true`) distills the recorded actions into a - reusable procedure in `/knowledge`. *Spot-check:* grep `/knowledge` (via the agent or + reusable procedure in `/knowledge`. _Spot-check:_ grep `/knowledge` (via the agent or `cpki.knowledge_base_files`) and confirm the over-limit/policy-exception procedure exists. 4. **Fresh agent succeeds unaided.** A **new** thread (and ideally a different seeded user, to prove cross-thread/cross-user transfer), same approval request. The agent greps `/knowledge`, - files a *justifying* exception, finalizes, approves → **201** — **no human help, nothing + files a _justifying_ exception, finalizes, approves → **201** — **no human help, nothing added to the prompt.** **Pass criteria:** step 1 fails, step 4 succeeds, and the **only** thing that changed between @@ -425,9 +435,10 @@ the REST proof (§5.1) gates "is there something to learn," the fresh-agent run learn it." **Anti-cheat checks (keep the proof honest):** + - The agent prompt at step 4 is **byte-identical** to step 1 (recipe still withheld — diff the runtime route prompt). -- A run that files a **decoy** code must still fail (the agent must learn *which* codes justify, +- A run that files a **decoy** code must still fail (the agent must learn _which_ codes justify, not just "file an exception"). - Distractor tools must remain harmless no-ops (a "success" from `sendSpendAlert` must not be mistaken for clearing the gate). @@ -436,8 +447,9 @@ learn it." ## 6. Recommended path: vendor into the Intelligence repo (clears 2a + 2b together) -The standalone Next.js banking demo can *render* the full UX today, but the **live learning +The standalone Next.js banking demo can _render_ the full UX today, but the **live learning loop's natural home is the Intelligence repo**, because: + - it already pins `@copilotkit/react-core@e103a19` (the hook-bearing build) — **2a is free there**; - it already runs `app-api` + `realtime-gateway` + `sl-worker` via `scripts/local-dev.sh`, and @@ -464,8 +476,9 @@ gives for free. ## 7. File / component touch-point summary **Edit (UX shell, FOR-148 — buildable now):** + - `src/app/wrapper.tsx` — add the teachable suggestion pill (§3.1); mount `RecordingProvider` - + `` (§3.3). + - `` (§3.3). - `src/app/page.tsx` — add the `fileAndApproveOverLimit` inline HITL tool whose `render` mounts `PolicyExceptionInline` (§3.2). - `src/components/transactions-list.tsx` — swap the page-modal entry for the inline card; wrap @@ -473,6 +486,7 @@ gives for free. - `src/app/globals.css` — `.recording-vignette` + `@keyframes` + reduced-motion variant (§3.3). **Create (UX shell, FOR-148):** + - `src/components/policy-exception-inline.tsx` — inline version of the file-exception flow (carries the two existing recording payloads verbatim). - `src/components/recording-context.tsx` — `isRecording` + `begin/endRecording` (ref-counted, @@ -480,6 +494,7 @@ gives for free. - `src/components/recording-vignette.tsx` — the edge-glow overlay. **Edit (unblock the loop):** + - `package.json` — pin `@copilotkit/react-core` (+ core/runtime/shared) to `e103a19` (FOR-146, option A) — or do this via vendoring (§6). - `src/lib/record-user-action.ts` — turn into a re-export of the real hook (FOR-146) so no call @@ -501,7 +516,7 @@ react/.../order-actions-bar.tsx, react/.../incident-create-modal.tsx}` and ## 8. Risks / open questions - **Hook parity at `e103a19`.** Confirm the published `react-core@e103a19` `v2` index exports - `useRecordUserActionInCurrentThread` *and* the `UserActionRecord` type (e-commerce imports the + `useRecordUserActionInCurrentThread` _and_ the `UserActionRecord` type (e-commerce imports the hook; verify the type export before relying on it in the re-export shim — otherwise keep the local type). - **Pin drift vs `workspace:*`.** Pinning the demo to `e103a19` takes it off the OSS monorepo @@ -512,8 +527,7 @@ react/.../order-actions-bar.tsx, react/.../incident-create-modal.tsx}` and org must have those users seeded (e-commerce seeds four users in `casa-de-erlang`). Mismatch = threads/knowledge land under an unexpected scope and the fresh-agent retrieval misses. - **Writer non-determinism.** The LLM distiller may phrase `/knowledge` differently run to run; - keep a deterministic fallback note for scripted live demos, and assert on *behavior* (the - 201) not on the knowledge text. + keep a deterministic fallback note for scripted live demos, and assert on _behavior_ (the 201) not on the knowledge text. - **Vignette over modals.** Ensure the overlay's `z-index` sits above page content but **below** HITL cards/toasts, and `pointer-events: none` everywhere, so it never blocks the approve buttons the human needs during recording. @@ -525,10 +539,10 @@ react/.../order-actions-bar.tsx, react/.../incident-create-modal.tsx}` and ## 9. Ticket map (suggested) -| Ticket | Scope | -|--------|-------| -| **FOR-145** | Verification harness: REST gate proof (works today) + fresh-agent learning proof (activates post-146/147). | -| **FOR-146** | Recording hook unblock: pin `e103a19` (or re-export shim) + one-line import swap. | +| Ticket | Scope | +| ----------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| **FOR-145** | Verification harness: REST gate proof (works today) + fresh-agent learning proof (activates post-146/147). | +| **FOR-146** | Recording hook unblock: pin `e103a19` (or re-export shim) + one-line import swap. | | **FOR-147** | Intelligence backend standup: `app-api` + gateway + `sl-worker` (`SL_ENABLED=true`); wire the three env vars (local-dev or hosted). | -| **FOR-148** | Teachable-demo UX shell (buildable now): suggested prompt + inline HITL card + recording vignette. | -| **FOR-149** | Close the loop: with 146+147 live, demonstrate fresh-agent success unaided and capture it in the harness. | +| **FOR-148** | Teachable-demo UX shell (buildable now): suggested prompt + inline HITL card + recording vignette. | +| **FOR-149** | Close the loop: with 146+147 live, demonstrate fresh-agent success unaided and capture it in the harness. | diff --git a/examples/showcases/banking/src/app/api/v1/exceptions/route.ts b/examples/showcases/banking/src/app/api/v1/exceptions/route.ts index da1430b401c..8b00742bb18 100644 --- a/examples/showcases/banking/src/app/api/v1/exceptions/route.ts +++ b/examples/showcases/banking/src/app/api/v1/exceptions/route.ts @@ -7,7 +7,10 @@ export const POST = async (req: NextRequest) => { try { const body = await req.json(); code = body?.code; - const exception = store.openPolicyException(body?.transactionId, code as string); + const exception = store.openPolicyException( + body?.transactionId, + code as string, + ); return new Response(JSON.stringify(exception), { status: 201 }); } catch (error) { const message = error instanceof Error ? error.message : ""; diff --git a/examples/showcases/banking/src/components/change-pin-dialog.tsx b/examples/showcases/banking/src/components/change-pin-dialog.tsx index cffbdf75dc5..19de6c9b7c9 100644 --- a/examples/showcases/banking/src/components/change-pin-dialog.tsx +++ b/examples/showcases/banking/src/components/change-pin-dialog.tsx @@ -17,7 +17,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { Card } from "@/app/api/v1/data"; +import type { Card } from "@/app/api/v1/data"; export function ChangePinDialog({ onDialogOpenChange, @@ -48,7 +48,10 @@ export function ChangePinDialog({
- diff --git a/examples/showcases/banking/src/components/chat/chat-panel-header.tsx b/examples/showcases/banking/src/components/chat/chat-panel-header.tsx index 65e80d21546..b89fff5b04c 100644 --- a/examples/showcases/banking/src/components/chat/chat-panel-header.tsx +++ b/examples/showcases/banking/src/components/chat/chat-panel-header.tsx @@ -22,8 +22,7 @@ export function ChatPanelHeader() { const configuration = useCopilotChatConfiguration(); const { isInboxOpen, toggleInbox, startNewConversation } = useChatInbox(); - const title = - configuration?.labels.modalHeaderTitle ?? IDENTITY.assistant; + const title = configuration?.labels.modalHeaderTitle ?? IDENTITY.assistant; const closePanel = () => configuration?.setModalOpen?.(false); diff --git a/examples/showcases/banking/src/components/credit-card-details.tsx b/examples/showcases/banking/src/components/credit-card-details.tsx index a6cb45f535b..f430d9d92b6 100644 --- a/examples/showcases/banking/src/components/credit-card-details.tsx +++ b/examples/showcases/banking/src/components/credit-card-details.tsx @@ -48,7 +48,9 @@ export function CreditCardDetails({
) : ( -

No expense policy assigned

+

+ No expense policy assigned +

)} diff --git a/examples/showcases/banking/src/components/statistics-chart.tsx b/examples/showcases/banking/src/components/statistics-chart.tsx index 51b2429dbb8..17a9b43b0d6 100644 --- a/examples/showcases/banking/src/components/statistics-chart.tsx +++ b/examples/showcases/banking/src/components/statistics-chart.tsx @@ -132,8 +132,8 @@ export function StatisticsChart({ ))}

- Latest {formatCurrency(last?.value ?? 0)}; range{" "} - {formatCurrency(min)}–{formatCurrency(max)}. + Latest {formatCurrency(last?.value ?? 0)}; range {formatCurrency(min)}– + {formatCurrency(max)}.

); diff --git a/examples/showcases/banking/src/components/ui/button.tsx b/examples/showcases/banking/src/components/ui/button.tsx index 7b169a10807..c2a1b36a1d6 100644 --- a/examples/showcases/banking/src/components/ui/button.tsx +++ b/examples/showcases/banking/src/components/ui/button.tsx @@ -1,6 +1,7 @@ import * as React from "react"; import { Slot } from "@radix-ui/react-slot"; -import { cva, type VariantProps } from "class-variance-authority"; +import { cva } from "class-variance-authority"; +import type { VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; @@ -12,8 +13,7 @@ const buttonVariants = cva( // Signature violet→indigo gradient pill with a soft glow. default: "brand-gradient text-brand-foreground shadow-[0_8px_20px_hsl(252_83%_60%/0.28)] hover:shadow-[0_12px_28px_hsl(252_83%_60%/0.4)] hover:brightness-[1.05]", - destructive: - "bg-negative text-white shadow-sm hover:bg-negative/90", + destructive: "bg-negative text-white shadow-sm hover:bg-negative/90", outline: "border border-hairline bg-surface text-ink shadow-soft hover:bg-brand-soft hover:text-brand-indigo hover:border-brand/40", secondary: diff --git a/examples/showcases/banking/src/components/ui/card.tsx b/examples/showcases/banking/src/components/ui/card.tsx index fdccedb7b64..ababd623ed5 100644 --- a/examples/showcases/banking/src/components/ui/card.tsx +++ b/examples/showcases/banking/src/components/ui/card.tsx @@ -45,11 +45,7 @@ const CardDescription = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( -

+

)); CardDescription.displayName = "CardDescription"; diff --git a/examples/showcases/banking/src/components/ui/dropdown-menu.tsx b/examples/showcases/banking/src/components/ui/dropdown-menu.tsx index a01b6decf72..8c3cc4bb496 100644 --- a/examples/showcases/banking/src/components/ui/dropdown-menu.tsx +++ b/examples/showcases/banking/src/components/ui/dropdown-menu.tsx @@ -167,10 +167,7 @@ const DropdownMenuSeparator = React.forwardRef< >(({ className, ...props }, ref) => ( )); diff --git a/examples/showcases/banking/src/components/ui/select.tsx b/examples/showcases/banking/src/components/ui/select.tsx index ef9a8172791..4ca7007df4f 100644 --- a/examples/showcases/banking/src/components/ui/select.tsx +++ b/examples/showcases/banking/src/components/ui/select.tsx @@ -144,10 +144,7 @@ const SelectSeparator = React.forwardRef< >(({ className, ...props }, ref) => ( )); diff --git a/examples/showcases/banking/tsconfig.json b/examples/showcases/banking/tsconfig.json index 877b650fcf5..67494258ac6 100644 --- a/examples/showcases/banking/tsconfig.json +++ b/examples/showcases/banking/tsconfig.json @@ -1,10 +1,6 @@ { "compilerOptions": { - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -22,9 +18,7 @@ } ], "paths": { - "@/*": [ - "./src/*" - ] + "@/*": ["./src/*"] }, "target": "ES2017" }, @@ -35,7 +29,5 @@ ".next/types/**/*.ts", ".next/dev/types/**/*.ts" ], - "exclude": [ - "node_modules" - ] + "exclude": ["node_modules"] } From 34aecf1c9798b1a8131191bc885302576a9d60cd Mon Sep 17 00:00:00 2001 From: tylerslaton <54378333+tylerslaton@users.noreply.github.com> Date: Thu, 25 Jun 2026 22:48:57 +0000 Subject: [PATCH 12/12] chore: release bot-teams v0.1.0 --- packages/bot-teams/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bot-teams/package.json b/packages/bot-teams/package.json index 5290bcf0c8a..fddb1809f53 100644 --- a/packages/bot-teams/package.json +++ b/packages/bot-teams/package.json @@ -1,6 +1,6 @@ { "name": "@copilotkit/bot-teams", - "version": "0.0.1", + "version": "0.1.0", "description": "Microsoft Teams platform adapter for CopilotKit JSX bots (@copilotkit/bot).", "license": "MIT", "repository": {