@assistant-ui/react-ag-ui 0.0.71 → 0.1.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 (70) hide show
  1. package/README.md +1 -1
  2. package/dist/agUiExtras.js +1 -2
  3. package/dist/hooks.js +1 -2
  4. package/dist/runtime/{AgUiThreadRuntimeCore.d.ts → AgUiThreadController.d.ts} +6 -12
  5. package/dist/runtime/AgUiThreadController.d.ts.map +1 -0
  6. package/dist/runtime/{AgUiThreadRuntimeCore.js → AgUiThreadController.js} +567 -194
  7. package/dist/runtime/AgUiThreadController.js.map +1 -0
  8. package/dist/runtime/adapter/conversions.d.ts +4 -4
  9. package/dist/runtime/adapter/conversions.d.ts.map +1 -1
  10. package/dist/runtime/adapter/conversions.js +120 -35
  11. package/dist/runtime/adapter/conversions.js.map +1 -1
  12. package/dist/runtime/adapter/replay-content.d.ts +9 -0
  13. package/dist/runtime/adapter/replay-content.d.ts.map +1 -0
  14. package/dist/runtime/adapter/replay-content.js +185 -0
  15. package/dist/runtime/adapter/replay-content.js.map +1 -0
  16. package/dist/runtime/adapter/run-aggregator.d.ts +3 -5
  17. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  18. package/dist/runtime/adapter/run-aggregator.js +48 -63
  19. package/dist/runtime/adapter/run-aggregator.js.map +1 -1
  20. package/dist/runtime/adapter/subscriber.d.ts +0 -24
  21. package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
  22. package/dist/runtime/adapter/subscriber.js +2 -24
  23. package/dist/runtime/adapter/subscriber.js.map +1 -1
  24. package/dist/runtime/adapter/tool-approval.d.ts +2 -0
  25. package/dist/runtime/adapter/tool-approval.d.ts.map +1 -1
  26. package/dist/runtime/adapter/tool-approval.js +23 -6
  27. package/dist/runtime/adapter/tool-approval.js.map +1 -1
  28. package/dist/runtime/agUiThreadState.d.ts +60 -0
  29. package/dist/runtime/agUiThreadState.d.ts.map +1 -0
  30. package/dist/runtime/agUiThreadState.js +94 -0
  31. package/dist/runtime/agUiThreadState.js.map +1 -0
  32. package/dist/runtime/event-parser.d.ts.map +1 -1
  33. package/dist/runtime/event-parser.js +46 -50
  34. package/dist/runtime/event-parser.js.map +1 -1
  35. package/dist/runtime/interrupt-internals.js +1 -2
  36. package/dist/runtime/logger.js +1 -2
  37. package/dist/runtime/mcp-tool-result.d.ts +3 -1
  38. package/dist/runtime/mcp-tool-result.d.ts.map +1 -1
  39. package/dist/runtime/mcp-tool-result.js +7 -5
  40. package/dist/runtime/mcp-tool-result.js.map +1 -1
  41. package/dist/runtime/types.d.ts +11 -35
  42. package/dist/runtime/types.d.ts.map +1 -1
  43. package/dist/runtime/types.js +1 -2
  44. package/dist/runtime/types.js.map +1 -1
  45. package/dist/useAgUiRuntime.js +3 -4
  46. package/dist/useAgUiRuntime.js.map +1 -1
  47. package/package.json +12 -11
  48. package/src/runtime/AgUiThreadController.deferred-results.test.ts +548 -0
  49. package/src/runtime/{AgUiThreadRuntimeCore.interrupt-cancel.test.ts → AgUiThreadController.interrupt-cancel.test.ts} +4 -4
  50. package/src/runtime/AgUiThreadController.replay.test.ts +3932 -0
  51. package/src/runtime/{AgUiThreadRuntimeCore.test.ts → AgUiThreadController.test.ts} +114 -6
  52. package/src/runtime/{AgUiThreadRuntimeCore.ts → AgUiThreadController.ts} +1037 -236
  53. package/src/runtime/adapter/conversions.content.test.ts +149 -5
  54. package/src/runtime/adapter/conversions.tools.test.ts +2 -1
  55. package/src/runtime/adapter/conversions.ts +249 -62
  56. package/src/runtime/adapter/replay-content.test.ts +235 -0
  57. package/src/runtime/adapter/replay-content.ts +306 -0
  58. package/src/runtime/adapter/run-aggregator.ts +79 -114
  59. package/src/runtime/adapter/subscriber.ts +1 -44
  60. package/src/runtime/adapter/tool-approval.ts +41 -3
  61. package/src/runtime/agUiThreadState.test.ts +224 -0
  62. package/src/runtime/agUiThreadState.ts +139 -0
  63. package/src/runtime/event-parser.ts +40 -58
  64. package/src/runtime/mcp-tool-result.ts +16 -2
  65. package/src/runtime/types.ts +16 -28
  66. package/src/useAgUiRuntime.fast-refresh.test.tsx +3 -3
  67. package/src/useAgUiRuntime.thread-switch.test.tsx +4 -4
  68. package/src/useAgUiRuntime.ts +3 -3
  69. package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +0 -1
  70. package/dist/runtime/AgUiThreadRuntimeCore.js.map +0 -1
