@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.
Files changed (60) hide show
  1. package/dist/{copilotkit-BU3OvveB.cjs → copilotkit-BkVEkUS0.cjs} +369 -154
  2. package/dist/copilotkit-BkVEkUS0.cjs.map +1 -0
  3. package/dist/{copilotkit-Bs98akp9.d.mts → copilotkit-ChjzWqn6.d.mts} +33 -8
  4. package/dist/copilotkit-ChjzWqn6.d.mts.map +1 -0
  5. package/dist/{copilotkit-Ap_yisA5.mjs → copilotkit-D5BTo0YG.mjs} +368 -153
  6. package/dist/copilotkit-D5BTo0YG.mjs.map +1 -0
  7. package/dist/{copilotkit-X2eGbOwf.d.cts → copilotkit-DCIXZawe.d.cts} +33 -8
  8. package/dist/copilotkit-DCIXZawe.d.cts.map +1 -0
  9. package/dist/index.cjs +3 -3
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +1 -1
  12. package/dist/index.d.mts +1 -1
  13. package/dist/index.mjs +4 -4
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/index.umd.js +180 -139
  16. package/dist/index.umd.js.map +1 -1
  17. package/dist/v2/context.cjs +4 -0
  18. package/dist/v2/context.cjs.map +1 -1
  19. package/dist/v2/context.d.cts +3 -1
  20. package/dist/v2/context.d.cts.map +1 -1
  21. package/dist/v2/context.d.mts +3 -1
  22. package/dist/v2/context.d.mts.map +1 -1
  23. package/dist/v2/context.mjs +3 -1
  24. package/dist/v2/context.mjs.map +1 -1
  25. package/dist/v2/headless.cjs +20 -5
  26. package/dist/v2/headless.cjs.map +1 -1
  27. package/dist/v2/headless.d.cts +9 -4
  28. package/dist/v2/headless.d.cts.map +1 -1
  29. package/dist/v2/headless.d.mts +9 -4
  30. package/dist/v2/headless.d.mts.map +1 -1
  31. package/dist/v2/headless.mjs +21 -6
  32. package/dist/v2/headless.mjs.map +1 -1
  33. package/dist/v2/index.cjs +1 -1
  34. package/dist/v2/index.css +1 -1
  35. package/dist/v2/index.d.cts +1 -1
  36. package/dist/v2/index.d.mts +1 -1
  37. package/dist/v2/index.mjs +1 -1
  38. package/dist/v2/index.umd.js +495 -278
  39. package/dist/v2/index.umd.js.map +1 -1
  40. package/package.json +8 -9
  41. package/dist/copilotkit-Ap_yisA5.mjs.map +0 -1
  42. package/dist/copilotkit-BU3OvveB.cjs.map +0 -1
  43. package/dist/copilotkit-Bs98akp9.d.mts.map +0 -1
  44. package/dist/copilotkit-X2eGbOwf.d.cts.map +0 -1
  45. package/skills/react-core/SKILL.md +0 -110
  46. package/skills/react-core/references/agent-access.md +0 -355
  47. package/skills/react-core/references/attachments.md +0 -311
  48. package/skills/react-core/references/capabilities.md +0 -138
  49. package/skills/react-core/references/chat-components.md +0 -229
  50. package/skills/react-core/references/client-side-tools.md +0 -358
  51. package/skills/react-core/references/custom-message-renderers.md +0 -223
  52. package/skills/react-core/references/debug-mode.md +0 -140
  53. package/skills/react-core/references/human-in-the-loop.md +0 -312
  54. package/skills/react-core/references/provider-setup.md +0 -358
  55. package/skills/react-core/references/rendering-activity-messages.md +0 -201
  56. package/skills/react-core/references/rendering-tool-calls.md +0 -319
  57. package/skills/react-core/references/suggestions.md +0 -211
  58. package/skills/react-core/references/switching-agents-recipes.md +0 -161
  59. package/skills/react-core/references/switching-agents.md +0 -240
  60. 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`