From eb9edcf8c0bb464f4d52318b95b98dc743d2536d Mon Sep 17 00:00:00 2001 From: GeneralJerel <85066839+GeneralJerel@users.noreply.github.com> Date: Fri, 31 Jul 2026 08:20:45 -0700 Subject: [PATCH 1/4] chore(deps): adopt the latest CopilotKit canary pair MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves from channels 0.2.2-canary.rc-1 / runtime 1.63.3-canary.rc-1 to 0.4.1-canary.1785477663 / 1.64.2-canary.1785477663 — the newest published pair, and the first that carries managed image delivery in both directions plus tool-progress rendering owned by the SDK rather than the platform. Stable `latest` (channels 0.4.0, runtime 1.64.1) sits behind this canary line on both packages, so pinning stable would roll those changes back. --- app/cleanup.test.ts | 4 +- package.json | 4 +- pnpm-lock.yaml | 155 ++++++++++++++++++++++++-------------------- 3 files changed, 87 insertions(+), 76 deletions(-) diff --git a/app/cleanup.test.ts b/app/cleanup.test.ts index 0f55db3..53fc1d3 100644 --- a/app/cleanup.test.ts +++ b/app/cleanup.test.ts @@ -13,10 +13,10 @@ const packageJson = JSON.parse( describe("launch dependency and cleanup contract", () => { it("keeps the exact Channels and Runtime canaries", () => { expect(packageJson.dependencies["@copilotkit/channels"]).toBe( - "0.2.2-canary.rc-1", + "0.4.1-canary.1785477663", ); expect(packageJson.dependencies["@copilotkit/runtime"]).toBe( - "1.63.3-canary.rc-1", + "1.64.2-canary.1785477663", ); }); diff --git a/package.json b/package.json index 877bea7..3fd51f4 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,8 @@ }, "dependencies": { "@ag-ui/client": "^0.0.57", - "@copilotkit/channels": "0.2.2-canary.rc-1", - "@copilotkit/runtime": "1.63.3-canary.rc-1", + "@copilotkit/channels": "0.4.1-canary.1785477663", + "@copilotkit/runtime": "1.64.2-canary.1785477663", "dotenv": "^16.4.5", "playwright": "^1.49.0", "tsx": "^4.19.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 595aef7..29837da 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,11 +15,11 @@ importers: specifier: ^0.0.57 version: 0.0.57 '@copilotkit/channels': - specifier: 0.2.2-canary.rc-1 - version: 0.2.2-canary.rc-1(@ag-ui/core@0.0.57)(@opentelemetry/api@1.9.1)(@types/express@5.0.6)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76) + specifier: 0.4.1-canary.1785477663 + version: 0.4.1-canary.1785477663(@ag-ui/core@0.0.57)(@opentelemetry/api@1.9.1)(@types/express@5.0.6)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76) '@copilotkit/runtime': - specifier: 1.63.3-canary.rc-1 - version: 1.63.3-canary.rc-1(@cfworker/json-schema@4.1.1)(@langchain/core@1.2.3(@opentelemetry/api@1.9.1)(openai@6.47.0(ws@8.21.1)(zod@3.25.76))(ws@8.21.1))(@langchain/langgraph-sdk@1.9.27(@langchain/core@1.2.3(@opentelemetry/api@1.9.1)(openai@6.47.0(ws@8.21.1)(zod@3.25.76))(ws@8.21.1)))(@opentelemetry/api@1.9.1)(langchain@1.5.3(@langchain/core@1.2.3(@opentelemetry/api@1.9.1)(openai@6.47.0(ws@8.21.1)(zod@3.25.76))(ws@8.21.1))(@opentelemetry/api@1.9.1)(openai@6.47.0(ws@8.21.1)(zod@3.25.76))(ws@8.21.1))(openai@6.47.0(ws@8.21.1)(zod@3.25.76))(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1))) + specifier: 1.64.2-canary.1785477663 + version: 1.64.2-canary.1785477663(@cfworker/json-schema@4.1.1)(@langchain/core@1.2.3(@opentelemetry/api@1.9.1)(openai@6.47.0(ws@8.21.1)(zod@3.25.76))(ws@8.21.1))(@langchain/langgraph-sdk@1.9.27(@langchain/core@1.2.3(@opentelemetry/api@1.9.1)(openai@6.47.0(ws@8.21.1)(zod@3.25.76))(ws@8.21.1)))(@opentelemetry/api@1.9.1)(@types/express@5.0.6)(langchain@1.5.3(@langchain/core@1.2.3(@opentelemetry/api@1.9.1)(openai@6.47.0(ws@8.21.1)(zod@3.25.76))(ws@8.21.1))(@opentelemetry/api@1.9.1)(openai@6.47.0(ws@8.21.1)(zod@3.25.76))(ws@8.21.1))(openai@6.47.0(ws@8.21.1)(zod@3.25.76))(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1))) dotenv: specifier: ^16.4.5 version: 16.6.1 @@ -200,52 +200,52 @@ packages: '@cfworker/json-schema@4.1.1': resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} - '@copilotkit/channels-core@0.2.2-canary.rc-1': - resolution: {integrity: sha512-e1o6AGi67ylOHIFS/GgKWwv879sD4fe4Fuy42PhEDjj83/wQ8Ya5Tdv/X3A3g7elnJDa9nX1UVJvJMbztN3O5Q==} + '@copilotkit/channels-core@0.4.1-canary.1785477663': + resolution: {integrity: sha512-qiHalKlLfyKxehsx2YRPN04CCJtzDTtRER1pJR4+AX62+blaNildI/rgGQXfMRHKdKhPnzMrTTbi2EX06Bq1QQ==} peerDependencies: vitest: ^4.0.0 peerDependenciesMeta: vitest: optional: true - '@copilotkit/channels-discord@0.2.2-canary.rc-1': - resolution: {integrity: sha512-fqjWRhSjRMdh0Ghlisk0gnT8tTyrPR47RpywDUoMxKQfkvObNx+GmgEqc/0c3LmT7yz3+W1z7Lab4AahrAFm8w==} + '@copilotkit/channels-discord@0.4.1-canary.1785477663': + resolution: {integrity: sha512-BdzYYzP0yCiP9Bz1k8K1ru0JBzIPtJpXo3hLbAUv0aXIyGlcXTbKL12lavUnx4dF2ALf8I5A4IY+RusmzzoO5Q==} - '@copilotkit/channels-intelligence@0.2.2-canary.rc-1': - resolution: {integrity: sha512-LmjgGB/yuwqvDIiEXK3ZJ4xbaafxT0xuRPMyA7ChrR53y1R1Jta134n+oD4DauI7iHKI5mJ9Bbc0sPPQ9gp+yQ==} + '@copilotkit/channels-intelligence@0.4.1-canary.1785477663': + resolution: {integrity: sha512-m6Y1gl1c2WiPgnGevtv7jH8c7aYgIRuD56y7DtwImeBzgAaNJeBwKJEo9C16MES73W1Al7A7ei79gnwGu/bCqw==} - '@copilotkit/channels-slack@0.2.2-canary.rc-1': - resolution: {integrity: sha512-9Du0gAtoMBEpg0EgCVsM+RZrF+LzIrc7R0m430f5njZ4bIncdFzUNcQ9SPfn64OuIQUl1+WfeTHT4qpZpN4m0g==} + '@copilotkit/channels-slack@0.4.1-canary.1785477663': + resolution: {integrity: sha512-GXxb+eCjBbV56Wvj9o9iiYZUx9zM9w14nl4dDTd3EbCF1lUrhjDVgeX7qHscOBJUSOTnE0hmbvBrql/0cGsyjQ==} - '@copilotkit/channels-teams@0.2.2-canary.rc-1': - resolution: {integrity: sha512-bsswD71VQHh48x03hNEYEq1CFjTb9BSDA7F1bRBkF8LLCEcL4BslYwOL23H+Nn6vvDhz8OSSa+7Tih/jYO7svQ==} + '@copilotkit/channels-teams@0.4.1-canary.1785477663': + resolution: {integrity: sha512-jGrMhVc/MSDDGvoOVFLFuKirYX62k/YugoDkF+tlolLUVje5OJ/OwR+C3jOR7r8nGgVV8xOgvzHNel4GTOY2hg==} - '@copilotkit/channels-telegram@0.2.2-canary.rc-1': - resolution: {integrity: sha512-w98CTf53pIAEZ5z+lEZSLWceO3gjVjNCbTaghFuXf45WSx4FAdZeY4eINbOm/OHbWhrXMhWhExYDfQD3vNaawg==} + '@copilotkit/channels-telegram@0.4.1-canary.1785477663': + resolution: {integrity: sha512-cTDgqC99oBLFKbvA7mIz8TWOLhS22Ko+r5Q2si122b7Fxz2T3FKOXecq2nC4OTrRMJxWiNVnxYD8MaeY2bfpnw==} - '@copilotkit/channels-ui@0.2.2-canary.rc-1': - resolution: {integrity: sha512-S6z5OzXfrQs5XsQOUF5cGF3qMjzHdcb+rcqvOnruDLqLWdr3PQjHuPpqgjMxELxN1ru+bZsraFBTVROm0zcY9Q==} + '@copilotkit/channels-ui@0.4.1-canary.1785477663': + resolution: {integrity: sha512-8taGvbmk7R8w3+uI4gYObcgLTjc3ztRoY1U5z4RJHg0mUtIouNESg4s3+Vje/93NOz6csCfxAriaZGILR+A/9g==} - '@copilotkit/channels-whatsapp@0.2.2-canary.rc-1': - resolution: {integrity: sha512-aKPxgR3EJ7RI7Mr4Pey+LZp0s7S8If9n1S8/Y1RRqJCqhRFkLcvXAeDCVp79k4p/HzFxzKzLpHhaLSGkOOOwAQ==} + '@copilotkit/channels-whatsapp@0.4.1-canary.1785477663': + resolution: {integrity: sha512-WyPTbWoYCulvWB6HkuRx+7rUheTloy61P11h6kuKpcM8DlHbEgEQRd0pifWD8K9xBWlYNtqlZ2CQwyw3OcDepQ==} - '@copilotkit/channels@0.2.2-canary.rc-1': - resolution: {integrity: sha512-hSc9Cvpak9M4UYLRcDTe2IkypKa87XytBhoMTBc/YLSXWudTtZMuLqlNnw2Ig+v4KYL7zo1h06pAlhGM4HLqXQ==} + '@copilotkit/channels@0.4.1-canary.1785477663': + resolution: {integrity: sha512-4/YRn7s0WnGwZONmi4bbl0GxKgbcbB4LeOkMvBjKenut+dMtb+Ft90G2VeUj5ZgYzrgypPmmbCHV2vTREJ5ysQ==} peerDependencies: vitest: ^4.0.0 peerDependenciesMeta: vitest: optional: true - '@copilotkit/core@1.63.3-canary.rc-1': - resolution: {integrity: sha512-0jjmGj0WqkAui6l9SjjXeV9Kx8womeLtdGeMsjSfQKiuEPi+JE2fG4I08pd7D/H7ijEXlL10u5DSbLO+hYeuaQ==} + '@copilotkit/core@1.64.2-canary.1785477663': + resolution: {integrity: sha512-qr6dSl5xiRFSkESGDhrjVU8ttngyo6se/dxAQfOs4wsH/sJdKT4agqkznCJExuWiQZ+v3jTA19TinKxxj5aZHg==} engines: {node: '>=18'} '@copilotkit/license-verifier@0.5.0': resolution: {integrity: sha512-vrwKtIpYwF0FT9ZoYASH8owa2cGV0dhDvJGaCRaRMStwDxpc6DRdydKkhx8cWZXyBRxEYcq/Vygv4JvevhQQdQ==} - '@copilotkit/runtime@1.63.3-canary.rc-1': - resolution: {integrity: sha512-j8jATXoqM8P3EmhMyWm4D6l3DUQSDWZwTeR0c3SgnljMgc3TAHD40m4c7pQ7xJ0XMXSvZqHDQYyAJjmltzYbXA==} + '@copilotkit/runtime@1.64.2-canary.1785477663': + resolution: {integrity: sha512-4s7Fe4ytbayw2ocoRvd5LMsO8uX/iDRwqikv4Z5ql/lv2oNELBympzxML8iu0wBoN5Wwl5tli7AXvcVcXUfnZw==} peerDependencies: '@anthropic-ai/sdk': '>=0.57.0' '@langchain/aws': '>=0.1.9' @@ -277,8 +277,8 @@ packages: openai: optional: true - '@copilotkit/shared@1.63.3-canary.rc-1': - resolution: {integrity: sha512-IGriuU7Z7jKQLLfqezo2L1DrJXWSUw33H65PLf4M0AfESaUhlsfcHom8YmA+E1v+PuXCr3MiWKH66fI1em42Dw==} + '@copilotkit/shared@1.64.2-canary.1785477663': + resolution: {integrity: sha512-j+KSTclsaYWyupt9oWyXQmN+nZf2O0UeS7JqtM9O0rPbN6Bg8zfmsYgJmkG7HIvYwSpv5swJ1XXF+u77XdvYhA==} peerDependencies: '@ag-ui/core': '>=0.0.48' @@ -2594,13 +2594,13 @@ snapshots: '@cfworker/json-schema@4.1.1': {} - '@copilotkit/channels-core@0.2.2-canary.rc-1(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76)': + '@copilotkit/channels-core@0.4.1-canary.1785477663(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76)': dependencies: '@ag-ui/client': 0.0.57 '@ag-ui/core': 0.0.57 - '@copilotkit/channels-ui': 0.2.2-canary.rc-1(@ag-ui/core@0.0.57) - '@copilotkit/core': 1.63.3-canary.rc-1(@ag-ui/core@0.0.57)(zod@3.25.76) - '@copilotkit/shared': 1.63.3-canary.rc-1(@ag-ui/core@0.0.57) + '@copilotkit/channels-ui': 0.4.1-canary.1785477663(@ag-ui/core@0.0.57) + '@copilotkit/core': 1.64.2-canary.1785477663(@ag-ui/core@0.0.57)(zod@3.25.76) + '@copilotkit/shared': 1.64.2-canary.1785477663(@ag-ui/core@0.0.57) zod-to-json-schema: 3.25.2(zod@3.25.76) optionalDependencies: vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)) @@ -2608,11 +2608,11 @@ snapshots: - encoding - zod - '@copilotkit/channels-discord@0.2.2-canary.rc-1(@ag-ui/core@0.0.57)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))': + '@copilotkit/channels-discord@0.4.1-canary.1785477663(@ag-ui/core@0.0.57)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))': dependencies: '@ag-ui/client': 0.0.57 - '@copilotkit/channels-core': 0.2.2-canary.rc-1(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76) - '@copilotkit/channels-ui': 0.2.2-canary.rc-1(@ag-ui/core@0.0.57) + '@copilotkit/channels-core': 0.4.1-canary.1785477663(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76) + '@copilotkit/channels-ui': 0.4.1-canary.1785477663(@ag-ui/core@0.0.57) discord.js: 14.27.0 zod: 3.25.76 transitivePeerDependencies: @@ -2622,26 +2622,35 @@ snapshots: - utf-8-validate - vitest - '@copilotkit/channels-intelligence@0.2.2-canary.rc-1(@ag-ui/core@0.0.57)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76)': + '@copilotkit/channels-intelligence@0.4.1-canary.1785477663(@ag-ui/core@0.0.57)(@opentelemetry/api@1.9.1)(@types/express@5.0.6)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76)': dependencies: '@ag-ui/client': 0.0.57 - '@copilotkit/channels-core': 0.2.2-canary.rc-1(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76) - '@copilotkit/channels-ui': 0.2.2-canary.rc-1(@ag-ui/core@0.0.57) + '@copilotkit/channels-core': 0.4.1-canary.1785477663(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76) + '@copilotkit/channels-slack': 0.4.1-canary.1785477663(@types/express@5.0.6)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1))) + '@copilotkit/channels-teams': 0.4.1-canary.1785477663(@opentelemetry/api@1.9.1)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1))) + '@copilotkit/channels-ui': 0.4.1-canary.1785477663(@ag-ui/core@0.0.57) phoenix: 1.8.9 transitivePeerDependencies: - '@ag-ui/core' + - '@opentelemetry/api' + - '@opentelemetry/api-logs' + - '@types/express' + - bufferutil + - debug - encoding + - supports-color + - utf-8-validate - vitest - zod - '@copilotkit/channels-slack@0.2.2-canary.rc-1(@types/express@5.0.6)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))': + '@copilotkit/channels-slack@0.4.1-canary.1785477663(@types/express@5.0.6)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))': dependencies: '@ag-ui/client': 0.0.57 '@ag-ui/core': 0.0.57 - '@copilotkit/channels-core': 0.2.2-canary.rc-1(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76) - '@copilotkit/channels-ui': 0.2.2-canary.rc-1(@ag-ui/core@0.0.57) - '@copilotkit/core': 1.63.3-canary.rc-1(@ag-ui/core@0.0.57)(zod@3.25.76) - '@copilotkit/shared': 1.63.3-canary.rc-1(@ag-ui/core@0.0.57) + '@copilotkit/channels-core': 0.4.1-canary.1785477663(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76) + '@copilotkit/channels-ui': 0.4.1-canary.1785477663(@ag-ui/core@0.0.57) + '@copilotkit/core': 1.64.2-canary.1785477663(@ag-ui/core@0.0.57)(zod@3.25.76) + '@copilotkit/shared': 1.64.2-canary.1785477663(@ag-ui/core@0.0.57) '@slack/bolt': 4.7.3(@types/express@5.0.6) '@slack/types': 2.22.0 '@slack/web-api': 7.19.0 @@ -2657,14 +2666,14 @@ snapshots: - utf-8-validate - vitest - '@copilotkit/channels-teams@0.2.2-canary.rc-1(@opentelemetry/api@1.9.1)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))': + '@copilotkit/channels-teams@0.4.1-canary.1785477663(@opentelemetry/api@1.9.1)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))': dependencies: '@ag-ui/client': 0.0.57 '@ag-ui/core': 0.0.57 - '@copilotkit/channels-core': 0.2.2-canary.rc-1(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76) - '@copilotkit/channels-ui': 0.2.2-canary.rc-1(@ag-ui/core@0.0.57) - '@copilotkit/core': 1.63.3-canary.rc-1(@ag-ui/core@0.0.57)(zod@3.25.76) - '@copilotkit/shared': 1.63.3-canary.rc-1(@ag-ui/core@0.0.57) + '@copilotkit/channels-core': 0.4.1-canary.1785477663(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76) + '@copilotkit/channels-ui': 0.4.1-canary.1785477663(@ag-ui/core@0.0.57) + '@copilotkit/core': 1.64.2-canary.1785477663(@ag-ui/core@0.0.57)(zod@3.25.76) + '@copilotkit/shared': 1.64.2-canary.1785477663(@ag-ui/core@0.0.57) '@microsoft/agents-activity': 1.7.1 '@microsoft/agents-hosting': 1.7.1(@opentelemetry/api@1.9.1) express: 4.22.2 @@ -2678,11 +2687,11 @@ snapshots: - supports-color - vitest - '@copilotkit/channels-telegram@0.2.2-canary.rc-1(@ag-ui/core@0.0.57)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))': + '@copilotkit/channels-telegram@0.4.1-canary.1785477663(@ag-ui/core@0.0.57)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))': dependencies: '@ag-ui/client': 0.0.57 - '@copilotkit/channels-core': 0.2.2-canary.rc-1(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76) - '@copilotkit/channels-ui': 0.2.2-canary.rc-1(@ag-ui/core@0.0.57) + '@copilotkit/channels-core': 0.4.1-canary.1785477663(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76) + '@copilotkit/channels-ui': 0.4.1-canary.1785477663(@ag-ui/core@0.0.57) grammy: 1.44.0 zod: 3.25.76 transitivePeerDependencies: @@ -2691,34 +2700,33 @@ snapshots: - supports-color - vitest - '@copilotkit/channels-ui@0.2.2-canary.rc-1(@ag-ui/core@0.0.57)': + '@copilotkit/channels-ui@0.4.1-canary.1785477663(@ag-ui/core@0.0.57)': dependencies: - '@copilotkit/shared': 1.63.3-canary.rc-1(@ag-ui/core@0.0.57) + '@copilotkit/shared': 1.64.2-canary.1785477663(@ag-ui/core@0.0.57) transitivePeerDependencies: - '@ag-ui/core' - encoding - '@copilotkit/channels-whatsapp@0.2.2-canary.rc-1(@ag-ui/core@0.0.57)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76)': + '@copilotkit/channels-whatsapp@0.4.1-canary.1785477663(@ag-ui/core@0.0.57)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76)': dependencies: '@ag-ui/client': 0.0.57 - '@copilotkit/channels-core': 0.2.2-canary.rc-1(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76) - '@copilotkit/channels-ui': 0.2.2-canary.rc-1(@ag-ui/core@0.0.57) + '@copilotkit/channels-core': 0.4.1-canary.1785477663(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76) + '@copilotkit/channels-ui': 0.4.1-canary.1785477663(@ag-ui/core@0.0.57) transitivePeerDependencies: - '@ag-ui/core' - encoding - vitest - zod - '@copilotkit/channels@0.2.2-canary.rc-1(@ag-ui/core@0.0.57)(@opentelemetry/api@1.9.1)(@types/express@5.0.6)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76)': + '@copilotkit/channels@0.4.1-canary.1785477663(@ag-ui/core@0.0.57)(@opentelemetry/api@1.9.1)(@types/express@5.0.6)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76)': dependencies: - '@copilotkit/channels-core': 0.2.2-canary.rc-1(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76) - '@copilotkit/channels-discord': 0.2.2-canary.rc-1(@ag-ui/core@0.0.57)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1))) - '@copilotkit/channels-intelligence': 0.2.2-canary.rc-1(@ag-ui/core@0.0.57)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76) - '@copilotkit/channels-slack': 0.2.2-canary.rc-1(@types/express@5.0.6)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1))) - '@copilotkit/channels-teams': 0.2.2-canary.rc-1(@opentelemetry/api@1.9.1)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1))) - '@copilotkit/channels-telegram': 0.2.2-canary.rc-1(@ag-ui/core@0.0.57)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1))) - '@copilotkit/channels-ui': 0.2.2-canary.rc-1(@ag-ui/core@0.0.57) - '@copilotkit/channels-whatsapp': 0.2.2-canary.rc-1(@ag-ui/core@0.0.57)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76) + '@copilotkit/channels-core': 0.4.1-canary.1785477663(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76) + '@copilotkit/channels-discord': 0.4.1-canary.1785477663(@ag-ui/core@0.0.57)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1))) + '@copilotkit/channels-slack': 0.4.1-canary.1785477663(@types/express@5.0.6)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1))) + '@copilotkit/channels-teams': 0.4.1-canary.1785477663(@opentelemetry/api@1.9.1)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1))) + '@copilotkit/channels-telegram': 0.4.1-canary.1785477663(@ag-ui/core@0.0.57)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1))) + '@copilotkit/channels-ui': 0.4.1-canary.1785477663(@ag-ui/core@0.0.57) + '@copilotkit/channels-whatsapp': 0.4.1-canary.1785477663(@ag-ui/core@0.0.57)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76) optionalDependencies: vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)) transitivePeerDependencies: @@ -2733,10 +2741,10 @@ snapshots: - utf-8-validate - zod - '@copilotkit/core@1.63.3-canary.rc-1(@ag-ui/core@0.0.57)(zod@3.25.76)': + '@copilotkit/core@1.64.2-canary.1785477663(@ag-ui/core@0.0.57)(zod@3.25.76)': dependencies: '@ag-ui/client': 0.0.57 - '@copilotkit/shared': 1.63.3-canary.rc-1(@ag-ui/core@0.0.57) + '@copilotkit/shared': 1.64.2-canary.1785477663(@ag-ui/core@0.0.57) '@tanstack/pacer': 0.20.1 phoenix: 1.8.9 rxjs: 7.8.2 @@ -2748,7 +2756,7 @@ snapshots: '@copilotkit/license-verifier@0.5.0': {} - '@copilotkit/runtime@1.63.3-canary.rc-1(@cfworker/json-schema@4.1.1)(@langchain/core@1.2.3(@opentelemetry/api@1.9.1)(openai@6.47.0(ws@8.21.1)(zod@3.25.76))(ws@8.21.1))(@langchain/langgraph-sdk@1.9.27(@langchain/core@1.2.3(@opentelemetry/api@1.9.1)(openai@6.47.0(ws@8.21.1)(zod@3.25.76))(ws@8.21.1)))(@opentelemetry/api@1.9.1)(langchain@1.5.3(@langchain/core@1.2.3(@opentelemetry/api@1.9.1)(openai@6.47.0(ws@8.21.1)(zod@3.25.76))(ws@8.21.1))(@opentelemetry/api@1.9.1)(openai@6.47.0(ws@8.21.1)(zod@3.25.76))(ws@8.21.1))(openai@6.47.0(ws@8.21.1)(zod@3.25.76))(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))': + '@copilotkit/runtime@1.64.2-canary.1785477663(@cfworker/json-schema@4.1.1)(@langchain/core@1.2.3(@opentelemetry/api@1.9.1)(openai@6.47.0(ws@8.21.1)(zod@3.25.76))(ws@8.21.1))(@langchain/langgraph-sdk@1.9.27(@langchain/core@1.2.3(@opentelemetry/api@1.9.1)(openai@6.47.0(ws@8.21.1)(zod@3.25.76))(ws@8.21.1)))(@opentelemetry/api@1.9.1)(@types/express@5.0.6)(langchain@1.5.3(@langchain/core@1.2.3(@opentelemetry/api@1.9.1)(openai@6.47.0(ws@8.21.1)(zod@3.25.76))(ws@8.21.1))(@opentelemetry/api@1.9.1)(openai@6.47.0(ws@8.21.1)(zod@3.25.76))(ws@8.21.1))(openai@6.47.0(ws@8.21.1)(zod@3.25.76))(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))': dependencies: '@ag-ui/a2ui-middleware': 0.0.10(@ag-ui/client@0.0.57)(rxjs@7.8.2) '@ag-ui/client': 0.0.57 @@ -2762,10 +2770,10 @@ snapshots: '@ai-sdk/google-vertex': 3.0.150(zod@3.25.76) '@ai-sdk/mcp': 1.0.62(zod@3.25.76) '@ai-sdk/openai': 3.0.85(zod@3.25.76) - '@copilotkit/channels-core': 0.2.2-canary.rc-1(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76) - '@copilotkit/channels-intelligence': 0.2.2-canary.rc-1(@ag-ui/core@0.0.57)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76) + '@copilotkit/channels-core': 0.4.1-canary.1785477663(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76) + '@copilotkit/channels-intelligence': 0.4.1-canary.1785477663(@ag-ui/core@0.0.57)(@opentelemetry/api@1.9.1)(@types/express@5.0.6)(vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(vite@8.1.4(@types/node@22.20.1)(esbuild@0.28.1)(tsx@4.23.1)))(zod@3.25.76) '@copilotkit/license-verifier': 0.5.0 - '@copilotkit/shared': 1.63.3-canary.rc-1(@ag-ui/core@0.0.57) + '@copilotkit/shared': 1.64.2-canary.1785477663(@ag-ui/core@0.0.57) '@graphql-yoga/plugin-defer-stream': 3.21.2(graphql-yoga@5.21.2(graphql@16.14.2))(graphql@16.14.2) '@hono/node-server': 1.19.14(hono@4.12.30) '@langchain/core': 1.2.3(@opentelemetry/api@1.9.1)(openai@6.47.0(ws@8.21.1)(zod@3.25.76))(ws@8.21.1) @@ -2800,9 +2808,12 @@ snapshots: transitivePeerDependencies: - '@cfworker/json-schema' - '@opentelemetry/api' + - '@opentelemetry/api-logs' - '@opentelemetry/exporter-trace-otlp-proto' - '@opentelemetry/sdk-trace-base' + - '@types/express' - bufferutil + - debug - encoding - react - react-dom @@ -2812,7 +2823,7 @@ snapshots: - vitest - vue - '@copilotkit/shared@1.63.3-canary.rc-1(@ag-ui/core@0.0.57)': + '@copilotkit/shared@1.64.2-canary.1785477663(@ag-ui/core@0.0.57)': dependencies: '@ag-ui/client': 0.0.57 '@ag-ui/core': 0.0.57 From 624e872027076a9678052b24e5841f7b0de04a00 Mon Sep 17 00:00:00 2001 From: GeneralJerel <85066839+GeneralJerel@users.noreply.github.com> Date: Fri, 31 Jul 2026 08:21:01 -0700 Subject: [PATCH 2/4] test(channel): adapt managed turn tests to the 0.4 turn contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two breaking changes in the Channels SDK reach these tests. `IncomingTurn` now requires `operation: MessageOperation` — provider-neutral revision identity (`kind`, `logicalMessageId`, `revisionId`, `mentioned`). A `turnOperation()` helper builds a `created`, channel-addressed revision with a distinct logical id per turn so revisions are never conflated. Every turn now runs on a clone of the agent (`isolateAgentInstance`), and `FakeAgent.clone()` constructs `new FakeAgent(...)` — which discards subclass overrides. The recording double therefore never observed a run, and neither did assertions reading `FakeAgent`'s own counters, since those accumulate on the clone rather than the instance the test holds. Recording now happens outside the instance and `clone()` re-applies this subclass's prototype, so assertions describe what OpenTag sent rather than the post-clone internals of the test double. --- app/channel.test.ts | 67 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 7 deletions(-) diff --git a/app/channel.test.ts b/app/channel.test.ts index fc2d492..adb255e 100644 --- a/app/channel.test.ts +++ b/app/channel.test.ts @@ -11,6 +11,7 @@ import { MemoryStore, type Channel, type ChannelNode, + type MessageOperation, } from "@copilotkit/channels"; import { defaultSlackContext, @@ -21,20 +22,63 @@ import { appContext } from "./context/app-context.js"; import { appTools } from "./tools/index.js"; import { createOpenTagChannel } from "./channel.js"; +/** + * Run inputs observed by whichever agent instance actually executed. + * + * The Channel isolates the agent per turn (`isolateAgentInstance`), so a turn + * runs on a clone and any state recorded on an instance field is invisible to + * the original object the test holds. Recording outside the instance keeps the + * assertions about what OpenTag *sent*, independent of clone semantics. + */ +interface RecordedRun { + parameters?: RunAgentParameters; + /** Snapshot at call time — the executing clone owns its own message log. */ + messages: FakeAgent["messages"]; +} + +const agentRuns: RecordedRun[] = []; + class CapturingAgent extends FakeAgent { - readonly calls: Array = []; + /** + * `FakeAgent.clone()` builds `new FakeAgent(...)`, which drops subclass + * overrides — and the Channel runs every turn on a clone. Re-apply this + * prototype so the recorder survives per-turn isolation. + */ + override clone(): this { + const cloned = super.clone(); + Object.setPrototypeOf(cloned, Object.getPrototypeOf(this)); + return cloned as this; + } override async runAgent( parameters?: RunAgentParameters, subscriber?: AgentSubscriber, ): Promise { - this.calls.push(parameters); + agentRuns.push({ parameters, messages: structuredClone(this.messages) }); return super.runAgent(parameters, subscriber); } } const channels: Channel[] = []; +let turnSeq = 0; + +// Managed turns carry provider-neutral revision identity. A plain inbound +// mention is a `created` revision that addresses the Channel; each turn needs +// its own logical id so revisions are never conflated. +function turnOperation( + overrides: Partial = {}, +): MessageOperation { + turnSeq += 1; + return { + kind: "created", + logicalMessageId: `m${turnSeq}`, + revisionId: `m${turnSeq}r1`, + mentioned: true, + ...overrides, + }; +} + function confirmWriteEnvelope( action = "Create Linear issue", detail: string | null = "CPK-9: Checkout 500s", @@ -104,6 +148,7 @@ function findIncidentButton( afterEach(async () => { await Promise.all(channels.splice(0).map((channel) => channel.ɵruntime.stop())); + agentRuns.length = 0; vi.restoreAllMocks(); }); @@ -150,6 +195,7 @@ describe("createOpenTagChannel", () => { await channel.ɵruntime.start(); await adapter.getSink().onTurn({ + operation: turnOperation(), conversationKey: "c1", replyTarget: {}, userText: "hello", @@ -157,7 +203,7 @@ describe("createOpenTagChannel", () => { user: { id: "U1", name: "Ada" }, }); - const call = (agent as CapturingAgent).calls[0]; + const call = agentRuns[0]?.parameters; expect(call?.tools?.map(({ name }) => name).sort()).toEqual( [...appTools, ...defaultSlackTools].map(({ name }) => name).sort(), ); @@ -176,6 +222,7 @@ describe("createOpenTagChannel", () => { await channel.ɵruntime.start(); await adapter.getSink().onTurn({ + operation: turnOperation(), conversationKey: "c1", replyTarget: {}, userText: "hello", @@ -183,7 +230,7 @@ describe("createOpenTagChannel", () => { user: { id: "T1", name: "Ada" }, }); - const call = (agent as CapturingAgent).calls[0]; + const call = agentRuns[0]?.parameters; expect(call?.tools?.map(({ name }) => name).sort()).toEqual( appTools.map(({ name }) => name).sort(), ); @@ -202,6 +249,7 @@ describe("createOpenTagChannel", () => { await channel.ɵruntime.start(); await adapter.getSink().onTurn({ + operation: turnOperation(), conversationKey: "c1", replyTarget: {}, userText: "fallback text", @@ -210,7 +258,7 @@ describe("createOpenTagChannel", () => { user: { id: "U1" }, }); - expect(agent.messages).toContainEqual( + expect(agentRuns[0]?.messages).toContainEqual( expect.objectContaining({ role: "user", content: parts }), ); }); @@ -274,6 +322,7 @@ describe("createOpenTagChannel", () => { await channel.ɵruntime.start(); await adapter.getSink().onTurn({ + operation: turnOperation(), conversationKey: "c1", replyTarget: {}, userText: "hello", @@ -313,6 +362,7 @@ describe("createOpenTagChannel", () => { await channel.ɵruntime.start(); const turn = adapter.getSink().onTurn({ + operation: turnOperation(), conversationKey: "c1", replyTarget: {}, userText: "file this", @@ -361,6 +411,7 @@ describe("createOpenTagChannel", () => { await channel.ɵruntime.start(); await adapter.getSink().onTurn({ + operation: turnOperation(), conversationKey: "c1", replyTarget: {}, userText: "file this", @@ -393,6 +444,7 @@ describe("createOpenTagChannel", () => { channels.push(firstChannel); await firstChannel.ɵruntime.start(); await firstAdapter.getSink().onTurn({ + operation: turnOperation(), conversationKey: "c1", replyTarget: {}, userText: "file this", @@ -410,7 +462,7 @@ describe("createOpenTagChannel", () => { const secondAdapter = new FakeAdapter({ platform: "intelligence" }); secondAdapter.stateStore = sharedState; - const secondAgent = new FakeAgent(); + const secondAgent = new CapturingAgent(); const secondChannel = createOpenTagChannel("opentag", secondAgent); secondChannel.ɵruntime.addAdapter(secondAdapter); channels.push(secondChannel); @@ -425,7 +477,7 @@ describe("createOpenTagChannel", () => { expect(secondAdapter.updated).toHaveLength(1); expect(JSON.stringify(secondAdapter.updated)).toContain("Approved"); - expect(secondAgent.runAgentCalls).toBe(1); + expect(agentRuns).toHaveLength(1); }); it("re-registers incident actions when a new Channel uses the same store", async () => { @@ -454,6 +506,7 @@ describe("createOpenTagChannel", () => { channels.push(firstChannel); await firstChannel.ɵruntime.start(); await firstAdapter.getSink().onTurn({ + operation: turnOperation(), conversationKey: "incident-thread", replyTarget: {}, userText: "show the incident", From 84559e1a796d100e2f1b94836cb4bc463dfde8ec Mon Sep 17 00:00:00 2001 From: GeneralJerel <85066839+GeneralJerel@users.noreply.github.com> Date: Fri, 31 Jul 2026 08:21:14 -0700 Subject: [PATCH 3/4] feat(session): build the "Open session" footer link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Groundwork for remote sessions: a managed reply can carry a link to the same conversation in the Intelligence console, plus the model badge for the turn that produced it. Config is all-or-nothing — a partial console location disables the footer rather than emitting a broken link — and rejects unresolved env templates, non-http schemes, and slugs that would escape the URL path. `pathSegment` selects the console view, so this points at the existing `/threads/:id` today and can move to a dedicated `/sessions/:id` without a code change. `sessionFooter` returns undefined when no canonical thread id is available, which is every turn today: an `IncomingTurn` carries `deliveryId` and `turnId` but no thread id, the delivery transcript response carries none, and the channel-threads route is not runtime-authed. Omitting the link beats guessing at one. Wiring follows once the platform surfaces it. The model badge mirrors the agent's own env resolution, so a test reads agent/agent.py and fails if those defaults drift — otherwise the badge would quietly claim the wrong model answered. --- app/session-link.test.ts | 162 +++++++++++++++++++++++++++++++++++++++ app/session-link.ts | 130 +++++++++++++++++++++++++++++++ 2 files changed, 292 insertions(+) create mode 100644 app/session-link.test.ts create mode 100644 app/session-link.ts diff --git a/app/session-link.test.ts b/app/session-link.test.ts new file mode 100644 index 0000000..252d537 --- /dev/null +++ b/app/session-link.test.ts @@ -0,0 +1,162 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { describe, expect, it } from "vitest"; +import { + AGENT_MODEL_DEFAULT, + AGENT_REASONING_DEFAULT, + agentModelBadge, + buildSessionUrl, + readSessionLinkConfig, + sessionFooter, + type SessionLinkConfig, +} from "./session-link.js"; + +const config: SessionLinkConfig = { + consoleUrl: "https://intelligence.example.com", + orgSlug: "acme-org", + projectSlug: "acme-project", + pathSegment: "threads", +}; + +const baseEnv = { + INTELLIGENCE_CONSOLE_URL: "https://intelligence.example.com", + INTELLIGENCE_ORG_SLUG: "acme-org", + INTELLIGENCE_PROJECT_SLUG: "acme-project", +} satisfies NodeJS.ProcessEnv; + +describe("readSessionLinkConfig", () => { + it("reads a complete configuration", () => { + expect(readSessionLinkConfig(baseEnv)).toEqual(config); + }); + + it("disables the footer when the console location is incomplete", () => { + for (const omitted of [ + "INTELLIGENCE_CONSOLE_URL", + "INTELLIGENCE_ORG_SLUG", + "INTELLIGENCE_PROJECT_SLUG", + ]) { + const env: NodeJS.ProcessEnv = { ...baseEnv }; + delete env[omitted]; + expect(readSessionLinkConfig(env)).toBeUndefined(); + } + }); + + it("honors an explicit off switch", () => { + expect( + readSessionLinkConfig({ ...baseEnv, SESSION_FOOTER: "off" }), + ).toBeUndefined(); + }); + + it("rejects an unresolved env template rather than linking to it", () => { + expect(() => + readSessionLinkConfig({ + ...baseEnv, + INTELLIGENCE_CONSOLE_URL: "https://${INTEL_HOST}", + }), + ).toThrow(/Unresolved template/); + }); + + it("rejects a non-http scheme", () => { + expect(() => + readSessionLinkConfig({ + ...baseEnv, + INTELLIGENCE_CONSOLE_URL: "javascript:alert(1)", + }), + ).toThrow(/scheme/); + }); + + it("rejects a slug that would escape the path", () => { + expect(() => + readSessionLinkConfig({ + ...baseEnv, + INTELLIGENCE_ORG_SLUG: "../../etc", + }), + ).toThrow(/INTELLIGENCE_ORG_SLUG/); + }); + + it("reduces a console url with a path to its origin", () => { + expect( + readSessionLinkConfig({ + ...baseEnv, + INTELLIGENCE_CONSOLE_URL: "https://intelligence.example.com/o/x", + })?.consoleUrl, + ).toBe("https://intelligence.example.com"); + }); +}); + +describe("buildSessionUrl", () => { + it("builds the console thread url", () => { + expect(buildSessionUrl(config, "ed1f290b-f826-4275-b56e-a4d22850117d")).toBe( + "https://intelligence.example.com/o/acme-org/acme-project/threads/ed1f290b-f826-4275-b56e-a4d22850117d", + ); + }); + + it("supports a dedicated sessions view without a code change", () => { + expect( + buildSessionUrl({ ...config, pathSegment: "sessions" }, "t1"), + ).toContain("/acme-project/sessions/t1"); + }); + + it("encodes the thread id", () => { + expect(buildSessionUrl(config, "a b/c")).toContain("/threads/a%20b%2Fc"); + }); + + it("requires a thread id", () => { + expect(() => buildSessionUrl(config, " ")).toThrow(/threadId/); + }); +}); + +describe("agentModelBadge", () => { + it("falls back to the agent's own defaults", () => { + expect(agentModelBadge({})).toBe( + `${AGENT_MODEL_DEFAULT}[${AGENT_REASONING_DEFAULT}]`, + ); + }); + + it("reports configured overrides", () => { + expect( + agentModelBadge({ + OPENAI_MODEL: "gpt-5.4-mini", + OPENAI_REASONING_EFFORT: "HIGH", + }), + ).toBe("gpt-5.4-mini[high]"); + }); +}); + +describe("sessionFooter", () => { + it("renders a Slack link and the model badge", () => { + expect(sessionFooter({ config, canonicalThreadId: "t1", env: {} })).toBe( + `<${buildSessionUrl(config, "t1")}|Open session> · ${AGENT_MODEL_DEFAULT}[${AGENT_REASONING_DEFAULT}]`, + ); + }); + + it("omits the footer when no canonical thread id is available", () => { + // The platform does not surface a canonical thread id to the runtime yet. + // Omitting beats linking somewhere wrong. + expect(sessionFooter({ config, env: {} })).toBeUndefined(); + expect( + sessionFooter({ config, canonicalThreadId: " ", env: {} }), + ).toBeUndefined(); + }); + + it("omits the footer when unconfigured", () => { + expect(sessionFooter({ canonicalThreadId: "t1", env: {} })).toBeUndefined(); + }); +}); + +describe("agent tuning defaults stay in sync with the agent", () => { + // The badge claims what the agent resolved. If agent.py's defaults move and + // these do not, the badge silently lies about the model that answered. + const agentSource = readFileSync( + resolve(process.cwd(), "agent/agent.py"), + "utf8", + ); + + it("mirrors the model default", () => { + expect(agentSource).toContain(`"OPENAI_MODEL", "${AGENT_MODEL_DEFAULT}"`); + }); + + it("mirrors the reasoning-effort default", () => { + expect(agentSource).toContain(`default="${AGENT_REASONING_DEFAULT}"`); + }); +}); diff --git a/app/session-link.ts b/app/session-link.ts new file mode 100644 index 0000000..fbd9284 --- /dev/null +++ b/app/session-link.ts @@ -0,0 +1,130 @@ +/** + * "Open session" footer support — the Slack-side half of remote sessions. + * + * A managed reply carries a link to the same conversation in the Intelligence + * console, plus the model badge for the turn that produced it. + * + * Thread identity is NOT resolvable from the SDK today: an `IncomingTurn` + * carries `deliveryId` and `turnId` but no canonical thread id, the + * delivery transcript response is `{ messages, truncation }`, and + * `GET /api/projects/:projectId/channels/:channelId/threads` is console-authed + * rather than runtime-authed. Until the platform surfaces `canonicalThreadId` + * on the delivery context, `sessionFooter` returns undefined and no footer is + * posted — the link is omitted rather than guessed. + */ + +/** Console location of a canonical Intelligence thread. */ +export interface SessionLinkConfig { + /** Console origin, e.g. `https://intelligence.copilotkit.ai`. */ + consoleUrl: string; + orgSlug: string; + projectSlug: string; + /** + * Path segment holding the conversation view. The console ships + * `/threads/:threadId` today; a dedicated `/sessions/:threadId` view can be + * selected without a code change once it exists. + */ + pathSegment: string; +} + +/** + * Agent tuning defaults, mirrored from `agent/agent.py` so the badge reports + * what the agent actually resolved. `session-link.test.ts` asserts these stay + * in sync with the Python source — a divergence would make the badge lie. + */ +export const AGENT_MODEL_DEFAULT = "gpt-5.5"; +export const AGENT_REASONING_DEFAULT = "low"; + +const DEFAULT_PATH_SEGMENT = "threads"; + +/** Reject anything that would land the user somewhere unintended. */ +function parseConsoleUrl(raw: string): string { + let url: URL; + try { + url = new URL(raw); + } catch { + throw new Error(`Invalid INTELLIGENCE_CONSOLE_URL: "${raw}"`); + } + if (url.protocol !== "https:" && url.protocol !== "http:") { + throw new Error( + `Invalid INTELLIGENCE_CONSOLE_URL scheme: "${url.protocol}"`, + ); + } + if (raw.includes("${")) { + throw new Error( + `Unresolved template in INTELLIGENCE_CONSOLE_URL: "${raw}"`, + ); + } + return url.origin; +} + +/** A slug reaches the URL path verbatim, so keep it to safe characters. */ +function parseSlug(raw: string, name: string): string { + const slug = raw.trim(); + if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(slug)) { + throw new Error(`Invalid ${name}: "${raw}"`); + } + return slug; +} + +/** + * Read footer configuration. Returns undefined when the console location is not + * fully configured, which disables the footer — a partial config is a + * misconfiguration, not a reason to emit a broken link. + */ +export function readSessionLinkConfig( + env: NodeJS.ProcessEnv = process.env, +): SessionLinkConfig | undefined { + const consoleUrl = env.INTELLIGENCE_CONSOLE_URL; + const orgSlug = env.INTELLIGENCE_ORG_SLUG; + const projectSlug = env.INTELLIGENCE_PROJECT_SLUG; + if (!consoleUrl || !orgSlug || !projectSlug) return undefined; + if (env.SESSION_FOOTER === "off") return undefined; + + return { + consoleUrl: parseConsoleUrl(consoleUrl), + orgSlug: parseSlug(orgSlug, "INTELLIGENCE_ORG_SLUG"), + projectSlug: parseSlug(projectSlug, "INTELLIGENCE_PROJECT_SLUG"), + pathSegment: parseSlug( + env.INTELLIGENCE_SESSION_PATH ?? DEFAULT_PATH_SEGMENT, + "INTELLIGENCE_SESSION_PATH", + ), + }; +} + +/** Model badge for the turn, e.g. `gpt-5.5[low]`. */ +export function agentModelBadge( + env: NodeJS.ProcessEnv = process.env, +): string { + const model = env.OPENAI_MODEL?.trim() || AGENT_MODEL_DEFAULT; + const reasoning = + env.OPENAI_REASONING_EFFORT?.trim().toLowerCase() || + AGENT_REASONING_DEFAULT; + return `${model}[${reasoning}]`; +} + +/** Console URL for one canonical thread. */ +export function buildSessionUrl( + config: SessionLinkConfig, + threadId: string, +): string { + const id = threadId.trim(); + if (!id) throw new Error("buildSessionUrl requires a threadId"); + return `${config.consoleUrl}/o/${config.orgSlug}/${config.projectSlug}/${config.pathSegment}/${encodeURIComponent(id)}`; +} + +/** + * Footer text for a completed turn, or undefined when it should be omitted — + * no config, or no canonical thread id to point at. + */ +export function sessionFooter(options: { + config?: SessionLinkConfig; + canonicalThreadId?: string; + env?: NodeJS.ProcessEnv; +}): string | undefined { + const { config, canonicalThreadId } = options; + if (!config || !canonicalThreadId?.trim()) return undefined; + + const url = buildSessionUrl(config, canonicalThreadId); + return `<${url}|Open session> · ${agentModelBadge(options.env)}`; +} From 1d759c684ba2bdea693f12f00e748b6bc4229e74 Mon Sep 17 00:00:00 2001 From: GeneralJerel <85066839+GeneralJerel@users.noreply.github.com> Date: Fri, 31 Jul 2026 08:25:02 -0700 Subject: [PATCH 4/4] feat(session): post the Open session footer on managed runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Corrects the premise of the previous commit. The canonical thread id is not missing from the SDK — the managed delivery adapter sets `conversationKey: delivery.canonicalThreadId` from the prepared delivery, and `channelPreparedDeliverySchema` has carried `canonicalThreadId` all along. So `Thread.conversationKey` already is the canonical thread id on the managed path, and no platform change is needed to render the footer. Local adapters put a provider-shaped key in the same field (Slack's `${teamId}:${channel}:${threadTs}`), which is not a thread id and would link to a console page for a thread that does not exist. The key is therefore accepted only when it has canonical UUID shape, and the footer is omitted otherwise. A failed footer post is reported and swallowed: the answer has already been delivered, so a cosmetic addition must never turn a completed turn into a user-facing failure. --- app/channel.test.ts | 43 ++++++++++++++++++++++++++++++++++++++++ app/channel.tsx | 20 +++++++++++++++++++ app/session-link.test.ts | 38 +++++++++++++++++++++++++++-------- app/session-link.ts | 43 +++++++++++++++++++++++++++++----------- 4 files changed, 124 insertions(+), 20 deletions(-) diff --git a/app/channel.test.ts b/app/channel.test.ts index adb255e..4941e4a 100644 --- a/app/channel.test.ts +++ b/app/channel.test.ts @@ -149,6 +149,7 @@ function findIncidentButton( afterEach(async () => { await Promise.all(channels.splice(0).map((channel) => channel.ɵruntime.stop())); agentRuns.length = 0; + vi.unstubAllEnvs(); vi.restoreAllMocks(); }); @@ -263,6 +264,48 @@ describe("createOpenTagChannel", () => { ); }); + it("appends an Open session footer to a managed run", async () => { + // The managed adapter carries the canonical thread id as the conversation + // key, so a configured deployment can link back to the same conversation. + vi.stubEnv("INTELLIGENCE_CONSOLE_URL", "https://intelligence.example.com"); + vi.stubEnv("INTELLIGENCE_ORG_SLUG", "acme-org"); + vi.stubEnv("INTELLIGENCE_PROJECT_SLUG", "acme-project"); + const { adapter, channel } = makeChannel(); + + await channel.ɵruntime.start(); + await adapter.getSink().onTurn({ + operation: turnOperation(), + conversationKey: "ed1f290b-f826-4275-b56e-a4d22850117d", + replyTarget: {}, + userText: "hello", + platform: "slack", + user: { id: "U1" }, + }); + + expect(JSON.stringify(adapter.posted)).toContain( + "/o/acme-org/acme-project/threads/ed1f290b-f826-4275-b56e-a4d22850117d|Open session>", + ); + }); + + it("omits the footer when the conversation is not managed", async () => { + vi.stubEnv("INTELLIGENCE_CONSOLE_URL", "https://intelligence.example.com"); + vi.stubEnv("INTELLIGENCE_ORG_SLUG", "acme-org"); + vi.stubEnv("INTELLIGENCE_PROJECT_SLUG", "acme-project"); + const { adapter, channel } = makeChannel(); + + await channel.ɵruntime.start(); + await adapter.getSink().onTurn({ + operation: turnOperation(), + conversationKey: "T1:C1:1785507740.811589", + replyTarget: {}, + userText: "hello", + platform: "slack", + user: { id: "U1" }, + }); + + expect(JSON.stringify(adapter.posted)).not.toContain("Open session"); + }); + it("personalizes suggested prompts when a thread starts", async () => { const { adapter, channel } = makeChannel(); diff --git a/app/channel.tsx b/app/channel.tsx index 4bb7be0..8d4633b 100644 --- a/app/channel.tsx +++ b/app/channel.tsx @@ -13,6 +13,7 @@ import { appContext } from "./context/app-context.js"; import { ConfirmWrite } from "./human-in-the-loop/index.js"; import { parseConfirmWriteInterrupt } from "./interrupt.js"; import { FILE_ISSUE_CALLBACK, fileIssueSubmit } from "./modals/file-issue.js"; +import { readSessionLinkConfig, sessionFooter } from "./session-link.js"; import { IncidentCard } from "./tools/showcase-tools.js"; import { appTools } from "./tools/index.js"; @@ -32,9 +33,28 @@ export function createOpenTagChannel( components: [IssueCard, IssueList, PageList, IncidentCard, ConfirmWrite], }); + const sessionLink = readSessionLinkConfig(); + channel.onMention(async ({ thread, message }) => { try { await thread.runAgent(managedRunInput(message)); + + const footer = sessionFooter({ + config: sessionLink, + conversationKey: thread.conversationKey, + }); + if (footer) { + try { + await thread.post(footer); + } catch (error) { + // The answer already landed. A missing footer is cosmetic, so it must + // never turn a delivered turn into a user-facing failure. + reportRecoverableError(error, { + operation: "post_session_footer", + recovery: "continue_without_session_footer", + }); + } + } } catch (error) { try { await thread.post( diff --git a/app/session-link.test.ts b/app/session-link.test.ts index 252d537..6784150 100644 --- a/app/session-link.test.ts +++ b/app/session-link.test.ts @@ -6,6 +6,7 @@ import { AGENT_REASONING_DEFAULT, agentModelBadge, buildSessionUrl, + canonicalThreadIdFrom, readSessionLinkConfig, sessionFooter, type SessionLinkConfig, @@ -123,24 +124,45 @@ describe("agentModelBadge", () => { }); }); +const MANAGED_KEY = "ed1f290b-f826-4275-b56e-a4d22850117d"; + +describe("canonicalThreadIdFrom", () => { + it("accepts a managed conversation key", () => { + expect(canonicalThreadIdFrom(MANAGED_KEY)).toBe(MANAGED_KEY); + }); + + it("rejects a Slack provider key", () => { + // A local adapter puts `${teamId}:${channel}:${threadTs}` here. Linking to + // it would produce a console URL for a thread that does not exist. + expect(canonicalThreadIdFrom("T123:C456:1785507740.811589")).toBeUndefined(); + }); + + it("rejects absent or blank keys", () => { + expect(canonicalThreadIdFrom(undefined)).toBeUndefined(); + expect(canonicalThreadIdFrom(" ")).toBeUndefined(); + }); +}); + describe("sessionFooter", () => { it("renders a Slack link and the model badge", () => { - expect(sessionFooter({ config, canonicalThreadId: "t1", env: {} })).toBe( - `<${buildSessionUrl(config, "t1")}|Open session> · ${AGENT_MODEL_DEFAULT}[${AGENT_REASONING_DEFAULT}]`, + expect( + sessionFooter({ config, conversationKey: MANAGED_KEY, env: {} }), + ).toBe( + `<${buildSessionUrl(config, MANAGED_KEY)}|Open session> · ${AGENT_MODEL_DEFAULT}[${AGENT_REASONING_DEFAULT}]`, ); }); - it("omits the footer when no canonical thread id is available", () => { - // The platform does not surface a canonical thread id to the runtime yet. - // Omitting beats linking somewhere wrong. - expect(sessionFooter({ config, env: {} })).toBeUndefined(); + it("omits the footer for a non-managed conversation", () => { expect( - sessionFooter({ config, canonicalThreadId: " ", env: {} }), + sessionFooter({ config, conversationKey: "T1:C1:123.456", env: {} }), ).toBeUndefined(); + expect(sessionFooter({ config, env: {} })).toBeUndefined(); }); it("omits the footer when unconfigured", () => { - expect(sessionFooter({ canonicalThreadId: "t1", env: {} })).toBeUndefined(); + expect( + sessionFooter({ conversationKey: MANAGED_KEY, env: {} }), + ).toBeUndefined(); }); }); diff --git a/app/session-link.ts b/app/session-link.ts index fbd9284..0ad9d2d 100644 --- a/app/session-link.ts +++ b/app/session-link.ts @@ -4,13 +4,12 @@ * A managed reply carries a link to the same conversation in the Intelligence * console, plus the model badge for the turn that produced it. * - * Thread identity is NOT resolvable from the SDK today: an `IncomingTurn` - * carries `deliveryId` and `turnId` but no canonical thread id, the - * delivery transcript response is `{ messages, truncation }`, and - * `GET /api/projects/:projectId/channels/:channelId/threads` is console-authed - * rather than runtime-authed. Until the platform surfaces `canonicalThreadId` - * on the delivery context, `sessionFooter` returns undefined and no footer is - * posted — the link is omitted rather than guessed. + * On the managed Intelligence path the canonical thread id arrives as the + * turn's `conversationKey` — the delivery adapter sets + * `conversationKey: delivery.canonicalThreadId` from the prepared delivery. + * Local adapters instead put a provider-shaped key there (Slack's + * `${teamId}:${channel}:${threadTs}`), which is not a thread id and must never + * become a link, so the key is accepted only when it has canonical shape. */ /** Console location of a canonical Intelligence thread. */ @@ -103,6 +102,25 @@ export function agentModelBadge( return `${model}[${reasoning}]`; } +/** + * Canonical Intelligence thread ids are UUIDs. A provider conversation key + * (`${teamId}:${channel}:${threadTs}`) is not one, so requiring the shape keeps + * a local-adapter key from being rendered as a console link. + */ +const CANONICAL_THREAD_ID = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +/** + * The canonical thread id carried by a managed turn's conversation key, or + * undefined when this conversation is not managed. + */ +export function canonicalThreadIdFrom( + conversationKey: string | undefined, +): string | undefined { + const key = conversationKey?.trim(); + return key && CANONICAL_THREAD_ID.test(key) ? key : undefined; +} + /** Console URL for one canonical thread. */ export function buildSessionUrl( config: SessionLinkConfig, @@ -115,16 +133,17 @@ export function buildSessionUrl( /** * Footer text for a completed turn, or undefined when it should be omitted — - * no config, or no canonical thread id to point at. + * no config, or a conversation with no canonical thread behind it. */ export function sessionFooter(options: { config?: SessionLinkConfig; - canonicalThreadId?: string; + conversationKey?: string; env?: NodeJS.ProcessEnv; }): string | undefined { - const { config, canonicalThreadId } = options; - if (!config || !canonicalThreadId?.trim()) return undefined; + const { config } = options; + const threadId = canonicalThreadIdFrom(options.conversationKey); + if (!config || !threadId) return undefined; - const url = buildSessionUrl(config, canonicalThreadId); + const url = buildSessionUrl(config, threadId); return `<${url}|Open session> · ${agentModelBadge(options.env)}`; }