@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.
- package/dist/model-context/frame/provider.d.ts.map +1 -1
- package/dist/model-context/frame/provider.js +2 -4
- package/dist/model-context/frame/provider.js.map +1 -1
- package/dist/react/RuntimeAdapter.js +4 -1
- package/dist/react/RuntimeAdapter.js.map +1 -1
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +2 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react/model-context/toolbox.d.ts +7 -1
- package/dist/react/model-context/toolbox.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +97 -0
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -0
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +111 -0
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -0
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +115 -0
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -0
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +444 -0
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -0
- package/dist/react/runtimes/RuntimeAdapterProvider.d.ts +18 -0
- package/dist/react/runtimes/RuntimeAdapterProvider.d.ts.map +1 -0
- package/dist/react/runtimes/RuntimeAdapterProvider.js +14 -0
- package/dist/react/runtimes/RuntimeAdapterProvider.js.map +1 -0
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +5 -0
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -0
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +528 -0
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -0
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts +15 -0
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts.map +1 -0
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js +83 -0
- package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js.map +1 -0
- package/dist/react/runtimes/cloud/auiV0.d.ts +62 -0
- package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -0
- package/dist/react/runtimes/cloud/auiV0.js +74 -0
- package/dist/react/runtimes/cloud/auiV0.js.map +1 -0
- package/dist/react/runtimes/cloud/index.d.ts +4 -0
- package/dist/react/runtimes/cloud/index.d.ts.map +1 -0
- package/dist/react/runtimes/cloud/index.js +4 -0
- package/dist/react/runtimes/cloud/index.js.map +1 -0
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts +13 -0
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts.map +1 -0
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +102 -0
- package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -0
- package/dist/react/runtimes/createMessageConverter.d.ts +17 -0
- package/dist/react/runtimes/createMessageConverter.d.ts.map +1 -0
- package/dist/react/runtimes/createMessageConverter.js +50 -0
- package/dist/react/runtimes/createMessageConverter.js.map +1 -0
- package/dist/react/runtimes/external-message-converter.d.ts +34 -0
- package/dist/react/runtimes/external-message-converter.d.ts.map +1 -0
- package/dist/react/runtimes/external-message-converter.js +309 -0
- package/dist/react/runtimes/external-message-converter.js.map +1 -0
- package/dist/react/runtimes/index.d.ts +10 -0
- package/dist/react/runtimes/index.d.ts.map +1 -0
- package/dist/react/runtimes/index.js +10 -0
- package/dist/react/runtimes/index.js.map +1 -0
- package/dist/react/runtimes/useExternalStoreRuntime.d.ts +4 -0
- package/dist/react/runtimes/useExternalStoreRuntime.d.ts.map +1 -0
- package/dist/react/runtimes/useExternalStoreRuntime.js +19 -0
- package/dist/react/runtimes/useExternalStoreRuntime.js.map +1 -0
- package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts +4 -0
- package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -0
- package/dist/react/runtimes/useRemoteThreadListRuntime.js +48 -0
- package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -0
- package/dist/react/runtimes/useToolInvocations.d.ts +38 -0
- package/dist/react/runtimes/useToolInvocations.d.ts.map +1 -0
- package/dist/react/runtimes/useToolInvocations.js +411 -0
- package/dist/react/runtimes/useToolInvocations.js.map +1 -0
- package/dist/react/types/store-augmentation.d.ts +0 -1
- package/dist/react/types/store-augmentation.d.ts.map +1 -1
- package/dist/react/types/store-augmentation.js +1 -1
- package/dist/react/types/store-augmentation.js.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.js +2 -0
- package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
- package/dist/store/index.d.ts +1 -1
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +1 -2
- package/dist/store/index.js.map +1 -1
- package/dist/utils/json/is-json-equal.d.ts +2 -0
- package/dist/utils/json/is-json-equal.d.ts.map +1 -0
- package/dist/utils/json/is-json-equal.js +31 -0
- package/dist/utils/json/is-json-equal.js.map +1 -0
- package/dist/utils/json/is-json.d.ts +6 -0
- package/dist/utils/json/is-json.d.ts.map +1 -0
- package/dist/utils/json/is-json.js +33 -0
- package/dist/utils/json/is-json.js.map +1 -0
- package/package.json +10 -9
- package/src/model-context/frame/provider.ts +2 -6
- package/src/react/RuntimeAdapter.ts +5 -1
- package/src/react/index.ts +2 -1
- package/src/react/model-context/toolbox.ts +7 -3
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +176 -0
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +534 -0
- package/src/react/runtimes/RuntimeAdapterProvider.tsx +40 -0
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +785 -0
- package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.ts +101 -0
- package/src/react/runtimes/cloud/auiV0.ts +160 -0
- package/src/react/runtimes/cloud/index.ts +3 -0
- package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +152 -0
- package/src/react/runtimes/createMessageConverter.ts +77 -0
- package/src/react/runtimes/external-message-converter.ts +487 -0
- package/src/react/runtimes/index.ts +30 -0
- package/src/react/runtimes/useExternalStoreRuntime.ts +27 -0
- package/src/react/runtimes/useRemoteThreadListRuntime.ts +76 -0
- package/src/react/runtimes/useToolInvocations.ts +594 -0
- package/src/react/types/store-augmentation.ts +0 -2
- package/src/runtime/base/base-composer-runtime-core.ts +2 -0
- package/src/store/index.ts +1 -2
- package/src/utils/json/is-json-equal.ts +48 -0
- 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
|
+
}
|
package/src/store/index.ts
CHANGED
|
@@ -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
|
+
};
|