mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-06-10 09:25:57 +00:00
fix: load paginated run history messages (#3305)
This commit is contained in:
@@ -119,6 +119,55 @@ function findLatestUnloadedRunIndex(
|
||||
return -1;
|
||||
}
|
||||
|
||||
type RunMessagesPageResponse = {
|
||||
data: RunMessage[];
|
||||
has_more?: boolean;
|
||||
hasMore?: boolean;
|
||||
};
|
||||
|
||||
export function runMessagesPageHasMore(result: RunMessagesPageResponse) {
|
||||
return result.has_more ?? result.hasMore ?? false;
|
||||
}
|
||||
|
||||
export function getOldestRunMessageSeq(messages: RunMessage[]) {
|
||||
let oldestSeq: number | null = null;
|
||||
for (const message of messages) {
|
||||
if (typeof message.seq !== "number") {
|
||||
continue;
|
||||
}
|
||||
oldestSeq =
|
||||
oldestSeq === null ? message.seq : Math.min(oldestSeq, message.seq);
|
||||
}
|
||||
return oldestSeq;
|
||||
}
|
||||
|
||||
export function getNextRunMessagesBeforeSeq(
|
||||
result: RunMessagesPageResponse,
|
||||
): number | null | undefined {
|
||||
if (!runMessagesPageHasMore(result)) {
|
||||
return null;
|
||||
}
|
||||
return getOldestRunMessageSeq(result.data) ?? undefined;
|
||||
}
|
||||
|
||||
export function buildRunMessagesUrl(
|
||||
baseUrl: string,
|
||||
threadId: string,
|
||||
runId: string,
|
||||
beforeSeq?: number,
|
||||
) {
|
||||
const normalizedBaseUrl = baseUrl.replace(/\/$/, "");
|
||||
const path = `/api/threads/${encodeURIComponent(threadId)}/runs/${encodeURIComponent(runId)}/messages`;
|
||||
const url = new URL(
|
||||
`${normalizedBaseUrl}${path}`,
|
||||
typeof window !== "undefined" ? window.location.origin : "http://localhost",
|
||||
);
|
||||
if (beforeSeq !== undefined) {
|
||||
url.searchParams.set("before_seq", String(beforeSeq));
|
||||
}
|
||||
return normalizedBaseUrl ? url.toString() : `${url.pathname}${url.search}`;
|
||||
}
|
||||
|
||||
export function mergeMessages(
|
||||
historyMessages: Message[],
|
||||
threadMessages: Message[],
|
||||
@@ -801,6 +850,7 @@ export function useThreadHistory(threadId: string) {
|
||||
const pendingLoadRef = useRef(false);
|
||||
const loadingRunIdRef = useRef<string | null>(null);
|
||||
const loadedRunIdsRef = useRef<Set<string>>(new Set());
|
||||
const runBeforeSeqRef = useRef<Map<string, number>>(new Map());
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
|
||||
@@ -841,16 +891,20 @@ export function useThreadHistory(threadId: string) {
|
||||
|
||||
const requestThreadId = threadIdRef.current;
|
||||
loadingRunIdRef.current = run.run_id;
|
||||
const result: { data: RunMessage[]; hasMore: boolean } = await fetch(
|
||||
`${getBackendBaseURL()}/api/threads/${encodeURIComponent(requestThreadId)}/runs/${encodeURIComponent(run.run_id)}/messages`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
const beforeSeq = runBeforeSeqRef.current.get(run.run_id);
|
||||
const url = buildRunMessagesUrl(
|
||||
getBackendBaseURL(),
|
||||
requestThreadId,
|
||||
run.run_id,
|
||||
beforeSeq,
|
||||
);
|
||||
const result: RunMessagesPageResponse = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
).then((res) => {
|
||||
credentials: "include",
|
||||
}).then((res) => {
|
||||
return res.json();
|
||||
});
|
||||
const _messages = result.data
|
||||
@@ -862,7 +916,18 @@ export function useThreadHistory(threadId: string) {
|
||||
setMessages((prev) =>
|
||||
dedupeMessagesByIdentity([..._messages, ...prev]),
|
||||
);
|
||||
loadedRunIdsRef.current.add(run.run_id);
|
||||
const nextBeforeSeq = getNextRunMessagesBeforeSeq(result);
|
||||
if (typeof nextBeforeSeq === "number") {
|
||||
runBeforeSeqRef.current.set(run.run_id, nextBeforeSeq);
|
||||
pendingLoadRef.current = true;
|
||||
} else if (nextBeforeSeq === undefined) {
|
||||
console.warn(
|
||||
`Run ${run.run_id} returned has_more without message seq values; leaving it pending for retry.`,
|
||||
);
|
||||
} else {
|
||||
runBeforeSeqRef.current.delete(run.run_id);
|
||||
loadedRunIdsRef.current.add(run.run_id);
|
||||
}
|
||||
indexRef.current = findLatestUnloadedRunIndex(
|
||||
runsRef.current,
|
||||
loadedRunIdsRef.current,
|
||||
@@ -886,6 +951,7 @@ export function useThreadHistory(threadId: string) {
|
||||
pendingLoadRef.current = false;
|
||||
loadingRunIdRef.current = null;
|
||||
loadedRunIdsRef.current = new Set();
|
||||
runBeforeSeqRef.current = new Map();
|
||||
loadingRef.current = false;
|
||||
setLoading(false);
|
||||
setMessages([]);
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface AgentThread extends Thread<AgentThreadState> {
|
||||
|
||||
export interface RunMessage {
|
||||
run_id: string;
|
||||
seq?: number;
|
||||
content: Message;
|
||||
metadata: {
|
||||
caller: string;
|
||||
|
||||
@@ -2,10 +2,25 @@ import type { Message } from "@langchain/langgraph-sdk";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
import {
|
||||
buildRunMessagesUrl,
|
||||
getNextRunMessagesBeforeSeq,
|
||||
getOldestRunMessageSeq,
|
||||
getSummarizationMiddlewareMessages,
|
||||
getVisibleOptimisticMessages,
|
||||
mergeMessages,
|
||||
runMessagesPageHasMore,
|
||||
} from "@/core/threads/hooks";
|
||||
import type { RunMessage } from "@/core/threads/types";
|
||||
|
||||
function runMessage(seq?: number): RunMessage {
|
||||
return {
|
||||
run_id: "run-1",
|
||||
...(seq === undefined ? {} : { seq }),
|
||||
content: {} as Message,
|
||||
metadata: { caller: "" },
|
||||
created_at: "2026-05-22T00:00:00Z",
|
||||
};
|
||||
}
|
||||
|
||||
test("mergeMessages removes duplicate messages already present in history", () => {
|
||||
const human = {
|
||||
@@ -254,3 +269,59 @@ test("getVisibleOptimisticMessages hides optimistic user input after later serve
|
||||
optimisticHuman,
|
||||
]);
|
||||
});
|
||||
|
||||
test("runMessagesPageHasMore reads backend snake_case pagination field", () => {
|
||||
expect(runMessagesPageHasMore({ data: [], has_more: true })).toBe(true);
|
||||
expect(runMessagesPageHasMore({ data: [], has_more: false })).toBe(false);
|
||||
});
|
||||
|
||||
test("runMessagesPageHasMore keeps compatibility with camelCase pagination field", () => {
|
||||
expect(runMessagesPageHasMore({ data: [], hasMore: true })).toBe(true);
|
||||
});
|
||||
|
||||
test("getOldestRunMessageSeq returns the cursor for the next older run page", () => {
|
||||
expect(
|
||||
getOldestRunMessageSeq([runMessage(8), runMessage(9), runMessage(10)]),
|
||||
).toBe(8);
|
||||
});
|
||||
|
||||
test("getOldestRunMessageSeq ignores rows without seq", () => {
|
||||
expect(getOldestRunMessageSeq([runMessage()])).toBeNull();
|
||||
});
|
||||
|
||||
test("getNextRunMessagesBeforeSeq keeps runs pending when has_more lacks seq", () => {
|
||||
expect(
|
||||
getNextRunMessagesBeforeSeq({ data: [runMessage()], has_more: true }),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test("getNextRunMessagesBeforeSeq marks runs loaded when no more pages exist", () => {
|
||||
expect(
|
||||
getNextRunMessagesBeforeSeq({ data: [runMessage()], has_more: false }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test("buildRunMessagesUrl encodes path segments and optional before_seq", () => {
|
||||
expect(
|
||||
buildRunMessagesUrl(
|
||||
"https://api.example.test/",
|
||||
"thread/with space",
|
||||
"run?one",
|
||||
18,
|
||||
),
|
||||
).toBe(
|
||||
"https://api.example.test/api/threads/thread%2Fwith%20space/runs/run%3Fone/messages?before_seq=18",
|
||||
);
|
||||
});
|
||||
|
||||
test("buildRunMessagesUrl omits before_seq when loading the latest page", () => {
|
||||
expect(
|
||||
buildRunMessagesUrl("https://api.example.test", "thread-1", "run-1"),
|
||||
).toBe("https://api.example.test/api/threads/thread-1/runs/run-1/messages");
|
||||
});
|
||||
|
||||
test("buildRunMessagesUrl returns a relative URL when using the nginx proxy", () => {
|
||||
expect(buildRunMessagesUrl("", "thread-1", "run-1", 42)).toBe(
|
||||
"/api/threads/thread-1/runs/run-1/messages?before_seq=42",
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user