@assistant-ui/core 0.2.0 → 0.2.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/README.md +45 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- 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 +18 -1
- package/dist/react/client/Tools.d.ts.map +1 -1
- package/dist/react/client/Tools.js +24 -19
- package/dist/react/client/Tools.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 +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/primitive-hooks/useComposerSend.d.ts.map +1 -1
- package/dist/react/primitive-hooks/useComposerSend.js +2 -3
- package/dist/react/primitive-hooks/useComposerSend.js.map +1 -1
- package/dist/react/primitives/message/MessageAttachments.js +1 -1
- package/dist/react/primitives/message/MessageAttachments.js.map +1 -1
- package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageParts.js +14 -10
- 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/RemoteThreadListHookInstanceManager.d.ts +2 -0
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +2 -0
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.d.ts +10 -1
- package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.js +21 -3
- package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
- package/dist/react/runtimes/useToolInvocations.d.ts +11 -1
- package/dist/react/runtimes/useToolInvocations.d.ts.map +1 -1
- package/dist/react/runtimes/useToolInvocations.js +325 -256
- 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/react/types/scopes/tools.d.ts +4 -0
- package/dist/react/types/scopes/tools.d.ts.map +1 -1
- package/dist/runtime/api/composer-runtime.d.ts +1 -0
- package/dist/runtime/api/composer-runtime.d.ts.map +1 -1
- package/dist/runtime/api/composer-runtime.js +2 -0
- package/dist/runtime/api/composer-runtime.js.map +1 -1
- package/dist/runtime/api/thread-runtime.d.ts +2 -0
- package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts +1 -0
- package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.js +1 -1
- package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.d.ts +1 -0
- package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
- package/dist/runtime/base/default-edit-composer-runtime-core.d.ts +1 -0
- package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/default-edit-composer-runtime-core.js +3 -0
- package/dist/runtime/base/default-edit-composer-runtime-core.js.map +1 -1
- package/dist/runtime/base/default-thread-composer-runtime-core.d.ts +1 -0
- package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/default-thread-composer-runtime-core.js +12 -1
- package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
- package/dist/runtime/interfaces/composer-runtime-core.d.ts +1 -0
- package/dist/runtime/interfaces/composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/interfaces/thread-runtime-core.d.ts +6 -0
- package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-adapter.d.ts +15 -0
- package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +1 -1
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +14 -12
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +2 -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 +13 -0
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts +1 -0
- package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.js +1 -0
- package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +2 -0
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +2 -0
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
- package/dist/runtimes/remote-thread-list/empty-thread-core.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/empty-thread-core.js +2 -0
- package/dist/runtimes/remote-thread-list/empty-thread-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/clients/no-op-composer-client.d.ts.map +1 -1
- package/dist/store/clients/no-op-composer-client.js +1 -0
- package/dist/store/clients/no-op-composer-client.js.map +1 -1
- package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/composer-runtime-client.js +1 -0
- package/dist/store/runtime-clients/composer-runtime-client.js.map +1 -1
- package/dist/store/scopes/composer.d.ts +9 -0
- package/dist/store/scopes/composer.d.ts.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/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/message.d.ts +50 -1
- package/dist/types/message.d.ts.map +1 -1
- package/dist/types/message.js +2 -1
- package/dist/types/message.js.map +1 -1
- package/package.json +7 -7
- package/src/index.ts +6 -0
- 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 +56 -22
- package/src/react/index.ts +2 -1
- 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/primitive-hooks/useComposerSend.ts +2 -3
- package/src/react/primitives/message/MessageAttachments.test.tsx +50 -0
- package/src/react/primitives/message/MessageAttachments.tsx +1 -1
- package/src/react/primitives/message/MessageParts.tsx +20 -9
- package/src/react/primitives/messagePart/MessagePartInProgress.ts +15 -0
- package/src/react/runtimes/cloud/auiV0.ts +37 -4
- package/src/react/runtimes/useToolInvocations.ts +422 -333
- package/src/react/types/MessagePartComponentTypes.ts +11 -0
- package/src/react/types/scopes/tools.ts +5 -0
- package/src/runtime/api/composer-runtime.ts +3 -0
- package/src/runtime/base/base-composer-runtime-core.ts +2 -1
- package/src/runtime/base/base-thread-runtime-core.ts +1 -0
- package/src/runtime/base/default-edit-composer-runtime-core.ts +4 -0
- package/src/runtime/base/default-thread-composer-runtime-core.ts +12 -1
- package/src/runtime/interfaces/composer-runtime-core.ts +1 -0
- package/src/runtime/interfaces/thread-runtime-core.ts +6 -0
- package/src/runtimes/external-store/external-store-adapter.ts +15 -0
- package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +15 -9
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +13 -0
- package/src/runtimes/local/local-thread-runtime-core.ts +1 -0
- package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +2 -0
- package/src/runtimes/remote-thread-list/empty-thread-core.ts +2 -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/clients/no-op-composer-client.ts +1 -0
- package/src/store/runtime-clients/composer-runtime-client.ts +1 -0
- package/src/store/scopes/composer.ts +9 -0
- package/src/store/scopes/model-context.ts +4 -1
- package/src/tests/auiV0Encode.test.ts +55 -0
- package/src/tests/composer-can-send.test.ts +112 -0
- package/src/tests/external-store-thread-list-runtime-core.test.ts +34 -0
- package/src/tests/external-store-thread-runtime-core.test.ts +113 -0
- package/src/types/index.ts +2 -0
- package/src/types/message.ts +66 -7
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
ToolResponse,
|
|
8
8
|
unstable_toolResultStream,
|
|
9
9
|
type Tool,
|
|
10
|
+
type ToolModelContentPart,
|
|
10
11
|
} from "assistant-stream";
|
|
11
12
|
import {
|
|
12
13
|
AssistantMetaTransformStream,
|
|
@@ -28,6 +29,7 @@ export type AddToolResultCommand = {
|
|
|
28
29
|
readonly result: ReadonlyJSONValue;
|
|
29
30
|
readonly isError: boolean;
|
|
30
31
|
readonly artifact?: ReadonlyJSONValue;
|
|
32
|
+
readonly modelContent?: readonly ToolModelContentPart[];
|
|
31
33
|
};
|
|
32
34
|
|
|
33
35
|
const isArgsTextComplete = (argsText: string) => {
|
|
@@ -67,16 +69,60 @@ type UseToolInvocationsParams = {
|
|
|
67
69
|
) => void;
|
|
68
70
|
};
|
|
69
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Streaming execution state for a frontend tool.
|
|
74
|
+
*
|
|
75
|
+
* Custom runtime integrations use this to mirror in-flight tool calls while
|
|
76
|
+
* `useToolInvocations` executes tools in the browser.
|
|
77
|
+
*/
|
|
70
78
|
export type ToolExecutionStatus =
|
|
71
|
-
| {
|
|
72
|
-
|
|
79
|
+
| {
|
|
80
|
+
/** The tool's execute function is currently running. */
|
|
81
|
+
type: "executing";
|
|
82
|
+
}
|
|
83
|
+
| {
|
|
84
|
+
/** The tool is waiting for a human input payload before continuing. */
|
|
85
|
+
type: "interrupt";
|
|
86
|
+
/** Human input request emitted by the tool execution context. */
|
|
87
|
+
payload: { type: "human"; payload: unknown };
|
|
88
|
+
};
|
|
73
89
|
|
|
74
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Per-logical-tool-call state. A single discriminator distinguishes restored
|
|
92
|
+
* snapshots (no controller; only used for signature comparison) from active
|
|
93
|
+
* snapshots that are being streamed through the assistant-stream pipeline.
|
|
94
|
+
*/
|
|
95
|
+
type ToolCallEntry = {
|
|
96
|
+
toolName: string;
|
|
97
|
+
/** Last observed `argsText` for this tool call. */
|
|
75
98
|
argsText: string;
|
|
99
|
+
/** Last observed `result !== undefined` for this tool call. */
|
|
76
100
|
hasResult: boolean;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
101
|
+
} & (
|
|
102
|
+
| {
|
|
103
|
+
/** Restored phase — observed during a history-load snapshot. */
|
|
104
|
+
controller?: undefined;
|
|
105
|
+
streamId?: undefined;
|
|
106
|
+
argsComplete?: undefined;
|
|
107
|
+
}
|
|
108
|
+
| {
|
|
109
|
+
/** Active phase — chunks are flowing through `controller`. */
|
|
110
|
+
controller: ToolCallStreamController;
|
|
111
|
+
/** Current physical stream id (differs from logical id after a rewrite). */
|
|
112
|
+
streamId: string;
|
|
113
|
+
argsComplete: boolean;
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Per-physical-stream-id execution lifecycle bookkeeping. Tracked separately
|
|
119
|
+
* from `ToolCallEntry` so that `reset()` can clear tool-call state
|
|
120
|
+
* synchronously while in-flight executions still find their cleanup info via
|
|
121
|
+
* `onExecutionEnd` after `abort()` settles.
|
|
122
|
+
*/
|
|
123
|
+
type ExecutingStream = {
|
|
124
|
+
logicalToolCallId: string;
|
|
125
|
+
abandoned: boolean;
|
|
80
126
|
};
|
|
81
127
|
|
|
82
128
|
export function useToolInvocations({
|
|
@@ -85,7 +131,37 @@ export function useToolInvocations({
|
|
|
85
131
|
onResult,
|
|
86
132
|
setToolStatuses,
|
|
87
133
|
}: UseToolInvocationsParams) {
|
|
88
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Single source of truth for per-tool-call lifecycle. Keyed by *logical*
|
|
136
|
+
* toolCallId (the id the host knows). Restored entries have no controller;
|
|
137
|
+
* active entries carry their stream id and rewrite/execution bookkeeping.
|
|
138
|
+
*/
|
|
139
|
+
const entriesRef = useRef<Map<string, ToolCallEntry>>(new Map());
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Reverse alias map populated only when a rewrite assigns a synthetic stream
|
|
143
|
+
* id to an entry. Identity mappings are implicit via the fallback in
|
|
144
|
+
* `getLogicalToolCallId`.
|
|
145
|
+
*/
|
|
146
|
+
const streamToLogicalRef = useRef<Map<string, string>>(new Map());
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Stream ids whose `result` chunks must be dropped before reaching `onResult`.
|
|
150
|
+
* Populated when:
|
|
151
|
+
* - an argsText rewrite supersedes a stream (the old stream's result, if
|
|
152
|
+
* any, is no longer authoritative)
|
|
153
|
+
* - `reset()` is called while a pre-resolved tool call has a never-settling
|
|
154
|
+
* Promise pending in the executor — the eventual cancellation chunk
|
|
155
|
+
* would otherwise be forwarded to a host that has already moved on.
|
|
156
|
+
*/
|
|
157
|
+
const abandonedStreamIdsRef = useRef<Set<string>>(new Set());
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Stream ids whose `execute` should be short-circuited in the tool wrapper.
|
|
161
|
+
* Tracked by physical stream id (not logical id) so cleanup is keyed off
|
|
162
|
+
* the same id the wrapper sees in its context.
|
|
163
|
+
*/
|
|
164
|
+
const skipExecuteStreamIdsRef = useRef<Set<string>>(new Set());
|
|
89
165
|
|
|
90
166
|
const humanInputRef = useRef<
|
|
91
167
|
Map<
|
|
@@ -97,24 +173,28 @@ export function useToolInvocations({
|
|
|
97
173
|
>
|
|
98
174
|
>(new Map());
|
|
99
175
|
|
|
176
|
+
/**
|
|
177
|
+
* In-flight `execute` invocations keyed by physical stream id. Lives outside
|
|
178
|
+
* `entriesRef` so `reset()` can drop tool-call state without orphaning the
|
|
179
|
+
* cleanup the cancellation `onExecutionEnd` still needs.
|
|
180
|
+
*/
|
|
181
|
+
const executingRef = useRef<Map<string, ExecutingStream>>(new Map());
|
|
182
|
+
|
|
100
183
|
const acRef = useRef<AbortController>(new AbortController());
|
|
101
184
|
const executingCountRef = useRef(0);
|
|
102
|
-
const startedExecutionToolCallIdsRef = useRef<Set<string>>(new Set());
|
|
103
185
|
const settledResolversRef = useRef<Array<() => void>>([]);
|
|
104
|
-
const toolCallIdAliasesRef = useRef<Map<string, string>>(new Map());
|
|
105
|
-
const ignoredResultToolCallIdsRef = useRef<Set<string>>(new Set());
|
|
106
186
|
const rewriteCounterRef = useRef(0);
|
|
107
187
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
188
|
+
/**
|
|
189
|
+
* `true` until the first snapshot has been processed; `reset()` flips it
|
|
190
|
+
* back to `true`. Snapshots observed while this is `true` are treated as
|
|
191
|
+
* historical: their tool calls are recorded in `entriesRef` as restored
|
|
192
|
+
* but no streamCall/execute fires. The next snapshot is processed as live.
|
|
193
|
+
*/
|
|
194
|
+
const pendingRestoreRef = useRef(true);
|
|
111
195
|
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
ignoredResultToolCallIdsRef.current.delete(toolCallId);
|
|
115
|
-
toolCallIdAliasesRef.current.delete(toolCallId);
|
|
116
|
-
return true;
|
|
117
|
-
};
|
|
196
|
+
const getLogicalToolCallId = (streamId: string) =>
|
|
197
|
+
streamToLogicalRef.current.get(streamId) ?? streamId;
|
|
118
198
|
|
|
119
199
|
const getWrappedTools = () => {
|
|
120
200
|
const tools = getTools();
|
|
@@ -124,17 +204,30 @@ export function useToolInvocations({
|
|
|
124
204
|
Object.entries(tools).map(([name, tool]) => {
|
|
125
205
|
const execute = tool.execute;
|
|
126
206
|
const streamCall = tool.streamCall;
|
|
207
|
+
const toModelOutput = tool.toModelOutput;
|
|
127
208
|
|
|
128
209
|
const wrappedTool = {
|
|
129
210
|
...tool,
|
|
130
211
|
...(execute !== undefined && {
|
|
131
212
|
execute: (
|
|
132
213
|
...[args, context]: Parameters<NonNullable<typeof execute>>
|
|
133
|
-
) =>
|
|
134
|
-
|
|
214
|
+
) => {
|
|
215
|
+
if (skipExecuteStreamIdsRef.current.has(context.toolCallId)) {
|
|
216
|
+
// Pre-resolved on first live observation: never invoke the
|
|
217
|
+
// host's execute fn. Returning a never-settling Promise keeps
|
|
218
|
+
// the executor's pending entry alive but enqueues nothing.
|
|
219
|
+
// The membership in skipExecuteStreamIdsRef must outlive the
|
|
220
|
+
// wrapper call so `reset()`'s seeding loop (which reads this
|
|
221
|
+
// Set to identify pre-resolved entries needing cancellation
|
|
222
|
+
// suppression) sees the entry. Growth is bounded by the
|
|
223
|
+
// number of pre-resolved tool calls observed in the session.
|
|
224
|
+
return new Promise(() => {}) as never;
|
|
225
|
+
}
|
|
226
|
+
return execute(args, {
|
|
135
227
|
...context,
|
|
136
228
|
toolCallId: getLogicalToolCallId(context.toolCallId),
|
|
137
|
-
})
|
|
229
|
+
});
|
|
230
|
+
},
|
|
138
231
|
}),
|
|
139
232
|
...(streamCall !== undefined && {
|
|
140
233
|
streamCall: (
|
|
@@ -145,12 +238,28 @@ export function useToolInvocations({
|
|
|
145
238
|
toolCallId: getLogicalToolCallId(context.toolCallId),
|
|
146
239
|
}),
|
|
147
240
|
}),
|
|
241
|
+
...(toModelOutput !== undefined && {
|
|
242
|
+
toModelOutput: (
|
|
243
|
+
options: Parameters<NonNullable<typeof toModelOutput>>[0],
|
|
244
|
+
) =>
|
|
245
|
+
toModelOutput({
|
|
246
|
+
...options,
|
|
247
|
+
toolCallId: getLogicalToolCallId(options.toolCallId),
|
|
248
|
+
}),
|
|
249
|
+
}),
|
|
148
250
|
} as Tool;
|
|
149
251
|
return [name, wrappedTool];
|
|
150
252
|
}),
|
|
151
253
|
) as Record<string, Tool>;
|
|
152
254
|
};
|
|
153
255
|
|
|
256
|
+
const resolveAllSettledResolvers = () => {
|
|
257
|
+
const resolvers = settledResolversRef.current;
|
|
258
|
+
settledResolversRef.current = [];
|
|
259
|
+
// biome-ignore lint/suspicious/useIterableCallbackReturn: forEach callback intentionally has no return
|
|
260
|
+
resolvers.forEach((resolve) => resolve());
|
|
261
|
+
};
|
|
262
|
+
|
|
154
263
|
const [controller] = useState(() => {
|
|
155
264
|
const [stream, controller] = createAssistantStreamController();
|
|
156
265
|
const transform = unstable_toolResultStream(
|
|
@@ -159,14 +268,12 @@ export function useToolInvocations({
|
|
|
159
268
|
(toolCallId: string, payload: unknown) => {
|
|
160
269
|
const logicalToolCallId = getLogicalToolCallId(toolCallId);
|
|
161
270
|
return new Promise<unknown>((resolve, reject) => {
|
|
162
|
-
// Reject previous human input request if it exists
|
|
163
271
|
const previous = humanInputRef.current.get(logicalToolCallId);
|
|
164
272
|
if (previous) {
|
|
165
273
|
previous.reject(
|
|
166
274
|
new Error("Human input request was superseded by a new request"),
|
|
167
275
|
);
|
|
168
276
|
}
|
|
169
|
-
|
|
170
277
|
humanInputRef.current.set(logicalToolCallId, { resolve, reject });
|
|
171
278
|
setToolStatuses((prev) => ({
|
|
172
279
|
...prev,
|
|
@@ -178,80 +285,91 @@ export function useToolInvocations({
|
|
|
178
285
|
});
|
|
179
286
|
},
|
|
180
287
|
{
|
|
181
|
-
onExecutionStart: (
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
288
|
+
onExecutionStart: (streamId: string) => {
|
|
289
|
+
if (skipExecuteStreamIdsRef.current.has(streamId)) return;
|
|
290
|
+
|
|
291
|
+
const logicalToolCallId = getLogicalToolCallId(streamId);
|
|
292
|
+
const abandoned = abandonedStreamIdsRef.current.has(streamId);
|
|
293
|
+
executingRef.current.set(streamId, {
|
|
294
|
+
logicalToolCallId,
|
|
295
|
+
abandoned,
|
|
296
|
+
});
|
|
187
297
|
executingCountRef.current++;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
// biome-ignore lint/suspicious/useIterableCallbackReturn: forEach callback intentionally has no return
|
|
201
|
-
settledResolversRef.current.forEach((resolve) => resolve());
|
|
202
|
-
settledResolversRef.current = [];
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
if (!wasStarted) {
|
|
208
|
-
return;
|
|
298
|
+
if (!abandoned) {
|
|
299
|
+
setToolStatuses((prev) => ({
|
|
300
|
+
...prev,
|
|
301
|
+
[logicalToolCallId]: { type: "executing" },
|
|
302
|
+
}));
|
|
209
303
|
}
|
|
210
|
-
|
|
304
|
+
},
|
|
305
|
+
onExecutionEnd: (streamId: string) => {
|
|
306
|
+
const info = executingRef.current.get(streamId);
|
|
307
|
+
if (!info) return;
|
|
308
|
+
executingRef.current.delete(streamId);
|
|
309
|
+
|
|
211
310
|
executingCountRef.current--;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
311
|
+
if (!info.abandoned) {
|
|
312
|
+
setToolStatuses((prev) => {
|
|
313
|
+
const next = { ...prev };
|
|
314
|
+
delete next[info.logicalToolCallId];
|
|
315
|
+
return next;
|
|
316
|
+
});
|
|
317
|
+
}
|
|
218
318
|
if (executingCountRef.current === 0) {
|
|
219
|
-
|
|
220
|
-
settledResolversRef.current.forEach((resolve) => resolve());
|
|
221
|
-
settledResolversRef.current = [];
|
|
319
|
+
resolveAllSettledResolvers();
|
|
222
320
|
}
|
|
223
321
|
},
|
|
224
322
|
},
|
|
225
323
|
);
|
|
324
|
+
|
|
226
325
|
stream
|
|
227
326
|
.pipeThrough(transform)
|
|
228
327
|
.pipeThrough(new AssistantMetaTransformStream())
|
|
229
328
|
.pipeTo(
|
|
230
329
|
new WritableStream({
|
|
231
330
|
write(chunk) {
|
|
232
|
-
if (chunk.type
|
|
233
|
-
if (shouldIgnoreAndCleanupResult(chunk.meta.toolCallId)) {
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
331
|
+
if (chunk.type !== "result") return;
|
|
236
332
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
333
|
+
const streamId = chunk.meta.toolCallId;
|
|
334
|
+
const logicalToolCallId = getLogicalToolCallId(streamId);
|
|
335
|
+
const entry = entriesRef.current.get(logicalToolCallId);
|
|
336
|
+
|
|
337
|
+
// Result chunk from a rewrite-superseded stream: drop and clean
|
|
338
|
+
// up the alias.
|
|
339
|
+
if (abandonedStreamIdsRef.current.delete(streamId)) {
|
|
340
|
+
streamToLogicalRef.current.delete(streamId);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Pre-resolved tool call whose entry has been cleared by
|
|
345
|
+
// `reset()`. Both the real result chunk and the post-abort
|
|
346
|
+
// cancellation chunk can land here in either order; suppress
|
|
347
|
+
// both via the long-lived `skipExecuteStreamIdsRef` marker.
|
|
348
|
+
if (!entry && skipExecuteStreamIdsRef.current.has(streamId)) {
|
|
349
|
+
return;
|
|
254
350
|
}
|
|
351
|
+
|
|
352
|
+
// The host already set the result (via the live snapshot's
|
|
353
|
+
// `setResponse` path). Suppress the executor's redundant emit.
|
|
354
|
+
if (entry?.hasResult) return;
|
|
355
|
+
|
|
356
|
+
if (streamId !== logicalToolCallId) {
|
|
357
|
+
streamToLogicalRef.current.delete(streamId);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
onResult({
|
|
361
|
+
type: "add-tool-result",
|
|
362
|
+
toolCallId: logicalToolCallId,
|
|
363
|
+
toolName: chunk.meta.toolName,
|
|
364
|
+
result: chunk.result,
|
|
365
|
+
isError: chunk.isError,
|
|
366
|
+
...(chunk.artifact !== undefined && {
|
|
367
|
+
artifact: chunk.artifact,
|
|
368
|
+
}),
|
|
369
|
+
...(chunk.modelContent !== undefined && {
|
|
370
|
+
modelContent: chunk.modelContent,
|
|
371
|
+
}),
|
|
372
|
+
});
|
|
255
373
|
},
|
|
256
374
|
}),
|
|
257
375
|
);
|
|
@@ -259,37 +377,7 @@ export function useToolInvocations({
|
|
|
259
377
|
return controller;
|
|
260
378
|
});
|
|
261
379
|
|
|
262
|
-
const ignoredToolIds = useRef<Set<string>>(new Set());
|
|
263
|
-
const isInitialState = useRef(true);
|
|
264
|
-
|
|
265
380
|
useEffect(() => {
|
|
266
|
-
const createToolState = ({
|
|
267
|
-
controller,
|
|
268
|
-
streamToolCallId,
|
|
269
|
-
}: {
|
|
270
|
-
controller: ToolCallStreamController;
|
|
271
|
-
streamToolCallId: string;
|
|
272
|
-
}): ToolState => ({
|
|
273
|
-
argsText: "",
|
|
274
|
-
hasResult: false,
|
|
275
|
-
argsComplete: false,
|
|
276
|
-
streamToolCallId,
|
|
277
|
-
controller,
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
const setToolState = (toolCallId: string, state: ToolState) => {
|
|
281
|
-
lastToolStates.current[toolCallId] = state;
|
|
282
|
-
return state;
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
const patchToolState = (
|
|
286
|
-
toolCallId: string,
|
|
287
|
-
state: ToolState,
|
|
288
|
-
patch: Partial<ToolState>,
|
|
289
|
-
) => {
|
|
290
|
-
return setToolState(toolCallId, { ...state, ...patch });
|
|
291
|
-
};
|
|
292
|
-
|
|
293
381
|
const hasExecutableTool = (toolName: string) => {
|
|
294
382
|
const tool = getTools()?.[toolName];
|
|
295
383
|
return tool?.execute !== undefined || tool?.streamCall !== undefined;
|
|
@@ -306,253 +394,251 @@ export function useToolInvocations({
|
|
|
306
394
|
}) => {
|
|
307
395
|
if (hasResult) return true;
|
|
308
396
|
if (!hasExecutableTool(toolName)) {
|
|
309
|
-
// Non-executable tools can emit parseable
|
|
310
|
-
//
|
|
397
|
+
// Non-executable tools can emit parseable JSON mid-stream; wait for
|
|
398
|
+
// the run to settle before closing.
|
|
311
399
|
return !state.isRunning && isArgsTextComplete(argsText);
|
|
312
400
|
}
|
|
313
401
|
return isArgsTextComplete(argsText);
|
|
314
402
|
};
|
|
315
403
|
|
|
316
|
-
const
|
|
317
|
-
toolCallId,
|
|
318
|
-
toolName,
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
toolCallId: string;
|
|
322
|
-
toolName: string;
|
|
323
|
-
state: ToolState;
|
|
324
|
-
}) => {
|
|
325
|
-
ignoredResultToolCallIdsRef.current.add(state.streamToolCallId);
|
|
326
|
-
state.controller.argsText.close();
|
|
327
|
-
|
|
328
|
-
const streamToolCallId = `${toolCallId}:rewrite:${rewriteCounterRef.current++}`;
|
|
329
|
-
toolCallIdAliasesRef.current.set(streamToolCallId, toolCallId);
|
|
404
|
+
const startActiveEntry = (
|
|
405
|
+
toolCallId: string,
|
|
406
|
+
toolName: string,
|
|
407
|
+
skipExecute: boolean,
|
|
408
|
+
): ToolCallEntry => {
|
|
330
409
|
const toolCallController = controller.addToolCallPart({
|
|
331
410
|
toolName,
|
|
332
|
-
toolCallId
|
|
411
|
+
toolCallId,
|
|
333
412
|
});
|
|
413
|
+
if (skipExecute) {
|
|
414
|
+
skipExecuteStreamIdsRef.current.add(toolCallId);
|
|
415
|
+
}
|
|
416
|
+
const entry: ToolCallEntry = {
|
|
417
|
+
toolName,
|
|
418
|
+
controller: toolCallController,
|
|
419
|
+
streamId: toolCallId,
|
|
420
|
+
argsText: "",
|
|
421
|
+
hasResult: false,
|
|
422
|
+
argsComplete: false,
|
|
423
|
+
};
|
|
424
|
+
entriesRef.current.set(toolCallId, entry);
|
|
425
|
+
return entry;
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const restartArgsStream = (entry: ToolCallEntry, toolCallId: string) => {
|
|
429
|
+
if (!entry.controller) return;
|
|
430
|
+
abandonedStreamIdsRef.current.add(entry.streamId);
|
|
431
|
+
// The wrapper's execute short-circuit follows the current stream id;
|
|
432
|
+
// the abandoned id stays in `skipExecuteStreamIdsRef` if it was there,
|
|
433
|
+
// which is harmless and keeps in-flight chunks consistent.
|
|
434
|
+
const wasSkipExecute = skipExecuteStreamIdsRef.current.has(
|
|
435
|
+
entry.streamId,
|
|
436
|
+
);
|
|
437
|
+
entry.controller.argsText.close();
|
|
438
|
+
|
|
439
|
+
const newStreamId = `${toolCallId}:rewrite:${rewriteCounterRef.current++}`;
|
|
440
|
+
streamToLogicalRef.current.set(newStreamId, toolCallId);
|
|
441
|
+
const newController = controller.addToolCallPart({
|
|
442
|
+
toolName: entry.toolName,
|
|
443
|
+
toolCallId: newStreamId,
|
|
444
|
+
});
|
|
445
|
+
if (wasSkipExecute) {
|
|
446
|
+
skipExecuteStreamIdsRef.current.add(newStreamId);
|
|
447
|
+
}
|
|
334
448
|
|
|
335
449
|
if (process.env.NODE_ENV !== "production") {
|
|
336
450
|
console.warn("started replacement stream tool call", {
|
|
337
451
|
toolCallId,
|
|
338
|
-
streamToolCallId,
|
|
452
|
+
streamToolCallId: newStreamId,
|
|
339
453
|
});
|
|
340
454
|
}
|
|
341
455
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}),
|
|
347
|
-
hasResult: state.hasResult,
|
|
348
|
-
});
|
|
456
|
+
entry.controller = newController;
|
|
457
|
+
entry.streamId = newStreamId;
|
|
458
|
+
entry.argsText = "";
|
|
459
|
+
entry.argsComplete = false;
|
|
349
460
|
};
|
|
350
461
|
|
|
351
|
-
const
|
|
352
|
-
|
|
462
|
+
const processArgsText = (
|
|
463
|
+
entry: ToolCallEntry,
|
|
464
|
+
content: {
|
|
465
|
+
toolCallId: string;
|
|
466
|
+
toolName: string;
|
|
467
|
+
argsText: string;
|
|
468
|
+
result?: unknown;
|
|
469
|
+
},
|
|
353
470
|
) => {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
471
|
+
if (!entry.controller) return;
|
|
472
|
+
const hasResult = content.result !== undefined;
|
|
473
|
+
|
|
474
|
+
if (content.argsText !== entry.argsText) {
|
|
475
|
+
let shouldWriteArgsText = true;
|
|
476
|
+
|
|
477
|
+
if (entry.argsComplete) {
|
|
478
|
+
if (isEquivalentCompleteArgsText(entry.argsText, content.argsText)) {
|
|
479
|
+
entry.argsText = content.argsText;
|
|
480
|
+
shouldWriteArgsText = false;
|
|
481
|
+
} else {
|
|
482
|
+
const canRestart =
|
|
483
|
+
!entry.hasResult && !executingRef.current.has(entry.streamId);
|
|
484
|
+
if (process.env.NODE_ENV !== "production") {
|
|
485
|
+
console.warn(
|
|
486
|
+
canRestart
|
|
487
|
+
? "argsText updated after controller was closed, restarting tool args stream:"
|
|
488
|
+
: "argsText updated after controller was closed:",
|
|
489
|
+
{ previous: entry.argsText, next: content.argsText },
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
if (!canRestart) {
|
|
493
|
+
entry.argsText = content.argsText;
|
|
494
|
+
shouldWriteArgsText = false;
|
|
359
495
|
} else {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
496
|
+
restartArgsStream(entry, content.toolCallId);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
} else if (!content.argsText.startsWith(entry.argsText)) {
|
|
500
|
+
// Mid-stream rewrite. If both texts parse to equivalent JSON it's a
|
|
501
|
+
// key-reorder snapshot — accept silently. Otherwise restart.
|
|
502
|
+
if (
|
|
503
|
+
isArgsTextComplete(entry.argsText) &&
|
|
504
|
+
isArgsTextComplete(content.argsText) &&
|
|
505
|
+
isEquivalentCompleteArgsText(entry.argsText, content.argsText)
|
|
506
|
+
) {
|
|
507
|
+
const shouldClose = shouldCloseArgsStream({
|
|
508
|
+
toolName: content.toolName,
|
|
509
|
+
argsText: content.argsText,
|
|
510
|
+
hasResult,
|
|
511
|
+
});
|
|
512
|
+
if (shouldClose) entry.controller.argsText.close();
|
|
513
|
+
entry.argsText = content.argsText;
|
|
514
|
+
entry.argsComplete = shouldClose;
|
|
515
|
+
shouldWriteArgsText = false;
|
|
516
|
+
} else {
|
|
517
|
+
if (process.env.NODE_ENV !== "production") {
|
|
518
|
+
console.warn(
|
|
519
|
+
"argsText rewrote previous snapshot, restarting tool args stream:",
|
|
520
|
+
{
|
|
521
|
+
previous: entry.argsText,
|
|
522
|
+
next: content.argsText,
|
|
378
523
|
toolCallId: content.toolCallId,
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
524
|
+
},
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
restartArgsStream(entry, content.toolCallId);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (shouldWriteArgsText) {
|
|
532
|
+
const delta = content.argsText.slice(entry.argsText.length);
|
|
533
|
+
entry.controller.argsText.append(delta);
|
|
534
|
+
const shouldClose = shouldCloseArgsStream({
|
|
535
|
+
toolName: content.toolName,
|
|
536
|
+
argsText: content.argsText,
|
|
537
|
+
hasResult,
|
|
538
|
+
});
|
|
539
|
+
if (shouldClose) entry.controller.argsText.close();
|
|
540
|
+
entry.argsText = content.argsText;
|
|
541
|
+
entry.argsComplete = shouldClose;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
388
544
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
shouldWriteArgsText = false;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
if (shouldWriteArgsText) {
|
|
406
|
-
const canRestartClosedArgsStream =
|
|
407
|
-
!lastState.hasResult &&
|
|
408
|
-
!startedExecutionToolCallIdsRef.current.has(
|
|
409
|
-
lastState.streamToolCallId,
|
|
410
|
-
);
|
|
411
|
-
|
|
412
|
-
if (process.env.NODE_ENV !== "production") {
|
|
413
|
-
console.warn(
|
|
414
|
-
canRestartClosedArgsStream
|
|
415
|
-
? "argsText updated after controller was closed, restarting tool args stream:"
|
|
416
|
-
: "argsText updated after controller was closed:",
|
|
417
|
-
{
|
|
418
|
-
previous: lastState.argsText,
|
|
419
|
-
next: content.argsText,
|
|
420
|
-
},
|
|
421
|
-
);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if (!canRestartClosedArgsStream) {
|
|
425
|
-
lastState = patchToolState(
|
|
426
|
-
content.toolCallId,
|
|
427
|
-
lastState,
|
|
428
|
-
{
|
|
429
|
-
argsText: content.argsText,
|
|
430
|
-
},
|
|
431
|
-
);
|
|
432
|
-
shouldWriteArgsText = false;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
if (shouldWriteArgsText) {
|
|
437
|
-
lastState = restartToolArgsStream({
|
|
438
|
-
toolCallId: content.toolCallId,
|
|
439
|
-
toolName: content.toolName,
|
|
440
|
-
state: lastState,
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
} else if (!content.argsText.startsWith(lastState.argsText)) {
|
|
444
|
-
// Check if this is key reordering (both are complete JSON)
|
|
445
|
-
// This happens when transitioning from streaming to complete state
|
|
446
|
-
// and the provider returns keys in a different order
|
|
447
|
-
if (
|
|
448
|
-
isArgsTextComplete(lastState.argsText) &&
|
|
449
|
-
isArgsTextComplete(content.argsText) &&
|
|
450
|
-
isEquivalentCompleteArgsText(
|
|
451
|
-
lastState.argsText,
|
|
452
|
-
content.argsText,
|
|
453
|
-
)
|
|
454
|
-
) {
|
|
455
|
-
const shouldClose = shouldCloseArgsStream({
|
|
456
|
-
toolName: content.toolName,
|
|
457
|
-
argsText: content.argsText,
|
|
458
|
-
hasResult: content.result !== undefined,
|
|
459
|
-
});
|
|
460
|
-
if (shouldClose) {
|
|
461
|
-
lastState.controller.argsText.close();
|
|
462
|
-
}
|
|
463
|
-
lastState = patchToolState(content.toolCallId, lastState, {
|
|
464
|
-
argsText: content.argsText,
|
|
465
|
-
argsComplete: shouldClose,
|
|
466
|
-
});
|
|
467
|
-
shouldWriteArgsText = false;
|
|
468
|
-
}
|
|
469
|
-
if (shouldWriteArgsText) {
|
|
470
|
-
if (process.env.NODE_ENV !== "production") {
|
|
471
|
-
console.warn(
|
|
472
|
-
"argsText rewrote previous snapshot, restarting tool args stream:",
|
|
473
|
-
{
|
|
474
|
-
previous: lastState.argsText,
|
|
475
|
-
next: content.argsText,
|
|
476
|
-
toolCallId: content.toolCallId,
|
|
477
|
-
},
|
|
478
|
-
);
|
|
479
|
-
}
|
|
480
|
-
lastState = restartToolArgsStream({
|
|
481
|
-
toolCallId: content.toolCallId,
|
|
482
|
-
toolName: content.toolName,
|
|
483
|
-
state: lastState,
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
if (shouldWriteArgsText) {
|
|
489
|
-
const argsTextDelta = content.argsText.slice(
|
|
490
|
-
lastState.argsText.length,
|
|
491
|
-
);
|
|
492
|
-
lastState.controller.argsText.append(argsTextDelta);
|
|
493
|
-
|
|
494
|
-
const shouldClose = shouldCloseArgsStream({
|
|
495
|
-
toolName: content.toolName,
|
|
496
|
-
argsText: content.argsText,
|
|
497
|
-
hasResult: content.result !== undefined,
|
|
498
|
-
});
|
|
499
|
-
if (shouldClose) {
|
|
500
|
-
lastState.controller.argsText.close();
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
lastState = patchToolState(content.toolCallId, lastState, {
|
|
504
|
-
argsText: content.argsText,
|
|
505
|
-
argsComplete: shouldClose,
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
}
|
|
545
|
+
if (!entry.argsComplete) {
|
|
546
|
+
const shouldClose = shouldCloseArgsStream({
|
|
547
|
+
toolName: content.toolName,
|
|
548
|
+
argsText: content.argsText,
|
|
549
|
+
hasResult,
|
|
550
|
+
});
|
|
551
|
+
if (shouldClose) {
|
|
552
|
+
entry.controller.argsText.close();
|
|
553
|
+
entry.argsText = content.argsText;
|
|
554
|
+
entry.argsComplete = true;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
};
|
|
509
558
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
hasResult: content.result !== undefined,
|
|
515
|
-
});
|
|
516
|
-
if (shouldClose) {
|
|
517
|
-
lastState.controller.argsText.close();
|
|
518
|
-
lastState = patchToolState(content.toolCallId, lastState, {
|
|
519
|
-
argsText: content.argsText,
|
|
520
|
-
argsComplete: true,
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
}
|
|
559
|
+
const processMessages = (
|
|
560
|
+
messages: readonly (typeof state.messages)[number][],
|
|
561
|
+
) => {
|
|
562
|
+
const isRestore = pendingRestoreRef.current;
|
|
524
563
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
564
|
+
messages.forEach((message) => {
|
|
565
|
+
message.content.forEach((content) => {
|
|
566
|
+
if (content.type !== "tool-call") return;
|
|
567
|
+
|
|
568
|
+
const existing = entriesRef.current.get(content.toolCallId);
|
|
569
|
+
|
|
570
|
+
if (isRestore) {
|
|
571
|
+
// Don't overwrite an already-active entry (e.g. live tool-call
|
|
572
|
+
// observed before this restore snapshot landed). Restore can only
|
|
573
|
+
// seed entries the runtime has never seen.
|
|
574
|
+
if (!existing?.controller) {
|
|
575
|
+
entriesRef.current.set(content.toolCallId, {
|
|
576
|
+
toolName: content.toolName,
|
|
577
|
+
argsText: content.argsText,
|
|
578
|
+
hasResult: content.result !== undefined,
|
|
579
|
+
});
|
|
540
580
|
}
|
|
581
|
+
if (content.messages) processMessages(content.messages);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
541
584
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
585
|
+
// Live snapshot.
|
|
586
|
+
let entry = existing;
|
|
587
|
+
|
|
588
|
+
if (entry && !entry.controller) {
|
|
589
|
+
// Restored entry observed in a live snapshot. Promote if its
|
|
590
|
+
// signature has changed; otherwise treat as still-historical.
|
|
591
|
+
const signatureChanged =
|
|
592
|
+
content.argsText !== entry.argsText ||
|
|
593
|
+
(content.result !== undefined) !== entry.hasResult;
|
|
594
|
+
if (!signatureChanged) {
|
|
595
|
+
if (content.messages) processMessages(content.messages);
|
|
596
|
+
return;
|
|
545
597
|
}
|
|
598
|
+
entriesRef.current.delete(content.toolCallId);
|
|
599
|
+
entry = undefined;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (!entry) {
|
|
603
|
+
entry = startActiveEntry(
|
|
604
|
+
content.toolCallId,
|
|
605
|
+
content.toolName,
|
|
606
|
+
content.result !== undefined,
|
|
607
|
+
);
|
|
546
608
|
}
|
|
609
|
+
|
|
610
|
+
processArgsText(entry, content);
|
|
611
|
+
|
|
612
|
+
if (content.result !== undefined && !entry.hasResult) {
|
|
613
|
+
// `entry` is in active phase from this point — either it was
|
|
614
|
+
// just created by `startActiveEntry` above, or it pre-existed
|
|
615
|
+
// and `processArgsText` preserved (or replaced via rewrite) its
|
|
616
|
+
// controller. Narrow once instead of asserting at every use.
|
|
617
|
+
const { controller: activeController } = entry;
|
|
618
|
+
if (!activeController) return;
|
|
619
|
+
entry.hasResult = true;
|
|
620
|
+
entry.argsComplete = true;
|
|
621
|
+
activeController.setResponse(
|
|
622
|
+
new ToolResponse({
|
|
623
|
+
result: content.result as ReadonlyJSONValue,
|
|
624
|
+
artifact: content.artifact as ReadonlyJSONValue | undefined,
|
|
625
|
+
isError: content.isError,
|
|
626
|
+
...(content.modelContent !== undefined
|
|
627
|
+
? { modelContent: content.modelContent }
|
|
628
|
+
: {}),
|
|
629
|
+
}),
|
|
630
|
+
);
|
|
631
|
+
activeController.close();
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (content.messages) processMessages(content.messages);
|
|
547
635
|
});
|
|
548
636
|
});
|
|
549
637
|
};
|
|
550
638
|
|
|
551
639
|
processMessages(state.messages);
|
|
552
640
|
|
|
553
|
-
|
|
554
|
-
isInitialState.current = false;
|
|
555
|
-
}
|
|
641
|
+
pendingRestoreRef.current = false;
|
|
556
642
|
}, [state, controller, getTools]);
|
|
557
643
|
|
|
558
644
|
const abort = (): Promise<void> => {
|
|
@@ -564,7 +650,6 @@ export function useToolInvocations({
|
|
|
564
650
|
acRef.current.abort();
|
|
565
651
|
acRef.current = new AbortController();
|
|
566
652
|
|
|
567
|
-
// Return a promise that resolves when all executing tools have settled
|
|
568
653
|
if (executingCountRef.current === 0) {
|
|
569
654
|
return Promise.resolve();
|
|
570
655
|
}
|
|
@@ -575,13 +660,17 @@ export function useToolInvocations({
|
|
|
575
660
|
|
|
576
661
|
return {
|
|
577
662
|
reset: () => {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
663
|
+
pendingRestoreRef.current = true;
|
|
664
|
+
entriesRef.current.clear();
|
|
665
|
+
// `skipExecuteStreamIdsRef` is not cleared: it has to outlive `reset()`
|
|
666
|
+
// so (a) any wrapper call still inbound through the stream pipeline
|
|
667
|
+
// continues to short-circuit `execute`, and (b) the consumer can
|
|
668
|
+
// recognize and drop any post-abort cancellation `result` chunks for
|
|
669
|
+
// pre-resolved streams whose entries have been cleared. Membership
|
|
670
|
+
// grows by one per pre-resolved tool call observed in the session.
|
|
581
671
|
void abort().finally(() => {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
ignoredResultToolCallIdsRef.current.clear();
|
|
672
|
+
executingRef.current.clear();
|
|
673
|
+
streamToLogicalRef.current.clear();
|
|
585
674
|
rewriteCounterRef.current = 0;
|
|
586
675
|
});
|
|
587
676
|
},
|