@assistant-ui/core 0.1.1 → 0.1.3

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 (110) hide show
  1. package/dist/model-context/frame/provider.d.ts.map +1 -1
  2. package/dist/model-context/frame/provider.js +2 -4
  3. package/dist/model-context/frame/provider.js.map +1 -1
  4. package/dist/react/RuntimeAdapter.js +4 -1
  5. package/dist/react/RuntimeAdapter.js.map +1 -1
  6. package/dist/react/index.d.ts +2 -1
  7. package/dist/react/index.d.ts.map +1 -1
  8. package/dist/react/index.js +2 -1
  9. package/dist/react/index.js.map +1 -1
  10. package/dist/react/model-context/toolbox.d.ts +7 -1
  11. package/dist/react/model-context/toolbox.d.ts.map +1 -1
  12. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +97 -0
  13. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -0
  14. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +111 -0
  15. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -0
  16. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +115 -0
  17. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -0
  18. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +444 -0
  19. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -0
  20. package/dist/react/runtimes/RuntimeAdapterProvider.d.ts +18 -0
  21. package/dist/react/runtimes/RuntimeAdapterProvider.d.ts.map +1 -0
  22. package/dist/react/runtimes/RuntimeAdapterProvider.js +14 -0
  23. package/dist/react/runtimes/RuntimeAdapterProvider.js.map +1 -0
  24. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +5 -0
  25. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -0
  26. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +528 -0
  27. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -0
  28. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts +15 -0
  29. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts.map +1 -0
  30. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js +83 -0
  31. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js.map +1 -0
  32. package/dist/react/runtimes/cloud/auiV0.d.ts +62 -0
  33. package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -0
  34. package/dist/react/runtimes/cloud/auiV0.js +74 -0
  35. package/dist/react/runtimes/cloud/auiV0.js.map +1 -0
  36. package/dist/react/runtimes/cloud/index.d.ts +4 -0
  37. package/dist/react/runtimes/cloud/index.d.ts.map +1 -0
  38. package/dist/react/runtimes/cloud/index.js +4 -0
  39. package/dist/react/runtimes/cloud/index.js.map +1 -0
  40. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts +13 -0
  41. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts.map +1 -0
  42. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +102 -0
  43. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -0
  44. package/dist/react/runtimes/createMessageConverter.d.ts +17 -0
  45. package/dist/react/runtimes/createMessageConverter.d.ts.map +1 -0
  46. package/dist/react/runtimes/createMessageConverter.js +50 -0
  47. package/dist/react/runtimes/createMessageConverter.js.map +1 -0
  48. package/dist/react/runtimes/external-message-converter.d.ts +34 -0
  49. package/dist/react/runtimes/external-message-converter.d.ts.map +1 -0
  50. package/dist/react/runtimes/external-message-converter.js +309 -0
  51. package/dist/react/runtimes/external-message-converter.js.map +1 -0
  52. package/dist/react/runtimes/index.d.ts +10 -0
  53. package/dist/react/runtimes/index.d.ts.map +1 -0
  54. package/dist/react/runtimes/index.js +10 -0
  55. package/dist/react/runtimes/index.js.map +1 -0
  56. package/dist/react/runtimes/useExternalStoreRuntime.d.ts +4 -0
  57. package/dist/react/runtimes/useExternalStoreRuntime.d.ts.map +1 -0
  58. package/dist/react/runtimes/useExternalStoreRuntime.js +19 -0
  59. package/dist/react/runtimes/useExternalStoreRuntime.js.map +1 -0
  60. package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts +4 -0
  61. package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -0
  62. package/dist/react/runtimes/useRemoteThreadListRuntime.js +48 -0
  63. package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -0
  64. package/dist/react/runtimes/useToolInvocations.d.ts +38 -0
  65. package/dist/react/runtimes/useToolInvocations.d.ts.map +1 -0
  66. package/dist/react/runtimes/useToolInvocations.js +411 -0
  67. package/dist/react/runtimes/useToolInvocations.js.map +1 -0
  68. package/dist/react/types/store-augmentation.d.ts +0 -1
  69. package/dist/react/types/store-augmentation.d.ts.map +1 -1
  70. package/dist/react/types/store-augmentation.js +1 -1
  71. package/dist/react/types/store-augmentation.js.map +1 -1
  72. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  73. package/dist/runtime/base/base-composer-runtime-core.js +2 -0
  74. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  75. package/dist/store/index.d.ts +1 -1
  76. package/dist/store/index.d.ts.map +1 -1
  77. package/dist/store/index.js +1 -2
  78. package/dist/store/index.js.map +1 -1
  79. package/dist/utils/json/is-json-equal.d.ts +2 -0
  80. package/dist/utils/json/is-json-equal.d.ts.map +1 -0
  81. package/dist/utils/json/is-json-equal.js +31 -0
  82. package/dist/utils/json/is-json-equal.js.map +1 -0
  83. package/dist/utils/json/is-json.d.ts +6 -0
  84. package/dist/utils/json/is-json.d.ts.map +1 -0
  85. package/dist/utils/json/is-json.js +33 -0
  86. package/dist/utils/json/is-json.js.map +1 -0
  87. package/package.json +10 -9
  88. package/src/model-context/frame/provider.ts +2 -6
  89. package/src/react/RuntimeAdapter.ts +5 -1
  90. package/src/react/index.ts +2 -1
  91. package/src/react/model-context/toolbox.ts +7 -3
  92. package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +176 -0
  93. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +534 -0
  94. package/src/react/runtimes/RuntimeAdapterProvider.tsx +40 -0
  95. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +785 -0
  96. package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.ts +101 -0
  97. package/src/react/runtimes/cloud/auiV0.ts +160 -0
  98. package/src/react/runtimes/cloud/index.ts +3 -0
  99. package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +152 -0
  100. package/src/react/runtimes/createMessageConverter.ts +77 -0
  101. package/src/react/runtimes/external-message-converter.ts +487 -0
  102. package/src/react/runtimes/index.ts +30 -0
  103. package/src/react/runtimes/useExternalStoreRuntime.ts +27 -0
  104. package/src/react/runtimes/useRemoteThreadListRuntime.ts +76 -0
  105. package/src/react/runtimes/useToolInvocations.ts +594 -0
  106. package/src/react/types/store-augmentation.ts +0 -2
  107. package/src/runtime/base/base-composer-runtime-core.ts +2 -0
  108. package/src/store/index.ts +1 -2
  109. package/src/utils/json/is-json-equal.ts +48 -0
  110. package/src/utils/json/is-json.ts +58 -0
