diff --git a/packages/drizzle-pkg/db.sqlite b/packages/drizzle-pkg/db.sqlite index 3bea3ff..c712273 100644 Binary files a/packages/drizzle-pkg/db.sqlite and b/packages/drizzle-pkg/db.sqlite differ diff --git a/server/service/agent/chat-engine.ts b/server/service/agent/chat-engine.ts index 067c289..776743d 100644 --- a/server/service/agent/chat-engine.ts +++ b/server/service/agent/chat-engine.ts @@ -506,6 +506,9 @@ export async function executeChat(params: ChatEngineParams): Promise> = []; let chunkPartId = 0; + let streamedParts: Array> = []; + let activeTextPartIdx = -1; + let activeReasoningPartIdx = -1; params.onClientClose?.(() => { if (!serverAbortController.signal.aborted) { @@ -561,8 +564,39 @@ export async function executeChat(params: ChatEngineParams): Promise { if (chunk.type === "text-delta") { streamedText += chunk.text; + if (activeTextPartIdx < 0) { + activeTextPartIdx = streamedParts.length; + streamedParts.push({ id: `p_${chunkPartId++}`, type: "text", text: "" }); + } + const tp = streamedParts[activeTextPartIdx] as Record; + tp.text = ((tp.text as string) || "") + chunk.text; + } else if (chunk.type === "text-start") { + activeTextPartIdx = streamedParts.length; + streamedParts.push({ id: `p_${chunkPartId++}`, type: "text", text: "" }); + } else if (chunk.type === "text-end") { + activeTextPartIdx = -1; } else if (chunk.type === "reasoning-delta") { streamedReasoning += chunk.text; + if (activeReasoningPartIdx < 0) { + activeReasoningPartIdx = streamedParts.length; + streamedParts.push({ id: `p_${chunkPartId++}`, type: "reasoning", text: "" }); + } + const rp = streamedParts[activeReasoningPartIdx] as Record; + rp.text = ((rp.text as string) || "") + chunk.text; + } else if (chunk.type === "reasoning-start") { + activeReasoningPartIdx = streamedParts.length; + streamedParts.push({ id: `p_${chunkPartId++}`, type: "reasoning", text: "" }); + } else if (chunk.type === "reasoning-end") { + activeReasoningPartIdx = -1; + } else if (chunk.type === "tool-input-start") { + streamedParts.push({ + id: `p_${chunkPartId++}`, + type: "tool-call", + toolCallId: chunk.id, + toolName: chunk.toolName, + args: undefined, + state: "preparing", + }); } else if (chunk.type === "tool-call") { streamedToolCalls.push({ id: `p_${chunkPartId++}`, @@ -572,6 +606,22 @@ export async function executeChat(params: ChatEngineParams): Promise p.type === "tool-call" && p.toolCallId === chunk.toolCallId, + ); + if (existing) { + existing.args = chunk.input; + existing.state = "call"; + } else { + streamedParts.push({ + id: `p_${chunkPartId++}`, + type: "tool-call", + toolCallId: chunk.toolCallId, + toolName: chunk.toolName, + args: chunk.input, + state: "call", + }); + } } else if (chunk.type === "tool-result") { const tcIdx = streamedToolCalls.findIndex((tc) => tc.toolCallId === chunk.toolCallId); const tcEntry = tcIdx >= 0 ? streamedToolCalls[tcIdx] : undefined; @@ -579,6 +629,13 @@ export async function executeChat(params: ChatEngineParams): Promise p.type === "tool-call" && p.toolCallId === chunk.toolCallId, + ); + if (part) { + part.result = chunk.output; + part.state = "result"; + } } }, onFinish: async ({ finishReason, usage, steps, text: assistantText }) => { @@ -602,6 +659,12 @@ export async function executeChat(params: ChatEngineParams): Promise [tr.toolCallId, tr.output])); const contentArr = step.content as Array>; + logger.info( + "[%s] [CHAT-ENGINE] step %d content order: %s", + requestId, + step.stepNumber, + contentArr.map((cp) => cp.type + (cp.type === "tool-call" ? `(${(cp as any).toolName})` : "")).join(" → "), + ); const approvalToolCallIds = new Set(); for (const cp of contentArr) { if (cp.type === "tool-approval-request" && (cp as any).approvalId && (cp as any).toolCall) { @@ -746,33 +809,21 @@ export async function executeChat(params: ChatEngineParams): Promise> = []; - let hasTextPart = false; - if (streamedReasoning.trim()) { - parts.push({ id: `p_${parts.length}`, type: "reasoning", text: streamedReasoning }); - } - if (partialText.trim()) { - parts.push({ id: `p_${parts.length}`, type: "text", text: partialText }); - hasTextPart = true; - } - for (const tc of streamedToolCalls) { - parts.push({ - id: `p_${parts.length}`, - type: "tool-call", - toolCallId: tc.toolCallId, - toolName: tc.toolName, - args: tc.args, - result: tc.result, - state: tc.result !== undefined ? "result" : "call", - }); - } - + const parts: Array> = streamedParts + .filter((p) => { + if (p.type === "text") return ((p.text as string) || "").trim().length > 0; + if (p.type === "reasoning") return ((p.text as string) || "").trim().length > 0; + return true; + }) + .map((p, i) => ({ ...p, id: `p_${i}` })); + let hasTextPart = parts.some((p) => p.type === "text"); const hasToolCallPart = parts.some((p) => p.type === "tool-call"); let finalText = partialText; if (!hasTextPart && hasToolCallPart) { const noticeText = "(对话已停止,以上是已完成的工具调用记录。)"; parts.push({ id: `p_${parts.length}`, type: "text", text: noticeText }); finalText = (finalText || "") + noticeText; + hasTextPart = true; logger.info("[%s] [CHAT-ENGINE] aborted with tool calls but no text, appended notice", requestId); }