mirror of
https://github.com/CopilotKit/CopilotKit.git
synced 2026-09-14 16:26:20 +08:00
main
3119 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f8160e09b9 | fix(react-core): gate suggestion submission on readiness + production-shaped SSE regression test | ||
|
|
9087121bd5 | fix(web-inspector): preserve state across pop-out | ||
|
|
1ef16b6789 |
feat(web-inspector): pop the Inspector into its own window
Keep the same live Inspector session in a named browser popup, restore it when the popup closes, and document the workflow. |
||
|
|
9b0e3dc88a | fix(react-core): gate CopilotChat submission on runtime readiness (empty assistant response) | ||
|
|
3fc33398af | Merge branch 'main' into codex/ent-1157-shared-clerk-session | ||
|
|
f1b26e4aa8 | fix(react-core): hide local inspector action in production | ||
|
|
367e7bda15 | feat(react-core): add local message inspector links | ||
|
|
af6c7ad5e0 | Merge branch 'main' into codex/ent-1157-shared-clerk-session | ||
|
|
4df1e3dccd |
docs(a2ui): require a literal-or-binding union for bound props (refs OSS-857)
Three findings from a LangGraph TypeScript onboarding run, plus the
supporting re-export.
The A2UI binder decides whether to resolve a `{ path }` binding by
inspecting the prop's Zod type: `scrapeSchemaBehavior` classifies a
`ZodUnion` containing an object with a `path` key as DYNAMIC and
everything else as STATIC, and STATIC returns the value untouched. A
bound prop declared as a plain `z.string()` therefore reaches the
renderer as the raw `{ path: "/origin" }` object, and the first thing
that renders it as text throws React error #31. The fixed-schema page
said the opposite — that renderer props are "plain z.string(), not a
path-or-literal union" — so the obvious declaration produced an opaque
crash. The reference cell already declares the union and carries a
comment explaining why, but that comment sits outside the
`definitions-types` region marker and so never reaches the page.
`DynamicStringSchema` is real; it lives in `@a2ui/web_core`, which is a
transitive dependency of `@copilotkit/a2ui-renderer` and so not
reliably importable from application code. Re-exported here with its
numeric/boolean/list siblings and their types.
The LangGraph quickstart's troubleshooting advice told everyone with a
connection problem to swap `localhost` for `0.0.0.0` or `127.0.0.1`.
That is backwards for the Node runtime: `langgraphjs dev` defaults to
`--host localhost`, which Node resolves to IPv6 and binds `::1` only,
so `127.0.0.1` is refused by the same running server. The Python CLI
defaults to `--host 127.0.0.1` and behaves the other way, so the advice
is now split across the page's existing Python/TypeScript language tabs
instead of stated once in shared prose.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
ac22a414ef | style: auto-fix formatting | ||
|
|
06cef9e659 |
feat(web-inspector): add view thread in your app
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. |
||
|
|
f8b5de55ab |
feat(runtime): report managed Channel drops and recoveries as telemetry (refs OSS-825) (#6465)
## 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. |
||
|
|
ec4439c8d9 | Merge origin/main into codex/ent-1157-shared-clerk-session | ||
|
|
471d74bad4 |
feat(react-ui): report feedback state to onThumbsUp/onThumbsDown (#6531)
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; ``` |
||
|
|
2d0b2838c0 |
fix(react-ui): pad the chat header on mobile viewports (#6530)
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.) |
||
|
|
72d57a41ec |
fix(channels-slack): rename decimal_allowed to Slack's is_decimal_allowed (OSS-794) (#6519)
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 | |
||
|
|
2f2481a048 | Merge branch 'main' into codex/pr-6253-review-fix | ||
|
|
08f05b39f4 | fix(core): preserve live message run reassignment | ||
|
|
cb11f6fb93 |
fix(runtime): warn when message content parts are dropped
normalizeMessageContent maps array content and handles only "text" and
"binary" parts. Any other part -- the {"type": "image", ...} case from
#1748 -- maps to "" and is filtered out with no signal at all, so an
agent emitting structured content sees its output silently vanish.
Carrying those parts through needs an AssistantMessage schema change and
is tracked separately in OSS-767. This makes the current drop visible in
the meantime, once per unrecognised part type so streaming does not flood
the log.
Refs #1748
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
ffd15801d6 |
fix(react-core): re-render consumers when the agent node changes
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 that. Adds the coverage the hook never had: transitions, run-start reset, run-error, and unsubscribe on unmount. Note this does NOT explain GH #1426 (interrupt agentMetadata.nodeName reporting the previous node). useInterrupt only evaluates the `enabled` predicate from a useEffect/useMemo keyed on its `pending` state, so the interrupt's own state update always forces a render before the predicate runs -- and the pre-fix code reads the ref correctly at that point. That report needs a different explanation; it is left open. Refs #1426 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c94032f3fa |
feat(react-ui): report feedback state to onThumbsUp/onThumbsDown
The thumbs callbacks only received the message, so a consumer could not tell whether a click applied feedback or retracted it. Custom AssistantMessage implementations that track their own toggle state had no way to pass that value through, and the built-in feedback state was write-only: clicking an active button re-applied the same value. Add an optional second argument reporting the state the click transitions to. The built-in AssistantMessage derives it from the message's current feedback, which also makes the built-in buttons toggle; a custom AssistantMessage may pass its own value. The argument is optional, so existing one-argument handlers are unaffected. The toggle logic is extracted to ./feedback so it can be unit tested — this package's vitest project runs in a node environment and has no component-rendering harness. Closes #2615 |
||
|
|
1ea18ca864 |
fix(react-ui): pad the chat header on mobile viewports
The `.copilotKitHeader` rule declared `padding-right` only inside `@media (min-width: 640px)`, so below that breakpoint the header's `space-between` children ran flush to the viewport edge and the close button had no gutter. Move the horizontal padding into the unconditional rule so it applies at every viewport. The value matches the 24px the sm breakpoint already used, so wide layouts are unchanged. Fixes #2493 |
||
|
|
6e3ed6e319 |
fix(core): copy the tool array handed to initialize
The merged tool view is memoized, so holding the caller's array meant an in-place mutation of it would go unnoticed. `setTools` already copied; `initialize` now matches. Also documents why the memo exists: `tools` is public and returned a stable array instance until a registry write, which consumers may rely on as an effect dependency — rebuilding on read would hand out a new array each time. |
||
|
|
b17c8556be |
feat(react-core): let a catch-all action wait for a response
`useCopilotAction({ name: "*" })` could only ever render. `getActionConfig`
short-circuited on the wildcard name before it looked for a wait-render, so
a catch-all declaring `renderAndWaitForResponse` was silently downgraded to
a render-only action: the render was called without `respond`, and in
practice threw `render is not a function` because the render-only path reads
`action.render`. Handling N human-in-the-loop tools therefore required N
separate hooks.
A catch-all now routes to the human-in-the-loop path when it declares a
wait-render, so the tool call stays pending until `respond` is called. Core
already had the execution half (`getWildcardTool`/`executeWildcardTool`),
so this is a routing fix rather than new machinery.
Two supporting changes make the render usable:
- The HITL render props now carry the name of the tool actually being
called. It equals the registration name for a normal action, but a
catch-all needs it to tell N tools apart, and `"*"` was being written over
it in both the v1 wrapper and the v2 hook.
- `CatchAllFrontendAction` accepts `renderAndWaitForResponse`/`renderAndWait`
alongside `render`, mutually exclusive as on `FrontendAction`, with
`CatchAllActionRenderPropsWait` exported for annotating the props.
Fixes #1746
|
||
|
|
31aa1e2162 |
fix(core): keep hook-registered tools across provider re-syncs
Frontend tools reached the registry two ways that shared one array:
provider-owned tools via `initialize`/`setTools`, and imperatively-added
ones via `addTool` (`useFrontendTool`, `useHumanInTheLoop`, direct
`core.addTool()` calls). Because `setTools` replaced the array wholesale,
every provider re-sync after mount silently wiped the hook tools.
A runtime with `openGenerativeUI` enabled made this reproducible on every
mount: the `/info` response flips the flag asynchronously, the provider
re-derives its tool list to add `generateSandboxedUi`, and the re-sync
dropped everything the hooks had registered — so the agent received only
`generateSandboxedUi` and the app's own tools were never advertised.
Split the two owners into their own buckets, merged on read with hook
entries taking precedence, mirroring what `CopilotKitCoreReact` already
does for render tool calls. `addTool` now shadows a provider tool of the
same name instead of refusing to register, and only warns when another
imperative registration already holds the name.
Also stop advertising a wildcard ("*") tool to the agent. It is a local
catch-all handler for calls with no exact match, so offering the model a
tool literally named `*` was never meaningful.
Fixes #4952
|
||
|
|
d45a478a83 |
fix(channels-slack): rename decimal_allowed to Slack's is_decimal_allowed
`SlackNativeProps` declared `decimal_allowed`. Slack's `number_input` field is `is_decimal_allowed`. Slack accepts a message whole or not at all, so the unrecognised key refused the entire `chat.postMessage` call with `invalid_blocks: invalid field at /blocks/N/element` and ended the Channels delivery that carried it — no exception reached the caller and nothing arrived in the channel. The typed path was therefore the broken one: a developer who bypassed our types and hand-wrote `is_decimal_allowed` got it right, while everyone using `Slack.Element.NumberInput` silently lost the message. Verified live against Slack, one delivery per case: - `decimal_allowed` alone — refused, `invalid_blocks: invalid field at /blocks/0/element` - `is_decimal_allowed` alone — delivered - both names present — refused with the same error, which isolates the unknown key as the cause: Slack's required field is satisfied and the payload is otherwise identical to the one that delivers Not treated as a breaking change and no alias is kept. The old name never worked, so no caller can depend on its behaviour, and keeping it would preserve the trap. Removing it converts a silent message loss into a compile error that names the right field. Also adds a test comparing every field name `native.ts` declares against Slack's own Block Kit declarations in `@slack/types` (already a dependency), parsed with the TypeScript parser rather than matched as text. Names `@slack/types` does not cover — `container`'s `blocks`, `rich_text_list`'s `offset`, `card`'s `slack_icon` and `subtext`, and the whole `data_visualization` vocabulary — are listed individually with the reason, so the check states its coverage instead of implying completeness. Floors on the vocabulary size and on the number of names actually compared keep a collapsed comparison from passing as success. Sweeping the remaining names turned up no other mismatch. |
||
|
|
703944880d | feat(angular): expose agent capabilities | ||
|
|
5720cd7fdc | feat(angular): support fetch credentials | ||
|
|
e86e7be328 |
fix(packages): declare the MIT SPDX license on five published packages
@copilotkit/core, /agentcore-runner, /sqlite-runner, /voice and /web-inspector all publish to npm without a "license" field, so registry metadata and automated license scanners report them as Unknown. The repo is MIT (see LICENSE) and every other published @copilotkit/* package already declares it; these five were simply missed. Reported downstream in #2860, where a corporate procurement scan blocked on Unknown-license dependencies. Note that the @ag-ui/* packages named in that report are NOT affected -- they have carried "license": "MIT" since at least 0.0.35, including every version the reporter's scanner flagged. Placed before "repository" to match the sibling packages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
12e89e60fa | Merge branch 'main' into fix/state-manager-run-id | ||
|
|
1f9b60b231 | chore: release monorepo v1.68.1 | ||
|
|
f8cb4d2447 | chore: release channels v0.9.0 | ||
|
|
e6864b6bdd | chore: release monorepo v1.68.0 | ||
|
|
f248a7eb30 |
feat(channels-slack): render table cells as rich_text when they carry markup
Portable <Cell> content was always emitted as a Slack `raw_text` cell, which is literal: markdown links, Slack link syntax and bare URLs all rendered as plain characters, so there was no way to get a clickable link or bold text into a table cell through the portable vocabulary. Body cells whose content contains a link, bold, italic, strikethrough or inline code are now emitted as a `rich_text` cell. Plain content still produces the byte-identical `raw_text` payload, and header cells always stay `raw_text` (Slack renders them bold already, and `rich_text` is not allowed in a `data_table` header cell). The conversion reuses `markdownToMrkdwn` — the package's single source of truth for the portable dialect — and tokenizes its `mrkdwn` output into rich-text runs, so the package keeps one markdown parser. The 2000-char cell budget now applies to the visible text of a rich cell. |
||
|
|
eb3f430ae1 | feat(runtime): mark Learning config experimental | ||
|
|
99da13de53 | fix(runtime): preserve Learning compatibility contracts | ||
|
|
a9f283ab55 | feat(runtime): assign threads to Learning Containers | ||
|
|
e7e8f7dc37 |
fix(runtime): only treat a request stream as consumed once it is drained
`isStreamConsumed` checked `req.complete` and the private `_readableState.ended`/`endEmitted` alongside `req.readableEnded`. The first two are set by the Node HTTP parser once all network bytes reach the socket, which happens before the route handler reads anything. Any framework that awaits between socket read and dispatch — notably the Next.js pages router — therefore reported an unread body as already consumed. With `bodyParser: false` there is no `req.body` to rebuild the request from either, so `copilotRuntimeNodeHttpEndpoint` logged "Request stream consumed with no available body" and forwarded an empty payload upstream, and the request failed with `400 Invalid JSON payload`. Rely only on `readableEnded`, which flips true after the `end` event fires from genuinely draining the stream. The `parsedBody !== undefined` check at the call site still covers the body-parser case. Diagnosed by @AlexNti in #3489, which patched the since-retired `packages/v1/runtime` path; re-applied here on `packages/runtime` with the tests ported and a live `http.IncomingMessage` regression test added. |
||
|
|
4e9eee3094 |
feat(runtime): add MiniMax built-in models (#6464)
Reason: Add the current MiniMax text models to BuiltInAgent model resolution. - Register MiniMax-M3 and MiniMax-M2.7 as built-in model identifiers. - Resolve MiniMax model strings through the global endpoint with API key and regional base URL configuration. - Document both model specifiers and cover global and China endpoint selection. Checks: - `node_modules/.bin/nx run @copilotkit/runtime:test -- src/agent/__tests__/resolve-model-baseurl.test.ts` - `node_modules/.bin/nx run @copilotkit/runtime:check-types` - `pnpm validate:model-names` - `node_modules/.bin/nx format:check --files=packages/runtime/src/agent/index.ts,packages/runtime/src/agent/__tests__/resolve-model-baseurl.test.ts` - `git diff --check` |
||
|
|
7187a0aa19 |
fix(channels-slack): three defects that silently broke Slack Block Kit (#6462)
Closes OSS-819. Part of OSS-794, which stays open for the OpenTag demonstration (OSS-820). *Reopened from #6454 — the branch was renamed so Linear links the right sub-issue, and GitHub closed the original rather than retargeting it. Same three commits, unchanged.* Three defects in the Slack Block Kit catalog, each verified against a real workspace. **99 lines changed across three files.** The reason these sat undetected matters more than their size: **a payload Slack refuses produces no error anywhere.** No log line, no exception, no failing test — the message simply never arrives, which is indistinguishable from a bot that had nothing to say. The renderer compounds it by design, dropping unknown nodes silently so one bad node cannot fail a whole message. ## 1. `container` was refused on every send Its children serialized into `blocks`; Slack reads `child_blocks`. ## 2. Every menu, checkbox, radio group, overflow and confirm dialog was refused The codec stamped `type` onto every catalog entry, including composition objects whose schema has none — Slack's option object is `{text, value}`, and the same holds for `confirm`, `option_group`, `conversation_filter`, `dispatch_action_config`, `slack_file`, `trigger` and `workflow`. An unknown field makes Slack reject the entire message, so the whole interactive surface was unusable through `Slack.Object.*`. Measured against a live workspace: **1 of 26 block elements delivered before this fix, 23 after.** Note the existing `native-catalog.test.ts` asserted the very assumption that was wrong — that every entry serializes its discriminator. It was green while the product was broken. It now asserts the corrected rule. ## 3. An image could not use a file already in the workspace The required-field check demanded `image_url` unconditionally; Slack accepts `image_url` *or* `slack_file`. An image needs alt text plus either source now, and passing neither is still an error. ## Two catalog corrections `file` leaves the authorable manifest. Slack: *"You can't add this block to app surfaces directly, but it will show up when retrieving messages that contain remote files."* The same sentence appears verbatim in `@slack/types`' own doc comment. It is an inbound shape; offering it as a component meant offering something that can never succeed. `alert` stays out with its citation — *"Alert blocks are currently only supported in modals."* Verified rather than assumed: Slack's own example payload posted verbatim into a message is refused, while a plain section in the same delivery seconds later arrives. ## How these were found A fixture per catalog entry — 19 authorable blocks, 26 elements, 15 composition objects — with the expected payload **transcribed from `docs.slack.dev`, not captured from our serializer**, delivered through a managed Channel into a real workspace. **55 of 60 deliver.** That corpus is a working instrument, not a deliverable, so it is deliberately not part of this PR — ~1700 lines of fixtures to maintain against a 99-line change is a bad trade for reviewers. It lives with the team and gets re-run when the catalog moves. One methodological note, because it changed what we count as proof: the first live run passed entries that demonstrated nothing. A rich-text block with one unstyled run renders exactly like a plain section; a carousel with one card renders like a card. Both were accepted and worthless as evidence — caught by a human looking at the output, not by the harness. Fixtures had to *exercise* each entry, and that is what surfaced defect 2. ## Found in the same pass, tracked separately - **OSS-817** — the managed path dropped every picker's value (9 of 26 elements). Fixed and confirmed live. - **OSS-818** — handler ids collide across structurally identical messages. ## Verification `test`, `check-types` and `build` green across `channels-slack`, `channels`, `channels-intelligence` and `runtime`, both with and without the fixture corpus present. Every block, element and object was delivered into a live Slack workspace and reviewed by eye. |
||
|
|
44d54c65d6 |
fix(react-core): repair useCopilotReadable effect deps, convert args, and dependencies (#6409)
Fixes #6383. Fixes #6243.
Both issues land in the same 35 lines of `useCopilotReadable`, so they
are fixed together. This PR also covers a third defect neither issue
reports.
All of it traces to a single commit:
|
||
|
|
47ad5e34a3 |
refactor(react-native)!: converge tool-call rendering onto CopilotKit's shared registry (#6438)
## What does this PR do?
`@copilotkit/react-native` maintained a **private tool-call render
registry** (`hooks/RenderToolContext.tsx`) alongside the canonical one
that `CopilotKitCoreReact` already provides — and which every React
Native app already ships, unused. This PR deletes the fork and points
React Native at the shared registry.
That fork caused three bugs:
| Bug | Symptom | Cause |
|---|---|---|
| **Tool renders never streamed** | A component registered with
`useRenderTool` / `useComponent` painted nothing until the tool call
completed | `CopilotChat` used `JSON.parse` on the argument buffer.
While a model writes a tool call that buffer is *invalid JSON by design*
— AG-UI delivers `TOOL_CALL_ARGS` deltas that are concatenated
client-side — so the parse threw on every delta, warned, and fell back
to `{}` |
| **`useComponent` rendered nowhere** | Silently, with no error | It
writes to core's registry; React Native's chat read React Native's
private `Map` |
| **Chat history degraded** | Navigating away from the registering
screen turned earlier tool calls into a `Called: <name>` placeholder |
The private `Map` deleted renderers on unmount; core deliberately keeps
them |
`@copilotkit/react-core` has used `partialJSONParse` on this path since
v2 shipped. React Native diverged because `useRenderToolCall` was
excluded from its re-exports on the stated grounds that it "depends on
DOM elements via `DefaultToolCallRenderer`" — a claim that was never
true of the hook itself. It was only ever reachable through the fat
`/v2` entry, whose weight is the real hazard (#4893). #5883 moved it
into `/v2/headless` on 2026-07-23; the exclusion comment was rewritten
the next day without revisiting the reason.
### What changed
- **One registry.** `useRenderTool` registers through `useFrontendTool`
into `CopilotKitCoreReact.renderToolCalls`. `CopilotChat` and any custom
surface consume react-core's `useRenderToolCall`.
- **Types are derived, not declared.** `RenderToolProps` is now
`React.ComponentProps<ReactToolCallRenderer<T>["render"]>`, so React
Native cannot drift from `ReactToolCallRenderer` — the contract every
registered renderer is actually invoked against. Change that contract
and `check-types` names every React Native renderer the change breaks.
React Native narrows only the *return* type to `ReactElement | null`,
which `FlatList`'s `renderItem` genuinely requires.
_Scope of that guarantee (corrected during review):_ it does **not**
extend to the type react-core publicly exports under the same name.
Web's `RenderToolProps<S>`
(`react-core/src/v2/hooks/use-render-tool.tsx`) is a separate
hand-declared union, generic over a schema, carrying arguments under
`parameters` (not `args`) and declaring `status` as string literals
rather than `ToolCallStatus` members. Both divergences are live today
and nothing type-checks them shut — the one place the shapes meet,
react-core's own bridge, compiles because a string-enum member is
assignable to its own literal type but not the reverse. Aligning web's
alias is a breaking web API change, filed separately.
- **`RenderToolContext.tsx` deleted** (−150 lines), along with 15 tests
that described the removed subsystem. One of them — `unregisters the
render function on unmount` — asserted the chat-history bug as a
requirement.
- **Two structural CI guards for #4893**, in opposite directions: a test
failing if any React Native source imports the fat `/v2` entry, and a
script failing if react-core's `/v2/headless` or `/v2/context` chunks
ever link shiki/mermaid/cytoscape/katex/streamdown. Both were verified
able to fail by deliberately introducing the regression. These are
*structural* assertions, not size budgets — `dev-docs/bundle-size.md`
freezes `limit` fields until OSS-122.
- **`react-native` added to the bundle-size glob**, which it had never
been in, plus a `size:headless` measurement.
React Native also gains capabilities it lacked: render props inferred
from your schema, `name`/`toolCallId` on render props, and `result` on
completed calls.
**Corrected during review — two capabilities this originally claimed are
not delivered:**
- **Wildcard (`"*"`) renderers do not work on React Native.** Because
`useRenderTool` routes through `useFrontendTool` (which calls
`addTool`), `name: "*"` registers a frontend tool literally named `*` —
advertised to the model, and colliding with core's separate
wildcard-executable-tool path. react-core's `useRenderTool` is
renderer-only and special-cases the wildcard; React Native's is not. The
guide now advises against it.
- **`followUp` (and `available`) are not forwarded**, and the handler's
`context` argument is dropped, so `stopAgent()`'s abort signal is
unreachable from an RN handler.
Both are tracked in § Known limitations for the follow-up that converges
React Native onto react-core's hooks — deleting RN's `useRenderTool` in
favour of re-exporting `useFrontendTool` (tool + renderer) and
react-core's `useRenderTool` (renderer-only, wildcard-capable). That is
an API change with its own migration note, so it is not in this PR.
### ⚠️ Breaking (in a minor)
`useRenderToolRegistry` and `RenderToolProvider` are **removed**. Both
are documented on the docs site, so this is a real break — see the
`BREAKING CHANGE:` footer on `db67ccf`, which is what the release notes
derive from, plus the rewritten reference pages.
```diff
- const registry = useRenderToolRegistry();
- const renderer = registry.get(toolCall.function.name);
- return renderer ? renderer({ args, status }) : null;
+ const renderToolCall = useRenderToolCall();
+ return renderToolCall({ toolCall });
```
Also note two semantic changes: `args` is `Partial<T>` **only** while
`status` is `"inProgress"`, and a render function is now captured at
registration — if it closes over changing values you must declare them
in `deps` (React Native previously refreshed the closure on every
render).
**Known limitation:** agent-scoped renderer resolution does not take
effect on React Native. `CopilotChatConfigurationProvider` is not in
RN's provider tree, so `agentId` always resolves to the default.
Renderers still resolve by name; two agents registering the same tool
name resolve arbitrarily. Filed separately.
### A data point worth recording
Adding `useRenderToolCall` to the measured headless entry moved the
bundle **92.8 kB → 92.7 kB**. Flat. The hook React Native spent months
not using was already inside the chunk every RN app resolves whole —
Metro doesn't tree-shake, so the fork never saved a byte. It cost them.
### Testing
- `@copilotkit/react-native`: **253 passing / 22 files** ·
`@copilotkit/react-core`: **1480 passing / 123 files** · `check-types`
and `build` green for both.
- Each of the three bugs has a deterministic test driving a real
`CopilotKitCoreReact` — no mocking of the code under test.
- Both #4893 guards carry mutation evidence: introduce the regression,
watch them fail, revert, watch them pass.
### Follow-up
`useRenderTool`'s JSDoc is split across two blocks, which orphans the
primary description from IDE hover (the `@param deps` warning still
surfaces). One-line fix, deliberately left out of the final fix wave.
## Related PRs and Issues
- **Supersedes #6346** (@davidmckayv) — its diagnoses were correct and
its test assertions are ported here, re-driven through the real registry
rather than a mocked local one. Credited via `Co-Authored-By` on
`4104bd1`.
- Addresses the React Native half of **#4893**.
- Builds on **#5883**, which created the lean `/v2/headless` entry this
PR consumes.
## Checklist
- [x] I have read the Contribution Guide
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation
- [x] "Allow edits by maintainers" is checked
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||
|
|
3a80c2696e |
feat(vue): mirror React's useAgent thread scoping, remove thread cloning (#6234)
## Problem
Vue's `useAgent` implemented per-thread agent **cloning** — a mechanism
React never had. Passing a `threadId` silently handed you a copy of the
agent:
```ts
useAgent({ agentId: "assistant", threadId: "thread-1" }) // → a clone, keyed (agent, threadId)
```
The clones lived in a module-level `WeakMap` (`globalThreadCloneMap`),
so:
- Nothing tied a clone's lifetime to the scope that created it — they
were never released.
- Components had to *look up* which copy was live.
`CopilotChatMessageView` called `getThreadClone(registryAgent,
config.threadId) ?? registryAgent` just to find the agent actually being
rendered.
- `getThreadClone` / `globalThreadCloneMap` were exported from the
module purely so components could do that lookup.
Meanwhile React grew an explicit contract for the same use case in
#6141: a private *proxied* agent, registered under a local `agentId` and
routed to a `runtimeAgentId`.
## Change
Deletes cloning entirely and ports React #6141's contract to Vue.
`cloneForThread`, `getOrCreateThreadClone`, `getThreadClone` and
`globalThreadCloneMap` are gone — zero references remain, including in
prose.
`UseAgentProps` becomes a base plus a two-branch union, with the same
all-or-nothing rule React now enforces:
```ts
useAgent() // shared registry agent
useAgent({ agentId }) // shared registry agent
useAgent({ agentId, runtimeAgentId, threadId }) // private proxied agent
```
Every partial set — `{ agentId, threadId }`, `{ agentId, runtimeAgentId
}`, `{ runtimeAgentId, threadId }` — is a compile error, backed by the
same three runtime guards with the same messages for callers TypeScript
doesn't reach.
### Parity with #6141
| | React (#6141) | Vue (this PR) |
|---|---|---|
| scoped branch | `agentId` / `threadId` / `runtimeAgentId`, all
required `string` | same, as `MaybeRefOrGetter<string>` |
| unscoped branch | `agentId?: string`, `threadId?: undefined`,
`runtimeAgentId?: undefined` | identical |
| runtime guards | 3 | same 3, same messages |
| thread resolution | prop → chat config, gated on `hasExplicitThreadId`
| identical |
| proxy registration | balanced effect on core + both ids | same deps |
## Two Vue-specific details
Both are load-bearing and were found by tests failing, not by
inspection:
**The pin watcher's first source is `() => agent.value`, not `agent`.**
Vue sets `forceTrigger` when any array watch source is a shallow ref, so
passing the ref directly re-ran the pin on *every* `triggerRef(agent)` —
i.e. every streamed message — re-pinning the inherited thread over one
`CopilotChat` had deliberately set for the chat it renders. Two existing
suites cover this (`uses the explicit agentId and threadId over
inherited configuration`). React has no equivalent hazard because effect
deps compare by identity.
**`CopilotChat` assigns `agent.threadId` inside its `/connect`
watcher**, not a separate one. `CopilotKitCore.connectAgent` reads that
field *synchronously* (`run-handler.ts`) to decide whether a restore is
fresh, so a later assignment lets `/connect` address the previous thread
— skipping the messages/state reset and re-stamping its restore key with
the stale id. Same placement as React's `CopilotChat`.
`CopilotChatMessageView` now resolves the registry agent directly
instead of consulting the clone map, and reads `copilotkit.agents` so it
recomputes when the registry changes.
## What callers see
**One agent per `agentId`** — the model React has always had. Thread
isolation is now explicit instead of implicit: ask for it and you get a
real, separately-registered agent rather than a copy that appears out of
nowhere.
```ts
// before — silently produced a copy of the "assistant" agent
useAgent({ agentId: "assistant", threadId: "thread-1" })
// now — an explicit private agent of your own, routed to "assistant"
useAgent({ agentId: "chat-1", runtimeAgentId: "assistant", threadId: "thread-1" })
```
Nothing in this repo needed updating: `CopilotChat`, `use-capabilities`,
`use-interrupt` and all six example apps already used `{ agentId }`.
`<CopilotChat agentId threadId>` is unchanged for consumers.
## Tests
`use-agent-thread-isolation.test.ts` (433 lines) covered clone semantics
that no longer exist; it's replaced by
`use-agent-thread-pinning.test.ts`, which pins the new invariants — one
instance per `agentId` never a copy, config-thread pinning gated on
explicitness, and all three all-or-nothing guards.
Four component suites used `getThreadClone` purely as a lookup to find
the agent under test and now read from the registry.
`MockMCPProxyAgent` recorded `addMessage` **only inside its `clone()`
override**, so those assertions were passing only because cloning
existed. The recording moves onto the class. `clone()` itself is left
intact everywhere — `CopilotKitCore`'s `SuggestionEngine` still clones
agents (`packages/core/src/core/suggestion-engine.ts`), so removing
those overrides would have planted a latent trap.
## Deliberately not included
Found while reviewing this area, real, but out of scope — each wants its
own change:
- `useAgent`'s header watcher **replaces** `agent.headers` instead of
calling `copilotkit.applyHeadersToAgent()`, dropping per-agent
construction-time headers. Regresses #5635 in Vue; React does this
correctly.
- `credentials` never reach a provisional agent.
- No `onAgentsChanged` subscription anywhere in `packages/vue`, so `()
=> copilotkit.value.agents` as a watch source never re-evaluates on
registry change.
- `/connect` is skipped for a plain `HttpAgent` — the `hasCustomConnect`
prototype comparison matches every real agent. Vue-only, no React
equivalent.
- `CopilotThreadsDrawer.ssr.test.ts` is a latent flake (5s timeout on a
dynamic import; passes in isolation).
|
||
|
|
4b17ea7d35 |
fix(scripts): tokenize before hunting loader calls in the purity gate
The #4893 hard-fail gate's loader-call detector gave WRONG VERDICTS IN BOTH DIRECTIONS. It layered two regexes — a comment/string/template alternation that blanked only the comment branch, and `\b(?:import|require(?:\.resolve)?)\s*\(` over the result — then classified an argument as static from the FIRST CHARACTER after the paren. All nine shapes below were reproduced against the real gate before the rewrite: false FAIL throw new Error("use require(path) instead") false FAIL `import(${x})` inside a template false FAIL o.import(y) / mod.require(x) (member calls, not loaders) false PASS /https:\/\//; …import(n) (the regex's `//` blanked the rest of the line, hiding a real dynamic call) false PASS import(`stream${n}`) (merely STARTS with a quote) false PASS import("zo" + n) (same) false PASS import(`${base}/v2/index.mjs`) (same — the fat entry) false PASS __require(name) (no \b inside `__require`) Replaced with `scanSource`, a single-pass tokenizer that classifies every character as code / comment / string / template / regex and returns a length-preserving masked view plus a literal-span list. The one surviving regex now only ever sees code, so import-shaped TEXT cannot reach it at all; an argument counts as static only when it is one COMPLETE literal with no concatenation or interpolation; `__require` is matched; and a member call is rejected both by lookbehind and by a whitespace-skipping back-scan (so `m\n .import(x)` is not a loader either). Proven in both directions: nine innocent/violation pairs run through the real `assertEntryPurity`, each innocent form CLEAN and each matching real violation FAIL. Re-proved end-to-end by prepending `import "streamdown"` to the real dist/v2/headless.mjs — exit 1 naming all five families — then restoring it byte-identically. On the untouched dist the scan sees 66 loader calls in the `.cjs` graph and classifies all 66 static, so it passes because it LOOKED. Also adds the first `.cjs` fixtures: every existing fixture was `.mjs`, leaving the script's `format: "cjs"` branch and the `require()` shape asserted by nothing. Tests 24 → 47. `stripComments` is renamed `maskNonCode`, since it now blanks literals and regexes too; it had no caller outside this script and its test. The RN guard keeps its own copy, untouched. dev-docs/bundle-size.md: the four holes a sibling agent documented as known limitations this round are closed and removed from that list; what genuinely remains (regex-vs-division heuristic, no JSX/TS, indirect loaders) replaces them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
4c17a8fe8c |
fix(react-native): key the messages fingerprint on object content
`messagesFingerprint`'s content key collapsed every object to 0, so an
in-place content replacement that kept the same message id was invisible to
every memo derived from it. Its comment claimed to mirror react-core's
`messagesMemoKey`, which stopped being true when react-core #6325
(
|
||
|
|
1b39c12e36 |
fix(scripts): stop the headless CLI gates skipping themselves on odd paths
Both scripts decided "am I the entrypoint?" by comparing `import.meta.url` to a `file://`-concatenated `process.argv[1]`. `import.meta.url` is percent-encoded and symlink-resolved; raw argv[1] is neither. So the comparison was false for any checkout path containing a space, for any invocation through a symlink (macOS /tmp is one), and on Windows — and a false guard skipped the whole CLI block. Reproduced before fixing: the #4893 purity gate and the bundle-size measurement both exited 0 having printed nothing and asserted nothing, which is worse than a gate with holes because it manufactures confidence. The guard was added by this PR so the modules could export internals to their new negative tests; making the gates testable introduced a way for them not to run. Both now compare real filesystem paths through an exported `isEntrypoint`: `fileURLToPath` defeats the encoding and Windows forms, `fs.realpathSync` on both sides defeats symlinks, and a `path.resolve` fallback keeps a nonexistent argv[1] from throwing. Each `node --test` suite gains five entry-guard tests, including an end-to-end spawn of the real script through a symlinked package-root alias whose name contains a space — the only case that catches the call site regressing back to a string comparison (verified: it fails against the old expression). The unit cases assert the naive comparison really would have failed, so none of them can pass vacuously. Both negative gates were re-proven to still bite: a doctored dist entry pulling streamdown fails the purity gate, and a stubbed dist entry trips the measurement's plausibility floor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c7d176f264 |
test(react-native): make the #4893 entry guard fail on violations, not on growth
The headless import-graph guard pinned the resolved graph EXACTLY — the
11-module list and the 8 bare specifiers, both `toEqual`. That catch-all was
deliberate (a heavy dependency nobody enumerated still had to be looked at),
but it also went red on innocent growth: adding any first-party `src/` module
to the headless graph failed it, on someone else's unrelated PR. A guard that
fails on innocent changes gets deleted by the third person who hits it, and
then it guards nothing.
Express the catch-all over PACKAGES instead of MODULES: the graph may only
reach packages a headless consumer is guaranteed to be able to resolve — this
package's `dependencies` plus its NON-optional `peerDependencies`, read from
package.json rather than hand-copied. That is precisely the promise the
headless entry sells ("bundles with nothing stubbed in metro.config.js"), so
it still fails on any new third-party edge, on every optional peer, on a
devDependency, and on a Node builtin — while a new first-party module or
another import of an already-sanctioned package is free.
The two other things the pin bought are kept explicitly:
- Comment stripping. The eight phantom specifiers JSDoc examples used to
harvest were all self-references, and this package's own name is not in the
guaranteed set, so a `stripComments` regression still fails here.
- Non-vacuity. Every remaining graph assertion is a deny-list, and a deny-list
over a truncated graph passes for the wrong reason, so a subset floor
asserts the walk still reaches the provider, the polyfills and the
react-core headless edge.
Not changed: comment stripping itself, the import()/require()/require.resolve
extraction, non-literal loader flagging, emitted-extension resolution, the
loud failure on unresolvable edges, the entry-presence tests, the #4893
fat-entry ban (still the assertion that catches `@copilotkit/react-core/v2`)
or the heavy-dependency ban. The runtime-export `beforeAll` is untouched.
Proven both directions: a new first-party module passes the loosened guard and
fails the old pin; `@copilotkit/react-core/v2`, `shiki`, an unenumerated
devDependency edge and a truncated walk each fail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
00caf5fa7b |
docs(react-native): fix the chat memo comment's identity rationale
`messagesFingerprint`'s header JSDoc and the matching inline comment near the
`listItems` memo justified keying on message CONTENT with a claim that is false:
that `agent.messages` is mutated in place throughout, and that "the AG-UI apply
pipeline reuses one array for a whole run".
It does not. `@ag-ui/client`'s `AbstractAgent.processApplyEvents` REASSIGNS
`this.messages = applied.messages` for every applied event, so a streaming run
hands down a new array — and new message, `toolCall` and `function` objects — per
delta. Verified against a real AG-UI run by the PR reviewer, and confirmed here in
@ag-ui/client 0.0.57's `AbstractAgent`. The old grep behind the claim ("assigning
`.messages` in packages/core/src hits test files only") is accurate but proves
nothing: `@ag-ui/client` is a dependency, outside that tree.
The fix itself stands. Identity is unreliable in BOTH directions, which is the
actual rationale: it changes on the apply path, and it does NOT change on the
paths these memos exist to serve — core splices tool results in place
(`agent.messages.splice(insertAt, 0, toolMessage)`,
packages/core/src/core/run-handler.ts:931, :1080), `AbstractAgent.addMessage` is a
`this.messages.push(...)`, and `useAgent` re-renders with a bare `forceUpdate()`
(packages/react-core/src/v2/hooks/use-agent.tsx:382-396). A signal that both
misses changes and fires without them cannot be a dependency, so the derivations
must key on content.
Comments only: three sites reworded (the JSDoc, the "cannot be used" pointer at
the `messagesKey` call, and the inline note on the `listItems` memo). `git diff`
touches no behaviour, type or dependency array — every changed line is a comment.
The `contentKey` length-vs-value paragraph is left alone; another change owns it.
Note: the same false claim is in commit 77ed31c437's body, which cannot be
rewritten, and in a GitHub review comment.
Not run in this worktree: it has no node_modules, and the change is comment-only,
so it cannot affect types, lint or tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
a3b814a041 |
fix(core): refresh Intelligence delegate headers before every join (#6469)
## Summary
`ProxiedCopilotRuntimeAgent` builds its `IntelligenceAgent` delegate
**once** and caches it for the proxy's lifetime, copying `headers` into
the delegate's constructor config. Nothing ever refreshed that copy, so
**a header that changed after the delegate was created never reached
`/connect` or `/run`** — for the life of the agent.
For a multi-tenant app carrying the active tenant in a header, the join
was attempted under the *previous* tenant's identity with the *new*
tenant's thread id, and the platform correctly answered
`THREAD_NOT_FOUND`. Only a full page reload cleared it, because that
rebuilds the delegate. A rotated or refreshed `Authorization` bearer has
the same exposure.
Reported by Sameday against 1.67.1 with a deterministic staging repro:
```
19:45:23.554 | /copilotkit/runtime/threads | hdr=<tenant B> | 200
19:45:23.886 | /copilotkit/runtime/threads/subscribe | hdr=<tenant B> | 200
19:45:23.893 | /copilotkit/runtime/agent/<id>/connect | hdr=<tenant A> | body.companyId=<tenant B> | 404
```
`/threads` carries **B** while `/connect` carries **A**, ~340ms apart in
the same switch. Not a race — a stale copy with no refresh path.
## Root cause
`setHeaders` / `applyHeadersToAgent` could not fix this: they write an
agent's `.headers`, and `IntelligenceAgent` exposed only `private
config`. `syncDelegate` *looks* like the refresh path, but its
`hasHeaders` probe is `"headers" in agent` — false for the delegate,
since `headers` is declared on `HttpAgent`, not on `AbstractAgent`. So
`config.headers` was the sole header source for Intelligence REST calls,
with no refresh path at all.
## The fix
Expose `headers` as a public accessor pair backed by `config`, and read
it in `requestJoinCredentials$`.
**The accessor is the entire fix**: it makes `hasHeaders` true, so
`syncDelegate` — which already runs on every `resolveDelegate()`, and is
preceded by `applyHeadersToAgent` in `RunHandler.connectAgent` — starts
actually refreshing the delegate before each join. No new plumbing.
Two things worth flagging for reviewers:
1. **The originally-suggested fix ("make `requestJoinCredentials$` read
live headers") does not work on its own** — and is actively harmful.
There was no live header source on the class to read: without the
accessor, `this.headers` is `undefined` and **every header is dropped**
(verified: only `Content-Type` survives). The read here goes through the
accessor for a single source of truth, not because that read carries the
fix.
2. **The setter replaces the config object rather than mutating it**,
because `clone()` shares the config reference. The join path alone would
mask an in-place write (`syncDelegate` rewrites headers just before
every join), but the credential re-acquisition inside a running pipeline
(`intelligence-agent.ts:563`) does not re-sync — so a clone's tenant
could ride out on the original's socket-error refresh. That's the same
cross-tenant leak this accessor exists to prevent.
`credentials` had the identical defect via `config.credentials`
(`hasCredentials` was false too) and gets the same treatment.
## Testing
**Unit tests (5 new, each written first and watched fail).** The pre-fix
failure is the staging symptom reproduced:
```
FAIL > sends a header changed after the delegate was created
AssertionError: expected { …(2) } to match object { 'X-Tenant': 'tenant-b' }
- "X-Tenant": "tenant-b",
+ "X-Tenant": "tenant-a",
```
Coverage: a header changed post-construction reaches `/connect`; the
same on the `/run` path (which was independently verified broken
pre-fix, sending tenant A where B was expected); credentials likewise; a
clone's header update must not reach the original
(`IntelligenceAgent.clone()` invariant — this one fails under in-place
config mutation); and a per-thread clone and its original each send
their own tenant.
**Verified beyond the unit tests.** Because the mocked-harness result
alone doesn't prove the production wiring, I drove the real chain —
`CopilotKitCore.setHeaders` → registry → proxy → delegate → outbound
POST — in a plain Node process with no vitest and no `vi.mock`, stubbing
only `fetch` at the network boundary. Same script against the unfixed
file, then the fix:
```
BEFORE (origin/main) AFTER (this PR)
"headers" in delegate: false "headers" in delegate: true
delegate.headers: undefined delegate.headers: { X-Tenant: tenant-b }
proxy.headers after setHeaders(B): proxy.headers after setHeaders(B):
{ X-Tenant: tenant-b } { X-Tenant: tenant-b }
0: POST /connect X-Tenant=tenant-a 0: POST /connect X-Tenant=tenant-a
1: POST /connect X-Tenant=tenant-a <-- 1: POST /connect X-Tenant=tenant-b credentials=include
FAIL (stale headers) PASS (live headers reach /connect)
```
The "before" column reproduces the report's tell exactly:
`proxy.headers` correct at tenant B while `/connect` still sends tenant
A, through the very API the report found ineffective.
**Gates** (run in a worktree with a freshly built `@copilotkit/shared`,
since a stale dist otherwise produces 20 unrelated
`core-inspector-metadata` failures and 4 `tsc` errors):
| Gate | Result |
| --- | --- |
| `@copilotkit/core` vitest | **654 passed / 654**, 59/59 files |
| `tsc --noEmit` | clean |
| `oxlint` | 0 errors (2 warnings, both pre-existing test helpers) |
| `oxfmt` | no reformatting needed |
**Not covered:** `fetch` is stubbed, so this does not exercise a live
Intelligence gateway or a browser tenant switch — it proves the outbound
header is correct, not the platform's response to it.
## Note for whoever merges
#6450 and #6468 also touch `intelligence-agent.ts` (thread-restore work)
but neither goes near the header path, so conflicts should be textual at
worst.
## Follow-up left out of scope
Two separate pre-existing defects surfaced while verifying this one.
Neither is touched here.
**1. `credentials` passed to a `ProxiedCopilotRuntimeAgent` constructor
are dropped at registration.** `applyCredentialsToAgent` overwrites
`agent.credentials` from core unconditionally, with no per-agent
baseline — unlike `applyHeadersToAgent`, which merges over the
`agentOwnHeaders` baseline captured for exactly this reason (#5635).
Probed in a real process: an agent constructed with `credentials:
"include"` in a core with none configured reports `undefined`
immediately after registration, and every join goes out without
credentials. Identical before and after this PR, so it is not a
regression from this change — but the headers/credentials asymmetry
looks unintended, given #5433 was specifically about preserving proxied
runtime credentials.
**2. `buildRuntimeUrl` reads `config.agentId`
(`intelligence-agent.ts:770`), (`intelligence-agent.ts:770`), so
`syncDelegate`'s `delegate.agentId = routedAgentId()` is cosmetic for
the REST URL. Same root-cause class as this bug, but latent rather than
live (routing is fixed per proxy instance).
Happy to file both separately.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||
|
|
70545f072a |
test(core): correct an overclaiming comment, tighten the credentials assertion
The per-thread-clone test's comment claimed it guards the copy-on-write setter. It does not: syncDelegate rewrites headers before every join, so it passes even with an in-place write (verified). Say what it actually pins — each proxy's joins carry its own tenant — and point at the clone-invariant test that does guard the setter. Also assert the pre-change join carried no credentials, so the credentials test shows a transition rather than a single end state. |