@assistant-ui/core 0.2.2 → 0.2.4
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/tool.d.ts +25 -0
- package/dist/model-context/tool.d.ts.map +1 -1
- package/dist/model-context/tool.js +25 -0
- package/dist/model-context/tool.js.map +1 -1
- package/dist/react/AssistantRuntimeProvider.d.ts +33 -0
- package/dist/react/AssistantRuntimeProvider.d.ts.map +1 -1
- package/dist/react/AssistantRuntimeProvider.js +22 -0
- package/dist/react/AssistantRuntimeProvider.js.map +1 -1
- package/dist/react/client/DataRenderers.d.ts +7 -0
- package/dist/react/client/DataRenderers.d.ts.map +1 -1
- package/dist/react/client/DataRenderers.js +7 -0
- package/dist/react/client/DataRenderers.js.map +1 -1
- package/dist/react/client/Tools.d.ts +12 -0
- package/dist/react/client/Tools.d.ts.map +1 -1
- package/dist/react/client/Tools.js +8 -0
- package/dist/react/client/Tools.js.map +1 -1
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/model-context/makeAssistantDataUI.d.ts +13 -0
- package/dist/react/model-context/makeAssistantDataUI.d.ts.map +1 -1
- package/dist/react/model-context/makeAssistantDataUI.js +6 -0
- package/dist/react/model-context/makeAssistantDataUI.js.map +1 -1
- package/dist/react/model-context/makeAssistantTool.d.ts +15 -0
- package/dist/react/model-context/makeAssistantTool.d.ts.map +1 -1
- package/dist/react/model-context/makeAssistantTool.js +8 -0
- package/dist/react/model-context/makeAssistantTool.js.map +1 -1
- package/dist/react/model-context/makeAssistantToolUI.d.ts +15 -0
- package/dist/react/model-context/makeAssistantToolUI.d.ts.map +1 -1
- package/dist/react/model-context/makeAssistantToolUI.js +8 -0
- package/dist/react/model-context/makeAssistantToolUI.js.map +1 -1
- package/dist/react/model-context/toolbox.d.ts +29 -0
- package/dist/react/model-context/toolbox.d.ts.map +1 -1
- package/dist/react/model-context/useAssistantDataUI.d.ts +9 -0
- package/dist/react/model-context/useAssistantDataUI.d.ts.map +1 -1
- package/dist/react/model-context/useAssistantDataUI.js +6 -0
- package/dist/react/model-context/useAssistantDataUI.js.map +1 -1
- package/dist/react/model-context/useAssistantTool.d.ts +34 -0
- package/dist/react/model-context/useAssistantTool.d.ts.map +1 -1
- package/dist/react/model-context/useAssistantTool.js +30 -0
- package/dist/react/model-context/useAssistantTool.js.map +1 -1
- package/dist/react/model-context/useAssistantToolUI.d.ts +12 -0
- package/dist/react/model-context/useAssistantToolUI.d.ts.map +1 -1
- package/dist/react/model-context/useAssistantToolUI.js +9 -0
- package/dist/react/model-context/useAssistantToolUI.js.map +1 -1
- package/dist/react/model-context/useToolArgsStatus.d.ts +29 -0
- package/dist/react/model-context/useToolArgsStatus.d.ts.map +1 -1
- package/dist/react/model-context/useToolArgsStatus.js +24 -0
- package/dist/react/model-context/useToolArgsStatus.js.map +1 -1
- package/dist/react/primitive-hooks/useActionBarCopy.d.ts.map +1 -1
- package/dist/react/primitive-hooks/useActionBarCopy.js +4 -3
- package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageGroupedParts.js +2 -1
- package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
- package/dist/react/primitives/message/MessageParts.js +8 -2
- package/dist/react/primitives/message/MessageParts.js.map +1 -1
- package/dist/react/primitives/messagePart/MessagePartInProgress.d.ts +6 -0
- package/dist/react/primitives/messagePart/MessagePartInProgress.d.ts.map +1 -0
- package/dist/react/primitives/messagePart/MessagePartInProgress.js +7 -0
- package/dist/react/primitives/messagePart/MessagePartInProgress.js.map +1 -0
- package/dist/react/runtimes/useToolInvocations.d.ts +9 -0
- package/dist/react/runtimes/useToolInvocations.d.ts.map +1 -1
- package/dist/react/runtimes/useToolInvocations.js +318 -264
- package/dist/react/runtimes/useToolInvocations.js.map +1 -1
- package/dist/react/types/MessagePartComponentTypes.d.ts +11 -0
- package/dist/react/types/MessagePartComponentTypes.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +1 -0
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js +11 -0
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/store/clients/model-context-client.d.ts.map +1 -1
- package/dist/store/clients/model-context-client.js +24 -4
- package/dist/store/clients/model-context-client.js.map +1 -1
- package/dist/store/scopes/model-context.d.ts +4 -1
- package/dist/store/scopes/model-context.d.ts.map +1 -1
- package/dist/types/message.d.ts +22 -0
- package/dist/types/message.d.ts.map +1 -1
- package/package.json +10 -9
- package/src/model-context/tool.ts +25 -0
- package/src/react/AssistantRuntimeProvider.tsx +33 -0
- package/src/react/client/DataRenderers.ts +7 -0
- package/src/react/client/Tools.ts +10 -0
- package/src/react/index.ts +1 -0
- package/src/react/model-context/makeAssistantDataUI.ts +13 -0
- package/src/react/model-context/makeAssistantTool.ts +15 -0
- package/src/react/model-context/makeAssistantToolUI.ts +15 -0
- package/src/react/model-context/toolbox.ts +32 -1
- package/src/react/model-context/useAssistantDataUI.ts +9 -0
- package/src/react/model-context/useAssistantTool.ts +34 -0
- package/src/react/model-context/useAssistantToolUI.ts +12 -0
- package/src/react/model-context/useToolArgsStatus.ts +29 -0
- package/src/react/primitive-hooks/useActionBarCopy.ts +9 -5
- package/src/react/primitives/message/MessageGroupedParts.tsx +2 -1
- package/src/react/primitives/message/MessageParts.tsx +20 -14
- package/src/react/primitives/messagePart/MessagePartInProgress.ts +15 -0
- package/src/react/runtimes/useToolInvocations.ts +410 -341
- package/src/react/types/MessagePartComponentTypes.ts +11 -0
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +11 -0
- package/src/store/clients/model-context-client.test.ts +108 -0
- package/src/store/clients/model-context-client.ts +36 -6
- package/src/store/scopes/model-context.ts +4 -1
- package/src/tests/external-store-thread-runtime-core.test.ts +113 -0
- package/src/types/message.ts +22 -0
|
@@ -27,25 +27,53 @@ const isEquivalentCompleteArgsText = (previous, next) => {
|
|
|
27
27
|
return isJSONValueEqual(previousValue, nextValue);
|
|
28
28
|
};
|
|
29
29
|
export function useToolInvocations({ state, getTools, onResult, setToolStatuses, }) {
|
|
30
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Single source of truth for per-tool-call lifecycle. Keyed by *logical*
|
|
32
|
+
* toolCallId (the id the host knows). Restored entries have no controller;
|
|
33
|
+
* active entries carry their stream id and rewrite/execution bookkeeping.
|
|
34
|
+
*/
|
|
35
|
+
const entriesRef = useRef(new Map());
|
|
36
|
+
/**
|
|
37
|
+
* Reverse alias map populated only when a rewrite assigns a synthetic stream
|
|
38
|
+
* id to an entry. Identity mappings are implicit via the fallback in
|
|
39
|
+
* `getLogicalToolCallId`.
|
|
40
|
+
*/
|
|
41
|
+
const streamToLogicalRef = useRef(new Map());
|
|
42
|
+
/**
|
|
43
|
+
* Stream ids whose `result` chunks must be dropped before reaching `onResult`.
|
|
44
|
+
* Populated when:
|
|
45
|
+
* - an argsText rewrite supersedes a stream (the old stream's result, if
|
|
46
|
+
* any, is no longer authoritative)
|
|
47
|
+
* - `reset()` is called while a pre-resolved tool call has a never-settling
|
|
48
|
+
* Promise pending in the executor — the eventual cancellation chunk
|
|
49
|
+
* would otherwise be forwarded to a host that has already moved on.
|
|
50
|
+
*/
|
|
51
|
+
const abandonedStreamIdsRef = useRef(new Set());
|
|
52
|
+
/**
|
|
53
|
+
* Stream ids whose `execute` should be short-circuited in the tool wrapper.
|
|
54
|
+
* Tracked by physical stream id (not logical id) so cleanup is keyed off
|
|
55
|
+
* the same id the wrapper sees in its context.
|
|
56
|
+
*/
|
|
57
|
+
const skipExecuteStreamIdsRef = useRef(new Set());
|
|
31
58
|
const humanInputRef = useRef(new Map());
|
|
59
|
+
/**
|
|
60
|
+
* In-flight `execute` invocations keyed by physical stream id. Lives outside
|
|
61
|
+
* `entriesRef` so `reset()` can drop tool-call state without orphaning the
|
|
62
|
+
* cleanup the cancellation `onExecutionEnd` still needs.
|
|
63
|
+
*/
|
|
64
|
+
const executingRef = useRef(new Map());
|
|
32
65
|
const acRef = useRef(new AbortController());
|
|
33
66
|
const executingCountRef = useRef(0);
|
|
34
|
-
const startedExecutionToolCallIdsRef = useRef(new Set());
|
|
35
67
|
const settledResolversRef = useRef([]);
|
|
36
|
-
const toolCallIdAliasesRef = useRef(new Map());
|
|
37
|
-
const ignoredResultToolCallIdsRef = useRef(new Set());
|
|
38
68
|
const rewriteCounterRef = useRef(0);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return true;
|
|
48
|
-
};
|
|
69
|
+
/**
|
|
70
|
+
* `true` until the first snapshot has been processed; `reset()` flips it
|
|
71
|
+
* back to `true`. Snapshots observed while this is `true` are treated as
|
|
72
|
+
* historical: their tool calls are recorded in `entriesRef` as restored
|
|
73
|
+
* but no streamCall/execute fires. The next snapshot is processed as live.
|
|
74
|
+
*/
|
|
75
|
+
const pendingRestoreRef = useRef(true);
|
|
76
|
+
const getLogicalToolCallId = (streamId) => streamToLogicalRef.current.get(streamId) ?? streamId;
|
|
49
77
|
const getWrappedTools = () => {
|
|
50
78
|
const tools = getTools();
|
|
51
79
|
if (!tools)
|
|
@@ -57,10 +85,23 @@ export function useToolInvocations({ state, getTools, onResult, setToolStatuses,
|
|
|
57
85
|
const wrappedTool = {
|
|
58
86
|
...tool,
|
|
59
87
|
...(execute !== undefined && {
|
|
60
|
-
execute: (...[args, context]) =>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
88
|
+
execute: (...[args, context]) => {
|
|
89
|
+
if (skipExecuteStreamIdsRef.current.has(context.toolCallId)) {
|
|
90
|
+
// Pre-resolved on first live observation: never invoke the
|
|
91
|
+
// host's execute fn. Returning a never-settling Promise keeps
|
|
92
|
+
// the executor's pending entry alive but enqueues nothing.
|
|
93
|
+
// The membership in skipExecuteStreamIdsRef must outlive the
|
|
94
|
+
// wrapper call so `reset()`'s seeding loop (which reads this
|
|
95
|
+
// Set to identify pre-resolved entries needing cancellation
|
|
96
|
+
// suppression) sees the entry. Growth is bounded by the
|
|
97
|
+
// number of pre-resolved tool calls observed in the session.
|
|
98
|
+
return new Promise(() => { });
|
|
99
|
+
}
|
|
100
|
+
return execute(args, {
|
|
101
|
+
...context,
|
|
102
|
+
toolCallId: getLogicalToolCallId(context.toolCallId),
|
|
103
|
+
});
|
|
104
|
+
},
|
|
64
105
|
}),
|
|
65
106
|
...(streamCall !== undefined && {
|
|
66
107
|
streamCall: (...[reader, context]) => streamCall(reader, {
|
|
@@ -78,12 +119,17 @@ export function useToolInvocations({ state, getTools, onResult, setToolStatuses,
|
|
|
78
119
|
return [name, wrappedTool];
|
|
79
120
|
}));
|
|
80
121
|
};
|
|
122
|
+
const resolveAllSettledResolvers = () => {
|
|
123
|
+
const resolvers = settledResolversRef.current;
|
|
124
|
+
settledResolversRef.current = [];
|
|
125
|
+
// biome-ignore lint/suspicious/useIterableCallbackReturn: forEach callback intentionally has no return
|
|
126
|
+
resolvers.forEach((resolve) => resolve());
|
|
127
|
+
};
|
|
81
128
|
const [controller] = useState(() => {
|
|
82
129
|
const [stream, controller] = createAssistantStreamController();
|
|
83
130
|
const transform = unstable_toolResultStream(getWrappedTools, () => acRef.current?.signal ?? new AbortController().signal, (toolCallId, payload) => {
|
|
84
131
|
const logicalToolCallId = getLogicalToolCallId(toolCallId);
|
|
85
132
|
return new Promise((resolve, reject) => {
|
|
86
|
-
// Reject previous human input request if it exists
|
|
87
133
|
const previous = humanInputRef.current.get(logicalToolCallId);
|
|
88
134
|
if (previous) {
|
|
89
135
|
previous.reject(new Error("Human input request was superseded by a new request"));
|
|
@@ -98,46 +144,38 @@ export function useToolInvocations({ state, getTools, onResult, setToolStatuses,
|
|
|
98
144
|
}));
|
|
99
145
|
});
|
|
100
146
|
}, {
|
|
101
|
-
onExecutionStart: (
|
|
102
|
-
if (
|
|
147
|
+
onExecutionStart: (streamId) => {
|
|
148
|
+
if (skipExecuteStreamIdsRef.current.has(streamId))
|
|
103
149
|
return;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
150
|
+
const logicalToolCallId = getLogicalToolCallId(streamId);
|
|
151
|
+
const abandoned = abandonedStreamIdsRef.current.has(streamId);
|
|
152
|
+
executingRef.current.set(streamId, {
|
|
153
|
+
logicalToolCallId,
|
|
154
|
+
abandoned,
|
|
155
|
+
});
|
|
107
156
|
executingCountRef.current++;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
onExecutionEnd: (toolCallId) => {
|
|
114
|
-
const wasStarted = startedExecutionToolCallIdsRef.current.delete(toolCallId);
|
|
115
|
-
if (ignoredResultToolCallIdsRef.current.has(toolCallId)) {
|
|
116
|
-
if (wasStarted) {
|
|
117
|
-
executingCountRef.current--;
|
|
118
|
-
if (executingCountRef.current === 0) {
|
|
119
|
-
// biome-ignore lint/suspicious/useIterableCallbackReturn: forEach callback intentionally has no return
|
|
120
|
-
settledResolversRef.current.forEach((resolve) => resolve());
|
|
121
|
-
settledResolversRef.current = [];
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return;
|
|
157
|
+
if (!abandoned) {
|
|
158
|
+
setToolStatuses((prev) => ({
|
|
159
|
+
...prev,
|
|
160
|
+
[logicalToolCallId]: { type: "executing" },
|
|
161
|
+
}));
|
|
125
162
|
}
|
|
126
|
-
|
|
163
|
+
},
|
|
164
|
+
onExecutionEnd: (streamId) => {
|
|
165
|
+
const info = executingRef.current.get(streamId);
|
|
166
|
+
if (!info)
|
|
127
167
|
return;
|
|
128
|
-
|
|
129
|
-
const logicalToolCallId = getLogicalToolCallId(toolCallId);
|
|
168
|
+
executingRef.current.delete(streamId);
|
|
130
169
|
executingCountRef.current--;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
170
|
+
if (!info.abandoned) {
|
|
171
|
+
setToolStatuses((prev) => {
|
|
172
|
+
const next = { ...prev };
|
|
173
|
+
delete next[info.logicalToolCallId];
|
|
174
|
+
return next;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
137
177
|
if (executingCountRef.current === 0) {
|
|
138
|
-
|
|
139
|
-
settledResolversRef.current.forEach((resolve) => resolve());
|
|
140
|
-
settledResolversRef.current = [];
|
|
178
|
+
resolveAllSettledResolvers();
|
|
141
179
|
}
|
|
142
180
|
},
|
|
143
181
|
});
|
|
@@ -146,52 +184,49 @@ export function useToolInvocations({ state, getTools, onResult, setToolStatuses,
|
|
|
146
184
|
.pipeThrough(new AssistantMetaTransformStream())
|
|
147
185
|
.pipeTo(new WritableStream({
|
|
148
186
|
write(chunk) {
|
|
149
|
-
if (chunk.type
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return;
|
|
160
|
-
onResult({
|
|
161
|
-
type: "add-tool-result",
|
|
162
|
-
toolCallId: logicalToolCallId,
|
|
163
|
-
toolName: chunk.meta.toolName,
|
|
164
|
-
result: chunk.result,
|
|
165
|
-
isError: chunk.isError,
|
|
166
|
-
...(chunk.artifact !== undefined && {
|
|
167
|
-
artifact: chunk.artifact,
|
|
168
|
-
}),
|
|
169
|
-
...(chunk.modelContent !== undefined && {
|
|
170
|
-
modelContent: chunk.modelContent,
|
|
171
|
-
}),
|
|
172
|
-
});
|
|
187
|
+
if (chunk.type !== "result")
|
|
188
|
+
return;
|
|
189
|
+
const streamId = chunk.meta.toolCallId;
|
|
190
|
+
const logicalToolCallId = getLogicalToolCallId(streamId);
|
|
191
|
+
const entry = entriesRef.current.get(logicalToolCallId);
|
|
192
|
+
// Result chunk from a rewrite-superseded stream: drop and clean
|
|
193
|
+
// up the alias.
|
|
194
|
+
if (abandonedStreamIdsRef.current.delete(streamId)) {
|
|
195
|
+
streamToLogicalRef.current.delete(streamId);
|
|
196
|
+
return;
|
|
173
197
|
}
|
|
198
|
+
// Pre-resolved tool call whose entry has been cleared by
|
|
199
|
+
// `reset()`. Both the real result chunk and the post-abort
|
|
200
|
+
// cancellation chunk can land here in either order; suppress
|
|
201
|
+
// both via the long-lived `skipExecuteStreamIdsRef` marker.
|
|
202
|
+
if (!entry && skipExecuteStreamIdsRef.current.has(streamId)) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// The host already set the result (via the live snapshot's
|
|
206
|
+
// `setResponse` path). Suppress the executor's redundant emit.
|
|
207
|
+
if (entry?.hasResult)
|
|
208
|
+
return;
|
|
209
|
+
if (streamId !== logicalToolCallId) {
|
|
210
|
+
streamToLogicalRef.current.delete(streamId);
|
|
211
|
+
}
|
|
212
|
+
onResult({
|
|
213
|
+
type: "add-tool-result",
|
|
214
|
+
toolCallId: logicalToolCallId,
|
|
215
|
+
toolName: chunk.meta.toolName,
|
|
216
|
+
result: chunk.result,
|
|
217
|
+
isError: chunk.isError,
|
|
218
|
+
...(chunk.artifact !== undefined && {
|
|
219
|
+
artifact: chunk.artifact,
|
|
220
|
+
}),
|
|
221
|
+
...(chunk.modelContent !== undefined && {
|
|
222
|
+
modelContent: chunk.modelContent,
|
|
223
|
+
}),
|
|
224
|
+
});
|
|
174
225
|
},
|
|
175
226
|
}));
|
|
176
227
|
return controller;
|
|
177
228
|
});
|
|
178
|
-
const ignoredToolIds = useRef(new Set());
|
|
179
|
-
const isInitialState = useRef(true);
|
|
180
229
|
useEffect(() => {
|
|
181
|
-
const createToolState = ({ controller, streamToolCallId, }) => ({
|
|
182
|
-
argsText: "",
|
|
183
|
-
hasResult: false,
|
|
184
|
-
argsComplete: false,
|
|
185
|
-
streamToolCallId,
|
|
186
|
-
controller,
|
|
187
|
-
});
|
|
188
|
-
const setToolState = (toolCallId, state) => {
|
|
189
|
-
lastToolStates.current[toolCallId] = state;
|
|
190
|
-
return state;
|
|
191
|
-
};
|
|
192
|
-
const patchToolState = (toolCallId, state, patch) => {
|
|
193
|
-
return setToolState(toolCallId, { ...state, ...patch });
|
|
194
|
-
};
|
|
195
230
|
const hasExecutableTool = (toolName) => {
|
|
196
231
|
const tool = getTools()?.[toolName];
|
|
197
232
|
return tool?.execute !== undefined || tool?.streamCall !== undefined;
|
|
@@ -200,194 +235,210 @@ export function useToolInvocations({ state, getTools, onResult, setToolStatuses,
|
|
|
200
235
|
if (hasResult)
|
|
201
236
|
return true;
|
|
202
237
|
if (!hasExecutableTool(toolName)) {
|
|
203
|
-
// Non-executable tools can emit parseable
|
|
204
|
-
//
|
|
238
|
+
// Non-executable tools can emit parseable JSON mid-stream; wait for
|
|
239
|
+
// the run to settle before closing.
|
|
205
240
|
return !state.isRunning && isArgsTextComplete(argsText);
|
|
206
241
|
}
|
|
207
242
|
return isArgsTextComplete(argsText);
|
|
208
243
|
};
|
|
209
|
-
const
|
|
210
|
-
ignoredResultToolCallIdsRef.current.add(state.streamToolCallId);
|
|
211
|
-
state.controller.argsText.close();
|
|
212
|
-
const streamToolCallId = `${toolCallId}:rewrite:${rewriteCounterRef.current++}`;
|
|
213
|
-
toolCallIdAliasesRef.current.set(streamToolCallId, toolCallId);
|
|
244
|
+
const startActiveEntry = (toolCallId, toolName, skipExecute) => {
|
|
214
245
|
const toolCallController = controller.addToolCallPart({
|
|
215
246
|
toolName,
|
|
216
|
-
toolCallId
|
|
247
|
+
toolCallId,
|
|
248
|
+
});
|
|
249
|
+
if (skipExecute) {
|
|
250
|
+
skipExecuteStreamIdsRef.current.add(toolCallId);
|
|
251
|
+
}
|
|
252
|
+
const entry = {
|
|
253
|
+
toolName,
|
|
254
|
+
controller: toolCallController,
|
|
255
|
+
streamId: toolCallId,
|
|
256
|
+
argsText: "",
|
|
257
|
+
hasResult: false,
|
|
258
|
+
argsComplete: false,
|
|
259
|
+
};
|
|
260
|
+
entriesRef.current.set(toolCallId, entry);
|
|
261
|
+
return entry;
|
|
262
|
+
};
|
|
263
|
+
const restartArgsStream = (entry, toolCallId) => {
|
|
264
|
+
if (!entry.controller)
|
|
265
|
+
return;
|
|
266
|
+
abandonedStreamIdsRef.current.add(entry.streamId);
|
|
267
|
+
// The wrapper's execute short-circuit follows the current stream id;
|
|
268
|
+
// the abandoned id stays in `skipExecuteStreamIdsRef` if it was there,
|
|
269
|
+
// which is harmless and keeps in-flight chunks consistent.
|
|
270
|
+
const wasSkipExecute = skipExecuteStreamIdsRef.current.has(entry.streamId);
|
|
271
|
+
entry.controller.argsText.close();
|
|
272
|
+
const newStreamId = `${toolCallId}:rewrite:${rewriteCounterRef.current++}`;
|
|
273
|
+
streamToLogicalRef.current.set(newStreamId, toolCallId);
|
|
274
|
+
const newController = controller.addToolCallPart({
|
|
275
|
+
toolName: entry.toolName,
|
|
276
|
+
toolCallId: newStreamId,
|
|
217
277
|
});
|
|
278
|
+
if (wasSkipExecute) {
|
|
279
|
+
skipExecuteStreamIdsRef.current.add(newStreamId);
|
|
280
|
+
}
|
|
218
281
|
if (process.env.NODE_ENV !== "production") {
|
|
219
282
|
console.warn("started replacement stream tool call", {
|
|
220
283
|
toolCallId,
|
|
221
|
-
streamToolCallId,
|
|
284
|
+
streamToolCallId: newStreamId,
|
|
222
285
|
});
|
|
223
286
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
287
|
+
entry.controller = newController;
|
|
288
|
+
entry.streamId = newStreamId;
|
|
289
|
+
entry.argsText = "";
|
|
290
|
+
entry.argsComplete = false;
|
|
291
|
+
};
|
|
292
|
+
const processArgsText = (entry, content) => {
|
|
293
|
+
if (!entry.controller)
|
|
294
|
+
return;
|
|
295
|
+
const hasResult = content.result !== undefined;
|
|
296
|
+
if (content.argsText !== entry.argsText) {
|
|
297
|
+
let shouldWriteArgsText = true;
|
|
298
|
+
if (entry.argsComplete) {
|
|
299
|
+
if (isEquivalentCompleteArgsText(entry.argsText, content.argsText)) {
|
|
300
|
+
entry.argsText = content.argsText;
|
|
301
|
+
shouldWriteArgsText = false;
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
const canRestart = !entry.hasResult && !executingRef.current.has(entry.streamId);
|
|
305
|
+
if (process.env.NODE_ENV !== "production") {
|
|
306
|
+
console.warn(canRestart
|
|
307
|
+
? "argsText updated after controller was closed, restarting tool args stream:"
|
|
308
|
+
: "argsText updated after controller was closed:", { previous: entry.argsText, next: content.argsText });
|
|
309
|
+
}
|
|
310
|
+
if (!canRestart) {
|
|
311
|
+
entry.argsText = content.argsText;
|
|
312
|
+
shouldWriteArgsText = false;
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
restartArgsStream(entry, content.toolCallId);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
else if (!content.argsText.startsWith(entry.argsText)) {
|
|
320
|
+
// Mid-stream rewrite. If both texts parse to equivalent JSON it's a
|
|
321
|
+
// key-reorder snapshot — accept silently. Otherwise restart.
|
|
322
|
+
if (isArgsTextComplete(entry.argsText) &&
|
|
323
|
+
isArgsTextComplete(content.argsText) &&
|
|
324
|
+
isEquivalentCompleteArgsText(entry.argsText, content.argsText)) {
|
|
325
|
+
const shouldClose = shouldCloseArgsStream({
|
|
326
|
+
toolName: content.toolName,
|
|
327
|
+
argsText: content.argsText,
|
|
328
|
+
hasResult,
|
|
329
|
+
});
|
|
330
|
+
if (shouldClose)
|
|
331
|
+
entry.controller.argsText.close();
|
|
332
|
+
entry.argsText = content.argsText;
|
|
333
|
+
entry.argsComplete = shouldClose;
|
|
334
|
+
shouldWriteArgsText = false;
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
if (process.env.NODE_ENV !== "production") {
|
|
338
|
+
console.warn("argsText rewrote previous snapshot, restarting tool args stream:", {
|
|
339
|
+
previous: entry.argsText,
|
|
340
|
+
next: content.argsText,
|
|
341
|
+
toolCallId: content.toolCallId,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
restartArgsStream(entry, content.toolCallId);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (shouldWriteArgsText) {
|
|
348
|
+
const delta = content.argsText.slice(entry.argsText.length);
|
|
349
|
+
entry.controller.argsText.append(delta);
|
|
350
|
+
const shouldClose = shouldCloseArgsStream({
|
|
351
|
+
toolName: content.toolName,
|
|
352
|
+
argsText: content.argsText,
|
|
353
|
+
hasResult,
|
|
354
|
+
});
|
|
355
|
+
if (shouldClose)
|
|
356
|
+
entry.controller.argsText.close();
|
|
357
|
+
entry.argsText = content.argsText;
|
|
358
|
+
entry.argsComplete = shouldClose;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (!entry.argsComplete) {
|
|
362
|
+
const shouldClose = shouldCloseArgsStream({
|
|
363
|
+
toolName: content.toolName,
|
|
364
|
+
argsText: content.argsText,
|
|
365
|
+
hasResult,
|
|
366
|
+
});
|
|
367
|
+
if (shouldClose) {
|
|
368
|
+
entry.controller.argsText.close();
|
|
369
|
+
entry.argsText = content.argsText;
|
|
370
|
+
entry.argsComplete = true;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
231
373
|
};
|
|
232
374
|
const processMessages = (messages) => {
|
|
375
|
+
const isRestore = pendingRestoreRef.current;
|
|
233
376
|
messages.forEach((message) => {
|
|
234
377
|
message.content.forEach((content) => {
|
|
235
|
-
if (content.type
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
toolCallIdAliasesRef.current.set(content.toolCallId, content.toolCallId);
|
|
252
|
-
const toolCallController = controller.addToolCallPart({
|
|
253
|
-
toolName: content.toolName,
|
|
254
|
-
toolCallId: content.toolCallId,
|
|
255
|
-
});
|
|
256
|
-
lastState = setToolState(content.toolCallId, createToolState({
|
|
257
|
-
controller: toolCallController,
|
|
258
|
-
streamToolCallId: content.toolCallId,
|
|
259
|
-
}));
|
|
260
|
-
}
|
|
261
|
-
if (content.argsText !== lastState.argsText) {
|
|
262
|
-
let shouldWriteArgsText = true;
|
|
263
|
-
if (lastState.argsComplete) {
|
|
264
|
-
if (isEquivalentCompleteArgsText(lastState.argsText, content.argsText)) {
|
|
265
|
-
lastState = patchToolState(content.toolCallId, lastState, {
|
|
266
|
-
argsText: content.argsText,
|
|
267
|
-
});
|
|
268
|
-
shouldWriteArgsText = false;
|
|
269
|
-
}
|
|
270
|
-
if (shouldWriteArgsText) {
|
|
271
|
-
const canRestartClosedArgsStream = !lastState.hasResult &&
|
|
272
|
-
!startedExecutionToolCallIdsRef.current.has(lastState.streamToolCallId);
|
|
273
|
-
if (process.env.NODE_ENV !== "production") {
|
|
274
|
-
console.warn(canRestartClosedArgsStream
|
|
275
|
-
? "argsText updated after controller was closed, restarting tool args stream:"
|
|
276
|
-
: "argsText updated after controller was closed:", {
|
|
277
|
-
previous: lastState.argsText,
|
|
278
|
-
next: content.argsText,
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
if (!canRestartClosedArgsStream) {
|
|
282
|
-
lastState = patchToolState(content.toolCallId, lastState, {
|
|
283
|
-
argsText: content.argsText,
|
|
284
|
-
});
|
|
285
|
-
shouldWriteArgsText = false;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
if (shouldWriteArgsText) {
|
|
289
|
-
lastState = restartToolArgsStream({
|
|
290
|
-
toolCallId: content.toolCallId,
|
|
291
|
-
toolName: content.toolName,
|
|
292
|
-
state: lastState,
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
else if (!content.argsText.startsWith(lastState.argsText)) {
|
|
297
|
-
// Check if this is key reordering (both are complete JSON)
|
|
298
|
-
// This happens when transitioning from streaming to complete state
|
|
299
|
-
// and the provider returns keys in a different order
|
|
300
|
-
if (isArgsTextComplete(lastState.argsText) &&
|
|
301
|
-
isArgsTextComplete(content.argsText) &&
|
|
302
|
-
isEquivalentCompleteArgsText(lastState.argsText, content.argsText)) {
|
|
303
|
-
const shouldClose = shouldCloseArgsStream({
|
|
304
|
-
toolName: content.toolName,
|
|
305
|
-
argsText: content.argsText,
|
|
306
|
-
hasResult: content.result !== undefined,
|
|
307
|
-
});
|
|
308
|
-
if (shouldClose) {
|
|
309
|
-
lastState.controller.argsText.close();
|
|
310
|
-
}
|
|
311
|
-
lastState = patchToolState(content.toolCallId, lastState, {
|
|
312
|
-
argsText: content.argsText,
|
|
313
|
-
argsComplete: shouldClose,
|
|
314
|
-
});
|
|
315
|
-
shouldWriteArgsText = false;
|
|
316
|
-
}
|
|
317
|
-
if (shouldWriteArgsText) {
|
|
318
|
-
if (process.env.NODE_ENV !== "production") {
|
|
319
|
-
console.warn("argsText rewrote previous snapshot, restarting tool args stream:", {
|
|
320
|
-
previous: lastState.argsText,
|
|
321
|
-
next: content.argsText,
|
|
322
|
-
toolCallId: content.toolCallId,
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
lastState = restartToolArgsStream({
|
|
326
|
-
toolCallId: content.toolCallId,
|
|
327
|
-
toolName: content.toolName,
|
|
328
|
-
state: lastState,
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
if (shouldWriteArgsText) {
|
|
333
|
-
const argsTextDelta = content.argsText.slice(lastState.argsText.length);
|
|
334
|
-
lastState.controller.argsText.append(argsTextDelta);
|
|
335
|
-
const shouldClose = shouldCloseArgsStream({
|
|
336
|
-
toolName: content.toolName,
|
|
337
|
-
argsText: content.argsText,
|
|
338
|
-
hasResult: content.result !== undefined,
|
|
339
|
-
});
|
|
340
|
-
if (shouldClose) {
|
|
341
|
-
lastState.controller.argsText.close();
|
|
342
|
-
}
|
|
343
|
-
lastState = patchToolState(content.toolCallId, lastState, {
|
|
344
|
-
argsText: content.argsText,
|
|
345
|
-
argsComplete: shouldClose,
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
if (!lastState.argsComplete) {
|
|
350
|
-
const shouldClose = shouldCloseArgsStream({
|
|
351
|
-
toolName: content.toolName,
|
|
352
|
-
argsText: content.argsText,
|
|
353
|
-
hasResult: content.result !== undefined,
|
|
354
|
-
});
|
|
355
|
-
if (shouldClose) {
|
|
356
|
-
lastState.controller.argsText.close();
|
|
357
|
-
lastState = patchToolState(content.toolCallId, lastState, {
|
|
358
|
-
argsText: content.argsText,
|
|
359
|
-
argsComplete: true,
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
if (content.result !== undefined && !lastState.hasResult) {
|
|
364
|
-
patchToolState(content.toolCallId, lastState, {
|
|
365
|
-
hasResult: true,
|
|
366
|
-
argsComplete: true,
|
|
367
|
-
});
|
|
368
|
-
lastState.controller.setResponse(new ToolResponse({
|
|
369
|
-
result: content.result,
|
|
370
|
-
artifact: content.artifact,
|
|
371
|
-
isError: content.isError,
|
|
372
|
-
...(content.modelContent !== undefined
|
|
373
|
-
? { modelContent: content.modelContent }
|
|
374
|
-
: {}),
|
|
375
|
-
}));
|
|
376
|
-
lastState.controller.close();
|
|
377
|
-
}
|
|
378
|
+
if (content.type !== "tool-call")
|
|
379
|
+
return;
|
|
380
|
+
const existing = entriesRef.current.get(content.toolCallId);
|
|
381
|
+
if (isRestore) {
|
|
382
|
+
// Don't overwrite an already-active entry (e.g. live tool-call
|
|
383
|
+
// observed before this restore snapshot landed). Restore can only
|
|
384
|
+
// seed entries the runtime has never seen.
|
|
385
|
+
if (!existing?.controller) {
|
|
386
|
+
entriesRef.current.set(content.toolCallId, {
|
|
387
|
+
toolName: content.toolName,
|
|
388
|
+
argsText: content.argsText,
|
|
389
|
+
hasResult: content.result !== undefined,
|
|
390
|
+
});
|
|
378
391
|
}
|
|
379
|
-
|
|
380
|
-
if (content.messages) {
|
|
392
|
+
if (content.messages)
|
|
381
393
|
processMessages(content.messages);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
// Live snapshot.
|
|
397
|
+
let entry = existing;
|
|
398
|
+
if (entry && !entry.controller) {
|
|
399
|
+
// Restored entry observed in a live snapshot. Promote if its
|
|
400
|
+
// signature has changed; otherwise treat as still-historical.
|
|
401
|
+
const signatureChanged = content.argsText !== entry.argsText ||
|
|
402
|
+
(content.result !== undefined) !== entry.hasResult;
|
|
403
|
+
if (!signatureChanged) {
|
|
404
|
+
if (content.messages)
|
|
405
|
+
processMessages(content.messages);
|
|
406
|
+
return;
|
|
382
407
|
}
|
|
408
|
+
entriesRef.current.delete(content.toolCallId);
|
|
409
|
+
entry = undefined;
|
|
410
|
+
}
|
|
411
|
+
if (!entry) {
|
|
412
|
+
entry = startActiveEntry(content.toolCallId, content.toolName, content.result !== undefined);
|
|
383
413
|
}
|
|
414
|
+
processArgsText(entry, content);
|
|
415
|
+
if (content.result !== undefined && !entry.hasResult) {
|
|
416
|
+
// `entry` is in active phase from this point — either it was
|
|
417
|
+
// just created by `startActiveEntry` above, or it pre-existed
|
|
418
|
+
// and `processArgsText` preserved (or replaced via rewrite) its
|
|
419
|
+
// controller. Narrow once instead of asserting at every use.
|
|
420
|
+
const { controller: activeController } = entry;
|
|
421
|
+
if (!activeController)
|
|
422
|
+
return;
|
|
423
|
+
entry.hasResult = true;
|
|
424
|
+
entry.argsComplete = true;
|
|
425
|
+
activeController.setResponse(new ToolResponse({
|
|
426
|
+
result: content.result,
|
|
427
|
+
artifact: content.artifact,
|
|
428
|
+
isError: content.isError,
|
|
429
|
+
...(content.modelContent !== undefined
|
|
430
|
+
? { modelContent: content.modelContent }
|
|
431
|
+
: {}),
|
|
432
|
+
}));
|
|
433
|
+
activeController.close();
|
|
434
|
+
}
|
|
435
|
+
if (content.messages)
|
|
436
|
+
processMessages(content.messages);
|
|
384
437
|
});
|
|
385
438
|
});
|
|
386
439
|
};
|
|
387
440
|
processMessages(state.messages);
|
|
388
|
-
|
|
389
|
-
isInitialState.current = false;
|
|
390
|
-
}
|
|
441
|
+
pendingRestoreRef.current = false;
|
|
391
442
|
}, [state, controller, getTools]);
|
|
392
443
|
const abort = () => {
|
|
393
444
|
humanInputRef.current.forEach(({ reject }) => {
|
|
@@ -396,7 +447,6 @@ export function useToolInvocations({ state, getTools, onResult, setToolStatuses,
|
|
|
396
447
|
humanInputRef.current.clear();
|
|
397
448
|
acRef.current.abort();
|
|
398
449
|
acRef.current = new AbortController();
|
|
399
|
-
// Return a promise that resolves when all executing tools have settled
|
|
400
450
|
if (executingCountRef.current === 0) {
|
|
401
451
|
return Promise.resolve();
|
|
402
452
|
}
|
|
@@ -406,13 +456,17 @@ export function useToolInvocations({ state, getTools, onResult, setToolStatuses,
|
|
|
406
456
|
};
|
|
407
457
|
return {
|
|
408
458
|
reset: () => {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
459
|
+
pendingRestoreRef.current = true;
|
|
460
|
+
entriesRef.current.clear();
|
|
461
|
+
// `skipExecuteStreamIdsRef` is not cleared: it has to outlive `reset()`
|
|
462
|
+
// so (a) any wrapper call still inbound through the stream pipeline
|
|
463
|
+
// continues to short-circuit `execute`, and (b) the consumer can
|
|
464
|
+
// recognize and drop any post-abort cancellation `result` chunks for
|
|
465
|
+
// pre-resolved streams whose entries have been cleared. Membership
|
|
466
|
+
// grows by one per pre-resolved tool call observed in the session.
|
|
412
467
|
void abort().finally(() => {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
ignoredResultToolCallIdsRef.current.clear();
|
|
468
|
+
executingRef.current.clear();
|
|
469
|
+
streamToLogicalRef.current.clear();
|
|
416
470
|
rewriteCounterRef.current = 0;
|
|
417
471
|
});
|
|
418
472
|
},
|