@assistant-ui/react-ag-ui 0.0.69 → 0.0.71

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 (30) hide show
  1. package/README.md +1 -1
  2. package/dist/runtime/AgUiThreadRuntimeCore.d.ts +6 -3
  3. package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
  4. package/dist/runtime/AgUiThreadRuntimeCore.js +77 -10
  5. package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
  6. package/dist/runtime/adapter/conversions.d.ts +5 -0
  7. package/dist/runtime/adapter/conversions.d.ts.map +1 -1
  8. package/dist/runtime/adapter/conversions.js +104 -15
  9. package/dist/runtime/adapter/conversions.js.map +1 -1
  10. package/dist/runtime/adapter/run-aggregator.d.ts +3 -0
  11. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  12. package/dist/runtime/adapter/run-aggregator.js +4 -2
  13. package/dist/runtime/adapter/run-aggregator.js.map +1 -1
  14. package/dist/runtime/types.d.ts +18 -2
  15. package/dist/runtime/types.d.ts.map +1 -1
  16. package/dist/runtime/types.js.map +1 -1
  17. package/dist/useAgUiRuntime.d.ts.map +1 -1
  18. package/dist/useAgUiRuntime.js +4 -0
  19. package/dist/useAgUiRuntime.js.map +1 -1
  20. package/package.json +5 -5
  21. package/src/runtime/AgUiThreadRuntimeCore.interrupt-cancel.test.ts +202 -0
  22. package/src/runtime/AgUiThreadRuntimeCore.ts +143 -8
  23. package/src/runtime/adapter/conversions.metadata.test.ts +400 -0
  24. package/src/runtime/adapter/conversions.ts +170 -28
  25. package/src/runtime/adapter/run-aggregator.approval.test.ts +4 -4
  26. package/src/runtime/adapter/run-aggregator.ts +13 -2
  27. package/src/runtime/types.ts +20 -1
  28. package/src/useAgUiRuntime.context.test.tsx +227 -0
  29. package/src/useAgUiRuntime.external-store.test.tsx +39 -0
  30. package/src/useAgUiRuntime.ts +4 -0
