@assistant-ui/react-ag-ui 0.0.70 → 0.1.0

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 (54) hide show
  1. package/README.md +1 -1
  2. package/dist/runtime/{AgUiThreadRuntimeCore.d.ts → AgUiThreadController.d.ts} +8 -12
  3. package/dist/runtime/AgUiThreadController.d.ts.map +1 -0
  4. package/dist/runtime/{AgUiThreadRuntimeCore.js → AgUiThreadController.js} +302 -162
  5. package/dist/runtime/AgUiThreadController.js.map +1 -0
  6. package/dist/runtime/adapter/conversions.d.ts +4 -4
  7. package/dist/runtime/adapter/conversions.d.ts.map +1 -1
  8. package/dist/runtime/adapter/conversions.js +111 -33
  9. package/dist/runtime/adapter/conversions.js.map +1 -1
  10. package/dist/runtime/adapter/run-aggregator.d.ts +6 -5
  11. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  12. package/dist/runtime/adapter/run-aggregator.js +50 -62
  13. package/dist/runtime/adapter/run-aggregator.js.map +1 -1
  14. package/dist/runtime/adapter/subscriber.d.ts +0 -24
  15. package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
  16. package/dist/runtime/adapter/subscriber.js +1 -22
  17. package/dist/runtime/adapter/subscriber.js.map +1 -1
  18. package/dist/runtime/agUiThreadState.d.ts +60 -0
  19. package/dist/runtime/agUiThreadState.d.ts.map +1 -0
  20. package/dist/runtime/agUiThreadState.js +95 -0
  21. package/dist/runtime/agUiThreadState.js.map +1 -0
  22. package/dist/runtime/event-parser.d.ts.map +1 -1
  23. package/dist/runtime/event-parser.js +45 -48
  24. package/dist/runtime/event-parser.js.map +1 -1
  25. package/dist/runtime/mcp-tool-result.d.ts +3 -1
  26. package/dist/runtime/mcp-tool-result.d.ts.map +1 -1
  27. package/dist/runtime/mcp-tool-result.js +6 -3
  28. package/dist/runtime/mcp-tool-result.js.map +1 -1
  29. package/dist/runtime/types.d.ts +13 -35
  30. package/dist/runtime/types.d.ts.map +1 -1
  31. package/dist/runtime/types.js.map +1 -1
  32. package/dist/useAgUiRuntime.d.ts.map +1 -1
  33. package/dist/useAgUiRuntime.js +4 -2
  34. package/dist/useAgUiRuntime.js.map +1 -1
  35. package/package.json +13 -12
  36. package/src/runtime/{AgUiThreadRuntimeCore.interrupt-cancel.test.ts → AgUiThreadController.interrupt-cancel.test.ts} +4 -4
  37. package/src/runtime/{AgUiThreadRuntimeCore.test.ts → AgUiThreadController.test.ts} +35 -6
  38. package/src/runtime/{AgUiThreadRuntimeCore.ts → AgUiThreadController.ts} +471 -169
  39. package/src/runtime/adapter/conversions.content.test.ts +37 -5
  40. package/src/runtime/adapter/conversions.tools.test.ts +2 -1
  41. package/src/runtime/adapter/conversions.ts +235 -58
  42. package/src/runtime/adapter/run-aggregator.ts +84 -114
  43. package/src/runtime/adapter/subscriber.ts +1 -44
  44. package/src/runtime/agUiThreadState.test.ts +205 -0
  45. package/src/runtime/agUiThreadState.ts +130 -0
  46. package/src/runtime/event-parser.ts +40 -58
  47. package/src/runtime/mcp-tool-result.ts +16 -2
  48. package/src/runtime/types.ts +18 -28
  49. package/src/useAgUiRuntime.external-store.test.tsx +39 -0
  50. package/src/useAgUiRuntime.fast-refresh.test.tsx +3 -3
  51. package/src/useAgUiRuntime.thread-switch.test.tsx +4 -4
  52. package/src/useAgUiRuntime.ts +5 -3
  53. package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +0 -1
  54. package/dist/runtime/AgUiThreadRuntimeCore.js.map +0 -1