@@ -0,0 +1,594 @@
1
+ declare const process: { env: { NODE_ENV?: string } };
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+ import {
5
+ createAssistantStreamController,
6
+ type ToolCallStreamController,
7
+ ToolResponse,
8
+ unstable_toolResultStream,
9
+ type Tool,
10
+ } from "assistant-stream";
11
+ import {
12
+ AssistantMetaTransformStream,
13
+ type ReadonlyJSONValue,
14
+ } from "assistant-stream/utils";
15
+ import { isJSONValueEqual } from "../../utils/json/is-json-equal";
16
+ import type { ThreadMessage } from "../../types";
17
+
18
+ export type AssistantTransportState = {
19
+ readonly messages: readonly ThreadMessage[];
20
+ readonly state?: ReadonlyJSONValue;
21
+ readonly isRunning: boolean;
22
+ };
23
+
24
+ export type AddToolResultCommand = {
25
+ readonly type: "add-tool-result";
26
+ readonly toolCallId: string;
27
+ readonly toolName: string;
28
+ readonly result: ReadonlyJSONValue;
29
+ readonly isError: boolean;
30
+ readonly artifact?: ReadonlyJSONValue;
31
+ };
32
+
33
+ const isArgsTextComplete = (argsText: string) => {
34
+ try {
35
+ JSON.parse(argsText);
36
+ return true;
37
+ } catch {
38
+ return false;
39
+ }
40
+ };
41
+
42
+ const parseArgsText = (argsText: string) => {
43
+ try {
44
+ return JSON.parse(argsText);
45
+ } catch {
46
+ return undefined;
47
+ }
48
+ };
49
+
50
+ const isEquivalentCompleteArgsText = (previous: string, next: string) => {
51
+ const previousValue = parseArgsText(previous);
52
+ const nextValue = parseArgsText(next);
53
+ if (previousValue === undefined || nextValue === undefined) return false;
54
+ return isJSONValueEqual(previousValue, nextValue);
55
+ };
56
+
57
+ type UseToolInvocationsParams = {
58
+ state: AssistantTransportState;
59
+ getTools: () => Record<string, Tool> | undefined;
60
+ onResult: (command: AddToolResultCommand) => void;
61
+ setToolStatuses: (
62
+ updater:
63
+ | Record<string, ToolExecutionStatus>
64
+ | ((
65
+ prev: Record<string, ToolExecutionStatus>,
66
+ ) => Record<string, ToolExecutionStatus>),
67
+ ) => void;
68
+ };
69
+
70
+ export type ToolExecutionStatus =
71
+ | { type: "executing" }
72
+ | { type: "interrupt"; payload: { type: "human"; payload: unknown } };
73
+
74
+ type ToolState = {
75
+ argsText: string;
76
+ hasResult: boolean;
77
+ argsComplete: boolean;
78
+ streamToolCallId: string;
79
+ controller: ToolCallStreamController;
80
+ };
81
+
82
+ export function useToolInvocations({
83
+ state,
84
+ getTools,
85
+ onResult,
86
+ setToolStatuses,
87
+ }: UseToolInvocationsParams) {
88
+ const lastToolStates = useRef<Record<string, ToolState>>({});
89
+
90
+ const humanInputRef = useRef<
91
+ Map<
92
+ string,
93
+ {
94
+ resolve: (payload: unknown) => void;
95
+ reject: (reason: unknown) => void;
96
+ }
97
+ >
98
+ >(new Map());
99
+
100
+ const acRef = useRef<AbortController>(new AbortController());
101
+ const executingCountRef = useRef(0);
102
+ const startedExecutionToolCallIdsRef = useRef<Set<string>>(new Set());
103
+ const settledResolversRef = useRef<Array<() => void>>([]);
104
+ const toolCallIdAliasesRef = useRef<Map<string, string>>(new Map());
105
+ const ignoredResultToolCallIdsRef = useRef<Set<string>>(new Set());
106
+ const rewriteCounterRef = useRef(0);
107
+
108
+ const getLogicalToolCallId = (toolCallId: string) => {
109
+ return toolCallIdAliasesRef.current.get(toolCallId) ?? toolCallId;
110
+ };
111
+
112
+ const shouldIgnoreAndCleanupResult = (toolCallId: string) => {
113
+ if (!ignoredResultToolCallIdsRef.current.has(toolCallId)) return false;
114
+ ignoredResultToolCallIdsRef.current.delete(toolCallId);
115
+ toolCallIdAliasesRef.current.delete(toolCallId);
116
+ return true;
117
+ };
118
+
119
+ const getWrappedTools = () => {
120
+ const tools = getTools();
121
+ if (!tools) return undefined;
122
+
123
+ return Object.fromEntries(
124
+ Object.entries(tools).map(([name, tool]) => {
125
+ const execute = tool.execute;
126
+ const streamCall = tool.streamCall;
127
+
128
+ const wrappedTool = {
129
+ ...tool,
130
+ ...(execute !== undefined && {
131
+ execute: (
132
+ ...[args, context]: Parameters<NonNullable<typeof execute>>
133
+ ) =>
134
+ execute(args, {
135
+ ...context,
136
+ toolCallId: getLogicalToolCallId(context.toolCallId),
137
+ }),
138
+ }),
139
+ ...(streamCall !== undefined && {
140
+ streamCall: (
141
+ ...[reader, context]: Parameters<NonNullable<typeof streamCall>>
142
+ ) =>
143
+ streamCall(reader, {
144
+ ...context,
145
+ toolCallId: getLogicalToolCallId(context.toolCallId),
146
+ }),
147
+ }),
148
+ } as Tool;
149
+ return [name, wrappedTool];
150
+ }),
151
+ ) as Record<string, Tool>;
152
+ };
153
+
154
+ const [controller] = useState(() => {
155
+ const [stream, controller] = createAssistantStreamController();
156
+ const transform = unstable_toolResultStream(
157
+ getWrappedTools,
158
+ () => acRef.current?.signal ?? new AbortController().signal,
159
+ (toolCallId: string, payload: unknown) => {
160
+ const logicalToolCallId = getLogicalToolCallId(toolCallId);
161
+ return new Promise<unknown>((resolve, reject) => {
162
+ // Reject previous human input request if it exists
163
+ const previous = humanInputRef.current.get(logicalToolCallId);
164
+ if (previous) {
165
+ previous.reject(
166
+ new Error("Human input request was superseded by a new request"),
167
+ );
168
+ }
169
+
170
+ humanInputRef.current.set(logicalToolCallId, { resolve, reject });
171
+ setToolStatuses((prev) => ({
172
+ ...prev,
173
+ [logicalToolCallId]: {
174
+ type: "interrupt",
175
+ payload: { type: "human", payload },
176
+ },
177
+ }));
178
+ });
179
+ },
180
+ {
181
+ onExecutionStart: (toolCallId: string) => {
182
+ if (ignoredResultToolCallIdsRef.current.has(toolCallId)) {
183
+ return;
184
+ }
185
+ startedExecutionToolCallIdsRef.current.add(toolCallId);
186
+ const logicalToolCallId = getLogicalToolCallId(toolCallId);
187
+ executingCountRef.current++;
188
+ setToolStatuses((prev) => ({
189
+ ...prev,
190
+ [logicalToolCallId]: { type: "executing" },
191
+ }));
192
+ },
193
+ onExecutionEnd: (toolCallId: string) => {
194
+ const wasStarted =
195
+ startedExecutionToolCallIdsRef.current.delete(toolCallId);
196
+ if (ignoredResultToolCallIdsRef.current.has(toolCallId)) {
197
+ if (wasStarted) {
198
+ executingCountRef.current--;
199
+ if (executingCountRef.current === 0) {
200
+ settledResolversRef.current.forEach((resolve) => resolve());
201
+ settledResolversRef.current = [];
202
+ }
203
+ }
204
+ return;
205
+ }
206
+ if (!wasStarted) {
207
+ return;
208
+ }
209
+ const logicalToolCallId = getLogicalToolCallId(toolCallId);
210
+ executingCountRef.current--;
211
+ setToolStatuses((prev) => {
212
+ const next = { ...prev };
213
+ delete next[logicalToolCallId];
214
+ return next;
215
+ });
216
+ // Resolve any waiting abort promises when all tools have settled
217
+ if (executingCountRef.current === 0) {
218
+ settledResolversRef.current.forEach((resolve) => resolve());
219
+ settledResolversRef.current = [];
220
+ }
221
+ },
222
+ },
223
+ );
224
+ stream
225
+ .pipeThrough(transform)
226
+ .pipeThrough(new AssistantMetaTransformStream())
227
+ .pipeTo(
228
+ new WritableStream({
229
+ write(chunk) {
230
+ if (chunk.type === "result") {
231
+ if (shouldIgnoreAndCleanupResult(chunk.meta.toolCallId)) {
232
+ return;
233
+ }
234
+
235
+ const logicalToolCallId = getLogicalToolCallId(
236
+ chunk.meta.toolCallId,
237
+ );
238
+ if (logicalToolCallId !== chunk.meta.toolCallId) {
239
+ toolCallIdAliasesRef.current.delete(chunk.meta.toolCallId);
240
+ }
241
+ // the tool call result was already set by the backend
242
+ if (lastToolStates.current[logicalToolCallId]?.hasResult) return;
243
+
244
+ onResult({
245
+ type: "add-tool-result",
246
+ toolCallId: logicalToolCallId,
247
+ toolName: chunk.meta.toolName,
248
+ result: chunk.result,
249
+ isError: chunk.isError,
250
+ ...(chunk.artifact && { artifact: chunk.artifact }),
251
+ });
252
+ }
253
+ },
254
+ }),
255
+ );
256
+
257
+ return controller;
258
+ });
259
+
260
+ const ignoredToolIds = useRef<Set<string>>(new Set());
261
+ const isInitialState = useRef(true);
262
+
263
+ useEffect(() => {
264
+ const createToolState = ({
265
+ controller,
266
+ streamToolCallId,
267
+ }: {
268
+ controller: ToolCallStreamController;
269
+ streamToolCallId: string;
270
+ }): ToolState => ({
271
+ argsText: "",
272
+ hasResult: false,
273
+ argsComplete: false,
274
+ streamToolCallId,
275
+ controller,
276
+ });
277
+
278
+ const setToolState = (toolCallId: string, state: ToolState) => {
279
+ lastToolStates.current[toolCallId] = state;
280
+ return state;
281
+ };
282
+
283
+ const patchToolState = (
284
+ toolCallId: string,
285
+ state: ToolState,
286
+ patch: Partial<ToolState>,
287
+ ) => {
288
+ return setToolState(toolCallId, { ...state, ...patch });
289
+ };
290
+
291
+ const hasExecutableTool = (toolName: string) => {
292
+ const tool = getTools()?.[toolName];
293
+ return tool?.execute !== undefined || tool?.streamCall !== undefined;
294
+ };
295
+
296
+ const shouldCloseArgsStream = ({
297
+ toolName,
298
+ argsText,
299
+ hasResult,
300
+ }: {
301
+ toolName: string;
302
+ argsText: string;
303
+ hasResult: boolean;
304
+ }) => {
305
+ if (hasResult) return true;
306
+ if (!hasExecutableTool(toolName)) {
307
+ // Non-executable tools can emit parseable snapshots mid-stream.
308
+ // Wait until the run settles before closing the args stream.
309
+ return !state.isRunning && isArgsTextComplete(argsText);
310
+ }
311
+ return isArgsTextComplete(argsText);
312
+ };
313
+
314
+ const restartToolArgsStream = ({
315
+ toolCallId,
316
+ toolName,
317
+ state,
318
+ }: {
319
+ toolCallId: string;
320
+ toolName: string;
321
+ state: ToolState;
322
+ }) => {
323
+ ignoredResultToolCallIdsRef.current.add(state.streamToolCallId);
324
+ state.controller.argsText.close();
325
+
326
+ const streamToolCallId = `${toolCallId}:rewrite:${rewriteCounterRef.current++}`;
327
+ toolCallIdAliasesRef.current.set(streamToolCallId, toolCallId);
328
+ const toolCallController = controller.addToolCallPart({
329
+ toolName,
330
+ toolCallId: streamToolCallId,
331
+ });
332
+
333
+ if (process.env.NODE_ENV !== "production") {
334
+ console.warn("started replacement stream tool call", {
335
+ toolCallId,
336
+ streamToolCallId,
337
+ });
338
+ }
339
+
340
+ return setToolState(toolCallId, {
341
+ ...createToolState({
342
+ controller: toolCallController,
343
+ streamToolCallId,
344
+ }),
345
+ hasResult: state.hasResult,
346
+ });
347
+ };
348
+
349
+ const processMessages = (
350
+ messages: readonly (typeof state.messages)[number][],
351
+ ) => {
352
+ messages.forEach((message) => {
353
+ message.content.forEach((content) => {
354
+ if (content.type === "tool-call") {
355
+ if (isInitialState.current) {
356
+ ignoredToolIds.current.add(content.toolCallId);
357
+ } else {
358
+ if (ignoredToolIds.current.has(content.toolCallId)) {
359
+ return;
360
+ }
361
+ let lastState = lastToolStates.current[content.toolCallId];
362
+ if (!lastState) {
363
+ toolCallIdAliasesRef.current.set(
364
+ content.toolCallId,
365
+ content.toolCallId,
366
+ );
367
+ const toolCallController = controller.addToolCallPart({
368
+ toolName: content.toolName,
369
+ toolCallId: content.toolCallId,
370
+ });
371
+ lastState = setToolState(
372
+ content.toolCallId,
373
+ createToolState({
374
+ controller: toolCallController,
375
+ streamToolCallId: content.toolCallId,
376
+ }),
377
+ );
378
+ }
379
+
380
+ if (content.argsText !== lastState.argsText) {
381
+ let shouldWriteArgsText = true;
382
+
383
+ if (lastState.argsComplete) {
384
+ if (
385
+ isEquivalentCompleteArgsText(
386
+ lastState.argsText,
387
+ content.argsText,
388
+ )
389
+ ) {
390
+ lastState = patchToolState(content.toolCallId, lastState, {
391
+ argsText: content.argsText,
392
+ });
393
+ shouldWriteArgsText = false;
394
+ }
395
+
396
+ if (shouldWriteArgsText) {
397
+ const canRestartClosedArgsStream =
398
+ !lastState.hasResult &&
399
+ !startedExecutionToolCallIdsRef.current.has(
400
+ lastState.streamToolCallId,
401
+ );
402
+
403
+ if (process.env.NODE_ENV !== "production") {
404
+ console.warn(
405
+ canRestartClosedArgsStream
406
+ ? "argsText updated after controller was closed, restarting tool args stream:"
407
+ : "argsText updated after controller was closed:",
408
+ {
409
+ previous: lastState.argsText,
410
+ next: content.argsText,
411
+ },
412
+ );
413
+ }
414
+
415
+ if (!canRestartClosedArgsStream) {
416
+ lastState = patchToolState(
417
+ content.toolCallId,
418
+ lastState,
419
+ {
420
+ argsText: content.argsText,
421
+ },
422
+ );
423
+ shouldWriteArgsText = false;
424
+ }
425
+ }
426
+
427
+ if (shouldWriteArgsText) {
428
+ lastState = restartToolArgsStream({
429
+ toolCallId: content.toolCallId,
430
+ toolName: content.toolName,
431
+ state: lastState,
432
+ });
433
+ }
434
+ } else if (!content.argsText.startsWith(lastState.argsText)) {
435
+ // Check if this is key reordering (both are complete JSON)
436
+ // This happens when transitioning from streaming to complete state
437
+ // and the provider returns keys in a different order
438
+ if (
439
+ isArgsTextComplete(lastState.argsText) &&
440
+ isArgsTextComplete(content.argsText) &&
441
+ isEquivalentCompleteArgsText(
442
+ lastState.argsText,
443
+ content.argsText,
444
+ )
445
+ ) {
446
+ const shouldClose = shouldCloseArgsStream({
447
+ toolName: content.toolName,
448
+ argsText: content.argsText,
449
+ hasResult: content.result !== undefined,
450
+ });
451
+ if (shouldClose) {
452
+ lastState.controller.argsText.close();
453
+ }
454
+ lastState = patchToolState(content.toolCallId, lastState, {
455
+ argsText: content.argsText,
456
+ argsComplete: shouldClose,
457
+ });
458
+ shouldWriteArgsText = false;
459
+ }
460
+ if (shouldWriteArgsText) {
461
+ if (process.env.NODE_ENV !== "production") {
462
+ console.warn(
463
+ "argsText rewrote previous snapshot, restarting tool args stream:",
464
+ {
465
+ previous: lastState.argsText,
466
+ next: content.argsText,
467
+ toolCallId: content.toolCallId,
468
+ },
469
+ );
470
+ }
471
+ lastState = restartToolArgsStream({
472
+ toolCallId: content.toolCallId,
473
+ toolName: content.toolName,
474
+ state: lastState,
475
+ });
476
+ }
477
+ }
478
+
479
+ if (shouldWriteArgsText) {
480
+ const argsTextDelta = content.argsText.slice(
481
+ lastState.argsText.length,
482
+ );
483
+ lastState.controller.argsText.append(argsTextDelta);
484
+
485
+ const shouldClose = shouldCloseArgsStream({
486
+ toolName: content.toolName,
487
+ argsText: content.argsText,
488
+ hasResult: content.result !== undefined,
489
+ });
490
+ if (shouldClose) {
491
+ lastState.controller.argsText.close();
492
+ }
493
+
494
+ lastState = patchToolState(content.toolCallId, lastState, {
495
+ argsText: content.argsText,
496
+ argsComplete: shouldClose,
497
+ });
498
+ }
499
+ }
500
+
501
+ if (!lastState.argsComplete) {
502
+ const shouldClose = shouldCloseArgsStream({
503
+ toolName: content.toolName,
504
+ argsText: content.argsText,
505
+ hasResult: content.result !== undefined,
506
+ });
507
+ if (shouldClose) {
508
+ lastState.controller.argsText.close();
509
+ lastState = patchToolState(content.toolCallId, lastState, {
510
+ argsText: content.argsText,
511
+ argsComplete: true,
512
+ });
513
+ }
514
+ }
515
+
516
+ if (content.result !== undefined && !lastState.hasResult) {
517
+ patchToolState(content.toolCallId, lastState, {
518
+ hasResult: true,
519
+ argsComplete: true,
520
+ });
521
+
522
+ lastState.controller.setResponse(
523
+ new ToolResponse({
524
+ result: content.result as ReadonlyJSONValue,
525
+ artifact: content.artifact as ReadonlyJSONValue | undefined,
526
+ isError: content.isError,
527
+ }),
528
+ );
529
+ lastState.controller.close();
530
+ }
531
+ }
532
+
533
+ // Recursively process nested messages
534
+ if (content.messages) {
535
+ processMessages(content.messages);
536
+ }
537
+ }
538
+ });
539
+ });
540
+ };
541
+
542
+ processMessages(state.messages);
543
+
544
+ if (isInitialState.current) {
545
+ isInitialState.current = false;
546
+ }
547
+ }, [state, controller, getTools]);
548
+
549
+ const abort = (): Promise<void> => {
550
+ humanInputRef.current.forEach(({ reject }) => {
551
+ reject(new Error("Tool execution aborted"));
552
+ });
553
+ humanInputRef.current.clear();
554
+
555
+ acRef.current.abort();
556
+ acRef.current = new AbortController();
557
+
558
+ // Return a promise that resolves when all executing tools have settled
559
+ if (executingCountRef.current === 0) {
560
+ return Promise.resolve();
561
+ }
562
+ return new Promise<void>((resolve) => {
563
+ settledResolversRef.current.push(resolve);
564
+ });
565
+ };
566
+
567
+ return {
568
+ reset: () => {
569
+ isInitialState.current = true;
570
+ void abort().finally(() => {
571
+ startedExecutionToolCallIdsRef.current.clear();
572
+ toolCallIdAliasesRef.current.clear();
573
+ ignoredResultToolCallIdsRef.current.clear();
574
+ rewriteCounterRef.current = 0;
575
+ });
576
+ },
577
+ abort,
578
+ resume: (toolCallId: string, payload: unknown) => {
579
+ const handlers = humanInputRef.current.get(toolCallId);
580
+ if (handlers) {
581
+ humanInputRef.current.delete(toolCallId);
582
+ setToolStatuses((prev) => ({
583
+ ...prev,
584
+ [toolCallId]: { type: "executing" },
585
+ }));
586
+ handlers.resolve(payload);
587
+ } else {
588
+ throw new Error(
589
+ `Tool call ${toolCallId} is not waiting for human input`,
590
+ );
591
+ }
592
+ },
593
+ };
594
+ }
@@ -1,5 +1,3 @@
1
- import "@assistant-ui/store";
2
-
3
1
  import type { ToolsClientSchema } from "./scopes/tools";
