@agents24/chat-react 0.1.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -5
- package/dist/{chunk-E3TJ3I6G.js → chunk-FFO6KEB4.js} +2 -1
- package/dist/chunk-FFO6KEB4.js.map +1 -0
- package/dist/controller-actions.d.ts +45 -0
- package/dist/controller-helpers.d.ts +27 -0
- package/dist/controller.d.ts +2 -7
- package/dist/index.cjs +600 -225
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +543 -175
- package/dist/index.js.map +1 -1
- package/dist/message-lifecycle.d.ts +15 -0
- package/dist/model.d.ts +2 -1
- package/dist/renderers.d.ts +1 -1
- package/dist/sse.d.ts +3 -1
- package/dist/templates/agent-chat-shell.d.ts +13 -1
- package/dist/templates/index.cjs +91 -24
- package/dist/templates/index.cjs.map +1 -1
- package/dist/templates/index.js +89 -24
- package/dist/templates/index.js.map +1 -1
- package/dist/transport.d.ts +14 -0
- package/dist/types.d.ts +63 -6
- package/dist/ui/adapters.d.ts +1 -2
- package/dist/ui/agent-chat-actions.d.ts +16 -0
- package/dist/ui/agent-chat-composer.d.ts +28 -0
- package/dist/ui/agent-chat-message.d.ts +40 -0
- package/dist/ui/agent-response-timeline.d.ts +24 -0
- package/dist/ui/index.cjs +944 -206
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.ts +5 -0
- package/dist/ui/index.js +932 -202
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/mcp-connect-required-card.d.ts +13 -0
- package/package.json +2 -1
- package/dist/chunk-E3TJ3I6G.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
LatestThreadScrollerProvider,
|
|
8
8
|
LatestThreadScrollerRoot,
|
|
9
9
|
LatestThreadScrollerViewport
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-FFO6KEB4.js";
|
|
11
11
|
import {
|
|
12
12
|
getActiveStreamingTextPartId,
|
|
13
13
|
isActiveStreamingTextPart,
|
|
@@ -15,7 +15,79 @@ import {
|
|
|
15
15
|
} from "./chunk-EWZO4QJI.js";
|
|
16
16
|
|
|
17
17
|
// src/controller.ts
|
|
18
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
18
|
+
import { useCallback as useCallback2, useEffect, useMemo, useRef, useState } from "react";
|
|
19
|
+
|
|
20
|
+
// src/controller-actions.ts
|
|
21
|
+
import { useCallback } from "react";
|
|
22
|
+
function useControllerMessageActions(input) {
|
|
23
|
+
const handleCopy = useCallback((content, messageId) => {
|
|
24
|
+
navigator.clipboard?.writeText(content);
|
|
25
|
+
input.setCopiedMessageId(messageId);
|
|
26
|
+
setTimeout(() => input.setCopiedMessageId(null), 200);
|
|
27
|
+
}, [input]);
|
|
28
|
+
const handleLike = useCallback(async (msg) => {
|
|
29
|
+
const nextLiked = !input.liked[msg.id];
|
|
30
|
+
input.setLiked((prev) => ({ ...prev, [msg.id]: nextLiked }));
|
|
31
|
+
if (nextLiked) input.setDisliked((prev) => ({ ...prev, [msg.id]: false }));
|
|
32
|
+
}, [input]);
|
|
33
|
+
const handleDislike = useCallback(async (msg) => {
|
|
34
|
+
const nextDisliked = !input.disliked[msg.id];
|
|
35
|
+
input.setDisliked((prev) => ({ ...prev, [msg.id]: nextDisliked }));
|
|
36
|
+
if (nextDisliked) input.setLiked((prev) => ({ ...prev, [msg.id]: false }));
|
|
37
|
+
}, [input]);
|
|
38
|
+
const handleRetry = useCallback(async (msg) => {
|
|
39
|
+
const index = input.messagesRef.current.findIndex((message) => message.id === msg.id);
|
|
40
|
+
if (index <= 0) return;
|
|
41
|
+
const userMessage = input.messagesRef.current[index - 1];
|
|
42
|
+
if (userMessage.role !== "user") return;
|
|
43
|
+
const trimmed = input.messagesRef.current.slice(0, index);
|
|
44
|
+
input.setMessages(trimmed);
|
|
45
|
+
input.messagesRef.current = trimmed;
|
|
46
|
+
if (input.activeThreadIdRef.current) input.persistThread(input.activeThreadIdRef.current, trimmed);
|
|
47
|
+
await input.handleSubmit({ text: userMessage.content, files: userMessage.attachments || [] });
|
|
48
|
+
}, [input]);
|
|
49
|
+
const upsertLiveVoiceMessage = useCallback((payload) => {
|
|
50
|
+
const content = payload.content?.trim() ?? "";
|
|
51
|
+
if (!content && !payload.citations?.length && !payload.reasoningSteps?.length) return;
|
|
52
|
+
input.setMessages((prev) => {
|
|
53
|
+
const currentId = input.liveVoiceIdsRef.current[payload.role];
|
|
54
|
+
const index = currentId ? prev.findIndex((message) => message.id === currentId) : -1;
|
|
55
|
+
const nextMessage = {
|
|
56
|
+
id: currentId || input.createId(),
|
|
57
|
+
role: payload.role,
|
|
58
|
+
content,
|
|
59
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
60
|
+
isFinal: Boolean(payload.isFinal),
|
|
61
|
+
isVoice: payload.role === "user",
|
|
62
|
+
parts: content ? [{ id: input.createId(), type: "text", kind: "text", text: content }] : [],
|
|
63
|
+
citations: payload.citations,
|
|
64
|
+
reasoningSteps: payload.reasoningSteps
|
|
65
|
+
};
|
|
66
|
+
const next = index === -1 ? [...prev, nextMessage] : prev.map((message, itemIndex) => itemIndex === index ? { ...message, ...nextMessage } : message);
|
|
67
|
+
input.liveVoiceIdsRef.current[payload.role] = payload.isFinal ? void 0 : nextMessage.id;
|
|
68
|
+
input.messagesRef.current = next;
|
|
69
|
+
if (input.activeThreadIdRef.current) input.persistThread(input.activeThreadIdRef.current, next);
|
|
70
|
+
return next;
|
|
71
|
+
});
|
|
72
|
+
}, [input]);
|
|
73
|
+
const startNewThread = useCallback(() => {
|
|
74
|
+
input.detachActiveStream();
|
|
75
|
+
input.requestSeqRef.current += 1;
|
|
76
|
+
input.activeThreadIdRef.current = null;
|
|
77
|
+
input.loadedThreadIdRef.current = null;
|
|
78
|
+
input.nextBeforeTurnIndexRef.current = null;
|
|
79
|
+
input.hasOlderTurnsRef.current = false;
|
|
80
|
+
input.storage.setActiveThreadId?.(null);
|
|
81
|
+
input.onActiveThreadIdChange?.(null);
|
|
82
|
+
input.setMessages([]);
|
|
83
|
+
input.messagesRef.current = [];
|
|
84
|
+
input.setHasOlderTurns(false);
|
|
85
|
+
input.setIsLoadingOlder(false);
|
|
86
|
+
input.setContextStatus(null);
|
|
87
|
+
input.setLoadingHistory(false);
|
|
88
|
+
}, [input]);
|
|
89
|
+
return { handleCopy, handleDislike, handleLike, handleRetry, startNewThread, upsertLiveVoiceMessage };
|
|
90
|
+
}
|
|
19
91
|
|
|
20
92
|
// src/context-window.ts
|
|
21
93
|
var SOURCE_PRIORITY = {
|
|
@@ -138,7 +210,8 @@ function compressionFromContextWindow(contextWindow) {
|
|
|
138
210
|
|
|
139
211
|
// src/model.ts
|
|
140
212
|
var DEFAULT_THREAD_PAGE_SIZE = 5;
|
|
141
|
-
var isRunningThreadStatus = (status) => (/* @__PURE__ */ new Set(["queued", "running", "cancelling"])).has(String(status || "").toLowerCase());
|
|
213
|
+
var isRunningThreadStatus = (status) => (/* @__PURE__ */ new Set(["queued", "running", "cancelling", "paused"])).has(String(status || "").toLowerCase());
|
|
214
|
+
var isAutoAttachThreadStatus = (status) => (/* @__PURE__ */ new Set(["queued", "running", "cancelling"])).has(String(status || "").toLowerCase());
|
|
142
215
|
var createChatId = () => {
|
|
143
216
|
if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
|
|
144
217
|
return `chat-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
@@ -150,12 +223,6 @@ var titleFromMessage = (text, files = []) => {
|
|
|
150
223
|
var threadActivityDate = (thread) => String(thread.updated_at || thread.last_activity_at || thread.created_at || (/* @__PURE__ */ new Date()).toISOString());
|
|
151
224
|
var assistantTextFromResponseBlocks = (blocks) => (blocks || []).filter((block) => block.kind === "assistant_text" && typeof block.text === "string").map((block) => String(block.text)).join("\n\n").trim();
|
|
152
225
|
var assistantTextFromParts = (parts) => (parts || []).filter((part) => part.kind === "text").map((part) => part.text).join("\n\n").trim();
|
|
153
|
-
var textFromFinalOutput = (value) => {
|
|
154
|
-
if (typeof value === "string") return value;
|
|
155
|
-
if (!value || typeof value !== "object") return "";
|
|
156
|
-
const record = value;
|
|
157
|
-
return String(record.message || record.text || record.answer || "");
|
|
158
|
-
};
|
|
159
226
|
var displayTextWithoutInlineAttachments = (text, hasAttachments) => {
|
|
160
227
|
if (!hasAttachments) return text;
|
|
161
228
|
const marker = "Attached text file (";
|
|
@@ -202,7 +269,7 @@ var responseBlocksFromTurn = (turn) => {
|
|
|
202
269
|
var assistantTextFromEvents = (events) => {
|
|
203
270
|
const assistantText = latestEventPayloadValue(events, "assistant_output_text");
|
|
204
271
|
if (typeof assistantText === "string" && assistantText.trim()) return assistantText;
|
|
205
|
-
return
|
|
272
|
+
return "";
|
|
206
273
|
};
|
|
207
274
|
var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
208
275
|
var optionalString = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
@@ -304,8 +371,17 @@ var partsFromResponseBlocks = (blocks, fallbackText) => {
|
|
|
304
371
|
});
|
|
305
372
|
return;
|
|
306
373
|
}
|
|
307
|
-
if (block.kind === "
|
|
308
|
-
|
|
374
|
+
if (block.kind === "hitl_request") {
|
|
375
|
+
const hitl = asRecord(block.hitl) || block;
|
|
376
|
+
parts.push({
|
|
377
|
+
id,
|
|
378
|
+
type: "hitl",
|
|
379
|
+
kind: "hitl",
|
|
380
|
+
hitl,
|
|
381
|
+
interruptId: optionalString(block.interruptId) || optionalString(hitl.interrupt_id) || null,
|
|
382
|
+
hitlKind: optionalString(block.hitlKind) || optionalString(hitl.kind) || null,
|
|
383
|
+
raw: block
|
|
384
|
+
});
|
|
309
385
|
return;
|
|
310
386
|
}
|
|
311
387
|
if (block.kind === "error") {
|
|
@@ -377,7 +453,7 @@ var turnToMessages = (turn, activeRunId) => {
|
|
|
377
453
|
attachments
|
|
378
454
|
});
|
|
379
455
|
}
|
|
380
|
-
const assistantText = turn.assistant_output_text || assistantTextFromResponseBlocks(responseBlocks) || assistantTextFromEvents(turn.run_events)
|
|
456
|
+
const assistantText = turn.assistant_output_text || assistantTextFromResponseBlocks(responseBlocks) || assistantTextFromEvents(turn.run_events);
|
|
381
457
|
if (assistantText || responseBlocks.length > 0 || isRunning) {
|
|
382
458
|
const parts = partsFromResponseBlocks(responseBlocks, assistantText);
|
|
383
459
|
messages.push({
|
|
@@ -431,6 +507,23 @@ var activeRunIdFromThread = (thread) => {
|
|
|
431
507
|
const lastRunId = hasCamelLastRunId ? thread?.lastRunId : thread?.last_run_id || thread?.lastRunId;
|
|
432
508
|
return lastRunId && isRunningThreadStatus(lastRunStatus) ? String(lastRunId) : null;
|
|
433
509
|
};
|
|
510
|
+
var autoAttachRunIdFromThread = (thread) => {
|
|
511
|
+
const hasCamelActiveRun = Boolean(
|
|
512
|
+
thread && Object.prototype.hasOwnProperty.call(thread, "activeRun")
|
|
513
|
+
);
|
|
514
|
+
const hasCamelLastRunStatus = Boolean(
|
|
515
|
+
thread && Object.prototype.hasOwnProperty.call(thread, "lastRunStatus")
|
|
516
|
+
);
|
|
517
|
+
const hasCamelLastRunId = Boolean(
|
|
518
|
+
thread && Object.prototype.hasOwnProperty.call(thread, "lastRunId")
|
|
519
|
+
);
|
|
520
|
+
const activeRun = hasCamelActiveRun ? thread?.activeRun || null : thread?.active_run || null;
|
|
521
|
+
const lastRunStatus = hasCamelLastRunStatus ? thread?.lastRunStatus || null : thread?.last_run_status || thread?.lastRunStatus || null;
|
|
522
|
+
const activeRunId = activeRun?.run_id ? String(activeRun.run_id) : "";
|
|
523
|
+
if (activeRunId && isAutoAttachThreadStatus(activeRun?.status || lastRunStatus)) return activeRunId;
|
|
524
|
+
const lastRunId = hasCamelLastRunId ? thread?.lastRunId : thread?.last_run_id || thread?.lastRunId;
|
|
525
|
+
return lastRunId && isAutoAttachThreadStatus(lastRunStatus) ? String(lastRunId) : null;
|
|
526
|
+
};
|
|
434
527
|
var hasUnfinishedAssistantMessage = (messages) => Boolean(messages?.some((message) => message.role === "assistant" && message.isFinal === false));
|
|
435
528
|
var hasStaleUnfinishedAssistantCache = (thread) => hasUnfinishedAssistantMessage(thread?.messages) && !activeRunIdFromThread(thread);
|
|
436
529
|
var activeRunIdFromThreadDetail = (thread) => {
|
|
@@ -440,7 +533,7 @@ var activeRunIdFromThreadDetail = (thread) => {
|
|
|
440
533
|
return runningTurn?.run_id ? String(runningTurn.run_id) : null;
|
|
441
534
|
};
|
|
442
535
|
|
|
443
|
-
// src/controller.ts
|
|
536
|
+
// src/controller-helpers.ts
|
|
444
537
|
var threadSummaryToStored = (thread) => ({
|
|
445
538
|
...thread,
|
|
446
539
|
id: String(thread.id),
|
|
@@ -449,6 +542,121 @@ var threadSummaryToStored = (thread) => ({
|
|
|
449
542
|
messages: [],
|
|
450
543
|
isHydrated: false
|
|
451
544
|
});
|
|
545
|
+
var isThreadNotFoundError = (error) => {
|
|
546
|
+
if (!error || typeof error !== "object" || !("status" in error)) return false;
|
|
547
|
+
return Number(error.status) === 404;
|
|
548
|
+
};
|
|
549
|
+
var isAbortError = (error) => {
|
|
550
|
+
if (!error || typeof error !== "object") return false;
|
|
551
|
+
const maybe = error;
|
|
552
|
+
return maybe.name === "AbortError" || String(maybe.message || "").toLowerCase().includes("aborted");
|
|
553
|
+
};
|
|
554
|
+
var abortDetachedStream = (controller) => {
|
|
555
|
+
if (!controller || controller.signal.aborted) return;
|
|
556
|
+
try {
|
|
557
|
+
const reason = typeof DOMException !== "undefined" ? new DOMException("Chat stream detached.", "AbortError") : new Error("Chat stream detached.");
|
|
558
|
+
controller.abort(reason);
|
|
559
|
+
} catch {
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
var hasRunState = (thread) => Boolean(
|
|
563
|
+
thread.active_run || thread.activeRun || thread.last_run_id || thread.lastRunId || thread.last_run_status || thread.lastRunStatus || thread.isRunning
|
|
564
|
+
);
|
|
565
|
+
var mergeStoredThreadsForRefresh = (currentThreads, serverThreads) => {
|
|
566
|
+
const currentById = new Map(currentThreads.map((thread) => [thread.id, thread]));
|
|
567
|
+
const serverIds = new Set(serverThreads.map((thread) => thread.id));
|
|
568
|
+
const mergedServerThreads = serverThreads.map((serverThread) => {
|
|
569
|
+
const current = currentById.get(serverThread.id);
|
|
570
|
+
if (!current) return serverThread;
|
|
571
|
+
const incomingHasRunState = hasRunState(serverThread);
|
|
572
|
+
return {
|
|
573
|
+
...current,
|
|
574
|
+
...serverThread,
|
|
575
|
+
messages: current.messages || [],
|
|
576
|
+
isHydrated: current.isHydrated,
|
|
577
|
+
hasOlderTurns: current.hasOlderTurns,
|
|
578
|
+
nextBeforeTurnIndex: current.nextBeforeTurnIndex,
|
|
579
|
+
active_run: incomingHasRunState ? serverThread.active_run : current.active_run,
|
|
580
|
+
activeRun: incomingHasRunState ? serverThread.activeRun : current.activeRun,
|
|
581
|
+
last_run_id: incomingHasRunState ? serverThread.last_run_id : current.last_run_id,
|
|
582
|
+
lastRunId: incomingHasRunState ? serverThread.lastRunId : current.lastRunId,
|
|
583
|
+
last_run_status: incomingHasRunState ? serverThread.last_run_status : current.last_run_status,
|
|
584
|
+
lastRunStatus: incomingHasRunState ? serverThread.lastRunStatus : current.lastRunStatus,
|
|
585
|
+
lastEventSeq: incomingHasRunState ? serverThread.lastEventSeq : current.lastEventSeq,
|
|
586
|
+
isRunning: incomingHasRunState ? serverThread.isRunning : current.isRunning
|
|
587
|
+
};
|
|
588
|
+
});
|
|
589
|
+
const localOnlyThreads = currentThreads.filter((thread) => {
|
|
590
|
+
if (serverIds.has(thread.id)) return false;
|
|
591
|
+
return Boolean(thread.isHydrated || thread.messages?.length || thread.isRunning || thread.activeRun || thread.active_run);
|
|
592
|
+
});
|
|
593
|
+
return [...mergedServerThreads, ...localOnlyThreads];
|
|
594
|
+
};
|
|
595
|
+
var stableStringify = (value) => JSON.stringify(value ?? null);
|
|
596
|
+
var sameStoredThread = (left, right) => {
|
|
597
|
+
if (left === right) return true;
|
|
598
|
+
if (!left || !right) return false;
|
|
599
|
+
return stableStringify(left) === stableStringify(right);
|
|
600
|
+
};
|
|
601
|
+
var applyThreadSummaryEvent = (currentThreads, event) => {
|
|
602
|
+
if (event.event === "snapshot_required") return currentThreads;
|
|
603
|
+
if (event.event === "thread.deleted") {
|
|
604
|
+
const next2 = currentThreads.filter((thread) => thread.id !== event.thread_id);
|
|
605
|
+
return next2.length === currentThreads.length ? currentThreads : next2;
|
|
606
|
+
}
|
|
607
|
+
const incoming = threadSummaryToStored(event.thread);
|
|
608
|
+
const index = currentThreads.findIndex((thread) => thread.id === incoming.id);
|
|
609
|
+
const sortByActivity = (threads) => [...threads].sort((a, b) => {
|
|
610
|
+
const left = Date.parse(String(a.updated_at || a.last_activity_at || a.created_at || ""));
|
|
611
|
+
const right = Date.parse(String(b.updated_at || b.last_activity_at || b.created_at || ""));
|
|
612
|
+
return (Number.isFinite(right) ? right : 0) - (Number.isFinite(left) ? left : 0);
|
|
613
|
+
});
|
|
614
|
+
if (index === -1) return sortByActivity([...currentThreads, incoming]);
|
|
615
|
+
const [merged] = mergeStoredThreadsForRefresh([currentThreads[index]], [incoming]);
|
|
616
|
+
if (sameStoredThread(currentThreads[index], merged)) return currentThreads;
|
|
617
|
+
const next = [...currentThreads];
|
|
618
|
+
next[index] = merged;
|
|
619
|
+
return sortByActivity(next);
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
// src/message-lifecycle.ts
|
|
623
|
+
function findStableAssistantMessageIndex(messages, input) {
|
|
624
|
+
return messages.findIndex(
|
|
625
|
+
(message) => message.role === "assistant" && (input.messageId && message.id === input.messageId || Boolean(input.runId && message.runId === input.runId))
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
function appendStableStreamingTurn(messages, userMessage, assistantMessage) {
|
|
629
|
+
const next = [...messages];
|
|
630
|
+
if (!next.some((message) => message.id === userMessage.id)) {
|
|
631
|
+
next.push(userMessage);
|
|
632
|
+
}
|
|
633
|
+
if (!next.some(
|
|
634
|
+
(message) => message.id === assistantMessage.id || Boolean(
|
|
635
|
+
assistantMessage.runId && message.role === "assistant" && message.runId === assistantMessage.runId
|
|
636
|
+
)
|
|
637
|
+
)) {
|
|
638
|
+
next.push(assistantMessage);
|
|
639
|
+
}
|
|
640
|
+
return next;
|
|
641
|
+
}
|
|
642
|
+
function upsertStableAssistantMessage(messages, input) {
|
|
643
|
+
const next = [...messages];
|
|
644
|
+
for (const baseMessage of input.baseMessages || []) {
|
|
645
|
+
const matchesTarget = baseMessage.role === "assistant" && (input.messageId && baseMessage.id === input.messageId || Boolean(input.runId && baseMessage.runId === input.runId));
|
|
646
|
+
if (!matchesTarget && !next.some((message) => message.id === baseMessage.id)) {
|
|
647
|
+
next.push(baseMessage);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
const index = findStableAssistantMessageIndex(next, input);
|
|
651
|
+
if (index === -1) {
|
|
652
|
+
next.push(input.create());
|
|
653
|
+
return next;
|
|
654
|
+
}
|
|
655
|
+
next[index] = input.update(next[index]);
|
|
656
|
+
return next;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/controller.ts
|
|
452
660
|
function useAgents24ChatController({
|
|
453
661
|
transport,
|
|
454
662
|
storage,
|
|
@@ -479,6 +687,7 @@ function useAgents24ChatController({
|
|
|
479
687
|
const [lastThinkingDurationMs, setLastThinkingDurationMs] = useState(null);
|
|
480
688
|
const [threads, setThreads] = useState(() => storage.listThreads());
|
|
481
689
|
const [isRefreshingThreads, setIsRefreshingThreads] = useState(false);
|
|
690
|
+
const [isSelectingThread, setIsSelectingThread] = useState(false);
|
|
482
691
|
const [activeRunId, setActiveRunId] = useState(null);
|
|
483
692
|
const textareaRef = useRef(null);
|
|
484
693
|
const activeThreadIdRef = useRef(activeThreadId);
|
|
@@ -492,18 +701,21 @@ function useAgents24ChatController({
|
|
|
492
701
|
const activeRunIdRef = useRef(null);
|
|
493
702
|
const reattachedRunIdRef = useRef(null);
|
|
494
703
|
const abortControllerRef = useRef(null);
|
|
704
|
+
const [lifecycleAbortController] = useState(() => new AbortController());
|
|
495
705
|
const streamingContentRef = useRef("");
|
|
496
706
|
const streamingMessageIdRef = useRef(null);
|
|
497
707
|
const reasoningRef = useRef([]);
|
|
498
708
|
const liveVoiceIdsRef = useRef({});
|
|
499
|
-
const
|
|
709
|
+
const refreshSeqRef = useRef(0);
|
|
710
|
+
const threadEventsCursorRef = useRef(null);
|
|
711
|
+
const setActiveRunIdValue = useCallback2((runId) => {
|
|
500
712
|
activeRunIdRef.current = runId;
|
|
501
713
|
setActiveRunId(runId);
|
|
502
714
|
}, []);
|
|
503
|
-
const syncThreadsFromStorage =
|
|
715
|
+
const syncThreadsFromStorage = useCallback2(() => {
|
|
504
716
|
setThreads(storage.listThreads());
|
|
505
717
|
}, [storage]);
|
|
506
|
-
const upsertStoredThread =
|
|
718
|
+
const upsertStoredThread = useCallback2(
|
|
507
719
|
(thread) => {
|
|
508
720
|
storage.upsertThread(thread);
|
|
509
721
|
syncThreadsFromStorage();
|
|
@@ -513,7 +725,7 @@ function useAgents24ChatController({
|
|
|
513
725
|
useEffect(() => {
|
|
514
726
|
messagesRef.current = messages;
|
|
515
727
|
}, [messages]);
|
|
516
|
-
const persistThread =
|
|
728
|
+
const persistThread = useCallback2(
|
|
517
729
|
(threadId, nextMessages, paging, options) => {
|
|
518
730
|
const existing = storage.getThread(threadId);
|
|
519
731
|
const firstUser = nextMessages.find((message) => message.role === "user");
|
|
@@ -530,7 +742,7 @@ function useAgents24ChatController({
|
|
|
530
742
|
},
|
|
531
743
|
[storage, upsertStoredThread]
|
|
532
744
|
);
|
|
533
|
-
const markThreadRunStatus =
|
|
745
|
+
const markThreadRunStatus = useCallback2(
|
|
534
746
|
(threadId, runId, status, lastEventSeq) => {
|
|
535
747
|
const existing = storage.getThread(threadId);
|
|
536
748
|
if (!existing || !runId) return;
|
|
@@ -560,18 +772,21 @@ function useAgents24ChatController({
|
|
|
560
772
|
},
|
|
561
773
|
[storage, upsertStoredThread]
|
|
562
774
|
);
|
|
563
|
-
const refresh =
|
|
775
|
+
const refresh = useCallback2(async () => {
|
|
776
|
+
const seq = ++refreshSeqRef.current;
|
|
564
777
|
setIsRefreshingThreads(true);
|
|
565
778
|
try {
|
|
566
|
-
const data = await transport.listThreads();
|
|
779
|
+
const data = await transport.listThreads({ signal: lifecycleAbortController.signal });
|
|
780
|
+
if (lifecycleAbortController.signal.aborted) return;
|
|
781
|
+
if (seq !== refreshSeqRef.current) return;
|
|
567
782
|
const nextThreads = (data.items || []).map((item) => threadSummaryToStored(item));
|
|
568
|
-
storage.setThreads(nextThreads);
|
|
783
|
+
storage.setThreads(mergeStoredThreadsForRefresh(storage.listThreads(), nextThreads));
|
|
569
784
|
setThreads(storage.listThreads());
|
|
570
785
|
} finally {
|
|
571
|
-
setIsRefreshingThreads(false);
|
|
786
|
+
if (!lifecycleAbortController.signal.aborted && seq === refreshSeqRef.current) setIsRefreshingThreads(false);
|
|
572
787
|
}
|
|
573
|
-
}, [storage, transport]);
|
|
574
|
-
const applyThreadId =
|
|
788
|
+
}, [lifecycleAbortController, storage, transport]);
|
|
789
|
+
const applyThreadId = useCallback2(
|
|
575
790
|
(threadId, baseMessages) => {
|
|
576
791
|
if (!threadId || activeThreadIdRef.current === threadId) return;
|
|
577
792
|
activeThreadIdRef.current = threadId;
|
|
@@ -585,18 +800,18 @@ function useAgents24ChatController({
|
|
|
585
800
|
streamingContentRef.current = value;
|
|
586
801
|
setStreamingContent(value);
|
|
587
802
|
};
|
|
588
|
-
const setLoadingHistory =
|
|
803
|
+
const setLoadingHistory = useCallback2((value) => {
|
|
589
804
|
isLoadingHistoryRef.current = value;
|
|
590
805
|
setIsLoadingHistory(value);
|
|
591
806
|
}, []);
|
|
592
|
-
const setReasoningSteps =
|
|
807
|
+
const setReasoningSteps = useCallback2((value) => {
|
|
593
808
|
reasoningRef.current = value || [];
|
|
594
809
|
setCurrentReasoning(value || []);
|
|
595
810
|
}, []);
|
|
596
|
-
const detachActiveStream =
|
|
811
|
+
const detachActiveStream = useCallback2(() => {
|
|
597
812
|
const controller = abortControllerRef.current;
|
|
598
813
|
abortControllerRef.current = null;
|
|
599
|
-
controller
|
|
814
|
+
abortDetachedStream(controller);
|
|
600
815
|
setActiveRunIdValue(null);
|
|
601
816
|
reattachedRunIdRef.current = null;
|
|
602
817
|
streamingMessageIdRef.current = null;
|
|
@@ -607,21 +822,36 @@ function useAgents24ChatController({
|
|
|
607
822
|
setStreamingContent("");
|
|
608
823
|
setCurrentReasoning([]);
|
|
609
824
|
}, [setActiveRunIdValue]);
|
|
610
|
-
const
|
|
825
|
+
const clearMissingThread = useCallback2(
|
|
826
|
+
(threadId) => {
|
|
827
|
+
storage.deleteThread?.(threadId);
|
|
828
|
+
syncThreadsFromStorage();
|
|
829
|
+
if (activeThreadIdRef.current !== threadId) return;
|
|
830
|
+
requestSeqRef.current += 1;
|
|
831
|
+
detachActiveStream();
|
|
832
|
+
activeThreadIdRef.current = null;
|
|
833
|
+
loadedThreadIdRef.current = null;
|
|
834
|
+
nextBeforeTurnIndexRef.current = null;
|
|
835
|
+
hasOlderTurnsRef.current = false;
|
|
836
|
+
storage.setActiveThreadId?.(null);
|
|
837
|
+
onActiveThreadIdChange?.(null);
|
|
838
|
+
setMessages([]);
|
|
839
|
+
messagesRef.current = [];
|
|
840
|
+
setHasOlderTurns(false);
|
|
841
|
+
setIsLoadingOlder(false);
|
|
842
|
+
setContextStatus(null);
|
|
843
|
+
setLoadingHistory(false);
|
|
844
|
+
},
|
|
845
|
+
[detachActiveStream, onActiveThreadIdChange, setLoadingHistory, storage, syncThreadsFromStorage]
|
|
846
|
+
);
|
|
847
|
+
const setLiveAssistantMessage = useCallback2(
|
|
611
848
|
(input) => {
|
|
612
849
|
setMessages((prev) => {
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
(input.baseMessages || []).forEach((message) => {
|
|
619
|
-
const isSameAssistant = message.id === input.messageId || Boolean(input.runId && message.role === "assistant" && message.runId === input.runId);
|
|
620
|
-
if (!isSameAssistant && !next2.some((item) => item.id === message.id)) {
|
|
621
|
-
next2.push(message);
|
|
622
|
-
}
|
|
623
|
-
});
|
|
624
|
-
next2.push({
|
|
850
|
+
const next = upsertStableAssistantMessage(prev, {
|
|
851
|
+
baseMessages: input.baseMessages,
|
|
852
|
+
messageId: input.messageId,
|
|
853
|
+
runId: input.runId,
|
|
854
|
+
create: () => ({
|
|
625
855
|
id: input.messageId,
|
|
626
856
|
role: "assistant",
|
|
627
857
|
runId: input.runId ?? null,
|
|
@@ -630,26 +860,23 @@ function useAgents24ChatController({
|
|
|
630
860
|
reasoningSteps: input.reasoning,
|
|
631
861
|
isFinal: false,
|
|
632
862
|
parts: input.parts || []
|
|
633
|
-
})
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
isFinal: false,
|
|
644
|
-
parts: input.parts ?? next[index].parts
|
|
645
|
-
};
|
|
863
|
+
}),
|
|
864
|
+
update: (message) => ({
|
|
865
|
+
...message,
|
|
866
|
+
runId: input.runId ?? message.runId ?? null,
|
|
867
|
+
content: input.content,
|
|
868
|
+
reasoningSteps: input.reasoning,
|
|
869
|
+
isFinal: false,
|
|
870
|
+
parts: input.parts ?? message.parts
|
|
871
|
+
})
|
|
872
|
+
});
|
|
646
873
|
messagesRef.current = next;
|
|
647
874
|
return next;
|
|
648
875
|
});
|
|
649
876
|
},
|
|
650
877
|
[]
|
|
651
878
|
);
|
|
652
|
-
const finalizeAssistantMessage =
|
|
879
|
+
const finalizeAssistantMessage = useCallback2(
|
|
653
880
|
(input) => {
|
|
654
881
|
const content = input.error || input.assistantText.trim();
|
|
655
882
|
if (!content) return input.baseMessages;
|
|
@@ -669,7 +896,17 @@ function useAgents24ChatController({
|
|
|
669
896
|
reasoningSteps: mergeReasoningSteps(input.reasoning, { finalize: true }),
|
|
670
897
|
thinkingDurationMs: input.thinkingDurationMs
|
|
671
898
|
};
|
|
672
|
-
const completed =
|
|
899
|
+
const completed = upsertStableAssistantMessage(input.baseMessages, {
|
|
900
|
+
messageId: input.messageId,
|
|
901
|
+
runId: input.runId,
|
|
902
|
+
create: () => assistant,
|
|
903
|
+
update: (message) => ({
|
|
904
|
+
...message,
|
|
905
|
+
...assistant,
|
|
906
|
+
id: message.id,
|
|
907
|
+
createdAt: message.createdAt
|
|
908
|
+
})
|
|
909
|
+
});
|
|
673
910
|
setMessages(completed);
|
|
674
911
|
messagesRef.current = completed;
|
|
675
912
|
if (input.threadId) {
|
|
@@ -693,7 +930,7 @@ function useAgents24ChatController({
|
|
|
693
930
|
},
|
|
694
931
|
[createId, persistThread, storage, upsertStoredThread]
|
|
695
932
|
);
|
|
696
|
-
const loadThread =
|
|
933
|
+
const loadThread = useCallback2(
|
|
697
934
|
async (threadId) => {
|
|
698
935
|
const seq = ++requestSeqRef.current;
|
|
699
936
|
setLoadingHistory(true);
|
|
@@ -703,7 +940,8 @@ function useAgents24ChatController({
|
|
|
703
940
|
const detail = await transport.getThread({
|
|
704
941
|
threadId,
|
|
705
942
|
limit: pageSize,
|
|
706
|
-
includeRunEvents: false
|
|
943
|
+
includeRunEvents: false,
|
|
944
|
+
signal: lifecycleAbortController.signal
|
|
707
945
|
});
|
|
708
946
|
if (activeThreadIdRef.current !== threadId || seq !== requestSeqRef.current) return;
|
|
709
947
|
void onThreadDetailLoaded?.(detail);
|
|
@@ -724,13 +962,20 @@ function useAgents24ChatController({
|
|
|
724
962
|
nextBeforeTurnIndex: paging.nextBeforeTurnIndex,
|
|
725
963
|
updated_at: threadActivityDate(detail)
|
|
726
964
|
});
|
|
965
|
+
} catch (error) {
|
|
966
|
+
if (isAbortError(error)) return;
|
|
967
|
+
if (isThreadNotFoundError(error)) {
|
|
968
|
+
clearMissingThread(threadId);
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
throw error;
|
|
727
972
|
} finally {
|
|
728
|
-
if (activeThreadIdRef.current === threadId) setLoadingHistory(false);
|
|
973
|
+
if (!lifecycleAbortController.signal.aborted && activeThreadIdRef.current === threadId && seq === requestSeqRef.current) setLoadingHistory(false);
|
|
729
974
|
}
|
|
730
975
|
},
|
|
731
|
-
[onThreadDetailLoaded, pageSize, setLoadingHistory, transport, upsertStoredThread]
|
|
976
|
+
[clearMissingThread, lifecycleAbortController, onThreadDetailLoaded, pageSize, setLoadingHistory, transport, upsertStoredThread]
|
|
732
977
|
);
|
|
733
|
-
const loadOlderTurns =
|
|
978
|
+
const loadOlderTurns = useCallback2(async () => {
|
|
734
979
|
const threadId = activeThreadIdRef.current;
|
|
735
980
|
const beforeTurnIndex = nextBeforeTurnIndexRef.current;
|
|
736
981
|
if (!threadId || beforeTurnIndex === null || isLoadingOlderRef.current) return;
|
|
@@ -741,7 +986,8 @@ function useAgents24ChatController({
|
|
|
741
986
|
threadId,
|
|
742
987
|
limit: pageSize,
|
|
743
988
|
beforeTurnIndex,
|
|
744
|
-
includeRunEvents: false
|
|
989
|
+
includeRunEvents: false,
|
|
990
|
+
signal: lifecycleAbortController.signal
|
|
745
991
|
});
|
|
746
992
|
if (activeThreadIdRef.current !== threadId) return;
|
|
747
993
|
const older = threadDetailToMessages(detail);
|
|
@@ -753,12 +999,19 @@ function useAgents24ChatController({
|
|
|
753
999
|
hasOlderTurnsRef.current = paging.hasOlderTurns;
|
|
754
1000
|
nextBeforeTurnIndexRef.current = paging.nextBeforeTurnIndex;
|
|
755
1001
|
persistThread(threadId, next, paging);
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
if (isAbortError(error)) return;
|
|
1004
|
+
if (isThreadNotFoundError(error)) {
|
|
1005
|
+
clearMissingThread(threadId);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
throw error;
|
|
756
1009
|
} finally {
|
|
757
|
-
if (activeThreadIdRef.current === threadId) setIsLoadingOlder(false);
|
|
1010
|
+
if (!lifecycleAbortController.signal.aborted && activeThreadIdRef.current === threadId) setIsLoadingOlder(false);
|
|
758
1011
|
isLoadingOlderRef.current = false;
|
|
759
1012
|
}
|
|
760
|
-
}, [pageSize, persistThread, transport]);
|
|
761
|
-
const handleStreamEvent =
|
|
1013
|
+
}, [clearMissingThread, lifecycleAbortController, pageSize, persistThread, transport]);
|
|
1014
|
+
const handleStreamEvent = useCallback2(
|
|
762
1015
|
(input) => {
|
|
763
1016
|
const { mode, event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages } = input;
|
|
764
1017
|
const payload = event.payload || {};
|
|
@@ -804,7 +1057,7 @@ function useAgents24ChatController({
|
|
|
804
1057
|
streamThreadIdRef.current = terminalThreadId;
|
|
805
1058
|
applyThreadId(terminalThreadId, baseMessages);
|
|
806
1059
|
}
|
|
807
|
-
const finalText = String(payload.assistant_output_text ||
|
|
1060
|
+
const finalText = String(payload.assistant_output_text || assistantTextFromResponseBlocks(responseBlocks || []) || streamingContentRef.current || "");
|
|
808
1061
|
finalizeAssistantMessage({
|
|
809
1062
|
threadId: streamThreadIdRef.current || activeThreadIdRef.current,
|
|
810
1063
|
baseMessages: messagesRef.current,
|
|
@@ -819,7 +1072,7 @@ function useAgents24ChatController({
|
|
|
819
1072
|
},
|
|
820
1073
|
[applyThreadId, finalizeAssistantMessage, markThreadRunStatus, onRuntimeEvent, onStreamErrorMessage, setActiveRunIdValue, setLiveAssistantMessage, setReasoningSteps]
|
|
821
1074
|
);
|
|
822
|
-
const runStream =
|
|
1075
|
+
const runStream = useCallback2(
|
|
823
1076
|
async (input) => {
|
|
824
1077
|
const startedAt = Date.now();
|
|
825
1078
|
const controller = new AbortController();
|
|
@@ -878,19 +1131,28 @@ function useAgents24ChatController({
|
|
|
878
1131
|
);
|
|
879
1132
|
}
|
|
880
1133
|
try {
|
|
1134
|
+
let streamResult = null;
|
|
881
1135
|
if (input.mode === "attach") {
|
|
882
1136
|
setActiveRunIdValue(input.runId);
|
|
883
1137
|
reattachedRunIdRef.current = input.runId;
|
|
884
|
-
await transport.attachRun(
|
|
1138
|
+
streamResult = await transport.attachRun(
|
|
885
1139
|
{ runId: input.runId, signal: controller.signal },
|
|
886
1140
|
(event) => handleStreamEvent({ mode: "attach", event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages })
|
|
887
1141
|
);
|
|
888
1142
|
} else {
|
|
889
|
-
await transport.streamMessage(
|
|
1143
|
+
streamResult = await transport.streamMessage(
|
|
890
1144
|
{ ...input.message, files: input.message.files || [], threadId: activeThreadIdRef.current, signal: controller.signal },
|
|
891
1145
|
(event) => handleStreamEvent({ mode: "submit", event, assistantMessageId, startedAt, streamThreadIdRef, baseMessages })
|
|
892
1146
|
);
|
|
893
1147
|
}
|
|
1148
|
+
if (streamResult?.runId) setActiveRunIdValue(streamResult.runId);
|
|
1149
|
+
if (streamResult?.threadId) {
|
|
1150
|
+
streamThreadIdRef.current = streamResult.threadId;
|
|
1151
|
+
applyThreadId(streamResult.threadId, baseMessages);
|
|
1152
|
+
if (streamResult.runId) {
|
|
1153
|
+
markThreadRunStatus(streamResult.threadId, streamResult.runId, "running");
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
894
1156
|
if (streamingContentRef.current.trim() && streamingMessageIdRef.current) {
|
|
895
1157
|
finalizeAssistantMessage({
|
|
896
1158
|
threadId: streamThreadIdRef.current,
|
|
@@ -904,7 +1166,7 @@ function useAgents24ChatController({
|
|
|
904
1166
|
}
|
|
905
1167
|
await refresh().catch(() => void 0);
|
|
906
1168
|
} catch (error) {
|
|
907
|
-
if (error
|
|
1169
|
+
if (!isAbortError(error)) {
|
|
908
1170
|
finalizeAssistantMessage({
|
|
909
1171
|
threadId: streamThreadIdRef.current,
|
|
910
1172
|
baseMessages: messagesRef.current,
|
|
@@ -933,18 +1195,22 @@ function useAgents24ChatController({
|
|
|
933
1195
|
},
|
|
934
1196
|
[createId, finalizeAssistantMessage, handleStreamEvent, onStreamErrorMessage, persistThread, refresh, setActiveRunIdValue, setReasoningSteps, transport]
|
|
935
1197
|
);
|
|
936
|
-
const handleSubmit =
|
|
1198
|
+
const handleSubmit = useCallback2(
|
|
937
1199
|
async (message) => {
|
|
938
1200
|
if (!message.text.trim() && !(message.files || []).length) return;
|
|
939
1201
|
await runStream({ mode: "submit", message });
|
|
940
1202
|
},
|
|
941
1203
|
[runStream]
|
|
942
1204
|
);
|
|
943
|
-
const
|
|
1205
|
+
const attachRun = useCallback2(async (runId, threadId) => {
|
|
1206
|
+
const resolvedThreadId = threadId ?? activeThreadIdRef.current;
|
|
1207
|
+
if (runId && resolvedThreadId) await runStream({ mode: "attach", runId, threadId: resolvedThreadId });
|
|
1208
|
+
}, [runStream]);
|
|
1209
|
+
const handleStop = useCallback2(() => {
|
|
944
1210
|
const runId = activeRunIdRef.current;
|
|
945
1211
|
const partial = streamingContentRef.current;
|
|
946
1212
|
const liveMessageId = streamingMessageIdRef.current;
|
|
947
|
-
abortControllerRef.current
|
|
1213
|
+
abortDetachedStream(abortControllerRef.current);
|
|
948
1214
|
abortControllerRef.current = null;
|
|
949
1215
|
setActiveRunIdValue(null);
|
|
950
1216
|
streamingMessageIdRef.current = null;
|
|
@@ -965,11 +1231,51 @@ function useAgents24ChatController({
|
|
|
965
1231
|
}
|
|
966
1232
|
}, [finalizeAssistantMessage, lastThinkingDurationMs, setActiveRunIdValue, setReasoningSteps, transport]);
|
|
967
1233
|
useEffect(() => {
|
|
968
|
-
refresh().catch(() => {
|
|
969
|
-
|
|
970
|
-
|
|
1234
|
+
refresh().catch((error) => {
|
|
1235
|
+
if (!isAbortError(error) && !lifecycleAbortController.signal.aborted) {
|
|
1236
|
+
setThreads(storage.listThreads());
|
|
1237
|
+
}
|
|
971
1238
|
});
|
|
972
|
-
}, [refresh, storage]);
|
|
1239
|
+
}, [lifecycleAbortController, refresh, storage]);
|
|
1240
|
+
useEffect(() => {
|
|
1241
|
+
return () => {
|
|
1242
|
+
lifecycleAbortController.abort();
|
|
1243
|
+
abortDetachedStream(abortControllerRef.current);
|
|
1244
|
+
abortControllerRef.current = null;
|
|
1245
|
+
};
|
|
1246
|
+
}, [lifecycleAbortController]);
|
|
1247
|
+
useEffect(() => {
|
|
1248
|
+
if (!transport.subscribeThreadEvents) return;
|
|
1249
|
+
let cancelled = false;
|
|
1250
|
+
let retryTimeout = null;
|
|
1251
|
+
let controller = null;
|
|
1252
|
+
const connect = () => {
|
|
1253
|
+
if (cancelled) return;
|
|
1254
|
+
controller = new AbortController();
|
|
1255
|
+
transport.subscribeThreadEvents?.(
|
|
1256
|
+
{ cursor: threadEventsCursorRef.current, signal: controller.signal },
|
|
1257
|
+
async (event) => {
|
|
1258
|
+
if (typeof event.cursor === "number") threadEventsCursorRef.current = event.cursor;
|
|
1259
|
+
if (event.event === "snapshot_required") {
|
|
1260
|
+
await refresh().catch(() => void 0);
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
const next = applyThreadSummaryEvent(storage.listThreads(), event);
|
|
1264
|
+
storage.setThreads(next);
|
|
1265
|
+
setThreads(storage.listThreads());
|
|
1266
|
+
}
|
|
1267
|
+
).catch((error) => {
|
|
1268
|
+
if (cancelled || isAbortError(error)) return;
|
|
1269
|
+
retryTimeout = setTimeout(connect, 1500);
|
|
1270
|
+
});
|
|
1271
|
+
};
|
|
1272
|
+
connect();
|
|
1273
|
+
return () => {
|
|
1274
|
+
cancelled = true;
|
|
1275
|
+
if (retryTimeout) clearTimeout(retryTimeout);
|
|
1276
|
+
controller?.abort();
|
|
1277
|
+
};
|
|
1278
|
+
}, [refresh, storage, transport]);
|
|
973
1279
|
useEffect(() => {
|
|
974
1280
|
syncThreadsFromStorage();
|
|
975
1281
|
}, [storageKey, syncThreadsFromStorage]);
|
|
@@ -983,6 +1289,9 @@ function useAgents24ChatController({
|
|
|
983
1289
|
detachActiveStream();
|
|
984
1290
|
}
|
|
985
1291
|
if (!activeThreadId) {
|
|
1292
|
+
if (previous === null && loadedThreadIdRef.current === null && messagesRef.current.length === 0 && !hasOlderTurnsRef.current && !isLoadingHistoryRef.current && !isLoadingOlderRef.current && nextBeforeTurnIndexRef.current === null) {
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
986
1295
|
requestSeqRef.current += 1;
|
|
987
1296
|
loadedThreadIdRef.current = null;
|
|
988
1297
|
setMessages([]);
|
|
@@ -1017,7 +1326,7 @@ function useAgents24ChatController({
|
|
|
1017
1326
|
useEffect(() => {
|
|
1018
1327
|
const threadId = activeThreadId;
|
|
1019
1328
|
if (!threadId || isLoadingHistory || loadedThreadIdRef.current !== threadId || activeRunIdRef.current) return;
|
|
1020
|
-
const runId =
|
|
1329
|
+
const runId = autoAttachRunIdFromThread(storage.getThread(threadId));
|
|
1021
1330
|
if (!runId || reattachedRunIdRef.current === runId) return;
|
|
1022
1331
|
void runStream({ mode: "attach", threadId, runId });
|
|
1023
1332
|
}, [activeThreadId, isLoadingHistory, runStream, storage, storageKey]);
|
|
@@ -1030,82 +1339,53 @@ function useAgents24ChatController({
|
|
|
1030
1339
|
if (!hasStaleUnfinishedAssistantCache(cached)) return;
|
|
1031
1340
|
void loadThread(threadId).catch(() => setLoadingHistory(false));
|
|
1032
1341
|
}, [activeThreadId, loadThread, setLoadingHistory, storage, storageKey]);
|
|
1033
|
-
const
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
setMessages
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
id: currentId || createId(),
|
|
1067
|
-
role: input.role,
|
|
1068
|
-
content,
|
|
1069
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
1070
|
-
isFinal: Boolean(input.isFinal),
|
|
1071
|
-
isVoice: input.role === "user",
|
|
1072
|
-
parts: content ? [{ id: createId(), type: "text", kind: "text", text: content }] : [],
|
|
1073
|
-
citations: input.citations,
|
|
1074
|
-
reasoningSteps: input.reasoningSteps
|
|
1075
|
-
};
|
|
1076
|
-
const next = index === -1 ? [...prev, nextMessage] : prev.map((message, itemIndex) => itemIndex === index ? { ...message, ...nextMessage } : message);
|
|
1077
|
-
liveVoiceIdsRef.current[input.role] = input.isFinal ? void 0 : nextMessage.id;
|
|
1078
|
-
messagesRef.current = next;
|
|
1079
|
-
if (activeThreadIdRef.current) persistThread(activeThreadIdRef.current, next);
|
|
1080
|
-
return next;
|
|
1081
|
-
});
|
|
1082
|
-
}, [createId, persistThread]);
|
|
1083
|
-
const startNewThread = useCallback(() => {
|
|
1084
|
-
detachActiveStream();
|
|
1085
|
-
requestSeqRef.current += 1;
|
|
1086
|
-
activeThreadIdRef.current = null;
|
|
1087
|
-
loadedThreadIdRef.current = null;
|
|
1088
|
-
nextBeforeTurnIndexRef.current = null;
|
|
1089
|
-
hasOlderTurnsRef.current = false;
|
|
1090
|
-
storage.setActiveThreadId?.(null);
|
|
1091
|
-
onActiveThreadIdChange?.(null);
|
|
1092
|
-
setMessages([]);
|
|
1093
|
-
messagesRef.current = [];
|
|
1094
|
-
setHasOlderTurns(false);
|
|
1095
|
-
setIsLoadingOlder(false);
|
|
1096
|
-
setContextStatus(null);
|
|
1097
|
-
setLoadingHistory(false);
|
|
1098
|
-
}, [detachActiveStream, onActiveThreadIdChange, setLoadingHistory, storage]);
|
|
1099
|
-
const loadThreadById = useCallback(
|
|
1342
|
+
const {
|
|
1343
|
+
handleCopy,
|
|
1344
|
+
handleDislike,
|
|
1345
|
+
handleLike,
|
|
1346
|
+
handleRetry,
|
|
1347
|
+
startNewThread,
|
|
1348
|
+
upsertLiveVoiceMessage
|
|
1349
|
+
} = useControllerMessageActions({
|
|
1350
|
+
activeThreadIdRef,
|
|
1351
|
+
createId,
|
|
1352
|
+
detachActiveStream,
|
|
1353
|
+
disliked,
|
|
1354
|
+
handleSubmit,
|
|
1355
|
+
hasOlderTurnsRef,
|
|
1356
|
+
liked,
|
|
1357
|
+
liveVoiceIdsRef,
|
|
1358
|
+
loadedThreadIdRef,
|
|
1359
|
+
messagesRef,
|
|
1360
|
+
nextBeforeTurnIndexRef,
|
|
1361
|
+
onActiveThreadIdChange,
|
|
1362
|
+
persistThread,
|
|
1363
|
+
requestSeqRef,
|
|
1364
|
+
setContextStatus,
|
|
1365
|
+
setCopiedMessageId,
|
|
1366
|
+
setDisliked,
|
|
1367
|
+
setHasOlderTurns,
|
|
1368
|
+
setIsLoadingOlder,
|
|
1369
|
+
setLiked,
|
|
1370
|
+
setLoadingHistory,
|
|
1371
|
+
setMessages,
|
|
1372
|
+
storage
|
|
1373
|
+
});
|
|
1374
|
+
const loadThreadById = useCallback2(
|
|
1100
1375
|
async (threadId) => {
|
|
1101
1376
|
if (!threadId) return;
|
|
1377
|
+
setIsSelectingThread(true);
|
|
1102
1378
|
if (activeThreadIdRef.current !== threadId) {
|
|
1103
1379
|
if (abortControllerRef.current) detachActiveStream();
|
|
1104
1380
|
activeThreadIdRef.current = threadId;
|
|
1105
1381
|
storage.setActiveThreadId?.(threadId);
|
|
1106
1382
|
onActiveThreadIdChange?.(threadId);
|
|
1107
1383
|
}
|
|
1108
|
-
|
|
1384
|
+
try {
|
|
1385
|
+
await loadThread(threadId);
|
|
1386
|
+
} finally {
|
|
1387
|
+
if (activeThreadIdRef.current === threadId) setIsSelectingThread(false);
|
|
1388
|
+
}
|
|
1109
1389
|
},
|
|
1110
1390
|
[detachActiveStream, loadThread, onActiveThreadIdChange, storage]
|
|
1111
1391
|
);
|
|
@@ -1123,6 +1403,7 @@ function useAgents24ChatController({
|
|
|
1123
1403
|
isLoadingHistory,
|
|
1124
1404
|
isLoadingOlder,
|
|
1125
1405
|
isRefreshingThreads,
|
|
1406
|
+
isSelectingThread,
|
|
1126
1407
|
hasOlderTurns,
|
|
1127
1408
|
liked,
|
|
1128
1409
|
disliked,
|
|
@@ -1130,6 +1411,7 @@ function useAgents24ChatController({
|
|
|
1130
1411
|
lastThinkingDurationMs,
|
|
1131
1412
|
activeRunId,
|
|
1132
1413
|
handleSubmit,
|
|
1414
|
+
attachRun,
|
|
1133
1415
|
handleStop,
|
|
1134
1416
|
handleCopy,
|
|
1135
1417
|
handleLike,
|
|
@@ -1147,6 +1429,7 @@ function useAgents24ChatController({
|
|
|
1147
1429
|
activeRunId,
|
|
1148
1430
|
activeThread,
|
|
1149
1431
|
activeThreadId,
|
|
1432
|
+
attachRun,
|
|
1150
1433
|
contextStatus,
|
|
1151
1434
|
currentReasoning,
|
|
1152
1435
|
disliked,
|
|
@@ -1161,6 +1444,7 @@ function useAgents24ChatController({
|
|
|
1161
1444
|
isLoadingHistory,
|
|
1162
1445
|
isLoadingOlder,
|
|
1163
1446
|
isRefreshingThreads,
|
|
1447
|
+
isSelectingThread,
|
|
1164
1448
|
lastThinkingDurationMs,
|
|
1165
1449
|
liked,
|
|
1166
1450
|
loadThreadById,
|
|
@@ -1205,13 +1489,16 @@ var DefaultChatPart = ({
|
|
|
1205
1489
|
if (part.kind === "ui-blocks") {
|
|
1206
1490
|
return /* @__PURE__ */ jsx("div", { "data-agents24-ui-blocks-part": true, "data-state": part.state });
|
|
1207
1491
|
}
|
|
1208
|
-
if (part.kind === "
|
|
1209
|
-
return /* @__PURE__ */ jsx("div", { "data-agents24-
|
|
1492
|
+
if (part.kind === "hitl") {
|
|
1493
|
+
return /* @__PURE__ */ jsx("div", { "data-agents24-hitl-part": part.interruptId || "" });
|
|
1210
1494
|
}
|
|
1211
1495
|
if (part.kind === "error") {
|
|
1212
1496
|
return /* @__PURE__ */ jsx("div", { "data-agents24-error-part": true, children: part.errorText });
|
|
1213
1497
|
}
|
|
1214
|
-
|
|
1498
|
+
if (part.kind === "data") {
|
|
1499
|
+
return /* @__PURE__ */ jsx("div", { "data-agents24-data-part": part.name });
|
|
1500
|
+
}
|
|
1501
|
+
return null;
|
|
1215
1502
|
};
|
|
1216
1503
|
var renderChatPart = (part, message, renderOptions) => /* @__PURE__ */ jsx(
|
|
1217
1504
|
DefaultChatPart,
|
|
@@ -1224,12 +1511,17 @@ var renderChatPart = (part, message, renderOptions) => /* @__PURE__ */ jsx(
|
|
|
1224
1511
|
);
|
|
1225
1512
|
|
|
1226
1513
|
// src/sse.ts
|
|
1514
|
+
var isAbortError2 = (error) => {
|
|
1515
|
+
if (!error || typeof error !== "object") return false;
|
|
1516
|
+
const maybe = error;
|
|
1517
|
+
return maybe.name === "AbortError" || String(maybe.message || "").toLowerCase().includes("aborted");
|
|
1518
|
+
};
|
|
1227
1519
|
var parseSseBlock = (block) => {
|
|
1228
1520
|
const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.replace(/^data:\s?/, "")).join("\n").trim();
|
|
1229
1521
|
if (!data) return null;
|
|
1230
1522
|
return JSON.parse(data);
|
|
1231
1523
|
};
|
|
1232
|
-
var consumeSseResponse = async (response, onEvent) => {
|
|
1524
|
+
var consumeSseResponse = async (response, onEvent, options = {}) => {
|
|
1233
1525
|
if (!response.ok) {
|
|
1234
1526
|
let message = response.statusText || "Failed to open chat stream.";
|
|
1235
1527
|
try {
|
|
@@ -1243,24 +1535,43 @@ var consumeSseResponse = async (response, onEvent) => {
|
|
|
1243
1535
|
if (!reader) throw new Error("The chat stream did not return a readable body.");
|
|
1244
1536
|
const decoder = new TextDecoder();
|
|
1245
1537
|
let buffer = "";
|
|
1246
|
-
let threadId = null;
|
|
1247
|
-
let runId = null;
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1538
|
+
let threadId = response.headers.get("X-Thread-ID") || null;
|
|
1539
|
+
let runId = response.headers.get("X-Run-ID") || null;
|
|
1540
|
+
let closed = false;
|
|
1541
|
+
const closeReader = () => {
|
|
1542
|
+
closed = true;
|
|
1543
|
+
queueMicrotask(() => {
|
|
1544
|
+
void reader.cancel().catch(() => {
|
|
1545
|
+
});
|
|
1546
|
+
});
|
|
1547
|
+
};
|
|
1548
|
+
if (options.signal?.aborted) {
|
|
1549
|
+
closeReader();
|
|
1550
|
+
return { threadId, runId };
|
|
1551
|
+
}
|
|
1552
|
+
options.signal?.addEventListener("abort", closeReader, { once: true });
|
|
1553
|
+
try {
|
|
1554
|
+
while (!closed) {
|
|
1555
|
+
const { value, done } = await reader.read();
|
|
1556
|
+
if (done) break;
|
|
1557
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1558
|
+
let boundary = buffer.indexOf("\n\n");
|
|
1559
|
+
while (boundary !== -1) {
|
|
1560
|
+
const block = buffer.slice(0, boundary);
|
|
1561
|
+
buffer = buffer.slice(boundary + 2);
|
|
1562
|
+
boundary = buffer.indexOf("\n\n");
|
|
1563
|
+
const event = parseSseBlock(block);
|
|
1564
|
+
if (!event) continue;
|
|
1565
|
+
if (event.run_id) runId = event.run_id;
|
|
1566
|
+
const payloadThreadId = event.payload?.thread_id;
|
|
1567
|
+
if (payloadThreadId) threadId = String(payloadThreadId);
|
|
1568
|
+
await onEvent(event);
|
|
1569
|
+
}
|
|
1263
1570
|
}
|
|
1571
|
+
} catch (error) {
|
|
1572
|
+
if (!isAbortError2(error)) throw error;
|
|
1573
|
+
} finally {
|
|
1574
|
+
options.signal?.removeEventListener("abort", closeReader);
|
|
1264
1575
|
}
|
|
1265
1576
|
return { threadId, runId };
|
|
1266
1577
|
};
|
|
@@ -1276,6 +1587,14 @@ var streamHeaders = (headers) => ({
|
|
|
1276
1587
|
Accept: "text/event-stream",
|
|
1277
1588
|
"Content-Type": "application/json"
|
|
1278
1589
|
});
|
|
1590
|
+
var ChatTransportError = class extends Error {
|
|
1591
|
+
constructor(message, status) {
|
|
1592
|
+
super(message);
|
|
1593
|
+
this.name = "ChatTransportError";
|
|
1594
|
+
this.status = status;
|
|
1595
|
+
}
|
|
1596
|
+
};
|
|
1597
|
+
var transportError = (response, fallback) => new ChatTransportError(response.statusText || fallback, response.status);
|
|
1279
1598
|
var createFetchChatTransport = ({
|
|
1280
1599
|
routes,
|
|
1281
1600
|
fetchImpl = fetch,
|
|
@@ -1289,7 +1608,7 @@ var createFetchChatTransport = ({
|
|
|
1289
1608
|
signal: input?.signal,
|
|
1290
1609
|
headers: jsonHeaders(await loadHeaders())
|
|
1291
1610
|
});
|
|
1292
|
-
if (!response.ok) throw
|
|
1611
|
+
if (!response.ok) throw transportError(response, "Failed to list chat threads.");
|
|
1293
1612
|
return response.json();
|
|
1294
1613
|
},
|
|
1295
1614
|
async getThread(input) {
|
|
@@ -1297,7 +1616,7 @@ var createFetchChatTransport = ({
|
|
|
1297
1616
|
signal: input.signal,
|
|
1298
1617
|
headers: jsonHeaders(await loadHeaders())
|
|
1299
1618
|
});
|
|
1300
|
-
if (!response.ok) throw
|
|
1619
|
+
if (!response.ok) throw transportError(response, "Failed to load chat thread.");
|
|
1301
1620
|
return response.json();
|
|
1302
1621
|
},
|
|
1303
1622
|
async streamMessage(input, onEvent) {
|
|
@@ -1313,7 +1632,7 @@ var createFetchChatTransport = ({
|
|
|
1313
1632
|
}
|
|
1314
1633
|
)
|
|
1315
1634
|
});
|
|
1316
|
-
return consumeSseResponse(response, onEvent);
|
|
1635
|
+
return consumeSseResponse(response, onEvent, { signal: input.signal });
|
|
1317
1636
|
},
|
|
1318
1637
|
async attachRun(input, onEvent) {
|
|
1319
1638
|
const response = await fetchImpl(routes.attachRun(input), {
|
|
@@ -1322,7 +1641,7 @@ var createFetchChatTransport = ({
|
|
|
1322
1641
|
headers: streamHeaders(await loadHeaders()),
|
|
1323
1642
|
body: JSON.stringify({})
|
|
1324
1643
|
});
|
|
1325
|
-
return consumeSseResponse(response, onEvent);
|
|
1644
|
+
return consumeSseResponse(response, onEvent, { signal: input.signal });
|
|
1326
1645
|
},
|
|
1327
1646
|
async cancelRun(input) {
|
|
1328
1647
|
const response = await fetchImpl(routes.cancelRun(input), {
|
|
@@ -1330,7 +1649,7 @@ var createFetchChatTransport = ({
|
|
|
1330
1649
|
headers: jsonHeaders(await loadHeaders()),
|
|
1331
1650
|
body: JSON.stringify({ assistant_output_text: input.assistantOutputText || void 0 })
|
|
1332
1651
|
});
|
|
1333
|
-
if (!response.ok) throw
|
|
1652
|
+
if (!response.ok) throw transportError(response, "Failed to cancel chat run.");
|
|
1334
1653
|
return response.json();
|
|
1335
1654
|
},
|
|
1336
1655
|
async deleteThread(input) {
|
|
@@ -1339,7 +1658,50 @@ var createFetchChatTransport = ({
|
|
|
1339
1658
|
method: "DELETE",
|
|
1340
1659
|
headers: jsonHeaders(await loadHeaders())
|
|
1341
1660
|
});
|
|
1342
|
-
if (!response.ok) throw
|
|
1661
|
+
if (!response.ok) throw transportError(response, "Failed to delete chat thread.");
|
|
1662
|
+
return response.json();
|
|
1663
|
+
},
|
|
1664
|
+
async subscribeThreadEvents(input, onEvent) {
|
|
1665
|
+
if (!routes.threadEvents) return;
|
|
1666
|
+
const response = await fetchImpl(routes.threadEvents({ cursor: input.cursor }), {
|
|
1667
|
+
method: "GET",
|
|
1668
|
+
signal: input.signal,
|
|
1669
|
+
headers: streamHeaders(await loadHeaders())
|
|
1670
|
+
});
|
|
1671
|
+
await consumeSseResponse(
|
|
1672
|
+
response,
|
|
1673
|
+
(event) => onEvent(event),
|
|
1674
|
+
{ signal: input.signal }
|
|
1675
|
+
);
|
|
1676
|
+
},
|
|
1677
|
+
async resumeHitl(input) {
|
|
1678
|
+
if (!routes.resumeRun) throw new ChatTransportError("HITL resume is not configured.", 501);
|
|
1679
|
+
const response = await fetchImpl(routes.resumeRun({ runId: input.runId }), {
|
|
1680
|
+
method: "POST",
|
|
1681
|
+
headers: jsonHeaders(await loadHeaders()),
|
|
1682
|
+
body: JSON.stringify({
|
|
1683
|
+
schema_version: "agents24.hitl.resume.v1",
|
|
1684
|
+
interrupt_id: input.interruptId,
|
|
1685
|
+
decisions: input.decisions,
|
|
1686
|
+
client: input.client
|
|
1687
|
+
})
|
|
1688
|
+
});
|
|
1689
|
+
if (!response.ok) throw transportError(response, "Failed to resume chat run.");
|
|
1690
|
+
return response.json();
|
|
1691
|
+
},
|
|
1692
|
+
async startMcpAuth(input) {
|
|
1693
|
+
if (!routes.startMcpAuth) throw new ChatTransportError("MCP auth is not configured.", 501);
|
|
1694
|
+
const response = await fetchImpl(routes.startMcpAuth({ runId: input.runId, serverId: input.serverId }), {
|
|
1695
|
+
method: "POST",
|
|
1696
|
+
headers: jsonHeaders(await loadHeaders()),
|
|
1697
|
+
body: JSON.stringify({
|
|
1698
|
+
interrupt_id: input.interruptId || void 0,
|
|
1699
|
+
principal_type: input.principalType || void 0,
|
|
1700
|
+
principal_id: input.principalId || void 0,
|
|
1701
|
+
client: input.client
|
|
1702
|
+
})
|
|
1703
|
+
});
|
|
1704
|
+
if (!response.ok) throw transportError(response, "Failed to start MCP authorization.");
|
|
1343
1705
|
return response.json();
|
|
1344
1706
|
}
|
|
1345
1707
|
};
|
|
@@ -1353,6 +1715,7 @@ import {
|
|
|
1353
1715
|
useMessageScrollerVisibility
|
|
1354
1716
|
} from "@shadcn/react/message-scroller";
|
|
1355
1717
|
export {
|
|
1718
|
+
ChatTransportError,
|
|
1356
1719
|
DEFAULT_THREAD_PAGE_SIZE,
|
|
1357
1720
|
DefaultChatPart,
|
|
1358
1721
|
DefaultToolPart,
|
|
@@ -1367,33 +1730,38 @@ export {
|
|
|
1367
1730
|
MessageScroller,
|
|
1368
1731
|
activeRunIdFromThread,
|
|
1369
1732
|
activeRunIdFromThreadDetail,
|
|
1733
|
+
appendStableStreamingTurn,
|
|
1370
1734
|
assistantTextFromParts,
|
|
1371
1735
|
assistantTextFromResponseBlocks,
|
|
1736
|
+
autoAttachRunIdFromThread,
|
|
1372
1737
|
compressionFromContextWindow,
|
|
1373
1738
|
consumeSseResponse,
|
|
1374
1739
|
createChatId,
|
|
1375
1740
|
createFetchChatTransport,
|
|
1741
|
+
findStableAssistantMessageIndex,
|
|
1376
1742
|
getActiveStreamingTextPartId,
|
|
1377
1743
|
hasStaleUnfinishedAssistantCache,
|
|
1378
1744
|
hasUnfinishedAssistantMessage,
|
|
1379
1745
|
isActiveStreamingTextPart,
|
|
1746
|
+
isAutoAttachThreadStatus,
|
|
1380
1747
|
isRunningThreadStatus,
|
|
1381
1748
|
latestContextWindowFromThread,
|
|
1382
1749
|
mergeContextWindow,
|
|
1383
1750
|
mergeContextWindowUpdate,
|
|
1384
1751
|
mergeReasoningSteps,
|
|
1752
|
+
mergeStoredThreadsForRefresh,
|
|
1385
1753
|
normalizeContextCompression,
|
|
1386
1754
|
normalizeContextWindow,
|
|
1387
1755
|
parseSseBlock,
|
|
1388
1756
|
partsFromResponseBlocks,
|
|
1389
1757
|
reasoningStepsFromParts,
|
|
1390
1758
|
renderChatPart,
|
|
1391
|
-
textFromFinalOutput,
|
|
1392
1759
|
threadActivityDate,
|
|
1393
1760
|
threadDetailToMessages,
|
|
1394
1761
|
threadPaging,
|
|
1395
1762
|
titleFromMessage,
|
|
1396
1763
|
toolStateFromStatus,
|
|
1764
|
+
upsertStableAssistantMessage,
|
|
1397
1765
|
useAgents24ChatController,
|
|
1398
1766
|
useMessageScroller,
|
|
1399
1767
|
useMessageScrollerScrollable,
|