Bug: v1 surfaces orphaned by the v1.50.0 re-implementation (readable context; server-side actions + MCP)
Summary
Commit 80dffec4e7 ("Reimplement CopilotKit on top of refreshed internals", v1.50.0, #2638) repointed useCopilotReadable from the v1 context tree onto the v2 flat context store. The writer moved; the readers did not. The v1 tree now has no writers at all, and four features that read from it silently produce nothing.
This is a tracking issue for the surfaces left behind. The hook-level defects from that same commit are fixed in #6409 (see #6383, #6243).
The v1 tree has no writers
The only remaining references to the v1 provider's addContext in packages/react-core/src/context/copilot-context.tsx are the type declaration (line 143) and the default no-op stub () => "" (line 278). Nothing calls it, so printTree always returns "".
What still reads it
1. CopilotTask — publicly exported and documented.
includeCopilotReadable defaults to true (copilot-task.ts:102, config.includeCopilotReadable !== false). At copilot-task.ts:129-133 it appends context.getContextString([], defaultCopilotContextCategories), which now evaluates to the literal string "\n\n". Every CopilotTask ships zero readable context while reference/v1/classes/CopilotTask.mdx promises otherwise. This is the most user-visible item here.
2. Dev console logReadables — react-ui/src/components/dev-console/utils.ts:72-83 always prints "No readables found".
3. CopilotTextarea autosuggestions — use-make-standard-autosuggestions-function.tsx:61 reads the empty tree. Moot in practice: the same port commented out the entire network call (lines 81-101) and replaced it with const response: any = {} (line 102), so the function unconditionally returns "". @copilotkit/react-textarea is published at 1.65.0 and is not marked deprecated.
4. useCopilotReadable's parentId / categories.
Both were dropped from the hook body by the same commit but left in UseCopilotReadableOptions and in the JSDoc, whose top-of-file example was built entirely on parentId. #6409 marks them @deprecated and rewrites the example; this issue tracks whether they should come back.
What the feature did
parentId built a tree that printTree rendered into an indented outline for the prompt — 1. / 2. at the root, A. / B. one level down, a. / b. below that. For a list of employees it produced:
1. Employee name: Jane Doe
A. Work profile: Senior engineer, backend
B. Employee metadata: Joined 2021, SF office
2. Employee name: Sam Ortiz
A. Work profile: Designer, growthThe grouping told the model which profile belonged to which person. Today those six entries are a flat array and pairing can only be inferred from array order.
categories filtered visibility: printTree(categories) emitted only top-level nodes whose category set intersected the requested set, which is how a CopilotTextarea could be shown a narrower slice of context than the main chat. (Note for any reimplementation: the filter applied only at the root — children of a matching node were printed unconditionally.)
Deciding
Not obviously one bug. Roughly:
- Restore hierarchy/categories in the v2 context store and re-wire the hook. The AG-UI
Contexttype is{ description, value }, so this needs a serialization story for the nesting. - Leave v2 flat, but fix or explicitly gate the orphaned readers so
CopilotTaskstops silently under-delivering, and removeparentId/categories. - Formally deprecate the v1 readable-context surface end to end.
Flagging (2) as the minimum, because CopilotTask's default-true option quietly does nothing today.
Second orphaned surface: v1 server-side actions and mcpServers never execute
Same commit, same shape as the readable-context half above — the tool definitions were ported to v2, the executor was not. Verified live on main and against the published @copilotkit/[email protected] tarball on 2026-09-05.
Unlike the readable-context half, this one is not silent-and-empty. It crashes the frontend.
The defect
getToolsFromActions (copilot-runtime.ts:530) and getToolsFromMCP (:815) both build every tool with:
execute: () => Promise.resolve(),That function is passed straight through to the AI SDK (agent/index.ts:724). Nothing anywhere in the package calls action.handler or an MCP tool's execute — grep for .handler( in packages/runtime/src returns nothing outside tests. The v1 Action.handler a user writes is never invoked.
convertMCPToolsToActions (mcp-tools-utils.ts:159) is the function that would have called it. It is dead outside its own unit test.
Why it surfaces as a Zod crash
agent/index.ts:1699-1710 serializes the result:
let serializedResult: string;
try {
serializedResult = JSON.stringify(toolResult);
} catch { /* ... */ }
const resultEvent: ToolCallResultEvent = { /* ... */ content: serializedResult };toolResult is undefined, and JSON.stringify(undefined) returns undefined, not a string. serializedResult is declared string and holds undefined, so the key is dropped from the emitted event. The frontend then rejects a TOOL_CALL_RESULT whose required content is missing.
Two more defects in the same method
mcpServers is ignored entirely without actions. getToolsFromMCP() is only reached inside if (actions) (:498-500). Configure mcpServers + createMCPClient and no actions, and you get zero tools — createMCPClient is never even called.
The request-scoped MCP merge is dead code. getToolsFromMCP(options) has exactly one call site (:500) and it passes no arguments, so the merge of properties.mcpServers / properties.mcpEndpoints at :755-760 can never run. The frontend setMcpServers path does not reach the runtime.
Reproduction
A test driving the real streamText with a MockLanguageModelV3, so the AI SDK genuinely invokes execute. The control case proves the harness detects execution:
| Case | Result |
|---|---|
CONTROL — tool with a real execute |
spy called once, content: "REAL_RESULT" |
v1 actions handler |
tool advertised as greet, handler called 0 times |
v1 mcpServers without actions |
0 tools registered, createMCPClient never called |
v1 mcpServers with actions |
tool advertised as mcp_greet, execute called 0 times |
event types: RUN_STARTED,TOOL_CALL_START,TOOL_CALL_ARGS,TOOL_CALL_END,TOOL_CALL_RESULT,RUN_FINISHED
TOOL_CALL_RESULT: {"type":"TOOL_CALL_RESULT","role":"tool","messageId":"0ffe…","toolCallId":"tc-1"}
RUN_FINISHED: {"type":"RUN_FINISHED","threadId":"test-thread","runId":"test-run"}No content, no error, no failed run. packages/runtime/src/lib/runtime/__tests__/ has no test covering action or MCP execution, which is why this survived.
Two issues were closed on this
- #2915 (2025-12-22,
1.50.1) reported it precisely: "the handler does not seem to be called… TOOL_CALL_RESULT does not have a 'content' property." Closed as "backend actions are deprecated as of 1.50.0, replaced by MCP." The replacement offered was v2BuiltInAgent({ mcpServers }), which does work — but the deprecation was never written down anywhere the reporter could find it (their reply: "Can we mention this somewhere in the docs?"), and deprecating a parameter is not the same as leaving it in place emitting a malformed event. The v1CopilotRuntime({ mcpServers })surface carries the identical no-op. - #3198 (2026-02-11,
1.51.3) was closed as "fixed by the v2 runtime rewrite — tool call results now always includecontent: JSON.stringify(toolResult)." The stringify is real and still there, but it cannot rescue a tool that never ran. #3198's exact repro (copilotRuntimeNextJSAppRouterEndpoint+OpenAIAdapter+actions:as a function) still fails on 1.65.0.
Both should be reopened against whatever this becomes, or closed pointing here.
Also blocked behind this, both on CopilotRuntime({ mcpServers, createMCPClient }): #2407 (dynamic API keys) and #2409 (server-name tool prefixes). Neither feature is reachable while the path does not execute.
Adjacent dead surface on the same class
Checking every declared constructor param for a reader, not just these two:
remoteActions,langserve,delegateAgentProcessingToServiceAdapter,onStopGeneration, and v1'sonErrorare declared and JSDoc'd onCopilotRuntimeConstructorParamsbut read nowhere in the package.CopilotRuntimeConstructorParamsextendsCopilotRuntimeOptionsVNext, so the type accepts every v2 option, while the constructor forwards nine (agents,runner,licenseToken,debug, the two middlewares,a2ui,mcpApps,openGenerativeUI).forwardHeadersis among the silently dropped — which is the option a dynamic-auth user would reach for first.
Deciding
Docs have already moved MCP onto v2 BuiltInAgent; built-in-agent/mcp-servers.mdx even names dynamic auth as the reason to prefer mcpClients. So the v1 runtime actions / mcpServers surface is undocumented but still exported, still typed, and still JSDoc'd as working.
Mirroring the options above:
- Wire the executor back up — resurrect
convertMCPToolsToActionsand givegetToolsFromActionsa realexecutethat callsaction.handler. - Leave it unwired, but fail loudly: throw at construction when
actionsormcpServersis passed, instead of emitting a malformed event that crashes the caller's frontend. - Remove the parameters from the type and the docs entirely.
Flagging (2) as the minimum, on the same reasoning as CopilotTask above — a documented-looking option that silently does nothing is the worst of the three, and here it also takes the frontend down with it.
This does not overlap #6474's scope.
Systematic scan of 80dffec4e7
Ran the pass this issue implies rather than continuing to find these one at a time. Method: take the commit's deletions as the ground truth for what lost an implementation, then trace every surviving public surface that depended on each deleted module, then execute the candidates rather than trusting the read.
The commit deleted eight source files. Three are the readable-context and chat story already covered above. The other five are the subject of this scan:
D packages/react-core/src/utils/extract.ts
D packages/runtime/src/lib/runtime/agui-action.ts
D packages/runtime/src/lib/runtime/remote-action-constructors.ts
D packages/runtime/src/lib/runtime/remote-actions.ts
D packages/runtime/src/lib/runtime/remote-lg-action.tsItem 3 — the GraphQL transport is unreachable, so CopilotTask cannot run at all
This escalates item 1 above, and I got it wrong the first time: CopilotTask does not merely ship empty context. It cannot complete a request.
CopilotTask.run() (react-core/src/lib/copilot-task.ts:143-151) builds a CopilotRuntimeClient and calls generateCopilotResponse — the GraphQL operation. The resolver still exists in the tree (runtime/src/graphql/resolvers/copilot.resolver.ts:155), and it is real code, not a stub. Nothing mounts it.
buildSchema(lib/integrations/shared.ts:58) is exported and never called anywhere in the package.- Every surviving v1 integration mounts the v2 route instead.
copilotRuntimeNextJSAppRouterEndpointand the node-http endpoint both callcreateCopilotEndpointSingleRoute, which registers.all("*", handler)againstcreateCopilotRuntimeHandlerinsingle-routemode (v2/runtime/endpoints/hono-single.ts:33-43). - So a
CopilotTaskGraphQL POST lands on an AG-UI handler that does not speak GraphQL.
Confirmed by running it. Feeding the handler the exact body CopilotRuntimeClient sends for CopilotTask.run():
POST /api/copilotkit { "operationName": "generateCopilotResponse", "query": "mutation …", "variables": {…} }
status: 400
content-type: application/json
body: {"error":"invalid_request","message":"Missing method field"}The whole graphql/resolvers/ tree is unreachable dead code, and so is buildSchema.
This changes the scope of #6474. That PR bridges getContextString() to the flat store so CopilotTask stops under-delivering context — correct as far as it goes, but it improves the input to a call that cannot succeed. @ShitK, this is not a problem with your PR; it is a second layer nobody had found when the scope was agreed. Worth deciding whether CopilotTask gets a transport or gets removed before more work goes into it.
Item 4 — the executors were deleted and the parameters were kept
This is the mechanism behind the actions / mcpServers section above, and it explains three more parameters at once. remote-actions.ts, remote-action-constructors.ts, remote-lg-action.ts and agui-action.ts were the machinery that turned a v1 Action or endpoint into something that actually ran — constructAGUIRemoteAction, isRemoteAgentAction, and the endpoint constructors. All four files were deleted. The parameters they served were not.
That accounts for the dead constructor options listed above as one story rather than five separate oversights: actions, remoteActions, remoteEndpoints, langserve, and mcpServers all lost their executor in this commit. EndpointDefinition, CopilotKitEndpoint and LangGraphPlatformEndpoint survive as exported types describing a capability that no longer exists.
extract() (react-core/src/utils/extract.ts) was deleted with no replacement and nothing calls it.
What I checked and found healthy
Recording these so the list is bounded and nobody re-walks them:
- Chat suggestions.
utils/suggestions.tswas deleted, but the feature was genuinely re-implemented —v2/hooks/use-configure-suggestions.tsxdrivescopilotkit.reloadSuggestions, with e2e coverage. Migrated, not orphaned. use-chat.ts. Deleted; the legacy hook rides the v2useAgentpath. Already resolved separately.availableAgents: []inuse-copilot-runtime-client.tsandcopilot-messages.tsx. These read like stubs and are not — both are arguments to aCopilotKitAgentDiscoveryErrorconstructor. No defect.
Coverage and limits
Honest about what this pass does and does not cover. It follows the commit's deletions, which is the highest-yield signal and is what found items 3 and 4. It does not exhaustively re-verify the 157 files it modified — a surface that was quietly repointed without deleting anything would not show up this way, which is exactly how the readable-context half in item 1 escaped, and that one was found by hand.
Verification status, so the next reader knows what to trust:
| Finding | Evidence |
|---|---|
v1 actions handler never called |
Executed — control test proves the harness detects execution |
v1 mcpServers ignored without actions |
Executed |
v1 MCP execute never called |
Executed |
GraphQL unreachable; CopilotTask cannot run |
Executed — handler returns 400 to the exact CopilotTask request body |
| Executors deleted, parameters kept | git show --name-status 80dffec4e7 |
Everything except item 4 is proven by running the code; item 4 is a git show. The one gap worth naming: the 400 above is the server refusing the request, which is decisive for the transport, but nobody has yet driven a real CopilotTask from a browser against a live runtime to see what the user actually experiences.
Scan, part two: the modified files
Part one followed the commit's deletions. This pass covers the gap named there — the 81 modified source files, where a surface could be repointed without anything being deleted. That is how item 1 escaped for eight months, so it is the half that matters.
Three patterns, applied across react-core, react-ui, react-textarea and runtime, skipping /v2/ and tests: a public function whose body was replaced by a constant; a network call commented out with a stub left behind; and a registry whose writers lost all their callers.
Item 5 — the v1 action registry has no writers either
Item 1 says the v1 readable-context tree has no writers. The same is true of the v1 action registry, and the two are the same defect on two registries.
setAction and removeAction are the only things that call setActions (copilot-provider/copilotkit.tsx:218,227), and they are the bodies of those two methods. Nothing else in the package calls either one. useCopilotAction no longer touches the v1 context registry at all — it delegates to the v2 hooks (useRenderToolCall, useHumanInTheLoop, useFrontendTool, use-copilot-action.ts:237-241), which is a correct migration. The registry it used to fill was just never removed.
context.actions is therefore permanently {}. Two surviving readers:
copilot-task.ts:114—Object.assign({}, context.actions). SoCopilotTasksees no actions, on top of no context (item 1) and no transport (item 3). Three independent failures stacked on one class.developer-console-modal.tsx:543—Object.values(context.actions). This extends item 2: the dev console's actions panel is blank for the same reasonlogReadablesis, which that item did not name.:674and:317readgetAllContext()and are blank for the item 1 reason.
Item 6 — CopilotTextarea insertion does not return empty, it throws
Item 3 above covers use-make-standard-autosuggestions-function.tsx, where the network call is commented out and const response: any = {} makes the function return "". There is a second hook in that same directory with the same treatment and a worse outcome.
use-make-standard-insertion-function.tsx:42:
const runtimeClient: any = {
generateCopilotResponse: (...args: any[]) => {},
};Eleven lines later, :53:
const messagesStream = runtimeClient.asStream(responsePromise);asStream is not on that object literal. Insertion and editing in CopilotTextarea raise TypeError: runtimeClient.asStream is not a function the moment a user triggers them. The any annotation is what lets it compile.
Same caveat as item 3: @copilotkit/react-textarea is published at 1.65.0 and is not marked deprecated.
Item 7 — the wider v1 context husk
Beyond addContext and setAction, these v1 context methods have no remaining caller anywhere in the repo outside their own definition:
addChatSuggestionConfiguration removeChatSuggestionConfiguration
addInterruptEvent resolveInterruptEvent
setInterruptAction removeInterruptAction
setRegisteredActions removeRegisteredAction
setCoagentStatesWithRef setRunId
setInternalErrorHandler removeInternalErrorHandlerStating the limit plainly: zero internal callers is not by itself proof of orphaning, because useCopilotContext() is public and an application may call these directly. It is a signal, not a verdict. For addContext and setAction we have more than a signal — the hooks that drove them demonstrably moved to the v2 store. The rest of this list needs the same one-by-one treatment before anyone acts on it.
What came back clean
useCopilotActionmigrated correctly. It delegates to the v2 hooks rather than writing to the dead v1 registry.- Chat suggestions,
use-chat.ts— as recorded in part one. - Stubbed-out network calls: exactly two in the whole scanned surface, both in
react-textarea, both listed above. No others.
The shape that emerges: the v1 hook layer was largely migrated properly. It is the v1 context object — and everything still reading from it — that was left hollow. CopilotTask and the dev console are the two consumers that never got repointed.
Where this scan still does not reach
react-ui and shared were scanned with the same three patterns and produced no hits, but a defect that matches none of those three shapes would not show up. And every item in pa
Source: CopilotKit/CopilotKit