feat(agent): 完善分析编排与结果传输
Generic Container CI/CD / test-build-publish (push) Failing after 1m20s
Agent CI/CD v2 / build-test-publish-and-deploy (push) Failing after 1m20s

This commit is contained in:
2026-08-26 18:04:51 +08:00
parent a438433068
commit 72ebf4d6c1
33 changed files with 1027 additions and 705 deletions
+153 -343
View File
@@ -2,24 +2,16 @@ import type { Event as OpencodeEvent, Part } from "@opencode-ai/sdk/v2";
import { writeLlmRequestAuditLog } from "../audit/llmRequestAudit.js";
import { type SupportedModel } from "../chat/models.js";
import { config } from "../config.js";
import { logger } from "../logger.js";
import {
type PermissionReply,
type OpencodeRuntimeAdapter,
} from "../runtime/opencode.js";
import {
buildPermissionDetail,
buildPermissionV2Detail,
buildReasoningProgressDetail,
buildSessionStatusDetail,
buildToolProgressDetail,
collectTextContent,
extractRequestReason,
extractSkillAuditInfo,
getErrorMessage,
getAssistantMessagePhase,
getToolProgressTitle,
getUnknownErrorMessage,
hasToolParams,
isPermissionAskedEvent,
@@ -35,6 +27,7 @@ import {
isQuestionV2RepliedEvent,
isSessionEvent,
isSkillEvent,
initialActivity,
logDevelopmentDebug,
normalizeQuestionAnswers,
normalizeQuestionPayload,
@@ -42,13 +35,15 @@ import {
normalizeTodoPriority,
normalizeTodoStatus,
normalizeToolParams,
normalizeToolStatus,
type ActivityPayload,
type ActivityUpdatePayload,
type PermissionRequestPayload,
type QuestionRequestPayload,
type AssistantMessagePhase,
type TodoItemPayload,
type TodoUpdatePayload,
} from "./chatStreamEvents.js";
import { createActivityTracker } from "./chatActivityTracker.js";
import {
resolvePermissionApproval,
type ApprovalMode,
@@ -56,10 +51,13 @@ import {
export {
collectTextContent,
initialActivity,
type PermissionRequestPayload,
type QuestionRequestPayload,
type TodoItemPayload,
type TodoUpdatePayload,
type ActivityPayload,
type ActivityUpdatePayload,
} from "./chatStreamEvents.js";
export type { ApprovalMode } from "./chatPermissionPolicy.js";
@@ -78,16 +76,6 @@ type StreamPromptOptions = {
write: (event: string, data: Record<string, unknown>) => void;
};
type ProgressStatus = "running" | "completed" | "error";
type ProgressPayload = {
id: string;
phase: string;
status: ProgressStatus;
title: string;
detail?: string;
};
const getPermissionTarget = (metadata: unknown) => {
if (!isObjectRecord(metadata)) {
return undefined;
@@ -115,38 +103,20 @@ const toRuntimeModel = (model?: SupportedModel) => {
};
};
const STRUCTURED_OUTPUT_TOOL_NAME = "StructuredOutput";
const STRUCTURED_FINAL_ANSWER_FORMAT = {
type: "json_schema" as const,
schema: {
type: "object",
properties: {
answer: {
type: "string",
description: "直接展示给用户的完整最终回答,使用简体中文和 Markdown。",
},
},
required: ["answer"],
additionalProperties: false,
},
};
const FINAL_ANSWER_TOOL_NAME = "final_answer";
const ACTIVITY_UPDATE_TOOL_NAME = "activity_update";
const requiresStructuredFinalAnswer = (model?: SupportedModel) =>
(model ?? config.OPENCODE_MODEL).split("/", 1)[0] === "deepseek";
const extractStructuredAnswer = (value: unknown) =>
const extractFinalAnswer = (value: unknown) =>
isObjectRecord(value) && typeof value.answer === "string"
? value.answer.trim()
: "";
const emitFinalMessage = async (
const resolveFinalMessage = async (
runtime: OpencodeRuntimeAdapter,
sessionId: string,
clientSessionId: string,
currentAssistantMessageIds: Set<string>,
assistantTextParts: Map<string, Map<string, string>>,
assistantTextPartPhases: Map<string, AssistantMessagePhase>,
write: (event: string, data: Record<string, unknown>) => void,
) => {
let text = [...currentAssistantMessageIds]
.reverse()
@@ -180,30 +150,53 @@ const emitFinalMessage = async (
(currentAssistantMessageIds.size === 0 ||
currentAssistantMessageIds.has(message.info.id)),
);
const structured =
assistantMessage && "structured" in assistantMessage.info
? assistantMessage.info.structured
: undefined;
const structuredAnswer = extractStructuredAnswer(structured);
text =
structuredAnswer ||
collectTextContent(
(assistantMessage?.parts ?? []).filter(
const assistantParts = assistantMessage?.parts ?? [];
text = collectTextContent(
assistantParts.filter(
(part) =>
part.type !== "text" ||
getAssistantMessagePhase(part.metadata) !== "commentary",
),
);
if (!text) {
const finalAnswerPart = [...assistantParts]
.reverse()
.find(
(part) =>
part.type !== "text" ||
getAssistantMessagePhase(part.metadata) !== "commentary",
),
);
part.type === "tool" && part.tool === FINAL_ANSWER_TOOL_NAME,
);
if (finalAnswerPart?.type === "tool") {
text = extractFinalAnswer(finalAnswerPart.state.input);
}
}
}
if (text) {
write("token", {
session_id: clientSessionId,
content: text,
});
return true;
}
return false;
return text.trim();
};
const normalizeActivityTodos = (value: unknown): TodoItemPayload[] | undefined => {
if (!Array.isArray(value)) return undefined;
const now = Date.now();
return value
.filter(isObjectRecord)
.map((todo, index) => {
const content = typeof todo.content === "string" ? todo.content.trim() : "";
return {
id:
typeof todo.id === "string" && todo.id.trim()
? todo.id.trim()
: `todo-${index}-${content.slice(0, 24)}`,
content,
status: normalizeTodoStatus(
typeof todo.status === "string" ? todo.status : "pending",
),
priority: normalizeTodoPriority(
typeof todo.priority === "string" ? todo.priority : "",
),
updated_at: now,
};
})
.filter((todo) => todo.content.length > 0);
};
export const streamPromptResponse = async ({
@@ -227,15 +220,13 @@ export const streamPromptResponse = async ({
const iterator = eventStream[Symbol.asyncIterator]();
const requestStartedAt = Date.now();
const promptStartedAt = Date.now();
const progressStartedAtMap = new Map<string, number>();
const finalizedProgressIds = new Set<string>();
const emittedToolParts = new Set<string>();
const emittedActivityParts = new Set<string>();
const emittedQuestionToolParts = new Set<string>();
const emittedQuestionRequestIds = new Set<string>();
const currentAssistantMessageIds = new Set<string>();
const assistantTextParts = new Map<string, Map<string, string>>();
const assistantTextPartPhases = new Map<string, AssistantMessagePhase>();
const emittedFinalTextLengths = new Map<string, number>();
const partTypes = new Map<string, Part["type"]>();
const pendingTextDeltas = new Map<string, string[]>();
const reasoningStatuses = new Map<string, "running" | "completed">();
@@ -246,13 +237,12 @@ export const streamPromptResponse = async ({
let lastSessionStatus: string | null = null;
let lastSessionStatusMessage: string | null = null;
let sawResponseActivity = false;
let emittedText = false;
let finalAnswerText = "";
let toolCallCount = 0;
let done = false;
let promptSettled = false;
let aborted = signal?.aborted ?? false;
let failed = false;
let structuredFinalAnswerEmitted = false;
const debugContext = {
sessionId,
clientSessionId,
@@ -278,94 +268,24 @@ export const streamPromptResponse = async ({
})
: null;
const emitProgress = ({ id, phase, status, title, detail }: ProgressPayload) => {
if (status === "running" && finalizedProgressIds.has(id)) {
return;
}
const activityTracker = createActivityTracker({ clientSessionId, write });
const now = Date.now();
const startedAt = progressStartedAtMap.get(id) ?? now;
if (!progressStartedAtMap.has(id)) {
progressStartedAtMap.set(id, startedAt);
}
if (status === "running") {
write("progress", {
session_id: clientSessionId,
id,
phase,
status,
title,
detail,
started_at: startedAt,
elapsed_ms: Math.max(0, now - startedAt),
});
return;
}
const durationMs = Math.max(0, now - startedAt);
finalizedProgressIds.add(id);
progressStartedAtMap.delete(id);
write("progress", {
session_id: clientSessionId,
id,
phase,
status,
title,
detail,
started_at: startedAt,
ended_at: now,
duration_ms: durationMs,
});
const captureFinalText = (text: string) => {
const answer = text.trim();
if (answer) finalAnswerText = answer;
};
const emitFinalText = (partId: string, text: string) => {
const emittedLength = emittedFinalTextLengths.get(partId) ?? 0;
if (text.length <= emittedLength) {
return;
}
const content = text.slice(emittedLength);
emittedFinalTextLengths.set(partId, text.length);
emittedText = true;
write("token", {
session_id: clientSessionId,
content,
});
};
const emitCommentaryProgress = (
partId: string,
text: string,
completed: boolean,
) => {
if (!text.trim()) {
return;
}
emitProgress({
id: `commentary-${partId}`,
phase: "commentary",
status: completed ? "completed" : "running",
title: completed ? "Agent 过程已更新" : "Agent 正在处理",
detail: text,
});
};
emitProgress({
id: "request-received",
phase: "start",
status: "running",
title: "已收到请求,正在启动 Agent 分析",
detail: "已接收用户消息,正在建立会话并准备进入分析、规划和工具调用阶段。",
});
activityTracker.start(
initialActivity.id,
initialActivity.title,
initialActivity.reason,
);
const promptPromise = runtime
.prompt(
sessionId,
message,
toRuntimeModel(model),
requiresStructuredFinalAnswer(model)
? { format: STRUCTURED_FINAL_ANSWER_FORMAT }
: undefined,
)
.then(() => {
promptSettled = true;
@@ -388,6 +308,8 @@ export const streamPromptResponse = async ({
...debugContext,
});
let pendingIteratorNext: ReturnType<typeof iterator.next> | undefined;
try {
while (!done) {
if (signal?.aborted) {
@@ -399,8 +321,8 @@ export const streamPromptResponse = async ({
break;
}
const nextEvent = iterator
.next()
pendingIteratorNext ??= iterator.next();
const nextEvent = pendingIteratorNext
.then((result) => ({ type: "event" as const, result }));
const nextPrompt = promptSettled
? null
@@ -426,6 +348,7 @@ export const streamPromptResponse = async ({
if (next.type === "prompt") {
continue;
}
pendingIteratorNext = undefined;
if (next.result.done) {
break;
}
@@ -465,18 +388,6 @@ export const streamPromptResponse = async ({
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
});
}
emitProgress({
id: "session-status",
phase: "session",
status: event.properties.status.type === "idle" ? "completed" : "running",
title:
event.properties.status.type === "retry"
? `模型请求重试中:${event.properties.status.message}`
: event.properties.status.type === "busy"
? "Agent 正在处理请求"
: "Agent 已空闲",
detail: buildSessionStatusDetail(event.properties.status),
});
continue;
}
@@ -508,17 +419,6 @@ export const streamPromptResponse = async ({
patterns: event.properties.patterns,
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
});
emitProgress({
id: `permission-${event.properties.id}`,
phase: "permission",
status: permissionApproval.autoReject
? "error"
: permissionApproval.autoApprove
? "completed"
: "running",
title: permissionApproval.title,
detail: permissionApproval.detail ?? buildPermissionDetail(event),
});
if (permissionApproval.autoApprove || permissionApproval.autoReject) {
const reply = permissionApproval.autoReject ? "reject" : "once";
await runtime.replyPermission({
@@ -534,12 +434,15 @@ export const streamPromptResponse = async ({
});
continue;
}
const activity = activityTracker.getCurrentContext();
write("permission_request", {
session_id: clientSessionId,
request_id: event.properties.id,
permission: event.properties.permission,
patterns: event.properties.patterns,
target: getPermissionTarget(event.properties.metadata),
activity_id: activity?.id,
reason: activity?.reason,
always: event.properties.always,
tool: event.properties.tool,
created_at: Date.now(),
@@ -565,17 +468,6 @@ export const streamPromptResponse = async ({
resources: event.properties.resources,
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
});
emitProgress({
id: `permission-${event.properties.id}`,
phase: "permission",
status: permissionApproval.autoReject
? "error"
: permissionApproval.autoApprove
? "completed"
: "running",
title: permissionApproval.title,
detail: permissionApproval.detail ?? buildPermissionV2Detail(event),
});
if (permissionApproval.autoApprove || permissionApproval.autoReject) {
const reply = permissionApproval.autoReject ? "reject" : "once";
await runtime.replyPermission({
@@ -591,12 +483,15 @@ export const streamPromptResponse = async ({
});
continue;
}
const activity = activityTracker.getCurrentContext();
write("permission_request", {
session_id: clientSessionId,
request_id: event.properties.id,
permission: event.properties.action,
patterns: event.properties.resources,
target: getPermissionTarget(event.properties.metadata),
activity_id: activity?.id,
reason: activity?.reason,
always: event.properties.save ?? [],
tool: undefined,
created_at: Date.now(),
@@ -612,21 +507,6 @@ export const streamPromptResponse = async ({
reply: event.properties.reply,
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
});
emitProgress({
id: `permission-${event.properties.requestID}`,
phase: "permission",
status: event.properties.reply === "reject" ? "error" : "completed",
title:
event.properties.reply === "reject"
? "权限请求已拒绝"
: "权限请求已允许",
detail:
event.properties.reply === "always"
? "已允许本次请求,并记住同类权限。"
: event.properties.reply === "once"
? "已允许本次请求。"
: "已拒绝本次请求。",
});
write("permission_response", {
session_id: clientSessionId,
request_id: event.properties.requestID,
@@ -643,21 +523,6 @@ export const streamPromptResponse = async ({
reply: event.properties.reply,
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
});
emitProgress({
id: `permission-${event.properties.requestID}`,
phase: "permission",
status: event.properties.reply === "reject" ? "error" : "completed",
title:
event.properties.reply === "reject"
? "权限请求已拒绝"
: "权限请求已允许",
detail:
event.properties.reply === "always"
? "已允许本次请求,并记住同类权限。"
: event.properties.reply === "once"
? "已允许本次请求。"
: "已拒绝本次请求。",
});
write("permission_response", {
session_id: clientSessionId,
request_id: event.properties.requestID,
@@ -674,15 +539,6 @@ export const streamPromptResponse = async ({
questionCount: event.properties.questions.length,
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
});
emitProgress({
id: `question-${event.properties.id}`,
phase: "question",
status: "running",
title: "等待用户补充信息",
detail: event.properties.questions
.map((question) => question.question)
.join("\n"),
});
const payload = normalizeQuestionPayload(event, clientSessionId);
emittedQuestionRequestIds.add(payload.request_id);
write("question_request", payload);
@@ -696,16 +552,6 @@ export const streamPromptResponse = async ({
requestId: event.properties.requestID,
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
});
emitProgress({
id: `question-${event.properties.requestID}`,
phase: "question",
status: "completed",
title: "已收到补充信息",
detail: normalizeQuestionAnswers(event.properties.answers)
.map((answer) => answer.join("、"))
.filter(Boolean)
.join("\n"),
});
write("question_response", {
session_id: clientSessionId,
request_id: event.properties.requestID,
@@ -721,13 +567,6 @@ export const streamPromptResponse = async ({
requestId: event.properties.requestID,
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
});
emitProgress({
id: `question-${event.properties.requestID}`,
phase: "question",
status: "completed",
title: "已跳过补充信息",
detail: "用户选择跳过本次补充信息。",
});
write("question_response", {
session_id: clientSessionId,
request_id: event.properties.requestID,
@@ -738,11 +577,11 @@ export const streamPromptResponse = async ({
if (isSkillEvent(event)) {
sawResponseActivity = true;
const { name, reason, payload } = extractSkillAuditInfo(event);
const { name, payload } = extractSkillAuditInfo(event);
const activity = activityTracker.getCurrentContext();
logDevelopmentDebug("skill event received", {
...debugContext,
skill: name,
reason: reason || null,
payloadKeys: Object.keys(payload).slice(0, 8),
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
});
@@ -753,8 +592,9 @@ export const streamPromptResponse = async ({
traceId,
projectId,
target: name,
reason,
reasonProvided: Boolean(reason),
activityId: activity?.id,
activityTitle: activity?.title,
activityReason: activity?.reason,
payload,
}).catch((error) => {
logger.warn({ err: error }, "failed to write skill audit log");
@@ -780,9 +620,7 @@ export const streamPromptResponse = async ({
assistantTextParts.set(event.properties.messageID, messageParts);
const phase = assistantTextPartPhases.get(event.properties.partID) ?? "unknown";
if (phase === "final_answer") {
emitFinalText(event.properties.partID, text);
} else if (phase === "commentary") {
emitCommentaryProgress(event.properties.partID, text, false);
captureFinalText(text);
}
} else if (!partType) {
const pending = pendingTextDeltas.get(event.properties.partID) ?? [];
@@ -809,9 +647,7 @@ export const streamPromptResponse = async ({
messageParts.set(part.id, text);
assistantTextParts.set(part.messageID, messageParts);
if (phase === "final_answer") {
emitFinalText(part.id, text);
} else if (phase === "commentary") {
emitCommentaryProgress(part.id, text, Boolean(part.time?.end));
captureFinalText(text);
}
} else {
pendingTextDeltas.delete(part.id);
@@ -827,14 +663,6 @@ export const streamPromptResponse = async ({
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
});
}
const reasoningDetail = buildReasoningProgressDetail(part.time.end);
emitProgress({
id: part.id,
phase: "planning",
status: part.time.end ? "completed" : "running",
title: part.time.end ? "分析规划完成" : "正在规划分析步骤",
detail: reasoningDetail,
});
}
if (part.type === "tool") {
if (!firstToolEventLogged) {
@@ -849,7 +677,6 @@ export const streamPromptResponse = async ({
});
}
const toolParams = normalizeToolParams(part.state.input);
const reason = extractRequestReason(toolParams);
const isToolFinalState =
part.state.status === "completed" || part.state.status === "error";
const nextToolStatus = String(part.state.status);
@@ -861,7 +688,6 @@ export const streamPromptResponse = async ({
partId: part.id,
tool: part.tool,
status: nextToolStatus,
reason: reason || null,
inputKeys: Object.keys(toolParams).slice(0, 8),
error:
part.state.status === "error" ? (part.state.error ?? "unknown") : null,
@@ -869,7 +695,7 @@ export const streamPromptResponse = async ({
});
}
if (part.tool === STRUCTURED_OUTPUT_TOOL_NAME) {
if (part.tool === FINAL_ANSWER_TOOL_NAME) {
if (part.state.status === "error") {
logger.warn(
{
@@ -878,38 +704,71 @@ export const streamPromptResponse = async ({
partId: part.id,
error: part.state.error,
},
"structured final answer tool failed",
"final answer tool failed",
);
continue;
}
if (
(part.state.status !== "running" &&
part.state.status !== "completed") ||
structuredFinalAnswerEmitted
part.state.status !== "completed")
) {
continue;
}
const answer = extractStructuredAnswer(toolParams);
const answer = extractFinalAnswer(toolParams);
if (!answer) {
logger.warn(
{ sessionId, clientSessionId, partId: part.id },
"structured final answer tool received without an answer",
"final answer tool received without an answer",
);
continue;
}
structuredFinalAnswerEmitted = true;
emitFinalText(part.id, answer);
logDevelopmentDebug("final answer submitted through StructuredOutput", {
captureFinalText(answer);
logDevelopmentDebug("final answer submitted through tool", {
...debugContext,
partId: part.id,
tool: part.tool,
answerChars: answer.length,
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
});
continue;
}
if (part.tool === ACTIVITY_UPDATE_TOOL_NAME) {
const title = typeof toolParams.title === "string"
? toolParams.title.trim()
: "";
const reason = typeof toolParams.reason === "string"
? toolParams.reason.trim()
: "";
const todos = normalizeActivityTodos(toolParams.todos);
if (
title &&
reason &&
!emittedActivityParts.has(part.id) &&
(hasToolParams(toolParams) || isToolFinalState)
) {
emittedActivityParts.add(part.id);
const activity = activityTracker.start(part.id, title, reason, todos);
void writeLlmRequestAuditLog({
kind: "activity",
sessionId,
clientSessionId,
traceId,
projectId,
target: ACTIVITY_UPDATE_TOOL_NAME,
activityId: activity.id,
activityTitle: activity.title,
activityReason: activity.reason,
payload: toolParams,
}).catch((error) => {
logger.warn({ err: error }, "failed to write activity audit log");
});
}
continue;
}
const questionToolPayload = normalizeQuestionToolPayload(
part,
toolParams,
@@ -926,49 +785,23 @@ export const streamPromptResponse = async ({
questionCount: questionToolPayload.questions.length,
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
});
emitProgress({
id: `question-${questionToolPayload.request_id}`,
phase: "question",
status: "running",
title: "等待用户补充信息",
detail: questionToolPayload.questions
.map((question) => question.question)
.join("\n"),
});
write("question_request", questionToolPayload);
}
continue;
}
emitProgress({
id: part.id,
phase: "tool",
status: normalizeToolStatus(part.state.status),
title: getToolProgressTitle(part.tool, part.state.status),
detail: buildToolProgressDetail(
part.tool,
part.state.status,
toolParams,
reason,
part.state.status === "error" ? part.state.error : undefined,
),
});
if (part.tool === "todowrite" || part.tool === "todo") {
continue;
}
activityTracker.upsertAction(part, toolParams);
if (
!emittedToolParts.has(part.id) &&
(hasToolParams(toolParams) || isToolFinalState)
) {
emittedToolParts.add(part.id);
toolCallCount += 1;
if (!reason) {
logger.warn(
{
tool: part.tool,
sessionId: sessionId,
clientSessionId,
},
"llm tool request missing reason",
);
}
const activity = activityTracker.getActionContext(part.id);
void writeLlmRequestAuditLog({
kind: "tool",
sessionId: sessionId,
@@ -976,8 +809,9 @@ export const streamPromptResponse = async ({
traceId,
projectId,
target: part.tool,
reason,
reasonProvided: Boolean(reason),
activityId: activity?.id,
activityTitle: activity?.title,
activityReason: activity?.reason,
payload: toolParams,
}).catch((error) => {
logger.warn({ err: error }, "failed to write tool audit log");
@@ -986,7 +820,6 @@ export const streamPromptResponse = async ({
session_id: clientSessionId,
tool: part.tool,
params: toolParams,
reason,
});
}
}
@@ -1007,18 +840,6 @@ export const streamPromptResponse = async ({
priority: normalizeTodoPriority(todo.priority),
updated_at: Date.now(),
}));
const completed = todos.filter(
(todo) => todo.status === "completed",
).length;
emitProgress({
id: "todo-progress",
phase: "planning",
status: completed === todos.length ? "completed" : "running",
title: `计划进度 ${completed}/${todos.length}`,
detail: todos
.map((todo) => `${todo.status}: ${todo.content}`)
.join("\n"),
});
write("todo_update", {
session_id: clientSessionId,
todos: normalizedTodos,
@@ -1036,6 +857,7 @@ export const streamPromptResponse = async ({
? getErrorMessage(event.properties.error)
: "opencode session error",
});
activityTracker.finalize("error");
write("error", {
session_id: clientSessionId,
message: event.properties.error
@@ -1059,17 +881,10 @@ export const streamPromptResponse = async ({
}
logDevelopmentDebug("session idle received", {
...debugContext,
emittedText,
hasFinalAnswer: Boolean(finalAnswerText),
toolCallCount,
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
});
emitProgress({
id: "session-status",
phase: "session",
status: "completed",
title: "Agent 已完成处理",
detail: "当前会话已无待执行任务,正在收尾并准备返回最终结果。",
});
done = true;
}
}
@@ -1079,6 +894,7 @@ export const streamPromptResponse = async ({
...debugContext,
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
});
activityTracker.finalize("cancelled");
await runtime.abortSession(sessionId).catch((error) => {
logger.warn({ sessionId: sessionId, err: error }, "failed to abort opencode session");
});
@@ -1096,40 +912,34 @@ export const streamPromptResponse = async ({
}
await promptPromise;
if (!emittedText) {
emittedText = await emitFinalMessage(
if (!finalAnswerText) {
finalAnswerText = await resolveFinalMessage(
runtime,
sessionId,
clientSessionId,
currentAssistantMessageIds,
assistantTextParts,
assistantTextPartPhases,
write,
);
}
emitProgress({
id: "request-received",
phase: "start",
status: "completed",
title: "请求处理完成",
detail: "本次请求的分析、工具执行和结果整理流程已经完成。",
});
emitProgress({
id: "request-completed",
phase: "complete",
status: "completed",
title: "分析完成",
detail: emittedText
? "最终回答已生成并推送到前端。"
: "已完成分析,并通过兜底消息补发最终回答内容。",
});
activityTracker.finalize("completed");
if (finalAnswerText) {
// Keep one compatibility cycle for deployed frontends that only consume token.
write("token", {
session_id: clientSessionId,
content: finalAnswerText,
});
write("final_answer", {
session_id: clientSessionId,
content: finalAnswerText,
});
}
write("done", {
session_id: clientSessionId,
total_duration_ms: Math.max(0, Date.now() - requestStartedAt),
});
logDevelopmentDebug("chat stream completed", {
...debugContext,
emittedText,
hasFinalAnswer: Boolean(finalAnswerText),
toolCallCount,
totalDurationMs: Math.max(0, Date.now() - requestStartedAt),
});