Persist IM runtime config locally

This commit is contained in:
taohe
2026-06-11 15:58:40 +08:00
parent 09872af36c
commit ade4a55cfe
8 changed files with 393 additions and 71 deletions
+7 -61
View File
@@ -1,5 +1,4 @@
import type { AIMessage, Message, Run } from "@langchain/langgraph-sdk";
import type { ThreadsClient } from "@langchain/langgraph-sdk/client";
import { useStream } from "@langchain/langgraph-sdk/react";
import {
type QueryClient,
@@ -24,6 +23,11 @@ import type { UploadedFileInfo } from "../uploads";
import { promptInputFilePartToFile, uploadFiles } from "../uploads";
import { fetchThreadTokenUsage } from "./api";
import {
buildThreadsSearchQueryOptions,
DEFAULT_THREAD_SEARCH_PARAMS,
type ThreadSearchParams,
} from "./thread-search-query";
import { threadTokenUsageQueryKey } from "./token-usage";
import type {
AgentThread,
@@ -1034,69 +1038,11 @@ export function useThreadHistory(
}
export function useThreads(
params: Parameters<ThreadsClient["search"]>[0] = {
limit: 50,
sortBy: "updated_at",
sortOrder: "desc",
select: ["thread_id", "updated_at", "values", "metadata"],
},
params: ThreadSearchParams = DEFAULT_THREAD_SEARCH_PARAMS,
) {
const apiClient = getAPIClient();
return useQuery<AgentThread[]>({
queryKey: ["threads", "search", params],
queryFn: async () => {
const maxResults = params.limit;
const initialOffset = params.offset ?? 0;
const DEFAULT_PAGE_SIZE = 50;
// Preserve prior semantics: if a non-positive limit is explicitly provided,
// delegate to a single search call with the original parameters.
if (maxResults !== undefined && maxResults <= 0) {
const response =
await apiClient.threads.search<AgentThreadState>(params);
return response as AgentThread[];
}
const pageSize =
typeof maxResults === "number" && maxResults > 0
? Math.min(DEFAULT_PAGE_SIZE, maxResults)
: DEFAULT_PAGE_SIZE;
const threads: AgentThread[] = [];
let offset = initialOffset;
while (true) {
if (typeof maxResults === "number" && threads.length >= maxResults) {
break;
}
const currentLimit =
typeof maxResults === "number"
? Math.min(pageSize, maxResults - threads.length)
: pageSize;
if (typeof maxResults === "number" && currentLimit <= 0) {
break;
}
const response = (await apiClient.threads.search<AgentThreadState>({
...params,
limit: currentLimit,
offset,
})) as AgentThread[];
threads.push(...response);
if (response.length < currentLimit) {
break;
}
offset += response.length;
}
return threads;
},
refetchOnWindowFocus: false,
...buildThreadsSearchQueryOptions(apiClient, params),
});
}
@@ -0,0 +1,84 @@
import type { ThreadsClient } from "@langchain/langgraph-sdk/client";
import type { AgentThread, AgentThreadState } from "./types";
type ThreadsSearchClient = {
threads: {
search: ThreadsClient["search"];
};
};
export type ThreadSearchParams = NonNullable<Parameters<ThreadsClient["search"]>[0]>;
export const DEFAULT_THREAD_SEARCH_PARAMS: ThreadSearchParams = {
limit: 50,
sortBy: "updated_at",
sortOrder: "desc",
select: ["thread_id", "updated_at", "values", "metadata"],
};
export const THREAD_SEARCH_REFETCH_INTERVAL_MS = 5000;
export function buildThreadsSearchQueryOptions(
apiClient: ThreadsSearchClient,
params: ThreadSearchParams = DEFAULT_THREAD_SEARCH_PARAMS,
) {
return {
queryKey: ["threads", "search", params],
queryFn: async () => {
const maxResults = params.limit;
const initialOffset = params.offset ?? 0;
const DEFAULT_PAGE_SIZE = 50;
// Preserve prior semantics: if a non-positive limit is explicitly provided,
// delegate to a single search call with the original parameters.
if (maxResults !== undefined && maxResults <= 0) {
const response =
await apiClient.threads.search<AgentThreadState>(params);
return response as AgentThread[];
}
const pageSize =
typeof maxResults === "number" && maxResults > 0
? Math.min(DEFAULT_PAGE_SIZE, maxResults)
: DEFAULT_PAGE_SIZE;
const threads: AgentThread[] = [];
let offset = initialOffset;
while (true) {
if (typeof maxResults === "number" && threads.length >= maxResults) {
break;
}
const currentLimit =
typeof maxResults === "number"
? Math.min(pageSize, maxResults - threads.length)
: pageSize;
if (typeof maxResults === "number" && currentLimit <= 0) {
break;
}
const response = (await apiClient.threads.search<AgentThreadState>({
...params,
limit: currentLimit,
offset,
})) as AgentThread[];
threads.push(...response);
if (response.length < currentLimit) {
break;
}
offset += response.length;
}
return threads;
},
refetchInterval: THREAD_SEARCH_REFETCH_INTERVAL_MS,
refetchIntervalInBackground: false,
refetchOnWindowFocus: false,
};
}
@@ -0,0 +1,19 @@
import { expect, test, vi } from "vitest";
import {
buildThreadsSearchQueryOptions,
DEFAULT_THREAD_SEARCH_PARAMS,
THREAD_SEARCH_REFETCH_INTERVAL_MS,
} from "@/core/threads/thread-search-query";
test("thread search query refreshes so IM-created sessions appear in the sidebar", () => {
const search = vi.fn();
const options = buildThreadsSearchQueryOptions(
{ threads: { search } },
DEFAULT_THREAD_SEARCH_PARAMS,
);
expect(options.refetchInterval).toBe(THREAD_SEARCH_REFETCH_INTERVAL_MS);
expect(options.refetchIntervalInBackground).toBe(false);
expect(options.refetchOnWindowFocus).toBe(false);
});