4
2
  import type { DataRenderersClientSchema } from "./scopes/dataRenderers";
5
3
 
@@ -151,6 +151,8 @@ export abstract class BaseComposerRuntimeCore
151
151
  }
152
152
 
153
153
  public async send() {
154
+ if (this.isEmpty) return;
155
+
154
156
  if (this._dictationSession) {
155
157
  this._dictationSession.cancel();
156
158
  this._cleanupDictation();
@@ -1,5 +1,4 @@
1
- // scope registration (module augmentation side effect)
2
- import "./scope-registration";
1
+ /// <reference path="./scope-registration.ts" />
3
2
 
4
3
  // scopes
5
4
  export * from "./scopes";
@@ -0,0 +1,48 @@
1
+ import type { ReadonlyJSONValue } from "assistant-stream/utils";
2
+ import { isJSONValue, isRecord } from "./is-json";
3
+
4
+ const MAX_JSON_DEPTH = 100;
5
+
6
+ const isJSONValueEqualAtDepth = (
7
+ a: ReadonlyJSONValue,
8
+ b: ReadonlyJSONValue,
9
+ currentDepth: number,
10
+ ): boolean => {
11
+ if (a === b) return true;
12
+ if (currentDepth > MAX_JSON_DEPTH) return false;
13
+
14
+ if (a == null || b == null) return false;
15
+
16
+ if (Array.isArray(a)) {
17
+ if (!Array.isArray(b) || a.length !== b.length) return false;
18
+ return a.every((item, index) =>
19
+ isJSONValueEqualAtDepth(
20
+ item,
21
+ b[index] as ReadonlyJSONValue,
22
+ currentDepth + 1,
23
+ ),
24
+ );
25
+ }
26
+
27
+ if (Array.isArray(b)) return false;
28
+ if (!isRecord(a) || !isRecord(b)) return false;
29
+
30
+ const aKeys = Object.keys(a);
31
+ const bKeys = Object.keys(b);
32
+ if (aKeys.length !== bKeys.length) return false;
33
+
34
+ return aKeys.every(
35
+ (key) =>
36
+ Object.hasOwn(b, key) &&
37
+ isJSONValueEqualAtDepth(
38
+ a[key] as ReadonlyJSONValue,
39
+ b[key] as ReadonlyJSONValue,
40
+ currentDepth + 1,
41
+ ),
42
+ );
43
+ };
44
+
45
+ export const isJSONValueEqual = (a: unknown, b: unknown): boolean => {
46
+ if (!isJSONValue(a) || !isJSONValue(b)) return false;
47
+ return isJSONValueEqualAtDepth(a, b, 0);
48
+ };