Skip to content

Commit bb6f02c

Browse files
mxmzbclaude
andcommitted
fix(core,react-core): handle onRunErrorEvent in useAgent and document in subscribeToAgentWithOptions
React's useAgent was missing onRunErrorEvent under OnRunStatusChanged, causing isRunning to stay true after protocol-level RUN_ERROR events (infinite spinner). Angular already handled this via PR CopilotKit#3749. Also updates SubscribeToAgentSubscriber JSDoc to accurately document onRunErrorEvent as the sixth allowed callback with rationale for its inclusion despite having stopPropagation in its return type, and adds a core-level test for onRunErrorEvent firing immediately during throttle windows. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 96f230c commit bb6f02c

3 files changed

Lines changed: 51 additions & 9 deletions

File tree

packages/core/src/__tests__/core-subscribe-to-agent.test.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@ function notifyStateChanged(agent: TestAgent) {
6060

6161
function notifyLifecycle(
6262
agent: TestAgent,
63-
event: "onRunInitialized" | "onRunFinalized" | "onRunFailed",
63+
event:
64+
| "onRunInitialized"
65+
| "onRunFinalized"
66+
| "onRunFailed"
67+
| "onRunErrorEvent",
6468
) {
6569
const base: AgentSubscriberParams = {
6670
messages: agent.messages,
@@ -71,6 +75,12 @@ function notifyLifecycle(
7175
if (event === "onRunFailed") {
7276
const params = { ...base, error: new Error("run failed") };
7377
agent.subscribers.forEach((s) => s.onRunFailed?.(params));
78+
} else if (event === "onRunErrorEvent") {
79+
const params = {
80+
...base,
81+
event: { type: "RUN_ERROR", message: "backend error" } as any,
82+
};
83+
agent.subscribers.forEach((s) => s.onRunErrorEvent?.(params));
7484
} else {
7585
agent.subscribers.forEach((s) => s[event]?.(base));
7686
}
@@ -397,6 +407,25 @@ describe("CopilotKitCore.subscribeToAgentWithOptions", () => {
397407
expect(onRunFailed).toHaveBeenCalledTimes(1);
398408
});
399409

410+
it("onRunErrorEvent fires immediately during throttle window", () => {
411+
const onMessages = vi.fn();
412+
const onRunErrorEvent = vi.fn();
413+
414+
core.subscribeToAgentWithOptions(
415+
agent,
416+
{ onMessagesChanged: onMessages, onRunErrorEvent },
417+
{ throttleMs: 100 },
418+
);
419+
420+
// Start throttle window
421+
agent.messages = [userMsg("1", "a")];
422+
notifyMessagesChanged(agent);
423+
424+
// Protocol-level RUN_ERROR — should fire immediately (not throttled)
425+
notifyLifecycle(agent, "onRunErrorEvent");
426+
expect(onRunErrorEvent).toHaveBeenCalledTimes(1);
427+
});
428+
400429
// -------------------------------------------------------------------------
401430
// Only subscribed callbacks are wrapped
402431
// -------------------------------------------------------------------------

packages/core/src/core/core.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -203,10 +203,11 @@ const ALLOWED_KEYS: ReadonlySet<(typeof SUBSCRIBE_TO_AGENT_KEYS)[number]> =
203203

204204
/**
205205
* The subset of `AgentSubscriber` callbacks accepted by
206-
* {@link CopilotKitCore.subscribeToAgentWithOptions}. Only the five callbacks listed
207-
* in {@link SUBSCRIBE_TO_AGENT_KEYS} are supported: `onMessagesChanged`,
208-
* `onStateChanged`, and the three run lifecycle callbacks
209-
* (`onRunInitialized`, `onRunFinalized`, `onRunFailed`).
206+
* {@link CopilotKitCore.subscribeToAgentWithOptions}. Only the callbacks
207+
* listed in {@link SUBSCRIBE_TO_AGENT_KEYS} are supported:
208+
* `onMessagesChanged`, `onStateChanged`, and the four run lifecycle
209+
* callbacks (`onRunInitialized`, `onRunFinalized`, `onRunFailed`,
210+
* `onRunErrorEvent`).
210211
*
211212
* Two categories of `AgentSubscriber` members are excluded:
212213
*
@@ -220,9 +221,18 @@ const ALLOWED_KEYS: ReadonlySet<(typeof SUBSCRIBE_TO_AGENT_KEYS)[number]> =
220221
* same data at a coarser granularity, and throttling per-item callbacks
221222
* would have different semantic expectations.
222223
*
223-
* Note: the included lifecycle callbacks also return
224-
* `Omit<AgentStateMutation, "stopPropagation">`. On the error path,
225-
* `safeCall` discards those return values (see its inline documentation).
224+
* `onRunErrorEvent` is technically an AG-UI event handler (its return type
225+
* includes `stopPropagation`), but it is included here because all
226+
* framework consumers need it to reset `isRunning` on protocol-level
227+
* `RUN_ERROR` events — distinct from `onRunFailed` which handles local
228+
* exceptions like network errors. In practice, consumers return `void`
229+
* from this callback, so the `stopPropagation` semantics are unused.
230+
*
231+
* Note: the included lifecycle callbacks return
232+
* `Omit<AgentStateMutation, "stopPropagation">` (or full
233+
* `AgentStateMutation` in the case of `onRunErrorEvent`). On the error
234+
* path, `safeCall` discards those return values (see its inline
235+
* documentation).
226236
*
227237
* Use `agent.subscribe()` directly when event mutation or per-item
228238
* notification semantics are needed.

packages/react-core/src/v2/hooks/use-agent.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export interface UseAgentProps {
4141
* provider specifies a non-zero `defaultThrottleMs`.
4242
*
4343
* Run lifecycle callbacks (`onRunInitialized`, `onRunFinalized`,
44-
* `onRunFailed`) always fire immediately.
44+
* `onRunFailed`, `onRunErrorEvent`) always fire immediately.
4545
*
4646
* @default undefined
4747
* When unset, inherits from the provider's `defaultThrottleMs`;
@@ -301,6 +301,9 @@ export function useAgent({
301301
handlers.onRunInitialized = batchedForceUpdate;
302302
handlers.onRunFinalized = batchedForceUpdate;
303303
handlers.onRunFailed = batchedForceUpdate;
304+
// Protocol-level RUN_ERROR event (distinct from onRunFailed which
305+
// handles local exceptions like network errors).
306+
handlers.onRunErrorEvent = batchedForceUpdate;
304307
}
305308

306309
const subscription = copilotkit.subscribeToAgentWithOptions(

0 commit comments

Comments
 (0)