@@ -0,0 +1,224 @@
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({
119
+ owner,
120
+ messageIds: ["assistant"],
121
+ });
122
+ expect(
123
+ reduceAgUiThreadState(withResume, {
124
+ type: "resumeDeferred",
125
+ owner,
126
+ messageId: "assistant",
127
+ }),
128
+ ).toBe(withResume);
129
+ const withTwoOwners = reduceAgUiThreadState(withResume, {
130
+ type: "resumeDeferred",
131
+ owner,
132
+ messageId: "assistant-next",
133
+ });
134
+ expect(withTwoOwners.pendingResume).toEqual({
135
+ owner,
136
+ messageIds: ["assistant", "assistant-next"],
137
+ });
138
+ expect(
139
+ reduceAgUiThreadState(withTwoOwners, {
140
+ type: "resumeDeferred",
141
+ owner: new AbortController(),
142
+ messageId: "new-run",
143
+ }).pendingResume?.messageIds,
144
+ ).toEqual(["new-run"]);
145
+ const withOwner = reduceAgUiThreadState(withResume, {
146
+ type: "a2uiResumeDeferred",
147
+ owner,
148
+ });
149
+ const action = { value: "submit" };
150
+ const withAction = reduceAgUiThreadState(withOwner, {
151
+ type: "a2uiActionDeferred",
152
+ action,
153
+ });
154
+ expect(withAction.pendingA2uiResumeOwner).toBe(owner);
155
+ expect(withAction.pendingA2uiAction).toBe(action);
156
+ expect(
157
+ reduceAgUiThreadState(withOwner, { type: "a2uiResumeDeferred", owner }),
158
+ ).toBe(withOwner);
159
+ expect(
160
+ reduceAgUiThreadState(withAction, { type: "a2uiActionDeferred", action }),
161
+ ).toBe(withAction);
162
+
163
+ const actionCleared = reduceAgUiThreadState(withAction, {
164
+ type: "a2uiActionCleared",
165
+ });
166
+ expect(actionCleared.pendingA2uiAction).toBeUndefined();
167
+ expect(actionCleared.pendingA2uiResumeOwner).toBe(owner);
168
+ const ownerCleared = reduceAgUiThreadState(actionCleared, {
169
+ type: "a2uiResumeCleared",
170
+ });
171
+ expect(ownerCleared.pendingA2uiResumeOwner).toBeNull();
172
+ expect(
173
+ reduceAgUiThreadState(ownerCleared, { type: "a2uiResumeCleared" }),
174
+ ).toBe(ownerCleared);
175
+ expect(
176
+ reduceAgUiThreadState(ownerCleared, { type: "a2uiActionCleared" }),
177
+ ).toBe(ownerCleared);
178
+ const resumeCleared = reduceAgUiThreadState(ownerCleared, {
179
+ type: "resumeCleared",
180
+ });
181
+ expect(resumeCleared.pendingResume).toBeNull();
182
+ expect(
183
+ reduceAgUiThreadState(resumeCleared, { type: "resumeCleared" }),
184
+ ).toBe(resumeCleared);
185
+ });
186
+
187
+ it("clears A2UI continuations and resets thread state without changing run flags", () => {
188
+ const owner = new AbortController();
189
+ let state = reduceAgUiThreadState(createAgUiThreadState(), {
190
+ type: "runStarted",
191
+ });
192
+ state = reduceAgUiThreadState(state, { type: "loadStarted" });
193
+ state = reduceAgUiThreadState(state, {
194
+ type: "resumeDeferred",
195
+ owner,
196
+ messageId: "assistant",
197
+ });
198
+ state = reduceAgUiThreadState(state, {
199
+ type: "a2uiResumeDeferred",
200
+ owner,
201
+ });
202
+ state = reduceAgUiThreadState(state, {
203
+ type: "a2uiActionDeferred",
204
+ action: { value: "submit" },
205
+ });
206
+ const cleared = reduceAgUiThreadState(state, {
207
+ type: "a2uiContinuationsCleared",
208
+ });
209
+ expect(cleared.pendingA2uiResumeOwner).toBeNull();
210
+ expect(cleared.pendingA2uiAction).toBeUndefined();
211
+ expect(cleared.pendingResume).toEqual({ owner, messageIds: ["assistant"] });
212
+ expect(
213
+ reduceAgUiThreadState(cleared, { type: "a2uiContinuationsCleared" }),
214
+ ).toBe(cleared);
215
+
216
+ const reset = reduceAgUiThreadState(state, { type: "resetThread" });
217
+ expect(reset).toMatchObject({ isRunning: true, isLoading: true });
218
+ expect(reset.stateSnapshot).toBeUndefined();
219
+ expect(reset.pendingResume).toBeNull();
220
+ expect(reset.pendingA2uiResumeOwner).toBeNull();
221
+ expect(reset.pendingA2uiAction).toBeUndefined();
222
+ expect(reduceAgUiThreadState(reset, { type: "resetThread" })).toBe(reset);
223
+ });
224
+ });
@@ -0,0 +1,139 @@
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: {
12
+ owner: AbortController;
13
+ messageIds: readonly string[];
14
+ } | null;
15
+ pendingA2uiResumeOwner: AbortController | null;
16
+ pendingA2uiAction: Record<string, unknown> | undefined;
17
+ };
18
+
19
+ export type AgUiThreadAction =
20
+ | { type: "runStarted" }
21
+ | { type: "runFinished" }
22
+ | { type: "loadRequested" }
23
+ | { type: "loadStarted" }
24
+ | { type: "loadFinished" }
25
+ | { type: "historyMutationAdvanced" }
26
+ | { type: "stateSnapshot"; snapshot: ReadonlyJSONValue }
27
+ | { type: "resetState" }
28
+ | { type: "runConfigRecorded"; runConfig: RunConfig }
29
+ | { type: "resumeDeferred"; owner: AbortController; messageId: string }
30
+ | { type: "resumeCleared" }
31
+ | { type: "a2uiResumeDeferred"; owner: AbortController }
32
+ | { type: "a2uiResumeCleared" }
33
+ | { type: "a2uiActionDeferred"; action: Record<string, unknown> }
34
+ | { type: "a2uiActionCleared" }
35
+ | { type: "a2uiContinuationsCleared" }
36
+ | { type: "resetThread" };
37
+
38
+ export const createAgUiThreadState = (): AgUiThreadState => ({
39
+ isRunning: false,
40
+ isLoading: false,
41
+ loadRequested: false,
42
+ historyMutationGeneration: 0,
43
+ stateSnapshot: undefined,
44
+ lastRunConfig: undefined,
45
+ pendingResume: null,
46
+ pendingA2uiResumeOwner: null,
47
+ pendingA2uiAction: undefined,
48
+ });
49
+
50
+ export const reduceAgUiThreadState = (
51
+ state: AgUiThreadState,
52
+ action: AgUiThreadAction,
53
+ ): AgUiThreadState => {
54
+ switch (action.type) {
55
+ case "runStarted":
56
+ return state.isRunning ? state : { ...state, isRunning: true };
57
+ case "runFinished":
58
+ return !state.isRunning ? state : { ...state, isRunning: false };
59
+ case "loadRequested":
60
+ return state.loadRequested ? state : { ...state, loadRequested: true };
61
+ case "loadStarted":
62
+ return state.isLoading ? state : { ...state, isLoading: true };
63
+ case "loadFinished":
64
+ return !state.isLoading ? state : { ...state, isLoading: false };
65
+ case "historyMutationAdvanced":
66
+ return {
67
+ ...state,
68
+ historyMutationGeneration: state.historyMutationGeneration + 1,
69
+ };
70
+ case "stateSnapshot":
71
+ return state.stateSnapshot === action.snapshot
72
+ ? state
73
+ : { ...state, stateSnapshot: action.snapshot };
74
+ case "resetState":
75
+ return state.stateSnapshot === undefined
76
+ ? state
77
+ : { ...state, stateSnapshot: undefined };
78
+ case "runConfigRecorded":
79
+ return state.lastRunConfig === action.runConfig
80
+ ? state
81
+ : { ...state, lastRunConfig: action.runConfig };
82
+ case "resumeDeferred":
83
+ return state.pendingResume?.owner === action.owner &&
84
+ state.pendingResume.messageIds.includes(action.messageId)
85
+ ? state
86
+ : {
87
+ ...state,
88
+ pendingResume: {
89
+ owner: action.owner,
90
+ messageIds:
91
+ state.pendingResume?.owner === action.owner
92
+ ? [...state.pendingResume.messageIds, action.messageId]
93
+ : [action.messageId],
94
+ },
95
+ };
96
+ case "resumeCleared":
97
+ return state.pendingResume === null
98
+ ? state
99
+ : { ...state, pendingResume: null };
100
+ case "a2uiResumeDeferred":
101
+ return state.pendingA2uiResumeOwner === action.owner
102
+ ? state
103
+ : { ...state, pendingA2uiResumeOwner: action.owner };
104
+ case "a2uiResumeCleared":
105
+ return state.pendingA2uiResumeOwner === null
106
+ ? state
107
+ : { ...state, pendingA2uiResumeOwner: null };
108
+ case "a2uiActionDeferred":
109
+ return state.pendingA2uiAction === action.action
110
+ ? state
111
+ : { ...state, pendingA2uiAction: action.action };
112
+ case "a2uiActionCleared":
113
+ return state.pendingA2uiAction === undefined
114
+ ? state
115
+ : { ...state, pendingA2uiAction: undefined };
116
+ case "a2uiContinuationsCleared":
117
+ return state.pendingA2uiResumeOwner === null &&
118
+ state.pendingA2uiAction === undefined
119
+ ? state
120
+ : {
121
+ ...state,
122
+ pendingA2uiResumeOwner: null,
123
+ pendingA2uiAction: undefined,
124
+ };
125
+ case "resetThread":
126
+ return state.stateSnapshot === undefined &&
127
+ state.pendingResume === null &&
128
+ state.pendingA2uiResumeOwner === null &&
129
+ state.pendingA2uiAction === undefined
130
+ ? state
131
+ : {
132
+ ...state,
133
+ stateSnapshot: undefined,
134
+ pendingResume: null,
135
+ pendingA2uiResumeOwner: null,
136
+ pendingA2uiAction: undefined,
137
+ };
138
+ }
139
+ };
@@ -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 } : {}),
@@ -12,7 +12,12 @@ import type {
12
12
  ThreadMessage,
13
13
  } from "@assistant-ui/core";
