Skip to content

Commit 20bdf57

Browse files
committed
docs(react-core): fix CR findings in v2 skills
- useAgent: isRunning is not on the return type; sourced from agent - addContext: agentId is not accepted; remove "per-agent escape hatch" - provider-setup: publicApiKey is canonical, publicLicenseKey is alias - chat-components/attachments: add missing copilotkit.runAgent call - attachments: map Attachment[] to InputContent[] before spreading - rendering-activity-messages: fix Rules-of-Hooks violation in "Correct" example - custom-message-renderers: guard runId.slice against missing-run-id fallback - human-in-the-loop: abort-on-unmount effect was capturing stale isRunning - client-side-tools: Skeleton imports from @/components/ui/skeleton - threads: CopilotKitIntelligence ships on @copilotkit/runtime/v2 with apiUrl/wsUrl/apiKey/organizationId config - provider-setup: remove stale-token useMemo lead example - package.json: add per-condition types to exports map
1 parent 0d6fec9 commit 20bdf57

11 files changed

Lines changed: 189 additions & 94 deletions

File tree

packages/react-core/package.json

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,24 @@
4343
"jsdelivr": "./dist/index.umd.js",
4444
"exports": {
4545
".": {
46-
"import": "./dist/index.mjs",
47-
"require": "./dist/index.cjs"
46+
"import": {
47+
"types": "./dist/index.d.mts",
48+
"default": "./dist/index.mjs"
49+
},
50+
"require": {
51+
"types": "./dist/index.d.cts",
52+
"default": "./dist/index.cjs"
53+
}
4854
},
4955
"./v2": {
50-
"import": "./dist/v2/index.mjs",
51-
"require": "./dist/v2/index.cjs"
56+
"import": {
57+
"types": "./dist/v2/index.d.mts",
58+
"default": "./dist/v2/index.mjs"
59+
},
60+
"require": {
61+
"types": "./dist/v2/index.d.cts",
62+
"default": "./dist/v2/index.cjs"
63+
}
5264
},
5365
"./package.json": "./package.json",
5466
"./v2/styles.css": "./dist/v2/index.css"

packages/react-core/skills/agent-access/SKILL.md

Lines changed: 32 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ description: >
55
push JSON-serializable shared state via useAgentContext({ description,
66
value }). Covers per-thread clone WeakMap, UseAgentUpdate filter,
77
ProxiedCopilotRuntimeAgent provisional agent, agent.addMessage /
8-
setState / abortRun. useAgentContext has no agentId parameter — context
9-
is intentionally global. For rare per-agent scoping, use
10-
copilotkit.addContext({ agentId }).
8+
setState / abortRun. useAgent returns { agent }; run status comes from
9+
agent.isRunning (subscribe via UseAgentUpdate.OnRunStatusChanged).
10+
useAgentContext has no agentId parameter and context is always global —
11+
the core addContext API also ignores any agentId field, so per-agent
12+
context scoping is not supported.
1113
type: framework
1214
framework: react
1315
library: copilotkit
@@ -49,7 +51,7 @@ export function ChatDriver({
4951
route: string;
5052
userId: string;
5153
}) {
52-
const { agent, isRunning } = useAgent({
54+
const { agent } = useAgent({
5355
agentId: "default",
5456
threadId: "main",
5557
updates: [
@@ -64,7 +66,8 @@ export function ChatDriver({
6466

6567
return (
6668
<div>
67-
{isRunning ? "…thinking" : "idle"}{agent.messages.length} messages
69+
{agent.isRunning ? "…thinking" : "idle"}{agent.messages.length}{" "}
70+
messages
6871
</div>
6972
);
7073
}
@@ -87,12 +90,17 @@ async function ask(text: string) {
8790
### Subscribe only to run-status to reduce re-renders
8891

8992
```tsx
90-
const { isRunning } = useAgent({
93+
const { agent } = useAgent({
9194
agentId: "default",
9295
updates: [UseAgentUpdate.OnRunStatusChanged],
9396
});
97+
const isRunning = agent.isRunning;
9498
```
9599

100+
`useAgent` returns `{ agent }` only; `isRunning` lives on the agent
101+
itself. Subscribing to `OnRunStatusChanged` forces a re-render when the
102+
value flips, so reading `agent.isRunning` stays live.
103+
96104
### Share app state with every agent run (global)
97105

98106
```tsx
@@ -103,31 +111,6 @@ const value = useMemo(
103111
useAgentContext({ description: "user cart + route", value });
104112
```
105113

106-
### Per-agent context (rare — escape hatch)
107-
108-
`useAgentContext` is intentionally global. For the rare case where a
109-
specific agent should see a different context, use the imperative API.
110-
111-
```tsx
112-
import { useCopilotKit } from "@copilotkit/react-core/v2";
113-
import { useEffect, useMemo } from "react";
114-
115-
const { copilotkit } = useCopilotKit();
116-
const value = useMemo(
117-
() => ({ papers: researchState.papers }),
118-
[researchState.papers],
119-
);
120-
121-
useEffect(() => {
122-
const id = copilotkit.addContext({
123-
description: "paper list",
124-
value: JSON.stringify(value),
125-
agentId: "research",
126-
});
127-
return () => copilotkit.removeContext(id);
128-
}, [copilotkit, value]);
129-
```
130-
131114
### Abort the run
132115

133116
```tsx
@@ -265,34 +248,37 @@ remove/re-add churn in the core context store.
265248

266249
Source: `packages/react-core/src/v2/hooks/use-agent-context.tsx:30-35`
267250

268-
### MEDIUM — Expecting `useAgentContext` to accept `agentId`
251+
### MEDIUM — Expecting `useAgentContext` or `copilotkit.addContext` to scope context per agent
269252

270253
Wrong:
271254

272255
```tsx
273256
useAgentContext({ agentId: "research", description: "paper list", value });
257+
// or the imperative form:
258+
copilotkit.addContext({
259+
description: "paper list",
260+
value: JSON.stringify(value),
261+
agentId: "research",
262+
});
274263
```
275264

276265
Correct:
277266

278267
```tsx
279-
const { copilotkit } = useCopilotKit();
280-
useEffect(() => {
281-
const id = copilotkit.addContext({
282-
description: "paper list",
283-
value: JSON.stringify(value),
284-
agentId: "research",
285-
});
286-
return () => copilotkit.removeContext(id);
287-
}, [copilotkit, value]);
268+
// Context is global — every agent run sees every registered entry.
269+
useAgentContext({ description: "paper list", value });
270+
271+
// When only one agent should key off a value, branch inside its prompt
272+
// or tool logic instead of trying to scope the context entry.
288273
```
289274

290-
`useAgentContext` is intentionally global by design — context is "state of
291-
the world" and flows to every agent. Per-agent scoping is the escape hatch
292-
via `copilotkit.addContext({ agentId })`. Most of the time you want the
293-
ambient (global) version.
275+
Context is intentionally global and there is no per-agent scoping hook.
276+
`useAgentContext` has no `agentId` parameter, and `copilotkit.addContext`
277+
destructures only `{ description, value }` — any `agentId` passed is
278+
silently dropped. Treat context as "state of the world" that every agent
279+
sees.
294280

295-
Source: `packages/react-core/src/v2/hooks/use-agent-context.tsx` (no `agentId` parameter)
281+
Source: `packages/react-core/src/v2/hooks/use-agent-context.tsx` (no `agentId` parameter); `packages/core/src/core/context-store.ts:26-31`
296282

297283
### MEDIUM — Two components using the same `(agentId, threadId)` expecting isolation
298284

packages/react-core/skills/attachments/SKILL.md

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,16 @@ export function ChatPanel() {
5656

5757
```tsx
5858
"use client";
59-
import { useAttachments, useAgent } from "@copilotkit/react-core/v2";
59+
import {
60+
useAttachments,
61+
useAgent,
62+
useCopilotKit,
63+
} from "@copilotkit/react-core/v2";
64+
import type { InputContent } from "@ag-ui/core";
6065

6166
export function CustomChatInput() {
6267
const { agent } = useAgent({ agentId: "default" });
68+
const { copilotkit } = useCopilotKit();
6369
const {
6470
attachments,
6571
containerRef,
@@ -86,13 +92,30 @@ export function CustomChatInput() {
8692
</button>
8793
))}
8894
<button
89-
onClick={() => {
95+
onClick={async () => {
9096
const ready = consumeAttachments();
97+
// `ready` is Attachment[] — map each to an AG-UI InputContent part
98+
// before spreading into the message content array.
99+
const contentParts: InputContent[] = [
100+
{ type: "text", text: "See attachments." },
101+
...ready.map(
102+
(att) =>
103+
({
104+
type: att.type,
105+
source: att.source,
106+
metadata: {
107+
...(att.filename ? { filename: att.filename } : {}),
108+
...att.metadata,
109+
},
110+
}) as InputContent,
111+
),
112+
];
91113
agent.addMessage({
92114
id: crypto.randomUUID(),
93115
role: "user",
94-
content: [{ type: "text", text: "See attachments." }, ...ready],
116+
content: contentParts,
95117
});
118+
await copilotkit.runAgent({ agent });
96119
}}
97120
>
98121
Send
@@ -102,6 +125,13 @@ export function CustomChatInput() {
102125
}
103126
```
104127

128+
`consumeAttachments()` returns the `Attachment[]` queue — each entry has
129+
`{ id, type, source, filename, status, metadata }` and is NOT a valid
130+
AG-UI content part. Map each attachment to an `InputContent` shape
131+
(`{ type, source, metadata }`) before spreading into a message's `content`
132+
array. See `packages/react-core/src/v2/components/chat/CopilotChat.tsx:247-268`
133+
for the canonical transform.
134+
105135
## Core Patterns
106136

107137
### Custom upload backend (S3 / presigned URL)

packages/react-core/skills/chat-components/SKILL.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,21 +76,24 @@ import {
7676
CopilotChatInput,
7777
CopilotChatMessageView,
7878
useAgent,
79+
useCopilotKit,
7980
} from "@copilotkit/react-core/v2";
8081

8182
export function HeadlessChat() {
82-
const { agent, isRunning } = useAgent({ agentId: "default" });
83+
const { agent } = useAgent({ agentId: "default" });
84+
const { copilotkit } = useCopilotKit();
8385

8486
return (
8587
<CopilotChatView
8688
messages={agent.messages}
87-
isRunning={isRunning}
88-
onSubmitInput={(text) => {
89+
isRunning={agent.isRunning}
90+
onSubmitInput={async (text) => {
8991
agent.addMessage({
9092
id: crypto.randomUUID(),
9193
role: "user",
9294
content: text,
9395
});
96+
await copilotkit.runAgent({ agent });
9497
}}
9598
>
9699
<CopilotChatMessageView />

packages/react-core/skills/client-side-tools/SKILL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,8 @@ Correct:
167167

168168
```tsx
169169
// First check package.json for shadcn / @mui/* / @chakra-ui/* / antd / @mantine/*, then:
170-
import { Card, CardContent, Skeleton } from "@/components/ui/card";
170+
import { Card, CardContent } from "@/components/ui/card";
171+
import { Skeleton } from "@/components/ui/skeleton";
171172

172173
useFrontendTool({
173174
name: "show",

packages/react-core/skills/custom-message-renderers/SKILL.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,15 @@ const ResearchNotes: ReactCustomMessageRenderer = {
118118
const DebugBefore: ReactCustomMessageRenderer = {
119119
render: ({ message, position, messageIndex, runId }) => {
120120
if (position !== "before" || message.role !== "user") return null;
121+
// `runId` is always a string, but it falls back to a synthetic
122+
// "missing-run-id:<messageId>" value before a run is registered.
123+
// Slice only when it looks like a real id, otherwise show a dash.
124+
const shortId = runId?.startsWith("missing-run-id:")
125+
? ""
126+
: (runId?.slice(0, 6) ?? "");
121127
return (
122128
<div style={{ opacity: 0.5, fontSize: 11 }}>
123-
#{messageIndex} · run {runId.slice(0, 6)}
129+
#{messageIndex} · run {shortId}
124130
</div>
125131
);
126132
},

packages/react-core/skills/human-in-the-loop/SKILL.md

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -119,20 +119,37 @@ render: ({ status, args, respond }) => {
119119
### Abort the run on unmount so threads unlock
120120

121121
```tsx
122-
import { useAgent } from "@copilotkit/react-core/v2";
123-
import { useEffect } from "react";
122+
import { useAgent, UseAgentUpdate } from "@copilotkit/react-core/v2";
123+
import { useEffect, useRef } from "react";
124124

125125
function HITLHost() {
126-
const { agent, isRunning } = useAgent({ agentId: "default" });
126+
const { agent } = useAgent({
127+
agentId: "default",
128+
updates: [UseAgentUpdate.OnRunStatusChanged],
129+
});
130+
// Track isRunning in a ref so the unmount cleanup reads the latest value
131+
// without re-firing on every transition.
132+
const runningRef = useRef(false);
133+
useEffect(() => {
134+
runningRef.current = agent.isRunning;
135+
}, [agent.isRunning]);
136+
127137
useEffect(() => {
128138
return () => {
129-
if (isRunning) agent.abortRun();
139+
if (runningRef.current) agent.abortRun();
130140
};
131-
}, [agent, isRunning]);
141+
}, [agent]);
142+
132143
return <DeleteConfirmHITL />;
133144
}
134145
```
135146

147+
`useAgent` returns `{ agent }` only — run status lives on `agent.isRunning`.
148+
Depending the cleanup effect directly on `agent.isRunning` would fire the
149+
cleanup on every status flip (not just unmount), aborting active runs.
150+
The ref pattern captures the latest value while the cleanup runs only
151+
when the host component truly unmounts.
152+
136153
### Collect structured user input mid-run
137154

138155
```tsx
@@ -276,12 +293,19 @@ Correct:
276293

277294
```tsx
278295
// Keep the HITL prompt at a layout level that persists across route changes, OR abort on unmount:
279-
const { agent, isRunning } = useAgent({ agentId: "default" });
296+
const { agent } = useAgent({
297+
agentId: "default",
298+
updates: [UseAgentUpdate.OnRunStatusChanged],
299+
});
300+
const runningRef = useRef(false);
301+
useEffect(() => {
302+
runningRef.current = agent.isRunning;
303+
}, [agent.isRunning]);
280304
useEffect(
281305
() => () => {
282-
if (isRunning) agent.abortRun();
306+
if (runningRef.current) agent.abortRun();
283307
},
284-
[agent, isRunning],
308+
[agent],
285309
);
286310
```
287311

0 commit comments

Comments
 (0)