@better-agent/client 0.1.0-canary.6 → 0.2.0-beta.2

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