@assistant-ui/react-ag-ui 0.0.62 → 0.0.63
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/LICENSE +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +0 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +1 -0
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +25 -0
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/adapter/subscriber.d.ts +9 -0
- package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
- package/dist/runtime/adapter/subscriber.js +3 -0
- package/dist/runtime/adapter/subscriber.js.map +1 -1
- package/dist/runtime/event-parser.d.ts.map +1 -1
- package/dist/runtime/event-parser.js +11 -0
- package/dist/runtime/event-parser.js.map +1 -1
- package/dist/runtime/types.d.ts +6 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/useAgUiRuntime.d.ts.map +1 -1
- package/dist/useAgUiRuntime.js +17 -3
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +8 -5
- package/src/runtime/AgUiThreadRuntimeCore.test.ts +43 -0
- package/src/runtime/AgUiThreadRuntimeCore.ts +0 -2
- package/src/runtime/adapter/run-aggregator.activity.test.ts +128 -0
- package/src/runtime/adapter/run-aggregator.ts +40 -0
- package/src/runtime/adapter/subscriber.test.ts +48 -0
- package/src/runtime/adapter/subscriber.ts +9 -0
- package/src/runtime/event-parser.test.ts +44 -0
- package/src/runtime/event-parser.ts +16 -0
- package/src/runtime/types.ts +7 -0
- package/src/useAgUiRuntime.fast-refresh.test.tsx +170 -0
- package/src/useAgUiRuntime.ts +14 -3
|
@@ -29,6 +29,7 @@ type Subscriber = {
|
|
|
29
29
|
onToolCallChunkEvent?: (payload: { event: unknown }) => void;
|
|
30
30
|
onToolCallResultEvent?: (payload: { event: unknown }) => void;
|
|
31
31
|
onActivitySnapshotEvent?: (payload: { event: unknown }) => void;
|
|
32
|
+
onActivityDeltaEvent?: (payload: { event: unknown }) => void;
|
|
32
33
|
onStateSnapshotEvent?: (payload: { event: unknown }) => void;
|
|
33
34
|
onStateDeltaEvent?: (payload: { event: unknown }) => void;
|
|
34
35
|
onMessagesSnapshotEvent?: (payload: { event: unknown }) => void;
|
|
@@ -39,6 +40,8 @@ type Subscriber = {
|
|
|
39
40
|
onSubagentErrorEvent?: (payload: { event: unknown }) => void;
|
|
40
41
|
onRunFinishedEvent?: (payload: { event: unknown }) => void;
|
|
41
42
|
onRunErrorEvent?: (payload: { event: unknown }) => void;
|
|
43
|
+
onStepStartedEvent?: (payload: { event: unknown }) => void;
|
|
44
|
+
onStepFinishedEvent?: (payload: { event: unknown }) => void;
|
|
42
45
|
onRunFinalized?: () => void;
|
|
43
46
|
onRunFailed?: (payload: { error: Error }) => void;
|
|
44
47
|
};
|
|
@@ -142,6 +145,8 @@ export const createAgUiSubscriber = (
|
|
|
142
145
|
dispatchIfValid(event, "TOOL_CALL_RESULT"),
|
|
143
146
|
onActivitySnapshotEvent: ({ event }) =>
|
|
144
147
|
dispatchIfValid(event, "ACTIVITY_SNAPSHOT"),
|
|
148
|
+
onActivityDeltaEvent: ({ event }) =>
|
|
149
|
+
dispatchIfValid(event, "ACTIVITY_DELTA"),
|
|
145
150
|
onStateSnapshotEvent: ({ event }) =>
|
|
146
151
|
dispatchIfValid(event, "STATE_SNAPSHOT"),
|
|
147
152
|
onStateDeltaEvent: ({ event }) => dispatchIfValid(event, "STATE_DELTA"),
|
|
@@ -174,6 +179,10 @@ export const createAgUiSubscriber = (
|
|
|
174
179
|
);
|
|
175
180
|
onRunFailed?.(error);
|
|
176
181
|
},
|
|
182
|
+
onStepStartedEvent: ({ event }) =>
|
|
183
|
+
logger?.debug?.("[agui] step boundary ignored", event),
|
|
184
|
+
onStepFinishedEvent: ({ event }) =>
|
|
185
|
+
logger?.debug?.("[agui] step boundary ignored", event),
|
|
177
186
|
onRunFinalized: () => {
|
|
178
187
|
if (runFinishedDispatched) return;
|
|
179
188
|
dispatch({ type: "RUN_FINISHED", runId });
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { parseAgUiEvent } from "./event-parser";
|
|
3
|
+
|
|
4
|
+
const delta = {
|
|
5
|
+
messageId: "act-1",
|
|
6
|
+
activityType: "progress",
|
|
7
|
+
patch: [{ op: "replace", path: "/step", value: 2 }],
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
describe("parseAgUiEvent ACTIVITY_DELTA", () => {
|
|
11
|
+
it("maps the client shape", () => {
|
|
12
|
+
expect(
|
|
13
|
+
parseAgUiEvent({
|
|
14
|
+
type: "ACTIVITY_DELTA",
|
|
15
|
+
...delta,
|
|
16
|
+
subagentRunId: "sub",
|
|
17
|
+
}),
|
|
18
|
+
).toEqual({ type: "ACTIVITY_DELTA", ...delta, subagentRunId: "sub" });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("maps the bare wire shape without optional fields", () => {
|
|
22
|
+
expect(
|
|
23
|
+
parseAgUiEvent({ type: "ACTIVITY_DELTA", ...delta, timestamp: 1 }),
|
|
24
|
+
).toEqual({ type: "ACTIVITY_DELTA", ...delta });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("rejects a missing messageId", () => {
|
|
28
|
+
const logger = { debug: vi.fn(), error: vi.fn() };
|
|
29
|
+
const { messageId: _, ...rest } = delta;
|
|
30
|
+
expect(
|
|
31
|
+
parseAgUiEvent({ type: "ACTIVITY_DELTA", ...rest }, { logger }),
|
|
32
|
+
).toBe(null);
|
|
33
|
+
expect(logger.debug).toHaveBeenCalledWith(
|
|
34
|
+
"[agui] ACTIVITY_DELTA missing messageId, activityType or patch array",
|
|
35
|
+
expect.objectContaining({ activityType: "progress" }),
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("rejects a patch that is not an array", () => {
|
|
40
|
+
expect(
|
|
41
|
+
parseAgUiEvent({ type: "ACTIVITY_DELTA", ...delta, patch: "nope" }),
|
|
42
|
+
).toBe(null);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -389,6 +389,22 @@ export const parseAgUiEvent = (
|
|
|
389
389
|
},
|
|
390
390
|
);
|
|
391
391
|
}
|
|
392
|
+
case "ACTIVITY_DELTA": {
|
|
393
|
+
const messageId = getString("messageId");
|
|
394
|
+
const activityType = getString("activityType");
|
|
395
|
+
if (!messageId || !activityType || !Array.isArray(payload.patch)) {
|
|
396
|
+
return reject("missing messageId, activityType or patch array");
|
|
397
|
+
}
|
|
398
|
+
return withOptional(
|
|
399
|
+
{
|
|
400
|
+
type: "ACTIVITY_DELTA" as const,
|
|
401
|
+
messageId,
|
|
402
|
+
activityType,
|
|
403
|
+
patch: payload.patch as any[],
|
|
404
|
+
},
|
|
405
|
+
{ subagentRunId: getString("subagentRunId") },
|
|
406
|
+
);
|
|
407
|
+
}
|
|
392
408
|
case "RAW":
|
|
393
409
|
return withOptional(
|
|
394
410
|
{ type: "RAW" as const, event: payload.event },
|
package/src/runtime/types.ts
CHANGED
|
@@ -249,6 +249,13 @@ export type AgUiEvent =
|
|
|
249
249
|
replace?: boolean;
|
|
250
250
|
subagentRunId?: string;
|
|
251
251
|
}
|
|
252
|
+
| {
|
|
253
|
+
type: "ACTIVITY_DELTA";
|
|
254
|
+
messageId: string;
|
|
255
|
+
activityType: string;
|
|
256
|
+
patch: any[];
|
|
257
|
+
subagentRunId?: string;
|
|
258
|
+
}
|
|
252
259
|
| { type: "RAW"; event: any; source?: string }
|
|
253
260
|
| { type: "CUSTOM"; name: string; value: any }
|
|
254
261
|
| { type: "STATE_SNAPSHOT"; snapshot: any }
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { act, Activity, version, type ComponentType } from "react";
|
|
4
|
+
import { afterAll, afterEach, expect, it, vi } from "vitest";
|
|
5
|
+
import type { HttpAgent } from "@ag-ui/client";
|
|
6
|
+
import type { AssistantRuntime } from "@assistant-ui/core";
|
|
7
|
+
import { AgUiThreadRuntimeCore } from "./runtime/AgUiThreadRuntimeCore";
|
|
8
|
+
import type { UseAgUiRuntimeOptions } from "./runtime/types";
|
|
9
|
+
import { useAgUiRuntime, type AgUiAssistantRuntime } from "./useAgUiRuntime";
|
|
10
|
+
|
|
11
|
+
const onReact18 = version.startsWith("18.");
|
|
12
|
+
|
|
13
|
+
const base = vi.hoisted(() => ({ version: 0 }));
|
|
14
|
+
vi.mock("@assistant-ui/core/react", async (importOriginal) => {
|
|
15
|
+
const actual =
|
|
16
|
+
await importOriginal<typeof import("@assistant-ui/core/react")>();
|
|
17
|
+
const derived = new WeakMap<AssistantRuntime, AssistantRuntime>();
|
|
18
|
+
return {
|
|
19
|
+
...actual,
|
|
20
|
+
useExternalStoreRuntime: (
|
|
21
|
+
store: Parameters<typeof actual.useExternalStoreRuntime>[0],
|
|
22
|
+
) => {
|
|
23
|
+
const runtime = actual.useExternalStoreRuntime(store);
|
|
24
|
+
if (base.version === 0) return runtime;
|
|
25
|
+
let next = derived.get(runtime);
|
|
26
|
+
if (!next) {
|
|
27
|
+
next = Object.create(runtime) as AssistantRuntime;
|
|
28
|
+
derived.set(runtime, next);
|
|
29
|
+
}
|
|
30
|
+
return next;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
type Family = { current: unknown };
|
|
36
|
+
type RendererInternals = {
|
|
37
|
+
setRefreshHandler: (resolve: (type: unknown) => Family | undefined) => void;
|
|
38
|
+
scheduleRefresh: (
|
|
39
|
+
root: unknown,
|
|
40
|
+
update: { staleFamilies: Set<Family>; updatedFamilies: Set<Family> },
|
|
41
|
+
) => void;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
let renderer: RendererInternals | undefined;
|
|
45
|
+
const fiberRoots = new Set<unknown>();
|
|
46
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
47
|
+
vi.stubGlobal("__REACT_DEVTOOLS_GLOBAL_HOOK__", {
|
|
48
|
+
supportsFiber: true,
|
|
49
|
+
inject: (internals: RendererInternals) => {
|
|
50
|
+
renderer = internals;
|
|
51
|
+
return 1;
|
|
52
|
+
},
|
|
53
|
+
onScheduleFiberRoot: () => {},
|
|
54
|
+
onCommitFiberRoot: (_id: number, root: unknown) => fiberRoots.add(root),
|
|
55
|
+
onCommitFiberUnmount: () => {},
|
|
56
|
+
});
|
|
57
|
+
const { cleanup, render, waitFor } = await import("@testing-library/react");
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
cleanup();
|
|
61
|
+
fiberRoots.clear();
|
|
62
|
+
vi.restoreAllMocks();
|
|
63
|
+
base.version = 0;
|
|
64
|
+
});
|
|
65
|
+
afterAll(() => vi.unstubAllGlobals());
|
|
66
|
+
|
|
67
|
+
let options: UseAgUiRuntimeOptions;
|
|
68
|
+
let runtime: AgUiAssistantRuntime;
|
|
69
|
+
const createProbe = () => {
|
|
70
|
+
const Before = () => {
|
|
71
|
+
runtime = useAgUiRuntime(options);
|
|
72
|
+
return null;
|
|
73
|
+
};
|
|
74
|
+
const After = () => {
|
|
75
|
+
runtime = useAgUiRuntime(options);
|
|
76
|
+
return null;
|
|
77
|
+
};
|
|
78
|
+
return { Before, After };
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const refresh = async (Before: unknown, After: unknown) => {
|
|
82
|
+
const family: Family = { current: After };
|
|
83
|
+
renderer!.setRefreshHandler((type) =>
|
|
84
|
+
type === Before || type === After ? family : undefined,
|
|
85
|
+
);
|
|
86
|
+
await act(async () => {
|
|
87
|
+
for (const root of fiberRoots) {
|
|
88
|
+
renderer!.scheduleRefresh(root, {
|
|
89
|
+
staleFamilies: new Set(),
|
|
90
|
+
updatedFamilies: new Set([family]),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
await act(async () => {});
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
it("keeps the wrapper across refresh and replaces it when the base runtime changes", async () => {
|
|
98
|
+
const agent = {
|
|
99
|
+
runAgent: vi.fn(),
|
|
100
|
+
abortRun: vi.fn(),
|
|
101
|
+
} as unknown as HttpAgent;
|
|
102
|
+
options = { agent };
|
|
103
|
+
const { Before, After } = createProbe();
|
|
104
|
+
const view = render(<Before />);
|
|
105
|
+
const wrapper = runtime;
|
|
106
|
+
await refresh(Before, After);
|
|
107
|
+
expect(runtime).toBe(wrapper);
|
|
108
|
+
|
|
109
|
+
base.version = 1;
|
|
110
|
+
view.rerender(<After />);
|
|
111
|
+
await act(async () => {});
|
|
112
|
+
expect(runtime).not.toBe(wrapper);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const expectInFlightRunAcrossRefresh = async (
|
|
116
|
+
exit: "hidden" | "unmount",
|
|
117
|
+
inActivity: boolean,
|
|
118
|
+
) => {
|
|
119
|
+
let resolveRun!: () => void;
|
|
120
|
+
const run = new Promise<void>((resolve) => {
|
|
121
|
+
resolveRun = resolve;
|
|
122
|
+
});
|
|
123
|
+
const agent = {
|
|
124
|
+
runAgent: vi.fn(() => run),
|
|
125
|
+
abortRun: vi.fn(),
|
|
126
|
+
} as unknown as HttpAgent;
|
|
127
|
+
const attach = vi.spyOn(AgUiThreadRuntimeCore.prototype, "attachRuntime");
|
|
128
|
+
const detach = vi.spyOn(AgUiThreadRuntimeCore.prototype, "detachRuntime");
|
|
129
|
+
options = { agent };
|
|
130
|
+
const { Before, After } = createProbe();
|
|
131
|
+
const Shell: ComponentType<{ mode: "visible" | "hidden" }> = inActivity
|
|
132
|
+
? ({ mode }) => (
|
|
133
|
+
<Activity mode={mode}>
|
|
134
|
+
<Before />
|
|
135
|
+
</Activity>
|
|
136
|
+
)
|
|
137
|
+
: () => <Before />;
|
|
138
|
+
const view = render(<Shell mode="visible" />);
|
|
139
|
+
const wrapper = runtime;
|
|
140
|
+
const core = attach.mock.instances[0]!;
|
|
141
|
+
act(() => {
|
|
142
|
+
void runtime.thread.append("hello");
|
|
143
|
+
});
|
|
144
|
+
await waitFor(() => expect(agent.runAgent).toHaveBeenCalledOnce());
|
|
145
|
+
|
|
146
|
+
await refresh(Before, After);
|
|
147
|
+
expect(agent.abortRun).not.toHaveBeenCalled();
|
|
148
|
+
expect(detach).not.toHaveBeenCalled();
|
|
149
|
+
expect(runtime).toBe(wrapper);
|
|
150
|
+
expect(attach.mock.instances.at(-1)).toBe(core);
|
|
151
|
+
|
|
152
|
+
if (exit === "hidden") view.rerender(<Shell mode="hidden" />);
|
|
153
|
+
else view.unmount();
|
|
154
|
+
await act(async () => {});
|
|
155
|
+
expect(detach).toHaveBeenCalledOnce();
|
|
156
|
+
expect(agent.abortRun).toHaveBeenCalledOnce();
|
|
157
|
+
resolveRun();
|
|
158
|
+
await act(async () => {
|
|
159
|
+
await run;
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Activity is React 19 only.
|
|
164
|
+
it.skipIf(onReact18).each(["hidden", "unmount"] as const)(
|
|
165
|
+
"keeps an in-flight run across refresh and detaches when %s",
|
|
166
|
+
(exit) => expectInFlightRunAcrossRefresh(exit, true),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
it("keeps an in-flight run across refresh and detaches when unmounted outside an Activity", () =>
|
|
170
|
+
expectInFlightRunAcrossRefresh("unmount", false));
|
package/src/useAgUiRuntime.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
useExternalStoreSharedOptions,
|
|
15
15
|
useRuntimeAdapters,
|
|
16
16
|
} from "@assistant-ui/core/react";
|
|
17
|
+
import { useReplaySafeEffect } from "@assistant-ui/store/internal";
|
|
17
18
|
import { createMessageQueue } from "@assistant-ui/core";
|
|
18
19
|
import type {
|
|
19
20
|
MessageQueueController,
|
|
@@ -309,16 +310,26 @@ export function useAgUiRuntime(
|
|
|
309
310
|
|
|
310
311
|
const baseRuntime = useExternalStoreRuntime(store);
|
|
311
312
|
|
|
312
|
-
const
|
|
313
|
+
const createRuntime = (): AgUiAssistantRuntime => {
|
|
313
314
|
const wrapper = Object.create(baseRuntime) as AgUiAssistantRuntime;
|
|
314
315
|
wrapper.unstable_getPendingInterrupts = () =>
|
|
315
316
|
core.getPendingInterrupts()?.interrupts ?? [];
|
|
316
317
|
wrapper.unstable_submitInterruptResponses = (responses) =>
|
|
317
318
|
core.submitInterruptResponses(responses);
|
|
318
319
|
return wrapper;
|
|
319
|
-
}
|
|
320
|
+
};
|
|
321
|
+
const [pinnedRuntime, setPinnedRuntime] = useState(() => ({
|
|
322
|
+
baseRuntime,
|
|
323
|
+
runtime: createRuntime(),
|
|
324
|
+
}));
|
|
325
|
+
let currentRuntime = pinnedRuntime;
|
|
326
|
+
if (pinnedRuntime.baseRuntime !== baseRuntime) {
|
|
327
|
+
currentRuntime = { baseRuntime, runtime: createRuntime() };
|
|
328
|
+
setPinnedRuntime(currentRuntime);
|
|
329
|
+
}
|
|
330
|
+
const runtime = currentRuntime.runtime;
|
|
320
331
|
|
|
321
|
-
|
|
332
|
+
useReplaySafeEffect(() => {
|
|
322
333
|
core.attachRuntime(runtime);
|
|
323
334
|
return () => {
|
|
324
335
|
core.detachRuntime();
|