14
14
  import type { JoinStrategy } from "@assistant-ui/core/react";
15
- import type { AbstractAgent, Context, Message } from "@ag-ui/client";
15
+ import type {
16
+ AbstractAgent,
17
+ ContentPart,
18
+ Context,
19
+ Message,
20
+ } from "@ag-ui/client";
16
21
  import type { Logger } from "./logger";
17
22
  import type { ReadonlyJSONValue } from "assistant-stream/utils";
18
23
  import type { McpToolCallResult } from "./mcp-tool-result";
@@ -33,6 +38,7 @@ export type AgUiCustomMetadata = {
33
38
  * represent (a developer record rides as a system message). */
34
39
  role?: "developer";
35
40
  interrupts?: AgUiInterrupt[];
41
+ pendingToolCallIds?: string[];
36
42
  opaqueReasoning?: AgUiOpaqueReasoning[];
37
43
  };
38
44
 
@@ -189,8 +195,9 @@ export type AgUiRuntimeExtras = {
189
195
  };
190
196
 
191
197
  export type AgUiRunFinishedOutcome =
192
- | { type: "success" }
193
- | { type: "interrupt"; interrupts: AgUiInterrupt[] };
198
+ | { type: "success"; pendingToolCallIds?: string[] }
199
+ | { type: "interrupt"; interrupts: AgUiInterrupt[] }
200
+ | { type: "cancelled" };
194
201
 