@@ -0,0 +1,227 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
+ import { act, cleanup, renderHook } from "@testing-library/react";
5
+ import type { HttpAgent, RunAgentParameters } from "@ag-ui/client";
6
+ import { useAgUiRuntime } from "./useAgUiRuntime";
7
+
8
+ const createAgent = () => {
9
+ const runAgent = vi.fn(async (_input: RunAgentParameters) => {});
10
+ return {
11
+ agent: { runAgent, abortRun: vi.fn() } as unknown as HttpAgent,
12
+ runAgent,
13
+ };
14
+ };
15
+
16
+ const send = async (
17
+ runtime: ReturnType<typeof useAgUiRuntime>,
18
+ text: string,
19
+ ) => {
20
+ await act(async () => {
21
+ await runtime.thread.append({
22
+ role: "user",
23
+ content: [{ type: "text", text }],
24
+ });
25
+ });
26
+ };
27
+
28
+ afterEach(cleanup);
29
+
30
+ describe("useAgUiRuntime run context", () => {
31
+ it("appends run context entries after the system entry in order", async () => {
32
+ const { agent, runAgent } = createAgent();
33
+ const getRunContext = vi.fn(() => [
34
+ { description: "account", value: "123" },
35
+ { description: "locale", value: "en-US" },
36
+ ]);
37
+ const { result } = renderHook(() =>
38
+ useAgUiRuntime({ agent, getRunContext }),
39
+ );
40
+ result.current.registerModelContextProvider({
41
+ getModelContext: () => ({ system: "Be helpful" }),
42
+ });
43
+
44
+ await send(result.current, "hello");
45
+
46
+ expect(getRunContext).toHaveBeenCalledOnce();
47
+ expect(runAgent.mock.calls[0]?.[0].context).toEqual([
48
+ { description: "system", value: "Be helpful" },
49
+ { description: "account", value: "123" },
50
+ { description: "locale", value: "en-US" },
51
+ ]);
52
+ });
53
+
54
+ it("keeps the previous context with no getter", async () => {
55
+ const { agent, runAgent } = createAgent();
56
+ const { result } = renderHook(() => useAgUiRuntime({ agent }));
57
+
58
+ await send(result.current, "first");
59
+ expect(runAgent.mock.calls[0]?.[0].context).toEqual([]);
60
+
61
+ result.current.registerModelContextProvider({
62
+ getModelContext: () => ({ system: "Be helpful" }),
63
+ });
64
+ await send(result.current, "second");
65
+ expect(runAgent.mock.calls[1]?.[0].context).toEqual([
66
+ { description: "system", value: "Be helpful" },
67
+ ]);
68
+ });
69
+
70
+ it("runs without host entries when the getter throws", async () => {
71
+ const { agent, runAgent } = createAgent();
72
+ const onError = vi.fn();
73
+ const loggerError = vi.fn();
74
+ const getRunContext = vi.fn(() => {
75
+ throw "context unavailable";
76
+ });
77
+ const { result } = renderHook(() =>
78
+ useAgUiRuntime({
79
+ agent,
80
+ getRunContext,
81
+ onError,
82
+ logger: { error: loggerError },
83
+ }),
84
+ );
85
+ result.current.registerModelContextProvider({
86
+ getModelContext: () => ({ system: "Be helpful" }),
87
+ });
88
+
89
+ await send(result.current, "hello");
90
+
91
+ expect(getRunContext).toHaveBeenCalledOnce();
92
+ expect(onError).toHaveBeenCalledExactlyOnceWith(expect.any(Error));
93
+ expect(onError.mock.calls[0]?.[0].message).toBe("context unavailable");
94
+ expect(loggerError).toHaveBeenCalledExactlyOnceWith(
95
+ "[agui] getRunContext failed",
96
+ onError.mock.calls[0]?.[0],
97
+ );
98
+ expect(runAgent).toHaveBeenCalledOnce();
99
+ expect(runAgent.mock.calls[0]?.[0].context).toEqual([
100
+ { description: "system", value: "Be helpful" },
101
+ ]);
102
+ });
103
+
104
+ it("uses the latest getter on the next run", async () => {
105
+ const { agent, runAgent } = createAgent();
106
+ const first = vi.fn(() => [{ description: "account", value: "one" }]);
107
+ const second = vi.fn(() => [{ description: "account", value: "two" }]);
108
+ const { result, rerender } = renderHook(
109
+ ({ getRunContext }) => useAgUiRuntime({ agent, getRunContext }),
110
+ { initialProps: { getRunContext: first } },
111
+ );
112
+
113
+ await send(result.current, "first");
114
+ rerender({ getRunContext: second });
115
+ await send(result.current, "second");
116
+
117
+ expect(first).toHaveBeenCalledOnce();
118
+ expect(second).toHaveBeenCalledOnce();
119
+ expect(runAgent.mock.calls.map(([input]) => input.context)).toEqual([
120
+ [{ description: "account", value: "one" }],
121
+ [{ description: "account", value: "two" }],
122
+ ]);
123
+ });
124
+
125
+ it("calls the getter again when resuming a run", async () => {
126
+ const runAgent = vi.fn(
127
+ async (
128
+ _input: RunAgentParameters,
129
+ subscriber: {
130
+ onRunFinishedEvent?: (params: unknown) => unknown;
131
+ onRunFinalized?: (params: unknown) => unknown;
132
+ },
133
+ ) => {
134
+ if (runAgent.mock.calls.length === 1) {
135
+ subscriber.onRunFinishedEvent?.({
136
+ event: {
137
+ type: "RUN_FINISHED",
138
+ runId: "first",
139
+ outcome: {
140
+ type: "interrupt",
141
+ interrupts: [{ id: "input", reason: "input_required" }],
142
+ },
143
+ },
144
+ });
145
+ }
146
+ subscriber.onRunFinalized?.(undefined);
147
+ },
148
+ );
149
+ const agent = { runAgent, abortRun: vi.fn() } as unknown as HttpAgent;
150
+ const getRunContext = vi
151
+ .fn()
152
+ .mockReturnValueOnce([{ description: "account", value: "before" }])
153
+ .mockReturnValueOnce([{ description: "account", value: "after" }]);
154
+ const { result } = renderHook(() =>
155
+ useAgUiRuntime({ agent, getRunContext }),
156
+ );
157
+
158
+ await send(result.current, "first");
159
+ await act(async () => {
160
+ await result.current.unstable_submitInterruptResponses([
161
+ { interruptId: "input", status: "resolved", payload: "answer" },
162
+ ]);
163
+ });
164
+
165
+ expect(getRunContext).toHaveBeenCalledTimes(2);
166
+ expect(runAgent.mock.calls.map(([input]) => input.context)).toEqual([
167
+ [{ description: "account", value: "before" }],
168
+ [{ description: "account", value: "after" }],
169
+ ]);
170
+ expect(runAgent.mock.calls[1]?.[0].resume).toEqual([
171
+ { interruptId: "input", status: "resolved", payload: "answer" },
172
+ ]);
173
+ });
174
+
175
+ it("sends an interrupt response when the resume getter throws", async () => {
176
+ const runAgent = vi.fn(
177
+ async (
178
+ _input: RunAgentParameters,
179
+ subscriber: {
180
+ onRunFinishedEvent?: (params: unknown) => unknown;
181
+ onRunFinalized?: (params: unknown) => unknown;
182
+ },
183
+ ) => {
184
+ if (runAgent.mock.calls.length === 1) {
185
+ subscriber.onRunFinishedEvent?.({
186
+ event: {
187
+ type: "RUN_FINISHED",
188
+ runId: "first",
189
+ outcome: {
190
+ type: "interrupt",
191
+ interrupts: [{ id: "input", reason: "input_required" }],
192
+ },
193
+ },
194
+ });
195
+ }
196
+ subscriber.onRunFinalized?.(undefined);
197
+ },
198
+ );
199
+ const agent = { runAgent, abortRun: vi.fn() } as unknown as HttpAgent;
200
+ const error = new Error("context unavailable");
201
+ const onError = vi.fn();
202
+ const getRunContext = vi
203
+ .fn()
204
+ .mockReturnValueOnce([{ description: "account", value: "before" }])
205
+ .mockImplementationOnce(() => {
206
+ throw error;
207
+ });
208
+ const { result } = renderHook(() =>
209
+ useAgUiRuntime({ agent, getRunContext, onError }),
210
+ );
211
+
212
+ await send(result.current, "first");
213
+ await act(async () => {
214
+ await result.current.unstable_submitInterruptResponses([
215
+ { interruptId: "input", status: "resolved", payload: "answer" },
216
+ ]);
217
+ });
218
+
219
+ expect(getRunContext).toHaveBeenCalledTimes(2);
220
+ expect(onError).toHaveBeenCalledExactlyOnceWith(error);
221
+ expect(runAgent).toHaveBeenCalledTimes(2);
222
+ expect(runAgent.mock.calls[1]?.[0].context).toEqual([]);
223
+ expect(runAgent.mock.calls[1]?.[0].resume).toEqual([
224
+ { interruptId: "input", status: "resolved", payload: "answer" },
225
+ ]);
226
+ });
227
+ });
@@ -49,3 +49,42 @@ it("re-renders when the core publishes a state change", () => {
49
49
  expect(renders).toBeGreaterThan(initialRenders);
50
50
  expect(result.current.thread.getState().messages).toHaveLength(1);
51
51
  });
