@copilotkit/react-core 1.70.3 → 1.71.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{copilotkit-BU3OvveB.cjs → copilotkit-BkVEkUS0.cjs} +369 -154
- package/dist/copilotkit-BkVEkUS0.cjs.map +1 -0
- package/dist/{copilotkit-Bs98akp9.d.mts → copilotkit-ChjzWqn6.d.mts} +33 -8
- package/dist/copilotkit-ChjzWqn6.d.mts.map +1 -0
- package/dist/{copilotkit-Ap_yisA5.mjs → copilotkit-D5BTo0YG.mjs} +368 -153
- package/dist/copilotkit-D5BTo0YG.mjs.map +1 -0
- package/dist/{copilotkit-X2eGbOwf.d.cts → copilotkit-DCIXZawe.d.cts} +33 -8
- package/dist/copilotkit-DCIXZawe.d.cts.map +1 -0
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +4 -4
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +180 -139
- package/dist/index.umd.js.map +1 -1
- package/dist/v2/context.cjs +4 -0
- package/dist/v2/context.cjs.map +1 -1
- package/dist/v2/context.d.cts +3 -1
- package/dist/v2/context.d.cts.map +1 -1
- package/dist/v2/context.d.mts +3 -1
- package/dist/v2/context.d.mts.map +1 -1
- package/dist/v2/context.mjs +3 -1
- package/dist/v2/context.mjs.map +1 -1
- package/dist/v2/headless.cjs +20 -5
- package/dist/v2/headless.cjs.map +1 -1
- package/dist/v2/headless.d.cts +9 -4
- package/dist/v2/headless.d.cts.map +1 -1
- package/dist/v2/headless.d.mts +9 -4
- package/dist/v2/headless.d.mts.map +1 -1
- package/dist/v2/headless.mjs +21 -6
- package/dist/v2/headless.mjs.map +1 -1
- package/dist/v2/index.cjs +1 -1
- package/dist/v2/index.css +1 -1
- package/dist/v2/index.d.cts +1 -1
- package/dist/v2/index.d.mts +1 -1
- package/dist/v2/index.mjs +1 -1
- package/dist/v2/index.umd.js +495 -278
- package/dist/v2/index.umd.js.map +1 -1
- package/package.json +8 -9
- package/dist/copilotkit-Ap_yisA5.mjs.map +0 -1
- package/dist/copilotkit-BU3OvveB.cjs.map +0 -1
- package/dist/copilotkit-Bs98akp9.d.mts.map +0 -1
- package/dist/copilotkit-X2eGbOwf.d.cts.map +0 -1
- package/skills/react-core/SKILL.md +0 -110
- package/skills/react-core/references/agent-access.md +0 -355
- package/skills/react-core/references/attachments.md +0 -311
- package/skills/react-core/references/capabilities.md +0 -138
- package/skills/react-core/references/chat-components.md +0 -229
- package/skills/react-core/references/client-side-tools.md +0 -358
- package/skills/react-core/references/custom-message-renderers.md +0 -223
- package/skills/react-core/references/debug-mode.md +0 -140
- package/skills/react-core/references/human-in-the-loop.md +0 -312
- package/skills/react-core/references/provider-setup.md +0 -358
- package/skills/react-core/references/rendering-activity-messages.md +0 -201
- package/skills/react-core/references/rendering-tool-calls.md +0 -319
- package/skills/react-core/references/suggestions.md +0 -211
- package/skills/react-core/references/switching-agents-recipes.md +0 -161
- package/skills/react-core/references/switching-agents.md +0 -240
- package/skills/react-core/references/threads.md +0 -289
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
# CopilotKit Custom Message Renderers (React)
|
|
2
|
-
|
|
3
|
-
This skill builds on `copilotkit/provider-setup` and
|
|
4
|
-
`copilotkit/chat-components`. `useRenderCustomMessages` is consumed
|
|
5
|
-
internally by `<CopilotChat>` / `<CopilotChatView>`.
|
|
6
|
-
|
|
7
|
-
Key rules:
|
|
8
|
-
|
|
9
|
-
- Renderers are passed to the `CopilotKit` provider via `renderCustomMessages`.
|
|
10
|
-
- The hook returns `null` when called outside `CopilotChatConfigurationProvider`.
|
|
11
|
-
- First non-null result wins — agent-scoped renderers evaluated first.
|
|
12
|
-
- `stateSnapshot` is `undefined` before the run's `runId` resolves.
|
|
13
|
-
|
|
14
|
-
## Setup
|
|
15
|
-
|
|
16
|
-
```tsx
|
|
17
|
-
"use client";
|
|
18
|
-
import { CopilotKit } from "@copilotkit/react-core/v2";
|
|
19
|
-
import type { ReactCustomMessageRenderer } from "@copilotkit/react-core/v2";
|
|
20
|
-
import { useMemo } from "react";
|
|
21
|
-
import { Button } from "@/components/ui/button";
|
|
22
|
-
|
|
23
|
-
const CopyButton: ReactCustomMessageRenderer = {
|
|
24
|
-
render: ({ message, position }) => {
|
|
25
|
-
if (position !== "after") return null;
|
|
26
|
-
if (message.role !== "assistant") return null;
|
|
27
|
-
const content = typeof message.content === "string" ? message.content : "";
|
|
28
|
-
if (!content) return null;
|
|
29
|
-
return (
|
|
30
|
-
<Button
|
|
31
|
-
variant="ghost"
|
|
32
|
-
size="sm"
|
|
33
|
-
onClick={() => navigator.clipboard.writeText(content)}
|
|
34
|
-
>
|
|
35
|
-
Copy
|
|
36
|
-
</Button>
|
|
37
|
-
);
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export function Providers({ children }: { children: React.ReactNode }) {
|
|
42
|
-
const renderers = useMemo(() => [CopyButton], []);
|
|
43
|
-
return (
|
|
44
|
-
<CopilotKit runtimeUrl="/api/copilotkit" renderCustomMessages={renderers}>
|
|
45
|
-
{children}
|
|
46
|
-
</CopilotKit>
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## Core Patterns
|
|
52
|
-
|
|
53
|
-
### State-snapshot viewer after completed runs
|
|
54
|
-
|
|
55
|
-
```tsx
|
|
56
|
-
const StateSnapshotRenderer: ReactCustomMessageRenderer = {
|
|
57
|
-
render: ({ message, position, stateSnapshot }) => {
|
|
58
|
-
if (position !== "after") return null;
|
|
59
|
-
if (message.role !== "assistant") return null;
|
|
60
|
-
if (!stateSnapshot) return null; // run not yet resolved
|
|
61
|
-
return (
|
|
62
|
-
<details>
|
|
63
|
-
<summary>Agent state</summary>
|
|
64
|
-
<pre>{JSON.stringify(stateSnapshot, null, 2)}</pre>
|
|
65
|
-
</details>
|
|
66
|
-
);
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### Agent-scoped renderer
|
|
72
|
-
|
|
73
|
-
```tsx
|
|
74
|
-
const ResearchNotes: ReactCustomMessageRenderer = {
|
|
75
|
-
agentId: "research",
|
|
76
|
-
render: ({ message, position, stateSnapshot }) => {
|
|
77
|
-
if (position !== "after" || !stateSnapshot) return null;
|
|
78
|
-
const notes = (stateSnapshot as { notes?: string[] }).notes ?? [];
|
|
79
|
-
return (
|
|
80
|
-
<ul>
|
|
81
|
-
{notes.map((n, i) => (
|
|
82
|
-
<li key={i}>{n}</li>
|
|
83
|
-
))}
|
|
84
|
-
</ul>
|
|
85
|
-
);
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### Debug panel before user messages
|
|
91
|
-
|
|
92
|
-
```tsx
|
|
93
|
-
const DebugBefore: ReactCustomMessageRenderer = {
|
|
94
|
-
render: ({ message, position, messageIndex, runId }) => {
|
|
95
|
-
if (position !== "before" || message.role !== "user") return null;
|
|
96
|
-
// `runId` is always a string, but it falls back to a synthetic
|
|
97
|
-
// "missing-run-id:<messageId>" value before a run is registered.
|
|
98
|
-
// Slice only when it looks like a real id, otherwise show a dash.
|
|
99
|
-
const shortId = runId?.startsWith("missing-run-id:")
|
|
100
|
-
? "—"
|
|
101
|
-
: (runId?.slice(0, 6) ?? "—");
|
|
102
|
-
return (
|
|
103
|
-
<div style={{ opacity: 0.5, fontSize: 11 }}>
|
|
104
|
-
#{messageIndex} · run {shortId}
|
|
105
|
-
</div>
|
|
106
|
-
);
|
|
107
|
-
},
|
|
108
|
-
};
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
## Common Mistakes
|
|
112
|
-
|
|
113
|
-
### HIGH — Using the hook outside a chat configuration provider
|
|
114
|
-
|
|
115
|
-
Wrong:
|
|
116
|
-
|
|
117
|
-
```tsx
|
|
118
|
-
// Component mounted outside <CopilotChat>/<CopilotChatView>
|
|
119
|
-
function StandaloneRenderer() {
|
|
120
|
-
const render = useRenderCustomMessages(); // returns null — no chat config in tree
|
|
121
|
-
return render ? render({ message, position: "after" }) : null;
|
|
122
|
-
}
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
Correct:
|
|
126
|
-
|
|
127
|
-
```tsx
|
|
128
|
-
// Option A — register renderers via the provider prop so <CopilotChat> picks them up:
|
|
129
|
-
<CopilotKit renderCustomMessages={renderers}>
|
|
130
|
-
<CopilotChat agentId="default" />
|
|
131
|
-
</CopilotKit>;
|
|
132
|
-
|
|
133
|
-
// Option B — call the hook only inside a chat-configured subtree:
|
|
134
|
-
import { CopilotChatConfigurationProvider } from "@copilotkit/react-core/v2";
|
|
135
|
-
<CopilotChatConfigurationProvider agentId="default">
|
|
136
|
-
<ComponentThatCallsUseRenderCustomMessages />
|
|
137
|
-
</CopilotChatConfigurationProvider>;
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
`useRenderCustomMessages` returns `null` when there is no
|
|
141
|
-
`CopilotChatConfigurationProvider` in the tree. `<CopilotChat>` wraps its
|
|
142
|
-
children in one automatically; direct use outside a chat component
|
|
143
|
-
requires the explicit wrapper.
|
|
144
|
-
|
|
145
|
-
Source: `packages/react-core/src/v2/hooks/use-render-custom-messages.tsx:15-17`
|
|
146
|
-
|
|
147
|
-
### MEDIUM — Relying on `stateSnapshot` during early streaming
|
|
148
|
-
|
|
149
|
-
Wrong:
|
|
150
|
-
|
|
151
|
-
```tsx
|
|
152
|
-
render: ({ stateSnapshot }) => <pre>{JSON.stringify(stateSnapshot.items)}</pre>;
|
|
153
|
-
// Crashes during the first token — stateSnapshot is undefined before runId resolves.
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
Correct:
|
|
157
|
-
|
|
158
|
-
```tsx
|
|
159
|
-
render: ({ stateSnapshot }) => (
|
|
160
|
-
<pre>{stateSnapshot ? JSON.stringify(stateSnapshot.items) : "…"}</pre>
|
|
161
|
-
);
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
`stateSnapshot` comes from `copilotkit.getStateByRun(agentId, threadId,
|
|
165
|
-
runId)`. `runId` is `undefined` until the run is registered, so the
|
|
166
|
-
snapshot starts `undefined` and only becomes truthy after the first
|
|
167
|
-
state emit. Guard with a fallback.
|
|
168
|
-
|
|
169
|
-
Source: `packages/react-core/src/v2/hooks/use-render-custom-messages.tsx:69-71`
|
|
170
|
-
|
|
171
|
-
### MEDIUM — Expecting every renderer in the array to run
|
|
172
|
-
|
|
173
|
-
Wrong:
|
|
174
|
-
|
|
175
|
-
```tsx
|
|
176
|
-
// Both renderers want to add an "after assistant" button and return <div>…</div>
|
|
177
|
-
// Only the first one (or the agent-scoped one) fires — the second is skipped.
|
|
178
|
-
const renderers = [Renderer1, Renderer2];
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
Correct:
|
|
182
|
-
|
|
183
|
-
```tsx
|
|
184
|
-
// Merge the two into a single renderer that returns one element:
|
|
185
|
-
const Combined: ReactCustomMessageRenderer = {
|
|
186
|
-
render: (props) => (
|
|
187
|
-
<div className="flex gap-1">
|
|
188
|
-
<Renderer1Inner {...props} />
|
|
189
|
-
<Renderer2Inner {...props} />
|
|
190
|
-
</div>
|
|
191
|
-
),
|
|
192
|
-
};
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
The hook iterates the sorted renderer list and breaks at the first non-null
|
|
196
|
-
result. Two independent renderers returning JSX for the same
|
|
197
|
-
`(message, position)` pair will have only one fire. Compose them into a
|
|
198
|
-
single renderer if you want both to appear.
|
|
199
|
-
|
|
200
|
-
Source: `packages/react-core/src/v2/hooks/use-render-custom-messages.tsx:73-95`
|
|
201
|
-
|
|
202
|
-
### MEDIUM — Memoization miss on `renderCustomMessages` array
|
|
203
|
-
|
|
204
|
-
Wrong:
|
|
205
|
-
|
|
206
|
-
```tsx
|
|
207
|
-
<CopilotKit
|
|
208
|
-
renderCustomMessages={[CopyButton, DebugBefore]} // fresh array every render
|
|
209
|
-
/>
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
Correct:
|
|
213
|
-
|
|
214
|
-
```tsx
|
|
215
|
-
const renderers = useMemo(() => [CopyButton, DebugBefore], []);
|
|
216
|
-
<CopilotKit renderCustomMessages={renderers} />;
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
The provider's stable-array-prop diff console-errors when a new array
|
|
220
|
-
identity appears every render and thrashes renderer registration.
|
|
221
|
-
Memoize or hoist.
|
|
222
|
-
|
|
223
|
-
Source: `packages/react-core/src/v2/providers/CopilotKitProvider.tsx` (useStableArrayProp)
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
# CopilotKit Debug Mode (React)
|
|
2
|
-
|
|
3
|
-
This skill builds on `copilotkit/provider-setup`. Both debug surfaces are
|
|
4
|
-
props on the `CopilotKit` provider (from `@copilotkit/react-core/v2`).
|
|
5
|
-
|
|
6
|
-
Two independent knobs:
|
|
7
|
-
|
|
8
|
-
1. `enableInspector` disables the development-only visual Inspector when set
|
|
9
|
-
to `false`.
|
|
10
|
-
2. `debug` controls console logging for the event pipeline.
|
|
11
|
-
|
|
12
|
-
The Inspector is always off in production. Configure `debug` separately for
|
|
13
|
-
the logging behavior you need.
|
|
14
|
-
|
|
15
|
-
## Setup
|
|
16
|
-
|
|
17
|
-
```tsx
|
|
18
|
-
"use client";
|
|
19
|
-
import { CopilotKit } from "@copilotkit/react-core/v2";
|
|
20
|
-
|
|
21
|
-
export function Providers({ children }: { children: React.ReactNode }) {
|
|
22
|
-
return (
|
|
23
|
-
<CopilotKit
|
|
24
|
-
runtimeUrl="/api/copilotkit"
|
|
25
|
-
debug={{ events: true, lifecycle: true, verbose: false }}
|
|
26
|
-
>
|
|
27
|
-
{children}
|
|
28
|
-
</CopilotKit>
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
The Inspector is enabled automatically in development browser builds on any
|
|
34
|
-
host. Production builds never load it.
|
|
35
|
-
|
|
36
|
-
## Core Patterns
|
|
37
|
-
|
|
38
|
-
### Full payload logging during a repro
|
|
39
|
-
|
|
40
|
-
`debug: true` enables `events + lifecycle` but keeps `verbose` off to avoid
|
|
41
|
-
leaking PII by default. For a bug repro, explicitly set `verbose: true` to
|
|
42
|
-
dump full message/tool-call payloads.
|
|
43
|
-
|
|
44
|
-
```tsx
|
|
45
|
-
<CopilotKit
|
|
46
|
-
runtimeUrl="/api/copilotkit"
|
|
47
|
-
debug={{ events: true, lifecycle: true, verbose: true }}
|
|
48
|
-
/>
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
### Disable the Inspector in development
|
|
52
|
-
|
|
53
|
-
```tsx
|
|
54
|
-
<CopilotKit runtimeUrl="/api/copilotkit" enableInspector={false} />
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Use this when you want no Inspector FAB in local development. Production
|
|
58
|
-
builds never load the Inspector.
|
|
59
|
-
|
|
60
|
-
## Common Mistakes
|
|
61
|
-
|
|
62
|
-
### HIGH — Using `showDevConsole` to control the Inspector
|
|
63
|
-
|
|
64
|
-
Wrong:
|
|
65
|
-
|
|
66
|
-
```tsx
|
|
67
|
-
<CopilotKit runtimeUrl="/api/copilotkit" showDevConsole="auto" />
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
Correct:
|
|
71
|
-
|
|
72
|
-
```tsx
|
|
73
|
-
<CopilotKit runtimeUrl="/api/copilotkit" />
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
`showDevConsole` no longer controls Inspector visibility. Omit it. The
|
|
77
|
-
Inspector is on in development and off in production.
|
|
78
|
-
|
|
79
|
-
Source: `packages/react-core/src/v2/providers/CopilotKitProvider.tsx:301-321`
|
|
80
|
-
|
|
81
|
-
### MEDIUM — Expecting `debug: true` to log full payloads
|
|
82
|
-
|
|
83
|
-
Wrong:
|
|
84
|
-
|
|
85
|
-
```tsx
|
|
86
|
-
<CopilotKit debug={true} />
|
|
87
|
-
// Then wondering why message contents aren't in the console
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
Correct:
|
|
91
|
-
|
|
92
|
-
```tsx
|
|
93
|
-
<CopilotKit debug={{ events: true, lifecycle: true, verbose: true }} />
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
`debug: true` is shorthand for `{ events: true, lifecycle: true, verbose: false }`.
|
|
97
|
-
`verbose` defaults to `false` to avoid logging user message bodies / tool
|
|
98
|
-
arguments / state snapshots — it must be opted into explicitly.
|
|
99
|
-
|
|
100
|
-
Source: `docs/snippets/shared/troubleshooting/debug-mode.mdx:85-93`
|
|
101
|
-
|
|
102
|
-
### MEDIUM — Passing fields that aren't in `DebugConfig`
|
|
103
|
-
|
|
104
|
-
Wrong:
|
|
105
|
-
|
|
106
|
-
```tsx
|
|
107
|
-
<CopilotKit debug={{ events: true, network: true, errors: true }} />
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
Correct:
|
|
111
|
-
|
|
112
|
-
```tsx
|
|
113
|
-
<CopilotKit debug={{ events: true, lifecycle: true, verbose: true }} />
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
`DebugConfig` has exactly three fields: `events`, `lifecycle`, `verbose`.
|
|
117
|
-
Anything else is silently ignored by the type-narrowing at the provider.
|
|
118
|
-
|
|
119
|
-
Source: `packages/react-core/src/v2/providers/CopilotKitProvider.tsx` (DebugConfig type)
|
|
120
|
-
|
|
121
|
-
### MEDIUM — Inspector crashing in sandboxed iframes
|
|
122
|
-
|
|
123
|
-
Wrong:
|
|
124
|
-
|
|
125
|
-
```tsx
|
|
126
|
-
// App embedded in a sandboxed iframe with the development Inspector enabled
|
|
127
|
-
<CopilotKit runtimeUrl="..." />
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
Correct:
|
|
131
|
-
|
|
132
|
-
```tsx
|
|
133
|
-
<CopilotKit runtimeUrl="..." enableInspector={false} />
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
The inspector persists its anchor via `localStorage`. In sandboxed iframes
|
|
137
|
-
without storage access, `loadInspectorState` throws on mount. Disable it for
|
|
138
|
-
an iframe deployment or whitelist storage in the sandbox attrs.
|
|
139
|
-
|
|
140
|
-
Source: `packages/web-inspector/src/lib/persistence.ts` (`loadInspectorState`)
|
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
# CopilotKit Human-in-the-Loop (React)
|
|
2
|
-
|
|
3
|
-
This skill builds on `copilotkit/provider-setup`, `copilotkit/client-side-tools`,
|
|
4
|
-
and `copilotkit/rendering-tool-calls`.
|
|
5
|
-
|
|
6
|
-
`useHumanInTheLoop` is `useFrontendTool` minus the `handler` plus a
|
|
7
|
-
`render` that receives a `respond` function. The hook synthesizes a
|
|
8
|
-
Promise-based handler — the Promise resolves when `respond(result)` is
|
|
9
|
-
called. No `respond` call → infinite hang.
|
|
10
|
-
|
|
11
|
-
Status is camelCase: `"inProgress" | "executing" | "complete"`. `respond`
|
|
12
|
-
is `undefined` except during `"executing"`.
|
|
13
|
-
|
|
14
|
-
## UI-kit detection rule
|
|
15
|
-
|
|
16
|
-
Before writing the approval UI, check the consumer's `package.json` for a
|
|
17
|
-
UI kit (shadcn `AlertDialog`, MUI `Dialog`, Chakra `Modal`, Ant `Modal`,
|
|
18
|
-
Mantine `Modal`) and reuse it. Don't hand-roll an overlay.
|
|
19
|
-
|
|
20
|
-
## Setup
|
|
21
|
-
|
|
22
|
-
```tsx
|
|
23
|
-
"use client";
|
|
24
|
-
import { useHumanInTheLoop } from "@copilotkit/react-core/v2";
|
|
25
|
-
import { z } from "zod";
|
|
26
|
-
import {
|
|
27
|
-
AlertDialog,
|
|
28
|
-
AlertDialogAction,
|
|
29
|
-
AlertDialogCancel,
|
|
30
|
-
AlertDialogContent,
|
|
31
|
-
AlertDialogDescription,
|
|
32
|
-
AlertDialogFooter,
|
|
33
|
-
AlertDialogHeader,
|
|
34
|
-
AlertDialogTitle,
|
|
35
|
-
} from "@/components/ui/alert-dialog";
|
|
36
|
-
|
|
37
|
-
export function DeleteConfirmHITL() {
|
|
38
|
-
useHumanInTheLoop({
|
|
39
|
-
name: "confirmDelete",
|
|
40
|
-
description: "Confirm a destructive delete with the user",
|
|
41
|
-
parameters: z.object({ id: z.string(), label: z.string() }),
|
|
42
|
-
render: ({ status, args, respond }) => (
|
|
43
|
-
<AlertDialog open>
|
|
44
|
-
<AlertDialogContent>
|
|
45
|
-
<AlertDialogHeader>
|
|
46
|
-
<AlertDialogTitle>Delete {args.label}?</AlertDialogTitle>
|
|
47
|
-
<AlertDialogDescription>
|
|
48
|
-
This action cannot be undone.
|
|
49
|
-
</AlertDialogDescription>
|
|
50
|
-
</AlertDialogHeader>
|
|
51
|
-
<AlertDialogFooter>
|
|
52
|
-
<AlertDialogCancel
|
|
53
|
-
disabled={status !== "executing"}
|
|
54
|
-
onClick={() => respond?.("denied")}
|
|
55
|
-
>
|
|
56
|
-
Cancel
|
|
57
|
-
</AlertDialogCancel>
|
|
58
|
-
<AlertDialogAction
|
|
59
|
-
disabled={status !== "executing"}
|
|
60
|
-
onClick={() => respond?.("approved")}
|
|
61
|
-
>
|
|
62
|
-
Delete
|
|
63
|
-
</AlertDialogAction>
|
|
64
|
-
</AlertDialogFooter>
|
|
65
|
-
</AlertDialogContent>
|
|
66
|
-
</AlertDialog>
|
|
67
|
-
),
|
|
68
|
-
});
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
## Core Patterns
|
|
74
|
-
|
|
75
|
-
### Always call `respond` in every branch
|
|
76
|
-
|
|
77
|
-
```tsx
|
|
78
|
-
render: ({ status, args, respond }) => {
|
|
79
|
-
if (status !== "executing" || !respond) {
|
|
80
|
-
return <div>Awaiting decision…</div>;
|
|
81
|
-
}
|
|
82
|
-
return (
|
|
83
|
-
<div>
|
|
84
|
-
<button onClick={() => respond("approved")}>Approve</button>
|
|
85
|
-
<button onClick={() => respond("denied")}>Reject</button>
|
|
86
|
-
<button onClick={() => respond({ action: "skip", reason: "timeout" })}>
|
|
87
|
-
Skip
|
|
88
|
-
</button>
|
|
89
|
-
</div>
|
|
90
|
-
);
|
|
91
|
-
};
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
### Abort the run on unmount so threads unlock
|
|
95
|
-
|
|
96
|
-
```tsx
|
|
97
|
-
import { useAgent, UseAgentUpdate } from "@copilotkit/react-core/v2";
|
|
98
|
-
import { useEffect, useRef } from "react";
|
|
99
|
-
|
|
100
|
-
function HITLHost() {
|
|
101
|
-
const { agent } = useAgent({
|
|
102
|
-
agentId: "default",
|
|
103
|
-
updates: [UseAgentUpdate.OnRunStatusChanged],
|
|
104
|
-
});
|
|
105
|
-
// Track isRunning in a ref so the unmount cleanup reads the latest value
|
|
106
|
-
// without re-firing on every transition.
|
|
107
|
-
const runningRef = useRef(false);
|
|
108
|
-
useEffect(() => {
|
|
109
|
-
runningRef.current = agent.isRunning;
|
|
110
|
-
}, [agent.isRunning]);
|
|
111
|
-
|
|
112
|
-
useEffect(() => {
|
|
113
|
-
return () => {
|
|
114
|
-
if (runningRef.current) agent.abortRun();
|
|
115
|
-
};
|
|
116
|
-
}, [agent]);
|
|
117
|
-
|
|
118
|
-
return <DeleteConfirmHITL />;
|
|
119
|
-
}
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
`useAgent` returns `{ agent }` only — run status lives on `agent.isRunning`.
|
|
123
|
-
Depending the cleanup effect directly on `agent.isRunning` would fire the
|
|
124
|
-
cleanup on every status flip (not just unmount), aborting active runs.
|
|
125
|
-
The ref pattern captures the latest value while the cleanup runs only
|
|
126
|
-
when the host component truly unmounts.
|
|
127
|
-
|
|
128
|
-
### Collect structured user input mid-run
|
|
129
|
-
|
|
130
|
-
```tsx
|
|
131
|
-
useHumanInTheLoop({
|
|
132
|
-
name: "askUserForPriority",
|
|
133
|
-
parameters: z.object({ taskId: z.string() }),
|
|
134
|
-
render: ({ status, args, respond }) => {
|
|
135
|
-
if (status !== "executing" || !respond) return <div>Waiting…</div>;
|
|
136
|
-
return (
|
|
137
|
-
<div>
|
|
138
|
-
{["low", "medium", "high"].map((p) => (
|
|
139
|
-
<button
|
|
140
|
-
key={p}
|
|
141
|
-
onClick={() => respond({ taskId: args.taskId, priority: p })}
|
|
142
|
-
>
|
|
143
|
-
{p}
|
|
144
|
-
</button>
|
|
145
|
-
))}
|
|
146
|
-
</div>
|
|
147
|
-
);
|
|
148
|
-
},
|
|
149
|
-
});
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
## Common Mistakes
|
|
153
|
-
|
|
154
|
-
### CRITICAL — Never calling `respond()`
|
|
155
|
-
|
|
156
|
-
Wrong:
|
|
157
|
-
|
|
158
|
-
```tsx
|
|
159
|
-
useHumanInTheLoop({
|
|
160
|
-
name: "confirmDelete",
|
|
161
|
-
parameters: z.object({ id: z.string() }),
|
|
162
|
-
render: ({ args, status, respond }) => (
|
|
163
|
-
<div>
|
|
164
|
-
<p>Delete {args.id}?</p>
|
|
165
|
-
<button>OK</button>
|
|
166
|
-
</div>
|
|
167
|
-
),
|
|
168
|
-
});
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
Correct:
|
|
172
|
-
|
|
173
|
-
```tsx
|
|
174
|
-
useHumanInTheLoop({
|
|
175
|
-
name: "confirmDelete",
|
|
176
|
-
parameters: z.object({ id: z.string() }),
|
|
177
|
-
render: ({ args, status, respond }) => (
|
|
178
|
-
<div>
|
|
179
|
-
<p>Delete {args.id}?</p>
|
|
180
|
-
<button onClick={() => respond?.("approved")}>OK</button>
|
|
181
|
-
<button onClick={() => respond?.("denied")}>Cancel</button>
|
|
182
|
-
</div>
|
|
183
|
-
),
|
|
184
|
-
});
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
The synthesized handler returns a Promise that resolves only when `respond`
|
|
188
|
-
is called. Never calling it (including reject / cancel paths) hangs the
|
|
189
|
-
run indefinitely and leaves the thread locked on the server.
|
|
190
|
-
|
|
191
|
-
Source: `packages/react-core/src/v2/hooks/use-human-in-the-loop.tsx:13-26`
|
|
192
|
-
|
|
193
|
-
### CRITICAL — Writing a custom overlay when the app has a Dialog primitive
|
|
194
|
-
|
|
195
|
-
Wrong:
|
|
196
|
-
|
|
197
|
-
```tsx
|
|
198
|
-
render: ({ respond }) => (
|
|
199
|
-
<div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.5)" }}>
|
|
200
|
-
…
|
|
201
|
-
</div>
|
|
202
|
-
);
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
Correct:
|
|
206
|
-
|
|
207
|
-
```tsx
|
|
208
|
-
import {
|
|
209
|
-
AlertDialog,
|
|
210
|
-
AlertDialogContent,
|
|
211
|
-
AlertDialogAction,
|
|
212
|
-
} from "@/components/ui/alert-dialog";
|
|
213
|
-
|
|
214
|
-
render: ({ respond }) => (
|
|
215
|
-
<AlertDialog open>
|
|
216
|
-
<AlertDialogContent>
|
|
217
|
-
…
|
|
218
|
-
<AlertDialogAction onClick={() => respond?.("approved")}>
|
|
219
|
-
OK
|
|
220
|
-
</AlertDialogAction>
|
|
221
|
-
</AlertDialogContent>
|
|
222
|
-
</AlertDialog>
|
|
223
|
-
);
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
Check `package.json` for shadcn / MUI / Chakra / Ant / Mantine before
|
|
227
|
-
writing an overlay. Their dialog primitives handle focus trapping,
|
|
228
|
-
escape-to-close, and accessibility — raw JSX skips all of that.
|
|
229
|
-
|
|
230
|
-
Source: maintainer interview (Phase 2c)
|
|
231
|
-
|
|
232
|
-
### HIGH — Calling `respond` during `inProgress` or `complete`
|
|
233
|
-
|
|
234
|
-
Wrong:
|
|
235
|
-
|
|
236
|
-
```tsx
|
|
237
|
-
render: ({ status, respond }) => (
|
|
238
|
-
<button onClick={() => (respond as any)("yes")}>Yes</button>
|
|
239
|
-
);
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
Correct:
|
|
243
|
-
|
|
244
|
-
```tsx
|
|
245
|
-
render: ({ status, respond }) =>
|
|
246
|
-
status === "executing" && respond ? (
|
|
247
|
-
<button onClick={() => respond("yes")}>Yes</button>
|
|
248
|
-
) : (
|
|
249
|
-
<p>Waiting…</p>
|
|
250
|
-
);
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
`respond` is `undefined` outside `status === "executing"`. Widening it to
|
|
254
|
-
`any` silently no-ops — the button click appears to work, but nothing
|
|
255
|
-
resolves the Promise.
|
|
256
|
-
|
|
257
|
-
Source: `packages/react-core/src/v2/types/human-in-the-loop.ts:8-32`
|
|
258
|
-
|
|
259
|
-
### HIGH — Unmounting the render mid-executing
|
|
260
|
-
|
|
261
|
-
Wrong:
|
|
262
|
-
|
|
263
|
-
```tsx
|
|
264
|
-
// User clicks away to a different route while the agent is waiting on respond()
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
Correct:
|
|
268
|
-
|
|
269
|
-
```tsx
|
|
270
|
-
// Keep the HITL prompt at a layout level that persists across route changes, OR abort on unmount:
|
|
271
|
-
const { agent } = useAgent({
|
|
272
|
-
agentId: "default",
|
|
273
|
-
updates: [UseAgentUpdate.OnRunStatusChanged],
|
|
274
|
-
});
|
|
275
|
-
const runningRef = useRef(false);
|
|
276
|
-
useEffect(() => {
|
|
277
|
-
runningRef.current = agent.isRunning;
|
|
278
|
-
}, [agent.isRunning]);
|
|
279
|
-
useEffect(
|
|
280
|
-
() => () => {
|
|
281
|
-
if (runningRef.current) agent.abortRun();
|
|
282
|
-
},
|
|
283
|
-
[agent],
|
|
284
|
-
);
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
`useHumanInTheLoop` removes its renderer on unmount (unlike
|
|
288
|
-
`useFrontendTool`, which keeps renderers for history). If the renderer
|
|
289
|
-
unmounts mid-`executing`, the pending Promise is abandoned and the run
|
|
290
|
-
hangs. Either lift the HITL UI to a layout-level component, or abort the
|
|
291
|
-
run on unmount.
|
|
292
|
-
|
|
293
|
-
Source: `packages/react-core/src/v2/hooks/use-human-in-the-loop.tsx:76-80`
|
|
294
|
-
|
|
295
|
-
### MEDIUM — Using hyphenated `"in-progress"` status
|
|
296
|
-
|
|
297
|
-
Wrong:
|
|
298
|
-
|
|
299
|
-
```tsx
|
|
300
|
-
render: ({ status }) => (status === "in-progress" ? <Spinner /> : <Form />);
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
Correct:
|
|
304
|
-
|
|
305
|
-
```tsx
|
|
306
|
-
render: ({ status }) => (status === "inProgress" ? <Spinner /> : <Form />);
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
Same camelCase rule as `rendering-tool-calls`: the discriminated union
|
|
310
|
-
only matches `"inProgress" | "executing" | "complete"`.
|
|
311
|
-
|
|
312
|
-
Source: `packages/react-core/src/v2/types/human-in-the-loop.ts:8-32`
|