@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.
@@ -0,0 +1,1342 @@
1
+ import { EventType, RuntimeInterruptReason } from "@better-agent/core";
2
+ import { createRuntimeStateControl } from "@better-agent/core/runtime";
3
+
4
+ //#region src/core/errors.ts
5
+ var BetterAgentClientError = class extends Error {
6
+ status;
7
+ code;
8
+ details;
9
+ constructor(message, options = {}) {
10
+ super(message, { cause: options.cause });
11
+ this.name = "BetterAgentClientError";
12
+ this.status = options.status;
13
+ this.code = options.code;
14
+ this.details = options.details;
15
+ }
16
+ };
17
+ const toBetterAgentClientError = (error) => {
18
+ if (error instanceof BetterAgentClientError) return error;
19
+ if (error instanceof Error) return new BetterAgentClientError(error.message, { cause: error });
20
+ return new BetterAgentClientError("An unknown error occurred.", { cause: error });
21
+ };
22
+
23
+ //#endregion
24
+ //#region src/ui/helpers.ts
25
+ const toMessageRole = (role) => {
26
+ switch (role) {
27
+ case "user": return "user";
28
+ case "system": return "system";
29
+ case "developer": return "system";
30
+ default: return "assistant";
31
+ }
32
+ };
33
+ const sourceToUrl = (source) => {
34
+ if (source.type === "data") return `data:${source.mimeType ?? "application/octet-stream"};base64,${source.value}`;
35
+ return source.value;
36
+ };
37
+ const urlToSource = (url, mimeType) => {
38
+ const match = /^data:([^;,]+)?;base64,(.*)$/.exec(url);
39
+ if (match) return {
40
+ type: "data",
41
+ value: match[2] ?? "",
42
+ mimeType: mimeType ?? match[1] ?? "application/octet-stream"
43
+ };
44
+ return {
45
+ type: "url",
46
+ value: url,
47
+ ...mimeType ? { mimeType } : {}
48
+ };
49
+ };
50
+ const contentToParts = (content) => {
51
+ if (typeof content === "string") return [{
52
+ type: "text",
53
+ text: content
54
+ }];
55
+ if (!Array.isArray(content)) return [];
56
+ return content.flatMap((part) => {
57
+ if (part.type === "text") return [{
58
+ type: "text",
59
+ text: part.text
60
+ }];
61
+ if (part.type === "image" || part.type === "audio" || part.type === "video") return [{
62
+ type: part.type,
63
+ url: sourceToUrl(part.source),
64
+ mimeType: part.source.mimeType
65
+ }];
66
+ return [];
67
+ });
68
+ };
69
+ const partsToContent = (parts) => {
70
+ const content = [];
71
+ for (const part of parts) {
72
+ if (part.type === "text") {
73
+ if (part.text) content.push({
74
+ type: "text",
75
+ text: part.text
76
+ });
77
+ continue;
78
+ }
79
+ if (part.type === "image" || part.type === "audio" || part.type === "video") {
80
+ content.push({
81
+ type: part.type,
82
+ source: urlToSource(part.url, part.mimeType)
83
+ });
84
+ continue;
85
+ }
86
+ if (part.type === "file") content.push({
87
+ type: "document",
88
+ source: urlToSource(part.url, part.mimeType)
89
+ });
90
+ }
91
+ if (content.length === 1 && content[0]?.type === "text") return content[0].text;
92
+ return content;
93
+ };
94
+ const contentToText = (content) => {
95
+ if (typeof content === "string") return content;
96
+ return content.filter((part) => part.type === "text").map((part) => part.text).join("");
97
+ };
98
+
99
+ //#endregion
100
+ //#region src/ui/convert.ts
101
+ const isAgentMessageContent = (content) => {
102
+ return content === void 0 || typeof content === "string" || Array.isArray(content);
103
+ };
104
+ const fromAgentMessages = (messages) => {
105
+ const parseToolInput = (input) => {
106
+ try {
107
+ return JSON.parse(input);
108
+ } catch {
109
+ return;
110
+ }
111
+ };
112
+ const parseToolContent = (content) => {
113
+ try {
114
+ return JSON.parse(content);
115
+ } catch {
116
+ return content;
117
+ }
118
+ };
119
+ const toolResults = /* @__PURE__ */ new Map();
120
+ for (const message of messages) if (message.role === "tool") toolResults.set(message.toolCallId, {
121
+ content: message.content,
122
+ error: message.error,
123
+ status: message.status,
124
+ approval: message.approval
125
+ });
126
+ const uiMessages = [];
127
+ let pendingReasoning = [];
128
+ for (const message of messages) {
129
+ const parts = isAgentMessageContent(message.content) ? contentToParts(message.content) : [];
130
+ if (message.role === "reasoning") {
131
+ if (typeof message.content === "string") pendingReasoning = [...pendingReasoning, {
132
+ id: message.id,
133
+ part: {
134
+ type: "reasoning",
135
+ text: message.content
136
+ }
137
+ }];
138
+ continue;
139
+ }
140
+ if (message.role === "assistant") {
141
+ const assistantParts = [...pendingReasoning.map((reasoning) => reasoning.part), ...parts];
142
+ pendingReasoning = [];
143
+ if (Array.isArray(message.toolCalls)) for (const toolCall of message.toolCalls) {
144
+ const result = toolResults.get(toolCall.id);
145
+ const toolPart = {
146
+ inputText: toolCall.function.arguments,
147
+ input: parseToolInput(toolCall.function.arguments),
148
+ toolCallId: toolCall.id,
149
+ toolName: toolCall.function.name,
150
+ type: "tool-call",
151
+ ...toolCall.providerExecuted ? { providerExecuted: true } : {},
152
+ state: "input-available",
153
+ ...result?.approval ? {
154
+ approval: {
155
+ interruptId: `${toolCall.id}:approval`,
156
+ needsApproval: true,
157
+ approved: result.approval.approved,
158
+ ...result.approval.metadata ? { metadata: result.approval.metadata } : {}
159
+ },
160
+ state: "approval-responded"
161
+ } : {}
162
+ };
163
+ assistantParts.push(toolPart);
164
+ if (result) {
165
+ const deniedResult = result.content.length > 0 ? (() => {
166
+ const parsed = parseToolContent(result.content);
167
+ return typeof parsed === "string" ? parsed : void 0;
168
+ })() : void 0;
169
+ const resultPart = result.status === "denied" ? {
170
+ type: "tool-result",
171
+ toolCallId: toolCall.id,
172
+ state: "output-denied",
173
+ result: deniedResult
174
+ } : result.status === "error" || result.error ? {
175
+ type: "tool-result",
176
+ toolCallId: toolCall.id,
177
+ state: "output-error",
178
+ error: result.error ?? result.content
179
+ } : {
180
+ type: "tool-result",
181
+ toolCallId: toolCall.id,
182
+ state: "output-available",
183
+ result: parseToolContent(result.content)
184
+ };
185
+ assistantParts.push(resultPart);
186
+ }
187
+ }
188
+ for (const source of message.sources ?? []) assistantParts.push({
189
+ type: "source",
190
+ sourceId: source.id,
191
+ sourceType: "url",
192
+ url: source.url,
193
+ ...source.title ? { title: source.title } : {}
194
+ });
195
+ uiMessages.push({
196
+ id: message.id,
197
+ role: message.role,
198
+ parts: assistantParts
199
+ });
200
+ continue;
201
+ }
202
+ if (message.role === "user" || message.role === "system" || message.role === "developer") uiMessages.push({
203
+ id: message.id,
204
+ role: toMessageRole(message.role),
205
+ parts
206
+ });
207
+ }
208
+ if (pendingReasoning.length > 0) for (const reasoning of pendingReasoning) uiMessages.push({
209
+ id: reasoning.id,
210
+ role: "assistant",
211
+ parts: [reasoning.part]
212
+ });
213
+ return uiMessages;
214
+ };
215
+ const toAgentMessages = (messages) => {
216
+ return messages.map((message) => {
217
+ const content = partsToContent(message.parts);
218
+ if (message.role === "assistant") {
219
+ const toolCalls = message.parts.filter((part) => part.type === "tool-call").map((part) => ({
220
+ id: part.toolCallId,
221
+ type: "function",
222
+ function: {
223
+ name: part.toolName,
224
+ arguments: part.inputText
225
+ },
226
+ ...part.providerExecuted ? { providerExecuted: true } : {}
227
+ }));
228
+ return {
229
+ id: message.id,
230
+ role: "assistant",
231
+ content,
232
+ ...toolCalls.length > 0 ? { toolCalls } : {}
233
+ };
234
+ }
235
+ if (message.role === "system") return {
236
+ id: message.id,
237
+ role: "system",
238
+ content: contentToText(content)
239
+ };
240
+ return {
241
+ id: message.id,
242
+ role: "user",
243
+ content
244
+ };
245
+ });
246
+ };
247
+
248
+ //#endregion
249
+ //#region src/ui/messages.ts
250
+ const createMessageId = () => {
251
+ const webCrypto = globalThis.crypto;
252
+ if (typeof webCrypto?.randomUUID === "function") return `msg_${webCrypto.randomUUID()}`;
253
+ if (typeof webCrypto?.getRandomValues === "function") {
254
+ const bytes = webCrypto.getRandomValues(new Uint8Array(16));
255
+ return `msg_${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
256
+ }
257
+ return `msg_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
258
+ };
259
+ const createUserUIMessage = (content) => ({
260
+ id: createMessageId(),
261
+ role: "user",
262
+ parts: [{
263
+ type: "text",
264
+ text: content
265
+ }]
266
+ });
267
+
268
+ //#endregion
269
+ //#region src/ui/reducer.ts
270
+ const updateMessage = (messages, messageId, update) => {
271
+ return messages.map((message) => message.id === messageId ? update(message) : message);
272
+ };
273
+ const upsertMessage = (messages, message, update) => {
274
+ return messages.some((current) => current.id === message.id) ? updateMessage(messages, message.id, update) : [...messages, update(message)];
275
+ };
276
+ const updateToolPart = (messages, toolCallId, update) => {
277
+ return messages.map((message) => ({
278
+ ...message,
279
+ parts: message.parts.map((part) => part.type === "tool-call" && part.toolCallId === toolCallId ? update(part) : part)
280
+ }));
281
+ };
282
+ const upsertToolResultPartByToolCallId = (messages, toolCallId, update) => {
283
+ return messages.map((message) => {
284
+ const existingResultIndex = message.parts.findIndex((part) => part.type === "tool-result" && part.toolCallId === toolCallId);
285
+ if (existingResultIndex !== -1) return {
286
+ ...message,
287
+ parts: message.parts.map((part, index) => index === existingResultIndex && part.type === "tool-result" ? update(part) : part)
288
+ };
289
+ const toolCallIndex = message.parts.findIndex((part) => part.type === "tool-call" && part.toolCallId === toolCallId);
290
+ if (toolCallIndex === -1) return message;
291
+ const parts = [...message.parts];
292
+ parts.splice(toolCallIndex + 1, 0, update({
293
+ type: "tool-result",
294
+ toolCallId,
295
+ state: "output-available"
296
+ }));
297
+ return {
298
+ ...message,
299
+ parts
300
+ };
301
+ });
302
+ };
303
+ const upsertSourcePart = (messages, messageId, part) => {
304
+ return upsertMessage(messages, {
305
+ id: messageId,
306
+ role: "assistant",
307
+ parts: []
308
+ }, (message) => {
309
+ return message.parts.some((current) => current.type === "source" && current.sourceId === part.sourceId) ? message : {
310
+ ...message,
311
+ parts: [...message.parts, part]
312
+ };
313
+ });
314
+ };
315
+ const parseToolContent = (content) => {
316
+ if (!content.trim()) return "";
317
+ try {
318
+ return JSON.parse(content);
319
+ } catch {
320
+ return content;
321
+ }
322
+ };
323
+ const createUIReducerState = (messages = []) => {
324
+ return { messages };
325
+ };
326
+ const applyUIEvent = (state, event) => {
327
+ if (event.type === EventType.RUN_STARTED) {
328
+ const messages = event.input?.messages?.filter((message) => message.role !== "system");
329
+ if (!messages || messages.length === 0) return state;
330
+ return {
331
+ ...state,
332
+ messages: fromAgentMessages(messages)
333
+ };
334
+ }
335
+ if (event.type === EventType.TEXT_MESSAGE_START) {
336
+ if (state.messages.some((message) => message.id === event.messageId)) return state;
337
+ return {
338
+ ...state,
339
+ messages: [...state.messages, {
340
+ id: event.messageId,
341
+ role: toMessageRole(event.role),
342
+ parts: []
343
+ }]
344
+ };
345
+ }
346
+ if (event.type === EventType.TEXT_MESSAGE_CONTENT) {
347
+ const messageIndex = state.messages.findIndex((message) => message.id === event.messageId);
348
+ const targetMessage = messageIndex === -1 ? {
349
+ id: event.messageId,
350
+ role: "assistant",
351
+ parts: []
352
+ } : state.messages[messageIndex];
353
+ const parts = [...targetMessage.parts];
354
+ const lastPart = parts[parts.length - 1];
355
+ if (lastPart?.type === "text") parts[parts.length - 1] = {
356
+ ...lastPart,
357
+ text: lastPart.text + event.delta
358
+ };
359
+ else parts.push({
360
+ type: "text",
361
+ text: event.delta
362
+ });
363
+ const nextMessage = {
364
+ ...targetMessage,
365
+ parts
366
+ };
367
+ return {
368
+ ...state,
369
+ messages: messageIndex === -1 ? [...state.messages, nextMessage] : state.messages.map((message, index) => index === messageIndex ? nextMessage : message)
370
+ };
371
+ }
372
+ if (event.type === EventType.REASONING_MESSAGE_START) {
373
+ if (state.messages.some((message) => message.id === event.messageId)) return state;
374
+ return {
375
+ ...state,
376
+ messages: [...state.messages, {
377
+ id: event.messageId,
378
+ role: "assistant",
379
+ parts: []
380
+ }]
381
+ };
382
+ }
383
+ if (event.type === EventType.REASONING_MESSAGE_CONTENT) {
384
+ const messageIndex = state.messages.findIndex((message) => message.id === event.messageId);
385
+ const targetMessage = messageIndex === -1 ? {
386
+ id: event.messageId,
387
+ role: "assistant",
388
+ parts: []
389
+ } : state.messages[messageIndex];
390
+ const parts = [...targetMessage.parts];
391
+ const lastPart = parts[parts.length - 1];
392
+ if (lastPart?.type === "reasoning") parts[parts.length - 1] = {
393
+ ...lastPart,
394
+ text: lastPart.text + event.delta
395
+ };
396
+ else parts.push({
397
+ type: "reasoning",
398
+ text: event.delta
399
+ });
400
+ const nextMessage = {
401
+ ...targetMessage,
402
+ parts
403
+ };
404
+ return {
405
+ ...state,
406
+ messages: messageIndex === -1 ? [...state.messages, nextMessage] : state.messages.map((message, index) => index === messageIndex ? nextMessage : message)
407
+ };
408
+ }
409
+ if (event.type === EventType.REASONING_MESSAGE_END) return state;
410
+ if (event.type === EventType.MESSAGES_SNAPSHOT) return {
411
+ ...state,
412
+ messages: fromAgentMessages(event.messages)
413
+ };
414
+ if (event.type === EventType.CUSTOM && event.name === "source" && event.value) {
415
+ const value = event.value;
416
+ return {
417
+ ...state,
418
+ messages: upsertSourcePart(state.messages, value.messageId, {
419
+ type: "source",
420
+ sourceId: value.source.id,
421
+ sourceType: "url",
422
+ url: value.source.url,
423
+ ...value.source.title ? { title: value.source.title } : {}
424
+ })
425
+ };
426
+ }
427
+ if (event.type === EventType.TOOL_CALL_START) {
428
+ if (!event.parentMessageId) return state;
429
+ if (state.messages.some((message) => message.parts.some((part) => part.type === "tool-call" && part.toolCallId === event.toolCallId))) return state;
430
+ return {
431
+ ...state,
432
+ messages: upsertMessage(state.messages, {
433
+ id: event.parentMessageId,
434
+ role: "assistant",
435
+ parts: []
436
+ }, (message) => ({
437
+ ...message,
438
+ parts: [...message.parts, {
439
+ type: "tool-call",
440
+ state: "input-streaming",
441
+ toolCallId: event.toolCallId,
442
+ toolName: event.toolCallName,
443
+ inputText: "",
444
+ ...event.providerExecuted !== void 0 ? { providerExecuted: event.providerExecuted } : {}
445
+ }]
446
+ }))
447
+ };
448
+ }
449
+ if (event.type === EventType.TOOL_CALL_ARGS) return {
450
+ ...state,
451
+ messages: updateToolPart(state.messages, event.toolCallId, (part) => ({
452
+ ...part,
453
+ inputText: part.inputText.length === 0 ? event.delta : event.delta.startsWith(part.inputText) ? event.delta : part.inputText.endsWith(event.delta) ? part.inputText : part.inputText + event.delta
454
+ }))
455
+ };
456
+ if (event.type === EventType.TOOL_CALL_CHUNK) {
457
+ if (!event.toolCallId || !event.input || typeof event.input !== "string") return state;
458
+ return {
459
+ ...state,
460
+ messages: updateToolPart(state.messages, event.toolCallId, (part) => ({
461
+ ...part,
462
+ inputText: part.inputText.length === 0 ? event.input : event.input.startsWith(part.inputText) ? event.input : part.inputText.startsWith(event.input) ? part.inputText : event.input
463
+ }))
464
+ };
465
+ }
466
+ if (event.type === EventType.TOOL_CALL_END) return {
467
+ ...state,
468
+ messages: updateToolPart(state.messages, event.toolCallId, (part) => ({
469
+ ...part,
470
+ state: "input-available",
471
+ input: typeof part.inputText === "string" && part.inputText.length > 0 ? (() => {
472
+ try {
473
+ return JSON.parse(part.inputText);
474
+ } catch {
475
+ return part.input;
476
+ }
477
+ })() : part.input
478
+ }))
479
+ };
480
+ if (event.type === EventType.TOOL_CALL_RESULT) {
481
+ const status = "status" in event ? event.status : void 0;
482
+ const parsedContent = parseToolContent(event.content);
483
+ const deniedResult = status === "denied" && typeof parsedContent !== "string" ? void 0 : parsedContent;
484
+ return {
485
+ ...state,
486
+ messages: upsertToolResultPartByToolCallId(state.messages, event.toolCallId, (part) => ({
487
+ ...part,
488
+ state: status === "denied" ? "output-denied" : status === "error" ? "output-error" : "output-available",
489
+ ...status === "error" ? {
490
+ error: event.content,
491
+ result: void 0
492
+ } : {
493
+ result: deniedResult,
494
+ error: void 0
495
+ }
496
+ }))
497
+ };
498
+ }
499
+ return { ...state };
500
+ };
501
+
502
+ //#endregion
503
+ //#region src/core/browser-lifecycle.ts
504
+ const markPageTeardown = () => {
505
+ if (typeof window !== "undefined") window.__baPageTeardown = true;
506
+ };
507
+ const ensureBrowserTeardownTracking = () => {
508
+ if (typeof window === "undefined") return;
509
+ window.__baPageTeardown ??= false;
510
+ if (window.__baPageTeardownInstalled) return;
511
+ window.__baPageTeardownInstalled = true;
512
+ window.addEventListener("pagehide", markPageTeardown);
513
+ window.addEventListener("beforeunload", markPageTeardown);
514
+ };
515
+ const isBrowserPageTearingDown = () => typeof window !== "undefined" && window.__baPageTeardown === true;
516
+
517
+ //#endregion
518
+ //#region src/core/controller.ts
519
+ var AgentController = class {
520
+ messages;
521
+ stateControl;
522
+ status = "ready";
523
+ error;
524
+ runId;
525
+ threadId;
526
+ pendingClientTools = [];
527
+ pendingToolApprovals = [];
528
+ stateRevision = 0;
529
+ snapshot;
530
+ listeners = /* @__PURE__ */ new Set();
531
+ activeAbortController;
532
+ finishMessageStartIndex = 0;
533
+ stopRequested = false;
534
+ stopWaitTimer;
535
+ abortRequestedRunId;
536
+ started = false;
537
+ constructor(agent, options) {
538
+ this.agent = agent;
539
+ this.options = options;
540
+ ensureBrowserTeardownTracking();
541
+ this.messages = options.initialMessages ?? [];
542
+ this.stateControl = createRuntimeStateControl(options.initialState);
543
+ this.threadId = options.threadId;
544
+ this.runId = options.initialInterruptState?.runId;
545
+ this.pendingClientTools = [...options.initialInterruptState?.pendingClientTools ?? []];
546
+ this.pendingToolApprovals = [...options.initialInterruptState?.pendingToolApprovals ?? []];
547
+ this.status = options.initialInterruptState?.status ?? (this.pendingClientTools.length > 0 || this.pendingToolApprovals.length > 0 ? "interrupted" : "ready");
548
+ for (const approval of this.pendingToolApprovals) this.updateToolCallPart(approval.toolCallId, {
549
+ state: "approval-requested",
550
+ approval: {
551
+ interruptId: approval.interruptId,
552
+ needsApproval: true,
553
+ ...approval.metadata ? { metadata: approval.metadata } : {}
554
+ }
555
+ });
556
+ this.snapshot = this.createSnapshot();
557
+ }
558
+ start() {
559
+ if (this.started) return;
560
+ this.started = true;
561
+ const shouldAutoHydrateThread = this.threadId && this.options.initialMessages === void 0 && "memory" in this.agent;
562
+ if (this.options.resume) (async () => {
563
+ try {
564
+ if (shouldAutoHydrateThread) await this.loadMessages(this.threadId, { beforeRunId: this.options.resume?.runId });
565
+ await this.resume(this.options.resume);
566
+ } catch {}
567
+ })();
568
+ else if (this.pendingClientTools.length > 0 && !this.hasUndecidedApprovals()) this.runPendingInterruptsWithLifecycle();
569
+ else if (shouldAutoHydrateThread && this.threadId) this.loadThread(this.threadId).catch(() => {});
570
+ }
571
+ getSnapshot() {
572
+ return this.snapshot;
573
+ }
574
+ createSnapshot() {
575
+ return {
576
+ messages: this.messages,
577
+ state: this.stateControl.get(),
578
+ status: this.status,
579
+ error: this.error,
580
+ runId: this.runId,
581
+ threadId: this.threadId,
582
+ isRunning: this.status === "submitted" || this.status === "streaming",
583
+ pendingClientTools: this.pendingClientTools,
584
+ pendingToolApprovals: this.getPendingToolApprovalsSnapshot()
585
+ };
586
+ }
587
+ getPendingToolApprovalsSnapshot() {
588
+ return this.pendingToolApprovals.filter((approval) => approval.approved === void 0);
589
+ }
590
+ isClientToolInterrupt(interrupt) {
591
+ return Boolean(interrupt?.reason === RuntimeInterruptReason.ClientToolPending && interrupt.toolCallId);
592
+ }
593
+ isApprovalInterrupt(interrupt) {
594
+ return Boolean(interrupt?.reason === RuntimeInterruptReason.ToolApprovalPending && interrupt.toolCallId);
595
+ }
596
+ getToolCallPart(toolCallId) {
597
+ for (const message of this.messages) {
598
+ const part = message.parts.find((candidate) => candidate.type === "tool-call" && candidate.toolCallId === toolCallId);
599
+ if (part) return part;
600
+ }
601
+ }
602
+ combineAbortSignals(primary, secondary) {
603
+ if (!secondary) return primary;
604
+ return AbortSignal.any([primary, secondary]);
605
+ }
606
+ hasUndecidedApprovals() {
607
+ return this.pendingToolApprovals.some((approval) => approval.approved === void 0);
608
+ }
609
+ subscribe(listener) {
610
+ this.listeners.add(listener);
611
+ return () => {
612
+ this.listeners.delete(listener);
613
+ };
614
+ }
615
+ setMessages(messages) {
616
+ this.messages = messages;
617
+ this.notify();
618
+ }
619
+ async loadMessages(threadId = this.threadId, options) {
620
+ if (!threadId) throw new BetterAgentClientError("Cannot load messages without a threadId.");
621
+ const memory = "memory" in this.agent ? this.agent.memory : void 0;
622
+ if (!memory) throw new BetterAgentClientError("Agent memory is not available.");
623
+ try {
624
+ const messages = await memory.messages.list(threadId, options);
625
+ this.threadId = threadId;
626
+ this.messages = fromAgentMessages(messages.map((message) => {
627
+ const { threadId: _threadId, runId: _runId, createdAt: _createdAt, ...rest } = message;
628
+ return rest;
629
+ }));
630
+ for (const approval of this.pendingToolApprovals) this.updateToolCallPart(approval.toolCallId, {
631
+ state: "approval-requested",
632
+ approval: {
633
+ interruptId: approval.interruptId,
634
+ needsApproval: true,
635
+ ...approval.metadata ? { metadata: approval.metadata } : {}
636
+ }
637
+ });
638
+ this.error = void 0;
639
+ this.notify();
640
+ } catch (error) {
641
+ this.error = toBetterAgentClientError(error);
642
+ this.status = "error";
643
+ this.notify();
644
+ throw this.error;
645
+ }
646
+ }
647
+ async loadThread(threadId) {
648
+ const memory = "memory" in this.agent ? this.agent.memory : void 0;
649
+ if (!memory) throw new BetterAgentClientError("Agent memory is not available.");
650
+ const runtime = await memory.threads.runtime(threadId);
651
+ if (runtime.resumable) {
652
+ await this.loadMessages(threadId, { beforeRunId: runtime.resumable.runId });
653
+ await this.resume(runtime.resumable);
654
+ return;
655
+ }
656
+ await this.loadMessages(threadId);
657
+ if (runtime.interrupted) {
658
+ this.hydrateInterrupts(runtime.interrupted.runId, runtime.interrupted.interrupts);
659
+ if (this.pendingClientTools.length > 0 && !this.hasUndecidedApprovals()) await this.runPendingInterruptsWithLifecycle();
660
+ }
661
+ }
662
+ hydrateInterrupts(runId, interrupts) {
663
+ const approvalInterrupts = interrupts.filter((interrupt) => this.isApprovalInterrupt(interrupt));
664
+ const clientToolInterrupts = interrupts.filter((interrupt) => this.isClientToolInterrupt(interrupt));
665
+ const approvals = [];
666
+ for (const interrupt of approvalInterrupts) {
667
+ const pending = this.createPendingApproval(runId, interrupt);
668
+ if ("isError" in pending) return;
669
+ approvals.push(pending);
670
+ this.updateToolCallPart(interrupt.toolCallId, {
671
+ state: "approval-requested",
672
+ approval: {
673
+ interruptId: interrupt.id,
674
+ needsApproval: true,
675
+ ...interrupt.metadata ? { metadata: interrupt.metadata } : {}
676
+ }
677
+ });
678
+ }
679
+ const clientTools = [];
680
+ for (const interrupt of clientToolInterrupts) {
681
+ const pending = this.createPendingClientTool(runId, interrupt);
682
+ if ("isError" in pending) return;
683
+ clientTools.push(pending);
684
+ }
685
+ this.runId = runId;
686
+ this.pendingToolApprovals = approvals;
687
+ this.pendingClientTools = clientTools;
688
+ this.status = "interrupted";
689
+ this.error = void 0;
690
+ this.notify();
691
+ }
692
+ async selectThread(threadId) {
693
+ this.stop();
694
+ await this.loadThread(threadId);
695
+ }
696
+ clearThread() {
697
+ this.stop();
698
+ this.messages = [];
699
+ this.threadId = void 0;
700
+ this.runId = void 0;
701
+ this.error = void 0;
702
+ this.pendingClientTools = [];
703
+ this.pendingToolApprovals = [];
704
+ this.status = "ready";
705
+ this.notify();
706
+ }
707
+ clearStopWaitTimer() {
708
+ if (this.stopWaitTimer) {
709
+ clearTimeout(this.stopWaitTimer);
710
+ this.stopWaitTimer = void 0;
711
+ }
712
+ }
713
+ resetStopRequest() {
714
+ this.stopRequested = false;
715
+ this.abortRequestedRunId = void 0;
716
+ this.clearStopWaitTimer();
717
+ }
718
+ abortServerRun(runId) {
719
+ if (this.abortRequestedRunId === runId) return;
720
+ this.abortRequestedRunId = runId;
721
+ this.agent.runs.abort(runId).catch((error) => {
722
+ console.error("[better-agent] abortRun failed", error);
723
+ });
724
+ }
725
+ abortActiveRequest() {
726
+ this.activeAbortController?.abort();
727
+ this.activeAbortController = void 0;
728
+ }
729
+ stop() {
730
+ this.stopRequested = true;
731
+ const runId = this.runId;
732
+ if (runId) {
733
+ this.clearStopWaitTimer();
734
+ this.abortServerRun(runId);
735
+ this.abortActiveRequest();
736
+ } else if (this.activeAbortController && !this.stopWaitTimer) {
737
+ const activeAbortController = this.activeAbortController;
738
+ this.stopWaitTimer = setTimeout(() => {
739
+ if (this.stopRequested && this.activeAbortController === activeAbortController) this.abortActiveRequest();
740
+ this.clearStopWaitTimer();
741
+ }, 2e3);
742
+ }
743
+ if (this.status !== "ready") {
744
+ this.status = "ready";
745
+ this.notify();
746
+ }
747
+ }
748
+ safeLifecycle(name, fn) {
749
+ try {
750
+ const result = fn();
751
+ if (result !== void 0 && typeof result === "object" && "then" in result && typeof result.then === "function") result.catch((err) => {
752
+ console.error(`[better-agent] ${name} callback failed`, err);
753
+ });
754
+ } catch (err) {
755
+ console.error(`[better-agent] ${name} callback failed`, err);
756
+ }
757
+ }
758
+ emitEvent(hooks, event) {
759
+ const onEvent = hooks.onEvent;
760
+ if (!onEvent) return;
761
+ this.safeLifecycle("onEvent", () => onEvent(event));
762
+ }
763
+ invokeLifecycleFinish(hooks, finish) {
764
+ this.safeLifecycle("onFinish", () => hooks.onFinish?.(finish));
765
+ const error = finish.error;
766
+ if (finish.isError && error) {
767
+ const onError = hooks.onError;
768
+ if (onError) this.safeLifecycle("onError", () => onError(error));
769
+ }
770
+ }
771
+ createFinish(finish) {
772
+ const messages = [...this.messages];
773
+ const generatedMessages = messages.slice(this.finishMessageStartIndex);
774
+ const message = generatedMessages[generatedMessages.length - 1];
775
+ return {
776
+ ...message ? { message } : {},
777
+ generatedMessages,
778
+ messages,
779
+ runId: this.runId,
780
+ threadId: this.threadId,
781
+ pendingClientTools: finish.pendingClientTools ?? [...this.pendingClientTools],
782
+ pendingToolApprovals: finish.pendingToolApprovals ?? this.getPendingToolApprovalsSnapshot(),
783
+ ...finish
784
+ };
785
+ }
786
+ async sendMessage(input, sendOptions) {
787
+ const hooks = {
788
+ onEvent: this.options.onEvent,
789
+ onFinish: this.options.onFinish,
790
+ onError: this.options.onError
791
+ };
792
+ let terminal;
793
+ this.stop();
794
+ this.resetStopRequest();
795
+ this.error = void 0;
796
+ this.pendingClientTools = [];
797
+ this.pendingToolApprovals = [];
798
+ this.status = "submitted";
799
+ const inputMessages = typeof input === "string" ? [createUserUIMessage(input)] : input;
800
+ this.messages = [...this.messages, ...inputMessages];
801
+ this.finishMessageStartIndex = this.messages.length;
802
+ this.notify();
803
+ const abortController = new AbortController();
804
+ this.activeAbortController = abortController;
805
+ const streamSignal = this.combineAbortSignals(abortController.signal, sendOptions?.signal ?? null);
806
+ const context = sendOptions?.context ?? this.options.context;
807
+ try {
808
+ const stream = this.agent.stream({
809
+ messages: toAgentMessages(this.threadId ? inputMessages : this.messages).map(({ id: _id, ...message }) => message),
810
+ ...context !== void 0 ? { context } : {},
811
+ ...this.stateControl.get() !== void 0 ? { state: this.stateControl.get() } : {},
812
+ ...this.threadId !== void 0 ? { threadId: this.threadId } : {}
813
+ }, { signal: streamSignal });
814
+ terminal = await this.consumeStream(stream, hooks, streamSignal);
815
+ } catch (error) {
816
+ if (streamSignal.aborted || isBrowserPageTearingDown()) {
817
+ this.status = "ready";
818
+ this.notify();
819
+ terminal = this.createFinish({
820
+ isAbort: true,
821
+ isDisconnect: false,
822
+ isError: false,
823
+ isInterrupted: false
824
+ });
825
+ } else {
826
+ this.error = toBetterAgentClientError(error);
827
+ this.status = "error";
828
+ this.notify();
829
+ terminal = this.createFinish({
830
+ isAbort: false,
831
+ isDisconnect: false,
832
+ isError: true,
833
+ isInterrupted: false,
834
+ error: this.error
835
+ });
836
+ }
837
+ } finally {
838
+ if (this.activeAbortController === abortController) this.activeAbortController = void 0;
839
+ if (terminal !== void 0) this.invokeLifecycleFinish(hooks, terminal);
840
+ }
841
+ }
842
+ async approveToolCall(interruptId, metadata) {
843
+ await this.resolveApprovalDecision(interruptId, true, metadata);
844
+ }
845
+ async rejectToolCall(interruptId, metadata) {
846
+ await this.resolveApprovalDecision(interruptId, false, metadata);
847
+ }
848
+ async resume(resume) {
849
+ const nextResume = resume ?? (this.runId ? { runId: this.runId } : void 0);
850
+ if (!nextResume) throw new BetterAgentClientError("Cannot resume without a runId.");
851
+ const hooks = {
852
+ onEvent: this.options.onEvent,
853
+ onFinish: this.options.onFinish,
854
+ onError: this.options.onError
855
+ };
856
+ const abortController = new AbortController();
857
+ this.resetStopRequest();
858
+ this.activeAbortController?.abort();
859
+ this.activeAbortController = abortController;
860
+ this.runId = nextResume.runId;
861
+ this.status = "streaming";
862
+ this.finishMessageStartIndex = this.messages.length;
863
+ this.notify();
864
+ let terminal;
865
+ try {
866
+ const stream = this.agent.runs.resumeStream({
867
+ runId: nextResume.runId,
868
+ afterSequence: nextResume.afterSequence
869
+ }, { signal: abortController.signal });
870
+ terminal = await this.consumeStream(stream, hooks, abortController.signal);
871
+ } catch (error) {
872
+ if (abortController.signal.aborted || isBrowserPageTearingDown()) {
873
+ this.status = "ready";
874
+ this.notify();
875
+ terminal = this.createFinish({
876
+ isAbort: true,
877
+ isDisconnect: false,
878
+ isError: false,
879
+ isInterrupted: false
880
+ });
881
+ } else {
882
+ this.error = toBetterAgentClientError(error);
883
+ this.status = "error";
884
+ this.notify();
885
+ terminal = this.createFinish({
886
+ isAbort: false,
887
+ isDisconnect: false,
888
+ isError: true,
889
+ isInterrupted: false,
890
+ error: this.error
891
+ });
892
+ }
893
+ } finally {
894
+ if (this.activeAbortController === abortController) this.activeAbortController = void 0;
895
+ if (terminal !== void 0) this.invokeLifecycleFinish(hooks, terminal);
896
+ }
897
+ }
898
+ async consumeStream(stream, hooks, signal, beforeSuccessFinish) {
899
+ for await (const event of stream) {
900
+ if (signal.aborted) break;
901
+ const terminal = await this.processStreamEvent(event, hooks, signal);
902
+ if (terminal) return terminal;
903
+ }
904
+ if (signal.aborted) return this.createFinish({
905
+ isAbort: true,
906
+ isDisconnect: false,
907
+ isError: false,
908
+ isInterrupted: false
909
+ });
910
+ beforeSuccessFinish?.();
911
+ this.status = "ready";
912
+ this.notify();
913
+ return this.createFinish({
914
+ isAbort: false,
915
+ isDisconnect: false,
916
+ isError: false,
917
+ isInterrupted: false
918
+ });
919
+ }
920
+ async processStreamEvent(event, hooks, signal) {
921
+ if (event.type === EventType.RUN_ERROR) {
922
+ this.error = new BetterAgentClientError("message" in event && typeof event.message === "string" ? event.message : "Run failed.", {
923
+ code: "code" in event && typeof event.code === "string" ? event.code : void 0,
924
+ details: event
925
+ });
926
+ this.status = "error";
927
+ this.notify();
928
+ this.emitEvent(hooks, event);
929
+ return this.createFinish({
930
+ isAbort: false,
931
+ isDisconnect: false,
932
+ isError: true,
933
+ isInterrupted: false,
934
+ error: this.error
935
+ });
936
+ }
937
+ if (event.type === EventType.RUN_FINISHED && event.outcome === "interrupt") {
938
+ this.applyEvent(event);
939
+ this.notify();
940
+ this.emitEvent(hooks, event);
941
+ const interrupts = event.interrupts ?? [];
942
+ const runId = event.runId ?? this.runId;
943
+ if (!runId && interrupts.length > 0) {
944
+ this.error = new BetterAgentClientError("Interrupt missing runId.");
945
+ this.status = "error";
946
+ this.notify();
947
+ return this.createFinish({
948
+ isAbort: false,
949
+ isDisconnect: false,
950
+ isError: true,
951
+ isInterrupted: false,
952
+ error: this.error
953
+ });
954
+ }
955
+ if (runId && interrupts.length > 0) {
956
+ this.runId = runId;
957
+ return await this.resolveInterrupts({
958
+ runId,
959
+ interrupts,
960
+ hooks,
961
+ signal
962
+ });
963
+ }
964
+ this.status = "interrupted";
965
+ this.notify();
966
+ return this.createFinish({
967
+ isAbort: false,
968
+ isDisconnect: false,
969
+ isError: false,
970
+ isInterrupted: true,
971
+ interruptReason: "other"
972
+ });
973
+ }
974
+ this.applyEvent(event);
975
+ if (this.stopRequested) return this.createFinish({
976
+ isAbort: true,
977
+ isDisconnect: false,
978
+ isError: false,
979
+ isInterrupted: false
980
+ });
981
+ this.status = "streaming";
982
+ this.notify();
983
+ this.emitEvent(hooks, event);
984
+ }
985
+ createPendingClientTool(runId, interrupt) {
986
+ const toolCall = this.getToolCallPart(interrupt.toolCallId);
987
+ if (!toolCall) {
988
+ this.error = new BetterAgentClientError(`Client tool call not found for toolCallId: ${interrupt.toolCallId}`);
989
+ this.status = "error";
990
+ this.notify();
991
+ return this.createFinish({
992
+ isAbort: false,
993
+ isDisconnect: false,
994
+ isError: true,
995
+ isInterrupted: false,
996
+ error: this.error
997
+ });
998
+ }
999
+ return {
1000
+ interruptId: interrupt.id,
1001
+ runId,
1002
+ toolCallId: interrupt.toolCallId,
1003
+ toolName: toolCall.toolName,
1004
+ input: toolCall.input,
1005
+ ...interrupt.expiresAt !== void 0 ? { expiresAt: interrupt.expiresAt } : {}
1006
+ };
1007
+ }
1008
+ createPendingApproval(runId, interrupt) {
1009
+ const toolCall = this.getToolCallPart(interrupt.toolCallId);
1010
+ if (!toolCall) {
1011
+ this.error = new BetterAgentClientError(`Approval tool call not found for toolCallId: ${interrupt.toolCallId}`);
1012
+ this.status = "error";
1013
+ this.notify();
1014
+ return this.createFinish({
1015
+ isAbort: false,
1016
+ isDisconnect: false,
1017
+ isError: true,
1018
+ isInterrupted: false,
1019
+ error: this.error
1020
+ });
1021
+ }
1022
+ return {
1023
+ interruptId: interrupt.id,
1024
+ runId,
1025
+ toolCallId: interrupt.toolCallId,
1026
+ toolName: toolCall.toolName,
1027
+ input: toolCall.input,
1028
+ metadata: interrupt.metadata,
1029
+ ...interrupt.expiresAt !== void 0 ? { expiresAt: interrupt.expiresAt } : {}
1030
+ };
1031
+ }
1032
+ async createClientToolResumeEntry(pending) {
1033
+ const handler = this.getToolHandler(pending.toolName);
1034
+ if (!handler) return;
1035
+ try {
1036
+ const payload = await handler(pending.input);
1037
+ const resumePayload = payload === void 0 ? {
1038
+ status: "success",
1039
+ result: {}
1040
+ } : {
1041
+ status: "success",
1042
+ result: payload
1043
+ };
1044
+ this.upsertToolResultPart(pending.toolCallId, {
1045
+ state: "output-available",
1046
+ result: typeof resumePayload.result === "string" ? resumePayload.result : JSON.stringify(resumePayload.result)
1047
+ });
1048
+ this.notify();
1049
+ return {
1050
+ interruptId: pending.interruptId,
1051
+ status: "resolved",
1052
+ payload: resumePayload
1053
+ };
1054
+ } catch (error) {
1055
+ const message = error instanceof Error ? error.message : String(error);
1056
+ const resumePayload = {
1057
+ status: "error",
1058
+ error: message
1059
+ };
1060
+ this.upsertToolResultPart(pending.toolCallId, {
1061
+ state: "output-error",
1062
+ error: message
1063
+ });
1064
+ this.notify();
1065
+ return {
1066
+ interruptId: pending.interruptId,
1067
+ status: "resolved",
1068
+ payload: resumePayload
1069
+ };
1070
+ }
1071
+ }
1072
+ async resolveInterrupts(params) {
1073
+ const { hooks, runId, signal } = params;
1074
+ const approvalInterrupts = params.interrupts.filter((interrupt) => this.isApprovalInterrupt(interrupt));
1075
+ const clientToolInterrupts = params.interrupts.filter((interrupt) => this.isClientToolInterrupt(interrupt));
1076
+ if (approvalInterrupts.length > 0) {
1077
+ const approvals = [];
1078
+ for (const interrupt of approvalInterrupts) {
1079
+ const pending = this.createPendingApproval(runId, interrupt);
1080
+ if ("isError" in pending) return pending;
1081
+ approvals.push(pending);
1082
+ this.updateToolCallPart(interrupt.toolCallId, {
1083
+ state: "approval-requested",
1084
+ approval: {
1085
+ interruptId: interrupt.id,
1086
+ needsApproval: true,
1087
+ ...interrupt.metadata ? { metadata: interrupt.metadata } : {}
1088
+ }
1089
+ });
1090
+ }
1091
+ const clientTools = [];
1092
+ for (const interrupt of clientToolInterrupts) {
1093
+ const pending = this.createPendingClientTool(runId, interrupt);
1094
+ if ("isError" in pending) return pending;
1095
+ clientTools.push(pending);
1096
+ }
1097
+ this.pendingToolApprovals = approvals;
1098
+ this.pendingClientTools = clientTools;
1099
+ this.status = "interrupted";
1100
+ this.notify();
1101
+ return this.createFinish({
1102
+ isAbort: false,
1103
+ isDisconnect: false,
1104
+ isError: false,
1105
+ isInterrupted: true,
1106
+ interruptReason: "tool_approval_pending"
1107
+ });
1108
+ }
1109
+ if (clientToolInterrupts.length > 0) {
1110
+ const clientTools = [];
1111
+ for (const interrupt of clientToolInterrupts) {
1112
+ const pending = this.createPendingClientTool(runId, interrupt);
1113
+ if ("isError" in pending) return pending;
1114
+ clientTools.push(pending);
1115
+ }
1116
+ this.pendingClientTools = clientTools;
1117
+ this.pendingToolApprovals = [];
1118
+ return this.resolvePendingInterruptBatch(hooks, signal);
1119
+ }
1120
+ this.pendingClientTools = [];
1121
+ this.pendingToolApprovals = [];
1122
+ this.status = "interrupted";
1123
+ this.notify();
1124
+ return this.createFinish({
1125
+ isAbort: false,
1126
+ isDisconnect: false,
1127
+ isError: false,
1128
+ isInterrupted: true,
1129
+ interruptReason: "other"
1130
+ });
1131
+ }
1132
+ async streamWithResume(resume, hooks, signal) {
1133
+ const stream = this.agent.stream({
1134
+ resume,
1135
+ ...this.stateControl.get() !== void 0 ? { state: this.stateControl.get() } : {},
1136
+ ...this.threadId !== void 0 ? { threadId: this.threadId } : {}
1137
+ }, { signal });
1138
+ return this.consumeStream(stream, hooks, signal, () => {
1139
+ this.pendingClientTools = [];
1140
+ this.pendingToolApprovals = [];
1141
+ });
1142
+ }
1143
+ async runPendingInterruptsWithLifecycle() {
1144
+ const hooks = {
1145
+ onEvent: this.options.onEvent,
1146
+ onFinish: this.options.onFinish,
1147
+ onError: this.options.onError
1148
+ };
1149
+ const abortController = new AbortController();
1150
+ this.resetStopRequest();
1151
+ this.activeAbortController?.abort();
1152
+ this.activeAbortController = abortController;
1153
+ this.error = void 0;
1154
+ this.finishMessageStartIndex = this.messages.length;
1155
+ this.status = "streaming";
1156
+ this.notify();
1157
+ let terminal;
1158
+ try {
1159
+ terminal = await this.resolvePendingInterruptBatch(hooks, abortController.signal);
1160
+ } catch (e) {
1161
+ if (abortController.signal.aborted) terminal = this.createFinish({
1162
+ isAbort: true,
1163
+ isDisconnect: false,
1164
+ isError: false,
1165
+ isInterrupted: false
1166
+ });
1167
+ else {
1168
+ this.error = toBetterAgentClientError(e);
1169
+ this.status = "error";
1170
+ this.notify();
1171
+ terminal = this.createFinish({
1172
+ isAbort: false,
1173
+ isDisconnect: false,
1174
+ isError: true,
1175
+ isInterrupted: false,
1176
+ error: this.error
1177
+ });
1178
+ }
1179
+ } finally {
1180
+ if (this.activeAbortController === abortController) this.activeAbortController = void 0;
1181
+ if (terminal !== void 0) this.invokeLifecycleFinish(hooks, terminal);
1182
+ }
1183
+ }
1184
+ async resolvePendingInterruptBatch(hooks, signal) {
1185
+ if (this.hasUndecidedApprovals()) {
1186
+ this.status = "interrupted";
1187
+ this.notify();
1188
+ return this.createFinish({
1189
+ isAbort: false,
1190
+ isDisconnect: false,
1191
+ isError: false,
1192
+ isInterrupted: true,
1193
+ interruptReason: "tool_approval_pending"
1194
+ });
1195
+ }
1196
+ const runId = this.runId ?? this.pendingToolApprovals[0]?.runId ?? this.pendingClientTools[0]?.runId;
1197
+ if (!runId) {
1198
+ this.error = new BetterAgentClientError("Pending interrupt missing runId.");
1199
+ this.status = "error";
1200
+ this.notify();
1201
+ return this.createFinish({
1202
+ isAbort: false,
1203
+ isDisconnect: false,
1204
+ isError: true,
1205
+ isInterrupted: false,
1206
+ error: this.error
1207
+ });
1208
+ }
1209
+ const resume = this.pendingToolApprovals.map((approval) => ({
1210
+ interruptId: approval.interruptId,
1211
+ status: "resolved",
1212
+ payload: {
1213
+ approved: approval.approved,
1214
+ ...approval.responseMetadata ? { metadata: approval.responseMetadata } : {}
1215
+ }
1216
+ }));
1217
+ if (this.pendingClientTools.some((pending) => !this.getToolHandler(pending.toolName))) {
1218
+ this.status = "interrupted";
1219
+ this.notify();
1220
+ return this.createFinish({
1221
+ isAbort: false,
1222
+ isDisconnect: false,
1223
+ isError: false,
1224
+ isInterrupted: true,
1225
+ interruptReason: "client_tool_pending"
1226
+ });
1227
+ }
1228
+ for (const pending of this.pendingClientTools) {
1229
+ const entry = await this.createClientToolResumeEntry(pending);
1230
+ if (entry) resume.push(entry);
1231
+ }
1232
+ if (resume.length === 0) {
1233
+ this.status = "interrupted";
1234
+ this.notify();
1235
+ return this.createFinish({
1236
+ isAbort: false,
1237
+ isDisconnect: false,
1238
+ isError: false,
1239
+ isInterrupted: true,
1240
+ interruptReason: "other"
1241
+ });
1242
+ }
1243
+ this.runId = runId;
1244
+ return this.streamWithResume(resume, hooks, signal);
1245
+ }
1246
+ getToolHandler(toolName) {
1247
+ return this.options.toolHandlers?.[toolName];
1248
+ }
1249
+ updateToolCallPart(toolCallId, update) {
1250
+ this.messages = this.messages.map((message) => ({
1251
+ ...message,
1252
+ parts: message.parts.map((part) => part.type === "tool-call" && part.toolCallId === toolCallId ? {
1253
+ ...part,
1254
+ ...update
1255
+ } : part)
1256
+ }));
1257
+ }
1258
+ upsertToolResultPart(toolCallId, update) {
1259
+ this.messages = this.messages.map((message) => {
1260
+ const existingResultIndex = message.parts.findIndex((part) => part.type === "tool-result" && part.toolCallId === toolCallId);
1261
+ const existingToolCallIndex = message.parts.findIndex((part) => part.type === "tool-call" && part.toolCallId === toolCallId);
1262
+ if (existingResultIndex === -1 && existingToolCallIndex === -1) return message;
1263
+ if (existingResultIndex !== -1) return {
1264
+ ...message,
1265
+ parts: message.parts.map((part, index) => index === existingResultIndex && part.type === "tool-result" ? {
1266
+ ...part,
1267
+ ...update
1268
+ } : part)
1269
+ };
1270
+ const parts = [...message.parts];
1271
+ const insertIndex = existingToolCallIndex + 1;
1272
+ parts.splice(insertIndex, 0, {
1273
+ type: "tool-result",
1274
+ toolCallId,
1275
+ state: "output-available",
1276
+ ...update
1277
+ });
1278
+ return {
1279
+ ...message,
1280
+ parts
1281
+ };
1282
+ });
1283
+ }
1284
+ async resolveApprovalDecision(interruptId, approved, metadata) {
1285
+ const approvalIndex = this.pendingToolApprovals.findIndex((approval) => approval.interruptId === interruptId);
1286
+ if (approvalIndex === -1) throw new BetterAgentClientError(`Pending approval not found for interruptId: ${interruptId}`);
1287
+ const approval = this.pendingToolApprovals[approvalIndex];
1288
+ if (!approval) return;
1289
+ const normalizedMetadata = typeof metadata === "string" ? { note: metadata } : metadata;
1290
+ this.error = void 0;
1291
+ this.pendingToolApprovals = this.pendingToolApprovals.map((pending, index) => index === approvalIndex ? {
1292
+ ...pending,
1293
+ approved,
1294
+ responseMetadata: normalizedMetadata
1295
+ } : pending);
1296
+ this.updateToolCallPart(approval.toolCallId, {
1297
+ state: "approval-responded",
1298
+ approval: {
1299
+ interruptId: approval.interruptId,
1300
+ needsApproval: true,
1301
+ approved,
1302
+ ...normalizedMetadata ? { metadata: normalizedMetadata } : {}
1303
+ }
1304
+ });
1305
+ this.notify();
1306
+ if (this.hasUndecidedApprovals()) {
1307
+ this.status = "interrupted";
1308
+ this.notify();
1309
+ return;
1310
+ }
1311
+ await this.runPendingInterruptsWithLifecycle();
1312
+ }
1313
+ applyEvent(event) {
1314
+ const rid = "runId" in event && typeof event.runId === "string" ? event.runId : void 0;
1315
+ const tid = "threadId" in event && typeof event.threadId === "string" && event.threadId.length > 0 ? event.threadId : void 0;
1316
+ if (rid) {
1317
+ this.runId = rid;
1318
+ if (this.stopRequested) {
1319
+ this.clearStopWaitTimer();
1320
+ this.abortServerRun(rid);
1321
+ this.abortActiveRequest();
1322
+ return;
1323
+ }
1324
+ }
1325
+ if (tid) this.threadId = tid;
1326
+ if (this.stopRequested) return;
1327
+ this.stateControl.apply(event);
1328
+ if (event.type === EventType.STATE_SNAPSHOT || event.type === EventType.STATE_DELTA) this.stateRevision += 1;
1329
+ this.messages = applyUIEvent(createUIReducerState(this.messages), event).messages;
1330
+ }
1331
+ notify() {
1332
+ this.snapshot = this.createSnapshot();
1333
+ for (const listener of this.listeners) listener();
1334
+ }
1335
+ };
1336
+ function createAgentController(agent, options) {
1337
+ return new AgentController(agent, options);
1338
+ }
1339
+
1340
+ //#endregion
1341
+ export { BetterAgentClientError as a, toAgentMessages as i, createAgentController as n, toBetterAgentClientError as o, fromAgentMessages as r, AgentController as t };
1342
+ //# sourceMappingURL=controller-xMlzSCc7.mjs.map