195
202
  export type AgUiSubagentFinishedOutcome =
196
203
  | { type: "success" }
@@ -213,38 +220,27 @@ export type AgUiEvent =
213
220
  subagentRunId?: string;
214
221
  }
215
222
  | { type: "TEXT_MESSAGE_END"; messageId?: string; subagentRunId?: string }
216
- | {
217
- type: "TEXT_MESSAGE_CHUNK";
218
- messageId?: string;
219
- delta: string;
220
- subagentRunId?: string;
221
- }
222
- | { type: "THINKING_START"; title?: string; timestamp?: number }
223
- | { type: "THINKING_TEXT_MESSAGE_START"; timestamp?: number }
224
- | { type: "THINKING_TEXT_MESSAGE_CONTENT"; delta: string }
225
- | { type: "THINKING_TEXT_MESSAGE_END"; timestamp?: number }
226
- | { type: "THINKING_END"; timestamp?: number }
227
223
  | {
228
224
  type: "REASONING_START";
229
- messageId?: string;
225
+ messageId: string;
230
226
  subagentRunId?: string;
231
227
  timestamp?: number;
232
228
  }
233
229
  | {
234
230
  type: "REASONING_MESSAGE_START";
235
- messageId?: string;
231
+ messageId: string;
236
232
  subagentRunId?: string;
237
233
  timestamp?: number;
238
234
  }