52
+
53
+ it("passes joinStrategy to the active run", async () => {
54
+ const agent = {
55
+ runAgent: vi.fn(async (_input: unknown, subscriber: any) => {
56
+ for (const [messageId, delta] of [
57
+ ["a", "A"],
58
+ ["b", "B"],
59
+ ]) {
60
+ subscriber.onTextMessageStartEvent?.({
61
+ event: { type: "TEXT_MESSAGE_START", messageId },
62
+ });
63
+ subscriber.onTextMessageContentEvent?.({
64
+ event: { type: "TEXT_MESSAGE_CONTENT", messageId, delta },
65
+ });
66
+ }
67
+ subscriber.onRunFinalized?.();
68
+ }),
69
+ abortRun: vi.fn(),
70
+ } as unknown as HttpAgent;
71
+ const { result } = renderHook(() =>
72
+ useAgUiRuntime({ agent, joinStrategy: "concat-content" }),
73
+ );
74
+
75
+ await act(async () => {
76
+ await result.current.thread.append({
77
+ role: "user",
78
+ content: [{ type: "text", text: "hi" }],
79
+ });
80
+ });
81
+
82
+ const assistants = result.current.thread
83
+ .getState()
84
+ .messages.filter((message) => message.role === "assistant");
85
+ expect(assistants).toHaveLength(1);
86
+ expect(assistants[0]?.content).toMatchObject([
87
+ { type: "text", text: "A" },
88
+ { type: "text", text: "B" },
89
+ ]);
90
+ });
@@ -75,6 +75,8 @@ export function useAgUiRuntime(
75
75
  agent: options.agent,
76
76
  logger,
77
77
  showThinking: options.showThinking ?? true,
78
+ joinStrategy: options.joinStrategy,
79
+ getRunContext: options.getRunContext,
78
80
  resumeTranscript: options.resumeTranscript,
79
81
  autoCancelPendingToolCalls: options.autoCancelPendingToolCalls,
80
82
  ...(options.onError && { onError: options.onError }),
@@ -95,6 +97,8 @@ export function useAgUiRuntime(
95
97
  agent: options.agent,
96
98
  logger,
97
99
  showThinking: options.showThinking ?? true,
100
+ joinStrategy: options.joinStrategy,
101
+ getRunContext: options.getRunContext,
98
102
  resumeTranscript: options.resumeTranscript,
99
103
  autoCancelPendingToolCalls: options.autoCancelPendingToolCalls,
100
104
  ...(options.onError && { onError: options.onError }),