@@ -0,0 +1,205 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ createAgUiThreadState,
4
+ reduceAgUiThreadState,
5
+ } from "./agUiThreadState";
6
+
7
+ describe("reduceAgUiThreadState", () => {
8
+ it("tracks run and load phases without changing state for repeated phases", () => {
9
+ const initial = createAgUiThreadState();
10
+ expect(reduceAgUiThreadState(initial, { type: "runFinished" })).toBe(
11
+ initial,
12
+ );
13
+ expect(reduceAgUiThreadState(initial, { type: "loadFinished" })).toBe(
14
+ initial,
15
+ );
16
+
17
+ const running = reduceAgUiThreadState(initial, { type: "runStarted" });
18
+ const requested = reduceAgUiThreadState(running, { type: "loadRequested" });
19
+ const loading = reduceAgUiThreadState(requested, { type: "loadStarted" });
20
+ expect(loading).toMatchObject({
21
+ isRunning: true,
22
+ isLoading: true,
23
+ loadRequested: true,
24
+ });
25
+ expect(reduceAgUiThreadState(running, { type: "runStarted" })).toBe(
26
+ running,
27
+ );
28
+ expect(reduceAgUiThreadState(requested, { type: "loadRequested" })).toBe(
29
+ requested,
30
+ );
31
+ expect(reduceAgUiThreadState(loading, { type: "loadStarted" })).toBe(
32
+ loading,
33
+ );
34
+ expect(initial).toMatchObject({
35
+ isRunning: false,
36
+ isLoading: false,
37
+ loadRequested: false,
38
+ });
39
+
40
+ const stopped = reduceAgUiThreadState(loading, { type: "runFinished" });
41
+ const loaded = reduceAgUiThreadState(stopped, { type: "loadFinished" });
42
+ expect(loaded.isRunning).toBe(false);
43
+ expect(loaded.isLoading).toBe(false);
44
+ expect(loaded.loadRequested).toBe(true);
45
+ });
46
+
47
+ it("advances history mutation generation across thread resets", () => {
48
+ const initial = createAgUiThreadState();
49
+ const first = reduceAgUiThreadState(initial, {
50
+ type: "historyMutationAdvanced",
51
+ });
52
+ const reset = reduceAgUiThreadState(first, { type: "resetThread" });
53
+ const second = reduceAgUiThreadState(reset, {
54
+ type: "historyMutationAdvanced",
55
+ });
56
+ expect(initial.historyMutationGeneration).toBe(0);
57
+ expect(reset.historyMutationGeneration).toBe(1);
58
+ expect(second.historyMutationGeneration).toBe(2);
59
+ });
60
+
61
+ it("replaces the state snapshot", () => {
62
+ const initial = createAgUiThreadState();
63
+ const snapshot = { count: 1 };
64
+ const first = reduceAgUiThreadState(initial, {
65
+ type: "stateSnapshot",
66
+ snapshot,
67
+ });
68
+ expect(first.stateSnapshot).toBe(snapshot);
69
+ expect(
70
+ reduceAgUiThreadState(first, { type: "stateSnapshot", snapshot }),
71
+ ).toBe(first);
72
+ });
73
+
74
+ it("sets and resets state snapshots", () => {
75
+ const initial = createAgUiThreadState();
76
+ const first = reduceAgUiThreadState(initial, {
77
+ type: "stateSnapshot",
78
+ snapshot: { count: 1 },
79
+ });
80
+ const second = reduceAgUiThreadState(first, {
81
+ type: "stateSnapshot",
82
+ snapshot: { count: 2 },
83
+ });
84
+ expect(second.stateSnapshot).toEqual({ count: 2 });
85
+ expect(first.stateSnapshot).toEqual({ count: 1 });
86
+ expect(
87
+ reduceAgUiThreadState(second, {
88
+ type: "stateSnapshot",
89
+ snapshot: second.stateSnapshot!,
90
+ }),
91
+ ).toBe(second);
92
+ const reset = reduceAgUiThreadState(second, { type: "resetState" });
93
+ expect(reset.stateSnapshot).toBeUndefined();
94
+ expect(reduceAgUiThreadState(reset, { type: "resetState" })).toBe(reset);
95
+ });
96
+
97
+ it("records run config and manages deferred continuations by owner", () => {
98
+ const initial = createAgUiThreadState();
99
+ const runConfig = {};
100
+ const configured = reduceAgUiThreadState(initial, {
101
+ type: "runConfigRecorded",
102
+ runConfig,
103
+ });
104
+ expect(configured.lastRunConfig).toBe(runConfig);
105
+ expect(
106
+ reduceAgUiThreadState(configured, {
107
+ type: "runConfigRecorded",
108
+ runConfig,
109
+ }),
110
+ ).toBe(configured);
111
+
112
+ const owner = new AbortController();
113
+ const withResume = reduceAgUiThreadState(configured, {
114
+ type: "resumeDeferred",
115
+ owner,
116
+ messageId: "assistant",
117
+ });
118
+ expect(withResume.pendingResume).toEqual({ owner, messageId: "assistant" });
119
+ expect(
120
+ reduceAgUiThreadState(withResume, {
121
+ type: "resumeDeferred",
122
+ owner,
123
+ messageId: "assistant",
124
+ }),
125
+ ).toBe(withResume);
126
+ const withOwner = reduceAgUiThreadState(withResume, {
127
+ type: "a2uiResumeDeferred",
128
+ owner,
129
+ });
130
+ const action = { value: "submit" };
131
+ const withAction = reduceAgUiThreadState(withOwner, {
132
+ type: "a2uiActionDeferred",
133
+ action,
134
+ });
135
+ expect(withAction.pendingA2uiResumeOwner).toBe(owner);
136
+ expect(withAction.pendingA2uiAction).toBe(action);
137
+ expect(
138
+ reduceAgUiThreadState(withOwner, { type: "a2uiResumeDeferred", owner }),
139
+ ).toBe(withOwner);
140
+ expect(
141
+ reduceAgUiThreadState(withAction, { type: "a2uiActionDeferred", action }),
142
+ ).toBe(withAction);
143
+
144
+ const actionCleared = reduceAgUiThreadState(withAction, {
145
+ type: "a2uiActionCleared",
146
+ });
147
+ expect(actionCleared.pendingA2uiAction).toBeUndefined();
148
+ expect(actionCleared.pendingA2uiResumeOwner).toBe(owner);
149
+ const ownerCleared = reduceAgUiThreadState(actionCleared, {
150
+ type: "a2uiResumeCleared",
151
+ });
152
+ expect(ownerCleared.pendingA2uiResumeOwner).toBeNull();
153
+ expect(
154
+ reduceAgUiThreadState(ownerCleared, { type: "a2uiResumeCleared" }),
155
+ ).toBe(ownerCleared);
156
+ expect(
157
+ reduceAgUiThreadState(ownerCleared, { type: "a2uiActionCleared" }),
158
+ ).toBe(ownerCleared);
159
+ const resumeCleared = reduceAgUiThreadState(ownerCleared, {
160
+ type: "resumeCleared",
161
+ });
162
+ expect(resumeCleared.pendingResume).toBeNull();
163
+ expect(
164
+ reduceAgUiThreadState(resumeCleared, { type: "resumeCleared" }),
165
+ ).toBe(resumeCleared);
166
+ });
167
+
168
+ it("clears A2UI continuations and resets thread state without changing run flags", () => {
169
+ const owner = new AbortController();
170
+ let state = reduceAgUiThreadState(createAgUiThreadState(), {
171
+ type: "runStarted",
172
+ });
173
+ state = reduceAgUiThreadState(state, { type: "loadStarted" });
174
+ state = reduceAgUiThreadState(state, {
175
+ type: "resumeDeferred",
176
+ owner,
177
+ messageId: "assistant",
178
+ });
179
+ state = reduceAgUiThreadState(state, {
180
+ type: "a2uiResumeDeferred",
181
+ owner,
182
+ });
183
+ state = reduceAgUiThreadState(state, {
184
+ type: "a2uiActionDeferred",
185
+ action: { value: "submit" },
186
+ });
187
+ const cleared = reduceAgUiThreadState(state, {
188
+ type: "a2uiContinuationsCleared",
189
+ });
190
+ expect(cleared.pendingA2uiResumeOwner).toBeNull();
191
+ expect(cleared.pendingA2uiAction).toBeUndefined();
192
+ expect(cleared.pendingResume).toEqual({ owner, messageId: "assistant" });
193
+ expect(
194
+ reduceAgUiThreadState(cleared, { type: "a2uiContinuationsCleared" }),
195
+ ).toBe(cleared);
196
+
197
+ const reset = reduceAgUiThreadState(state, { type: "resetThread" });
198
+ expect(reset).toMatchObject({ isRunning: true, isLoading: true });
199
+ expect(reset.stateSnapshot).toBeUndefined();
200
+ expect(reset.pendingResume).toBeNull();
201
+ expect(reset.pendingA2uiResumeOwner).toBeNull();
202
+ expect(reset.pendingA2uiAction).toBeUndefined();
203
+ expect(reduceAgUiThreadState(reset, { type: "resetThread" })).toBe(reset);
204
+ });
205
+ });
@@ -0,0 +1,130 @@
1
+ import type { RunConfig } from "@assistant-ui/core";
2
+ import type { ReadonlyJSONValue } from "assistant-stream/utils";
3
+
4
+ export type AgUiThreadState = {
5
+ isRunning: boolean;
6
+ isLoading: boolean;
7
+ loadRequested: boolean;
8
+ historyMutationGeneration: number;
9
+ stateSnapshot: ReadonlyJSONValue | undefined;
10
+ lastRunConfig: RunConfig | undefined;
11
+ pendingResume: { owner: AbortController; messageId: string } | null;
12
+ pendingA2uiResumeOwner: AbortController | null;
13
+ pendingA2uiAction: Record<string, unknown> | undefined;
14
+ };
15
+
16
+ export type AgUiThreadAction =
17
+ | { type: "runStarted" }
18
+ | { type: "runFinished" }
19
+ | { type: "loadRequested" }
20
+ | { type: "loadStarted" }
21
+ | { type: "loadFinished" }
22
+ | { type: "historyMutationAdvanced" }
23
+ | { type: "stateSnapshot"; snapshot: ReadonlyJSONValue }
24
+ | { type: "resetState" }
25
+ | { type: "runConfigRecorded"; runConfig: RunConfig }
26
+ | { type: "resumeDeferred"; owner: AbortController; messageId: string }
27
+ | { type: "resumeCleared" }
28
+ | { type: "a2uiResumeDeferred"; owner: AbortController }
29
+ | { type: "a2uiResumeCleared" }
30
+ | { type: "a2uiActionDeferred"; action: Record<string, unknown> }
31
+ | { type: "a2uiActionCleared" }
32
+ | { type: "a2uiContinuationsCleared" }
33
+ | { type: "resetThread" };
34
+
35
+ export const createAgUiThreadState = (): AgUiThreadState => ({
36
+ isRunning: false,
37
+ isLoading: false,
38
+ loadRequested: false,
39
+ historyMutationGeneration: 0,
40
+ stateSnapshot: undefined,
41
+ lastRunConfig: undefined,
42
+ pendingResume: null,
43
+ pendingA2uiResumeOwner: null,
44
+ pendingA2uiAction: undefined,
45
+ });
46
+
47
+ export const reduceAgUiThreadState = (
48
+ state: AgUiThreadState,
49
+ action: AgUiThreadAction,
50
+ ): AgUiThreadState => {
51
+ switch (action.type) {
52
+ case "runStarted":
53
+ return state.isRunning ? state : { ...state, isRunning: true };
54
+ case "runFinished":
55
+ return !state.isRunning ? state : { ...state, isRunning: false };
56
+ case "loadRequested":
57
+ return state.loadRequested ? state : { ...state, loadRequested: true };
58
+ case "loadStarted":
59
+ return state.isLoading ? state : { ...state, isLoading: true };
60
+ case "loadFinished":
61
+ return !state.isLoading ? state : { ...state, isLoading: false };
62
+ case "historyMutationAdvanced":
63
+ return {
64
+ ...state,
65
+ historyMutationGeneration: state.historyMutationGeneration + 1,
66
+ };
67
+ case "stateSnapshot":
68
+ return state.stateSnapshot === action.snapshot
69
+ ? state
70
+ : { ...state, stateSnapshot: action.snapshot };
71
+ case "resetState":
72
+ return state.stateSnapshot === undefined
73
+ ? state
74
+ : { ...state, stateSnapshot: undefined };
75
+ case "runConfigRecorded":
76
+ return state.lastRunConfig === action.runConfig
77
+ ? state
78
+ : { ...state, lastRunConfig: action.runConfig };
79
+ case "resumeDeferred":
80
+ return state.pendingResume?.owner === action.owner &&
81
+ state.pendingResume.messageId === action.messageId
82
+ ? state
83
+ : {
84
+ ...state,
85
+ pendingResume: { owner: action.owner, messageId: action.messageId },
86
+ };
87
+ case "resumeCleared":
88
+ return state.pendingResume === null
89
+ ? state
90
+ : { ...state, pendingResume: null };
91
+ case "a2uiResumeDeferred":
92
+ return state.pendingA2uiResumeOwner === action.owner
93
+ ? state
94
+ : { ...state, pendingA2uiResumeOwner: action.owner };
95
+ case "a2uiResumeCleared":
96
+ return state.pendingA2uiResumeOwner === null
97
+ ? state
98
+ : { ...state, pendingA2uiResumeOwner: null };
99
+ case "a2uiActionDeferred":
100
+ return state.pendingA2uiAction === action.action
101
+ ? state
102
+ : { ...state, pendingA2uiAction: action.action };
103
+ case "a2uiActionCleared":
104
+ return state.pendingA2uiAction === undefined
105
+ ? state
106
+ : { ...state, pendingA2uiAction: undefined };
107
+ case "a2uiContinuationsCleared":
108
+ return state.pendingA2uiResumeOwner === null &&
109
+ state.pendingA2uiAction === undefined
110
+ ? state
111
+ : {
112
+ ...state,
113
+ pendingA2uiResumeOwner: null,
114
+ pendingA2uiAction: undefined,
115
+ };
116
+ case "resetThread":
117
+ return state.stateSnapshot === undefined &&
118
+ state.pendingResume === null &&
119
+ state.pendingA2uiResumeOwner === null &&
120
+ state.pendingA2uiAction === undefined
121
+ ? state
122
+ : {
123
+ ...state,
124
+ stateSnapshot: undefined,
125
+ pendingResume: null,
126
+ pendingA2uiResumeOwner: null,
127
+ pendingA2uiAction: undefined,
128
+ };
129
+ }
130
+ };
@@ -7,6 +7,7 @@ import type {
7
7
  import type { Logger } from "./logger";
8
8
  import { parseMcpToolCallResult } from "./mcp-tool-result";
9
9
  import { withRawResponseSchema } from "./interrupt-internals";
10
+ import type { ContentPart } from "@ag-ui/client";
10
11
 
11
12
  export type ParseAgUiEventOptions = {
12
13
  logger?: Logger;
@@ -79,7 +80,19 @@ const parseRunFinishedOutcome = (
79
80
  );
80
81
  return undefined;
81
82
  }
82
- if (raw.type === "success") return { type: "success" };
83
+ if (raw.type === "success") {
84
+ return withOptional(
85
+ { type: "success" as const },
86
+ {
87
+ pendingToolCallIds:
88
+ Array.isArray(raw.pendingToolCallIds) &&
89
+ raw.pendingToolCallIds.every(isString)
90
+ ? raw.pendingToolCallIds
91
+ : undefined,
92
+ },
93
+ );
94
+ }
95
+ if (raw.type === "cancelled") return { type: "cancelled" };
83
96
  if (raw.type === "interrupt") {
84
97
  if (!Array.isArray(raw.interrupts)) {
85
98
  logRejectedEvent(
@@ -210,74 +223,50 @@ export const parseAgUiEvent = (
210
223
  subagentRunId: getString("subagentRunId"),
211
224
  },
212
225
  );
213
- case "TEXT_MESSAGE_CHUNK": {
214
- const delta = getString("delta") ?? "";
215
- return withOptional(
216
- { type: "TEXT_MESSAGE_CHUNK" as const, delta },
217
- {
218
- messageId: getString("messageId"),
219
- subagentRunId: getString("subagentRunId"),
220
- },
221
- );
222
- }
223
- case "THINKING_START":
224
- return withOptional(
225
- { type: "THINKING_START" as const },
226
- { title: getString("title"), timestamp },
227
- );
228
- case "THINKING_TEXT_MESSAGE_START":
229
- return withOptional(
230
- { type: "THINKING_TEXT_MESSAGE_START" as const },
231
- { timestamp },
232
- );
233
- case "THINKING_TEXT_MESSAGE_CONTENT": {
234
- const delta = getString("delta") ?? "";
235
- return { type: "THINKING_TEXT_MESSAGE_CONTENT", delta };
236
- }
237
- case "THINKING_TEXT_MESSAGE_END":
238
- return withOptional(
239
- { type: "THINKING_TEXT_MESSAGE_END" as const },
240
- { timestamp },
241
- );
242
- case "THINKING_END":
243
- return withOptional({ type: "THINKING_END" as const }, { timestamp });
244
- case "REASONING_START":
226
+ case "REASONING_START": {
227
+ const messageId = getString("messageId");
228
+ if (!messageId) return reject("missing messageId");
245
229
  return withOptional(
246
- { type: "REASONING_START" as const },
230
+ { type: "REASONING_START" as const, messageId },
247
231
  {
248
- messageId: getString("messageId"),
249
232
  subagentRunId: getString("subagentRunId"),
250
233
  timestamp,
251
234
  },
252
235
  );
253
- case "REASONING_MESSAGE_START":
236
+ }
237
+ case "REASONING_MESSAGE_START": {
238
+ const messageId = getString("messageId");
239
+ if (!messageId) return reject("missing messageId");
254
240
  return withOptional(
255
- { type: "REASONING_MESSAGE_START" as const },
241
+ { type: "REASONING_MESSAGE_START" as const, messageId },
256
242
  {
257
- messageId: getString("messageId"),
258
243
  subagentRunId: getString("subagentRunId"),
259
244
  timestamp,
260
245
  },
261
246
  );
247
+ }
262
248
  case "REASONING_MESSAGE_CONTENT": {
249
+ const messageId = getString("messageId");
250
+ if (!messageId) return reject("missing messageId");
263
251
  const delta = getString("delta") ?? "";
264
252
  return withOptional(
265
- { type: "REASONING_MESSAGE_CONTENT" as const, delta },
253
+ { type: "REASONING_MESSAGE_CONTENT" as const, messageId, delta },
266
254
  {
267
- messageId: getString("messageId"),
268
255
  subagentRunId: getString("subagentRunId"),
269
256
  },
270
257
  );
271
258
  }
272
- case "REASONING_MESSAGE_END":
259
+ case "REASONING_MESSAGE_END": {
260
+ const messageId = getString("messageId");
261
+ if (!messageId) return reject("missing messageId");
273
262
  return withOptional(
274
- { type: "REASONING_MESSAGE_END" as const },
263
+ { type: "REASONING_MESSAGE_END" as const, messageId },
275
264
  {
276
- messageId: getString("messageId"),
277
265
  subagentRunId: getString("subagentRunId"),
278
266
  timestamp,
279
267
  },
280
268
  );
269
+ }
281
270
  case "REASONING_ENCRYPTED_VALUE": {
282
271
  const entityId = getString("entityId");
283
272
  const encryptedValue = getString("encryptedValue");
@@ -299,15 +288,17 @@ export const parseAgUiEvent = (
299
288
  ...(subagentRunId !== undefined ? { subagentRunId } : {}),
300
289
  };
301
290
  }
302
- case "REASONING_END":
291
+ case "REASONING_END": {
292
+ const messageId = getString("messageId");
293
+ if (!messageId) return reject("missing messageId");
303
294
  return withOptional(
304
- { type: "REASONING_END" as const },
295
+ { type: "REASONING_END" as const, messageId },
305
296
  {
306
- messageId: getString("messageId"),
307
297
  subagentRunId: getString("subagentRunId"),
308
298
  timestamp,
309
299
  },
310
300
  );
301
+ }
311
302
  case "TOOL_CALL_START": {
312
303
  const toolCallId = getString("toolCallId");
313
304
  if (!toolCallId) return reject("missing toolCallId");
@@ -337,21 +328,12 @@ export const parseAgUiEvent = (
337
328
  { subagentRunId: getString("subagentRunId") },
338
329
  );
339
330
  }
340
- case "TOOL_CALL_CHUNK":
341
- return withOptional(
342
- { type: "TOOL_CALL_CHUNK" as const },
343
- {
344
- toolCallId: getString("toolCallId"),
345
- toolCallName: getString("toolCallName"),
346
- parentMessageId: getString("parentMessageId"),
347
- delta: getString("delta"),
348
- subagentRunId: getString("subagentRunId"),
349
- },
350
- );
351
331
  case "TOOL_CALL_RESULT": {
352
332
  const toolCallId = getString("toolCallId");
353
333
  if (!toolCallId) return reject("missing toolCallId");
354
- const content = getString("content") ?? "";
334
+ const content = Array.isArray(payload.content)
335
+ ? (payload.content as ContentPart[])
336
+ : (getString("content") ?? "");
355
337
  return withOptional(
356
338
  {
357
339
  type: "TOOL_CALL_RESULT" as const,
@@ -1,5 +1,6 @@
1
1
  import { isRecord } from "@assistant-ui/core/internal";
2
2
  import { isMcpAppUri } from "@assistant-ui/core";
3
+ import { contentToText, type ContentPart } from "@ag-ui/client";
3
4
 
4
5
  export type McpToolCallResult = Record<string, unknown> & {
5
6
  content: unknown[];
@@ -8,9 +9,16 @@ export type McpToolCallResult = Record<string, unknown> & {
8
9
  isError?: boolean;
9
10
  };
10
11
 
12
+ export const toolContentText = (content: string | ContentPart[]): string =>
13
+ contentToText(
14
+ Array.isArray(content)
15
+ ? (content.filter(isRecord) as ContentPart[])
16
+ : content,
17
+ );
18
+
11
19
  export const parseMcpToolCallResult = (
12
20
  source: Record<string, unknown>,
13
- modelContent: string,
21
+ content: string | ContentPart[],
14
22
  ): McpToolCallResult | undefined => {
15
23
  const structuredContent = isRecord(source.structuredContent)
16
24
  ? source.structuredContent
@@ -18,8 +26,14 @@ export const parseMcpToolCallResult = (
18
26
  const meta = isRecord(source._meta) ? source._meta : undefined;
19
27
  if (!structuredContent && !meta) return undefined;
20
28
 
29
+ const modelContent = toolContentText(content);
30
+
21
31
  return {
22
- content: modelContent ? [{ type: "text", text: modelContent }] : [],
32
+ content: Array.isArray(content)
33
+ ? content
34
+ : modelContent
35
+ ? [{ type: "text", text: modelContent }]
36
+ : [],
23
37
  ...(structuredContent ? { structuredContent } : {}),
24
38
  ...(meta ? { _meta: meta } : {}),
25
39
  ...(typeof source.isError === "boolean" ? { isError: source.isError } : {}),
@@ -11,7 +11,13 @@ import type {
11
11
  ThreadHistoryAdapter,
12
12
  ThreadMessage,
13
13
  } from "@assistant-ui/core";
14
- import type { AbstractAgent, Context, Message } from "@ag-ui/client";
14
+ import type { JoinStrategy } from "@assistant-ui/core/react";
15
+ import type {
16
+ AbstractAgent,
17
+ ContentPart,
18
+ Context,
19
+ Message,
20
+ } from "@ag-ui/client";
15
21
  import type { Logger } from "./logger";
16
22
  import type { ReadonlyJSONValue } from "assistant-stream/utils";
17
23
  import type { McpToolCallResult } from "./mcp-tool-result";
@@ -32,6 +38,7 @@ export type AgUiCustomMetadata = {
32
38
  * represent (a developer record rides as a system message). */
33
39
  role?: "developer";
34
40
  interrupts?: AgUiInterrupt[];
41
+ pendingToolCallIds?: string[];
35
42
  opaqueReasoning?: AgUiOpaqueReasoning[];
36
43
  };
37
44
 
@@ -82,6 +89,7 @@ export type UseAgUiRuntimeOptions = ExternalStoreSharedOptions & {
82
89
  agent: AbstractAgent;
83
90
  logger?: Partial<Logger>;
84
91
  showThinking?: boolean;
92
+ joinStrategy?: JoinStrategy;
85
93
  /**
86
94
  * Return context entries for each run. `messages` is the exact AG-UI
87
95
  * transcript sent in the run input, after transcript selection and message
@@ -187,8 +195,9 @@ export type AgUiRuntimeExtras = {
187
195
  };
188
196
 
189
197
  export type AgUiRunFinishedOutcome =
190
- | { type: "success" }
191
- | { type: "interrupt"; interrupts: AgUiInterrupt[] };
198
+ | { type: "success"; pendingToolCallIds?: string[] }
199
+ | { type: "interrupt"; interrupts: AgUiInterrupt[] }
200
+ | { type: "cancelled" };
192
201
 
193
202
  export type AgUiSubagentFinishedOutcome =
194
203
  | { type: "success" }
@@ -211,38 +220,27 @@ export type AgUiEvent =
211
220
  subagentRunId?: string;
212
221
  }
213
222
  | { type: "TEXT_MESSAGE_END"; messageId?: string; subagentRunId?: string }
214
- | {
215
- type: "TEXT_MESSAGE_CHUNK";
216
- messageId?: string;
217
- delta: string;
218
- subagentRunId?: string;
219
- }
220
- | { type: "THINKING_START"; title?: string; timestamp?: number }
221
- | { type: "THINKING_TEXT_MESSAGE_START"; timestamp?: number }
222
- | { type: "THINKING_TEXT_MESSAGE_CONTENT"; delta: string }
223
- | { type: "THINKING_TEXT_MESSAGE_END"; timestamp?: number }
224
- | { type: "THINKING_END"; timestamp?: number }
225
223
  | {
226
224
  type: "REASONING_START";
227
- messageId?: string;
225
+ messageId: string;
228
226
  subagentRunId?: string;
229
227
  timestamp?: number;
230
228
  }
231
229
  | {
232
230
  type: "REASONING_MESSAGE_START";
233
- messageId?: string;
231
+ messageId: string;
234
232
  subagentRunId?: string;
235
233
  timestamp?: number;
236
234
  }
237
235
  | {
238
236
  type: "REASONING_MESSAGE_CONTENT";
239
- messageId?: string;
237
+ messageId: string;
240
238
  delta: string;
241
239
  subagentRunId?: string;
242
240
  }
243
241
  | {
244
242
  type: "REASONING_MESSAGE_END";
245
- messageId?: string;
243
+ messageId: string;
246
244
  subagentRunId?: string;
247
245
  timestamp?: number;
248
246
  }
@@ -255,7 +253,7 @@ export type AgUiEvent =
255
253
  }
256
254
  | {
257
255
  type: "REASONING_END";
258
- messageId?: string;
256
+ messageId: string;
259
257
  subagentRunId?: string;
260
258
  timestamp?: number;
261
259
  }
@@ -273,19 +271,11 @@ export type AgUiEvent =
273
271
  subagentRunId?: string;
274
272
  }
275
273
  | { type: "TOOL_CALL_END"; toolCallId: string; subagentRunId?: string }
276
- | {
277
- type: "TOOL_CALL_CHUNK";
278
- toolCallId?: string;
279
- toolCallName?: string;
280
- parentMessageId?: string;
281
- delta?: string;
282
- subagentRunId?: string;
283
- }
284
274
  | {
285
275
  type: "TOOL_CALL_RESULT";
286
276
  messageId?: string;
287
277
  toolCallId: string;
288
- content: string;
278
+ content: string | ContentPart[];
289
279
  role?: "tool";
290
280
  mcpResult?: McpToolCallResult;
291
281
  subagentRunId?: string;