239
235
  | {
240
236
  type: "REASONING_MESSAGE_CONTENT";
241
- messageId?: string;
237
+ messageId: string;
242
238
  delta: string;
243
239
  subagentRunId?: string;
244
240
  }
245
241
  | {
246
242
  type: "REASONING_MESSAGE_END";
247
- messageId?: string;
243
+ messageId: string;
248
244
  subagentRunId?: string;
249
245
  timestamp?: number;
250
246
  }
@@ -257,7 +253,7 @@ export type AgUiEvent =
257
253
  }
258
254
  | {
259
255
  type: "REASONING_END";
260
- messageId?: string;
256
+ messageId: string;
261
257
  subagentRunId?: string;
262
258
  timestamp?: number;
263
259
  }
@@ -275,19 +271,11 @@ export type AgUiEvent =
275
271
  subagentRunId?: string;
276
272
  }
277
273
  | { type: "TOOL_CALL_END"; toolCallId: string; subagentRunId?: string }
278
- | {
279
- type: "TOOL_CALL_CHUNK";
280
- toolCallId?: string;
281
- toolCallName?: string;
282
- parentMessageId?: string;
283
- delta?: string;
284
- subagentRunId?: string;
285
- }
286
274
  | {
287
275
  type: "TOOL_CALL_RESULT";
288
276
  messageId?: string;
289
277
  toolCallId: string;
290
- content: string;
278
+ content: string | ContentPart[];
291
279
  role?: "tool";
292
280
  mcpResult?: McpToolCallResult;
293
281
  subagentRunId?: string;
@@ -7,7 +7,7 @@ import type {
7
7
  AssistantRuntime,
8
8
  MessageQueueController,
9
9
  } from "@assistant-ui/core";
10
- import { AgUiThreadRuntimeCore } from "./runtime/AgUiThreadRuntimeCore";
10
+ import { AgUiThreadController } from "./runtime/AgUiThreadController";
11
11
  import type { UseAgUiRuntimeOptions } from "./runtime/types";
12
12
  import { useAgUiRuntime, type AgUiAssistantRuntime } from "./useAgUiRuntime";
13
13
 
@@ -230,8 +230,8 @@ const expectInFlightRunAcrossRefresh = async (
230
230
  runAgent: vi.fn(() => run),
231
231
  abortRun: vi.fn(),
232
232
  } as unknown as HttpAgent;
233
- const attach = vi.spyOn(AgUiThreadRuntimeCore.prototype, "attachRuntime");
234
- const detach = vi.spyOn(AgUiThreadRuntimeCore.prototype, "detachRuntime");
233
+ const attach = vi.spyOn(AgUiThreadController.prototype, "attachRuntime");
234
+ const detach = vi.spyOn(AgUiThreadController.prototype, "detachRuntime");
235
235
  options = { agent };
236
236
  const { Before, After } = createProbe();
237
237
  const Shell: ComponentType<{ mode: "visible" | "hidden" }> = inActivity