@better-agent/client 0.1.0-canary.0
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 +3 -0
- package/dist/controller-CJ79_cSR.d.mts +657 -0
- package/dist/controller-CJ79_cSR.d.mts.map +1 -0
- package/dist/controller-Cf_JhTdJ.mjs +2123 -0
- package/dist/controller-Cf_JhTdJ.mjs.map +1 -0
- package/dist/index.d.mts +266 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +643 -0
- package/dist/index.mjs.map +1 -0
- package/dist/preact/index.d.mts +96 -0
- package/dist/preact/index.d.mts.map +1 -0
- package/dist/preact/index.mjs +134 -0
- package/dist/preact/index.mjs.map +1 -0
- package/dist/react/index.d.mts +96 -0
- package/dist/react/index.d.mts.map +1 -0
- package/dist/react/index.mjs +123 -0
- package/dist/react/index.mjs.map +1 -0
- package/dist/solid/index.d.mts +99 -0
- package/dist/solid/index.d.mts.map +1 -0
- package/dist/solid/index.mjs +130 -0
- package/dist/solid/index.mjs.map +1 -0
- package/dist/svelte/index.d.mts +83 -0
- package/dist/svelte/index.d.mts.map +1 -0
- package/dist/svelte/index.mjs +107 -0
- package/dist/svelte/index.mjs.map +1 -0
- package/dist/utils-CiHUj_BW.mjs +34 -0
- package/dist/utils-CiHUj_BW.mjs.map +1 -0
- package/dist/vue/index.d.mts +95 -0
- package/dist/vue/index.d.mts.map +1 -0
- package/dist/vue/index.mjs +155 -0
- package/dist/vue/index.mjs.map +1 -0
- package/package.json +117 -0
|
@@ -0,0 +1,2123 @@
|
|
|
1
|
+
//#region src/core/browser-lifecycle.ts
|
|
2
|
+
const markPageTeardown = () => {
|
|
3
|
+
if (typeof window !== "undefined") window.__baPageTeardown = true;
|
|
4
|
+
};
|
|
5
|
+
/** Installs one browser teardown tracker for refresh/navigation. */
|
|
6
|
+
const ensureBrowserTeardownTracking = () => {
|
|
7
|
+
if (typeof window === "undefined") return;
|
|
8
|
+
window.__baPageTeardown ??= false;
|
|
9
|
+
if (window.__baPageTeardownInstalled) return;
|
|
10
|
+
window.__baPageTeardownInstalled = true;
|
|
11
|
+
window.addEventListener("pagehide", markPageTeardown);
|
|
12
|
+
window.addEventListener("beforeunload", markPageTeardown);
|
|
13
|
+
};
|
|
14
|
+
/** Returns true while the current browser page is being torn down. */
|
|
15
|
+
const isBrowserPageTearingDown = () => typeof window !== "undefined" && window.__baPageTeardown === true;
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/core/error.ts
|
|
19
|
+
/** Internal error used to mark broken stream transport. */
|
|
20
|
+
var StreamDisconnectError = class extends Error {
|
|
21
|
+
cause;
|
|
22
|
+
constructor(message, cause) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "StreamDisconnectError";
|
|
25
|
+
this.cause = cause;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
29
|
+
const stripSurfacePrefix = (message) => message.replace(/^(run failed|stream failed):\s*/i, "");
|
|
30
|
+
const extractMessage = (error) => {
|
|
31
|
+
if (error instanceof Error && typeof error.message === "string" && error.message.trim()) return stripSurfacePrefix(error.message);
|
|
32
|
+
if (!isRecord(error)) return void 0;
|
|
33
|
+
const message = typeof error.message === "string" && error.message || typeof error.detail === "string" && error.detail || typeof error.title === "string" && error.title;
|
|
34
|
+
return typeof message === "string" && message.trim().length > 0 ? stripSurfacePrefix(message) : void 0;
|
|
35
|
+
};
|
|
36
|
+
/** Normalizes unknown failures into `AgentClientError`. */
|
|
37
|
+
const toAgentClientError = (error, fallbackMessage = "Run failed.") => {
|
|
38
|
+
if (error instanceof Error) {
|
|
39
|
+
const enriched = error;
|
|
40
|
+
enriched.message = stripSurfacePrefix(enriched.message);
|
|
41
|
+
if (typeof enriched.detail === "string" && enriched.detail.length > 0) enriched.detail = stripSurfacePrefix(enriched.detail);
|
|
42
|
+
if (enriched.raw === void 0) enriched.raw = error;
|
|
43
|
+
return enriched;
|
|
44
|
+
}
|
|
45
|
+
const message = extractMessage(error) ?? fallbackMessage;
|
|
46
|
+
const next = new Error(message);
|
|
47
|
+
next.raw = error;
|
|
48
|
+
if (!isRecord(error)) return next;
|
|
49
|
+
if (typeof error.code === "string") next.code = error.code;
|
|
50
|
+
if (typeof error.status === "number") next.status = error.status;
|
|
51
|
+
if (typeof error.retryable === "boolean") next.retryable = error.retryable;
|
|
52
|
+
if (typeof error.title === "string") next.title = error.title;
|
|
53
|
+
if (typeof error.detail === "string") next.detail = stripSurfacePrefix(error.detail);
|
|
54
|
+
if (typeof error.traceId === "string") next.traceId = error.traceId;
|
|
55
|
+
if (Array.isArray(error.issues)) next.issues = error.issues;
|
|
56
|
+
if (isRecord(error.context)) next.context = error.context;
|
|
57
|
+
if (Array.isArray(error.trace)) next.trace = error.trace;
|
|
58
|
+
return next;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Resolves a user-facing message from an event error payload.
|
|
62
|
+
*/
|
|
63
|
+
const getEventErrorMessage = (error) => toAgentClientError(error).message;
|
|
64
|
+
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/core/utils.ts
|
|
67
|
+
const withProviderMetadata = (part) => part.providerMetadata !== void 0 && typeof part.providerMetadata === "object" && part.providerMetadata !== null ? { providerMetadata: part.providerMetadata } : {};
|
|
68
|
+
/** Converts one persisted part into a UI part. */
|
|
69
|
+
const contentPartToUIPart = (part) => {
|
|
70
|
+
if (typeof part !== "object" || part === null || typeof part.type !== "string") return null;
|
|
71
|
+
const record = part;
|
|
72
|
+
switch (record.type) {
|
|
73
|
+
case "text": return typeof record.text === "string" ? {
|
|
74
|
+
type: "text",
|
|
75
|
+
text: record.text,
|
|
76
|
+
...withProviderMetadata(record),
|
|
77
|
+
state: "complete"
|
|
78
|
+
} : null;
|
|
79
|
+
case "image": return {
|
|
80
|
+
type: "image",
|
|
81
|
+
source: record.source,
|
|
82
|
+
...withProviderMetadata(record),
|
|
83
|
+
state: "complete"
|
|
84
|
+
};
|
|
85
|
+
case "file": return {
|
|
86
|
+
type: "file",
|
|
87
|
+
source: record.source,
|
|
88
|
+
...withProviderMetadata(record),
|
|
89
|
+
state: "complete"
|
|
90
|
+
};
|
|
91
|
+
case "audio": return {
|
|
92
|
+
type: "audio",
|
|
93
|
+
source: record.source,
|
|
94
|
+
...withProviderMetadata(record),
|
|
95
|
+
state: "complete"
|
|
96
|
+
};
|
|
97
|
+
case "video": return {
|
|
98
|
+
type: "video",
|
|
99
|
+
source: record.source,
|
|
100
|
+
...withProviderMetadata(record),
|
|
101
|
+
state: "complete"
|
|
102
|
+
};
|
|
103
|
+
case "embedding": return Array.isArray(record.embedding) && record.embedding.every((value) => typeof value === "number") ? {
|
|
104
|
+
type: "embedding",
|
|
105
|
+
embedding: record.embedding,
|
|
106
|
+
...withProviderMetadata(record),
|
|
107
|
+
state: "complete"
|
|
108
|
+
} : null;
|
|
109
|
+
case "transcript": return typeof record.text === "string" ? {
|
|
110
|
+
type: "transcript",
|
|
111
|
+
text: record.text,
|
|
112
|
+
...Array.isArray(record.segments) ? { segments: record.segments } : {},
|
|
113
|
+
...withProviderMetadata(record),
|
|
114
|
+
state: "complete"
|
|
115
|
+
} : null;
|
|
116
|
+
case "reasoning": return typeof record.text === "string" && (record.visibility === "summary" || record.visibility === "full") ? {
|
|
117
|
+
type: "reasoning",
|
|
118
|
+
text: record.text,
|
|
119
|
+
visibility: record.visibility,
|
|
120
|
+
...typeof record.provider === "string" ? { provider: record.provider } : {},
|
|
121
|
+
...withProviderMetadata(record),
|
|
122
|
+
state: "complete"
|
|
123
|
+
} : null;
|
|
124
|
+
default: return null;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const toModelInputParts = (message) => {
|
|
128
|
+
const out = [];
|
|
129
|
+
for (const part of message.parts) {
|
|
130
|
+
const providerMetadata = "providerMetadata" in part ? part.providerMetadata : void 0;
|
|
131
|
+
if (part.type === "text") {
|
|
132
|
+
out.push({
|
|
133
|
+
type: "text",
|
|
134
|
+
text: part.text,
|
|
135
|
+
...providerMetadata !== void 0 ? { providerMetadata } : {}
|
|
136
|
+
});
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (part.type === "audio") {
|
|
140
|
+
if (part.source.kind === "url") {
|
|
141
|
+
out.push({
|
|
142
|
+
type: "audio",
|
|
143
|
+
source: {
|
|
144
|
+
kind: "url",
|
|
145
|
+
url: part.source.url
|
|
146
|
+
},
|
|
147
|
+
...providerMetadata !== void 0 ? { providerMetadata } : {}
|
|
148
|
+
});
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
out.push({
|
|
152
|
+
type: "audio",
|
|
153
|
+
source: {
|
|
154
|
+
kind: "base64",
|
|
155
|
+
data: part.source.data,
|
|
156
|
+
mimeType: part.source.mimeType ?? "audio/wav"
|
|
157
|
+
},
|
|
158
|
+
...providerMetadata !== void 0 ? { providerMetadata } : {}
|
|
159
|
+
});
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (part.type === "image") {
|
|
163
|
+
if (part.source.kind === "url") {
|
|
164
|
+
out.push({
|
|
165
|
+
type: "image",
|
|
166
|
+
source: {
|
|
167
|
+
kind: "url",
|
|
168
|
+
url: part.source.url
|
|
169
|
+
},
|
|
170
|
+
...providerMetadata !== void 0 ? { providerMetadata } : {}
|
|
171
|
+
});
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
out.push({
|
|
175
|
+
type: "image",
|
|
176
|
+
source: {
|
|
177
|
+
kind: "base64",
|
|
178
|
+
data: part.source.data,
|
|
179
|
+
mimeType: part.source.mimeType ?? "image/png"
|
|
180
|
+
},
|
|
181
|
+
...providerMetadata !== void 0 ? { providerMetadata } : {}
|
|
182
|
+
});
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (part.type === "file") {
|
|
186
|
+
if (part.source.kind === "url") {
|
|
187
|
+
out.push({
|
|
188
|
+
type: "file",
|
|
189
|
+
source: {
|
|
190
|
+
kind: "url",
|
|
191
|
+
url: part.source.url,
|
|
192
|
+
...part.source.mimeType ? { mimeType: part.source.mimeType } : {},
|
|
193
|
+
...part.source.filename ? { filename: part.source.filename } : {}
|
|
194
|
+
},
|
|
195
|
+
...providerMetadata !== void 0 ? { providerMetadata } : {}
|
|
196
|
+
});
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (part.source.kind === "provider-file") {
|
|
200
|
+
out.push({
|
|
201
|
+
type: "file",
|
|
202
|
+
source: {
|
|
203
|
+
kind: "provider-file",
|
|
204
|
+
ref: part.source.ref,
|
|
205
|
+
...part.source.mimeType ? { mimeType: part.source.mimeType } : {},
|
|
206
|
+
...part.source.filename ? { filename: part.source.filename } : {}
|
|
207
|
+
},
|
|
208
|
+
...providerMetadata !== void 0 ? { providerMetadata } : {}
|
|
209
|
+
});
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
out.push({
|
|
213
|
+
type: "file",
|
|
214
|
+
source: {
|
|
215
|
+
kind: "base64",
|
|
216
|
+
data: part.source.data,
|
|
217
|
+
mimeType: part.source.mimeType,
|
|
218
|
+
...part.source.filename ? { filename: part.source.filename } : {}
|
|
219
|
+
},
|
|
220
|
+
...providerMetadata !== void 0 ? { providerMetadata } : {}
|
|
221
|
+
});
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (part.type === "video") {
|
|
225
|
+
if (part.source.kind === "url") {
|
|
226
|
+
out.push({
|
|
227
|
+
type: "video",
|
|
228
|
+
source: {
|
|
229
|
+
kind: "url",
|
|
230
|
+
url: part.source.url
|
|
231
|
+
},
|
|
232
|
+
...providerMetadata !== void 0 ? { providerMetadata } : {}
|
|
233
|
+
});
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
out.push({
|
|
237
|
+
type: "video",
|
|
238
|
+
source: {
|
|
239
|
+
kind: "base64",
|
|
240
|
+
data: part.source.data,
|
|
241
|
+
mimeType: part.source.mimeType ?? "video/mp4"
|
|
242
|
+
},
|
|
243
|
+
...providerMetadata !== void 0 ? { providerMetadata } : {}
|
|
244
|
+
});
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (part.type === "embedding") {
|
|
248
|
+
out.push({
|
|
249
|
+
type: "embedding",
|
|
250
|
+
embedding: part.embedding,
|
|
251
|
+
...providerMetadata !== void 0 ? { providerMetadata } : {}
|
|
252
|
+
});
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (part.type === "transcript") {
|
|
256
|
+
out.push({
|
|
257
|
+
type: "transcript",
|
|
258
|
+
text: part.text,
|
|
259
|
+
...part.segments ? { segments: part.segments } : {},
|
|
260
|
+
...providerMetadata !== void 0 ? { providerMetadata } : {}
|
|
261
|
+
});
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (part.type === "reasoning") out.push({
|
|
265
|
+
type: "reasoning",
|
|
266
|
+
text: part.text,
|
|
267
|
+
visibility: part.visibility,
|
|
268
|
+
...part.provider ? { provider: part.provider } : {},
|
|
269
|
+
...providerMetadata !== void 0 ? { providerMetadata } : {}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return out;
|
|
273
|
+
};
|
|
274
|
+
/** Converts UI messages into model input messages. */
|
|
275
|
+
const toModelMessages = (msgs) => msgs.flatMap((message) => {
|
|
276
|
+
const parts = toModelInputParts(message);
|
|
277
|
+
const items = [];
|
|
278
|
+
if (parts.length > 0) {
|
|
279
|
+
const hasNonText = parts.some((part) => part.type !== "text");
|
|
280
|
+
items.push({
|
|
281
|
+
type: "message",
|
|
282
|
+
role: message.role,
|
|
283
|
+
content: hasNonText || parts.length !== 1 || parts[0]?.type !== "text" ? parts : parts[0].text
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
const completedCalls = /* @__PURE__ */ new Map();
|
|
287
|
+
for (const part of message.parts) {
|
|
288
|
+
if (part.type !== "tool-call") continue;
|
|
289
|
+
const toolName = part.name;
|
|
290
|
+
if (toolName && (part.status === "success" || part.status === "error" || part.state === "completed")) completedCalls.set(part.callId, toolName);
|
|
291
|
+
}
|
|
292
|
+
for (const part of message.parts) {
|
|
293
|
+
if (part.type !== "tool-result") continue;
|
|
294
|
+
const toolName = completedCalls.get(part.callId);
|
|
295
|
+
if (!toolName || part.status !== "success" && part.status !== "error") continue;
|
|
296
|
+
items.push({
|
|
297
|
+
type: "tool-call",
|
|
298
|
+
name: toolName,
|
|
299
|
+
callId: part.callId,
|
|
300
|
+
result: part.result,
|
|
301
|
+
...part.status === "error" ? { isError: true } : {}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
return items;
|
|
305
|
+
});
|
|
306
|
+
const contentToUIParts = (content) => {
|
|
307
|
+
if (typeof content === "string") return [{
|
|
308
|
+
type: "text",
|
|
309
|
+
text: content,
|
|
310
|
+
state: "complete"
|
|
311
|
+
}];
|
|
312
|
+
return content.map((part) => contentPartToUIPart(part)).filter((part) => part !== null);
|
|
313
|
+
};
|
|
314
|
+
/** Converts model input messages back into UI messages. */
|
|
315
|
+
const fromModelMessages = (messages, options) => {
|
|
316
|
+
const generateId = options?.generateId ?? makeLocalMessageId;
|
|
317
|
+
const result = [];
|
|
318
|
+
let activeAssistantMessageIndex;
|
|
319
|
+
for (const item of messages) {
|
|
320
|
+
if (item.type === "message") {
|
|
321
|
+
const parts = contentToUIParts(item.content);
|
|
322
|
+
if (parts.length === 0) continue;
|
|
323
|
+
result.push({
|
|
324
|
+
localId: generateId(),
|
|
325
|
+
role: item.role ?? "user",
|
|
326
|
+
parts
|
|
327
|
+
});
|
|
328
|
+
activeAssistantMessageIndex = (item.role ?? "user") === "assistant" ? result.length - 1 : void 0;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const status = item.isError ? "error" : "success";
|
|
332
|
+
const toolParts = [{
|
|
333
|
+
type: "tool-call",
|
|
334
|
+
callId: item.callId,
|
|
335
|
+
name: item.name,
|
|
336
|
+
status,
|
|
337
|
+
state: "completed"
|
|
338
|
+
}, {
|
|
339
|
+
type: "tool-result",
|
|
340
|
+
callId: item.callId,
|
|
341
|
+
result: item.result,
|
|
342
|
+
status
|
|
343
|
+
}];
|
|
344
|
+
if (activeAssistantMessageIndex !== void 0) {
|
|
345
|
+
const message = result[activeAssistantMessageIndex];
|
|
346
|
+
if (message?.role === "assistant") {
|
|
347
|
+
result[activeAssistantMessageIndex] = {
|
|
348
|
+
...message,
|
|
349
|
+
parts: [...message.parts, ...toolParts]
|
|
350
|
+
};
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
result.push({
|
|
355
|
+
localId: generateId(),
|
|
356
|
+
role: "assistant",
|
|
357
|
+
parts: toolParts
|
|
358
|
+
});
|
|
359
|
+
activeAssistantMessageIndex = result.length - 1;
|
|
360
|
+
}
|
|
361
|
+
return result;
|
|
362
|
+
};
|
|
363
|
+
/** Converts durable conversation items back into UI messages. */
|
|
364
|
+
const fromConversationItems = (items, options) => {
|
|
365
|
+
const generateId = options?.generateId ?? makeLocalMessageId;
|
|
366
|
+
const result = [];
|
|
367
|
+
let activeAssistantMessageIndex;
|
|
368
|
+
for (const item of items) {
|
|
369
|
+
if (item.type === "message") {
|
|
370
|
+
const parts = contentToUIParts(item.content);
|
|
371
|
+
if (parts.length === 0) continue;
|
|
372
|
+
result.push({
|
|
373
|
+
localId: generateId(),
|
|
374
|
+
role: item.role ?? "user",
|
|
375
|
+
parts
|
|
376
|
+
});
|
|
377
|
+
activeAssistantMessageIndex = (item.role ?? "user") === "assistant" ? result.length - 1 : void 0;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if ("arguments" in item) {
|
|
381
|
+
const part = {
|
|
382
|
+
type: "tool-call",
|
|
383
|
+
callId: item.callId,
|
|
384
|
+
name: item.name,
|
|
385
|
+
args: item.arguments,
|
|
386
|
+
status: "pending",
|
|
387
|
+
state: "input-complete"
|
|
388
|
+
};
|
|
389
|
+
if (activeAssistantMessageIndex !== void 0) {
|
|
390
|
+
const message = result[activeAssistantMessageIndex];
|
|
391
|
+
if (message?.role === "assistant") {
|
|
392
|
+
result[activeAssistantMessageIndex] = {
|
|
393
|
+
...message,
|
|
394
|
+
parts: [...message.parts, part]
|
|
395
|
+
};
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
result.push({
|
|
400
|
+
localId: generateId(),
|
|
401
|
+
role: "assistant",
|
|
402
|
+
parts: [part]
|
|
403
|
+
});
|
|
404
|
+
activeAssistantMessageIndex = result.length - 1;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
let appendedToExistingMessage = false;
|
|
408
|
+
for (let index = result.length - 1; index >= 0; index -= 1) {
|
|
409
|
+
const message = result[index];
|
|
410
|
+
if (!message || message.role !== "assistant") continue;
|
|
411
|
+
const partIndex = message.parts.findIndex((part) => part.type === "tool-call" && part.callId === item.callId);
|
|
412
|
+
if (partIndex < 0) continue;
|
|
413
|
+
const existingPart = message.parts[partIndex];
|
|
414
|
+
if (!existingPart || existingPart.type !== "tool-call") break;
|
|
415
|
+
const status = item.isError ? "error" : "success";
|
|
416
|
+
const parts = message.parts.slice();
|
|
417
|
+
parts[partIndex] = {
|
|
418
|
+
...existingPart,
|
|
419
|
+
...existingPart.name === void 0 ? { name: item.name } : {},
|
|
420
|
+
status,
|
|
421
|
+
state: "completed"
|
|
422
|
+
};
|
|
423
|
+
parts.splice(partIndex + 1, 0, {
|
|
424
|
+
type: "tool-result",
|
|
425
|
+
callId: item.callId,
|
|
426
|
+
result: item.result,
|
|
427
|
+
status
|
|
428
|
+
});
|
|
429
|
+
result[index] = {
|
|
430
|
+
...message,
|
|
431
|
+
parts
|
|
432
|
+
};
|
|
433
|
+
appendedToExistingMessage = true;
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
if (appendedToExistingMessage) continue;
|
|
437
|
+
const status = item.isError ? "error" : "success";
|
|
438
|
+
const toolParts = [{
|
|
439
|
+
type: "tool-call",
|
|
440
|
+
callId: item.callId,
|
|
441
|
+
name: item.name,
|
|
442
|
+
status,
|
|
443
|
+
state: "completed"
|
|
444
|
+
}, {
|
|
445
|
+
type: "tool-result",
|
|
446
|
+
callId: item.callId,
|
|
447
|
+
result: item.result,
|
|
448
|
+
status
|
|
449
|
+
}];
|
|
450
|
+
if (activeAssistantMessageIndex !== void 0) {
|
|
451
|
+
const message = result[activeAssistantMessageIndex];
|
|
452
|
+
if (message?.role === "assistant") {
|
|
453
|
+
result[activeAssistantMessageIndex] = {
|
|
454
|
+
...message,
|
|
455
|
+
parts: [...message.parts, ...toolParts]
|
|
456
|
+
};
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
result.push({
|
|
461
|
+
localId: generateId(),
|
|
462
|
+
role: "assistant",
|
|
463
|
+
parts: toolParts
|
|
464
|
+
});
|
|
465
|
+
activeAssistantMessageIndex = result.length - 1;
|
|
466
|
+
}
|
|
467
|
+
return result;
|
|
468
|
+
};
|
|
469
|
+
/** Creates a local message id. */
|
|
470
|
+
const makeLocalMessageId = () => globalThis.crypto?.randomUUID?.() ?? `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
471
|
+
/** Normalizes optimistic message options. */
|
|
472
|
+
const normalizeOptimisticUserMessageConfig = (optimisticUserMessage) => {
|
|
473
|
+
if (optimisticUserMessage === true) return {
|
|
474
|
+
enabled: true,
|
|
475
|
+
onError: "fail"
|
|
476
|
+
};
|
|
477
|
+
if (optimisticUserMessage === false) return {
|
|
478
|
+
enabled: false,
|
|
479
|
+
onError: "fail"
|
|
480
|
+
};
|
|
481
|
+
if (!optimisticUserMessage) return {
|
|
482
|
+
enabled: true,
|
|
483
|
+
onError: "fail"
|
|
484
|
+
};
|
|
485
|
+
return {
|
|
486
|
+
enabled: optimisticUserMessage.enabled ?? true,
|
|
487
|
+
onError: optimisticUserMessage.onError ?? "fail"
|
|
488
|
+
};
|
|
489
|
+
};
|
|
490
|
+
/** Deep-merges model option objects. Arrays are replaced, not merged. */
|
|
491
|
+
const mergeModelOptions = (...parts) => {
|
|
492
|
+
const merged = {};
|
|
493
|
+
const mergeInto = (target, source) => {
|
|
494
|
+
for (const [key, value] of Object.entries(source)) {
|
|
495
|
+
const existing = target[key];
|
|
496
|
+
if (existing !== null && typeof existing === "object" && !Array.isArray(existing) && value !== null && typeof value === "object" && !Array.isArray(value)) target[key] = mergeInto({ ...existing }, value);
|
|
497
|
+
else target[key] = value;
|
|
498
|
+
}
|
|
499
|
+
return target;
|
|
500
|
+
};
|
|
501
|
+
for (const part of parts) if (part) mergeInto(merged, part);
|
|
502
|
+
return merged;
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
//#endregion
|
|
506
|
+
//#region src/core/reducer.ts
|
|
507
|
+
/** Applies one core event to the current message state. */
|
|
508
|
+
const applyEvent = (state, event, options) => {
|
|
509
|
+
switch (event.type) {
|
|
510
|
+
case "RUN_STARTED":
|
|
511
|
+
if (!options?.synthesizeReplayUserMessage) return state;
|
|
512
|
+
return maybeInsertReplayUserMessage(state, event.runInput, event.runId);
|
|
513
|
+
case "TEXT_MESSAGE_START": {
|
|
514
|
+
const role = event.role ?? "assistant";
|
|
515
|
+
return ensureMessage(state, event.messageId, role);
|
|
516
|
+
}
|
|
517
|
+
case "TEXT_MESSAGE_CONTENT": return updateAssistantMessage(state, event.messageId, (msg) => appendText(msg, event.delta));
|
|
518
|
+
case "TEXT_MESSAGE_END": return updateAssistantMessage(state, event.messageId, (msg) => markLatestPartComplete(msg, "text"));
|
|
519
|
+
case "AUDIO_MESSAGE_START": {
|
|
520
|
+
const role = event.role ?? "assistant";
|
|
521
|
+
return ensureMessage(state, event.messageId, role);
|
|
522
|
+
}
|
|
523
|
+
case "AUDIO_MESSAGE_CONTENT": return updateAssistantMessage(state, event.messageId, (msg) => appendAudio(msg, event.delta));
|
|
524
|
+
case "AUDIO_MESSAGE_END": return updateAssistantMessage(state, event.messageId, (msg) => markLatestPartComplete(msg, "audio"));
|
|
525
|
+
case "IMAGE_MESSAGE_START": {
|
|
526
|
+
const role = event.role ?? "assistant";
|
|
527
|
+
return ensureMessage(state, event.messageId, role);
|
|
528
|
+
}
|
|
529
|
+
case "IMAGE_MESSAGE_CONTENT": return updateAssistantMessage(state, event.messageId, (msg) => appendImage(msg, event.delta));
|
|
530
|
+
case "IMAGE_MESSAGE_END": return updateAssistantMessage(state, event.messageId, (msg) => markLatestPartComplete(msg, "image"));
|
|
531
|
+
case "VIDEO_MESSAGE_START": {
|
|
532
|
+
const role = event.role ?? "assistant";
|
|
533
|
+
return ensureMessage(state, event.messageId, role);
|
|
534
|
+
}
|
|
535
|
+
case "VIDEO_MESSAGE_CONTENT": return updateAssistantMessage(state, event.messageId, (msg) => appendVideo(msg, event.delta));
|
|
536
|
+
case "VIDEO_MESSAGE_END": return updateAssistantMessage(state, event.messageId, (msg) => markLatestPartComplete(msg, "video"));
|
|
537
|
+
case "EMBEDDING_MESSAGE_START": {
|
|
538
|
+
const role = event.role ?? "assistant";
|
|
539
|
+
return ensureMessage(state, event.messageId, role);
|
|
540
|
+
}
|
|
541
|
+
case "EMBEDDING_MESSAGE_CONTENT": return updateAssistantMessage(state, event.messageId, (msg) => appendEmbedding(msg, event.delta));
|
|
542
|
+
case "EMBEDDING_MESSAGE_END": return updateAssistantMessage(state, event.messageId, (msg) => markLatestPartComplete(msg, "embedding"));
|
|
543
|
+
case "TRANSCRIPT_MESSAGE_START": {
|
|
544
|
+
const role = event.role ?? "assistant";
|
|
545
|
+
return ensureMessage(state, event.messageId, role);
|
|
546
|
+
}
|
|
547
|
+
case "TRANSCRIPT_MESSAGE_CONTENT": return updateAssistantMessage(state, event.messageId, (msg) => appendTranscriptText(msg, event.delta));
|
|
548
|
+
case "TRANSCRIPT_MESSAGE_SEGMENT": return updateAssistantMessage(state, event.messageId, (msg) => upsertTranscriptSegment(msg, event.segment));
|
|
549
|
+
case "TRANSCRIPT_MESSAGE_END": return updateAssistantMessage(state, event.messageId, (msg) => markLatestPartComplete(msg, "transcript"));
|
|
550
|
+
case "REASONING_MESSAGE_START": {
|
|
551
|
+
const role = event.role ?? "assistant";
|
|
552
|
+
return ensureAndUpdateMessage(state, event.messageId, role, (msg) => appendReasoningText(msg, "", event.visibility));
|
|
553
|
+
}
|
|
554
|
+
case "REASONING_MESSAGE_CONTENT": return updateAssistantMessage(state, event.messageId, (msg) => appendReasoningText(msg, event.delta, event.visibility));
|
|
555
|
+
case "REASONING_MESSAGE_END": return updateAssistantMessage(state, event.messageId, (msg) => markLatestPartComplete(msg, "reasoning"));
|
|
556
|
+
case "TOOL_CALL_START": return updateResolvedToolMessage(state, event.parentMessageId, event.toolCallId, (msg) => isToolCallCompleted(msg, event.toolCallId) ? msg : upsertToolPart(msg, "tool-call", event.toolCallId, {
|
|
557
|
+
name: event.toolCallName,
|
|
558
|
+
...event.toolTarget !== void 0 ? { toolTarget: event.toolTarget } : {},
|
|
559
|
+
status: "pending",
|
|
560
|
+
state: "awaiting-input"
|
|
561
|
+
}));
|
|
562
|
+
case "TOOL_CALL_ARGS": return updateResolvedToolMessage(state, event.parentMessageId, event.toolCallId, (msg) => isToolCallCompleted(msg, event.toolCallId) ? msg : upsertToolPart(msg, "tool-call", event.toolCallId, {
|
|
563
|
+
name: event.toolCallName,
|
|
564
|
+
...event.toolTarget !== void 0 ? { toolTarget: event.toolTarget } : {},
|
|
565
|
+
status: "pending",
|
|
566
|
+
state: "input-streaming"
|
|
567
|
+
}, (part) => part.type === "tool-call" ? {
|
|
568
|
+
...part,
|
|
569
|
+
args: (part.args ?? "") + event.delta
|
|
570
|
+
} : part));
|
|
571
|
+
case "TOOL_CALL_END": return updateResolvedToolMessage(state, event.parentMessageId, event.toolCallId, (msg) => {
|
|
572
|
+
if (isToolCallCompleted(msg, event.toolCallId)) return msg;
|
|
573
|
+
if (isApprovalLifecycleState(findToolCallPart(msg, event.toolCallId)?.state)) return msg;
|
|
574
|
+
return upsertToolPart(msg, "tool-call", event.toolCallId, {
|
|
575
|
+
name: event.toolCallName,
|
|
576
|
+
...event.toolTarget !== void 0 ? { toolTarget: event.toolTarget } : {},
|
|
577
|
+
status: "pending",
|
|
578
|
+
state: "input-complete"
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
case "TOOL_CALL_RESULT": return updateResolvedToolMessage(state, event.parentMessageId, event.toolCallId, (msg) => {
|
|
582
|
+
const status = event.isError ? "error" : "success";
|
|
583
|
+
return upsertToolPart(upsertToolPart(msg, "tool-result", event.toolCallId, {
|
|
584
|
+
result: event.result,
|
|
585
|
+
status
|
|
586
|
+
}), "tool-call", event.toolCallId, {
|
|
587
|
+
name: event.toolCallName,
|
|
588
|
+
...event.toolTarget !== void 0 ? { toolTarget: event.toolTarget } : {},
|
|
589
|
+
status,
|
|
590
|
+
state: "completed"
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
case "TOOL_APPROVAL_REQUIRED": return updateResolvedToolMessage(state, event.parentMessageId, event.toolCallId, (msg) => {
|
|
594
|
+
return updateToolApprovalMeta(upsertToolPart(msg, "tool-call", event.toolCallId, {
|
|
595
|
+
name: event.toolCallName,
|
|
596
|
+
...event.toolTarget !== void 0 ? { toolTarget: event.toolTarget } : {},
|
|
597
|
+
status: "pending",
|
|
598
|
+
state: "approval-requested"
|
|
599
|
+
}), event.toolCallId, {
|
|
600
|
+
input: event.toolInput,
|
|
601
|
+
...event.meta !== void 0 ? { meta: event.meta } : {}
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
case "TOOL_APPROVAL_UPDATED": return updateResolvedToolMessage(state, event.parentMessageId, event.toolCallId, (msg) => {
|
|
605
|
+
return updateToolApprovalMeta(upsertToolPart(msg, "tool-call", event.toolCallId, {
|
|
606
|
+
name: event.toolCallName,
|
|
607
|
+
...event.toolTarget !== void 0 ? { toolTarget: event.toolTarget } : {},
|
|
608
|
+
status: getApprovalStatus(event.state),
|
|
609
|
+
state: getApprovalPartState(event.state)
|
|
610
|
+
}), event.toolCallId, {
|
|
611
|
+
...event.toolInput !== void 0 ? { input: event.toolInput } : {},
|
|
612
|
+
...event.meta !== void 0 ? { meta: event.meta } : {},
|
|
613
|
+
...event.note !== void 0 ? { note: event.note } : {},
|
|
614
|
+
...event.actorId !== void 0 ? { actorId: event.actorId } : {}
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
default: return state;
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
/** Creates message state from UI messages. */
|
|
621
|
+
const createMessageState = (initial = []) => {
|
|
622
|
+
const byLocalId = /* @__PURE__ */ new Map();
|
|
623
|
+
initial.forEach((m, i) => byLocalId.set(m.localId, i));
|
|
624
|
+
return {
|
|
625
|
+
messages: initial,
|
|
626
|
+
byLocalId
|
|
627
|
+
};
|
|
628
|
+
};
|
|
629
|
+
const findMessageLocalIdByToolCallId = (state, toolCallId) => {
|
|
630
|
+
for (let i = state.messages.length - 1; i >= 0; i -= 1) {
|
|
631
|
+
const message = state.messages[i];
|
|
632
|
+
if (!message) continue;
|
|
633
|
+
if (message.parts.some((part) => (part.type === "tool-call" || part.type === "tool-result") && part.callId === toolCallId)) return message.localId;
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
const findLatestAssistantMessageLocalId = (state) => {
|
|
637
|
+
for (let i = state.messages.length - 1; i >= 0; i -= 1) {
|
|
638
|
+
const message = state.messages[i];
|
|
639
|
+
if (message?.role === "assistant") return message.localId;
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
const resolveCurrentTurnAssistantLocalId = (state) => {
|
|
643
|
+
for (let i = state.messages.length - 1; i >= 0; i -= 1) {
|
|
644
|
+
const message = state.messages[i];
|
|
645
|
+
if (message?.role !== "user") continue;
|
|
646
|
+
const userLocalId = message.localId;
|
|
647
|
+
let latestAssistantAfterUser;
|
|
648
|
+
for (let j = i + 1; j < state.messages.length; j += 1) {
|
|
649
|
+
const candidate = state.messages[j];
|
|
650
|
+
if (candidate?.role === "assistant") latestAssistantAfterUser = candidate.localId;
|
|
651
|
+
}
|
|
652
|
+
return latestAssistantAfterUser ?? `assistant_turn:${userLocalId}`;
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
const resolveToolMessageLocalId = (state, parentMessageId, toolCallId) => {
|
|
656
|
+
if (parentMessageId) {
|
|
657
|
+
if (state.byLocalId.has(parentMessageId)) return parentMessageId;
|
|
658
|
+
}
|
|
659
|
+
return findMessageLocalIdByToolCallId(state, toolCallId) ?? resolveCurrentTurnAssistantLocalId(state) ?? findLatestAssistantMessageLocalId(state);
|
|
660
|
+
};
|
|
661
|
+
const maybeInsertReplayUserMessage = (state, runInput, runId) => {
|
|
662
|
+
const synthesized = toReplayUserMessage(runInput, runId);
|
|
663
|
+
if (!synthesized) return state;
|
|
664
|
+
if (state.byLocalId.has(synthesized.localId)) return state;
|
|
665
|
+
const latestUserMessage = findLatestUserMessage(state);
|
|
666
|
+
if (latestUserMessage && latestUserMessage.localId === synthesized.localId) return state;
|
|
667
|
+
return {
|
|
668
|
+
...state,
|
|
669
|
+
messages: [...state.messages, synthesized],
|
|
670
|
+
byLocalId: new Map(state.byLocalId).set(synthesized.localId, state.messages.length)
|
|
671
|
+
};
|
|
672
|
+
};
|
|
673
|
+
const toReplayUserMessage = (runInput, runId) => {
|
|
674
|
+
const input = runInput.input;
|
|
675
|
+
if (typeof input === "string") return {
|
|
676
|
+
localId: `user_run:${runId}`,
|
|
677
|
+
id: `user_run:${runId}`,
|
|
678
|
+
role: "user",
|
|
679
|
+
parts: [{
|
|
680
|
+
type: "text",
|
|
681
|
+
text: input,
|
|
682
|
+
state: "complete"
|
|
683
|
+
}],
|
|
684
|
+
status: "sent"
|
|
685
|
+
};
|
|
686
|
+
if (isReplayInputArray(input)) return toLatestReplayUserMessage(input, runId);
|
|
687
|
+
if (isSingleReplayMessageItem(input)) return toLatestReplayUserMessage([input], runId);
|
|
688
|
+
};
|
|
689
|
+
const toLatestReplayUserMessage = (input, runId) => {
|
|
690
|
+
const latestUserMessage = [...fromModelMessages(input, { generateId: (() => {
|
|
691
|
+
let index = 0;
|
|
692
|
+
return () => `user_run:${runId}:${(index++).toString(36)}`;
|
|
693
|
+
})() })].reverse().find((message) => message.role === "user");
|
|
694
|
+
if (!latestUserMessage) return;
|
|
695
|
+
return {
|
|
696
|
+
...latestUserMessage,
|
|
697
|
+
status: "sent"
|
|
698
|
+
};
|
|
699
|
+
};
|
|
700
|
+
const findLatestUserMessage = (state) => [...state.messages].reverse().find((message) => message.role === "user");
|
|
701
|
+
const isSingleReplayMessageItem = (input) => {
|
|
702
|
+
if (typeof input !== "object" || input === null) return false;
|
|
703
|
+
const record = input;
|
|
704
|
+
return record.type === "message" && (record.role === void 0 || typeof record.role === "string") && (typeof record.content === "string" || Array.isArray(record.content));
|
|
705
|
+
};
|
|
706
|
+
const isReplayInputArray = (input) => Array.isArray(input);
|
|
707
|
+
const ensureMessage = (state, localId, role) => {
|
|
708
|
+
if (state.byLocalId.has(localId)) return state;
|
|
709
|
+
const next = {
|
|
710
|
+
localId,
|
|
711
|
+
id: localId,
|
|
712
|
+
role,
|
|
713
|
+
parts: []
|
|
714
|
+
};
|
|
715
|
+
return {
|
|
716
|
+
...state,
|
|
717
|
+
messages: [...state.messages, next],
|
|
718
|
+
byLocalId: new Map(state.byLocalId).set(localId, state.messages.length)
|
|
719
|
+
};
|
|
720
|
+
};
|
|
721
|
+
const updateMessage = (state, localId, updater) => {
|
|
722
|
+
const idx = state.byLocalId.get(localId);
|
|
723
|
+
if (idx === void 0) return state;
|
|
724
|
+
const nextMessages = state.messages.slice();
|
|
725
|
+
const current = nextMessages[idx];
|
|
726
|
+
if (!current) return state;
|
|
727
|
+
nextMessages[idx] = updater(current);
|
|
728
|
+
return {
|
|
729
|
+
...state,
|
|
730
|
+
messages: nextMessages
|
|
731
|
+
};
|
|
732
|
+
};
|
|
733
|
+
const ensureAndUpdateMessage = (state, localId, role, updater) => updateMessage(ensureMessage(state, localId, role), localId, updater);
|
|
734
|
+
const appendText = (msg, delta) => {
|
|
735
|
+
const parts = msg.parts.slice();
|
|
736
|
+
const last = parts[parts.length - 1];
|
|
737
|
+
if (last && last.type === "text" && last.state !== "complete") parts[parts.length - 1] = {
|
|
738
|
+
...last,
|
|
739
|
+
text: last.text + delta
|
|
740
|
+
};
|
|
741
|
+
else parts.push({
|
|
742
|
+
type: "text",
|
|
743
|
+
text: delta
|
|
744
|
+
});
|
|
745
|
+
return {
|
|
746
|
+
...msg,
|
|
747
|
+
parts
|
|
748
|
+
};
|
|
749
|
+
};
|
|
750
|
+
const appendAudio = (msg, delta) => {
|
|
751
|
+
const parts = msg.parts.slice();
|
|
752
|
+
const last = parts[parts.length - 1];
|
|
753
|
+
if (last?.type === "audio" && last.source.kind === "base64" && last.state !== "complete") parts[parts.length - 1] = {
|
|
754
|
+
...last,
|
|
755
|
+
source: {
|
|
756
|
+
kind: "base64",
|
|
757
|
+
data: `${last.source.data}${delta.data}`,
|
|
758
|
+
mimeType: delta.mimeType
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
else parts.push({
|
|
762
|
+
type: "audio",
|
|
763
|
+
source: {
|
|
764
|
+
kind: "base64",
|
|
765
|
+
data: delta.data,
|
|
766
|
+
mimeType: delta.mimeType
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
return {
|
|
770
|
+
...msg,
|
|
771
|
+
parts
|
|
772
|
+
};
|
|
773
|
+
};
|
|
774
|
+
const appendImage = (msg, delta) => {
|
|
775
|
+
const parts = msg.parts.slice();
|
|
776
|
+
const last = parts[parts.length - 1];
|
|
777
|
+
if (delta.kind === "base64" && last?.type === "image" && last.source.kind === "base64" && last.state !== "complete") parts[parts.length - 1] = {
|
|
778
|
+
...last,
|
|
779
|
+
source: {
|
|
780
|
+
kind: "base64",
|
|
781
|
+
data: `${last.source.data}${delta.data}`,
|
|
782
|
+
mimeType: delta.mimeType
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
else if (delta.kind === "url") parts.push({
|
|
786
|
+
type: "image",
|
|
787
|
+
source: {
|
|
788
|
+
kind: "url",
|
|
789
|
+
url: delta.url
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
else parts.push({
|
|
793
|
+
type: "image",
|
|
794
|
+
source: {
|
|
795
|
+
kind: "base64",
|
|
796
|
+
data: delta.data,
|
|
797
|
+
mimeType: delta.mimeType
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
return {
|
|
801
|
+
...msg,
|
|
802
|
+
parts
|
|
803
|
+
};
|
|
804
|
+
};
|
|
805
|
+
const appendVideo = (msg, delta) => {
|
|
806
|
+
const parts = msg.parts.slice();
|
|
807
|
+
const last = parts[parts.length - 1];
|
|
808
|
+
if (delta.kind === "base64" && last?.type === "video" && last.source.kind === "base64" && last.state !== "complete") parts[parts.length - 1] = {
|
|
809
|
+
...last,
|
|
810
|
+
source: {
|
|
811
|
+
kind: "base64",
|
|
812
|
+
data: `${last.source.data}${delta.data}`,
|
|
813
|
+
mimeType: delta.mimeType
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
else if (delta.kind === "url") parts.push({
|
|
817
|
+
type: "video",
|
|
818
|
+
source: {
|
|
819
|
+
kind: "url",
|
|
820
|
+
url: delta.url
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
else parts.push({
|
|
824
|
+
type: "video",
|
|
825
|
+
source: {
|
|
826
|
+
kind: "base64",
|
|
827
|
+
data: delta.data,
|
|
828
|
+
mimeType: delta.mimeType
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
return {
|
|
832
|
+
...msg,
|
|
833
|
+
parts
|
|
834
|
+
};
|
|
835
|
+
};
|
|
836
|
+
const appendEmbedding = (msg, delta) => {
|
|
837
|
+
const parts = msg.parts.slice();
|
|
838
|
+
const last = parts[parts.length - 1];
|
|
839
|
+
if (last?.type === "embedding" && last.state !== "complete") parts[parts.length - 1] = {
|
|
840
|
+
...last,
|
|
841
|
+
embedding: [...last.embedding, ...delta]
|
|
842
|
+
};
|
|
843
|
+
else parts.push({
|
|
844
|
+
type: "embedding",
|
|
845
|
+
embedding: delta
|
|
846
|
+
});
|
|
847
|
+
return {
|
|
848
|
+
...msg,
|
|
849
|
+
parts
|
|
850
|
+
};
|
|
851
|
+
};
|
|
852
|
+
const appendTranscriptText = (msg, delta) => {
|
|
853
|
+
const parts = msg.parts.slice();
|
|
854
|
+
const last = parts[parts.length - 1];
|
|
855
|
+
if (last?.type === "transcript" && last.state !== "complete") parts[parts.length - 1] = {
|
|
856
|
+
...last,
|
|
857
|
+
text: last.text + delta
|
|
858
|
+
};
|
|
859
|
+
else parts.push({
|
|
860
|
+
type: "transcript",
|
|
861
|
+
text: delta
|
|
862
|
+
});
|
|
863
|
+
return {
|
|
864
|
+
...msg,
|
|
865
|
+
parts
|
|
866
|
+
};
|
|
867
|
+
};
|
|
868
|
+
const appendReasoningText = (msg, delta, visibility, provider) => {
|
|
869
|
+
const parts = msg.parts.slice();
|
|
870
|
+
const last = parts[parts.length - 1];
|
|
871
|
+
if (last?.type === "reasoning" && last.state !== "complete" && last.visibility === visibility && (last.provider ?? void 0) === (provider ?? void 0)) parts[parts.length - 1] = {
|
|
872
|
+
...last,
|
|
873
|
+
text: last.text + delta
|
|
874
|
+
};
|
|
875
|
+
else parts.push({
|
|
876
|
+
type: "reasoning",
|
|
877
|
+
text: delta,
|
|
878
|
+
visibility,
|
|
879
|
+
...provider !== void 0 ? { provider } : {}
|
|
880
|
+
});
|
|
881
|
+
return {
|
|
882
|
+
...msg,
|
|
883
|
+
parts
|
|
884
|
+
};
|
|
885
|
+
};
|
|
886
|
+
const upsertTranscriptSegment = (msg, segment) => {
|
|
887
|
+
const parts = msg.parts.slice();
|
|
888
|
+
const last = parts[parts.length - 1];
|
|
889
|
+
if (last?.type === "transcript" && last.state !== "complete") {
|
|
890
|
+
const segments = last.segments ? [...last.segments] : [];
|
|
891
|
+
const idx = segments.findIndex((item) => item.id === segment.id);
|
|
892
|
+
if (idx === -1) segments.push(segment);
|
|
893
|
+
else segments[idx] = segment;
|
|
894
|
+
parts[parts.length - 1] = {
|
|
895
|
+
...last,
|
|
896
|
+
segments
|
|
897
|
+
};
|
|
898
|
+
} else parts.push({
|
|
899
|
+
type: "transcript",
|
|
900
|
+
text: "",
|
|
901
|
+
segments: [segment]
|
|
902
|
+
});
|
|
903
|
+
return {
|
|
904
|
+
...msg,
|
|
905
|
+
parts
|
|
906
|
+
};
|
|
907
|
+
};
|
|
908
|
+
const markLatestPartComplete = (msg, partType) => {
|
|
909
|
+
const parts = msg.parts.slice();
|
|
910
|
+
for (let i = parts.length - 1; i >= 0; i -= 1) {
|
|
911
|
+
const part = parts[i];
|
|
912
|
+
if (!part || part.type !== partType) continue;
|
|
913
|
+
parts[i] = {
|
|
914
|
+
...part,
|
|
915
|
+
state: "complete"
|
|
916
|
+
};
|
|
917
|
+
return {
|
|
918
|
+
...msg,
|
|
919
|
+
parts
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
return msg;
|
|
923
|
+
};
|
|
924
|
+
const isApprovalLifecycleState = (state) => state === "approval-requested" || state === "approval-approved" || state === "approval-denied" || state === "approval-expired";
|
|
925
|
+
const getApprovalPartState = (state) => state === "requested" ? "approval-requested" : state === "approved" ? "approval-approved" : state === "denied" ? "approval-denied" : "approval-expired";
|
|
926
|
+
const getApprovalStatus = (state) => state === "denied" || state === "expired" ? "error" : "pending";
|
|
927
|
+
const updateToolApprovalMeta = (msg, toolCallId, patch) => upsertToolPart(msg, "tool-call", toolCallId, {}, (part) => part.type === "tool-call" ? {
|
|
928
|
+
...part,
|
|
929
|
+
approval: {
|
|
930
|
+
...part.approval ?? {},
|
|
931
|
+
...patch.input !== void 0 ? { input: patch.input } : {},
|
|
932
|
+
...patch.meta !== void 0 ? { meta: patch.meta } : {},
|
|
933
|
+
...patch.note !== void 0 ? { note: patch.note } : {},
|
|
934
|
+
...patch.actorId !== void 0 ? { actorId: patch.actorId } : {}
|
|
935
|
+
}
|
|
936
|
+
} : part);
|
|
937
|
+
const isToolCallCompleted = (msg, toolCallId) => msg.parts.some((part) => part.type === "tool-call" && part.callId === toolCallId && (part.status === "success" || part.state === "completed" || part.state === "approval-denied" || part.state === "approval-expired"));
|
|
938
|
+
const findToolCallPart = (msg, toolCallId) => msg.parts.find((part) => part.type === "tool-call" && part.callId === toolCallId);
|
|
939
|
+
const upsertToolPart = (msg, kind, toolCallId, patch, updater) => {
|
|
940
|
+
const parts = msg.parts.slice();
|
|
941
|
+
const idx = parts.findIndex((p) => p.type === kind && "callId" in p && p.callId === toolCallId);
|
|
942
|
+
if (idx === -1) {
|
|
943
|
+
if (kind === "tool-call") parts.push({
|
|
944
|
+
type: "tool-call",
|
|
945
|
+
callId: toolCallId,
|
|
946
|
+
status: "pending",
|
|
947
|
+
...patch
|
|
948
|
+
});
|
|
949
|
+
else parts.push({
|
|
950
|
+
type: "tool-result",
|
|
951
|
+
callId: toolCallId,
|
|
952
|
+
status: "pending",
|
|
953
|
+
...patch
|
|
954
|
+
});
|
|
955
|
+
return {
|
|
956
|
+
...msg,
|
|
957
|
+
parts
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
const existing = parts[idx];
|
|
961
|
+
if (!existing) return {
|
|
962
|
+
...msg,
|
|
963
|
+
parts
|
|
964
|
+
};
|
|
965
|
+
if (kind === "tool-call" && existing.type === "tool-call") {
|
|
966
|
+
const merged = {
|
|
967
|
+
...existing,
|
|
968
|
+
...patch,
|
|
969
|
+
type: "tool-call"
|
|
970
|
+
};
|
|
971
|
+
parts[idx] = updater ? updater(merged) : merged;
|
|
972
|
+
} else if (kind === "tool-result" && existing.type === "tool-result") {
|
|
973
|
+
const merged = {
|
|
974
|
+
...existing,
|
|
975
|
+
...patch,
|
|
976
|
+
type: "tool-result"
|
|
977
|
+
};
|
|
978
|
+
parts[idx] = updater ? updater(merged) : merged;
|
|
979
|
+
}
|
|
980
|
+
return {
|
|
981
|
+
...msg,
|
|
982
|
+
parts
|
|
983
|
+
};
|
|
984
|
+
};
|
|
985
|
+
const updateAssistantMessage = (state, messageId, updater) => ensureAndUpdateMessage(state, messageId, "assistant", updater);
|
|
986
|
+
const updateResolvedToolMessage = (state, parentMessageId, toolCallId, updater) => {
|
|
987
|
+
const targetMessageLocalId = resolveToolMessageLocalId(state, parentMessageId, toolCallId);
|
|
988
|
+
if (!targetMessageLocalId) return state;
|
|
989
|
+
return ensureAndUpdateMessage(state, targetMessageLocalId, "assistant", updater);
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
//#endregion
|
|
993
|
+
//#region src/core/response.ts
|
|
994
|
+
/** Builds a deterministic local id for messages synthesized from model output. */
|
|
995
|
+
const makeResponseLocalId = (prefix, index) => `${prefix}_${index.toString(36)}`;
|
|
996
|
+
/** Converts one model output item into a UI message. */
|
|
997
|
+
const createMessageFromOutputItem = (item, index) => {
|
|
998
|
+
if (item.type === "message") {
|
|
999
|
+
const parts = typeof item.content === "string" ? [{
|
|
1000
|
+
type: "text",
|
|
1001
|
+
text: item.content,
|
|
1002
|
+
state: "complete"
|
|
1003
|
+
}] : item.content.map((part) => contentPartToUIPart(part)).filter((part) => part !== null);
|
|
1004
|
+
if (parts.length === 0) return null;
|
|
1005
|
+
return {
|
|
1006
|
+
localId: makeResponseLocalId("response_message", index),
|
|
1007
|
+
role: item.role,
|
|
1008
|
+
parts
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
if (item.type === "tool-call") return {
|
|
1012
|
+
localId: makeResponseLocalId("response_tool_call", index),
|
|
1013
|
+
role: "assistant",
|
|
1014
|
+
parts: [{
|
|
1015
|
+
type: "tool-call",
|
|
1016
|
+
callId: item.callId,
|
|
1017
|
+
name: item.name,
|
|
1018
|
+
args: item.arguments,
|
|
1019
|
+
status: "pending",
|
|
1020
|
+
state: "input-complete"
|
|
1021
|
+
}]
|
|
1022
|
+
};
|
|
1023
|
+
if (item.type !== "provider-tool-result") return null;
|
|
1024
|
+
const status = item.isError ? "error" : "success";
|
|
1025
|
+
return {
|
|
1026
|
+
localId: makeResponseLocalId("response_tool_result", index),
|
|
1027
|
+
role: "assistant",
|
|
1028
|
+
parts: [{
|
|
1029
|
+
type: "tool-call",
|
|
1030
|
+
callId: item.callId,
|
|
1031
|
+
name: item.name,
|
|
1032
|
+
status,
|
|
1033
|
+
state: "completed"
|
|
1034
|
+
}, {
|
|
1035
|
+
type: "tool-result",
|
|
1036
|
+
callId: item.callId,
|
|
1037
|
+
result: item.result,
|
|
1038
|
+
status
|
|
1039
|
+
}]
|
|
1040
|
+
};
|
|
1041
|
+
};
|
|
1042
|
+
/** Attaches a provider tool result to the latest matching assistant message. */
|
|
1043
|
+
const attachToolResultToMatchingMessage = (messages, item) => {
|
|
1044
|
+
let messageIndex = -1;
|
|
1045
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
1046
|
+
const message = messages[index];
|
|
1047
|
+
if (!message || message.role !== "assistant") continue;
|
|
1048
|
+
if (message.parts.some((part) => part.type === "tool-call" && part.callId === item.callId)) {
|
|
1049
|
+
messageIndex = index;
|
|
1050
|
+
break;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
if (messageIndex < 0) return false;
|
|
1054
|
+
const message = messages[messageIndex];
|
|
1055
|
+
if (!message || message.role !== "assistant") return false;
|
|
1056
|
+
const nextMessages = messages.slice();
|
|
1057
|
+
const status = item.isError ? "error" : "success";
|
|
1058
|
+
nextMessages[messageIndex] = {
|
|
1059
|
+
...message,
|
|
1060
|
+
parts: message.parts.flatMap((part) => part.type === "tool-call" && part.callId === item.callId ? [{
|
|
1061
|
+
...part,
|
|
1062
|
+
status,
|
|
1063
|
+
state: "completed"
|
|
1064
|
+
}, {
|
|
1065
|
+
type: "tool-result",
|
|
1066
|
+
callId: item.callId,
|
|
1067
|
+
result: item.result,
|
|
1068
|
+
status
|
|
1069
|
+
}] : [part])
|
|
1070
|
+
};
|
|
1071
|
+
messages.splice(0, messages.length, ...nextMessages);
|
|
1072
|
+
return true;
|
|
1073
|
+
};
|
|
1074
|
+
/**
|
|
1075
|
+
* Converts model response output into UI messages.
|
|
1076
|
+
*/
|
|
1077
|
+
const getMessagesFromResponse = (response) => {
|
|
1078
|
+
const messages = [];
|
|
1079
|
+
for (const [index, item] of (response?.output ?? []).entries()) {
|
|
1080
|
+
if (item.type === "provider-tool-result" && attachToolResultToMatchingMessage(messages, item)) continue;
|
|
1081
|
+
const message = createMessageFromOutputItem(item, index);
|
|
1082
|
+
if (message) messages.push(message);
|
|
1083
|
+
}
|
|
1084
|
+
return messages;
|
|
1085
|
+
};
|
|
1086
|
+
|
|
1087
|
+
//#endregion
|
|
1088
|
+
//#region src/core/controller.ts
|
|
1089
|
+
/**
|
|
1090
|
+
* Framework-agnostic controller for one agent conversation.
|
|
1091
|
+
*/
|
|
1092
|
+
var AgentChatController = class {
|
|
1093
|
+
id;
|
|
1094
|
+
state;
|
|
1095
|
+
status = "ready";
|
|
1096
|
+
error = void 0;
|
|
1097
|
+
lastStreamId;
|
|
1098
|
+
lastRunId;
|
|
1099
|
+
lastResponse;
|
|
1100
|
+
lastStructured;
|
|
1101
|
+
lastAppliedSeq = -1;
|
|
1102
|
+
lastAppliedSeqByStream = /* @__PURE__ */ new Map();
|
|
1103
|
+
initialMessages;
|
|
1104
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1105
|
+
destroyed = false;
|
|
1106
|
+
initialized = false;
|
|
1107
|
+
warnedHistoryCombo = false;
|
|
1108
|
+
options;
|
|
1109
|
+
activeAbortController = null;
|
|
1110
|
+
constructor(client, options) {
|
|
1111
|
+
this.client = client;
|
|
1112
|
+
this.options = options;
|
|
1113
|
+
this.id = options.id ?? makeLocalMessageId();
|
|
1114
|
+
const normalized = this.normalizeMessages(options.initialMessages ?? []);
|
|
1115
|
+
this.state = createMessageState(normalized);
|
|
1116
|
+
this.initialMessages = normalized;
|
|
1117
|
+
this.lastStreamId = this.getConfiguredInitialStreamId(options);
|
|
1118
|
+
}
|
|
1119
|
+
/** Returns the current messages. */
|
|
1120
|
+
getMessages() {
|
|
1121
|
+
return this.state.messages;
|
|
1122
|
+
}
|
|
1123
|
+
/** Returns the current status. */
|
|
1124
|
+
getStatus() {
|
|
1125
|
+
return this.status;
|
|
1126
|
+
}
|
|
1127
|
+
/** Returns the latest client error. */
|
|
1128
|
+
getError() {
|
|
1129
|
+
return this.error;
|
|
1130
|
+
}
|
|
1131
|
+
/** Returns the latest stream id. */
|
|
1132
|
+
getStreamId() {
|
|
1133
|
+
return this.lastStreamId;
|
|
1134
|
+
}
|
|
1135
|
+
/** Returns the latest run id. */
|
|
1136
|
+
getRunId() {
|
|
1137
|
+
return this.lastRunId;
|
|
1138
|
+
}
|
|
1139
|
+
/** True while a request is active. */
|
|
1140
|
+
get isLoading() {
|
|
1141
|
+
return this.status === "hydrating" || this.status === "submitted" || this.status === "streaming";
|
|
1142
|
+
}
|
|
1143
|
+
/** True while stream events are being consumed. */
|
|
1144
|
+
get isStreaming() {
|
|
1145
|
+
return this.status === "streaming";
|
|
1146
|
+
}
|
|
1147
|
+
/** Returns pending tool approvals. */
|
|
1148
|
+
getPendingToolApprovals() {
|
|
1149
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1150
|
+
const pending = [];
|
|
1151
|
+
for (const message of this.state.messages) for (const part of message.parts) if (part.type === "tool-call" && part.state === "approval-requested" && !seen.has(part.callId)) {
|
|
1152
|
+
pending.push({
|
|
1153
|
+
toolCallId: part.callId,
|
|
1154
|
+
toolName: part.name,
|
|
1155
|
+
args: part.args,
|
|
1156
|
+
toolTarget: part.toolTarget,
|
|
1157
|
+
input: part.approval?.input,
|
|
1158
|
+
meta: part.approval?.meta,
|
|
1159
|
+
note: part.approval?.note,
|
|
1160
|
+
actorId: part.approval?.actorId
|
|
1161
|
+
});
|
|
1162
|
+
seen.add(part.callId);
|
|
1163
|
+
}
|
|
1164
|
+
return pending;
|
|
1165
|
+
}
|
|
1166
|
+
/** Returns an immutable snapshot. */
|
|
1167
|
+
getSnapshot() {
|
|
1168
|
+
return {
|
|
1169
|
+
id: this.id,
|
|
1170
|
+
conversationId: this.options.conversationId,
|
|
1171
|
+
messages: this.state.messages,
|
|
1172
|
+
status: this.status,
|
|
1173
|
+
error: this.error,
|
|
1174
|
+
streamId: this.lastStreamId,
|
|
1175
|
+
runId: this.lastRunId,
|
|
1176
|
+
isLoading: this.isLoading,
|
|
1177
|
+
isStreaming: this.isStreaming,
|
|
1178
|
+
pendingToolApprovals: this.getPendingToolApprovals()
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
/** Stops the active run or stream. */
|
|
1182
|
+
stop() {
|
|
1183
|
+
if (this.destroyed) return;
|
|
1184
|
+
this.cancelActiveWork();
|
|
1185
|
+
this.setStatus("ready");
|
|
1186
|
+
}
|
|
1187
|
+
/** Replaces the transport client. */
|
|
1188
|
+
updateClient(client) {
|
|
1189
|
+
this.client = client;
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Subscribes to state updates.
|
|
1193
|
+
*/
|
|
1194
|
+
subscribe(listener) {
|
|
1195
|
+
this.listeners.add(listener);
|
|
1196
|
+
return () => {
|
|
1197
|
+
this.listeners.delete(listener);
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
/** Calls each subscribed listener after controller state changes. */
|
|
1201
|
+
notify() {
|
|
1202
|
+
if (this.destroyed) return;
|
|
1203
|
+
for (const listener of this.listeners) listener();
|
|
1204
|
+
}
|
|
1205
|
+
/** Stores run and stream ids from response headers. */
|
|
1206
|
+
updateResponseIds(response) {
|
|
1207
|
+
const runId = response.headers.get("x-run-id");
|
|
1208
|
+
if (runId && runId !== this.lastRunId) this.lastRunId = runId;
|
|
1209
|
+
const streamId = response.headers.get("x-stream-id");
|
|
1210
|
+
if (streamId && streamId !== this.lastStreamId) this.lastStreamId = streamId;
|
|
1211
|
+
}
|
|
1212
|
+
/** Starts hydration or resume behavior. */
|
|
1213
|
+
init() {
|
|
1214
|
+
if (this.initialized) return;
|
|
1215
|
+
this.initialized = true;
|
|
1216
|
+
ensureBrowserTeardownTracking();
|
|
1217
|
+
if (this.options.hydrateFromServer && this.options.conversationId) {
|
|
1218
|
+
this.hydrateFromServer();
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
this.initResume();
|
|
1222
|
+
}
|
|
1223
|
+
/** Starts resume behavior, even with messages when allowed. */
|
|
1224
|
+
initResume(options) {
|
|
1225
|
+
const resume = this.options.resume;
|
|
1226
|
+
if (!resume) return;
|
|
1227
|
+
const hasMessages = this.state.messages.length > 0;
|
|
1228
|
+
const explicitStreamId = typeof resume === "object" && resume !== null && typeof resume.streamId === "string" ? resume.streamId : void 0;
|
|
1229
|
+
const explicitAfterSeq = typeof resume === "object" && resume !== null ? resume.afterSeq : void 0;
|
|
1230
|
+
const streamAfterSeq = explicitAfterSeq ?? (explicitStreamId ? this.lastAppliedSeqByStream.get(explicitStreamId) : void 0);
|
|
1231
|
+
if (hasMessages && explicitStreamId === void 0 && explicitAfterSeq === void 0 && !options?.allowWithMessages) return;
|
|
1232
|
+
if (explicitStreamId) {
|
|
1233
|
+
this.resumeStream({
|
|
1234
|
+
streamId: explicitStreamId,
|
|
1235
|
+
...streamAfterSeq !== void 0 ? { afterSeq: streamAfterSeq } : {}
|
|
1236
|
+
});
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
if (!this.options.conversationId) {
|
|
1240
|
+
console.warn("[better-agent] `resume` requires `conversationId` unless an explicit `streamId` is provided.");
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
if (explicitAfterSeq !== void 0) {
|
|
1244
|
+
this.resumeConversation({ afterSeq: explicitAfterSeq });
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
this.resumeConversation();
|
|
1248
|
+
}
|
|
1249
|
+
/** Hydrates from server history before resuming. */
|
|
1250
|
+
async hydrateFromServer() {
|
|
1251
|
+
const conversationId = this.options.conversationId;
|
|
1252
|
+
if (!conversationId) {
|
|
1253
|
+
this.initResume();
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
const loadConversation = this.client.loadConversation;
|
|
1257
|
+
if (typeof loadConversation !== "function") {
|
|
1258
|
+
this.initResume();
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
const { signal, controller } = this.startOperation();
|
|
1262
|
+
this.setStatus("hydrating");
|
|
1263
|
+
let hydrationCompleted = false;
|
|
1264
|
+
this.setError(void 0);
|
|
1265
|
+
try {
|
|
1266
|
+
const result = await loadConversation.call(this.client, this.options.agent, conversationId, { signal });
|
|
1267
|
+
this.throwIfAborted(signal);
|
|
1268
|
+
if (result) {
|
|
1269
|
+
const uiMessages = fromConversationItems(result.items);
|
|
1270
|
+
this.initialMessages = uiMessages;
|
|
1271
|
+
this.state = createMessageState(uiMessages);
|
|
1272
|
+
this.notify();
|
|
1273
|
+
}
|
|
1274
|
+
this.setStatus("ready");
|
|
1275
|
+
hydrationCompleted = true;
|
|
1276
|
+
} catch (e) {
|
|
1277
|
+
if (this.isAbortError(e, signal)) return;
|
|
1278
|
+
const err = this.toError(e, "Hydration failed");
|
|
1279
|
+
this.options.onError?.(err);
|
|
1280
|
+
this.setStatus("ready");
|
|
1281
|
+
hydrationCompleted = true;
|
|
1282
|
+
} finally {
|
|
1283
|
+
this.finishOperation(controller);
|
|
1284
|
+
if (hydrationCompleted) this.initResume({ allowWithMessages: true });
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
/** Sends one message. */
|
|
1288
|
+
async sendMessage(input, options) {
|
|
1289
|
+
return this.submitWithInternalOptions(input, options?.signal ? { signal: options.signal } : void 0);
|
|
1290
|
+
}
|
|
1291
|
+
/** Retries a user message by local id. */
|
|
1292
|
+
async retryMessage(localId) {
|
|
1293
|
+
const message = this.getMessageByLocalId(localId);
|
|
1294
|
+
if (!message) throw new Error(`Message '${localId}' was not found.`);
|
|
1295
|
+
if (message.role !== "user") throw new Error("Only user messages can be retried.");
|
|
1296
|
+
const idx = this.state.byLocalId.get(localId);
|
|
1297
|
+
if (idx === void 0) throw new Error(`Message '${localId}' was not found.`);
|
|
1298
|
+
const retryMessages = this.state.messages.slice(0, idx + 1).map((candidate) => candidate.localId === localId ? this.createPendingUserMessage(candidate) ?? candidate : candidate);
|
|
1299
|
+
return this.submitWithInternalOptions({
|
|
1300
|
+
input: toModelMessages(retryMessages),
|
|
1301
|
+
sendClientHistory: true
|
|
1302
|
+
}, {
|
|
1303
|
+
replaceLocalId: localId,
|
|
1304
|
+
replaceMessage: this.createPendingUserMessage(message),
|
|
1305
|
+
serializedHistoryInput: true
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
/** Resumes an existing stream. */
|
|
1309
|
+
async resumeStream(options) {
|
|
1310
|
+
const { streamId } = options;
|
|
1311
|
+
const { signal, controller } = this.startOperation();
|
|
1312
|
+
this.setStatus("submitted");
|
|
1313
|
+
this.setError(void 0);
|
|
1314
|
+
try {
|
|
1315
|
+
const events = this.client.resumeStream(this.options.agent, {
|
|
1316
|
+
streamId,
|
|
1317
|
+
afterSeq: options.afterSeq ?? this.lastAppliedSeqByStream.get(streamId) ?? -1
|
|
1318
|
+
}, {
|
|
1319
|
+
signal,
|
|
1320
|
+
onResponse: (response) => {
|
|
1321
|
+
this.updateResponseIds(response);
|
|
1322
|
+
}
|
|
1323
|
+
});
|
|
1324
|
+
const result = await this.consumeStreamUntilTerminal(events, {
|
|
1325
|
+
signal,
|
|
1326
|
+
replay: true,
|
|
1327
|
+
disconnectMessage: "Resume stream disconnected."
|
|
1328
|
+
});
|
|
1329
|
+
if (!result.receivedEvent) {
|
|
1330
|
+
this.setStatus("ready");
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
if (!result.terminalState) throw this.toStreamDisconnectError(void 0, "Stream ended before terminal run event.");
|
|
1334
|
+
if (result.terminalState === "error") throw result.terminalError ?? /* @__PURE__ */ new Error("Resume failed.");
|
|
1335
|
+
this.setStatus("ready");
|
|
1336
|
+
if (result.receivedEvent && result.terminalState) this.emitFinish({
|
|
1337
|
+
streamId,
|
|
1338
|
+
isAbort: result.terminalState === "aborted"
|
|
1339
|
+
});
|
|
1340
|
+
} catch (e) {
|
|
1341
|
+
if (this.destroyed || this.isAbortError(e, signal) || this.isStreamDisconnectError(e) && this.isPageTeardownLike()) {
|
|
1342
|
+
this.setStatus("ready");
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
const err = toAgentClientError(e, "Resume failed.");
|
|
1346
|
+
this.setError(err);
|
|
1347
|
+
this.setStatus("error");
|
|
1348
|
+
if (this.isStreamDisconnectError(e)) this.options.onDisconnect?.({
|
|
1349
|
+
error: err,
|
|
1350
|
+
runId: this.lastRunId,
|
|
1351
|
+
streamId
|
|
1352
|
+
});
|
|
1353
|
+
this.options.onError?.(err);
|
|
1354
|
+
} finally {
|
|
1355
|
+
this.finishOperation(controller);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
/** Resumes the active stream for the current conversation. */
|
|
1359
|
+
async resumeConversation(options) {
|
|
1360
|
+
const conversationId = this.options.conversationId;
|
|
1361
|
+
if (!conversationId) {
|
|
1362
|
+
console.warn("[better-agent] `resumeConversation()` requires `conversationId`.");
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
const { signal, controller } = this.startOperation();
|
|
1366
|
+
this.setStatus("submitted");
|
|
1367
|
+
this.setError(void 0);
|
|
1368
|
+
try {
|
|
1369
|
+
const events = this.client.resumeConversation(this.options.agent, {
|
|
1370
|
+
conversationId,
|
|
1371
|
+
...options?.afterSeq !== void 0 ? { afterSeq: options.afterSeq } : {}
|
|
1372
|
+
}, {
|
|
1373
|
+
signal,
|
|
1374
|
+
onResponse: (response) => {
|
|
1375
|
+
this.updateResponseIds(response);
|
|
1376
|
+
}
|
|
1377
|
+
});
|
|
1378
|
+
const result = await this.consumeStreamUntilTerminal(events, {
|
|
1379
|
+
signal,
|
|
1380
|
+
replay: true,
|
|
1381
|
+
disconnectMessage: "Resume stream disconnected."
|
|
1382
|
+
});
|
|
1383
|
+
if (!result.receivedEvent) {
|
|
1384
|
+
this.setStatus("ready");
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
if (!result.terminalState) throw this.toStreamDisconnectError(void 0, "Stream ended before terminal run event.");
|
|
1388
|
+
if (result.terminalState === "error") throw result.terminalError ?? /* @__PURE__ */ new Error("Resume failed.");
|
|
1389
|
+
this.setStatus("ready");
|
|
1390
|
+
if (result.receivedEvent && result.terminalState) this.emitFinish({
|
|
1391
|
+
conversationId,
|
|
1392
|
+
isAbort: result.terminalState === "aborted"
|
|
1393
|
+
});
|
|
1394
|
+
} catch (e) {
|
|
1395
|
+
if (this.destroyed || this.isAbortError(e, signal) || this.isStreamDisconnectError(e) && this.isPageTeardownLike()) {
|
|
1396
|
+
this.setStatus("ready");
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
const err = toAgentClientError(e, "Resume failed.");
|
|
1400
|
+
this.setError(err);
|
|
1401
|
+
this.setStatus("error");
|
|
1402
|
+
if (this.isStreamDisconnectError(e)) this.options.onDisconnect?.({
|
|
1403
|
+
error: err,
|
|
1404
|
+
runId: this.lastRunId,
|
|
1405
|
+
streamId: this.lastStreamId
|
|
1406
|
+
});
|
|
1407
|
+
this.options.onError?.(err);
|
|
1408
|
+
} finally {
|
|
1409
|
+
this.finishOperation(controller);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
/** Submits a tool approval decision. */
|
|
1413
|
+
async approveToolCall(params) {
|
|
1414
|
+
const runId = params.runId ?? this.lastRunId;
|
|
1415
|
+
if (!runId) throw new Error("Cannot submit tool approval response without a runId.");
|
|
1416
|
+
await this.client.submitToolApproval({
|
|
1417
|
+
agent: this.options.agent,
|
|
1418
|
+
runId,
|
|
1419
|
+
toolCallId: params.toolCallId,
|
|
1420
|
+
decision: params.decision,
|
|
1421
|
+
...params.note !== void 0 ? { note: params.note } : {},
|
|
1422
|
+
...params.actorId !== void 0 ? { actorId: params.actorId } : {}
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
/** Clears the current error. */
|
|
1426
|
+
clearError() {
|
|
1427
|
+
this.setError(void 0);
|
|
1428
|
+
this.setStatus("ready");
|
|
1429
|
+
}
|
|
1430
|
+
/** Resets local state to the initial snapshot. */
|
|
1431
|
+
reset() {
|
|
1432
|
+
this.cancelActiveWork();
|
|
1433
|
+
this.state = createMessageState(this.initialMessages);
|
|
1434
|
+
this.lastResponse = void 0;
|
|
1435
|
+
this.lastStructured = void 0;
|
|
1436
|
+
this.lastRunId = void 0;
|
|
1437
|
+
this.lastStreamId = this.getConfiguredInitialStreamId(this.options);
|
|
1438
|
+
this.error = void 0;
|
|
1439
|
+
this.status = "ready";
|
|
1440
|
+
this.lastAppliedSeq = -1;
|
|
1441
|
+
this.lastAppliedSeqByStream.clear();
|
|
1442
|
+
this.notify();
|
|
1443
|
+
}
|
|
1444
|
+
/** Replaces local messages. */
|
|
1445
|
+
setMessages(value) {
|
|
1446
|
+
const current = this.state.messages;
|
|
1447
|
+
const nextRaw = typeof value === "function" ? value(current) : value;
|
|
1448
|
+
this.state = createMessageState(this.normalizeMessages(nextRaw));
|
|
1449
|
+
this.notify();
|
|
1450
|
+
}
|
|
1451
|
+
/** Merges new options into the controller configuration. */
|
|
1452
|
+
updateOptions(partial) {
|
|
1453
|
+
const nextOptions = {
|
|
1454
|
+
...this.options,
|
|
1455
|
+
...partial,
|
|
1456
|
+
...partial.resume !== void 0 && typeof partial.resume === "object" && partial.resume !== null ? { resume: {
|
|
1457
|
+
...typeof this.options.resume === "object" && this.options.resume !== null ? this.options.resume : {},
|
|
1458
|
+
...partial.resume
|
|
1459
|
+
} } : {},
|
|
1460
|
+
...partial.optimisticUserMessage && typeof partial.optimisticUserMessage === "object" && !Array.isArray(partial.optimisticUserMessage) ? { optimisticUserMessage: {
|
|
1461
|
+
...typeof this.options.optimisticUserMessage === "object" && this.options.optimisticUserMessage !== null && !Array.isArray(this.options.optimisticUserMessage) ? this.options.optimisticUserMessage : {},
|
|
1462
|
+
...partial.optimisticUserMessage
|
|
1463
|
+
} } : {}
|
|
1464
|
+
};
|
|
1465
|
+
const shouldResetSession = this.hasSessionConfigurationChanged(this.options, nextOptions);
|
|
1466
|
+
this.options = nextOptions;
|
|
1467
|
+
if (shouldResetSession) this.resetForSessionChange();
|
|
1468
|
+
}
|
|
1469
|
+
/** Destroys the controller. */
|
|
1470
|
+
destroy() {
|
|
1471
|
+
this.cancelActiveWork();
|
|
1472
|
+
this.destroyed = true;
|
|
1473
|
+
this.listeners.clear();
|
|
1474
|
+
}
|
|
1475
|
+
/** Applies one streamed event to local state. */
|
|
1476
|
+
applyEvent(ev, options) {
|
|
1477
|
+
this.options.onEvent?.(ev);
|
|
1478
|
+
const streamKey = typeof ev.streamId === "string" && ev.streamId.length > 0 ? ev.streamId : typeof ev.runId === "string" && ev.runId.length > 0 ? ev.runId : void 0;
|
|
1479
|
+
if (typeof ev.seq === "number") if (streamKey) {
|
|
1480
|
+
const prev = this.lastAppliedSeqByStream.get(streamKey) ?? -1;
|
|
1481
|
+
if (ev.seq <= prev) return;
|
|
1482
|
+
this.lastAppliedSeqByStream.set(streamKey, ev.seq);
|
|
1483
|
+
} else {
|
|
1484
|
+
if (ev.seq <= this.lastAppliedSeq) return;
|
|
1485
|
+
this.lastAppliedSeq = ev.seq;
|
|
1486
|
+
}
|
|
1487
|
+
if (typeof ev.runId === "string" && ev.runId.length > 0 && ev.runId !== this.lastRunId) this.lastRunId = ev.runId;
|
|
1488
|
+
if (ev.type === "RUN_FINISHED") {
|
|
1489
|
+
const result = ev.result;
|
|
1490
|
+
this.lastResponse = result?.response;
|
|
1491
|
+
this.lastStructured = result?.structured;
|
|
1492
|
+
}
|
|
1493
|
+
if (ev.type === "DATA_PART") {
|
|
1494
|
+
const payload = {
|
|
1495
|
+
data: ev.data,
|
|
1496
|
+
...ev.id ? { id: ev.id } : {}
|
|
1497
|
+
};
|
|
1498
|
+
this.options.onData?.(payload);
|
|
1499
|
+
}
|
|
1500
|
+
if (ev.streamId && ev.streamId !== this.lastStreamId) this.lastStreamId = ev.streamId;
|
|
1501
|
+
if (options?.replay && ev.type === "RUN_STARTED") this.reconcileReplayUserMessage(ev.runInput, ev.runId);
|
|
1502
|
+
this.state = applyEvent(this.state, ev, { synthesizeReplayUserMessage: Boolean(options?.replay) });
|
|
1503
|
+
this.notify();
|
|
1504
|
+
}
|
|
1505
|
+
/** Reads streamed events until the stream finishes or a terminal event appears. */
|
|
1506
|
+
async consumeStreamUntilTerminal(events, options) {
|
|
1507
|
+
const result = { receivedEvent: false };
|
|
1508
|
+
try {
|
|
1509
|
+
for await (const event of events) {
|
|
1510
|
+
this.throwIfAborted(options.signal);
|
|
1511
|
+
result.receivedEvent = true;
|
|
1512
|
+
if (this.status !== "streaming") this.setStatus("streaming");
|
|
1513
|
+
const terminal = this.getTerminalStateFromEvent(event);
|
|
1514
|
+
if (terminal) {
|
|
1515
|
+
result.terminalState = terminal.state;
|
|
1516
|
+
result.terminalError = terminal.error;
|
|
1517
|
+
}
|
|
1518
|
+
this.applyEvent(event, { replay: options.replay });
|
|
1519
|
+
}
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
if (this.isAbortError(error, options.signal)) throw error;
|
|
1522
|
+
throw this.toStreamDisconnectError(error, options.disconnectMessage);
|
|
1523
|
+
}
|
|
1524
|
+
this.throwIfAborted(options.signal);
|
|
1525
|
+
return result;
|
|
1526
|
+
}
|
|
1527
|
+
/** Reads terminal state from one streamed event. */
|
|
1528
|
+
getTerminalStateFromEvent(event) {
|
|
1529
|
+
if (event.type === "RUN_FINISHED") return { state: "finished" };
|
|
1530
|
+
if (event.type === "RUN_ABORTED") return { state: "aborted" };
|
|
1531
|
+
if (event.type === "RUN_ERROR") return {
|
|
1532
|
+
state: "error",
|
|
1533
|
+
error: toAgentClientError(event.error, getEventErrorMessage(event.error))
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
/** Sends one request through final or stream delivery. */
|
|
1537
|
+
async submitWithInternalOptions(runInput, internalOptions) {
|
|
1538
|
+
const { signal, controller } = this.startOperation(internalOptions?.signal);
|
|
1539
|
+
this.setStatus("submitted");
|
|
1540
|
+
this.setError(void 0);
|
|
1541
|
+
const context = this.createSubmissionContext(runInput, internalOptions, signal);
|
|
1542
|
+
try {
|
|
1543
|
+
this.warnIfClientHistoryReplacesConversation(context);
|
|
1544
|
+
this.applyLocalSubmissionState(context);
|
|
1545
|
+
const requestInput = this.buildRequestInput(context);
|
|
1546
|
+
if (context.useFinalDelivery) return await this.runFinalDelivery(context, requestInput);
|
|
1547
|
+
return await this.runStreamDelivery(context, requestInput);
|
|
1548
|
+
} catch (e) {
|
|
1549
|
+
return this.handleSubmissionFailure(e, context);
|
|
1550
|
+
} finally {
|
|
1551
|
+
this.finishOperation(controller);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
/** Builds the mutable context for one submission. */
|
|
1555
|
+
createSubmissionContext(runInput, internalOptions, signal) {
|
|
1556
|
+
return {
|
|
1557
|
+
signal,
|
|
1558
|
+
conversationId: typeof runInput.conversationId === "string" ? runInput.conversationId : this.options.conversationId,
|
|
1559
|
+
inputValue: runInput.input,
|
|
1560
|
+
sendClientHistory: typeof runInput.sendClientHistory === "boolean" ? runInput.sendClientHistory : Boolean(this.options.sendClientHistory),
|
|
1561
|
+
optimisticLocalId: internalOptions?.reuseOptimisticLocalId,
|
|
1562
|
+
optimisticMessageMarkedSent: false,
|
|
1563
|
+
optimisticConfig: normalizeOptimisticUserMessageConfig(this.options.optimisticUserMessage),
|
|
1564
|
+
preSubmitState: this.state,
|
|
1565
|
+
useFinalDelivery: Boolean(internalOptions?.forceRun) || !this.shouldUseStreamDelivery(),
|
|
1566
|
+
runInput,
|
|
1567
|
+
internalOptions
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
/** Warns when client history will replace stored server history. */
|
|
1571
|
+
warnIfClientHistoryReplacesConversation(context) {
|
|
1572
|
+
if (!context.sendClientHistory || !context.conversationId || context.internalOptions?.replaceLocalId || this.warnedHistoryCombo) return;
|
|
1573
|
+
this.warnedHistoryCombo = true;
|
|
1574
|
+
console.warn("[better-agent] Using sendClientHistory with conversationId. Client history will replace server-stored history on each request. For server-managed history, remove sendClientHistory.");
|
|
1575
|
+
}
|
|
1576
|
+
/** Applies retry replacement or optimistic user insertion. */
|
|
1577
|
+
applyLocalSubmissionState(context) {
|
|
1578
|
+
const replaceLocalId = context.internalOptions?.replaceLocalId;
|
|
1579
|
+
if (replaceLocalId) {
|
|
1580
|
+
this.replaceRetryMessage(replaceLocalId, context.internalOptions?.replaceMessage ?? context.inputValue);
|
|
1581
|
+
context.optimisticLocalId = replaceLocalId;
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
if (context.optimisticLocalId || !context.optimisticConfig.enabled) return;
|
|
1585
|
+
if (!(!context.sendClientHistory && this.canOptimisticallyRenderUserTurn(context.inputValue) || context.sendClientHistory && this.canOmitSubmittedInputFromSerializedHistory(context.inputValue))) return;
|
|
1586
|
+
const optimisticMessage = this.createPendingUserMessage(context.inputValue);
|
|
1587
|
+
if (!optimisticMessage) return;
|
|
1588
|
+
const optimisticLocalId = this.generateMessageId();
|
|
1589
|
+
context.optimisticLocalId = optimisticLocalId;
|
|
1590
|
+
this.state = createMessageState([...this.state.messages, {
|
|
1591
|
+
...optimisticMessage,
|
|
1592
|
+
localId: optimisticLocalId
|
|
1593
|
+
}]);
|
|
1594
|
+
this.notify();
|
|
1595
|
+
}
|
|
1596
|
+
/** Builds the request input for the next transport call. */
|
|
1597
|
+
buildRequestInput(context) {
|
|
1598
|
+
return this.prepareInputForRequest(context.inputValue, this.state.messages, {
|
|
1599
|
+
sendClientHistory: context.sendClientHistory,
|
|
1600
|
+
optimisticLocalId: context.optimisticLocalId,
|
|
1601
|
+
serializedHistoryInput: Boolean(context.internalOptions?.serializedHistoryInput)
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
/** Runs one request through final delivery. */
|
|
1605
|
+
async runFinalDelivery(context, requestInput) {
|
|
1606
|
+
const result = await this.client.run(this.options.agent, this.createRunPayload(context.runInput, requestInput.inputToSend, requestInput.serializedClientHistory), {
|
|
1607
|
+
onResponse: this.options.onResponse,
|
|
1608
|
+
signal: context.signal
|
|
1609
|
+
});
|
|
1610
|
+
const normalized = this.normalizeFinalRunResult(result);
|
|
1611
|
+
this.captureNormalizedRunResult(normalized);
|
|
1612
|
+
this.markOptimisticMessageSent(context);
|
|
1613
|
+
this.setStatus("ready");
|
|
1614
|
+
this.emitFinish({
|
|
1615
|
+
isAbort: false,
|
|
1616
|
+
conversationId: context.conversationId
|
|
1617
|
+
});
|
|
1618
|
+
return {
|
|
1619
|
+
runId: normalized.runId,
|
|
1620
|
+
streamId: this.lastStreamId
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
/** Runs one request through streamed delivery. */
|
|
1624
|
+
async runStreamDelivery(context, requestInput) {
|
|
1625
|
+
const requestOptions = this.buildStreamRequestOptions(context.optimisticLocalId, () => {
|
|
1626
|
+
context.optimisticMessageMarkedSent = true;
|
|
1627
|
+
}, context.signal);
|
|
1628
|
+
const stream = this.client.stream(this.options.agent, this.createRunPayload(context.runInput, requestInput.inputToSend, requestInput.serializedClientHistory), requestOptions);
|
|
1629
|
+
const result = await this.consumeStreamUntilTerminal(stream, {
|
|
1630
|
+
signal: context.signal,
|
|
1631
|
+
disconnectMessage: "Stream disconnected."
|
|
1632
|
+
});
|
|
1633
|
+
if (!result.terminalState) throw this.toStreamDisconnectError(void 0, "Stream ended before terminal run event.");
|
|
1634
|
+
if (result.terminalState === "error") throw result.terminalError ?? /* @__PURE__ */ new Error("Run failed.");
|
|
1635
|
+
this.setStatus("ready");
|
|
1636
|
+
this.emitFinish({
|
|
1637
|
+
isAbort: result.terminalState === "aborted",
|
|
1638
|
+
conversationId: context.conversationId
|
|
1639
|
+
});
|
|
1640
|
+
return { streamId: this.lastStreamId };
|
|
1641
|
+
}
|
|
1642
|
+
/** Handles aborts, fallback-to-run, and terminal submission errors. */
|
|
1643
|
+
async handleSubmissionFailure(error, context) {
|
|
1644
|
+
if (this.destroyed || this.isAbortError(error, context.signal) || this.isStreamDisconnectError(error) && this.isPageTeardownLike()) {
|
|
1645
|
+
if (this.state !== context.preSubmitState && !context.optimisticMessageMarkedSent) {
|
|
1646
|
+
this.state = context.preSubmitState;
|
|
1647
|
+
this.notify();
|
|
1648
|
+
}
|
|
1649
|
+
if (!this.destroyed) this.setStatus("ready");
|
|
1650
|
+
return { streamId: this.lastStreamId };
|
|
1651
|
+
}
|
|
1652
|
+
let err = toAgentClientError(error, "Run failed.");
|
|
1653
|
+
if (!context.internalOptions?.forceRun && this.shouldFallbackToRun(err)) try {
|
|
1654
|
+
return await this.submitWithInternalOptions(context.runInput, {
|
|
1655
|
+
...context.internalOptions,
|
|
1656
|
+
forceRun: true,
|
|
1657
|
+
reuseOptimisticLocalId: context.optimisticLocalId
|
|
1658
|
+
});
|
|
1659
|
+
} catch (fallbackError) {
|
|
1660
|
+
err = toAgentClientError(fallbackError, "Run failed.");
|
|
1661
|
+
}
|
|
1662
|
+
this.applyOptimisticFailure(context, err);
|
|
1663
|
+
this.setError(err);
|
|
1664
|
+
this.setStatus("error");
|
|
1665
|
+
if (this.isStreamDisconnectError(error)) this.options.onDisconnect?.({
|
|
1666
|
+
error: err,
|
|
1667
|
+
...this.lastRunId ? { runId: this.lastRunId } : {},
|
|
1668
|
+
...this.lastStreamId ? { streamId: this.lastStreamId } : {}
|
|
1669
|
+
});
|
|
1670
|
+
this.options.onError?.(err);
|
|
1671
|
+
return this.lastStreamId ? { streamId: this.lastStreamId } : {};
|
|
1672
|
+
}
|
|
1673
|
+
/** Marks the optimistic user message as sent after a successful request. */
|
|
1674
|
+
markOptimisticMessageSent(context) {
|
|
1675
|
+
if (!context.optimisticLocalId || context.optimisticMessageMarkedSent) return;
|
|
1676
|
+
this.updateMessageByLocalId(context.optimisticLocalId, (msg) => {
|
|
1677
|
+
const { error: _error, ...rest } = msg;
|
|
1678
|
+
return {
|
|
1679
|
+
...rest,
|
|
1680
|
+
status: "sent"
|
|
1681
|
+
};
|
|
1682
|
+
});
|
|
1683
|
+
context.optimisticMessageMarkedSent = true;
|
|
1684
|
+
}
|
|
1685
|
+
/** Applies optimistic-message failure handling. */
|
|
1686
|
+
applyOptimisticFailure(context, err) {
|
|
1687
|
+
if (!context.optimisticLocalId) return;
|
|
1688
|
+
if (context.optimisticConfig.onError === "remove") {
|
|
1689
|
+
this.removeMessageByLocalId(context.optimisticLocalId);
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
const failed = this.updateMessageByLocalId(context.optimisticLocalId, (msg) => ({
|
|
1693
|
+
...msg,
|
|
1694
|
+
status: "failed",
|
|
1695
|
+
error: err.message
|
|
1696
|
+
}));
|
|
1697
|
+
if (failed) this.options.onOptimisticUserMessageError?.({
|
|
1698
|
+
message: failed,
|
|
1699
|
+
error: err
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
/** Stores the latest run result from final delivery. */
|
|
1703
|
+
captureNormalizedRunResult(result) {
|
|
1704
|
+
if (result.runId) this.lastRunId = result.runId;
|
|
1705
|
+
if (result.response) {
|
|
1706
|
+
this.lastResponse = result.response;
|
|
1707
|
+
this.appendResponseMessages(result.response);
|
|
1708
|
+
}
|
|
1709
|
+
this.lastStructured = result.structured;
|
|
1710
|
+
}
|
|
1711
|
+
setStatus(status) {
|
|
1712
|
+
if (this.status === status) return;
|
|
1713
|
+
this.status = status;
|
|
1714
|
+
this.notify();
|
|
1715
|
+
}
|
|
1716
|
+
/** Stores the latest controller error without notifying listeners. */
|
|
1717
|
+
setError(error) {
|
|
1718
|
+
this.error = error;
|
|
1719
|
+
}
|
|
1720
|
+
/** Normalizes unknown failures into an `Error` with a fallback message. */
|
|
1721
|
+
toError(error, fallback) {
|
|
1722
|
+
return error instanceof Error ? error : new Error(fallback);
|
|
1723
|
+
}
|
|
1724
|
+
/** Starts one controller operation. */
|
|
1725
|
+
startOperation(externalSignal) {
|
|
1726
|
+
this.throwIfDestroyed();
|
|
1727
|
+
this.cancelActiveWork();
|
|
1728
|
+
const controller = new AbortController();
|
|
1729
|
+
this.activeAbortController = controller;
|
|
1730
|
+
return {
|
|
1731
|
+
controller,
|
|
1732
|
+
signal: this.mergeSignals(controller.signal, externalSignal)
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
/** Clears the tracked active operation if it matches the provided controller. */
|
|
1736
|
+
finishOperation(controller) {
|
|
1737
|
+
if (this.activeAbortController === controller) this.activeAbortController = null;
|
|
1738
|
+
}
|
|
1739
|
+
/** Aborts and forgets any in-flight controller operation. */
|
|
1740
|
+
cancelActiveWork() {
|
|
1741
|
+
this.activeAbortController?.abort();
|
|
1742
|
+
this.activeAbortController = null;
|
|
1743
|
+
}
|
|
1744
|
+
/** Merges the controller abort signal with an optional external signal. */
|
|
1745
|
+
mergeSignals(primary, externalSignal) {
|
|
1746
|
+
if (!externalSignal) return primary;
|
|
1747
|
+
const controller = new AbortController();
|
|
1748
|
+
const abortFrom = (source) => {
|
|
1749
|
+
if (!controller.signal.aborted) controller.abort(source.reason);
|
|
1750
|
+
};
|
|
1751
|
+
if (primary.aborted) abortFrom(primary);
|
|
1752
|
+
else primary.addEventListener("abort", () => abortFrom(primary), { once: true });
|
|
1753
|
+
if (externalSignal.aborted) abortFrom(externalSignal);
|
|
1754
|
+
else externalSignal.addEventListener("abort", () => abortFrom(externalSignal), { once: true });
|
|
1755
|
+
return controller.signal;
|
|
1756
|
+
}
|
|
1757
|
+
/** Throws an abort-shaped error when the given signal has already aborted. */
|
|
1758
|
+
throwIfAborted(signal) {
|
|
1759
|
+
if (signal.aborted) throw this.toAbortError(signal.reason);
|
|
1760
|
+
}
|
|
1761
|
+
/** Rejects work after the controller has been destroyed. */
|
|
1762
|
+
throwIfDestroyed() {
|
|
1763
|
+
if (this.destroyed) throw new Error("AgentChatController has been destroyed.");
|
|
1764
|
+
}
|
|
1765
|
+
/** Returns `true` when a failure should be treated as an abort. */
|
|
1766
|
+
isAbortError(error, signal) {
|
|
1767
|
+
return (signal?.aborted ?? false) || error instanceof DOMException && error.name === "AbortError" || error instanceof Error && error.name === "AbortError";
|
|
1768
|
+
}
|
|
1769
|
+
/** Returns true when a failure came from stream disconnection. */
|
|
1770
|
+
isStreamDisconnectError(error) {
|
|
1771
|
+
return error instanceof StreamDisconnectError;
|
|
1772
|
+
}
|
|
1773
|
+
/** Returns true when the page is being torn down during navigation or refresh. */
|
|
1774
|
+
isPageTeardownLike() {
|
|
1775
|
+
return isBrowserPageTearingDown() || typeof document !== "undefined" && document.visibilityState === "hidden";
|
|
1776
|
+
}
|
|
1777
|
+
/** Converts an abort reason into a standard `AbortError` instance. */
|
|
1778
|
+
toAbortError(reason) {
|
|
1779
|
+
if (reason instanceof Error) return reason;
|
|
1780
|
+
try {
|
|
1781
|
+
return new DOMException(typeof reason === "string" ? reason : "The operation was aborted.", "AbortError");
|
|
1782
|
+
} catch {
|
|
1783
|
+
const error = new Error(typeof reason === "string" ? reason : "The operation was aborted.");
|
|
1784
|
+
error.name = "AbortError";
|
|
1785
|
+
return error;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
/** Wraps one failure as a stream disconnect. */
|
|
1789
|
+
toStreamDisconnectError(error, fallback = "Stream disconnected.") {
|
|
1790
|
+
return new StreamDisconnectError(this.toError(error, fallback).message, error);
|
|
1791
|
+
}
|
|
1792
|
+
/** Binds an already-visible current user turn to the replay run identity. */
|
|
1793
|
+
reconcileReplayUserMessage(runInput, runId) {
|
|
1794
|
+
const latestMessage = this.state.messages.at(-1);
|
|
1795
|
+
if (!latestMessage || latestMessage.role !== "user") return;
|
|
1796
|
+
const replayMessage = this.toReplayUserMessage(runInput, runId);
|
|
1797
|
+
if (!replayMessage || this.state.byLocalId.has(replayMessage.localId)) return;
|
|
1798
|
+
if (!this.isSameUserTurn(latestMessage, replayMessage)) return;
|
|
1799
|
+
const nextMessages = this.state.messages.slice();
|
|
1800
|
+
nextMessages[nextMessages.length - 1] = {
|
|
1801
|
+
...latestMessage,
|
|
1802
|
+
localId: replayMessage.localId,
|
|
1803
|
+
...replayMessage.id !== void 0 ? { id: replayMessage.id } : {},
|
|
1804
|
+
status: "sent"
|
|
1805
|
+
};
|
|
1806
|
+
this.state = createMessageState(nextMessages);
|
|
1807
|
+
}
|
|
1808
|
+
/** Reconstructs the replayed current user turn from one RUN_STARTED event. */
|
|
1809
|
+
toReplayUserMessage(runInput, runId) {
|
|
1810
|
+
const input = runInput.input;
|
|
1811
|
+
if (typeof input === "string") return {
|
|
1812
|
+
localId: `user_run:${runId}`,
|
|
1813
|
+
id: `user_run:${runId}`,
|
|
1814
|
+
role: "user",
|
|
1815
|
+
parts: [{
|
|
1816
|
+
type: "text",
|
|
1817
|
+
text: input,
|
|
1818
|
+
state: "complete"
|
|
1819
|
+
}],
|
|
1820
|
+
status: "sent"
|
|
1821
|
+
};
|
|
1822
|
+
const replayInput = Array.isArray(input) ? input : this.isSingleReplayMessageInput(input) ? [input] : void 0;
|
|
1823
|
+
if (!replayInput) return;
|
|
1824
|
+
let index = 0;
|
|
1825
|
+
const latestUserMessage = [...fromModelMessages(replayInput, { generateId: () => `user_run:${runId}:${(index++).toString(36)}` })].reverse().find((message) => message.role === "user");
|
|
1826
|
+
return latestUserMessage ? {
|
|
1827
|
+
...latestUserMessage,
|
|
1828
|
+
status: "sent"
|
|
1829
|
+
} : void 0;
|
|
1830
|
+
}
|
|
1831
|
+
/** Detects one structured replay message item. */
|
|
1832
|
+
isSingleReplayMessageInput(input) {
|
|
1833
|
+
return typeof input === "object" && input !== null && input.type === "message" && (typeof input.content === "string" || Array.isArray(input.content));
|
|
1834
|
+
}
|
|
1835
|
+
/** Matches one already-visible user turn to its replayed equivalent. */
|
|
1836
|
+
isSameUserTurn(current, replayed) {
|
|
1837
|
+
return JSON.stringify(current.parts) === JSON.stringify(replayed.parts);
|
|
1838
|
+
}
|
|
1839
|
+
/** Generates a local message id using the configured factory when present. */
|
|
1840
|
+
generateMessageId(message) {
|
|
1841
|
+
return this.options.generateMessageId?.(message) ?? makeLocalMessageId();
|
|
1842
|
+
}
|
|
1843
|
+
/** Ensures every incoming UI message has a stable local id. */
|
|
1844
|
+
normalizeMessages(list) {
|
|
1845
|
+
return list.map((message) => ({
|
|
1846
|
+
...message,
|
|
1847
|
+
localId: message.localId ?? this.generateMessageId(message)
|
|
1848
|
+
}));
|
|
1849
|
+
}
|
|
1850
|
+
/** Detects option changes that require resetting conversation session state. */
|
|
1851
|
+
hasSessionConfigurationChanged(prev, next) {
|
|
1852
|
+
return prev.agent !== next.agent || prev.conversationId !== next.conversationId || prev.hydrateFromServer !== next.hydrateFromServer || Boolean(prev.resume) !== Boolean(next.resume) || (typeof prev.resume === "object" ? prev.resume.streamId : void 0) !== (typeof next.resume === "object" ? next.resume.streamId : void 0) || (typeof prev.resume === "object" ? prev.resume.afterSeq : void 0) !== (typeof next.resume === "object" ? next.resume.afterSeq : void 0);
|
|
1853
|
+
}
|
|
1854
|
+
/** Resets controller state after a session-defining option changes. */
|
|
1855
|
+
resetForSessionChange() {
|
|
1856
|
+
this.cancelActiveWork();
|
|
1857
|
+
const normalized = this.normalizeMessages(this.options.initialMessages ?? []);
|
|
1858
|
+
this.state = createMessageState(normalized);
|
|
1859
|
+
this.initialMessages = normalized;
|
|
1860
|
+
this.lastResponse = void 0;
|
|
1861
|
+
this.lastStructured = void 0;
|
|
1862
|
+
this.lastRunId = void 0;
|
|
1863
|
+
this.lastStreamId = this.getConfiguredInitialStreamId(this.options);
|
|
1864
|
+
this.error = void 0;
|
|
1865
|
+
this.status = "ready";
|
|
1866
|
+
this.lastAppliedSeq = -1;
|
|
1867
|
+
this.lastAppliedSeqByStream.clear();
|
|
1868
|
+
this.warnedHistoryCombo = false;
|
|
1869
|
+
this.initialized = false;
|
|
1870
|
+
this.notify();
|
|
1871
|
+
this.init();
|
|
1872
|
+
}
|
|
1873
|
+
/** Reads the initial resume stream id from controller options. */
|
|
1874
|
+
getConfiguredInitialStreamId(options) {
|
|
1875
|
+
return typeof options.resume === "object" && options.resume !== null ? options.resume.streamId : void 0;
|
|
1876
|
+
}
|
|
1877
|
+
/** Returns one message by local id from the current message state. */
|
|
1878
|
+
getMessageByLocalId(localId) {
|
|
1879
|
+
const idx = this.state.byLocalId.get(localId);
|
|
1880
|
+
return idx === void 0 ? void 0 : this.state.messages[idx];
|
|
1881
|
+
}
|
|
1882
|
+
/** Updates one message by local id and notifies listeners when found. */
|
|
1883
|
+
updateMessageByLocalId(localId, updater) {
|
|
1884
|
+
const idx = this.state.byLocalId.get(localId);
|
|
1885
|
+
if (idx === void 0) return void 0;
|
|
1886
|
+
const current = this.state.messages[idx];
|
|
1887
|
+
if (!current) return void 0;
|
|
1888
|
+
const next = this.state.messages.slice();
|
|
1889
|
+
next[idx] = updater(current);
|
|
1890
|
+
this.state = createMessageState(next);
|
|
1891
|
+
this.notify();
|
|
1892
|
+
return next[idx];
|
|
1893
|
+
}
|
|
1894
|
+
/** Removes one local message from state and notifies listeners. */
|
|
1895
|
+
removeMessageByLocalId(localId) {
|
|
1896
|
+
if (this.state.byLocalId.get(localId) === void 0) return;
|
|
1897
|
+
this.state = createMessageState(this.state.messages.filter((msg) => msg.localId !== localId));
|
|
1898
|
+
this.notify();
|
|
1899
|
+
}
|
|
1900
|
+
/** Resolves the effective delivery mode for the next submission. */
|
|
1901
|
+
shouldUseStreamDelivery() {
|
|
1902
|
+
if (this.options.delivery === "stream") return true;
|
|
1903
|
+
if (this.options.delivery === "final") return false;
|
|
1904
|
+
return true;
|
|
1905
|
+
}
|
|
1906
|
+
/** Detects stream capability errors that should retry through `run()`. */
|
|
1907
|
+
shouldFallbackToRun(error) {
|
|
1908
|
+
if (this.options.delivery !== "auto") return false;
|
|
1909
|
+
const message = error.message.toLowerCase();
|
|
1910
|
+
return message.includes("does not support streaming generation") || message.includes("streaming generation is not supported") || message.includes("streaming is not supported");
|
|
1911
|
+
}
|
|
1912
|
+
/** Appends model response messages to local state after final delivery. */
|
|
1913
|
+
appendResponseMessages(response) {
|
|
1914
|
+
const responseMessages = getMessagesFromResponse(response).map((msg, i) => ({
|
|
1915
|
+
...msg,
|
|
1916
|
+
localId: this.generateMessageId(msg),
|
|
1917
|
+
...msg.id ? {} : { id: `response_${i.toString(36)}` }
|
|
1918
|
+
}));
|
|
1919
|
+
if (responseMessages.length === 0) return;
|
|
1920
|
+
this.state = createMessageState([...this.state.messages, ...responseMessages]);
|
|
1921
|
+
this.notify();
|
|
1922
|
+
}
|
|
1923
|
+
/** Normalizes supported final-run response shapes into one controller shape. */
|
|
1924
|
+
normalizeFinalRunResult(payload) {
|
|
1925
|
+
if (typeof payload !== "object" || payload === null) return {};
|
|
1926
|
+
const record = payload;
|
|
1927
|
+
const next = {};
|
|
1928
|
+
if (typeof record.runId === "string") next.runId = record.runId;
|
|
1929
|
+
const maybeResponse = (typeof record.result === "object" && record.result !== null && typeof record.result.response === "object" && record.result.response !== null ? record.result.response : void 0) ?? (typeof record.response === "object" && record.response !== null ? record.response : "output" in record && Array.isArray(record.output) ? record : void 0);
|
|
1930
|
+
if (maybeResponse) next.response = maybeResponse;
|
|
1931
|
+
if ("structured" in record) next.structured = record.structured;
|
|
1932
|
+
return next;
|
|
1933
|
+
}
|
|
1934
|
+
/** Builds the input payload, optionally serializing client history into it. */
|
|
1935
|
+
prepareInputForRequest(inputValue, baseMessages, options) {
|
|
1936
|
+
if (!options.sendClientHistory) return {
|
|
1937
|
+
inputToSend: inputValue,
|
|
1938
|
+
serializedClientHistory: false
|
|
1939
|
+
};
|
|
1940
|
+
if (options.serializedHistoryInput) return {
|
|
1941
|
+
inputToSend: inputValue,
|
|
1942
|
+
serializedClientHistory: true
|
|
1943
|
+
};
|
|
1944
|
+
const preparedMessages = this.options.prepareMessages?.({
|
|
1945
|
+
messages: baseMessages,
|
|
1946
|
+
input: inputValue
|
|
1947
|
+
});
|
|
1948
|
+
if (preparedMessages) return {
|
|
1949
|
+
inputToSend: preparedMessages,
|
|
1950
|
+
serializedClientHistory: true
|
|
1951
|
+
};
|
|
1952
|
+
const serializedMessages = toModelMessages(baseMessages);
|
|
1953
|
+
const inputItems = this.normalizeInputItems(inputValue);
|
|
1954
|
+
if (typeof inputValue === "string") return {
|
|
1955
|
+
inputToSend: options.optimisticLocalId ? serializedMessages : [...serializedMessages, {
|
|
1956
|
+
type: "message",
|
|
1957
|
+
role: "user",
|
|
1958
|
+
content: inputValue
|
|
1959
|
+
}],
|
|
1960
|
+
serializedClientHistory: true
|
|
1961
|
+
};
|
|
1962
|
+
if (inputItems) return {
|
|
1963
|
+
inputToSend: options.optimisticLocalId && this.canOmitSubmittedInputFromSerializedHistory(inputValue) ? serializedMessages : [...serializedMessages, ...inputItems],
|
|
1964
|
+
serializedClientHistory: true
|
|
1965
|
+
};
|
|
1966
|
+
return {
|
|
1967
|
+
inputToSend: inputValue,
|
|
1968
|
+
serializedClientHistory: false
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
/** Builds the run payload for one request. */
|
|
1972
|
+
createRunPayload(runInput, inputToSend, serializedClientHistory) {
|
|
1973
|
+
const conversationId = typeof runInput.conversationId === "string" ? runInput.conversationId : this.options.conversationId;
|
|
1974
|
+
const modelOptions = mergeModelOptions(this.options.modelOptions, runInput.modelOptions);
|
|
1975
|
+
const { modelOptions: _modelOptions, ...restRunInput } = runInput;
|
|
1976
|
+
return {
|
|
1977
|
+
...restRunInput,
|
|
1978
|
+
input: inputToSend,
|
|
1979
|
+
modelOptions: Object.keys(modelOptions).length > 0 ? modelOptions : void 0,
|
|
1980
|
+
conversationId,
|
|
1981
|
+
context: runInput.context !== void 0 || this.options.context !== void 0 ? runInput.context ?? this.options.context : void 0,
|
|
1982
|
+
replaceHistory: serializedClientHistory && Boolean(conversationId) ? true : void 0
|
|
1983
|
+
};
|
|
1984
|
+
}
|
|
1985
|
+
/** Builds stream request hooks for ids, callbacks, and optimistic updates. */
|
|
1986
|
+
buildStreamRequestOptions(optimisticLocalId, onMarkedSent, signal) {
|
|
1987
|
+
if (!(this.options.onResponse || this.options.onToolCall || this.options.toolHandlers) && !optimisticLocalId && !signal) return void 0;
|
|
1988
|
+
return {
|
|
1989
|
+
signal,
|
|
1990
|
+
onResponse: (response) => {
|
|
1991
|
+
this.updateResponseIds(response);
|
|
1992
|
+
this.options.onResponse?.(response);
|
|
1993
|
+
if (response.ok && optimisticLocalId) {
|
|
1994
|
+
this.updateMessageByLocalId(optimisticLocalId, (msg) => {
|
|
1995
|
+
const { error: _error, ...rest } = msg;
|
|
1996
|
+
return {
|
|
1997
|
+
...rest,
|
|
1998
|
+
status: "sent"
|
|
1999
|
+
};
|
|
2000
|
+
});
|
|
2001
|
+
onMarkedSent();
|
|
2002
|
+
}
|
|
2003
|
+
},
|
|
2004
|
+
onToolCall: this.options.onToolCall,
|
|
2005
|
+
toolHandlers: this.options.toolHandlers
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
/** Replaces one user message slot with a pending retry version. */
|
|
2009
|
+
replaceRetryMessage(replaceLocalId, inputValue) {
|
|
2010
|
+
const current = this.state.messages;
|
|
2011
|
+
const idx = this.state.byLocalId.get(replaceLocalId);
|
|
2012
|
+
const nextMessage = this.createPendingUserMessage(inputValue) ?? {
|
|
2013
|
+
localId: replaceLocalId,
|
|
2014
|
+
role: "user",
|
|
2015
|
+
parts: [{
|
|
2016
|
+
type: "text",
|
|
2017
|
+
text: String(inputValue ?? "")
|
|
2018
|
+
}],
|
|
2019
|
+
status: "pending"
|
|
2020
|
+
};
|
|
2021
|
+
let nextMessages = current;
|
|
2022
|
+
if (idx === void 0) nextMessages = [...current, nextMessage];
|
|
2023
|
+
else {
|
|
2024
|
+
const copy = current.slice(0, idx);
|
|
2025
|
+
copy.push(nextMessage);
|
|
2026
|
+
nextMessages = copy;
|
|
2027
|
+
}
|
|
2028
|
+
if (nextMessages !== current) {
|
|
2029
|
+
this.state = createMessageState(nextMessages);
|
|
2030
|
+
this.notify();
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
/** Derives a pending user message from retry input when possible. */
|
|
2034
|
+
createPendingUserMessage(inputValue) {
|
|
2035
|
+
if (typeof inputValue === "string") return {
|
|
2036
|
+
localId: this.generateMessageId(),
|
|
2037
|
+
role: "user",
|
|
2038
|
+
parts: [{
|
|
2039
|
+
type: "text",
|
|
2040
|
+
text: inputValue
|
|
2041
|
+
}],
|
|
2042
|
+
status: "pending"
|
|
2043
|
+
};
|
|
2044
|
+
if (typeof inputValue === "object" && inputValue !== null && "localId" in inputValue) {
|
|
2045
|
+
const message = inputValue;
|
|
2046
|
+
if (message.role === "user" && Array.isArray(message.parts)) {
|
|
2047
|
+
const { error: _error, ...rest } = message;
|
|
2048
|
+
return {
|
|
2049
|
+
...rest,
|
|
2050
|
+
status: "pending"
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
const items = this.normalizeInputItems(inputValue);
|
|
2055
|
+
if (!items) return;
|
|
2056
|
+
const latestUser = [...fromModelMessages(items)].reverse().find((message) => message.role === "user");
|
|
2057
|
+
if (!latestUser) return;
|
|
2058
|
+
const { error: _error, ...rest } = latestUser;
|
|
2059
|
+
return {
|
|
2060
|
+
...rest,
|
|
2061
|
+
status: "pending"
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
/** Normalizes supported input shapes into input items when possible. */
|
|
2065
|
+
normalizeInputItems(inputValue) {
|
|
2066
|
+
if (Array.isArray(inputValue)) return inputValue;
|
|
2067
|
+
if (typeof inputValue === "object" && inputValue !== null && typeof inputValue.type === "string") return [inputValue];
|
|
2068
|
+
}
|
|
2069
|
+
/** Returns true when input is safely representable as one optimistic user turn. */
|
|
2070
|
+
canOptimisticallyRenderUserTurn(inputValue) {
|
|
2071
|
+
if (typeof inputValue === "string") return true;
|
|
2072
|
+
if (typeof inputValue === "object" && inputValue !== null && "localId" in inputValue) {
|
|
2073
|
+
const message = inputValue;
|
|
2074
|
+
return message.role === "user" && Array.isArray(message.parts);
|
|
2075
|
+
}
|
|
2076
|
+
const items = this.normalizeInputItems(inputValue);
|
|
2077
|
+
if (!items || items.length !== 1) return false;
|
|
2078
|
+
const [item] = items;
|
|
2079
|
+
return item?.type === "message" && (item.role === void 0 || item.role === "user");
|
|
2080
|
+
}
|
|
2081
|
+
/** Returns true when serialized history can reuse the optimistic user turn. */
|
|
2082
|
+
canOmitSubmittedInputFromSerializedHistory(inputValue) {
|
|
2083
|
+
if (typeof inputValue === "string") return true;
|
|
2084
|
+
if (typeof inputValue === "object" && inputValue !== null && "localId" in inputValue) {
|
|
2085
|
+
const message = inputValue;
|
|
2086
|
+
return message.role === "user" && Array.isArray(message.parts);
|
|
2087
|
+
}
|
|
2088
|
+
const items = this.normalizeInputItems(inputValue);
|
|
2089
|
+
if (!items || items.length !== 1) return false;
|
|
2090
|
+
const [item] = items;
|
|
2091
|
+
return item?.type === "message" && item.role === "user";
|
|
2092
|
+
}
|
|
2093
|
+
/** Calls `onFinish` with the latest completion data. */
|
|
2094
|
+
emitFinish(overrides) {
|
|
2095
|
+
const response = this.lastResponse;
|
|
2096
|
+
const params = {
|
|
2097
|
+
messages: this.state.messages,
|
|
2098
|
+
isAbort: overrides.isAbort
|
|
2099
|
+
};
|
|
2100
|
+
if (this.lastRunId) params.runId = this.lastRunId;
|
|
2101
|
+
const streamIdToUse = overrides.streamId ?? this.lastStreamId;
|
|
2102
|
+
if (streamIdToUse) params.streamId = streamIdToUse;
|
|
2103
|
+
const conversationId = overrides.conversationId ?? this.options.conversationId;
|
|
2104
|
+
if (conversationId) params.conversationId = conversationId;
|
|
2105
|
+
if (response) {
|
|
2106
|
+
params.response = response;
|
|
2107
|
+
params.finishReason = response.finishReason;
|
|
2108
|
+
params.usage = response.usage;
|
|
2109
|
+
}
|
|
2110
|
+
if (this.lastStructured !== void 0) params.structured = this.lastStructured;
|
|
2111
|
+
this.options.onFinish?.(params);
|
|
2112
|
+
}
|
|
2113
|
+
};
|
|
2114
|
+
/**
|
|
2115
|
+
* Creates an `AgentChatController`.
|
|
2116
|
+
*/
|
|
2117
|
+
function createAgentChatController(client, options) {
|
|
2118
|
+
return new AgentChatController(client, options);
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
//#endregion
|
|
2122
|
+
export { toModelMessages as a, fromModelMessages as i, createAgentChatController as n, getEventErrorMessage as o, fromConversationItems as r, toAgentClientError as s, AgentChatController as t };
|
|
2123
|
+
//# sourceMappingURL=controller-Cf_JhTdJ.mjs.map
|