|
|
|
@ -506,6 +506,9 @@ export async function executeChat(params: ChatEngineParams): Promise<ChatEngineR |
|
|
|
let streamedReasoning = ""; |
|
|
|
let streamedToolCalls: Array<Record<string, unknown>> = []; |
|
|
|
let chunkPartId = 0; |
|
|
|
let streamedParts: Array<Record<string, unknown>> = []; |
|
|
|
let activeTextPartIdx = -1; |
|
|
|
let activeReasoningPartIdx = -1; |
|
|
|
|
|
|
|
params.onClientClose?.(() => { |
|
|
|
if (!serverAbortController.signal.aborted) { |
|
|
|
@ -561,8 +564,39 @@ export async function executeChat(params: ChatEngineParams): Promise<ChatEngineR |
|
|
|
onChunk: ({ chunk }) => { |
|
|
|
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<string, unknown>; |
|
|
|
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<string, unknown>; |
|
|
|
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<ChatEngineR |
|
|
|
args: chunk.input, |
|
|
|
state: "call", |
|
|
|
}); |
|
|
|
const existing = streamedParts.find( |
|
|
|
(p) => 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<ChatEngineR |
|
|
|
tcEntry.result = chunk.output; |
|
|
|
tcEntry.state = "result"; |
|
|
|
} |
|
|
|
const part = streamedParts.find( |
|
|
|
(p) => 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<ChatEngineR |
|
|
|
for (const step of steps) { |
|
|
|
const toolResultMap = new Map(step.toolResults.map((tr) => [tr.toolCallId, tr.output])); |
|
|
|
const contentArr = step.content as Array<Record<string, unknown>>; |
|
|
|
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<string>(); |
|
|
|
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<ChatEngineR |
|
|
|
const partialText = streamedText; |
|
|
|
logger.info("[%s] [CHAT-ENGINE] aborted: streamedTextLen=%d streamedToolCalls=%d", requestId, partialText.length, streamedToolCalls.length); |
|
|
|
|
|
|
|
const parts: Array<Record<string, unknown>> = []; |
|
|
|
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<Record<string, unknown>> = 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); |
|
|
|
} |
|
|
|
|
|
|
|
|