Merge origin/dhk-1: resolve config.yaml database backend conflict

Use sqlite as temporary fallback while MySQL is unavailable; keeps
local persistence instead of in-memory backend.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
董宏凯andCursor committed 2026-05-23 17:44:52 +08:00
commit 7fcf22de90
171 files changed
+41064 -10709

No files matched your search

@@ -0,0 +1,233 @@
import { apiFetch } from "@/core/api/fetch-client";
import { getBackendBaseURL } from "@/core/config";
export type ThreadIdMap = Record<string, string>;
export interface InitMultiAgentRequest {
agentNames: string[];
mainAgentName: string;
model?: string;
}
export interface InitMultiAgentResponse {
status: "success";
main_agent_name: string;
thread_ids: ThreadIdMap;
}
export async function initMultiAgent(
payload: InitMultiAgentRequest,
): Promise<InitMultiAgentResponse> {
const res = await apiFetch(`${getBackendBaseURL()}/api/multi-agent/init`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
agent_names: payload.agentNames,
main_agent_name: payload.mainAgentName,
model: payload.model,
}),
});
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(`init multi-agent failed (${res.status}): ${text}`);
}
return (await res.json()) as InitMultiAgentResponse;
}
export type StreamAgentType = "leader" | "special";
export type LeaderDispatch = [string, string];
export interface StreamMultiAgentRequest {
agentType: StreamAgentType;
dAgentThreadId: ThreadIdMap;
agentName: string;
newMessage: string;
model?: string;
skillStopNames?: string[];
signal?: AbortSignal;
/** Fires for every non-status data frame the upstream emits. */
onUpstreamFrame?: (frame: unknown) => void;
/**
* Fires for every AI text chunk parsed out of LangGraph messages-tuple frames.
* `text` is the incremental delta (concatenate to rebuild the message);
* `messageId` is stable per AI message so callers can group deltas.
*/
onTextDelta?: (text: string, messageId: string) => void;
}
export interface StreamMultiAgentResult {
/** For leader runs: dispatched [agentName, task] tuples. Empty = consensus. */
dispatched: LeaderDispatch[];
/** For special runs: the completion status string. */
message: string | null;
/** Final assistant text emitted by the underlying agent. */
content: string;
/** Echoed agent_name of the agent that produced this run. */
agentName: string | null;
}
/**
* Identify the final status frame our backend appends after the LangGraph
* stream. Required keys are `status` (array for leader, string for special)
* and `content` (final assistant text); `agent_name` is also expected.
*/
function isFinalStatusFrame(value: unknown): value is {
status: LeaderDispatch[] | string;
content?: string;
agent_name?: string;
} {
if (!value || typeof value !== "object") return false;
if (!("status" in value) || !("content" in value)) return false;
const status = (value as { status: unknown }).status;
if (typeof status === "string") return true;
if (Array.isArray(status)) {
return status.every(
(item) =>
Array.isArray(item) && item.length === 2 && typeof item[0] === "string" && typeof item[1] === "string",
);
}
return false;
}
/**
* Extract an AI text delta from a LangGraph `messages-tuple` data frame.
* Frames look like `[chunk, metadata]`; an AI text chunk carries `type` in
* {"ai","AIMessageChunk"} and a `content` that is either a string or a list of
* structured content blocks. Returns null when the frame is not an AI text
* chunk (e.g., tool calls, tool results, value snapshots).
*/
function extractAiTextDelta(parsed: unknown): { text: string; id: string } | null {
if (!Array.isArray(parsed) || parsed.length < 1) return null;
const chunk = parsed[0];
if (!chunk || typeof chunk !== "object") return null;
const obj = chunk as Record<string, unknown>;
const type = String(obj.type ?? "").toLowerCase();
// Accept any AI-shaped message chunk LangChain might emit across versions:
// "ai" (older), "AIMessageChunk" (newer streaming), or plain "AIMessage".
if (type !== "ai" && type !== "aimessagechunk" && type !== "aimessage") return null;
const content = obj.content;
let text = "";
if (typeof content === "string") {
text = content;
} else if (Array.isArray(content)) {
for (const item of content) {
if (typeof item === "string") text += item;
else if (item && typeof item === "object") {
const block = item as Record<string, unknown>;
const blockText = block.text;
if (typeof blockText === "string") text += blockText;
}
}
}
if (!text) return null;
const id = typeof obj.id === "string" ? obj.id : "";
return { text, id };
}
/**
* Open an SSE connection to /api/multi-agent/run/stream and resolve once the
* stream completes. The final frame (`{status: ...}`) is interpreted; every
* other `data:` frame is forwarded to `onUpstreamFrame` so the caller can do
* its own rendering (we don't try to mimic LangGraph SDK semantics here).
*/
export async function streamMultiAgent(
req: StreamMultiAgentRequest,
): Promise<StreamMultiAgentResult> {
const res = await apiFetch(`${getBackendBaseURL()}/api/multi-agent/run/stream`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
agent_type: req.agentType,
d_agent_thread_id: req.dAgentThreadId,
agent_name: req.agentName,
new_message: req.newMessage,
model: req.model,
skill_stop_names: req.skillStopNames ?? [],
}),
signal: req.signal,
});
if (!res.ok || !res.body) {
const text = await res.text().catch(() => "");
throw new Error(`run/stream failed (${res.status}): ${text}`);
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
// Track the last `event:` line so logging can show which event a `data:`
// belongs to. The frontend doesn't actually dispatch by event name — every
// data frame goes through the same parser — but the label is invaluable
// for diagnosing why a sub-agent stream "looks empty".
let lastEvent = "";
let frameSeq = 0;
const debugLabel = `[stream:${req.agentType}:${req.agentName}]`;
const debugEnabled =
typeof window !== "undefined" &&
(window as unknown as { __MULTI_AGENT_DEBUG__?: boolean }).__MULTI_AGENT_DEBUG__ !== false;
const result: StreamMultiAgentResult = { dispatched: [], message: null, content: "", agentName: null };
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const rawLine of lines) {
const line = rawLine.trimEnd();
if (!line) continue;
if (line.startsWith("event:")) {
lastEvent = line.slice(6).trim();
continue;
}
if (!line.startsWith("data:")) continue;
const payload = line.slice(5).trim();
if (!payload) continue;
let parsed: unknown;
try {
parsed = JSON.parse(payload);
} catch {
if (debugEnabled) console.debug(debugLabel, `bad JSON in event=${lastEvent}:`, payload.slice(0, 200));
continue;
}
frameSeq += 1;
if (debugEnabled) console.debug(debugLabel, `frame#${frameSeq} event=${lastEvent}`, parsed);
if (isFinalStatusFrame(parsed)) {
if (Array.isArray(parsed.status)) {
result.dispatched = parsed.status;
} else {
result.message = parsed.status;
}
if (typeof parsed.content === "string") {
result.content = parsed.content;
}
if (typeof parsed.agent_name === "string") {
result.agentName = parsed.agent_name;
}
continue;
}
const delta = extractAiTextDelta(parsed);
if (delta) {
if (debugEnabled) console.debug(debugLabel, `→ delta (${delta.text.length} chars)`);
req.onTextDelta?.(delta.text, delta.id);
}
req.onUpstreamFrame?.(parsed);
}
}
if (debugEnabled) {
console.debug(debugLabel, `stream ended after ${frameSeq} frames; final content length=${result.content.length}`);
}
return result;
}
File diff suppressed because it is too large. Load diff