@tanstack/ai-client 0.18.5 → 0.18.6
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/esm/chat-client.d.ts +264 -0
- package/dist/esm/chat-client.js +1097 -0
- package/dist/esm/chat-client.js.map +1 -0
- package/dist/esm/client-persistor.d.ts +86 -0
- package/dist/esm/client-persistor.js +243 -0
- package/dist/esm/client-persistor.js.map +1 -0
- package/dist/esm/connection-adapters.d.ts +203 -0
- package/dist/esm/connection-adapters.js +611 -0
- package/dist/esm/connection-adapters.js.map +1 -0
- package/dist/esm/devtools-noop.d.ts +63 -0
- package/dist/esm/devtools-noop.js +141 -0
- package/dist/esm/devtools-noop.js.map +1 -0
- package/dist/esm/devtools.d.ts +276 -0
- package/dist/esm/devtools.js +1178 -0
- package/dist/esm/devtools.js.map +1 -0
- package/dist/esm/events.d.ts +129 -0
- package/dist/esm/events.js +227 -0
- package/dist/esm/events.js.map +1 -0
- package/dist/esm/generation-client.d.ts +99 -0
- package/dist/esm/generation-client.js +325 -0
- package/dist/esm/generation-client.js.map +1 -0
- package/dist/esm/generation-types.d.ts +242 -0
- package/dist/esm/generation-types.js +14 -0
- package/dist/esm/generation-types.js.map +1 -0
- package/dist/esm/index.d.ts +16 -0
- package/dist/esm/index.js +44 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/realtime-client.d.ts +98 -0
- package/dist/esm/realtime-client.js +403 -0
- package/dist/esm/realtime-client.js.map +1 -0
- package/dist/esm/realtime-types.d.ts +78 -0
- package/dist/esm/response-stream.d.ts +7 -0
- package/dist/esm/response-stream.js +32 -0
- package/dist/esm/response-stream.js.map +1 -0
- package/dist/esm/sse-parser.d.ts +8 -0
- package/dist/esm/sse-parser.js +52 -0
- package/dist/esm/sse-parser.js.map +1 -0
- package/dist/esm/sse-utils.d.ts +1 -0
- package/dist/esm/sse-utils.js +11 -0
- package/dist/esm/sse-utils.js.map +1 -0
- package/dist/esm/tool-types.d.ts +20 -0
- package/dist/esm/types.d.ts +419 -0
- package/dist/esm/types.js +11 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/video-generation-client.d.ts +110 -0
- package/dist/esm/video-generation-client.js +382 -0
- package/dist/esm/video-generation-client.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,1097 @@
|
|
|
1
|
+
import { StreamProcessor, normalizeToUIMessage, generateMessageId, convertSchemaToJsonSchema, isStandardSchema, parseWithStandardSchema } from "@tanstack/ai/client";
|
|
2
|
+
import { createNoOpChatDevtoolsBridge } from "./devtools-noop.js";
|
|
3
|
+
import { normalizeConnectionAdapter, getChunkRunId, fetcherToConnectionAdapter } from "./connection-adapters.js";
|
|
4
|
+
import { ChatPersistor } from "./client-persistor.js";
|
|
5
|
+
function resolveTransport(transport) {
|
|
6
|
+
const { connection, fetcher } = transport;
|
|
7
|
+
if (connection && fetcher) {
|
|
8
|
+
throw new Error(
|
|
9
|
+
"ChatClient: pass either `connection` or `fetcher`, not both."
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
if (connection) return connection;
|
|
13
|
+
if (fetcher) return fetcherToConnectionAdapter(fetcher);
|
|
14
|
+
throw new Error("ChatClient: either `connection` or `fetcher` is required.");
|
|
15
|
+
}
|
|
16
|
+
class ChatClient {
|
|
17
|
+
processor;
|
|
18
|
+
connection;
|
|
19
|
+
uniqueId;
|
|
20
|
+
threadId;
|
|
21
|
+
// All persistence concerns (hydrate / save / clear, plus suppression of late
|
|
22
|
+
// chunks after a mid-stream clear) live in ChatPersistor so this class stays
|
|
23
|
+
// focused on streaming. Undefined when no `persistence` adapter is configured.
|
|
24
|
+
persistor;
|
|
25
|
+
currentRunId = null;
|
|
26
|
+
// Track the legacy `body` option and the canonical `forwardedProps`
|
|
27
|
+
// option as separate slots so that `updateOptions({ forwardedProps })`
|
|
28
|
+
// doesn't wipe a previously-set `body` (and vice versa). They are
|
|
29
|
+
// merged on every send, with `forwardedProps` winning on key collision.
|
|
30
|
+
bodyOption = {};
|
|
31
|
+
forwardedPropsOption = {};
|
|
32
|
+
context = void 0;
|
|
33
|
+
pendingMessageBody = void 0;
|
|
34
|
+
isLoading = false;
|
|
35
|
+
isSubscribed = false;
|
|
36
|
+
error = void 0;
|
|
37
|
+
status = "ready";
|
|
38
|
+
connectionStatus = "disconnected";
|
|
39
|
+
abortController = null;
|
|
40
|
+
clientToolsRef;
|
|
41
|
+
devtoolsBridge;
|
|
42
|
+
/**
|
|
43
|
+
* Alias for `this.events`. The bridge installs an
|
|
44
|
+
* emitter that auto-attaches run/thread context and auto-emits a
|
|
45
|
+
* snapshot after every event, so chat-client only ever calls
|
|
46
|
+
* `this.events.X(...)` exactly like it did before devtools landed.
|
|
47
|
+
*/
|
|
48
|
+
events;
|
|
49
|
+
currentStreamId = null;
|
|
50
|
+
currentMessageId = null;
|
|
51
|
+
postStreamActions = [];
|
|
52
|
+
// Track pending client tool executions to await them before stream finalization
|
|
53
|
+
pendingToolExecutions = /* @__PURE__ */ new Map();
|
|
54
|
+
activeClientTools = null;
|
|
55
|
+
activeContext = void 0;
|
|
56
|
+
// Flag to deduplicate continuation checks during action draining
|
|
57
|
+
continuationPending = false;
|
|
58
|
+
subscriptionAbortController = null;
|
|
59
|
+
processingResolve = null;
|
|
60
|
+
errorReportedGeneration = null;
|
|
61
|
+
streamGeneration = 0;
|
|
62
|
+
// Tracks whether a queued checkForContinuation was skipped because
|
|
63
|
+
// continuationPending was true (chained approval scenario)
|
|
64
|
+
continuationSkipped = false;
|
|
65
|
+
draining = false;
|
|
66
|
+
sessionGenerating = false;
|
|
67
|
+
activeRunIds = /* @__PURE__ */ new Set();
|
|
68
|
+
devtoolsMounted = false;
|
|
69
|
+
callbacksRef;
|
|
70
|
+
constructor(options) {
|
|
71
|
+
this.uniqueId = options.id || this.generateUniqueId("chat");
|
|
72
|
+
this.threadId = options.threadId || this.generateUniqueId("thread");
|
|
73
|
+
if (options.persistence) {
|
|
74
|
+
this.persistor = new ChatPersistor(
|
|
75
|
+
options.persistence,
|
|
76
|
+
this.uniqueId,
|
|
77
|
+
(messages) => this.processor.setMessages(messages)
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
this.bodyOption = options.body || {};
|
|
81
|
+
this.forwardedPropsOption = options.forwardedProps || {};
|
|
82
|
+
this.context = options.context;
|
|
83
|
+
this.connection = normalizeConnectionAdapter(resolveTransport(options));
|
|
84
|
+
this.clientToolsRef = { current: /* @__PURE__ */ new Map() };
|
|
85
|
+
if (options.tools) {
|
|
86
|
+
for (const tool of options.tools) {
|
|
87
|
+
this.clientToolsRef.current.set(tool.name, tool);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
this.devtoolsBridge = (options.devtoolsBridgeFactory ?? createNoOpChatDevtoolsBridge)(this.buildDevtoolsBridgeOptions(options.devtools));
|
|
91
|
+
this.events = this.devtoolsBridge.events;
|
|
92
|
+
this.callbacksRef = {
|
|
93
|
+
current: {
|
|
94
|
+
onResponse: options.onResponse || (() => {
|
|
95
|
+
}),
|
|
96
|
+
onChunk: options.onChunk || (() => {
|
|
97
|
+
}),
|
|
98
|
+
onFinish: options.onFinish || (() => {
|
|
99
|
+
}),
|
|
100
|
+
onError: options.onError || (() => {
|
|
101
|
+
}),
|
|
102
|
+
onMessagesChange: options.onMessagesChange || (() => {
|
|
103
|
+
}),
|
|
104
|
+
onLoadingChange: options.onLoadingChange || (() => {
|
|
105
|
+
}),
|
|
106
|
+
onErrorChange: options.onErrorChange || (() => {
|
|
107
|
+
}),
|
|
108
|
+
onStatusChange: options.onStatusChange || (() => {
|
|
109
|
+
}),
|
|
110
|
+
onSubscriptionChange: options.onSubscriptionChange || (() => {
|
|
111
|
+
}),
|
|
112
|
+
onConnectionStatusChange: options.onConnectionStatusChange || (() => {
|
|
113
|
+
}),
|
|
114
|
+
onSessionGeneratingChange: options.onSessionGeneratingChange || (() => {
|
|
115
|
+
}),
|
|
116
|
+
onCustomEvent: options.onCustomEvent || (() => {
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const persistedMessages = this.persistor?.readInitial();
|
|
121
|
+
const initialMessages = Array.isArray(persistedMessages) ? persistedMessages : options.initialMessages;
|
|
122
|
+
this.processor = new StreamProcessor({
|
|
123
|
+
...options.streamProcessor?.chunkStrategy ? { chunkStrategy: options.streamProcessor.chunkStrategy } : {},
|
|
124
|
+
...initialMessages ? { initialMessages } : {},
|
|
125
|
+
events: {
|
|
126
|
+
onMessagesChange: (messages) => {
|
|
127
|
+
this.persistor?.notifyMessagesChanged(messages);
|
|
128
|
+
this.callbacksRef.current.onMessagesChange(messages);
|
|
129
|
+
},
|
|
130
|
+
onStreamStart: () => {
|
|
131
|
+
this.setStatus("streaming");
|
|
132
|
+
const assistantMessageId = this.processor.getCurrentAssistantMessageId();
|
|
133
|
+
if (!assistantMessageId) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const messages = this.processor.getMessages();
|
|
137
|
+
const assistantMessage = messages.find(
|
|
138
|
+
(m) => m.id === assistantMessageId
|
|
139
|
+
);
|
|
140
|
+
if (assistantMessage) {
|
|
141
|
+
this.currentMessageId = assistantMessage.id;
|
|
142
|
+
this.events.messageAppended(
|
|
143
|
+
assistantMessage,
|
|
144
|
+
this.currentStreamId || void 0
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
onStreamEnd: (message) => {
|
|
149
|
+
this.callbacksRef.current.onFinish(message);
|
|
150
|
+
this.setStatus("ready");
|
|
151
|
+
this.resolveProcessing();
|
|
152
|
+
},
|
|
153
|
+
onError: (error) => {
|
|
154
|
+
this.reportStreamError(error);
|
|
155
|
+
},
|
|
156
|
+
onTextUpdate: (messageId, content) => {
|
|
157
|
+
if (this.currentStreamId) {
|
|
158
|
+
this.events.textUpdated(this.currentStreamId, messageId, content);
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
onThinkingUpdate: (messageId, content) => {
|
|
162
|
+
if (this.currentStreamId) {
|
|
163
|
+
this.events.thinkingUpdated(
|
|
164
|
+
this.currentStreamId,
|
|
165
|
+
messageId,
|
|
166
|
+
content,
|
|
167
|
+
void 0
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
onStructuredOutputChange: (args) => {
|
|
172
|
+
const streamId = this.devtoolsBridge.resolveStreamId();
|
|
173
|
+
const eventName = args.phase === "start" ? "structured-output:started" : args.phase === "complete" ? "structured-output:completed" : args.phase === "error" ? "structured-output:errored" : "structured-output:updated";
|
|
174
|
+
this.currentMessageId = args.messageId;
|
|
175
|
+
this.events.structuredOutputChanged(
|
|
176
|
+
eventName,
|
|
177
|
+
streamId,
|
|
178
|
+
args.messageId,
|
|
179
|
+
{
|
|
180
|
+
status: args.status,
|
|
181
|
+
raw: args.raw,
|
|
182
|
+
...args.partial !== void 0 ? { partial: args.partial } : {},
|
|
183
|
+
...args.data !== void 0 ? { data: args.data } : {},
|
|
184
|
+
...args.reasoning !== void 0 ? { reasoning: args.reasoning } : {},
|
|
185
|
+
...args.errorMessage !== void 0 ? { errorMessage: args.errorMessage } : {},
|
|
186
|
+
...args.delta !== void 0 ? { delta: args.delta } : {}
|
|
187
|
+
}
|
|
188
|
+
);
|
|
189
|
+
},
|
|
190
|
+
onToolCallStateChange: (messageId, toolCallId, state, args) => {
|
|
191
|
+
const messages = this.processor.getMessages();
|
|
192
|
+
const message = messages.find((m) => m.id === messageId);
|
|
193
|
+
const toolCallPart = message?.parts.find(
|
|
194
|
+
(p) => p.type === "tool-call" && p.id === toolCallId
|
|
195
|
+
);
|
|
196
|
+
const toolName = toolCallPart?.name || "unknown";
|
|
197
|
+
if (this.currentStreamId) {
|
|
198
|
+
this.events.toolCallStateChanged(
|
|
199
|
+
this.currentStreamId,
|
|
200
|
+
messageId,
|
|
201
|
+
toolCallId,
|
|
202
|
+
toolName,
|
|
203
|
+
state,
|
|
204
|
+
args
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
onToolCall: (args) => {
|
|
209
|
+
const clientTools = this.activeClientTools ?? this.clientToolsRef.current;
|
|
210
|
+
const clientTool = clientTools.get(args.toolName);
|
|
211
|
+
const executeFunc = clientTool?.execute;
|
|
212
|
+
if (executeFunc) {
|
|
213
|
+
const runEventContext = this.devtoolsBridge.getCurrentRunEventContext();
|
|
214
|
+
const executionPromise = (async () => {
|
|
215
|
+
try {
|
|
216
|
+
const context = this.activeClientTools === null ? this.context : this.activeContext;
|
|
217
|
+
const output = await executeFunc(args.input, {
|
|
218
|
+
toolCallId: args.toolCallId,
|
|
219
|
+
context,
|
|
220
|
+
emitCustomEvent: () => {
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
await this.addToolResultForClientTool(
|
|
224
|
+
{
|
|
225
|
+
toolCallId: args.toolCallId,
|
|
226
|
+
tool: args.toolName,
|
|
227
|
+
output,
|
|
228
|
+
state: "output-available"
|
|
229
|
+
},
|
|
230
|
+
clientTool,
|
|
231
|
+
runEventContext
|
|
232
|
+
);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
await this.addToolResultForClientTool(
|
|
235
|
+
{
|
|
236
|
+
toolCallId: args.toolCallId,
|
|
237
|
+
tool: args.toolName,
|
|
238
|
+
output: null,
|
|
239
|
+
state: "output-error",
|
|
240
|
+
errorText: error.message
|
|
241
|
+
},
|
|
242
|
+
clientTool,
|
|
243
|
+
runEventContext
|
|
244
|
+
);
|
|
245
|
+
} finally {
|
|
246
|
+
this.pendingToolExecutions.delete(args.toolCallId);
|
|
247
|
+
}
|
|
248
|
+
})();
|
|
249
|
+
this.pendingToolExecutions.set(args.toolCallId, executionPromise);
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
onApprovalRequest: (args) => {
|
|
253
|
+
const streamId = this.devtoolsBridge.resolveStreamId();
|
|
254
|
+
const messageIdForApproval = this.findMessageIdForToolCall(args.toolCallId) ?? this.currentMessageId ?? "";
|
|
255
|
+
this.events.approvalRequested(
|
|
256
|
+
streamId,
|
|
257
|
+
messageIdForApproval,
|
|
258
|
+
args.toolCallId,
|
|
259
|
+
args.toolName,
|
|
260
|
+
args.input,
|
|
261
|
+
args.approvalId
|
|
262
|
+
);
|
|
263
|
+
},
|
|
264
|
+
onCustomEvent: (eventType, data, context) => {
|
|
265
|
+
this.callbacksRef.current.onCustomEvent(eventType, data, context);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
this.persistor?.hydrateAsync(persistedMessages);
|
|
270
|
+
}
|
|
271
|
+
mountDevtools() {
|
|
272
|
+
if (this.devtoolsMounted) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
this.devtoolsMounted = true;
|
|
276
|
+
this.devtoolsBridge.mountWithTools(this.processor.getMessages().length);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Drain a runId-less RUN_ERROR that belongs to a cleared run the client is
|
|
280
|
+
* still tracking. The persistor owns the cleared-run bookkeeping; the client
|
|
281
|
+
* owns the active-run / session / processing state.
|
|
282
|
+
*/
|
|
283
|
+
drainIgnoredRunlessChunk(chunk) {
|
|
284
|
+
if (chunk.type !== "RUN_ERROR") return;
|
|
285
|
+
const runId = this.persistor?.takeRunlessRunId();
|
|
286
|
+
if (!runId) return;
|
|
287
|
+
this.activeRunIds.delete(runId);
|
|
288
|
+
this.setSessionGenerating(this.activeRunIds.size > 0);
|
|
289
|
+
this.resolveProcessing();
|
|
290
|
+
}
|
|
291
|
+
updateRunLifecycle(chunk, options) {
|
|
292
|
+
if (chunk.type === "RUN_STARTED") {
|
|
293
|
+
const chunkRunId = getChunkRunId(chunk) ?? chunk.runId;
|
|
294
|
+
this.activeRunIds.add(chunkRunId);
|
|
295
|
+
this.persistor?.onRunStarted(chunkRunId);
|
|
296
|
+
this.setSessionGenerating(true);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (chunk.type !== "RUN_FINISHED" && chunk.type !== "RUN_ERROR") {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const runId = getChunkRunId(chunk);
|
|
303
|
+
if (runId) {
|
|
304
|
+
this.activeRunIds.delete(runId);
|
|
305
|
+
this.persistor?.onRunSettled(runId);
|
|
306
|
+
} else if (chunk.type === "RUN_ERROR") {
|
|
307
|
+
this.activeRunIds.clear();
|
|
308
|
+
this.persistor?.onSessionRunError();
|
|
309
|
+
}
|
|
310
|
+
this.setSessionGenerating(this.activeRunIds.size > 0);
|
|
311
|
+
if (options?.resolveProcessing !== false) {
|
|
312
|
+
this.resolveProcessing();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
generateUniqueId(prefix) {
|
|
316
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
|
317
|
+
}
|
|
318
|
+
setIsLoading(isLoading) {
|
|
319
|
+
this.isLoading = isLoading;
|
|
320
|
+
this.callbacksRef.current.onLoadingChange(isLoading);
|
|
321
|
+
this.events.loadingChanged(isLoading);
|
|
322
|
+
}
|
|
323
|
+
setStatus(status) {
|
|
324
|
+
this.status = status;
|
|
325
|
+
this.callbacksRef.current.onStatusChange(status);
|
|
326
|
+
this.devtoolsBridge.emitSnapshot();
|
|
327
|
+
}
|
|
328
|
+
setIsSubscribed(isSubscribed) {
|
|
329
|
+
this.isSubscribed = isSubscribed;
|
|
330
|
+
this.callbacksRef.current.onSubscriptionChange(isSubscribed);
|
|
331
|
+
this.devtoolsBridge.emitSnapshot();
|
|
332
|
+
}
|
|
333
|
+
setConnectionStatus(status) {
|
|
334
|
+
this.connectionStatus = status;
|
|
335
|
+
this.callbacksRef.current.onConnectionStatusChange(status);
|
|
336
|
+
this.devtoolsBridge.emitSnapshot();
|
|
337
|
+
}
|
|
338
|
+
setSessionGenerating(isGenerating) {
|
|
339
|
+
if (this.sessionGenerating === isGenerating) return;
|
|
340
|
+
this.sessionGenerating = isGenerating;
|
|
341
|
+
this.callbacksRef.current.onSessionGeneratingChange(isGenerating);
|
|
342
|
+
this.devtoolsBridge.emitSnapshot();
|
|
343
|
+
}
|
|
344
|
+
resetSessionGenerating() {
|
|
345
|
+
this.activeRunIds.clear();
|
|
346
|
+
this.persistor?.resetIgnored();
|
|
347
|
+
this.setSessionGenerating(false);
|
|
348
|
+
}
|
|
349
|
+
setError(error) {
|
|
350
|
+
this.error = error;
|
|
351
|
+
this.callbacksRef.current.onErrorChange(error);
|
|
352
|
+
this.events.errorChanged(error?.message || null);
|
|
353
|
+
}
|
|
354
|
+
buildDevtoolsBridgeOptions(devtools) {
|
|
355
|
+
return {
|
|
356
|
+
hookId: this.uniqueId,
|
|
357
|
+
clientId: this.uniqueId,
|
|
358
|
+
threadId: this.threadId,
|
|
359
|
+
metadata: {
|
|
360
|
+
hookName: devtools?.hookName ?? "useChat",
|
|
361
|
+
outputKind: devtools?.outputKind ?? "chat",
|
|
362
|
+
...devtools?.framework ? { framework: devtools.framework } : {},
|
|
363
|
+
...devtools?.name ? { name: devtools.name } : {}
|
|
364
|
+
},
|
|
365
|
+
getSnapshot: () => this.getDevtoolsSnapshot(),
|
|
366
|
+
getTools: () => this.clientToolsRef.current.values(),
|
|
367
|
+
getMessages: () => this.processor.getMessages(),
|
|
368
|
+
setMessages: (messages) => {
|
|
369
|
+
this.processor.setMessages(messages);
|
|
370
|
+
},
|
|
371
|
+
addToolResult: (toolCallId, output, errorText) => {
|
|
372
|
+
this.processor.addToolResult(toolCallId, output, errorText);
|
|
373
|
+
},
|
|
374
|
+
generateId: (prefix) => this.generateUniqueId(prefix)
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
getDevtoolsSnapshot() {
|
|
378
|
+
return {
|
|
379
|
+
messages: this.processor.getMessages(),
|
|
380
|
+
status: this.status,
|
|
381
|
+
isLoading: this.isLoading,
|
|
382
|
+
isSubscribed: this.isSubscribed,
|
|
383
|
+
connectionStatus: this.connectionStatus,
|
|
384
|
+
sessionGenerating: this.sessionGenerating,
|
|
385
|
+
activeRunIds: Array.from(this.activeRunIds),
|
|
386
|
+
...this.error ? { error: this.error.message } : {}
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
findMessageIdForToolCall(toolCallId) {
|
|
390
|
+
const messages = this.processor.getMessages();
|
|
391
|
+
for (const message of messages) {
|
|
392
|
+
const match = message.parts.find(
|
|
393
|
+
(part) => part.type === "tool-call" && part.id === toolCallId
|
|
394
|
+
);
|
|
395
|
+
if (match) return message.id;
|
|
396
|
+
}
|
|
397
|
+
return void 0;
|
|
398
|
+
}
|
|
399
|
+
abortSubscriptionLoop() {
|
|
400
|
+
this.subscriptionAbortController?.abort();
|
|
401
|
+
this.subscriptionAbortController = null;
|
|
402
|
+
}
|
|
403
|
+
resolveProcessing() {
|
|
404
|
+
this.processingResolve?.();
|
|
405
|
+
this.processingResolve = null;
|
|
406
|
+
}
|
|
407
|
+
cancelInFlightStream(options) {
|
|
408
|
+
this.abortController?.abort();
|
|
409
|
+
this.abortController = null;
|
|
410
|
+
if (options?.abortSubscription) {
|
|
411
|
+
this.abortSubscriptionLoop();
|
|
412
|
+
}
|
|
413
|
+
this.resolveProcessing();
|
|
414
|
+
this.setIsLoading(false);
|
|
415
|
+
if (options?.setReadyStatus) {
|
|
416
|
+
this.setStatus("ready");
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
reportStreamError(error) {
|
|
420
|
+
const alreadyReported = this.errorReportedGeneration === this.streamGeneration;
|
|
421
|
+
this.setError(error);
|
|
422
|
+
if (this.isLoading || this.status === "submitted" || this.status === "streaming") {
|
|
423
|
+
this.setStatus("error");
|
|
424
|
+
}
|
|
425
|
+
if (!alreadyReported) {
|
|
426
|
+
this.errorReportedGeneration = this.streamGeneration;
|
|
427
|
+
this.callbacksRef.current.onError(error);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Start the background subscription loop.
|
|
432
|
+
*/
|
|
433
|
+
startSubscription() {
|
|
434
|
+
this.subscriptionAbortController = new AbortController();
|
|
435
|
+
const signal = this.subscriptionAbortController.signal;
|
|
436
|
+
this.consumeSubscription(signal).catch((err) => {
|
|
437
|
+
if (err instanceof Error && err.name !== "AbortError") {
|
|
438
|
+
this.setConnectionStatus("error");
|
|
439
|
+
this.resetSessionGenerating();
|
|
440
|
+
this.setIsSubscribed(false);
|
|
441
|
+
this.reportStreamError(err);
|
|
442
|
+
}
|
|
443
|
+
this.resolveProcessing();
|
|
444
|
+
}).finally(() => {
|
|
445
|
+
if (this.subscriptionAbortController?.signal !== signal) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
this.subscriptionAbortController = null;
|
|
449
|
+
if (!signal.aborted && this.isSubscribed) {
|
|
450
|
+
this.setIsSubscribed(false);
|
|
451
|
+
if (this.connectionStatus !== "error") {
|
|
452
|
+
this.setConnectionStatus("disconnected");
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Consume chunks from the connection subscription.
|
|
459
|
+
*/
|
|
460
|
+
async consumeSubscription(signal) {
|
|
461
|
+
const stream = this.connection.subscribe(signal);
|
|
462
|
+
for await (const chunk of stream) {
|
|
463
|
+
if (signal.aborted) break;
|
|
464
|
+
if (this.connectionStatus === "connecting") {
|
|
465
|
+
this.setConnectionStatus("connected");
|
|
466
|
+
}
|
|
467
|
+
const shouldIgnore = this.persistor?.shouldIgnoreChunk(chunk) ?? false;
|
|
468
|
+
if (shouldIgnore) {
|
|
469
|
+
if (chunk.type === "RUN_FINISHED" || chunk.type === "RUN_ERROR") {
|
|
470
|
+
if (getChunkRunId(chunk)) {
|
|
471
|
+
this.updateRunLifecycle(chunk, { resolveProcessing: false });
|
|
472
|
+
} else {
|
|
473
|
+
this.drainIgnoredRunlessChunk(chunk);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
this.callbacksRef.current.onChunk(chunk);
|
|
479
|
+
this.devtoolsBridge.observeChunk(chunk);
|
|
480
|
+
this.processor.processChunk(chunk);
|
|
481
|
+
this.updateRunLifecycle(chunk);
|
|
482
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Ensure subscription loop is running, starting it if needed.
|
|
487
|
+
*/
|
|
488
|
+
ensureSubscription() {
|
|
489
|
+
if (!this.isSubscribed) {
|
|
490
|
+
this.subscribe();
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (!this.subscriptionAbortController || this.subscriptionAbortController.signal.aborted) {
|
|
494
|
+
this.subscribe({ restart: true });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Create a promise that resolves when onStreamEnd fires.
|
|
499
|
+
* Used by streamResponse to await processing completion.
|
|
500
|
+
*/
|
|
501
|
+
waitForProcessing() {
|
|
502
|
+
this.resolveProcessing();
|
|
503
|
+
return new Promise((resolve) => {
|
|
504
|
+
this.processingResolve = resolve;
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Send a message and stream the response.
|
|
509
|
+
* Supports both simple string content and multimodal content (images, audio, video, documents).
|
|
510
|
+
*
|
|
511
|
+
* @param content - The message content. Can be:
|
|
512
|
+
* - A simple string for text-only messages
|
|
513
|
+
* - A MultimodalContent object with content array and optional custom ID
|
|
514
|
+
* @param body - Optional body parameters to merge with the client's base body for this request.
|
|
515
|
+
* Uses shallow merge with per-message body taking priority.
|
|
516
|
+
*
|
|
517
|
+
* @example
|
|
518
|
+
* ```ts
|
|
519
|
+
* // Simple text message
|
|
520
|
+
* await client.sendMessage('Hello!')
|
|
521
|
+
*
|
|
522
|
+
* // Text message with custom body params
|
|
523
|
+
* await client.sendMessage('Hello!', { temperature: 0.7 })
|
|
524
|
+
*
|
|
525
|
+
* // Multimodal message with image
|
|
526
|
+
* await client.sendMessage({
|
|
527
|
+
* content: [
|
|
528
|
+
* { type: 'text', content: 'What is in this image?' },
|
|
529
|
+
* { type: 'image', source: { type: 'url', value: 'https://example.com/photo.jpg' } }
|
|
530
|
+
* ]
|
|
531
|
+
* })
|
|
532
|
+
*
|
|
533
|
+
* // Multimodal message with custom ID and body params
|
|
534
|
+
* await client.sendMessage(
|
|
535
|
+
* {
|
|
536
|
+
* content: [
|
|
537
|
+
* { type: 'text', content: 'Describe this audio' },
|
|
538
|
+
* { type: 'audio', source: { type: 'data', value: 'base64...' } }
|
|
539
|
+
* ],
|
|
540
|
+
* id: 'custom-message-id'
|
|
541
|
+
* },
|
|
542
|
+
* { model: 'gpt-4-audio' }
|
|
543
|
+
* )
|
|
544
|
+
* ```
|
|
545
|
+
*/
|
|
546
|
+
async sendMessage(content, body) {
|
|
547
|
+
this.mountDevtools();
|
|
548
|
+
const emptyMessage = typeof content === "string" && !content.trim();
|
|
549
|
+
if (emptyMessage || this.isLoading) {
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
const normalizedContent = this.normalizeMessageInput(content);
|
|
553
|
+
this.pendingMessageBody = body;
|
|
554
|
+
const userMessage = this.processor.addUserMessage(
|
|
555
|
+
normalizedContent.content,
|
|
556
|
+
normalizedContent.id
|
|
557
|
+
);
|
|
558
|
+
this.events.messageSent(userMessage.id, normalizedContent.content);
|
|
559
|
+
await this.streamResponse();
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Normalize the message input to extract content and optional id.
|
|
563
|
+
* Trims string content automatically.
|
|
564
|
+
*/
|
|
565
|
+
normalizeMessageInput(input) {
|
|
566
|
+
if (typeof input === "string") {
|
|
567
|
+
return { content: input.trim() };
|
|
568
|
+
}
|
|
569
|
+
return { content: input.content, id: input.id };
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Append a message and stream the response
|
|
573
|
+
*/
|
|
574
|
+
async append(message) {
|
|
575
|
+
this.mountDevtools();
|
|
576
|
+
const normalizedMessage = normalizeToUIMessage(message, generateMessageId);
|
|
577
|
+
if (normalizedMessage.role === "system") {
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const uiMessage = normalizedMessage;
|
|
581
|
+
this.events.messageAppended(uiMessage);
|
|
582
|
+
const messages = this.processor.getMessages();
|
|
583
|
+
this.processor.setMessages([...messages, uiMessage]);
|
|
584
|
+
this.devtoolsBridge.emitSnapshot();
|
|
585
|
+
if (this.isLoading) {
|
|
586
|
+
this.queuePostStreamAction(async () => {
|
|
587
|
+
await this.streamResponse();
|
|
588
|
+
});
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
await this.streamResponse();
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Stream a response from the LLM.
|
|
595
|
+
* Returns true if the stream completed successfully, false on abort or error.
|
|
596
|
+
*/
|
|
597
|
+
async streamResponse() {
|
|
598
|
+
if (this.isLoading) {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
const generation = ++this.streamGeneration;
|
|
602
|
+
const runId = `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
603
|
+
this.currentRunId = runId;
|
|
604
|
+
this.setIsLoading(true);
|
|
605
|
+
this.setStatus("submitted");
|
|
606
|
+
this.setError(void 0);
|
|
607
|
+
this.errorReportedGeneration = null;
|
|
608
|
+
this.abortController = new AbortController();
|
|
609
|
+
const signal = this.abortController.signal;
|
|
610
|
+
this.pendingToolExecutions.clear();
|
|
611
|
+
let streamCompletedSuccessfully = false;
|
|
612
|
+
let activeDevtoolsRunId = null;
|
|
613
|
+
let runTerminalEventEmitted = false;
|
|
614
|
+
try {
|
|
615
|
+
const messages = this.processor.getMessages();
|
|
616
|
+
const clientTools = new Map(this.clientToolsRef.current);
|
|
617
|
+
const runtimeContext = this.context;
|
|
618
|
+
await this.callbacksRef.current.onResponse();
|
|
619
|
+
if (signal.aborted) {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
const mergedBody = {
|
|
623
|
+
...this.bodyOption,
|
|
624
|
+
...this.forwardedPropsOption,
|
|
625
|
+
...this.pendingMessageBody
|
|
626
|
+
};
|
|
627
|
+
this.pendingMessageBody = void 0;
|
|
628
|
+
this.currentStreamId = this.generateUniqueId("stream");
|
|
629
|
+
this.devtoolsBridge.setCurrentStreamId(this.currentStreamId);
|
|
630
|
+
this.currentMessageId = null;
|
|
631
|
+
this.activeClientTools = clientTools;
|
|
632
|
+
this.activeContext = runtimeContext;
|
|
633
|
+
this.processor.prepareAssistantMessage();
|
|
634
|
+
this.ensureSubscription();
|
|
635
|
+
const processingComplete = this.waitForProcessing();
|
|
636
|
+
const runContext = {
|
|
637
|
+
threadId: this.threadId,
|
|
638
|
+
runId,
|
|
639
|
+
clientTools: Array.from(clientTools.values()).map((t) => ({
|
|
640
|
+
name: t.name,
|
|
641
|
+
description: t.description,
|
|
642
|
+
parameters: t.inputSchema ? convertSchemaToJsonSchema(t.inputSchema) : { type: "object" }
|
|
643
|
+
})),
|
|
644
|
+
forwardedProps: { ...mergedBody }
|
|
645
|
+
};
|
|
646
|
+
this.devtoolsBridge.beginRun(runContext.runId, this.threadId);
|
|
647
|
+
activeDevtoolsRunId = runContext.runId;
|
|
648
|
+
this.devtoolsBridge.emitRunLifecycle(
|
|
649
|
+
"run:created",
|
|
650
|
+
runContext.runId,
|
|
651
|
+
"created"
|
|
652
|
+
);
|
|
653
|
+
this.devtoolsBridge.emitRunLifecycle(
|
|
654
|
+
"run:started",
|
|
655
|
+
runContext.runId,
|
|
656
|
+
"started"
|
|
657
|
+
);
|
|
658
|
+
this.devtoolsBridge.emitSnapshot();
|
|
659
|
+
await this.connection.send(messages, mergedBody, signal, runContext);
|
|
660
|
+
await processingComplete;
|
|
661
|
+
if (generation !== this.streamGeneration) {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
if (this.status === "error") {
|
|
665
|
+
if (activeDevtoolsRunId) {
|
|
666
|
+
this.devtoolsBridge.emitRunLifecycle(
|
|
667
|
+
"run:errored",
|
|
668
|
+
activeDevtoolsRunId,
|
|
669
|
+
"errored",
|
|
670
|
+
this.error ? { error: this.error.message } : {}
|
|
671
|
+
);
|
|
672
|
+
runTerminalEventEmitted = true;
|
|
673
|
+
}
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
if (this.pendingToolExecutions.size > 0) {
|
|
677
|
+
await Promise.all(this.pendingToolExecutions.values());
|
|
678
|
+
}
|
|
679
|
+
this.processor.finalizeStream();
|
|
680
|
+
streamCompletedSuccessfully = true;
|
|
681
|
+
} catch (err) {
|
|
682
|
+
if (err instanceof Error) {
|
|
683
|
+
if (err.name === "AbortError") {
|
|
684
|
+
if (activeDevtoolsRunId) {
|
|
685
|
+
this.devtoolsBridge.emitRunLifecycle(
|
|
686
|
+
"run:cancelled",
|
|
687
|
+
activeDevtoolsRunId,
|
|
688
|
+
"cancelled"
|
|
689
|
+
);
|
|
690
|
+
runTerminalEventEmitted = true;
|
|
691
|
+
}
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
if (generation === this.streamGeneration) {
|
|
695
|
+
this.reportStreamError(err);
|
|
696
|
+
if (activeDevtoolsRunId) {
|
|
697
|
+
this.devtoolsBridge.emitRunLifecycle(
|
|
698
|
+
"run:errored",
|
|
699
|
+
activeDevtoolsRunId,
|
|
700
|
+
"errored",
|
|
701
|
+
{ error: err.message }
|
|
702
|
+
);
|
|
703
|
+
runTerminalEventEmitted = true;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
} finally {
|
|
708
|
+
if (generation === this.streamGeneration) {
|
|
709
|
+
this.currentStreamId = null;
|
|
710
|
+
this.devtoolsBridge.setCurrentStreamId(null);
|
|
711
|
+
this.currentMessageId = null;
|
|
712
|
+
this.currentRunId = null;
|
|
713
|
+
this.activeClientTools = null;
|
|
714
|
+
this.activeContext = void 0;
|
|
715
|
+
this.abortController = null;
|
|
716
|
+
this.setIsLoading(false);
|
|
717
|
+
this.pendingMessageBody = void 0;
|
|
718
|
+
if (activeDevtoolsRunId && !runTerminalEventEmitted) {
|
|
719
|
+
if (streamCompletedSuccessfully) {
|
|
720
|
+
this.devtoolsBridge.emitRunLifecycle(
|
|
721
|
+
"run:completed",
|
|
722
|
+
activeDevtoolsRunId,
|
|
723
|
+
"completed"
|
|
724
|
+
);
|
|
725
|
+
} else if (signal.aborted) {
|
|
726
|
+
this.devtoolsBridge.emitRunLifecycle(
|
|
727
|
+
"run:cancelled",
|
|
728
|
+
activeDevtoolsRunId,
|
|
729
|
+
"cancelled"
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
await this.drainPostStreamActions();
|
|
734
|
+
if (streamCompletedSuccessfully) {
|
|
735
|
+
const messages = this.processor.getMessages();
|
|
736
|
+
const lastPart = messages.at(-1)?.parts.at(-1);
|
|
737
|
+
const { finishReason } = this.processor.getState();
|
|
738
|
+
if (lastPart?.type === "tool-result" && finishReason !== "stop" && this.shouldAutoSend()) {
|
|
739
|
+
try {
|
|
740
|
+
await this.checkForContinuation();
|
|
741
|
+
} catch (error) {
|
|
742
|
+
console.error("Failed to continue flow after tool result:", error);
|
|
743
|
+
}
|
|
744
|
+
} else if (this.status !== "ready") {
|
|
745
|
+
this.setStatus("ready");
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return streamCompletedSuccessfully;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Start the client subscription loop.
|
|
754
|
+
* This controls the connection lifecycle independently from request lifecycle.
|
|
755
|
+
*/
|
|
756
|
+
subscribe(options) {
|
|
757
|
+
const restart = options?.restart === true;
|
|
758
|
+
if (this.isSubscribed && !restart) {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (this.isSubscribed && restart) {
|
|
762
|
+
this.abortSubscriptionLoop();
|
|
763
|
+
}
|
|
764
|
+
this.setIsSubscribed(true);
|
|
765
|
+
this.setConnectionStatus("connecting");
|
|
766
|
+
this.startSubscription();
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Unsubscribe and fully tear down live behavior.
|
|
770
|
+
* This aborts an in-flight request and the subscription loop.
|
|
771
|
+
*/
|
|
772
|
+
unsubscribe() {
|
|
773
|
+
this.cancelInFlightStream({
|
|
774
|
+
setReadyStatus: true,
|
|
775
|
+
abortSubscription: true
|
|
776
|
+
});
|
|
777
|
+
this.resetSessionGenerating();
|
|
778
|
+
this.setIsSubscribed(false);
|
|
779
|
+
this.setConnectionStatus("disconnected");
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Reload the last assistant message
|
|
783
|
+
*/
|
|
784
|
+
async reload() {
|
|
785
|
+
const messages = this.processor.getMessages();
|
|
786
|
+
if (messages.length === 0) return;
|
|
787
|
+
const lastUserMessageIndex = messages.findLastIndex(
|
|
788
|
+
(m) => m.role === "user"
|
|
789
|
+
);
|
|
790
|
+
if (lastUserMessageIndex === -1) return;
|
|
791
|
+
if (this.isLoading) {
|
|
792
|
+
this.cancelInFlightStream();
|
|
793
|
+
}
|
|
794
|
+
this.events.reloaded(lastUserMessageIndex);
|
|
795
|
+
this.processor.removeMessagesAfter(lastUserMessageIndex);
|
|
796
|
+
this.devtoolsBridge.emitSnapshot();
|
|
797
|
+
await this.streamResponse();
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Stop the current stream
|
|
801
|
+
*/
|
|
802
|
+
stop() {
|
|
803
|
+
const hadLocalStream = this.abortController !== null;
|
|
804
|
+
this.cancelInFlightStream({ setReadyStatus: true });
|
|
805
|
+
if (hadLocalStream) {
|
|
806
|
+
this.resetSessionGenerating();
|
|
807
|
+
}
|
|
808
|
+
this.events.stopped();
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Clear all messages
|
|
812
|
+
*/
|
|
813
|
+
clear() {
|
|
814
|
+
if (this.persistor) {
|
|
815
|
+
this.persistor.snapshotClear({
|
|
816
|
+
messages: this.processor.getMessages(),
|
|
817
|
+
activeRunIds: this.activeRunIds,
|
|
818
|
+
currentRunId: this.currentRunId
|
|
819
|
+
});
|
|
820
|
+
if (this.isLoading) {
|
|
821
|
+
this.cancelInFlightStream({ setReadyStatus: true });
|
|
822
|
+
this.resetSessionGenerating();
|
|
823
|
+
} else if (this.activeRunIds.size > 0) {
|
|
824
|
+
this.resetSessionGenerating();
|
|
825
|
+
}
|
|
826
|
+
this.persistor.beginClear();
|
|
827
|
+
}
|
|
828
|
+
this.processor.clearMessages();
|
|
829
|
+
this.persistor?.remove();
|
|
830
|
+
this.setError(void 0);
|
|
831
|
+
this.events.messagesCleared();
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Add the result of a client-side tool execution
|
|
835
|
+
*/
|
|
836
|
+
async addToolResult(result) {
|
|
837
|
+
const clientTool = this.clientToolsRef.current.get(result.tool);
|
|
838
|
+
await this.addToolResultForClientTool(result, clientTool);
|
|
839
|
+
}
|
|
840
|
+
async addToolResultForClientTool(result, clientTool, context) {
|
|
841
|
+
if (clientTool && result.state !== "output-error") {
|
|
842
|
+
try {
|
|
843
|
+
result = {
|
|
844
|
+
...result,
|
|
845
|
+
output: this.validateClientToolOutput(clientTool, result.output)
|
|
846
|
+
};
|
|
847
|
+
} catch (error) {
|
|
848
|
+
result = {
|
|
849
|
+
...result,
|
|
850
|
+
output: null,
|
|
851
|
+
state: "output-error",
|
|
852
|
+
errorText: error.message
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
this.events.toolResultAdded(
|
|
857
|
+
result.toolCallId,
|
|
858
|
+
result.tool,
|
|
859
|
+
result.output,
|
|
860
|
+
result.state || "output-available",
|
|
861
|
+
context
|
|
862
|
+
);
|
|
863
|
+
this.processor.addToolResult(
|
|
864
|
+
result.toolCallId,
|
|
865
|
+
result.output,
|
|
866
|
+
result.state === "output-error" ? result.errorText || "Tool execution failed" : void 0
|
|
867
|
+
);
|
|
868
|
+
if (this.isLoading) {
|
|
869
|
+
this.queuePostStreamAction(() => this.checkForContinuation());
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
await this.checkForContinuation();
|
|
873
|
+
}
|
|
874
|
+
validateClientToolOutput(clientTool, output) {
|
|
875
|
+
if (clientTool.outputSchema && isStandardSchema(clientTool.outputSchema)) {
|
|
876
|
+
return parseWithStandardSchema(clientTool.outputSchema, output);
|
|
877
|
+
}
|
|
878
|
+
return output;
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Respond to a tool approval request
|
|
882
|
+
*/
|
|
883
|
+
async addToolApprovalResponse(response) {
|
|
884
|
+
const messages = this.processor.getMessages();
|
|
885
|
+
let foundToolCallId;
|
|
886
|
+
for (const msg of messages) {
|
|
887
|
+
const toolCallPart = msg.parts.find(
|
|
888
|
+
(p) => p.type === "tool-call" && p.approval?.id === response.id
|
|
889
|
+
);
|
|
890
|
+
if (toolCallPart) {
|
|
891
|
+
foundToolCallId = toolCallPart.id;
|
|
892
|
+
break;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (foundToolCallId) {
|
|
896
|
+
this.events.toolApprovalResponded(
|
|
897
|
+
response.id,
|
|
898
|
+
foundToolCallId,
|
|
899
|
+
response.approved
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
this.processor.addToolApprovalResponse(response.id, response.approved);
|
|
903
|
+
this.devtoolsBridge.emitSnapshot();
|
|
904
|
+
if (this.isLoading) {
|
|
905
|
+
this.queuePostStreamAction(() => this.checkForContinuation());
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
await this.checkForContinuation();
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Queue an action to be executed after the current stream ends
|
|
912
|
+
*/
|
|
913
|
+
queuePostStreamAction(action) {
|
|
914
|
+
this.postStreamActions.push(action);
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Drain and execute all queued post-stream actions
|
|
918
|
+
*/
|
|
919
|
+
async drainPostStreamActions() {
|
|
920
|
+
if (this.draining) return;
|
|
921
|
+
this.draining = true;
|
|
922
|
+
try {
|
|
923
|
+
let action;
|
|
924
|
+
while ((action = this.postStreamActions.shift()) !== void 0) {
|
|
925
|
+
await action();
|
|
926
|
+
}
|
|
927
|
+
} finally {
|
|
928
|
+
this.draining = false;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Check if we should continue the flow and do so if needed
|
|
933
|
+
*/
|
|
934
|
+
async checkForContinuation() {
|
|
935
|
+
if (this.continuationPending || this.isLoading) {
|
|
936
|
+
this.continuationSkipped = true;
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
if (this.shouldAutoSend()) {
|
|
940
|
+
this.continuationPending = true;
|
|
941
|
+
this.continuationSkipped = false;
|
|
942
|
+
let succeeded = false;
|
|
943
|
+
try {
|
|
944
|
+
succeeded = await this.streamResponse();
|
|
945
|
+
} finally {
|
|
946
|
+
this.continuationPending = false;
|
|
947
|
+
}
|
|
948
|
+
if (this.continuationSkipped && succeeded) {
|
|
949
|
+
this.continuationSkipped = false;
|
|
950
|
+
await this.checkForContinuation();
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Check if all tool calls are complete and we should auto-send.
|
|
956
|
+
* Requires that there is at least one tool call in the last assistant message;
|
|
957
|
+
* a text-only response has nothing to auto-send.
|
|
958
|
+
*/
|
|
959
|
+
shouldAutoSend() {
|
|
960
|
+
const messages = this.processor.getMessages();
|
|
961
|
+
const lastAssistant = messages.findLast(
|
|
962
|
+
(m) => m.role === "assistant"
|
|
963
|
+
);
|
|
964
|
+
if (!lastAssistant) return false;
|
|
965
|
+
const hasToolCalls = lastAssistant.parts.some(
|
|
966
|
+
(p) => p.type === "tool-call"
|
|
967
|
+
);
|
|
968
|
+
if (!hasToolCalls) return false;
|
|
969
|
+
return this.processor.areAllToolsComplete();
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Get current messages
|
|
973
|
+
*/
|
|
974
|
+
getMessages() {
|
|
975
|
+
return this.processor.getMessages();
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Get loading state
|
|
979
|
+
*/
|
|
980
|
+
getIsLoading() {
|
|
981
|
+
return this.isLoading;
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Get current status
|
|
985
|
+
*/
|
|
986
|
+
getStatus() {
|
|
987
|
+
return this.status;
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Get whether the subscription loop is active
|
|
991
|
+
*/
|
|
992
|
+
getIsSubscribed() {
|
|
993
|
+
return this.isSubscribed;
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Get current connection lifecycle status
|
|
997
|
+
*/
|
|
998
|
+
getConnectionStatus() {
|
|
999
|
+
return this.connectionStatus;
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Whether the shared session is actively generating.
|
|
1003
|
+
* Derived from stream run events (RUN_STARTED / RUN_FINISHED / RUN_ERROR).
|
|
1004
|
+
* Unlike `isLoading` (request-local), this reflects shared generation
|
|
1005
|
+
* activity visible to all subscribers (e.g. across tabs/devices).
|
|
1006
|
+
*/
|
|
1007
|
+
getSessionGenerating() {
|
|
1008
|
+
return this.sessionGenerating;
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Get current error
|
|
1012
|
+
*/
|
|
1013
|
+
getError() {
|
|
1014
|
+
return this.error;
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Manually set messages
|
|
1018
|
+
*/
|
|
1019
|
+
setMessagesManually(messages) {
|
|
1020
|
+
this.processor.setMessages(messages);
|
|
1021
|
+
this.devtoolsBridge.emitSnapshot();
|
|
1022
|
+
}
|
|
1023
|
+
updateOptions(options) {
|
|
1024
|
+
if (options.connection !== void 0 || options.fetcher !== void 0) {
|
|
1025
|
+
const wasSubscribed = this.isSubscribed;
|
|
1026
|
+
if (this.isLoading) {
|
|
1027
|
+
this.cancelInFlightStream({
|
|
1028
|
+
setReadyStatus: true,
|
|
1029
|
+
abortSubscription: true
|
|
1030
|
+
});
|
|
1031
|
+
} else if (wasSubscribed) {
|
|
1032
|
+
this.abortSubscriptionLoop();
|
|
1033
|
+
}
|
|
1034
|
+
this.resetSessionGenerating();
|
|
1035
|
+
this.setIsSubscribed(false);
|
|
1036
|
+
this.setConnectionStatus("disconnected");
|
|
1037
|
+
this.connection = normalizeConnectionAdapter(
|
|
1038
|
+
resolveTransport({
|
|
1039
|
+
connection: options.connection,
|
|
1040
|
+
fetcher: options.fetcher
|
|
1041
|
+
})
|
|
1042
|
+
);
|
|
1043
|
+
if (wasSubscribed) {
|
|
1044
|
+
this.subscribe();
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
if (options.body !== void 0) {
|
|
1048
|
+
this.bodyOption = options.body;
|
|
1049
|
+
}
|
|
1050
|
+
if (options.forwardedProps !== void 0) {
|
|
1051
|
+
this.forwardedPropsOption = options.forwardedProps;
|
|
1052
|
+
}
|
|
1053
|
+
if ("context" in options) {
|
|
1054
|
+
this.context = options.context;
|
|
1055
|
+
}
|
|
1056
|
+
if (options.tools !== void 0) {
|
|
1057
|
+
this.clientToolsRef.current = /* @__PURE__ */ new Map();
|
|
1058
|
+
for (const tool of options.tools) {
|
|
1059
|
+
this.clientToolsRef.current.set(tool.name, tool);
|
|
1060
|
+
}
|
|
1061
|
+
this.devtoolsBridge.notifyToolsChanged();
|
|
1062
|
+
}
|
|
1063
|
+
if (options.onResponse !== void 0) {
|
|
1064
|
+
this.callbacksRef.current.onResponse = options.onResponse;
|
|
1065
|
+
}
|
|
1066
|
+
if (options.onChunk !== void 0) {
|
|
1067
|
+
this.callbacksRef.current.onChunk = options.onChunk;
|
|
1068
|
+
}
|
|
1069
|
+
if (options.onFinish !== void 0) {
|
|
1070
|
+
this.callbacksRef.current.onFinish = options.onFinish;
|
|
1071
|
+
}
|
|
1072
|
+
if (options.onError !== void 0) {
|
|
1073
|
+
this.callbacksRef.current.onError = options.onError;
|
|
1074
|
+
}
|
|
1075
|
+
if (options.onSubscriptionChange !== void 0) {
|
|
1076
|
+
this.callbacksRef.current.onSubscriptionChange = options.onSubscriptionChange;
|
|
1077
|
+
}
|
|
1078
|
+
if (options.onConnectionStatusChange !== void 0) {
|
|
1079
|
+
this.callbacksRef.current.onConnectionStatusChange = options.onConnectionStatusChange;
|
|
1080
|
+
}
|
|
1081
|
+
if (options.onSessionGeneratingChange !== void 0) {
|
|
1082
|
+
this.callbacksRef.current.onSessionGeneratingChange = options.onSessionGeneratingChange;
|
|
1083
|
+
}
|
|
1084
|
+
if (options.onCustomEvent !== void 0) {
|
|
1085
|
+
this.callbacksRef.current.onCustomEvent = options.onCustomEvent;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
dispose() {
|
|
1089
|
+
this.unsubscribe();
|
|
1090
|
+
this.devtoolsBridge.dispose();
|
|
1091
|
+
this.devtoolsMounted = false;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
export {
|
|
1095
|
+
ChatClient
|
|
1096
|
+
};
|
|
1097
|
+
//# sourceMappingURL=chat-client.js.map
|