mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-06-18 13:46:02 +00:00
737abc0e45
* fix: ignore stale run reconnect conflicts * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: ignore stale run reconnect conflicts --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
172 lines
5.2 KiB
TypeScript
172 lines
5.2 KiB
TypeScript
"use client";
|
|
|
|
import { Client as LangGraphClient } from "@langchain/langgraph-sdk/client";
|
|
|
|
import { getLangGraphBaseURL } from "../config";
|
|
import { isStaticWebsiteOnly } from "../static-mode";
|
|
import {
|
|
loadStaticDemoThread,
|
|
loadStaticDemoThreads,
|
|
staticDemoThreadState,
|
|
} from "../threads/static-demo";
|
|
import type { AgentThreadState } from "../threads/types";
|
|
|
|
import { isStateChangingMethod, readCsrfCookie } from "./fetcher";
|
|
import { sanitizeRunStreamOptions } from "./stream-mode";
|
|
|
|
/**
|
|
* SDK ``onRequest`` hook that mints the ``X-CSRF-Token`` header from the
|
|
* live ``csrf_token`` cookie just before each outbound fetch.
|
|
*
|
|
* Reading the cookie per-request (rather than baking it into the SDK's
|
|
* ``defaultHeaders`` at construction) handles login / logout / password
|
|
* change cookie rotation transparently. Both the ``/api/langgraph/*`` SDK
|
|
* path and the direct REST endpoints in ``fetcher.ts:fetchWithAuth``
|
|
* share :func:`readCsrfCookie` and :const:`STATE_CHANGING_METHODS` so
|
|
* the contract stays in lockstep.
|
|
*/
|
|
function injectCsrfHeader(_url: URL, init: RequestInit): RequestInit {
|
|
if (!isStateChangingMethod(init.method ?? "GET")) {
|
|
return init;
|
|
}
|
|
const token = readCsrfCookie();
|
|
if (!token) return init;
|
|
const headers = new Headers(init.headers);
|
|
if (!headers.has("X-CSRF-Token")) {
|
|
headers.set("X-CSRF-Token", token);
|
|
}
|
|
return { ...init, headers };
|
|
}
|
|
|
|
export function isInactiveRunStreamError(error: unknown): boolean {
|
|
const status =
|
|
typeof error === "object" && error !== null
|
|
? Reflect.get(error, "status")
|
|
: undefined;
|
|
const message =
|
|
typeof error === "string"
|
|
? error
|
|
: error instanceof Error
|
|
? error.message
|
|
: typeof error === "object" && error !== null
|
|
? String(Reflect.get(error, "message") ?? "")
|
|
: "";
|
|
|
|
// Match the gateway's store-only run response in
|
|
// backend/app/gateway/routers/thread_runs.py until the API exposes a
|
|
// structured error code for inactive run streams.
|
|
return (
|
|
(status === 409 || message.includes("HTTP 409")) &&
|
|
message.includes("not active on this worker") &&
|
|
message.includes("cannot be streamed")
|
|
);
|
|
}
|
|
|
|
export function clearReconnectRun(
|
|
threadId: string | null | undefined,
|
|
runId: string,
|
|
): void {
|
|
if (typeof window === "undefined" || !threadId) return;
|
|
|
|
const key = `lg:stream:${threadId}`;
|
|
try {
|
|
const storage = window.sessionStorage;
|
|
if (storage.getItem(key) === runId) {
|
|
storage.removeItem(key);
|
|
}
|
|
} catch {
|
|
// Ignore storage access failures so reconnect cleanup never throws.
|
|
}
|
|
}
|
|
|
|
function createCompatibleClient(isMock?: boolean): LangGraphClient {
|
|
if (isStaticWebsiteOnly() && !isMock) {
|
|
return createStaticClient();
|
|
}
|
|
|
|
const apiUrl = getLangGraphBaseURL(isMock);
|
|
console.log(`Creating API client with base URL: ${apiUrl}`);
|
|
const client = new LangGraphClient({
|
|
apiUrl,
|
|
onRequest: injectCsrfHeader,
|
|
});
|
|
|
|
const originalRunStream = client.runs.stream.bind(client.runs);
|
|
client.runs.stream = ((threadId, assistantId, payload) =>
|
|
originalRunStream(
|
|
threadId,
|
|
assistantId,
|
|
sanitizeRunStreamOptions(payload),
|
|
)) as typeof client.runs.stream;
|
|
|
|
const originalJoinStream = client.runs.joinStream.bind(client.runs);
|
|
client.runs.joinStream = async function* (threadId, runId, options) {
|
|
try {
|
|
yield* originalJoinStream(
|
|
threadId,
|
|
runId,
|
|
sanitizeRunStreamOptions(options),
|
|
);
|
|
} catch (error) {
|
|
if (isInactiveRunStreamError(error)) {
|
|
clearReconnectRun(threadId, runId);
|
|
return;
|
|
}
|
|
throw error;
|
|
}
|
|
} as typeof client.runs.joinStream;
|
|
|
|
return client;
|
|
}
|
|
|
|
function createStaticClient(): LangGraphClient {
|
|
const apiUrl =
|
|
typeof window === "undefined"
|
|
? "http://localhost:3000"
|
|
: window.location.origin;
|
|
const client = new LangGraphClient({ apiUrl });
|
|
|
|
client.threads.search = (async (query) => {
|
|
return loadStaticDemoThreads(query);
|
|
}) as typeof client.threads.search;
|
|
|
|
client.threads.get = (async (threadId) => {
|
|
return loadStaticDemoThread(threadId);
|
|
}) as typeof client.threads.get;
|
|
|
|
client.threads.getState = (async (threadId) => {
|
|
return staticDemoThreadState(await loadStaticDemoThread(threadId));
|
|
}) as typeof client.threads.getState;
|
|
|
|
client.threads.getHistory = (async (threadId) => {
|
|
return [staticDemoThreadState(await loadStaticDemoThread(threadId))];
|
|
}) as typeof client.threads.getHistory;
|
|
|
|
client.threads.update = (async (threadId) => {
|
|
return loadStaticDemoThread(threadId);
|
|
}) as typeof client.threads.update;
|
|
|
|
client.runs.list = (async () => []) as typeof client.runs.list;
|
|
client.runs.stream = async function* () {
|
|
/* empty */
|
|
} as typeof client.runs.stream;
|
|
client.runs.joinStream = async function* () {
|
|
/* empty */
|
|
} as typeof client.runs.joinStream;
|
|
|
|
return client as LangGraphClient<AgentThreadState>;
|
|
}
|
|
|
|
const _clients = new Map<string, LangGraphClient>();
|
|
export function getAPIClient(isMock?: boolean): LangGraphClient {
|
|
const cacheKey = isMock ? "mock" : "default";
|
|
let client = _clients.get(cacheKey);
|
|
|
|
if (!client) {
|
|
client = createCompatibleClient(isMock);
|
|
_clients.set(cacheKey, client);
|
|
}
|
|
|
|
return client;
|
|
}
|