Every Playwright install in CI passed `--with-deps`, which runs `apt-get
update` before downloading the browser. apt on the runners cannot always
reach azure.archive.ubuntu.com; when it can't it retries for many minutes,
which is long enough to burn a job's whole `timeout-minutes` budget before
a single test runs. GitHub renders that kill as "The operation was
canceled", so it reads as a test failure rather than an infrastructure hang.
Chromium's system libraries are already present on the Ubuntu runner
images, and every one of these steps installs chromium only, so the browser
download is all they need. Six jobs lose their apt dependency:
test_unit, test_e2e-legacy-v1, test_e2e-showcase-on-demand,
test_showcase-frontend-matrix, showcase_eval and showcase_capture-previews.
Ports CopilotKit/website#529 to this repo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Widening the partialjson constraint in #6123 invalidated poetry.lock's
content-hash, so a bare `poetry install` in sdk-python fails with
"pyproject.toml changed significantly since poetry.lock was last generated".
CI never saw it because test_unit-python-sdk.yml runs `poetry lock` first, and
publishing is unaffected because `poetry build` ignores the lock — but local
dev is blocked until someone relocks. Refreshed with Poetry 2.1.3 (the lock's
own generator) so the diff stays limited to the hash, and moved partialjson to
1.1.0 so CI exercises the version a fresh install now resolves. ag-ui-langgraph
is deliberately left at 0.0.42: a from-scratch resolve pulls 0.0.43, which
fails four intercepted-tool-call tests on a missing `emit_raw_events`.
The parse path had no coverage at all — disabling JSONParser.parse outright
left all 225 tests passing, because every partialjson failure mode degrades to
"no predicted state was emitted" behind the bare `except` in predict_state().
These tests pin the guarantees the `>=0.0.8,<2.0.0` range must keep, and are
version-agnostic about intermediate frames, which legitimately differ (1.1.0
preserves trailing whitespace mid-string where 0.0.8 dropped it).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Let the Inspector load a saved thread into the official React or Vue chat. Core owns a two-way EventClient bridge. Official chat configuration applies an in-memory override that wins over a pinned threadId. Production builds hide the action.
## Why
A managed Channel that loses its gateway link is invisible outside the
host process. The only trace is the injected `log` seam, wired to
`logger.warn` — which for a self-hosted or Railway-hosted runtime
reaches nobody who can act on it.
## What
- `oss.runtime.channel_session_dropped` — carries the cause already
computed for the log line (`reason`, and the transport `code` when the
transport named one).
- `oss.runtime.channel_session_recovered` — carries `downForMs`, so
outage duration is measurable rather than inferred from log timestamps.
- The drop cause is now replayed on every "still down" reminder. In prod
those lines read `still down after 233134s; Phoenix is retrying` with
**no cause at all**, so an operator had to scroll back to the first line
— 15 minutes earlier, or hours, given the exponential backoff — to learn
it was an HTTP 502.
## Deliberate choices
- **No Channel name in the events.** It is a customer-chosen identifier
that can carry business meaning, so it stays out of anonymous OSS
telemetry. No message content or credentials either. Per-channel
aggregate counts still work without it.
- **An `online` transition with no preceding drop emits nothing** — a
session can report online without having dropped, and that is not a
recovery.
- **Capture is fire-and-forget with failures swallowed**, the same
contract `fireInstanceCreatedTelemetry` uses. The `try` also covers a
`capture` that throws synchronously. Telemetry must never break a live
session.
- The `gave_up` line's OSS-670 wording is untouched — it deliberately
says retries continue, and that is now accurate.
## Testing
Three tests added to `channel-manager-reconnect.test.ts`, each watched
fail first: the dropped event with its cause, the recovered event with a
positive duration, and the no-bogus-recovery guard. A fourth pins the
cause on the repeat log line.
```
✓ src/v2/runtime/core/__tests__/channel-manager-reconnect.test.ts (11 tests)
Tests 11 passed (11)
```
Wider run: 106 tests pass across `core/__tests__` and `telemetry`. Two
notes, both verified pre-existing by stashing this branch's changes and
re-running:
- `channel-manager-recovery.test.ts` fails to *load* in my worktree
(`Cannot find package '@copilotkit/channels-slack/render'`) — a
subpath-export resolution artifact of a worktree with symlinked
`node_modules`, identical with these changes stashed.
- `tsc --noEmit` reports 11 errors, the same 11 before and after this
change, none in the files touched here.
`oxfmt` and `oxlint` clean on all three files. Lefthook was bypassed on
the commit because of the same worktree `node_modules` symlinking; I ran
both tools manually over exactly the staged files instead.
Refs OSS-825.
## Summary
- replace the disconnected Deep Agents state emitters with complete
Python and TypeScript agent construction
- stream partial `searches` tool arguments and persist the completed
state with `Command` and `ToolMessage`
- add a rendered-document regression for both language paths
## Root cause
The guide rendered `agent.state.searches`, but its backend snippets
never connected their state-producing functions to a Deep Agent. The
frontend therefore had no executable path that could produce the
documented state.
## Validation
- focused rendered-doc test: red before the change, green after it
- Python smoke against `copilotkit==0.1.95` and `deepagents==0.7.7`
- TypeScript typecheck and command smoke against
`@copilotkit/sdk-js@1.68.1` and `deepagents@1.12.4`
- rendered-doc suite: 34/34
- shell-docs typecheck, lint, and production build
The full shell-docs suite has one unrelated failure already present on
`main`: the Channels overview repeats its light image where its existing
test expects the dark image.
Linear:
[FAC-50](https://linear.app/copilotkit/issue/FAC-50/showcase-docs-deep-agents-python-state-rendering-example-misses)
## Problem
PR #6505 added an inline `NODE_OPTIONS="--max-old-space-size=1536"` to
the `langgraph-typescript` agent launch in
`showcase/integrations/langgraph-typescript/entrypoint.sh`, intending to
bound V8 old-space on the many-core Railway host.
In staging this cap is forcing crash-restarts, not delivering savings.
Staging `showcase-langgraph-typescript` hit a V8 heap-OOM `exit 134` at
`2026-08-18T09:52:31Z` (RSS dropped `2.289 GB -> 0.902 GB` on the
crash-reset).
The cap is also structurally un-overridable. It's appended as
`${NODE_OPTIONS:+$NODE_OPTIONS }--max-old-space-size=1536`, so it is
always the *last* `--max-old-space-size` flag on the command line — and
V8 takes the last flag when the same one repeats. An operator-supplied
`NODE_OPTIONS` override therefore always loses to the inline `1536`, so
a Railway env var can't raise the ceiling for this process; only another
code change can.
## Change
Removes only the inline `--max-old-space-size=1536` addition (and its
now-stale explanatory comment) from `entrypoint.sh`, restoring the exact
pre-#6505 launch line:
```
cd /app/src/agent && PORT=8123 HOST=0.0.0.0 npm start &> >(awk '{print "[agent] " $0; fflush()}') &
```
`NODE_OPTIONS` now passes through untouched — an operator override wins
again, and with no `NODE_OPTIONS` set V8 falls back to its own default
sizing (pre-#6505 behavior).
Untouched, by design:
- Worker-recycle logic in the same entrypoint
- `langgraph-python` / `langgraph-fastapi` entrypoints and their
`MALLOC_ARENA_MAX` / `MALLOC_TRIM_THRESHOLD_` allocator tuning (also
from #6505)
`git diff --stat` confirms the diff is scoped to exactly one file:
```
showcase/integrations/langgraph-typescript/entrypoint.sh | 12 +-----------
1 file changed, 1 insertion(+), 11 deletions(-)
```
## Local red-green proof
Reconstructed the NODE_OPTIONS composition with plain `node` (v25.8.0 —
absolute MiB numbers will vary by machine/Node version, but the
*ordering*, which is the defect, will not):
**RED — current (pre-fix) launch, operator override lost:**
```
NODE_OPTIONS="--max-old-space-size=3072 --max-old-space-size=1536" \
node -e 'console.log(require("v8").getHeapStatistics().heap_size_limit/1048576)'
=> 1728
```
The operator asked for a 3072 MiB ceiling and got capped to 1728 — well
below what was requested, and the source of the crash-restart loop.
**GREEN 1/2 — fix applied, operator override now wins:**
```
NODE_OPTIONS="--max-old-space-size=3072" \
node -e 'console.log(require("v8").getHeapStatistics().heap_size_limit/1048576)'
=> 3264
```
**GREEN 2/2 — fix applied, no NODE_OPTIONS set, V8 default restored
(pre-#6505 behavior):**
```
node -e 'console.log(require("v8").getHeapStatistics().heap_size_limit/1048576)'
=> 4288
```
## Post-merge validation gate
This PR does not attempt to prove the fix in production. Before this is
considered validated, it needs a **24h+ re-soak on a pinned image
digest** of the `showcase-langgraph-typescript` service to confirm the
OOM/exit-134 crash-restart loop is gone under real traffic.
Ref: #6505
## Summary
- initialize the shared rendering example after `useAgent` reports that
the real agent is ready
- seed only missing title and item fields while preserving existing and
unrelated agent state
- cover the LangGraph Python, Strands Python, Strands TypeScript, and
Google ADK rendered routes
- remove a duplicated sentence from the shared-state callout
## Root cause
The shared page rendered `state.title` and `state.items`, but it never
supplied meaningful initial values. A fresh example therefore showed an
`Untitled` heading and an empty list. The initializer must also survive
the provisional-to-real agent swap and must not replace state owned by
the backend or the user.
## Validation
- `npx vitest run src/lib/__tests__/llm-text.test.ts` (33 passed)
- `npm run typecheck`
- `npm run lint` (passes with existing repository warnings)
- `npm run build` (223 pages generated)
- `oxfmt --check` on both changed files
- `git diff --check`
Linear: FAC-105
## Summary
- add package-owned tool-rendering setup for Claude Agent SDK Python and
TypeScript
- expose the existing adapter, MCP server, allowlist, and executable
handler path from canonical source
- insert the setup once in the shared tool-rendering guide
- cover generated setup, visual MDX rendering, both LLM-text routes, and
an unaffected framework
## Why
The public Claude tool-rendering pages stopped after the backend schema
and pure handler. They did not show how the schema becomes an executable
SDK tool or reaches `ClaudeAgentAdapter` through an MCP server.
This fixes [FAC-132](https://linear.app/copilotkit/issue/FAC-132) and
[FAC-136](https://linear.app/copilotkit/issue/FAC-136).
## Validation
- focused shell-docs matrix: 44 tests passed
- shell-docs typecheck passed
- shell-docs production build passed
- D6 `claude-sdk-python:tool-rendering`: green
- D6 `claude-sdk-typescript:tool-rendering`: green
- unaffected LangGraph controls were blocked by local Docker `ENOSPC`
and a stale agent backend; the focused route-isolation test passed
PR #6505 added an inline NODE_OPTIONS="--max-old-space-size=1536" to the
langgraph-typescript agent launch to bound V8 old-space on the many-core
Railway host. In production the cap is forcing crash-restarts rather than
saving memory: staging showcase-langgraph-typescript hit a V8 heap-OOM
exit 134 at 2026-08-18T09:52:31Z (RSS dropped 2.289 GB -> 0.902 GB on the
crash-reset).
The cap is also structurally broken for override: because it's appended
after ${NODE_OPTIONS:+$NODE_OPTIONS }, an operator-supplied
--max-old-space-size loses to the inline 1536 (V8 takes the last flag of
a duplicate, but the inline one is always last). A Railway env var can
raise the ceiling for the frontend process but not for the agent process
this line targets, so there's no way to dial the cap up without another
code change.
This removes only the inline --max-old-space-size=1536 addition (and its
now-stale explanatory comment) from entrypoint.sh, restoring the exact
pre-#6505 launch line so NODE_OPTIONS passes through untouched — an
operator override wins again, and with no NODE_OPTIONS set V8 falls back
to its own default sizing. Worker-recycle and the langgraph-python/
langgraph-fastapi allocator tuning (MALLOC_ARENA_MAX, MALLOC_TRIM_THRESHOLD_)
added in the same PR are untouched.
Local red-green proof (node v25.8.0, numbers will vary by machine/node
version but the ordering is the defect):
RED - current launch's NODE_OPTIONS composition, operator override lost:
NODE_OPTIONS="--max-old-space-size=3072 --max-old-space-size=1536" \
node -e 'console.log(require("v8").getHeapStatistics().heap_size_limit/1048576)'
=> 1728 (capped well below the requested 3072)
GREEN 1/2 - fix applied, operator override now wins:
NODE_OPTIONS="--max-old-space-size=3072" \
node -e 'console.log(require("v8").getHeapStatistics().heap_size_limit/1048576)'
=> 3264
GREEN 2/2 - fix applied, no NODE_OPTIONS set, V8 default restored (pre-#6505):
node -e 'console.log(require("v8").getHeapStatistics().heap_size_limit/1048576)'
=> 4288
Post-merge validation gate: this needs a 24h+ re-soak on a pinned image
digest before it's considered proven in production; this PR does not
attempt that.
## Summary
- document the normal fixed-schema request path accurately:
`shouldUseClaudeAgentSdk` → `ClaudeAgentAdapter` → an in-process SDK MCP
server
- show the exact `mcp__copilotkit__display_flight` registration,
`allowedTools`, handler dispatch, and returned A2UI operations
- describe the direct Anthropic Messages loop as the fallback for
aimock, runtime tools, thinking, and structured input
- add regression tests for request-path selection, MCP wiring, generated
setup content, rendered LLM text, the visual `FrameworkSetup` path, and
isolation from every other public framework
- run the backend unit suite during the Claude TypeScript Docker image
build, so the PR showcase build gates this path
## Validation
- `npm run test:unit` in `showcase/integrations/claude-sdk-typescript` —
13 passed
- `npm run typecheck` and `npm run build` in
`showcase/integrations/claude-sdk-typescript`
- focused shell-docs suite — 39 passed
- `npx tsc --noEmit` and `npm run build` in `showcase/shell-docs`
- full shell-docs suite — 406 passed; 1 unrelated Channels dark-image
assertion fails identically on `main`
- GitHub CI is green, including the Claude TypeScript image build that
executes the backend unit suite and the shell-docs image build
- real D6 probe could not start because this isolated worktree has no
`showcase/.env`; aimock also intentionally selects the fallback branch,
so the new unit test covers the production SDK/MCP branch directly
Linear:
[FAC-140](https://linear.app/copilotkit/issue/FAC-140/claude-sdk-fixed-schema-a2ui-backend-example-lacks-mcp-tool-wiring)
## What does this PR do?
Relaxes the over-strict `partialjson` version constraint in
`sdk-python/pyproject.toml` so the CopilotKit Python SDK can consume
newer compatible `partialjson` releases.
**Change:** `sdk-python/pyproject.toml`
- Before: `partialjson = "^0.0.8"`
- After: `partialjson = ">=0.0.8,<2.0.0"`
## Why
Poetry interprets `^0.0.8` on a `0.0.x` version as `>=0.0.8,<0.0.9`,
which effectively pins the SDK to **exactly** `0.0.8`. This blocks users
from receiving newer, compatible `partialjson` releases (currently
published: `0.0.9`, `0.1.0`, `1.0.0`, `1.1.0`). Relaxing the constraint
lets the SDK pick up fixes and features while still treating the next
major (`2.0.0`) as the breaking-change boundary.
I verified the API the SDK actually uses — `from partialjson.json_parser
import JSONParser` and `JSONParser().parse(...)` — is unchanged across
all published versions up to `1.1.0` (the `JSONParser.__init__` /
`parse` signatures are backward compatible, and the SDK call passes no
arguments). There is no breaking-API risk within the chosen range.
## Test plan
- [x] Validated `sdk-python/pyproject.toml` is well-formed:
`python3 -c "import tomllib,pathlib;
tomllib.load(pathlib.Path('sdk-python/pyproject.toml').open('rb'))"`
- [x] Grepped `sdk-python` for `partialjson` usages — only
`copilotkit/runloop.py` imports/uses it; API is stable across the
allowed range.
- [ ] `poetry lock` / `poetry install` in `sdk-python` resolves a
`partialjson` version within `>=0.0.8,<2.0.0`.
## Changed files
- `sdk-python/pyproject.toml` — single line changed (dependency range
only). No source, example, or lockfile changes.
## Checklist
- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] No documentation change required (dependency range only)
- [x] "Allow edits by maintainers" is checked
Relates to #4131
Closes#2615.
## Problem
The v1 thumbs callbacks were typed `(message: Message) => void`, so a
consumer
received the message but not *what the click did*. A custom
`AssistantMessage`
that keeps its own toggle state — the case in the issue — had nowhere to
put
that value:
```tsx
onClick={() => {
setReactionValue((v) => (v === "like" ? null : "like"));
onThumbsUp?.(message); // 👈 no way to say whether this applied or retracted
}}
```
The only workaround was counting clicks per message.
The built-in path had the matching gap. `Chat.tsx` already tracked
`messageFeedback`, but `handleThumbsUp` unconditionally wrote
`"thumbsUp"`, so
the state was write-only: clicking an active button re-applied the same
value
and there was no way to un-vote.
These callbacks are live API — `RenderMessage` forwards `onThumbsUp`,
`onThumbsDown` and `feedback` to whatever component is passed as the
`AssistantMessage` prop, which is exactly the customisation the issue
describes.
## Fix
Add an optional second argument reporting the state the click
transitions to:
```ts
onThumbsUp?: (message: Message, isActive?: boolean) => void;
```
- The built-in `AssistantMessage` derives it from the message's current
`feedback`, so a second click on an active button now reports `false`
and
**retracts** the feedback rather than re-applying it.
- A custom `AssistantMessage` can pass its own value straight through.
- The parameter is optional and appended, so existing one-argument
handlers
keep type-checking and keep working unchanged.
`onFeedbackGiven` fires only on the applying click — its signature is
`(messageId, "thumbsUp" | "thumbsDown")` and has no way to express a
retraction, so reporting one would be a lie.
Note this is a small behavioural change to the built-in buttons:
previously a
repeat click was a no-op, now it clears the vote. That is what makes the
reported state meaningful, and it matches the toggle UX in the issue.
The toggle logic lives in a new `./feedback` module. This package's
vitest
project runs `environment: "node"` and only collects `*.test.ts`, so
there is no
component-rendering harness here — extracting the pure functions is what
makes
the behaviour testable at all.
## Testing
`packages/react-ui` — full suite, including the 10 new cases:
```
✓ src/components/chat/feedback.test.ts (10 tests) 3ms
✓ src/components/chat/Markdown.test.ts (29 tests) 5ms
✓ src/components/chat/Markdown.xss.test.ts (13 tests) 182ms
✓ src/css/sidebar-full-height.test.ts (4 tests) 2ms
Test Files 10 passed (10)
Tests 68 passed (68)
```
Coverage: activation from no feedback, deactivation on the
already-active
button, switching to the opposite button, retraction removing the map
entry
rather than storing a falsy value, other messages left untouched, no
mutation of
the previous map, referential stability when nothing changes, and a
toggle
round-trip.
**Mutation checks** — broke each mechanism and confirmed the tests fail,
so they
are not self-fulfilling:
| Mutation | Result |
| --- | --- |
| `isActivatingClick` → `return true` | `Tests 1 failed \| 9 passed` |
| `applyFeedbackClick` retraction branch → `if (false)` | `Tests 3
failed \| 7 passed` |
Both were reverted and the suite returned to 68 passing.
**Types/build** — `tsc --noEmit` clean, `tsdown` clean, and the widened
signature reaches the published types:
```
$ grep -n "onThumbsUp" dist/index.d.mts
102: onThumbsUp?: (message: Message, isActive?: boolean) => void;
185: onThumbsUp?: (message: Message, isActive?: boolean) => void;
272: onThumbsUp?: (message: Message, isActive?: boolean) => void;
```
Fixes#2493.
## Problem
`.copilotKitHeader` declared its horizontal padding like this:
```css
.copilotKitHeader {
padding-left: 1.5rem; /* no padding-right */
justify-content: space-between;
}
@media (min-width: 640px) {
.copilotKitHeader {
padding-left: 1.5rem; /* duplicate of the base rule */
padding-right: 24px; /* only above the sm breakpoint */
}
}
```
`padding-right` existed **only** inside `@media (min-width: 640px)`. The
header
lays out `space-between`, so below 640px the title sat against the left
edge
(fine — `padding-left` is unconditional) while the controls ran flush to
the
right edge with no gutter at all. That is the cramped mobile header in
the
report.
Worth noting because it misleads when reading the file: the
`.copilotKitHeader > button { position: absolute; right: 16px }` rule
further
down does **not** apply to the close button. `Header.tsx` nests it
inside
`.copilotKitHeaderControls`, so the button is a grandchild and the child
combinator never matches. The button is in normal flow, which is why it
lands
exactly on the padding edge.
## Fix
Move the horizontal padding onto the unconditional rule and drop the
duplicated
declarations from the media query, leaving it responsible only for the
border
radii. `24px === 1.5rem` at the default root font size, so layouts at
640px and
above are byte-for-byte unchanged; only the sub-640px case gains the
gutter it
was missing.
## Testing
Added `src/css/header-padding.test.ts`, which strips every `@media`
block and
asserts the remaining unconditional `.copilotKitHeader` rule declares
both
`padding-left` and `padding-right` — i.e. that the padding is not
breakpoint-gated
again.
Full `react-ui` suite:
```
✓ src/esm-compat.test.ts (1 test) 2ms
✓ src/css/sidebar-full-height.test.ts (4 tests) 2ms
✓ src/css/header-padding.test.ts (1 test) 1ms
✓ src/hooks/__tests__/use-push-to-talk.test.ts (2 tests) 2ms
✓ src/components/chat/Markdown.test.ts (29 tests) 5ms
✓ src/components/chat/Markdown.xss.test.ts (13 tests) 182ms
Test Files 10 passed (10)
Tests 68 passed (68)
```
**Mutation check** — removed `padding-right: 1.5rem` from the base rule
and
re-ran, confirming the new test fails rather than passing vacuously:
```
37| expect(base).toMatch(/padding-left:/);
38| expect(base).toMatch(/padding-right:/);
| ^
Test Files 1 failed (1)
Tests 1 failed (1)
```
**Build** — `tsdown` succeeds and the compiled `dist/index.css` carries
the
declaration:
```
.copilotKitHeader {
...
padding-left: 1.5rem;
padding-right: 1.5rem;
...
}
```
**Visual** — rendered the real `Header.tsx` markup against the compiled
stylesheet at a 390px viewport, with one copy re-gating `padding-right`
behind
the breakpoint to reproduce the previous rule. Before, the close button
sits
flush on the right border; after, it clears it by 24px, matching the
existing
left gutter. (Screenshot to follow in a comment.)
The Teams docs described a setup flow that no longer exists: register an
app by
hand in Microsoft Entra, point an Azure Bot at the Intelligence
messaging
endpoint, paste a client ID, tenant ID, and secret into the wizard, then
download a package to upload. Setup is now a single CLI command that
creates a
Teams-managed bot in the reader's own tenant, registers the endpoint,
and
produces the app package the reader uploads.
This rewrites the three pages a reader hits while connecting Teams,
repoints the
doc test that pinned the old prose, and rolls in OSS-833 (refs OSS-833)
— naming
which kind of Microsoft bot identity setup creates.
All three pages were reviewed rendered, not just as a diff.
## What changed
**`channels/intelligence.mdx`** — the Connect Microsoft Teams step is
now the
command the wizard shows you (`channels add --adapter teams
--provision`), with
a note to use the copy button because the project and channel ids bind
the
Microsoft app to that exact Channel. Documents the package hand-off: the
command
writes `<channel-name>-teams-app.zip`, prints the upload steps, and
blocks until
you confirm, because Microsoft exposes no install API. The upload must
come from
the team's own Apps tab — the personal Apps section installs to personal
scope,
which cannot be promoted to a Team afterwards. Adds a section on the two
permissions and one on what **Created and installed** does and does not
prove.
Drops the Entra walkthrough and the credential entry.
**`frontends/teams.mdx`** — "managed Azure Bot" → "managed Microsoft
Teams bot",
and states up front that the bot lives in the reader's own tenant with
no Azure
subscription involved. This is the one place that sentence survives; the
Intelligence step no longer repeats it. The "Teams cannot reach the bot"
accordion now points at
Teams Developer Portal → Bot management, and calls out that Developer
Portal
reports a padded endpoint value as a save failure, which reads like a
portal
fault rather than a bad value. Also: in a team channel you must mention
the bot
— an unmentioned message is ambient and only reaches the agent on a
subscribed
thread.
**`frontends/teams.mdx`, `## What kind of bot this is`** — rolls in
**OSS-833**,
minimally. The docs said only what setup does *not* create, which does
not
answer the tenant administrator who has to account for a new identity in
their
own directory; the only way to learn the answer was to read the CLI
source and
notice `--teams-managed`. One section now names the Teams-managed bot we
create
(credentials in Developer Portal → Tools → Bot management, rotatable by
their
admins, single-tenant, nothing billable, and it stays theirs if they
stop using
CopilotKit), contrasts it with an Azure Bot and a self-managed Entra app
registration, and says the choice is effectively permanent because the
app ID is
the manifest's bot ID. Left as a section on the page that already
carried the
bot-ownership sentence rather than a new page, placed after the
orientation links
so it reads as its own section rather than swallowing them.
**`channels/files-and-multimodality.mdx`** — corrects the file
permission. The
manifest requests the read-only `Files.Read.All`, not the broad
`Files.ReadWrite.All`, and it is optional rather than a gate on the
connection
reporting ready. Skipping it costs only files uploaded to a Team
channel, which
arrive as SharePoint references; 1:1 chat files and pasted images are
unaffected.
## Testing
Every factual claim was checked against the shipped implementation in
the
Intelligence repo, not against the previous docs:
| Claim | Verified at |
| --- | --- |
| `channels add --adapter teams --provision` is the command |
`apps/cli/src/commands/channels.ts:925` quotes it verbatim in its own
error message |
| `--project-id` / `--channel-id` are only valid on that path |
`apps/cli/src/commands/channels.ts:1044` |
| `Files.Read.All` is prompted and defaults to skipping |
`apps/cli/src/commands/channels.ts:381` — prompt is `[y/N]`, "It can be
granted later." |
| `Files.ReadWrite.All` satisfies it without a second grant |
`apps/app-api/src/channels/teams-graph-access.ts:81`;
`apps/cli/src/services/microsoft-teams-graph.ts:107` leaves such an app
untouched |
| Absence omits only the attachment; 1:1 and pasted images unaffected |
`apps/app-api/src/channels/teams-graph-files.ts:460` — the doc prose
matches the shipped warning |
| Team-channel uploads arrive as SharePoint references |
`apps/app-api/src/channels/teams-graph-files.ts:252` |
| `ChannelMessage.Read.Group` is per-Team RSC and required |
`teams-graph-access.ts:88`;
`apps/app-api/src/channels/teams-ingress.ts:1480` |
| The command writes a zip and waits rather than installing |
`apps/cli/src/services/teams-provision.ts:766-803` — "Microsoft's
tooling has no install command", then blocks on
`waitForAction('installation')` |
| Upload from the team's Apps tab, not personal Apps |
`apps/cli/src/commands/channels.ts:407-419` — the printed steps, and why
personal scope is a dead end |
| No upload option ⇒ Ctrl-C, admin installs, re-run resumes |
`apps/cli/src/commands/channels.ts:419` |
| `--teams-package` is browser-built branding, validated then deleted |
`apps/cli/src/index.ts:279`; `channels.ts:533-552` validates before
authorizing removal, `channels.ts:631-633` deletes |
| The wizard really does render a copy button for the command |
`Intelligence:apps/app-frontend/react-shell/src/channels/teams-guided-adapter-setup.tsx:652-674`
|
| We create a Teams-managed bot, registered single-tenant |
`apps/cli/src/services/microsoft-teams-cli.ts:411` passes
`--teams-managed` and `--sign-in-audience myOrg`; `:127` pins
`botLocation: z.literal('teams-managed')` |
| Microsoft's CLI knows only two kinds, so no self-managed Entra app |
`@microsoft/teams.cli@3.0.3` `dist/apps/bot-location.d.ts` —
`BotLocation = 'tm' \| 'azure'` |
| Migration to Azure is one-way and replaces the registration |
`dist/commands/app/bot/migrate.js:15,58,68`; the `bot` command group
contains only `get` and `migrate` |
| The app ID is the manifest's bot ID, so identity changes need a new
package | `Intelligence:libs/channels-setup/src/teams-package.ts:51-53`
(`botId: id`), built by `createTeamsAppPackage(label, clientId)` |
The second commit exists because the first got two of these wrong — it
said the
command "installs it to a Team you choose" and that "there is no package
to
download", which also contradicted the Teams tutorial's own "upload the
complete
zip that setup produced". Both are corrected, and the doc test now pins
the
package hand-off in place of the struck Azure sentence.
Doc test, rebased onto `main` (`4526bb00d2`):
```
$ npx vitest run src/lib/__tests__/channels-docs.test.ts # showcase/shell-docs
Test Files 1 failed (1)
Tests 1 failed | 29 passed (30)
❯ src/lib/__tests__/channels-docs.test.ts:97:30
```
The 30th test is new: it pins the bot-identity section, since "name the
kind we
create" is exactly the claim that erodes back into "Azure Bot is not
part of the
normal path".
The one remaining failure is **pre-existing on `main`** and unrelated to
this
PR. It asserts the Channels overview embeds
`channels-architecture-dark.png`; `channels/index.mdx` references
`channels-architecture-light.png` twice and no dark variant, as of
`47a4a84896 docs(channels): re-export architecture diagram at 4000px` —
a commit
on `main` that this branch does not touch. Proof it is not mine: running
main's
version of the test file against this branch's docs gives **2** failures
— line
97 plus line 521, main's now-stale `expect(teams).toContain("Microsoft
Entra")`
— and this PR's updated test file removes only the second.
That pre-existing break is left alone here rather than folded in, since
fixing
it means deciding whether the overview wants a dark diagram or the
assertion is
obsolete.
OSS-794.
## The defect
`SlackNativeProps` declared `decimal_allowed`. Slack's `number_input`
field is **`is_decimal_allowed`** (confirmed in `@slack/types@2.22.0`,
where it is also *required*).
Slack accepts a message whole or not at all. The unrecognised key
refused the entire `chat.postMessage` call and ended the Channels
delivery that carried it — no exception reached the caller, nothing
arrived in the channel.
So the **typed path was the broken one**. Anyone who bypassed our types
and hand-wrote `is_decimal_allowed` got it right by accident; anyone
using `Slack.Element.NumberInput` silently lost the message. That is the
opposite of what an SDK should do.
## Proof, before the change
Verified live against Slack (private `#bot-test`), one delivery per case
— a refusal ends the whole turn's delivery, so batching would have made
the results meaningless.
| Case | Payload | Outcome |
| --- | --- | --- |
| A | `decimal_allowed: true` (our typed spelling) | **refused** —
`invalid_blocks: invalid field at /blocks/0/element` |
| B | no decimals field at all | **refused** — same error |
| C | `is_decimal_allowed: true` (Slack's spelling) | **delivered** |
| E | *both* names present | **refused** — same error |
Case B refuses for its own reason: `@slack/types` marks
`is_decimal_allowed` **required**, so omitting it is independently
invalid. That makes B a poor control, so case E was added — it satisfies
Slack's required field and is otherwise byte-identical to the payload
that delivers, leaving the unknown `decimal_allowed` key as the only
possible cause. A vs C vs E isolates the name as the whole story.
In the refused threads only the caption arrived; the block itself is
simply absent — the silent-loss shape the defect produces in production.
## Is this breaking?
**No, and no alias is kept.** The old name never worked: every payload
carrying it was refused by Slack, so no caller can be depending on
working behaviour. Keeping `decimal_allowed` as an alias would preserve
the trap. Removing it converts a silent message loss into a compile
error that names the right field:
```
error TS2561: Object literal may only specify known properties, but 'decimal_allowed'
does not exist in type 'SlackNativeProps<unknown>'. Did you mean to write 'is_decimal_allowed'?
```
## Proof, after the change
Built locally, copied over the installed `@copilotkit/channels-slack` in
a real OpenTag runtime, confirmed the process held the new build (`tsc`
reads the copied `.d.ts` — it accepts `is_decimal_allowed` with no cast
and rejects `decimal_allowed` with the error above), then re-ran case A
through the fixed typed surface: **delivered**, block present in the
thread.
## Sibling sweep
Every field name `native.ts` declares was compared against Slack's Block
Kit vocabulary. **No other mismatch was found.** Names absent from
`@slack/types` are absent because `@slack/types` does not model those
blocks, not because they are misspelled — they are enumerated in the
guard.
Two adjacent observations, not fixed here: `dispatch_action`,
`min_length` and `max_length` are Slack fields we do not declare at all
(callers reach them via `as never`). Those are gaps rather than
mismatches, so they are out of scope for this PR.
## The guard
`src/__tests__/native-field-names.test.ts` compares every field name
`native.ts` declares against Slack's own Block Kit declarations in
`@slack/types` — already a direct dependency of this package, so no new
dependency was added. Declarations are read with the TypeScript parser
rather than matched as text.
**It fails when it should.** Temporarily restoring the misspelling turns
it red with an actionable message:
> native.ts declares `decimal_allowed`, which @slack/types does not.
Slack refuses a whole message for one unrecognised key, so a wrong name
here deletes every message using the component. Either correct the name
to Slack's, or — if Slack really does accept it and @slack/types is
simply behind — add it to NOT_COVERED_BY_SLACK_TYPES with the reason.
**It is honest about coverage.** `@slack/types` is incomplete, so a
missing name is not proof a name is wrong. Every uncovered field is
listed individually with its reason rather than being waved through:
- `blocks` — `container`'s child slot; `@slack/types` declares no
container block
- `offset` — documented on `rich_text_list`, absent from `@slack/types`'
`RichTextList`
- `slack_icon`, `subtext` — accepted on `card`, not declared by
`CardBlock`
- `chart`, `segments`, `series`, `axis_config`, `categories`, `x_label`,
`y_label`, `data` — `data_visualization` has no counterpart in
`@slack/types` at all
**It cannot pass trivially.** Floors assert the extracted vocabulary is
real (≥100 names, plus spot checks) and that a meaningful number of
names were actually compared (≥30), so a collapsed comparison fails
instead of looking green. Two further assertions keep the exemption list
from rotting: an exemption `@slack/types` has since started declaring,
or one for a name we no longer declare, both fail.
## Verification
| Command | Result |
| --- | --- |
| `oxfmt --check packages/channels-slack` | clean, 66 files |
| `oxlint` on both changed files | 0 warnings, 0 errors |
| `nx run @copilotkit/channels-slack:check-types` | pass (+ 8 dependent
tasks) |
| `nx run @copilotkit/channels-slack:test` | 31 files, 411 tests passed
|
| `nx run @copilotkit/channels-slack:build` | pass |
| lefthook pre-commit (`test-and-check-packages`, all packages) | pass |
The docs said only what setup does not create — no Azure subscription, no
Azure Bot resource. That answers nothing for the tenant administrator who has
to account for a new identity in their own directory, and the only way to
learn the answer was to read our CLI source and notice `--teams-managed`.
Names the kind we create and contrasts it with the two alternatives: where the
credentials live, who rotates them, that it is single-tenant, and that nothing
is billable. Also states that the choice is effectively permanent, since the
app ID is the manifest's bot ID.
Kept to one section on the tutorial page, which already carried the
bot-ownership sentence, rather than a new page. It sits after the orientation
links as an `h2` alongside the page's other sections; as an `h3` wedged between
the intro and those links, the links read as part of it.
refs OSS-833
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The rewrite claimed the command "installs it to a Team you choose" and that
"there is no package to download". Neither is true. Microsoft exposes no
install API, so `runAutomaticTeamsProvision` writes
`<channel-name>-teams-app.zip`, prints upload steps, and blocks on Enter
before verifying the installation. The old prose and the Teams tutorial's
"upload the complete zip that setup produced" also contradicted each other.
Documents what the command actually hands you: the package path, that the
upload must come from the team's own Apps tab rather than the personal Apps
section — personal scope yields a working DM that cannot be promoted — and
that a tenant disallowing custom apps means Ctrl-C, get an administrator to
install it, and re-run to resume rather than create a second app.
Also covers `--teams-package`, which the wizard adds when the reader
customizes branding: the browser builds that file, the CLI validates it,
builds the app from it, and deletes the local copy. It decides how the app
looks and does not replace the Team upload.
Strikes "no Azure subscription and no Azure Bot resource are involved" from
the Intelligence step. The Teams tutorial still says it, once, where a reader
arriving with Azure expectations actually starts.
The doc test pinned that struck sentence. It now pins the package hand-off
instead, which is the claim that was wrong and would otherwise drift back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Teams docs described a flow that no longer exists: registering an app
by hand in Microsoft Entra, pointing an Azure Bot at the Intelligence
messaging endpoint, pasting a client ID, tenant ID, and secret into the
wizard, and downloading a package to upload. Setup is now a single CLI
command that creates a Teams-managed bot in the reader's own tenant,
registers the endpoint, and installs the app.
Also corrects the file permission. The manifest requests the read-only
`Files.Read.All`, not `Files.ReadWrite.All`, and it is optional: skipping
it costs only files uploaded to a Team channel, and no longer holds the
connection back from reporting ready.
The doc test pinned the old prose, so it is repointed at the new
contract — the command's flags, both permissions and which is optional,
and that Entra and Azure Bot appear only to say they are not involved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bundles the CopilotKit-side work from **OSS-769**, **OSS-767**
(partial), and the unshipped remainder of **OSS-609**.
## OSS-769 — `useCoAgent().nodeName` never updates
`useAgentNodeName` tracked the current node in a ref and returned
`nodeNameRef.current`. Mutating a ref schedules no render, so a
component reading `useCoAgent().nodeName` kept showing whichever node
was current at its last render and never updated on its own — it only
appeared to work when something unrelated happened to re-render it.
Backing the value with state fixes it. Five lines, no API change.
## OSS-767 (partial) — silent content-part drops (#1748)
`normalizeMessageContent` handles only `text` and `binary` parts;
anything else — the `{"type": "image", ...}` case from the report — maps
to `""` and is filtered out with no signal, so an agent emitting
structured content sees its output vanish silently.
This makes the drop visible, once per unrecognised part type so
streaming doesn't flood the log. Deliberately **not** the schema change:
carrying structured assistant content needs an `AssistantMessage`
decision upstream in `ag-ui`, which stays open on OSS-767.
## OSS-609 — the five docs gaps that never shipped
Gap #2 shipped in #6403; gaps 1, 3, 4, 5 and 6 did not.
| Gap | Where | Closes |
| --- | --- | --- |
| AWS Lambda self-hosting | `docs/deploy/aws-lambda.mdx` | #1151 |
| Per-user thread authorization | `docs/auth.mdx` (new section) | #2241
|
| Thread persistence without the platform |
`docs/threads-self-managed.mdx` | #6090 |
| DIY guardrails / DLP | `docs/integrations/langgraph/guardrails.mdx` |
#3414 |
| When you need an MCP App | `docs/agentic-protocols/mcp.mdx` (new
section) | #5991 |
The Lambda guide leads with the constraint that actually bites —
streaming is opt-in on every front door, so a chat runtime deployed with
the defaults appears to hang for the whole run and then dumps the reply
at once. It documents the Function URL + `RESPONSE_STREAM` path as the
default, and API Gateway REST + `responseTransferMode: STREAM` for
anyone who needs a REST API in front.
`threads-self-managed` follows the existing shared-snippet pattern with
per-framework wrappers, because two nav contracts require it: every
authored framework must publish the page, and every React destination
must map to an Angular one (`ANGULAR_DOC_REDIRECTS`). All nine wrappers
were confirmed necessary by deleting one and watching the suite fail.
## Review corrections
Two blockers from @MikeRyanDev, both verified against primary sources
before changing anything.
**API Gateway REST APIs can stream**
([`91b3e632d5`](https://github.com/CopilotKit/CopilotKit/commit/91b3e632d5)).
The guide was built on the pre-November-2025 limitation and claimed no
API Gateway type supports response streaming, steering readers to a
buffered `serverless-http` setup. REST gained it via
`responseTransferMode: STREAM`, which also lifts the 10 MB cap and
29-second timeout. REST and HTTP are now split; REST is documented as a
streaming front door (payload-format-1.0 event adapter, `AWS_PROXY`
integration on the `2021-11-15/.../response-streaming-invocations` URI,
CLI/CDK/SAM config), and the buffered fallback is scoped to HTTP APIs
and ALB, which still have no streaming path. Added the constraints that
actually matter for chat: the 30-second idle timeout on edge-optimized
endpoints (5 min Regional), and the console Test tab always buffering so
a working config looks broken.
**`identifyUser` is the platform's thread-scoping binding**
([`91b3e632d5`](https://github.com/CopilotKit/CopilotKit/commit/91b3e632d5)).
The section told every reader to build an ownership table and enforce it
in `onBeforeHandler`. On the Intelligence path the runtime already
resolves `identifyUser(request)` server-side and carries that id to the
platform; `listThreads` is scoped by user *and* filtered by `agentId`,
so the "every user of one project sees that project's threads" claim was
wrong. `identifyUser` is now documented as the binding, and the DIY
pattern is scoped to SSE runtimes, custom stores, and the local
in-memory runner.
**Follow-up correction — two routes are genuinely unscoped**
([`240672ff56`](https://github.com/CopilotKit/CopilotKit/commit/240672ff56)).
My rewrite then over-claimed. `handleGetThreadEvents` and
`handleGetThreadState` resolve the caller and discard it, and the
platform client takes no `userId` on either method
(`client.ts:1113`/`1135`) — unlike `getThreadMessages` at `1063`. Both
hit project-authenticated `_inspect` endpoints, so any caller
`identifyUser` accepts can read the event log and agent state of **any
thread in the project** given its id. The blanket guarantee is replaced
by a per-route table marking those two explicitly unscoped, plus an
`onBeforeHandler` guard narrowed to them.
That is a live gap in shipped runtime code, not a docs error, and it is
tracked as **OSS-851** — a platform-side `_inspect` change plus matching
runtime/client work and tests, out of scope for a docs PR. The interim
callout in `auth.mdx` comes out when OSS-851 lands.
## Not in this PR
**OSS-772** and **OSS-773** are already merged in
`oss-path-to-production` (#237, #238). Both are telemetry-sink changes
with no CopilotKit-side component. OSS-773's remaining half — re-keying
runtime `distinct_id` from email to the Clerk subject — is recorded on
the ticket as an open decision, not a task.
## Testing
**OSS-769.** New `use-agent-nodename.test.tsx`, 5 tests. Against
unmodified `origin/main`, **4 of 5 fail**:
```
× re-renders consumers on every node transition
× reports 'end' when a run errors
× resets to 'start' when a new run begins
✓ unsubscribes on unmount
× carries the agent, thread, and current node
Tests 4 failed | 1 passed (5)
```
With the fix: `Tests 5 passed (5)`. These assert only re-render
behaviour under normal `act()` flushing — no manufactured intra-batch
window.
**Full react-core suite:** `Tests 7 failed | 1496 passed (1503)`. All 7
failures are **pre-existing** `ResizeObserver is not a constructor`
under jsdom, confined to `CopilotChatView.pinToSend` and
`use-pin-to-send` — neither of which this PR touches.
**Typecheck:** `packages/react-core` → `tsc --noEmit` exit 0, no output.
**OSS-767:** 3 new tests covering the warn, warn-once-per-type, and
no-warn-for-supported-types. `src/graphql/message-conversion/` → `Tests
125 passed (125)`.
**Docs:** `showcase/shell-docs` → `Tests 1 failed | 373 passed (374)`.
The single failure (`channels-docs > publishes the Channels overview
only through provider navigation`) is **pre-existing**; baselining with
all changes stashed reproduces it and nothing else. Re-run unchanged
after both review-correction commits.
**Review corrections.** The AWS rewrite was checked against the AWS
sources rather than written from memory — the REST streaming
announcement, `configuration-response-streaming`,
`response-transfer-mode` (endpoint-type idle timeouts, unsupported
buffered-only features), `response-streaming-lambda-configure`
(CLI/OpenAPI shapes), the CFN `Integration` reference, and the CDK
`ResponseTransferMode` enum. Two details corrected in passing:
`InvokeWithResponseStream` authorizes against plain
`lambda:InvokeFunction` (no new grant, contrary to what the streaming
URI suggests), and ALB still has no Lambda streaming path.
The auth corrections were verified by reading the handlers and the
platform client, not the tests — `resolve-intelligence-user.ts`,
`intelligence/threads.ts` (every `resolveIntelligenceUser` call site),
and `intelligence-platform/client.ts`. The existing tests assert the
`threadId`-only call shape, so they pass under the defect and could not
have surfaced it. Also corrected: there is no `threads/delete` route —
delete is `DELETE` on `threads/update` (`fetch-handler.ts:606`).
Both edited pages MDX-compile clean, and all inbound
`#thread-authorization` anchors still resolve after the h3→h4 demotions.
Two nav tests broke during this work and are fixed rather than papered
over — adding a page to the Rich Threads group violated the
cross-framework ordering contract and the React→Angular parity contract:
```
src/lib/__tests__/docs-render.test.ts
src/lib/__tests__/angular-docs-content.test.ts
Test Files 2 passed (2)
Tests 33 passed (33)
```
All 15 internal links in the new pages resolve against the content tree.
Closes#1151, #2241, #3414, #5991, #6090
Refs #1748, OSS-851
## Summary
- replace the generated full-chat excerpt with a self-contained
`useAgent` / `useCopilotKit` run-and-stop example
- guard concurrent runs, report rejected runs, and disable Run/Stop
controls when they are not applicable
- remove the redundant incomplete Claude Python and TypeScript setup
excerpts while preserving context-neutral AG-UI guidance on every page
that reuses the setup block
- add renderer regressions for all five affected integration routes and
the three shared Claude setup surfaces
## Root cause
The shared Programmatic Control page extracted a region from the middle
of each integration's full chat component. The displayed region omitted
its imports and local helpers, so otherwise-valid showcase code became
an incomplete documentation sample. The Claude Python and TypeScript
setup content also rendered a second partial excerpt, and its
directional prose was reused on pages where there was no matching
example below.
## Ticket acceptance
| Ticket | Route | Acceptance |
| --- | --- | --- |
| [FAC-166](https://linear.app/copilotkit/issue/FAC-166) | LangGraph
Python | The shared example imports its hooks, adds a message, runs the
agent, handles failure, and exposes state-aware cancellation without
chat-shell helpers. |
| [FAC-149](https://linear.app/copilotkit/issue/FAC-149) | Google ADK |
The complete shared example follows the preserved `AGUIToolset()`
backend setup. |
| [FAC-151](https://linear.app/copilotkit/issue/FAC-151) | AWS Strands
Python | The route renders the same complete shared send/stop example. |
| [FAC-159](https://linear.app/copilotkit/issue/FAC-159) | Claude Agent
SDK Python | The route renders one complete shared send/stop example and
no redundant `createMessageId` setup excerpt. The identical Claude
TypeScript setup defect is fixed in the same change boundary. |
Closes FAC-166
Closes FAC-149
Closes FAC-151
Closes FAC-159
## Validation
- `npm run test -- src/lib/__tests__/llm-text.test.ts`
- `npm run typecheck`
- `npm run build`
## Summary
- parse and validate the Claude Python BYOA request before
`StreamingResponse` starts consuming the response stream
- keep the in-stream `RUN_ERROR` path scoped to agent and streaming
failures
- add verifier and runtime-harness guards that reject request parsing
inside `event_stream()`
Closes
[FAC-129](https://linear.app/copilotkit/issue/FAC-129/claude-agent-sdk-python-quickstart-chat-hangs-in-use-existing-agent).
## Validation
- `npm --prefix showcase/scripts test -- verify-shell-docs.test.ts
--run`
- Claude quickstart check in `npm --prefix showcase/scripts run
verify-shell-docs:fast` (passes; the full command retains unrelated
baseline failures)
- `npm --prefix showcase/shell-docs run typecheck`
- `npm --prefix showcase/shell-docs run build`
- production build route: `/claude-sdk-python/quickstart` returned HTTP
200
- `PYTHON_VERSIONS=3.11,3.12 npm --prefix showcase/scripts run
check-claude-quickstarts:runtime -- --skip-typescript` (with `uv
0.12.5`)