@agents24/react 0.2.6 → 0.2.8
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/compatibility.json +8 -8
- package/dist/index.d.ts +1 -0
- package/dist/index.js +852 -299
- package/dist/index.js.map +1 -1
- package/dist/model.d.ts +17 -1
- package/dist/state.d.ts +5 -1
- package/dist/thread-message-queue.d.ts +47 -0
- package/dist/thread-summary-subscription.d.ts +12 -0
- package/dist/use-agent-chat-feedback.d.ts +13 -0
- package/dist/use-agent-chat-queue-actions.d.ts +20 -0
- package/dist/use-agent-chat-thread-list.d.ts +20 -0
- package/dist/use-agent-chat.d.ts +11 -1
- package/dist/use-agent-resources.d.ts +14 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -29,7 +29,7 @@ function getThreadActivity(thread) {
|
|
|
29
29
|
return {
|
|
30
30
|
status,
|
|
31
31
|
runId: activeRun?.run_id || thread.last_run_id || null,
|
|
32
|
-
isSpinning: Boolean(activeRun),
|
|
32
|
+
isSpinning: Boolean(activeRun) && !["waiting", "interrupted"].includes(status || ""),
|
|
33
33
|
isPaused: status === "paused",
|
|
34
34
|
isFailed: status === "failed",
|
|
35
35
|
isCompleted: status === "completed",
|
|
@@ -37,6 +37,24 @@ function getThreadActivity(thread) {
|
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
39
|
var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
40
|
+
function computerResourceTargets(value) {
|
|
41
|
+
const output = record(value);
|
|
42
|
+
if (!output) return [];
|
|
43
|
+
const candidates = [
|
|
44
|
+
output.resource_target,
|
|
45
|
+
...Array.isArray(output.resource_targets) ? output.resource_targets : [],
|
|
46
|
+
...Array.isArray(output.files) ? output.files.map((file) => record(file)?.resource_target) : []
|
|
47
|
+
];
|
|
48
|
+
const seen = /* @__PURE__ */ new Set();
|
|
49
|
+
return candidates.flatMap((candidate) => {
|
|
50
|
+
const target = record(candidate);
|
|
51
|
+
if (target?.type !== "computer" || typeof target.thread_id !== "string" || typeof target.computer_id !== "string" || typeof target.path !== "string" || !target.path.startsWith("/workspace") || target.kind !== "file" && target.kind !== "directory") return [];
|
|
52
|
+
const key = `${target.thread_id}:${target.computer_id}:${target.path}:${target.kind}`;
|
|
53
|
+
if (seen.has(key)) return [];
|
|
54
|
+
seen.add(key);
|
|
55
|
+
return [target];
|
|
56
|
+
});
|
|
57
|
+
}
|
|
40
58
|
var text = (value) => typeof value === "string" && value.trim() ? value : null;
|
|
41
59
|
var finiteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
42
60
|
var statusToToolState = (value) => {
|
|
@@ -200,6 +218,7 @@ function partsFromResponseBlocks(blocks, fallbackText = "", fallbackPartId = "as
|
|
|
200
218
|
state: statusToToolState(block.status),
|
|
201
219
|
input: tool.input,
|
|
202
220
|
output: tool.output,
|
|
221
|
+
resourceTargets: computerResourceTargets(tool.output),
|
|
203
222
|
errorText: text(tool.error || block.error),
|
|
204
223
|
failure: failureView(tool.failure),
|
|
205
224
|
presentation: toolPresentation(tool)
|
|
@@ -369,6 +388,21 @@ function threadDetailToMessages(detail) {
|
|
|
369
388
|
return output;
|
|
370
389
|
});
|
|
371
390
|
}
|
|
391
|
+
function threadTimelineToMessages(items, detail) {
|
|
392
|
+
const feedbackByRun = new Map((detail?.turns || []).flatMap((turn) => turn.run_id && turn.feedback ? [[turn.run_id, turn.feedback]] : []));
|
|
393
|
+
return items.map((item) => ({
|
|
394
|
+
id: item.id,
|
|
395
|
+
role: item.role,
|
|
396
|
+
runId: item.run_id,
|
|
397
|
+
segmentIndex: item.segment_index,
|
|
398
|
+
content: item.text,
|
|
399
|
+
createdAt: item.created_at,
|
|
400
|
+
isFinal: item.status === "completed" || item.status === "delivered",
|
|
401
|
+
parts: item.role === "assistant" ? partsFromResponseBlocks(item.response_blocks, item.text, `${item.id}-text`) : item.text ? [{ id: `${item.id}-text`, type: "text", kind: "text", text: item.text }] : [],
|
|
402
|
+
attachments: item.role === "user" ? item.attachment_ids.map((id) => ({ id, filename: "Attachment" })) : void 0,
|
|
403
|
+
feedback: item.role === "assistant" && item.run_id ? feedbackByRun.get(item.run_id) : void 0
|
|
404
|
+
}));
|
|
405
|
+
}
|
|
372
406
|
function latestThreadContext(detail) {
|
|
373
407
|
let contextWindow = null;
|
|
374
408
|
for (const turn of detail.turns || []) {
|
|
@@ -477,12 +511,18 @@ function withAgentChatBackgroundError(state, cause) {
|
|
|
477
511
|
function clearAgentChatBackgroundError(state) {
|
|
478
512
|
return state.backgroundError ? { ...state, backgroundError: null } : state;
|
|
479
513
|
}
|
|
480
|
-
function reconcileRefreshedThreads(current, incoming, activeThreadId) {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
const
|
|
485
|
-
return
|
|
514
|
+
function reconcileRefreshedThreads(current, incoming, activeThreadId, concurrentEvents = []) {
|
|
515
|
+
const currentById = new Map(current.map((thread) => [thread.id, thread]));
|
|
516
|
+
const newest = incoming.map((thread) => preferNewestThreadSummary(currentById.get(thread.id), thread));
|
|
517
|
+
const active = activeThreadId && !incoming.some((thread) => thread.id === activeThreadId) ? current.find((thread) => thread.id === activeThreadId) : null;
|
|
518
|
+
const base = active?.isHydrated ? [active, ...newest] : newest;
|
|
519
|
+
return concurrentEvents.reduce(applyThreadSummaryEvent, base);
|
|
520
|
+
}
|
|
521
|
+
function preferNewestThreadSummary(current, incoming) {
|
|
522
|
+
if (!current?.updated_at || !incoming.updated_at) return incoming;
|
|
523
|
+
const currentTime = Date.parse(current.updated_at);
|
|
524
|
+
const incomingTime = Date.parse(incoming.updated_at);
|
|
525
|
+
return Number.isFinite(currentTime) && Number.isFinite(incomingTime) && currentTime > incomingTime ? current : incoming;
|
|
486
526
|
}
|
|
487
527
|
function adoptStreamingThread(state, threadId) {
|
|
488
528
|
const normalizedThreadId = threadId.trim();
|
|
@@ -509,13 +549,20 @@ function appendStableStreamingTurn(messages, userMessage, assistantMessage) {
|
|
|
509
549
|
))) next.push(assistantMessage);
|
|
510
550
|
return next;
|
|
511
551
|
}
|
|
552
|
+
function preserveMountedMessageIds(canonical, current) {
|
|
553
|
+
return canonical.map((message) => {
|
|
554
|
+
const mounted = current.find((item) => item.role === message.role && (item.durableId === message.id || item.id === message.id || message.role === "assistant" && message.segmentIndex === 1 && item.runId && item.runId === message.runId && !item.durableId));
|
|
555
|
+
return mounted && mounted.id !== message.id ? { ...message, id: mounted.id, durableId: message.id } : message;
|
|
556
|
+
});
|
|
557
|
+
}
|
|
512
558
|
function upsertStableAssistantMessage(messages, input) {
|
|
513
559
|
const next = [...messages];
|
|
514
560
|
for (const baseMessage of input.baseMessages || []) {
|
|
515
|
-
const isTarget = baseMessage.role === "assistant" && (Boolean(input.messageId && baseMessage.id === input.messageId) || Boolean(input.runId && baseMessage.runId === input.runId));
|
|
561
|
+
const isTarget = baseMessage.role === "assistant" && (Boolean(input.messageId && baseMessage.id === input.messageId) || Boolean(!input.strictMessageId && input.runId && baseMessage.runId === input.runId));
|
|
516
562
|
if (!isTarget && !next.some((message) => message.id === baseMessage.id)) next.push(baseMessage);
|
|
517
563
|
}
|
|
518
|
-
const
|
|
564
|
+
const exactIndex = next.findIndex((message) => message.role === "assistant" && Boolean(input.messageId && message.id === input.messageId));
|
|
565
|
+
const index = exactIndex >= 0 ? exactIndex : input.strictMessageId ? -1 : next.findIndex((message) => message.role === "assistant" && Boolean(input.runId && message.runId === input.runId));
|
|
519
566
|
if (index === -1) next.push(input.create());
|
|
520
567
|
else next[index] = input.update(next[index]);
|
|
521
568
|
return next;
|
|
@@ -561,11 +608,14 @@ function applyRuntimeEvent(state, event) {
|
|
|
561
608
|
const projection = assistantProjectionFromEvent(event);
|
|
562
609
|
const eventName = String(event.event || "");
|
|
563
610
|
const runId = event.run_id || state.activeRunId;
|
|
564
|
-
const
|
|
611
|
+
const segmentId = typeof event.payload?.segment_id === "string" ? event.payload.segment_id : null;
|
|
612
|
+
const distinctSegment = Boolean(segmentId && segmentId !== `${runId}:0`);
|
|
613
|
+
const messageId = distinctSegment ? `${runId || "run"}-assistant-${segmentId}` : state.streamingMessageId || `${runId || "run"}-assistant`;
|
|
565
614
|
const terminal = eventName === "run.completed" || eventName === "run.failed" || eventName === "run.cancelled";
|
|
566
615
|
const paused = eventName === "run.paused" || projection.parts.some((part) => part.kind === "hitl" && part.status === "pending");
|
|
616
|
+
const suspension = eventName === "run.waiting" ? "waiting" : eventName === "run.interrupted" ? "interrupted" : null;
|
|
567
617
|
const cancelling = eventName === "run.cancelling" || String(event.payload?.status || "") === "cancelling";
|
|
568
|
-
const existing = state.messages.find((message) => message.role === "assistant" &&
|
|
618
|
+
const existing = state.messages.find((message) => message.role === "assistant" && message.id === messageId) || (!distinctSegment ? state.messages.find((message) => message.role === "assistant" && Boolean(runId && message.runId === runId)) : void 0);
|
|
569
619
|
const payload = event.payload || {};
|
|
570
620
|
const eventThreadId = typeof payload.thread_id === "string" && payload.thread_id.trim() ? payload.thread_id.trim() : null;
|
|
571
621
|
const contextApplies = (!state.activeThreadId || !eventThreadId || eventThreadId === state.activeThreadId) && (!state.activeRunId || !runId || runId === state.activeRunId);
|
|
@@ -592,6 +642,7 @@ function applyRuntimeEvent(state, event) {
|
|
|
592
642
|
const messages = upsertStableAssistantMessage(state.messages, {
|
|
593
643
|
messageId,
|
|
594
644
|
runId,
|
|
645
|
+
strictMessageId: distinctSegment,
|
|
595
646
|
create: () => ({
|
|
596
647
|
id: messageId,
|
|
597
648
|
role: "assistant",
|
|
@@ -637,14 +688,26 @@ function applyRuntimeEvent(state, event) {
|
|
|
637
688
|
contextWindow,
|
|
638
689
|
contextCompression,
|
|
639
690
|
contextCompressionRevision: hasLiveCompressionUpdate ? state.contextCompressionRevision + 1 : state.contextCompressionRevision,
|
|
640
|
-
runState: paused ? "paused" : cancelling ? "cancelling" : terminal ? eventName === "run.completed" ? "completed" : eventName === "run.cancelled" ? "cancelled" : "failed" : "streaming",
|
|
641
|
-
streamingMessageId: terminal || paused ? null : messageId
|
|
691
|
+
runState: suspension || (paused ? "paused" : cancelling ? "cancelling" : terminal ? eventName === "run.completed" ? "completed" : eventName === "run.cancelled" ? "cancelled" : "failed" : "streaming"),
|
|
692
|
+
streamingMessageId: terminal || paused || suspension ? null : messageId
|
|
642
693
|
};
|
|
643
694
|
}
|
|
644
695
|
function applyRuntimeEvents(state, events) {
|
|
645
696
|
return events.reduce(applyRuntimeEvent, state);
|
|
646
697
|
}
|
|
647
698
|
function applyThreadSummaryEvent(threads, event) {
|
|
699
|
+
if (String(event.event) === "thread.task.updated") {
|
|
700
|
+
const value = event;
|
|
701
|
+
const handle = value.task?.handle;
|
|
702
|
+
if (typeof handle !== "string") return threads;
|
|
703
|
+
return threads.map((thread) => thread.id === value.thread_id ? {
|
|
704
|
+
...thread,
|
|
705
|
+
taskNotifications: [
|
|
706
|
+
...(thread.taskNotifications || []).filter((task) => task.handle !== handle),
|
|
707
|
+
{ ...value.task, handle }
|
|
708
|
+
].slice(-1e3)
|
|
709
|
+
} : thread);
|
|
710
|
+
}
|
|
648
711
|
if (event.event === "snapshot_required") return threads;
|
|
649
712
|
if (event.event === "thread.deleted") return threads.filter((thread) => thread.id !== event.thread_id);
|
|
650
713
|
if (event.event !== "thread.summary.updated") return threads;
|
|
@@ -652,12 +715,153 @@ function applyThreadSummaryEvent(threads, event) {
|
|
|
652
715
|
const index = threads.findIndex((thread) => thread.id === incoming.id);
|
|
653
716
|
if (index === -1) return [{ ...incoming, messages: incoming.messages || [], isHydrated: false }, ...threads];
|
|
654
717
|
const next = [...threads];
|
|
655
|
-
next[index] =
|
|
718
|
+
next[index] = preferNewestThreadSummary(next[index], {
|
|
719
|
+
...next[index],
|
|
720
|
+
...incoming,
|
|
721
|
+
messages: next[index].messages,
|
|
722
|
+
isHydrated: next[index].isHydrated
|
|
723
|
+
});
|
|
656
724
|
return next;
|
|
657
725
|
}
|
|
658
726
|
|
|
659
|
-
// src/
|
|
727
|
+
// src/thread-message-queue.ts
|
|
660
728
|
import * as React2 from "react";
|
|
729
|
+
function useThreadMessageQueue({ threadId, transport, onPromoted }) {
|
|
730
|
+
const [pendingMessages, setPendingMessages] = React2.useState([]);
|
|
731
|
+
const [messageQueue, setMessageQueue] = React2.useState(null);
|
|
732
|
+
const pendingRef = React2.useRef(/* @__PURE__ */ new Map());
|
|
733
|
+
const versionRef = React2.useRef(0);
|
|
734
|
+
const cursorRef = React2.useRef(0);
|
|
735
|
+
const promotedRef = React2.useRef(/* @__PURE__ */ new Set());
|
|
736
|
+
const threadRef = React2.useRef(threadId);
|
|
737
|
+
const transportRef = React2.useRef(transport);
|
|
738
|
+
const onPromotedRef = React2.useRef(onPromoted);
|
|
739
|
+
threadRef.current = threadId;
|
|
740
|
+
transportRef.current = transport;
|
|
741
|
+
onPromotedRef.current = onPromoted;
|
|
742
|
+
const hydrate = React2.useCallback((page, selectedThreadId) => {
|
|
743
|
+
if (threadRef.current !== selectedThreadId) return;
|
|
744
|
+
const pending = page.pending.filter((message) => !promotedRef.current.has(message.id));
|
|
745
|
+
for (const message of pending) pendingRef.current.set(message.id, message);
|
|
746
|
+
setPendingMessages(pending);
|
|
747
|
+
setMessageQueue(page.queue);
|
|
748
|
+
}, []);
|
|
749
|
+
const refresh = React2.useCallback(async (selectedThreadId = threadRef.current) => {
|
|
750
|
+
if (!selectedThreadId) return null;
|
|
751
|
+
const version = ++versionRef.current;
|
|
752
|
+
const page = await transportRef.current.list(selectedThreadId);
|
|
753
|
+
if (version !== versionRef.current || threadRef.current !== selectedThreadId) return null;
|
|
754
|
+
for (const item of page.items) {
|
|
755
|
+
if (item.role === "user" && item.status === "delivered" && item.run_id) {
|
|
756
|
+
const pending = pendingRef.current.get(item.id);
|
|
757
|
+
if (pending && !promotedRef.current.has(item.id)) {
|
|
758
|
+
promotedRef.current.add(item.id);
|
|
759
|
+
pendingRef.current.delete(item.id);
|
|
760
|
+
onPromotedRef.current(pending, item.run_id);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
hydrate(page, selectedThreadId);
|
|
765
|
+
return page;
|
|
766
|
+
}, [hydrate]);
|
|
767
|
+
const acceptNotice = React2.useCallback((notice) => {
|
|
768
|
+
if (notice.cursor && notice.cursor <= cursorRef.current) return false;
|
|
769
|
+
if (notice.cursor) cursorRef.current = notice.cursor;
|
|
770
|
+
const pending = pendingRef.current.get(notice.messageId);
|
|
771
|
+
if (notice.status === "delivered" && notice.runId && pending && !promotedRef.current.has(pending.id)) {
|
|
772
|
+
promotedRef.current.add(pending.id);
|
|
773
|
+
pendingRef.current.delete(pending.id);
|
|
774
|
+
versionRef.current += 1;
|
|
775
|
+
setPendingMessages((current) => current.filter((message) => message.id !== pending.id));
|
|
776
|
+
onPromotedRef.current(pending, notice.runId);
|
|
777
|
+
}
|
|
778
|
+
if (notice.status === "removed") pendingRef.current.delete(notice.messageId);
|
|
779
|
+
void refresh().catch(() => void 0);
|
|
780
|
+
return true;
|
|
781
|
+
}, [refresh]);
|
|
782
|
+
const submit = React2.useCallback(async (input) => {
|
|
783
|
+
const selectedThreadId = threadRef.current;
|
|
784
|
+
if (!selectedThreadId) throw new Error("No active Agent thread is available.");
|
|
785
|
+
const result = await transportRef.current.submit({ ...input, threadId: selectedThreadId });
|
|
786
|
+
if (threadRef.current !== selectedThreadId) return result;
|
|
787
|
+
versionRef.current += 1;
|
|
788
|
+
if (!result.run_id && ["queued", "steer_pending", "blocked"].includes(result.status)) {
|
|
789
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
790
|
+
const pending = {
|
|
791
|
+
id: result.message_id,
|
|
792
|
+
thread_id: selectedThreadId,
|
|
793
|
+
sequence: Number.MAX_SAFE_INTEGER,
|
|
794
|
+
run_id: null,
|
|
795
|
+
turn_id: null,
|
|
796
|
+
segment_index: null,
|
|
797
|
+
role: "user",
|
|
798
|
+
status: result.status,
|
|
799
|
+
requested_mode: input.mode,
|
|
800
|
+
text: input.input,
|
|
801
|
+
attachment_ids: input.attachmentIds || [],
|
|
802
|
+
response_blocks: [],
|
|
803
|
+
revision: 1,
|
|
804
|
+
created_at: now,
|
|
805
|
+
updated_at: now
|
|
806
|
+
};
|
|
807
|
+
pendingRef.current.set(pending.id, pending);
|
|
808
|
+
setPendingMessages((current) => current.some((message) => message.id === pending.id) ? current : [...current, pending]);
|
|
809
|
+
setMessageQueue((current) => ({
|
|
810
|
+
pending_count: Math.max(current?.pending_count || 0, result.queue_position || 1),
|
|
811
|
+
paused: current?.paused || false,
|
|
812
|
+
reason: current?.reason || null
|
|
813
|
+
}));
|
|
814
|
+
}
|
|
815
|
+
void refresh().catch(() => void 0);
|
|
816
|
+
return result;
|
|
817
|
+
}, [refresh]);
|
|
818
|
+
const edit = React2.useCallback(async (message, input) => {
|
|
819
|
+
const updated = await transportRef.current.edit(message, input);
|
|
820
|
+
versionRef.current += 1;
|
|
821
|
+
pendingRef.current.set(updated.id, updated);
|
|
822
|
+
await refresh(message.thread_id);
|
|
823
|
+
return updated;
|
|
824
|
+
}, [refresh]);
|
|
825
|
+
const remove = React2.useCallback(async (message) => {
|
|
826
|
+
await transportRef.current.remove(message);
|
|
827
|
+
versionRef.current += 1;
|
|
828
|
+
pendingRef.current.delete(message.id);
|
|
829
|
+
setPendingMessages((current) => current.filter((item) => item.id !== message.id));
|
|
830
|
+
await refresh(message.thread_id);
|
|
831
|
+
}, [refresh]);
|
|
832
|
+
const retractForEdit = React2.useCallback(async (message) => {
|
|
833
|
+
await remove(message);
|
|
834
|
+
return { text: message.text, attachment_ids: message.attachment_ids, attachments: message.attachments };
|
|
835
|
+
}, [remove]);
|
|
836
|
+
const resume = React2.useCallback(async () => {
|
|
837
|
+
const selectedThreadId = threadRef.current;
|
|
838
|
+
if (!selectedThreadId) return null;
|
|
839
|
+
const result = await transportRef.current.resume(selectedThreadId);
|
|
840
|
+
await refresh(selectedThreadId);
|
|
841
|
+
return result;
|
|
842
|
+
}, [refresh]);
|
|
843
|
+
React2.useEffect(() => {
|
|
844
|
+
versionRef.current += 1;
|
|
845
|
+
cursorRef.current = 0;
|
|
846
|
+
promotedRef.current.clear();
|
|
847
|
+
pendingRef.current.clear();
|
|
848
|
+
setPendingMessages([]);
|
|
849
|
+
setMessageQueue(null);
|
|
850
|
+
if (threadId) void refresh(threadId).catch(() => void 0);
|
|
851
|
+
}, [threadId, refresh]);
|
|
852
|
+
React2.useEffect(() => {
|
|
853
|
+
if (!threadId || !pendingMessages.length || messageQueue?.paused) return;
|
|
854
|
+
const clock = globalThis;
|
|
855
|
+
const timer = clock.setInterval(() => {
|
|
856
|
+
void refresh(threadId).catch(() => void 0);
|
|
857
|
+
}, 3e3);
|
|
858
|
+
return () => clock.clearInterval(timer);
|
|
859
|
+
}, [threadId, pendingMessages.length, messageQueue?.paused, refresh]);
|
|
860
|
+
return { pendingMessages, messageQueue, refresh, hydrate, acceptNotice, submit, edit, remove, retractForEdit, resume };
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// src/use-agent-chat.ts
|
|
864
|
+
import * as React6 from "react";
|
|
661
865
|
|
|
662
866
|
// src/runtime-event-buffer.ts
|
|
663
867
|
function runtimeEventFrameScheduler() {
|
|
@@ -707,6 +911,219 @@ function createRuntimeEventFrameBuffer(apply, scheduleFrame) {
|
|
|
707
911
|
};
|
|
708
912
|
}
|
|
709
913
|
|
|
914
|
+
// src/thread-summary-subscription.ts
|
|
915
|
+
function waitForReconnect(signal, delay) {
|
|
916
|
+
return new Promise((resolve) => {
|
|
917
|
+
if (signal.aborted) return resolve();
|
|
918
|
+
const clock = globalThis;
|
|
919
|
+
const finish = () => {
|
|
920
|
+
clock.clearTimeout(timer);
|
|
921
|
+
signal.removeEventListener("abort", finish);
|
|
922
|
+
resolve();
|
|
923
|
+
};
|
|
924
|
+
const timer = clock.setTimeout(finish, delay);
|
|
925
|
+
signal.addEventListener("abort", finish, { once: true });
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
async function subscribeToThreadSummaries(input) {
|
|
929
|
+
let cursor;
|
|
930
|
+
let failures = 0;
|
|
931
|
+
while (!input.signal.aborted) {
|
|
932
|
+
let refreshedFromSnapshot = false;
|
|
933
|
+
try {
|
|
934
|
+
await input.client.threads.events({ cursor, signal: input.signal }, async (event) => {
|
|
935
|
+
if (event.event === "snapshot_required") {
|
|
936
|
+
await input.refresh();
|
|
937
|
+
cursor = void 0;
|
|
938
|
+
refreshedFromSnapshot = true;
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
cursor = event.cursor;
|
|
942
|
+
failures = 0;
|
|
943
|
+
await input.onEvent(event);
|
|
944
|
+
});
|
|
945
|
+
if (input.signal.aborted) break;
|
|
946
|
+
if (!refreshedFromSnapshot) await input.refresh();
|
|
947
|
+
input.onRecovered();
|
|
948
|
+
failures = 0;
|
|
949
|
+
} catch (error) {
|
|
950
|
+
if (input.signal.aborted || isAbortLike(error)) break;
|
|
951
|
+
input.onError(error);
|
|
952
|
+
failures += 1;
|
|
953
|
+
try {
|
|
954
|
+
await input.refresh();
|
|
955
|
+
input.onRecovered();
|
|
956
|
+
} catch (refreshError) {
|
|
957
|
+
if (!input.signal.aborted && !isAbortLike(refreshError)) input.onError(refreshError);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
if (!input.signal.aborted) {
|
|
961
|
+
await waitForReconnect(input.signal, Math.min(3e4, 1e3 * 2 ** Math.min(failures, 5)));
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// src/use-agent-chat-thread-list.ts
|
|
967
|
+
import * as React3 from "react";
|
|
968
|
+
function summaryToStored(thread) {
|
|
969
|
+
return { ...thread, id: String(thread.id), messages: [], isHydrated: false };
|
|
970
|
+
}
|
|
971
|
+
function useAgentChatThreadList({ client, threadPageSize, stateRef, update, clearOperationError, failOperation }) {
|
|
972
|
+
const [isLoadingThreads, setIsLoadingThreads] = React3.useState(false);
|
|
973
|
+
const [isLoadingMoreThreads, setIsLoadingMoreThreads] = React3.useState(false);
|
|
974
|
+
const [threadTotal, setThreadTotal] = React3.useState();
|
|
975
|
+
const refreshPromiseRef = React3.useRef(null);
|
|
976
|
+
const eventsDuringRefreshRef = React3.useRef(null);
|
|
977
|
+
const refreshThreads = React3.useCallback(() => {
|
|
978
|
+
if (refreshPromiseRef.current) return refreshPromiseRef.current;
|
|
979
|
+
clearOperationError();
|
|
980
|
+
setIsLoadingThreads(true);
|
|
981
|
+
eventsDuringRefreshRef.current = [];
|
|
982
|
+
const request = (async () => {
|
|
983
|
+
try {
|
|
984
|
+
const result = await client.threads.list({ limit: threadPageSize });
|
|
985
|
+
const concurrentEvents = eventsDuringRefreshRef.current || [];
|
|
986
|
+
eventsDuringRefreshRef.current = null;
|
|
987
|
+
setThreadTotal(result.total);
|
|
988
|
+
update((current) => {
|
|
989
|
+
const byId = new Map(current.threads.map((thread) => [thread.id, thread]));
|
|
990
|
+
const incoming = result.items.map((item) => {
|
|
991
|
+
const existing = byId.get(item.id);
|
|
992
|
+
return { ...existing, ...summaryToStored(item), messages: existing?.messages || [], isHydrated: existing?.isHydrated };
|
|
993
|
+
});
|
|
994
|
+
return { ...current, threads: reconcileRefreshedThreads(current.threads, incoming, current.activeThreadId, concurrentEvents) };
|
|
995
|
+
});
|
|
996
|
+
} catch (error) {
|
|
997
|
+
if (!isAbortLike(error)) {
|
|
998
|
+
failOperation("refresh_threads", error);
|
|
999
|
+
throw error;
|
|
1000
|
+
}
|
|
1001
|
+
} finally {
|
|
1002
|
+
eventsDuringRefreshRef.current = null;
|
|
1003
|
+
setIsLoadingThreads(false);
|
|
1004
|
+
}
|
|
1005
|
+
})();
|
|
1006
|
+
refreshPromiseRef.current = request;
|
|
1007
|
+
void request.finally(() => {
|
|
1008
|
+
if (refreshPromiseRef.current === request) refreshPromiseRef.current = null;
|
|
1009
|
+
}).catch(() => void 0);
|
|
1010
|
+
return request;
|
|
1011
|
+
}, [clearOperationError, client, failOperation, threadPageSize, update]);
|
|
1012
|
+
const loadMoreThreads = React3.useCallback(async () => {
|
|
1013
|
+
if (isLoadingMoreThreads) return;
|
|
1014
|
+
const skip = stateRef.current.threads.length;
|
|
1015
|
+
if (threadTotal !== void 0 && skip >= threadTotal) return;
|
|
1016
|
+
clearOperationError();
|
|
1017
|
+
setIsLoadingMoreThreads(true);
|
|
1018
|
+
try {
|
|
1019
|
+
const result = await client.threads.list({ skip, limit: threadPageSize });
|
|
1020
|
+
setThreadTotal(result.total);
|
|
1021
|
+
update((current) => {
|
|
1022
|
+
const byId = new Map(current.threads.map((thread) => [thread.id, thread]));
|
|
1023
|
+
for (const item of result.items) {
|
|
1024
|
+
const existing = byId.get(item.id);
|
|
1025
|
+
byId.set(item.id, preferNewestThreadSummary(existing, {
|
|
1026
|
+
...existing,
|
|
1027
|
+
...summaryToStored(item),
|
|
1028
|
+
messages: existing?.messages || [],
|
|
1029
|
+
isHydrated: existing?.isHydrated
|
|
1030
|
+
}));
|
|
1031
|
+
}
|
|
1032
|
+
return { ...current, threads: [...byId.values()] };
|
|
1033
|
+
});
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
if (!isAbortLike(error)) {
|
|
1036
|
+
failOperation("load_more_threads", error);
|
|
1037
|
+
throw error;
|
|
1038
|
+
}
|
|
1039
|
+
} finally {
|
|
1040
|
+
setIsLoadingMoreThreads(false);
|
|
1041
|
+
}
|
|
1042
|
+
}, [clearOperationError, client, failOperation, isLoadingMoreThreads, stateRef, threadPageSize, threadTotal, update]);
|
|
1043
|
+
return { isLoadingThreads, isLoadingMoreThreads, threadTotal, eventsDuringRefreshRef, refreshThreads, loadMoreThreads };
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// src/use-agent-chat-feedback.ts
|
|
1047
|
+
import * as React4 from "react";
|
|
1048
|
+
function useAgentChatFeedback({ client, update, clearOperationError, failOperation }) {
|
|
1049
|
+
return React4.useCallback(async (message, input) => {
|
|
1050
|
+
const runId = message.runId;
|
|
1051
|
+
if (!runId || message.role !== "assistant" || message.isFinal === false) {
|
|
1052
|
+
const error = new Error("Only persisted final responses can receive feedback.");
|
|
1053
|
+
failOperation("set_feedback", error);
|
|
1054
|
+
throw error;
|
|
1055
|
+
}
|
|
1056
|
+
const previous = message.feedback ?? null;
|
|
1057
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1058
|
+
const optimistic = input.rating ? {
|
|
1059
|
+
rating: input.rating,
|
|
1060
|
+
reason: input.rating === "dislike" ? input.reason ?? null : null,
|
|
1061
|
+
comment: input.rating === "dislike" ? input.comment?.trim() || null : null,
|
|
1062
|
+
created_at: previous?.created_at || timestamp,
|
|
1063
|
+
updated_at: timestamp
|
|
1064
|
+
} : null;
|
|
1065
|
+
const replaceFeedback = (current, feedback) => ({
|
|
1066
|
+
...current,
|
|
1067
|
+
messages: current.messages.map((item) => item.id === message.id ? { ...item, feedback } : item),
|
|
1068
|
+
threads: current.threads.map((thread) => ({
|
|
1069
|
+
...thread,
|
|
1070
|
+
messages: thread.messages.map((item) => item.id === message.id ? { ...item, feedback } : item)
|
|
1071
|
+
}))
|
|
1072
|
+
});
|
|
1073
|
+
clearOperationError();
|
|
1074
|
+
update((current) => replaceFeedback(current, optimistic));
|
|
1075
|
+
try {
|
|
1076
|
+
const result = await client.runs.setFeedback({ runId, ...input });
|
|
1077
|
+
update((current) => replaceFeedback(current, result.feedback));
|
|
1078
|
+
} catch (error) {
|
|
1079
|
+
update((current) => withAgentChatOperationError(replaceFeedback(current, previous), "set_feedback", error));
|
|
1080
|
+
throw error;
|
|
1081
|
+
}
|
|
1082
|
+
}, [clearOperationError, client, failOperation, update]);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// src/use-agent-chat-queue-actions.ts
|
|
1086
|
+
import * as React5 from "react";
|
|
1087
|
+
function useAgentChatQueueActions({
|
|
1088
|
+
queue,
|
|
1089
|
+
stateRef,
|
|
1090
|
+
createId,
|
|
1091
|
+
clearOperationError,
|
|
1092
|
+
failOperation,
|
|
1093
|
+
refreshMessages,
|
|
1094
|
+
attachRun
|
|
1095
|
+
}) {
|
|
1096
|
+
const steer = React5.useCallback(async (text2) => {
|
|
1097
|
+
const threadId = stateRef.current.activeThreadId;
|
|
1098
|
+
if (!threadId || !text2.trim()) throw new Error("An active thread and text are required to steer.");
|
|
1099
|
+
clearOperationError();
|
|
1100
|
+
try {
|
|
1101
|
+
await queue.submit({ mode: "steer", input: text2.trim(), idempotencyKey: createId() });
|
|
1102
|
+
await refreshMessages(threadId);
|
|
1103
|
+
} catch (error) {
|
|
1104
|
+
failOperation("submit", error);
|
|
1105
|
+
throw error;
|
|
1106
|
+
}
|
|
1107
|
+
}, [clearOperationError, createId, failOperation, queue.submit, refreshMessages, stateRef]);
|
|
1108
|
+
const editPendingMessage = React5.useCallback(async (message, input) => {
|
|
1109
|
+
await queue.edit(message, {
|
|
1110
|
+
text: input.text,
|
|
1111
|
+
attachmentIds: input.attachmentIds ?? message.attachment_ids,
|
|
1112
|
+
requestedAgentId: input.requestedAgentId,
|
|
1113
|
+
requestedModelId: input.requestedModelId
|
|
1114
|
+
});
|
|
1115
|
+
}, [queue.edit]);
|
|
1116
|
+
const steerPendingMessage = React5.useCallback(async (message) => {
|
|
1117
|
+
await queue.edit(message, { text: message.text, attachmentIds: message.attachment_ids, mode: "steer" });
|
|
1118
|
+
}, [queue.edit]);
|
|
1119
|
+
const resumeQueue = React5.useCallback(async () => {
|
|
1120
|
+
if (!stateRef.current.activeThreadId) return;
|
|
1121
|
+
const result = await queue.resume();
|
|
1122
|
+
if (result?.run_id) void attachRun(result.run_id).catch(() => void 0);
|
|
1123
|
+
}, [attachRun, queue.resume, stateRef]);
|
|
1124
|
+
return { steer, editPendingMessage, steerPendingMessage, removePendingMessage: queue.remove, resumeQueue };
|
|
1125
|
+
}
|
|
1126
|
+
|
|
710
1127
|
// src/initial-thread.ts
|
|
711
1128
|
function resolveInitialActiveThreadId(options, storedThreadId) {
|
|
712
1129
|
return Object.prototype.hasOwnProperty.call(options, "activeThreadId") ? options.activeThreadId ?? null : storedThreadId;
|
|
@@ -719,9 +1136,6 @@ function isCurrentThreadOpenRequest(input) {
|
|
|
719
1136
|
}
|
|
720
1137
|
|
|
721
1138
|
// src/use-agent-chat.ts
|
|
722
|
-
function summaryToStored(thread) {
|
|
723
|
-
return { ...thread, id: String(thread.id), messages: [], isHydrated: false };
|
|
724
|
-
}
|
|
725
1139
|
function activeRunIdFromThread(thread) {
|
|
726
1140
|
if (!thread) return null;
|
|
727
1141
|
const activity = getThreadActivity(thread);
|
|
@@ -732,7 +1146,7 @@ function useAgentChat(options = {}) {
|
|
|
732
1146
|
const client = options.client || runtime.client;
|
|
733
1147
|
const pageSize = options.pageSize ?? 30;
|
|
734
1148
|
const threadPageSize = options.threadPageSize ?? 50;
|
|
735
|
-
const [state, setState] =
|
|
1149
|
+
const [state, setState] = React6.useState(() => ({
|
|
736
1150
|
...initialAgentChatState,
|
|
737
1151
|
activeThreadId: resolveInitialActiveThreadId(
|
|
738
1152
|
options,
|
|
@@ -740,24 +1154,25 @@ function useAgentChat(options = {}) {
|
|
|
740
1154
|
),
|
|
741
1155
|
threads: runtime.storage?.loadThreads() ?? []
|
|
742
1156
|
}));
|
|
743
|
-
const [
|
|
744
|
-
const [
|
|
745
|
-
const [
|
|
746
|
-
const
|
|
747
|
-
const
|
|
748
|
-
const
|
|
749
|
-
const
|
|
750
|
-
const
|
|
751
|
-
const
|
|
752
|
-
const
|
|
753
|
-
const
|
|
754
|
-
const
|
|
1157
|
+
const [isLoadingHistory, setIsLoadingHistory] = React6.useState(false);
|
|
1158
|
+
const [isLoadingOlder, setIsLoadingOlder] = React6.useState(false);
|
|
1159
|
+
const [isSubmitting, setIsSubmitting] = React6.useState(false);
|
|
1160
|
+
const streamControllerRef = React6.useRef(null);
|
|
1161
|
+
const threadOpenControllerRef = React6.useRef(null);
|
|
1162
|
+
const threadOpenGenerationRef = React6.useRef(0);
|
|
1163
|
+
const attachRunRef = React6.useRef(async () => void 0);
|
|
1164
|
+
const messageRefreshVersionRef = React6.useRef(0);
|
|
1165
|
+
const promotedRunHintRef = React6.useRef(null);
|
|
1166
|
+
const promotedAttachRef = React6.useRef(null);
|
|
1167
|
+
const firstSubmissionRef = React6.useRef(null);
|
|
1168
|
+
const firstSubmissionAdmittedRef = React6.useRef(null);
|
|
1169
|
+
const stateRef = React6.useRef(state);
|
|
755
1170
|
stateRef.current = state;
|
|
756
|
-
const persist =
|
|
1171
|
+
const persist = React6.useCallback((next) => {
|
|
757
1172
|
runtime.storage?.saveThreads(next.threads);
|
|
758
1173
|
runtime.storage?.saveActiveThreadId?.(next.activeThreadId);
|
|
759
1174
|
}, [runtime.storage]);
|
|
760
|
-
const update =
|
|
1175
|
+
const update = React6.useCallback((updater) => {
|
|
761
1176
|
setState((current) => {
|
|
762
1177
|
const next = updater(current);
|
|
763
1178
|
stateRef.current = next;
|
|
@@ -765,116 +1180,121 @@ function useAgentChat(options = {}) {
|
|
|
765
1180
|
return next;
|
|
766
1181
|
});
|
|
767
1182
|
}, [persist]);
|
|
768
|
-
const
|
|
1183
|
+
const queueTransport = React6.useMemo(() => ({
|
|
1184
|
+
list: (threadId) => client.threads.messages.list({ threadId, limit: pageSize }),
|
|
1185
|
+
submit: (input) => client.threads.messages.submit(input),
|
|
1186
|
+
edit: (message, input) => client.threads.messages.edit({
|
|
1187
|
+
threadId: message.thread_id,
|
|
1188
|
+
messageId: message.id,
|
|
1189
|
+
revision: message.revision,
|
|
1190
|
+
input: input.text,
|
|
1191
|
+
attachmentIds: input.attachmentIds ?? message.attachment_ids,
|
|
1192
|
+
mode: input.mode,
|
|
1193
|
+
requestedAgentId: input.requestedAgentId,
|
|
1194
|
+
requestedModelId: input.requestedModelId
|
|
1195
|
+
}),
|
|
1196
|
+
remove: async (message) => {
|
|
1197
|
+
await client.threads.messages.remove({ threadId: message.thread_id, messageId: message.id, revision: message.revision });
|
|
1198
|
+
},
|
|
1199
|
+
resume: (threadId) => client.threads.resumeQueue({ threadId })
|
|
1200
|
+
}), [client, pageSize]);
|
|
1201
|
+
const onQueuePromotion = React6.useCallback((message, runId) => {
|
|
1202
|
+
if (stateRef.current.activeThreadId !== message.thread_id) return;
|
|
1203
|
+
promotedRunHintRef.current = runId;
|
|
1204
|
+
const userMessage = threadTimelineToMessages([{ ...message, run_id: runId, status: "delivered" }])[0];
|
|
1205
|
+
update((current) => current.activeThreadId === message.thread_id && !current.messages.some((item) => item.id === userMessage.id) ? { ...current, messages: [...current.messages, userMessage] } : current);
|
|
1206
|
+
if (stateRef.current.activeRunId !== runId && promotedAttachRef.current !== runId) {
|
|
1207
|
+
promotedAttachRef.current = runId;
|
|
1208
|
+
void attachRunRef.current(runId).catch(() => {
|
|
1209
|
+
promotedAttachRef.current = null;
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
}, [update]);
|
|
1213
|
+
const queue = useThreadMessageQueue({ threadId: state.activeThreadId, transport: queueTransport, onPromoted: onQueuePromotion });
|
|
1214
|
+
const { pendingMessages, messageQueue } = queue;
|
|
1215
|
+
const eventBuffer = React6.useMemo(
|
|
769
1216
|
() => createRuntimeEventFrameBuffer(
|
|
770
1217
|
(events) => update((current) => applyRuntimeEvents(current, events)),
|
|
771
1218
|
runtimeEventFrameScheduler()
|
|
772
1219
|
),
|
|
773
1220
|
[update]
|
|
774
1221
|
);
|
|
775
|
-
const clearOperationError =
|
|
1222
|
+
const clearOperationError = React6.useCallback((operation) => {
|
|
776
1223
|
update((current) => clearAgentChatOperationError(current, operation));
|
|
777
1224
|
}, [update]);
|
|
778
|
-
const clearBackgroundError =
|
|
1225
|
+
const clearBackgroundError = React6.useCallback(() => {
|
|
779
1226
|
update(clearAgentChatBackgroundError);
|
|
780
1227
|
}, [update]);
|
|
781
|
-
const failOperation =
|
|
1228
|
+
const failOperation = React6.useCallback((operation, cause) => {
|
|
782
1229
|
update((current) => withAgentChatOperationError(current, operation, cause));
|
|
783
1230
|
}, [update]);
|
|
784
|
-
const
|
|
1231
|
+
const {
|
|
1232
|
+
isLoadingThreads,
|
|
1233
|
+
isLoadingMoreThreads,
|
|
1234
|
+
threadTotal,
|
|
1235
|
+
eventsDuringRefreshRef: threadEventsDuringRefreshRef,
|
|
1236
|
+
refreshThreads,
|
|
1237
|
+
loadMoreThreads
|
|
1238
|
+
} = useAgentChatThreadList({ client, threadPageSize, stateRef, update, clearOperationError, failOperation });
|
|
1239
|
+
const invalidateThreadOpen = React6.useCallback(() => {
|
|
785
1240
|
threadOpenGenerationRef.current += 1;
|
|
786
1241
|
threadOpenControllerRef.current?.abort("Thread selection changed.");
|
|
787
1242
|
threadOpenControllerRef.current = null;
|
|
788
1243
|
setIsLoadingHistory(false);
|
|
789
1244
|
}, []);
|
|
790
|
-
const detach =
|
|
1245
|
+
const detach = React6.useCallback(() => {
|
|
791
1246
|
eventBuffer.flush();
|
|
792
1247
|
streamControllerRef.current?.abort();
|
|
793
1248
|
streamControllerRef.current = null;
|
|
794
1249
|
update((current) => ({ ...current, streamingMessageId: null, runState: current.activeRunId ? "reconnecting" : "idle" }));
|
|
795
1250
|
}, [eventBuffer, update]);
|
|
796
|
-
|
|
1251
|
+
const refreshMessages = React6.useCallback(async (threadId, promotedRunId) => {
|
|
1252
|
+
const target = threadId || stateRef.current.activeThreadId;
|
|
1253
|
+
if (!target) return;
|
|
1254
|
+
const version = ++messageRefreshVersionRef.current;
|
|
1255
|
+
const page = await client.threads.messages.list({ threadId: target, limit: pageSize });
|
|
1256
|
+
if (stateRef.current.activeThreadId !== target || version !== messageRefreshVersionRef.current) return;
|
|
1257
|
+
queue.hydrate(page, target);
|
|
1258
|
+
const promoted = promotedRunId || promotedRunHintRef.current;
|
|
1259
|
+
if (promoted && page.items.some((message) => message.role === "user" && message.run_id === promoted)) {
|
|
1260
|
+
promotedRunHintRef.current = null;
|
|
1261
|
+
update((current) => {
|
|
1262
|
+
if (current.activeThreadId !== target) return current;
|
|
1263
|
+
const canonical = preserveMountedMessageIds(threadTimelineToMessages(page.items), current.messages);
|
|
1264
|
+
const canonicalIds = new Set(canonical.map((message) => message.id));
|
|
1265
|
+
const older = current.messages.filter((message) => !canonicalIds.has(message.id) && message.id !== current.streamingMessageId);
|
|
1266
|
+
const live = current.activeRunId === promoted ? current.messages.filter((message) => message.id === current.streamingMessageId) : [];
|
|
1267
|
+
return {
|
|
1268
|
+
...current,
|
|
1269
|
+
messages: [...older, ...canonical, ...live.filter((message) => !canonicalIds.has(message.id))],
|
|
1270
|
+
streamingMessageId: live.length ? current.streamingMessageId : null
|
|
1271
|
+
};
|
|
1272
|
+
});
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
if (stateRef.current.activeRunId) {
|
|
1276
|
+
const delivered = threadTimelineToMessages(page.items.filter((item) => item.role === "user" && item.run_id === stateRef.current.activeRunId && (item.segment_index ?? 0) > 0));
|
|
1277
|
+
if (delivered.length) update((current) => {
|
|
1278
|
+
if (current.activeThreadId !== target) return current;
|
|
1279
|
+
const additions = delivered.filter((item) => !current.messages.some((existing) => existing.id === item.id));
|
|
1280
|
+
return additions.length ? { ...current, messages: [...current.messages, ...additions] } : current;
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
if (!stateRef.current.activeRunId && page.items.length) {
|
|
1284
|
+
update((current) => current.activeThreadId === target && !current.activeRunId ? (() => {
|
|
1285
|
+
const messages = preserveMountedMessageIds(threadTimelineToMessages(page.items), current.messages);
|
|
1286
|
+
return { ...current, messages, threads: current.threads.map((thread) => thread.id === target ? { ...thread, messages } : thread) };
|
|
1287
|
+
})() : current);
|
|
1288
|
+
}
|
|
1289
|
+
}, [client, pageSize, queue.hydrate, update]);
|
|
1290
|
+
React6.useEffect(() => () => {
|
|
797
1291
|
eventBuffer.dispose();
|
|
798
1292
|
streamControllerRef.current?.abort();
|
|
799
1293
|
threadOpenControllerRef.current?.abort();
|
|
800
1294
|
}, [eventBuffer]);
|
|
801
|
-
const
|
|
802
|
-
if (refreshThreadsPromiseRef.current) return refreshThreadsPromiseRef.current;
|
|
803
|
-
clearOperationError();
|
|
804
|
-
setIsLoadingThreads(true);
|
|
805
|
-
const request = (async () => {
|
|
806
|
-
try {
|
|
807
|
-
const result = await client.threads.list({ limit: threadPageSize });
|
|
808
|
-
setThreadTotal(result.total);
|
|
809
|
-
update((current) => {
|
|
810
|
-
const byId = new Map(current.threads.map((thread) => [thread.id, thread]));
|
|
811
|
-
const nextThreads = result.items.map((item) => {
|
|
812
|
-
const existing = byId.get(item.id);
|
|
813
|
-
return { ...existing, ...summaryToStored(item), messages: existing?.messages || [], isHydrated: existing?.isHydrated };
|
|
814
|
-
});
|
|
815
|
-
return {
|
|
816
|
-
...current,
|
|
817
|
-
threads: reconcileRefreshedThreads(
|
|
818
|
-
current.threads,
|
|
819
|
-
nextThreads,
|
|
820
|
-
current.activeThreadId
|
|
821
|
-
)
|
|
822
|
-
};
|
|
823
|
-
});
|
|
824
|
-
} catch (error) {
|
|
825
|
-
if (!isAbortLike(error)) {
|
|
826
|
-
failOperation("refresh_threads", error);
|
|
827
|
-
throw error;
|
|
828
|
-
}
|
|
829
|
-
} finally {
|
|
830
|
-
setIsLoadingThreads(false);
|
|
831
|
-
}
|
|
832
|
-
})();
|
|
833
|
-
refreshThreadsPromiseRef.current = request;
|
|
834
|
-
void request.then(
|
|
835
|
-
() => {
|
|
836
|
-
if (refreshThreadsPromiseRef.current === request) refreshThreadsPromiseRef.current = null;
|
|
837
|
-
},
|
|
838
|
-
() => {
|
|
839
|
-
if (refreshThreadsPromiseRef.current === request) refreshThreadsPromiseRef.current = null;
|
|
840
|
-
}
|
|
841
|
-
);
|
|
842
|
-
return request;
|
|
843
|
-
}, [clearOperationError, client, failOperation, threadPageSize, update]);
|
|
844
|
-
const loadMoreThreads = React2.useCallback(async () => {
|
|
845
|
-
if (isLoadingMoreThreads) return;
|
|
846
|
-
const skip = stateRef.current.threads.length;
|
|
847
|
-
if (threadTotal !== void 0 && skip >= threadTotal) return;
|
|
848
|
-
clearOperationError();
|
|
849
|
-
setIsLoadingMoreThreads(true);
|
|
850
|
-
try {
|
|
851
|
-
const result = await client.threads.list({ skip, limit: threadPageSize });
|
|
852
|
-
setThreadTotal(result.total);
|
|
853
|
-
update((current) => {
|
|
854
|
-
const byId = new Map(current.threads.map((thread) => [thread.id, thread]));
|
|
855
|
-
for (const item of result.items) {
|
|
856
|
-
const existing = byId.get(item.id);
|
|
857
|
-
byId.set(item.id, {
|
|
858
|
-
...existing,
|
|
859
|
-
...summaryToStored(item),
|
|
860
|
-
messages: existing?.messages || [],
|
|
861
|
-
isHydrated: existing?.isHydrated
|
|
862
|
-
});
|
|
863
|
-
}
|
|
864
|
-
return { ...current, threads: [...byId.values()] };
|
|
865
|
-
});
|
|
866
|
-
} catch (error) {
|
|
867
|
-
if (!isAbortLike(error)) {
|
|
868
|
-
failOperation("load_more_threads", error);
|
|
869
|
-
throw error;
|
|
870
|
-
}
|
|
871
|
-
} finally {
|
|
872
|
-
setIsLoadingMoreThreads(false);
|
|
873
|
-
}
|
|
874
|
-
}, [clearOperationError, client, failOperation, isLoadingMoreThreads, threadPageSize, threadTotal, update]);
|
|
875
|
-
const openThread = React2.useCallback(async (threadId) => {
|
|
1295
|
+
const openThread = React6.useCallback(async (threadId) => {
|
|
876
1296
|
const previousSelection = captureAgentChatThreadSelection(stateRef.current);
|
|
877
|
-
threadOpenControllerRef.current?.abort(
|
|
1297
|
+
threadOpenControllerRef.current?.abort();
|
|
878
1298
|
const requestController = runtime.createAbortController();
|
|
879
1299
|
threadOpenControllerRef.current = requestController;
|
|
880
1300
|
const requestGeneration = threadOpenGenerationRef.current + 1;
|
|
@@ -908,7 +1328,16 @@ function useAgentChat(options = {}) {
|
|
|
908
1328
|
requestedThreadId: threadId,
|
|
909
1329
|
signalAborted: requestController.signal.aborted
|
|
910
1330
|
})) return;
|
|
911
|
-
const
|
|
1331
|
+
const timeline = await client.threads.messages.list({ threadId, limit: pageSize, signal: requestController.signal });
|
|
1332
|
+
if (!isCurrentThreadOpenRequest({
|
|
1333
|
+
activeThreadId: stateRef.current.activeThreadId,
|
|
1334
|
+
currentGeneration: threadOpenGenerationRef.current,
|
|
1335
|
+
requestGeneration,
|
|
1336
|
+
requestedThreadId: threadId,
|
|
1337
|
+
signalAborted: requestController.signal.aborted
|
|
1338
|
+
})) return;
|
|
1339
|
+
queue.hydrate(timeline, threadId);
|
|
1340
|
+
const messages = timeline.items.length ? threadTimelineToMessages(timeline.items, detail) : threadDetailToMessages(detail);
|
|
912
1341
|
const context = latestThreadContext(detail);
|
|
913
1342
|
const stored = {
|
|
914
1343
|
...detail,
|
|
@@ -942,7 +1371,7 @@ function useAgentChat(options = {}) {
|
|
|
942
1371
|
signalAborted: requestController.signal.aborted
|
|
943
1372
|
})) await attachRunRef.current(activeRunId).catch(() => void 0);
|
|
944
1373
|
} catch (error) {
|
|
945
|
-
if (!isAbortLike(error) && requestGeneration === threadOpenGenerationRef.current) {
|
|
1374
|
+
if (!requestController.signal.aborted && !isAbortLike(error) && requestGeneration === threadOpenGenerationRef.current) {
|
|
946
1375
|
failOperation("open_thread", error);
|
|
947
1376
|
update((current) => restoreAgentChatThreadSelection(current, previousSelection));
|
|
948
1377
|
if (previousSelection.activeRunId) {
|
|
@@ -956,15 +1385,23 @@ function useAgentChat(options = {}) {
|
|
|
956
1385
|
setIsLoadingHistory(false);
|
|
957
1386
|
}
|
|
958
1387
|
}
|
|
959
|
-
}, [clearOperationError, client, detach, failOperation, pageSize, runtime, update]);
|
|
960
|
-
const consumeEvent =
|
|
1388
|
+
}, [clearOperationError, client, detach, failOperation, pageSize, queue.hydrate, runtime, update]);
|
|
1389
|
+
const consumeEvent = React6.useCallback((event) => {
|
|
1390
|
+
if (event.event === "run.accepted" && !stateRef.current.activeThreadId && typeof event.payload?.message_id === "string") {
|
|
1391
|
+
const durableId = event.payload.message_id;
|
|
1392
|
+
update((current) => ({
|
|
1393
|
+
...current,
|
|
1394
|
+
messages: current.messages.map((message) => message.role === "user" && !message.runId && !message.durableId ? { ...message, durableId, runId: event.run_id } : message.role === "assistant" && !message.runId ? { ...message, runId: event.run_id } : message)
|
|
1395
|
+
}));
|
|
1396
|
+
}
|
|
961
1397
|
const eventThreadId = typeof event.payload?.thread_id === "string" ? event.payload.thread_id.trim() : "";
|
|
962
1398
|
if (eventThreadId && !stateRef.current.activeThreadId) {
|
|
963
1399
|
update((current) => adoptStreamingThread(current, eventThreadId));
|
|
1400
|
+
firstSubmissionAdmittedRef.current?.();
|
|
964
1401
|
}
|
|
965
1402
|
eventBuffer.push(event);
|
|
966
1403
|
}, [eventBuffer, update]);
|
|
967
|
-
const attachRun =
|
|
1404
|
+
const attachRun = React6.useCallback(async (runId, cursor) => {
|
|
968
1405
|
streamControllerRef.current?.abort();
|
|
969
1406
|
const controller = runtime.createAbortController();
|
|
970
1407
|
streamControllerRef.current = controller;
|
|
@@ -972,7 +1409,9 @@ function useAgentChat(options = {}) {
|
|
|
972
1409
|
update((current) => ({ ...current, activeRunId: runId, runState: "reconnecting", error: null }));
|
|
973
1410
|
try {
|
|
974
1411
|
const result = await client.runs.attach({ runId, cursor, signal: controller.signal }, consumeEvent);
|
|
1412
|
+
eventBuffer.flush();
|
|
975
1413
|
update((current) => ({ ...current, activeRunId: result.detached ? runId : current.activeRunId, cursor: result.cursor ?? current.cursor, runState: result.detached ? "reconnecting" : current.runState }));
|
|
1414
|
+
if (!result.detached && result.threadId) await refreshMessages(result.threadId);
|
|
976
1415
|
} catch (error) {
|
|
977
1416
|
if (!isAbortLike(error)) {
|
|
978
1417
|
update((current) => ({ ...current, runState: "failed", streamingMessageId: null }));
|
|
@@ -982,100 +1421,171 @@ function useAgentChat(options = {}) {
|
|
|
982
1421
|
} finally {
|
|
983
1422
|
if (streamControllerRef.current === controller) streamControllerRef.current = null;
|
|
984
1423
|
}
|
|
985
|
-
}, [clearOperationError, client, consumeEvent, failOperation, runtime, update]);
|
|
1424
|
+
}, [clearOperationError, client, consumeEvent, eventBuffer, failOperation, refreshMessages, runtime, update]);
|
|
986
1425
|
attachRunRef.current = attachRun;
|
|
987
|
-
const submit =
|
|
1426
|
+
const submit = React6.useCallback(async (input) => {
|
|
988
1427
|
if (!input.text.trim() && !input.attachmentIds?.length && !input.uploads?.length) return;
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
...current,
|
|
1031
|
-
messages: current.messages.map((message) => message.id === userId ? { ...message, attachments: resolvedAttachments } : message),
|
|
1032
|
-
runState: "streaming"
|
|
1033
|
-
}));
|
|
1034
|
-
}
|
|
1035
|
-
streamStarted = true;
|
|
1036
|
-
const result = await client.chat.stream({ input: input.text.trim(), threadId: stateRef.current.activeThreadId || void 0, attachmentIds, requestedModelId: input.requestedModelId, requestedAgentId: input.requestedAgentId, idempotencyKey, signal: controller.signal }, consumeEvent);
|
|
1037
|
-
eventBuffer.flush();
|
|
1038
|
-
update((current) => {
|
|
1039
|
-
const threadId = result.threadId || current.activeThreadId;
|
|
1040
|
-
return {
|
|
1041
|
-
...current,
|
|
1042
|
-
activeThreadId: threadId,
|
|
1043
|
-
activeRunId: result.detached ? result.runId : current.activeRunId,
|
|
1044
|
-
cursor: result.cursor,
|
|
1045
|
-
runState: result.detached ? "reconnecting" : current.runState,
|
|
1046
|
-
threads: threadId ? current.threads.map((thread) => thread.id === threadId ? { ...thread, messages: current.messages, isHydrated: true } : thread) : current.threads
|
|
1047
|
-
};
|
|
1048
|
-
});
|
|
1049
|
-
if (result.threadId) await refreshThreads().catch(() => void 0);
|
|
1050
|
-
} catch (error) {
|
|
1051
|
-
if (isAbortLike(error) && !streamStarted) {
|
|
1052
|
-
update((current) => current.streamingMessageId === assistantId ? {
|
|
1053
|
-
...current,
|
|
1054
|
-
messages: current.messages.filter((message) => message.id !== userId && message.id !== assistantId),
|
|
1055
|
-
runState: "idle",
|
|
1056
|
-
streamingMessageId: null
|
|
1057
|
-
} : current);
|
|
1058
|
-
} else if (!isAbortLike(error)) {
|
|
1059
|
-
update((current) => ({
|
|
1060
|
-
...current,
|
|
1061
|
-
messages: streamStarted ? current.messages : current.messages.filter((message) => message.id !== userId && message.id !== assistantId),
|
|
1062
|
-
runState: "failed",
|
|
1063
|
-
streamingMessageId: null
|
|
1064
|
-
}));
|
|
1428
|
+
if (firstSubmissionRef.current && !stateRef.current.activeThreadId) {
|
|
1429
|
+
await firstSubmissionRef.current;
|
|
1430
|
+
}
|
|
1431
|
+
const existingThreadId = stateRef.current.activeThreadId;
|
|
1432
|
+
if (existingThreadId) {
|
|
1433
|
+
clearOperationError();
|
|
1434
|
+
setIsSubmitting(true);
|
|
1435
|
+
try {
|
|
1436
|
+
const uploaded = input.uploads?.length ? await client.attachments.uploadBatch({
|
|
1437
|
+
uploads: input.uploads,
|
|
1438
|
+
threadId: existingThreadId,
|
|
1439
|
+
requestedAgentId: input.requestedAgentId,
|
|
1440
|
+
idempotencyKey: runtime.createId()
|
|
1441
|
+
}) : [];
|
|
1442
|
+
const result = await queue.submit({
|
|
1443
|
+
mode: "queue",
|
|
1444
|
+
input: input.text.trim(),
|
|
1445
|
+
attachmentIds: uploaded.length ? uploaded.map((item) => item.id) : input.attachmentIds,
|
|
1446
|
+
requestedAgentId: input.requestedAgentId,
|
|
1447
|
+
requestedModelId: input.requestedModelId,
|
|
1448
|
+
idempotencyKey: runtime.createId()
|
|
1449
|
+
});
|
|
1450
|
+
if (stateRef.current.activeThreadId !== existingThreadId) return;
|
|
1451
|
+
if (result.run_id) {
|
|
1452
|
+
const userMessage = {
|
|
1453
|
+
id: result.message_id,
|
|
1454
|
+
role: "user",
|
|
1455
|
+
runId: result.run_id,
|
|
1456
|
+
content: input.text.trim(),
|
|
1457
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1458
|
+
isFinal: true,
|
|
1459
|
+
parts: input.text.trim() ? [{ id: `${result.message_id}-text`, type: "text", kind: "text", text: input.text.trim() }] : [],
|
|
1460
|
+
attachments: input.attachments || []
|
|
1461
|
+
};
|
|
1462
|
+
update((current) => current.activeThreadId !== existingThreadId || current.messages.some((message) => message.id === userMessage.id) ? current : { ...current, messages: [...current.messages, userMessage] });
|
|
1463
|
+
}
|
|
1464
|
+
void refreshMessages(existingThreadId).catch(() => void 0);
|
|
1465
|
+
if (result.run_id && promotedAttachRef.current !== result.run_id && stateRef.current.activeRunId !== result.run_id) {
|
|
1466
|
+
void attachRunRef.current(result.run_id).catch(() => void 0);
|
|
1467
|
+
}
|
|
1468
|
+
} catch (error) {
|
|
1065
1469
|
failOperation("submit", error);
|
|
1066
1470
|
throw error;
|
|
1471
|
+
} finally {
|
|
1472
|
+
setIsSubmitting(false);
|
|
1473
|
+
}
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
const admission = new Promise((resolve) => {
|
|
1477
|
+
firstSubmissionAdmittedRef.current = resolve;
|
|
1478
|
+
});
|
|
1479
|
+
const stream = (async () => {
|
|
1480
|
+
invalidateThreadOpen();
|
|
1481
|
+
detach();
|
|
1482
|
+
const controller = runtime.createAbortController();
|
|
1483
|
+
streamControllerRef.current = controller;
|
|
1484
|
+
const userId = runtime.createId();
|
|
1485
|
+
const assistantId = runtime.createId();
|
|
1486
|
+
const idempotencyKey = runtime.createId();
|
|
1487
|
+
const optimisticAttachments = input.attachments?.length ? input.attachments : input.uploads?.map((upload, index) => ({
|
|
1488
|
+
id: `${userId}-attachment-${index}`,
|
|
1489
|
+
filename: upload.name,
|
|
1490
|
+
mediaType: upload.mediaType,
|
|
1491
|
+
status: "uploading"
|
|
1492
|
+
}));
|
|
1493
|
+
const userMessage = { id: userId, role: "user", content: input.text.trim(), createdAt: (/* @__PURE__ */ new Date()).toISOString(), parts: input.text.trim() ? [{ id: `${userId}-text`, type: "text", kind: "text", text: input.text.trim() }] : [], attachments: optimisticAttachments };
|
|
1494
|
+
const assistantMessage = { id: assistantId, role: "assistant", content: "", createdAt: (/* @__PURE__ */ new Date()).toISOString(), isFinal: false, parts: [] };
|
|
1495
|
+
clearOperationError();
|
|
1496
|
+
const hasPendingUploads = Boolean(input.uploads?.length);
|
|
1497
|
+
update((current) => ({
|
|
1498
|
+
...current,
|
|
1499
|
+
messages: appendStableStreamingTurn(current.messages, userMessage, assistantMessage),
|
|
1500
|
+
streamingMessageId: assistantId,
|
|
1501
|
+
runState: hasPendingUploads ? "uploading" : "streaming",
|
|
1502
|
+
error: null
|
|
1503
|
+
}));
|
|
1504
|
+
setIsSubmitting(true);
|
|
1505
|
+
let streamStarted = false;
|
|
1506
|
+
try {
|
|
1507
|
+
const uploaded = hasPendingUploads ? await client.attachments.uploadBatch({
|
|
1508
|
+
uploads: input.uploads || [],
|
|
1509
|
+
threadId: stateRef.current.activeThreadId || void 0,
|
|
1510
|
+
requestedAgentId: input.requestedAgentId,
|
|
1511
|
+
idempotencyKey: runtime.createId(),
|
|
1512
|
+
signal: controller.signal
|
|
1513
|
+
}) : [];
|
|
1514
|
+
if (hasPendingUploads && uploaded.length !== input.uploads?.length) {
|
|
1515
|
+
throw new Error("Attachment upload returned an incomplete batch");
|
|
1516
|
+
}
|
|
1517
|
+
const resolvedAttachments = uploaded.length ? uploaded.map(attachmentResultToChatAttachment) : input.attachments;
|
|
1518
|
+
const attachmentIds = uploaded.length ? uploaded.map((attachment) => attachment.id) : input.attachmentIds;
|
|
1519
|
+
if (uploaded.length) {
|
|
1520
|
+
update((current) => ({
|
|
1521
|
+
...current,
|
|
1522
|
+
messages: current.messages.map((message) => message.id === userId ? { ...message, attachments: resolvedAttachments } : message),
|
|
1523
|
+
runState: "streaming"
|
|
1524
|
+
}));
|
|
1525
|
+
}
|
|
1526
|
+
streamStarted = true;
|
|
1527
|
+
const result = await client.chat.stream({ input: input.text.trim(), threadId: stateRef.current.activeThreadId || void 0, attachmentIds, requestedModelId: input.requestedModelId, requestedAgentId: input.requestedAgentId, idempotencyKey, signal: controller.signal }, consumeEvent);
|
|
1528
|
+
eventBuffer.flush();
|
|
1529
|
+
update((current) => {
|
|
1530
|
+
const threadId = result.threadId || current.activeThreadId;
|
|
1531
|
+
return {
|
|
1532
|
+
...current,
|
|
1533
|
+
activeThreadId: threadId,
|
|
1534
|
+
activeRunId: result.detached ? result.runId : current.activeRunId,
|
|
1535
|
+
cursor: result.cursor,
|
|
1536
|
+
runState: result.detached ? "reconnecting" : current.runState,
|
|
1537
|
+
threads: threadId ? current.threads.map((thread) => thread.id === threadId ? { ...thread, messages: current.messages, isHydrated: true } : thread) : current.threads
|
|
1538
|
+
};
|
|
1539
|
+
});
|
|
1540
|
+
if (result.threadId) await refreshThreads().catch(() => void 0);
|
|
1541
|
+
if (result.threadId && !result.detached) await refreshMessages(result.threadId);
|
|
1542
|
+
} catch (error) {
|
|
1543
|
+
if (isAbortLike(error) && !streamStarted) {
|
|
1544
|
+
update((current) => current.streamingMessageId === assistantId ? {
|
|
1545
|
+
...current,
|
|
1546
|
+
messages: current.messages.filter((message) => message.id !== userId && message.id !== assistantId),
|
|
1547
|
+
runState: "idle",
|
|
1548
|
+
streamingMessageId: null
|
|
1549
|
+
} : current);
|
|
1550
|
+
} else if (!isAbortLike(error)) {
|
|
1551
|
+
update((current) => ({
|
|
1552
|
+
...current,
|
|
1553
|
+
messages: streamStarted ? current.messages : current.messages.filter((message) => message.id !== userId && message.id !== assistantId),
|
|
1554
|
+
runState: "failed",
|
|
1555
|
+
streamingMessageId: null
|
|
1556
|
+
}));
|
|
1557
|
+
failOperation("submit", error);
|
|
1558
|
+
throw error;
|
|
1559
|
+
}
|
|
1560
|
+
} finally {
|
|
1561
|
+
setIsSubmitting(false);
|
|
1562
|
+
if (streamControllerRef.current === controller) streamControllerRef.current = null;
|
|
1067
1563
|
}
|
|
1564
|
+
})();
|
|
1565
|
+
const accepted = Promise.race([stream, admission]);
|
|
1566
|
+
firstSubmissionRef.current = accepted;
|
|
1567
|
+
try {
|
|
1568
|
+
await accepted;
|
|
1068
1569
|
} finally {
|
|
1069
|
-
|
|
1070
|
-
|
|
1570
|
+
if (firstSubmissionRef.current === accepted) firstSubmissionRef.current = null;
|
|
1571
|
+
firstSubmissionAdmittedRef.current = null;
|
|
1071
1572
|
}
|
|
1072
|
-
}, [clearOperationError, client, consumeEvent, detach, eventBuffer, failOperation, invalidateThreadOpen, refreshThreads, runtime, update]);
|
|
1073
|
-
const
|
|
1573
|
+
}, [clearOperationError, client, consumeEvent, detach, eventBuffer, failOperation, invalidateThreadOpen, queue.submit, refreshMessages, refreshThreads, runtime, update]);
|
|
1574
|
+
const { steer, editPendingMessage, steerPendingMessage, removePendingMessage, resumeQueue } = useAgentChatQueueActions({
|
|
1575
|
+
queue,
|
|
1576
|
+
stateRef,
|
|
1577
|
+
createId: runtime.createId,
|
|
1578
|
+
clearOperationError,
|
|
1579
|
+
failOperation,
|
|
1580
|
+
refreshMessages,
|
|
1581
|
+
attachRun
|
|
1582
|
+
});
|
|
1583
|
+
const startNewThread = React6.useCallback(() => {
|
|
1074
1584
|
invalidateThreadOpen();
|
|
1075
1585
|
detach();
|
|
1076
1586
|
update((current) => ({ ...initialAgentChatState, threads: current.threads }));
|
|
1077
1587
|
}, [detach, invalidateThreadOpen, update]);
|
|
1078
|
-
const deleteThread =
|
|
1588
|
+
const deleteThread = React6.useCallback(async (threadId) => {
|
|
1079
1589
|
if (stateRef.current.activeThreadId === threadId) invalidateThreadOpen();
|
|
1080
1590
|
clearOperationError();
|
|
1081
1591
|
try {
|
|
@@ -1096,7 +1606,7 @@ function useAgentChat(options = {}) {
|
|
|
1096
1606
|
}
|
|
1097
1607
|
}
|
|
1098
1608
|
}, [clearOperationError, client, failOperation, invalidateThreadOpen, runtime, update]);
|
|
1099
|
-
const cancelRun =
|
|
1609
|
+
const cancelRun = React6.useCallback(async () => {
|
|
1100
1610
|
const runId = stateRef.current.activeRunId;
|
|
1101
1611
|
if (!runId || stateRef.current.runState === "cancelling") return;
|
|
1102
1612
|
const previousRunState = stateRef.current.runState;
|
|
@@ -1135,7 +1645,7 @@ function useAgentChat(options = {}) {
|
|
|
1135
1645
|
}
|
|
1136
1646
|
}
|
|
1137
1647
|
}, [clearOperationError, client, detach, failOperation, runtime, update]);
|
|
1138
|
-
const resumeHitl =
|
|
1648
|
+
const resumeHitl = React6.useCallback(async (part, response) => {
|
|
1139
1649
|
const runId = stateRef.current.activeRunId || stateRef.current.messages.find((message) => message.parts.includes(part))?.runId;
|
|
1140
1650
|
clearOperationError();
|
|
1141
1651
|
if (!runId) {
|
|
@@ -1156,54 +1666,8 @@ function useAgentChat(options = {}) {
|
|
|
1156
1666
|
throw error;
|
|
1157
1667
|
}
|
|
1158
1668
|
}, [attachRun, clearOperationError, client, failOperation, runtime]);
|
|
1159
|
-
const setFeedback =
|
|
1160
|
-
|
|
1161
|
-
if (!runId || message.role !== "assistant" || message.isFinal === false) {
|
|
1162
|
-
const error = new Error("Only persisted final responses can receive feedback.");
|
|
1163
|
-
failOperation("set_feedback", error);
|
|
1164
|
-
throw error;
|
|
1165
|
-
}
|
|
1166
|
-
const previous = message.feedback ?? null;
|
|
1167
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1168
|
-
const optimistic = input.rating ? {
|
|
1169
|
-
rating: input.rating,
|
|
1170
|
-
reason: input.rating === "dislike" ? input.reason ?? null : null,
|
|
1171
|
-
comment: input.rating === "dislike" ? input.comment?.trim() || null : null,
|
|
1172
|
-
created_at: previous?.created_at || timestamp,
|
|
1173
|
-
updated_at: timestamp
|
|
1174
|
-
} : null;
|
|
1175
|
-
const replaceFeedback = (current, feedback) => {
|
|
1176
|
-
const messages = current.messages.map(
|
|
1177
|
-
(item) => item.id === message.id ? { ...item, feedback } : item
|
|
1178
|
-
);
|
|
1179
|
-
return {
|
|
1180
|
-
...current,
|
|
1181
|
-
messages,
|
|
1182
|
-
threads: current.threads.map((thread) => ({
|
|
1183
|
-
...thread,
|
|
1184
|
-
messages: thread.messages.map(
|
|
1185
|
-
(item) => item.id === message.id ? { ...item, feedback } : item
|
|
1186
|
-
)
|
|
1187
|
-
}))
|
|
1188
|
-
};
|
|
1189
|
-
};
|
|
1190
|
-
clearOperationError();
|
|
1191
|
-
update((current) => replaceFeedback(current, optimistic));
|
|
1192
|
-
try {
|
|
1193
|
-
const result = await client.runs.setFeedback({ runId, ...input });
|
|
1194
|
-
update((current) => replaceFeedback(current, result.feedback));
|
|
1195
|
-
} catch (error) {
|
|
1196
|
-
update(
|
|
1197
|
-
(current) => withAgentChatOperationError(
|
|
1198
|
-
replaceFeedback(current, previous),
|
|
1199
|
-
"set_feedback",
|
|
1200
|
-
error
|
|
1201
|
-
)
|
|
1202
|
-
);
|
|
1203
|
-
throw error;
|
|
1204
|
-
}
|
|
1205
|
-
}, [clearOperationError, client, failOperation, update]);
|
|
1206
|
-
const loadOlder = React2.useCallback(async () => {
|
|
1669
|
+
const setFeedback = useAgentChatFeedback({ client, update, clearOperationError, failOperation });
|
|
1670
|
+
const loadOlder = React6.useCallback(async () => {
|
|
1207
1671
|
const thread = stateRef.current.threads.find((item) => item.id === stateRef.current.activeThreadId);
|
|
1208
1672
|
if (!thread?.nextBeforeTurnIndex) return;
|
|
1209
1673
|
clearOperationError();
|
|
@@ -1221,7 +1685,7 @@ function useAgentChat(options = {}) {
|
|
|
1221
1685
|
setIsLoadingOlder(false);
|
|
1222
1686
|
}
|
|
1223
1687
|
}, [clearOperationError, client, failOperation, pageSize, update]);
|
|
1224
|
-
const regenerate =
|
|
1688
|
+
const regenerate = React6.useCallback(async (message) => {
|
|
1225
1689
|
const index = stateRef.current.messages.findIndex((item) => item.id === message.id);
|
|
1226
1690
|
const user = stateRef.current.messages.slice(0, index).reverse().find((item) => item.role === "user");
|
|
1227
1691
|
if (user) await submit({
|
|
@@ -1230,25 +1694,50 @@ function useAgentChat(options = {}) {
|
|
|
1230
1694
|
attachments: user.attachments
|
|
1231
1695
|
});
|
|
1232
1696
|
}, [submit]);
|
|
1233
|
-
|
|
1697
|
+
React6.useEffect(() => {
|
|
1234
1698
|
const controller = runtime.createAbortController();
|
|
1235
1699
|
clearBackgroundError();
|
|
1236
|
-
void
|
|
1237
|
-
|
|
1700
|
+
void subscribeToThreadSummaries({
|
|
1701
|
+
client,
|
|
1702
|
+
signal: controller.signal,
|
|
1703
|
+
refresh: refreshThreads,
|
|
1704
|
+
onEvent(event) {
|
|
1705
|
+
threadEventsDuringRefreshRef.current?.push(event);
|
|
1706
|
+
update((current) => ({
|
|
1707
|
+
...clearAgentChatBackgroundError(current),
|
|
1708
|
+
threads: applyThreadSummaryEvent(current.threads, event)
|
|
1709
|
+
}));
|
|
1710
|
+
if (event.event === "thread.message.updated" && event.thread_id === stateRef.current.activeThreadId) {
|
|
1711
|
+
const wasQueued = queue.acceptNotice({
|
|
1712
|
+
messageId: event.message_id,
|
|
1713
|
+
status: event.status,
|
|
1714
|
+
runId: event.run_id,
|
|
1715
|
+
cursor: event.cursor
|
|
1716
|
+
});
|
|
1717
|
+
if (wasQueued) void refreshMessages(event.thread_id, event.run_id || void 0).catch(() => void 0);
|
|
1718
|
+
}
|
|
1719
|
+
if (event.event === "thread.summary.updated" && event.thread.id === stateRef.current.activeThreadId) {
|
|
1720
|
+
if (streamControllerRef.current && stateRef.current.messages.some((message) => message.role === "user" && !message.runId)) return;
|
|
1721
|
+
const promotedRunId = activeRunIdFromThread(summaryToStored(event.thread));
|
|
1722
|
+
void (async () => {
|
|
1723
|
+
await refreshMessages(String(event.thread.id), promotedRunId || void 0);
|
|
1724
|
+
if (promotedRunId && stateRef.current.activeRunId !== promotedRunId && stateRef.current.activeThreadId === String(event.thread.id) && promotedAttachRef.current !== promotedRunId) {
|
|
1725
|
+
promotedAttachRef.current = promotedRunId;
|
|
1726
|
+
await attachRunRef.current(promotedRunId);
|
|
1727
|
+
}
|
|
1728
|
+
})().catch(() => void 0);
|
|
1729
|
+
}
|
|
1730
|
+
},
|
|
1731
|
+
onError(error) {
|
|
1732
|
+
update((current) => withAgentChatBackgroundError(current, error));
|
|
1733
|
+
},
|
|
1734
|
+
onRecovered() {
|
|
1238
1735
|
clearBackgroundError();
|
|
1239
|
-
await refreshThreads();
|
|
1240
|
-
return;
|
|
1241
1736
|
}
|
|
1242
|
-
update((current) => ({
|
|
1243
|
-
...clearAgentChatBackgroundError(current),
|
|
1244
|
-
threads: applyThreadSummaryEvent(current.threads, event)
|
|
1245
|
-
}));
|
|
1246
|
-
}).catch((error) => {
|
|
1247
|
-
if (!isAbortLike(error)) update((current) => withAgentChatBackgroundError(current, error));
|
|
1248
1737
|
});
|
|
1249
1738
|
return () => controller.abort();
|
|
1250
|
-
}, [clearBackgroundError, client, refreshThreads, runtime.createAbortController, update]);
|
|
1251
|
-
|
|
1739
|
+
}, [clearBackgroundError, client, queue.acceptNotice, refreshMessages, refreshThreads, runtime.createAbortController, update]);
|
|
1740
|
+
React6.useEffect(() => {
|
|
1252
1741
|
const threadId = options.activeThreadId;
|
|
1253
1742
|
const activeThread2 = stateRef.current.threads.find((thread) => thread.id === stateRef.current.activeThreadId);
|
|
1254
1743
|
if (shouldHydrateSelectedThread(threadId, stateRef.current.activeThreadId, Boolean(activeThread2?.isHydrated))) {
|
|
@@ -1260,6 +1749,8 @@ function useAgentChat(options = {}) {
|
|
|
1260
1749
|
return {
|
|
1261
1750
|
...state,
|
|
1262
1751
|
activeThread,
|
|
1752
|
+
pendingMessages,
|
|
1753
|
+
messageQueue,
|
|
1263
1754
|
hasOlderTurns: Boolean(activeThread?.hasOlderTurns),
|
|
1264
1755
|
hasMoreThreads,
|
|
1265
1756
|
isLoadingHistory,
|
|
@@ -1278,6 +1769,13 @@ function useAgentChat(options = {}) {
|
|
|
1278
1769
|
openThread,
|
|
1279
1770
|
refreshThreads,
|
|
1280
1771
|
regenerate,
|
|
1772
|
+
refreshMessages,
|
|
1773
|
+
editPendingMessage,
|
|
1774
|
+
steerPendingMessage,
|
|
1775
|
+
removePendingMessage,
|
|
1776
|
+
retractPendingMessage: queue.retractForEdit,
|
|
1777
|
+
resumeQueue,
|
|
1778
|
+
steer,
|
|
1281
1779
|
resumeHitl,
|
|
1282
1780
|
setFeedback,
|
|
1283
1781
|
startNewThread,
|
|
@@ -1286,15 +1784,15 @@ function useAgentChat(options = {}) {
|
|
|
1286
1784
|
}
|
|
1287
1785
|
|
|
1288
1786
|
// src/use-agent-resources.ts
|
|
1289
|
-
import * as
|
|
1787
|
+
import * as React7 from "react";
|
|
1290
1788
|
function useAgentThreads(options = {}) {
|
|
1291
1789
|
const { client, createAbortController } = useAgents24Runtime();
|
|
1292
|
-
const [threads, setThreads] =
|
|
1293
|
-
const [total, setTotal] =
|
|
1294
|
-
const [isLoading, setIsLoading] =
|
|
1295
|
-
const [error, setError] =
|
|
1296
|
-
const controllerRef =
|
|
1297
|
-
const load =
|
|
1790
|
+
const [threads, setThreads] = React7.useState([]);
|
|
1791
|
+
const [total, setTotal] = React7.useState();
|
|
1792
|
+
const [isLoading, setIsLoading] = React7.useState(true);
|
|
1793
|
+
const [error, setError] = React7.useState(null);
|
|
1794
|
+
const controllerRef = React7.useRef(null);
|
|
1795
|
+
const load = React7.useCallback(async () => {
|
|
1298
1796
|
controllerRef.current?.abort();
|
|
1299
1797
|
const controller = createAbortController();
|
|
1300
1798
|
controllerRef.current = controller;
|
|
@@ -1313,7 +1811,7 @@ function useAgentThreads(options = {}) {
|
|
|
1313
1811
|
}
|
|
1314
1812
|
}
|
|
1315
1813
|
}, [client, createAbortController, options.limit, options.skip]);
|
|
1316
|
-
|
|
1814
|
+
React7.useEffect(() => {
|
|
1317
1815
|
void load();
|
|
1318
1816
|
return () => controllerRef.current?.abort();
|
|
1319
1817
|
}, [load]);
|
|
@@ -1321,11 +1819,11 @@ function useAgentThreads(options = {}) {
|
|
|
1321
1819
|
}
|
|
1322
1820
|
function useAgentThread(threadId, options = {}) {
|
|
1323
1821
|
const { client, createAbortController } = useAgents24Runtime();
|
|
1324
|
-
const [thread, setThread] =
|
|
1325
|
-
const [isLoading, setIsLoading] =
|
|
1326
|
-
const [error, setError] =
|
|
1327
|
-
const controllerRef =
|
|
1328
|
-
const load =
|
|
1822
|
+
const [thread, setThread] = React7.useState(null);
|
|
1823
|
+
const [isLoading, setIsLoading] = React7.useState(Boolean(threadId));
|
|
1824
|
+
const [error, setError] = React7.useState(null);
|
|
1825
|
+
const controllerRef = React7.useRef(null);
|
|
1826
|
+
const load = React7.useCallback(async () => {
|
|
1329
1827
|
if (!threadId) {
|
|
1330
1828
|
controllerRef.current?.abort();
|
|
1331
1829
|
controllerRef.current = null;
|
|
@@ -1352,24 +1850,73 @@ function useAgentThread(threadId, options = {}) {
|
|
|
1352
1850
|
}
|
|
1353
1851
|
}
|
|
1354
1852
|
}, [client, createAbortController, options.beforeTurnIndex, options.limit, threadId]);
|
|
1355
|
-
|
|
1853
|
+
React7.useEffect(() => {
|
|
1356
1854
|
void load();
|
|
1357
1855
|
return () => controllerRef.current?.abort();
|
|
1358
1856
|
}, [load]);
|
|
1359
1857
|
return { error, isLoading, refresh: load, thread };
|
|
1360
1858
|
}
|
|
1859
|
+
function useThreadComputer(threadId) {
|
|
1860
|
+
const { client, createAbortController } = useAgents24Runtime();
|
|
1861
|
+
const [computer, setComputer] = React7.useState(null);
|
|
1862
|
+
const [isLoading, setIsLoading] = React7.useState(Boolean(threadId));
|
|
1863
|
+
const [error, setError] = React7.useState(null);
|
|
1864
|
+
const controllerRef = React7.useRef(null);
|
|
1865
|
+
const refresh = React7.useCallback(async () => {
|
|
1866
|
+
controllerRef.current?.abort();
|
|
1867
|
+
if (!threadId) {
|
|
1868
|
+
setComputer(null);
|
|
1869
|
+
setIsLoading(false);
|
|
1870
|
+
return null;
|
|
1871
|
+
}
|
|
1872
|
+
const controller = createAbortController();
|
|
1873
|
+
controllerRef.current = controller;
|
|
1874
|
+
setIsLoading(true);
|
|
1875
|
+
setError(null);
|
|
1876
|
+
try {
|
|
1877
|
+
const result = await client.threads.computer.get({ threadId, signal: controller.signal });
|
|
1878
|
+
setComputer(result);
|
|
1879
|
+
return result;
|
|
1880
|
+
} catch (nextError) {
|
|
1881
|
+
if (!isAbortLike(nextError)) setError(nextError);
|
|
1882
|
+
return null;
|
|
1883
|
+
} finally {
|
|
1884
|
+
if (controllerRef.current === controller) {
|
|
1885
|
+
controllerRef.current = null;
|
|
1886
|
+
setIsLoading(false);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
}, [client, createAbortController, threadId]);
|
|
1890
|
+
React7.useEffect(() => {
|
|
1891
|
+
void refresh();
|
|
1892
|
+
return () => controllerRef.current?.abort();
|
|
1893
|
+
}, [refresh]);
|
|
1894
|
+
const list = React7.useCallback((input) => {
|
|
1895
|
+
if (!threadId) throw new Error("A thread is required to list computer files");
|
|
1896
|
+
return client.threads.computer.list({ threadId, ...input });
|
|
1897
|
+
}, [client, threadId]);
|
|
1898
|
+
const read = React7.useCallback((path) => {
|
|
1899
|
+
if (!threadId) throw new Error("A thread is required to read a computer file");
|
|
1900
|
+
return client.threads.computer.read({ threadId, path });
|
|
1901
|
+
}, [client, threadId]);
|
|
1902
|
+
const createContentAccess = React7.useCallback((path, download = false) => {
|
|
1903
|
+
if (!threadId) throw new Error("A thread is required to access a computer file");
|
|
1904
|
+
return client.threads.computer.createContentAccess({ threadId, path, download });
|
|
1905
|
+
}, [client, threadId]);
|
|
1906
|
+
return { computer, error, isLoading, list, read, createContentAccess, refresh };
|
|
1907
|
+
}
|
|
1361
1908
|
function useAgentRun() {
|
|
1362
1909
|
const { client, createAbortController, createId } = useAgents24Runtime();
|
|
1363
|
-
const controllerRef =
|
|
1364
|
-
const [isAttached, setIsAttached] =
|
|
1365
|
-
const [error, setError] =
|
|
1366
|
-
|
|
1367
|
-
const detach =
|
|
1910
|
+
const controllerRef = React7.useRef(null);
|
|
1911
|
+
const [isAttached, setIsAttached] = React7.useState(false);
|
|
1912
|
+
const [error, setError] = React7.useState(null);
|
|
1913
|
+
React7.useEffect(() => () => controllerRef.current?.abort(), []);
|
|
1914
|
+
const detach = React7.useCallback(() => {
|
|
1368
1915
|
controllerRef.current?.abort();
|
|
1369
1916
|
controllerRef.current = null;
|
|
1370
1917
|
setIsAttached(false);
|
|
1371
1918
|
}, []);
|
|
1372
|
-
const attach =
|
|
1919
|
+
const attach = React7.useCallback(async (runId, onEvent, cursor) => {
|
|
1373
1920
|
detach();
|
|
1374
1921
|
const controller = createAbortController();
|
|
1375
1922
|
controllerRef.current = controller;
|
|
@@ -1385,7 +1932,7 @@ function useAgentRun() {
|
|
|
1385
1932
|
setIsAttached(false);
|
|
1386
1933
|
}
|
|
1387
1934
|
}, [client, createAbortController, detach]);
|
|
1388
|
-
const cancel =
|
|
1935
|
+
const cancel = React7.useCallback(async (runId) => {
|
|
1389
1936
|
const result = await client.runs.cancel({ runId, idempotencyKey: createId() });
|
|
1390
1937
|
detach();
|
|
1391
1938
|
return result;
|
|
@@ -1394,9 +1941,9 @@ function useAgentRun() {
|
|
|
1394
1941
|
}
|
|
1395
1942
|
function useAgentAttachments() {
|
|
1396
1943
|
const { client, createId } = useAgents24Runtime();
|
|
1397
|
-
const [uploads, setUploads] =
|
|
1398
|
-
const [isUploading, setIsUploading] =
|
|
1399
|
-
const uploadBatch =
|
|
1944
|
+
const [uploads, setUploads] = React7.useState({});
|
|
1945
|
+
const [isUploading, setIsUploading] = React7.useState(false);
|
|
1946
|
+
const uploadBatch = React7.useCallback(async (input) => {
|
|
1400
1947
|
setIsUploading(true);
|
|
1401
1948
|
try {
|
|
1402
1949
|
const results = await client.attachments.uploadBatch({ uploads: input.uploads, threadId: input.threadId, requestedAgentId: input.requestedAgentId, idempotencyKey: createId(), signal: input.signal });
|
|
@@ -1409,7 +1956,7 @@ function useAgentAttachments() {
|
|
|
1409
1956
|
setIsUploading(false);
|
|
1410
1957
|
}
|
|
1411
1958
|
}, [client, createId]);
|
|
1412
|
-
const createContentAccess =
|
|
1959
|
+
const createContentAccess = React7.useCallback(
|
|
1413
1960
|
(input) => client.attachments.createContentAccess(input),
|
|
1414
1961
|
[client]
|
|
1415
1962
|
);
|
|
@@ -1417,9 +1964,9 @@ function useAgentAttachments() {
|
|
|
1417
1964
|
}
|
|
1418
1965
|
function useAgentHitl() {
|
|
1419
1966
|
const { client, createId } = useAgents24Runtime();
|
|
1420
|
-
const [isResolving, setIsResolving] =
|
|
1421
|
-
const [lastResult, setLastResult] =
|
|
1422
|
-
const resume =
|
|
1967
|
+
const [isResolving, setIsResolving] = React7.useState(false);
|
|
1968
|
+
const [lastResult, setLastResult] = React7.useState(null);
|
|
1969
|
+
const resume = React7.useCallback(async (input) => {
|
|
1423
1970
|
setIsResolving(true);
|
|
1424
1971
|
try {
|
|
1425
1972
|
const result = await client.hitl.resume({ ...input, idempotencyKey: createId() });
|
|
@@ -1433,8 +1980,8 @@ function useAgentHitl() {
|
|
|
1433
1980
|
}
|
|
1434
1981
|
function useAgentMcp() {
|
|
1435
1982
|
const { client, createId } = useAgents24Runtime();
|
|
1436
|
-
const [isAuthorizing, setIsAuthorizing] =
|
|
1437
|
-
const startAuthorization =
|
|
1983
|
+
const [isAuthorizing, setIsAuthorizing] = React7.useState(false);
|
|
1984
|
+
const startAuthorization = React7.useCallback(async (input) => {
|
|
1438
1985
|
setIsAuthorizing(true);
|
|
1439
1986
|
try {
|
|
1440
1987
|
return await client.mcp.startAuthorization({ ...input, idempotencyKey: createId() });
|
|
@@ -1442,7 +1989,7 @@ function useAgentMcp() {
|
|
|
1442
1989
|
setIsAuthorizing(false);
|
|
1443
1990
|
}
|
|
1444
1991
|
}, [client, createId]);
|
|
1445
|
-
const redeemCallback =
|
|
1992
|
+
const redeemCallback = React7.useCallback(async (input) => client.mcp.redeemCallback({ ...input, idempotencyKey: createId() }), [client, createId]);
|
|
1446
1993
|
return { isAuthorizing, redeemCallback, startAuthorization };
|
|
1447
1994
|
}
|
|
1448
1995
|
export {
|
|
@@ -1458,6 +2005,7 @@ export {
|
|
|
1458
2005
|
chatAttachmentIds,
|
|
1459
2006
|
clearAgentChatBackgroundError,
|
|
1460
2007
|
clearAgentChatOperationError,
|
|
2008
|
+
computerResourceTargets,
|
|
1461
2009
|
contextCompressionPartFromEvent,
|
|
1462
2010
|
createPortableId,
|
|
1463
2011
|
getThreadActivity,
|
|
@@ -1466,9 +2014,12 @@ export {
|
|
|
1466
2014
|
latestThreadContext,
|
|
1467
2015
|
modelTurnPartFromEvent,
|
|
1468
2016
|
partsFromResponseBlocks,
|
|
2017
|
+
preferNewestThreadSummary,
|
|
2018
|
+
preserveMountedMessageIds,
|
|
1469
2019
|
reconcileRefreshedThreads,
|
|
1470
2020
|
restoreAgentChatThreadSelection,
|
|
1471
2021
|
threadDetailToMessages,
|
|
2022
|
+
threadTimelineToMessages,
|
|
1472
2023
|
upsertStableAssistantMessage,
|
|
1473
2024
|
useAgentAttachments,
|
|
1474
2025
|
useAgentChat,
|
|
@@ -1479,6 +2030,8 @@ export {
|
|
|
1479
2030
|
useAgentThread,
|
|
1480
2031
|
useAgentThreads,
|
|
1481
2032
|
useAgents24Runtime,
|
|
2033
|
+
useThreadComputer,
|
|
2034
|
+
useThreadMessageQueue,
|
|
1482
2035
|
withAgentChatBackgroundError,
|
|
1483
2036
|
withAgentChatOperationError
|
|
1484
2037
|
};
|