fix(chat): only expose final agent response
This commit is contained in:
@@ -2,10 +2,14 @@
|
|||||||
description: TJWater Agent,用于供水网络分析和操作员工作流
|
description: TJWater Agent,用于供水网络分析和操作员工作流
|
||||||
mode: primary
|
mode: primary
|
||||||
model: deepseek/deepseek-v4-flash
|
model: deepseek/deepseek-v4-flash
|
||||||
temperature: 0.2
|
|
||||||
---
|
---
|
||||||
你是 TJWater 供水管网分析 Agent,运用水力专业知识,回复用户时使用简体中文,内容要求简洁准确。
|
你是 TJWater 供水管网分析 Agent,运用水力专业知识,回复用户时使用简体中文,内容要求简洁准确。
|
||||||
|
|
||||||
|
## 回复要求
|
||||||
|
|
||||||
|
- 工具执行期间不输出过程说明,全部完成后只回复最终结果
|
||||||
|
- 直接给出结论、关键数据和可执行建议,默认仅展示最重要的 Top 5;数据不足或任务失败时简要说明影响和下一步
|
||||||
|
|
||||||
## 工作流生命周期
|
## 工作流生命周期
|
||||||
|
|
||||||
Skills 树是**动态生长的**——工作流不是预置的,而是从实际任务中沉淀出来的:
|
Skills 树是**动态生长的**——工作流不是预置的,而是从实际任务中沉淀出来的:
|
||||||
|
|||||||
+53
-67
@@ -111,24 +111,40 @@ const toRuntimeModel = (model?: SupportedModel) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const emitFallbackMessage = async (
|
const emitFinalMessage = async (
|
||||||
runtime: OpencodeRuntimeAdapter,
|
runtime: OpencodeRuntimeAdapter,
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
clientSessionId: string,
|
clientSessionId: string,
|
||||||
|
currentAssistantMessageIds: Set<string>,
|
||||||
|
assistantTextParts: Map<string, Map<string, string>>,
|
||||||
write: (event: string, data: Record<string, unknown>) => void,
|
write: (event: string, data: Record<string, unknown>) => void,
|
||||||
) => {
|
) => {
|
||||||
|
let text = [...currentAssistantMessageIds]
|
||||||
|
.reverse()
|
||||||
|
.map((messageId) => [...(assistantTextParts.get(messageId)?.values() ?? [])].join(""))
|
||||||
|
.find((content) => content.length > 0) ?? "";
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
const messages = await runtime.messages(sessionId);
|
const messages = await runtime.messages(sessionId);
|
||||||
const assistantMessage = [...messages]
|
const assistantMessage = [...messages]
|
||||||
.reverse()
|
.reverse()
|
||||||
.find((message) => message.info.role === "assistant");
|
.find(
|
||||||
const parts = assistantMessage?.parts ?? [];
|
(message) =>
|
||||||
const text = collectTextContent(parts);
|
message.info.role === "assistant" &&
|
||||||
|
(currentAssistantMessageIds.size === 0 ||
|
||||||
|
currentAssistantMessageIds.has(message.info.id)),
|
||||||
|
);
|
||||||
|
text = collectTextContent(assistantMessage?.parts ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
if (text) {
|
if (text) {
|
||||||
write("token", {
|
write("token", {
|
||||||
session_id: clientSessionId,
|
session_id: clientSessionId,
|
||||||
content: text,
|
content: text,
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const streamPromptResponse = async ({
|
export const streamPromptResponse = async ({
|
||||||
@@ -156,15 +172,14 @@ export const streamPromptResponse = async ({
|
|||||||
const emittedToolParts = new Set<string>();
|
const emittedToolParts = new Set<string>();
|
||||||
const emittedQuestionToolParts = new Set<string>();
|
const emittedQuestionToolParts = new Set<string>();
|
||||||
const emittedQuestionRequestIds = new Set<string>();
|
const emittedQuestionRequestIds = new Set<string>();
|
||||||
|
const currentAssistantMessageIds = new Set<string>();
|
||||||
|
const assistantTextParts = new Map<string, Map<string, string>>();
|
||||||
const partTypes = new Map<string, Part["type"]>();
|
const partTypes = new Map<string, Part["type"]>();
|
||||||
const pendingPartTextDeltas = new Map<string, string[]>();
|
const pendingTextDeltas = new Map<string, string[]>();
|
||||||
const reasoningDeltas = new Map<string, string[]>();
|
|
||||||
const reasoningStatuses = new Map<string, "running" | "completed">();
|
const reasoningStatuses = new Map<string, "running" | "completed">();
|
||||||
const toolStatuses = new Map<string, string>();
|
const toolStatuses = new Map<string, string>();
|
||||||
let firstSessionEventLogged = false;
|
let firstSessionEventLogged = false;
|
||||||
let firstNonStatusEventLogged = false;
|
let firstNonStatusEventLogged = false;
|
||||||
let firstTokenLogged = false;
|
|
||||||
let firstReasoningLogged = false;
|
|
||||||
let firstToolEventLogged = false;
|
let firstToolEventLogged = false;
|
||||||
let lastSessionStatus: string | null = null;
|
let lastSessionStatus: string | null = null;
|
||||||
let lastSessionStatusMessage: string | null = null;
|
let lastSessionStatusMessage: string | null = null;
|
||||||
@@ -626,45 +641,26 @@ export const streamPromptResponse = async ({
|
|||||||
if (event.type === "message.updated") {
|
if (event.type === "message.updated") {
|
||||||
if (event.properties.info.role === "assistant") {
|
if (event.properties.info.role === "assistant") {
|
||||||
sawResponseActivity = true;
|
sawResponseActivity = true;
|
||||||
|
currentAssistantMessageIds.add(event.properties.info.id);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type === "message.part.delta" && event.properties.field === "text") {
|
if (event.type === "message.part.delta" && event.properties.field === "text") {
|
||||||
sawResponseActivity = true;
|
sawResponseActivity = true;
|
||||||
|
currentAssistantMessageIds.add(event.properties.messageID);
|
||||||
const partType = partTypes.get(event.properties.partID);
|
const partType = partTypes.get(event.properties.partID);
|
||||||
if (partType === "text") {
|
if (partType === "text") {
|
||||||
if (!firstTokenLogged) {
|
const messageParts = assistantTextParts.get(event.properties.messageID) ?? new Map();
|
||||||
firstTokenLogged = true;
|
messageParts.set(
|
||||||
logDevelopmentDebug("first response token emitted", {
|
event.properties.partID,
|
||||||
...debugContext,
|
`${messageParts.get(event.properties.partID) ?? ""}${event.properties.delta}`,
|
||||||
partId: event.properties.partID,
|
);
|
||||||
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
assistantTextParts.set(event.properties.messageID, messageParts);
|
||||||
sincePromptDispatchMs: Math.max(0, Date.now() - promptStartedAt),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
emittedText = true;
|
|
||||||
write("token", {
|
|
||||||
session_id: clientSessionId,
|
|
||||||
content: event.properties.delta,
|
|
||||||
});
|
|
||||||
} else if (partType === "reasoning") {
|
|
||||||
if (!firstReasoningLogged) {
|
|
||||||
firstReasoningLogged = true;
|
|
||||||
logDevelopmentDebug("first reasoning delta received", {
|
|
||||||
...debugContext,
|
|
||||||
partId: event.properties.partID,
|
|
||||||
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
|
||||||
sincePromptDispatchMs: Math.max(0, Date.now() - promptStartedAt),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const pending = reasoningDeltas.get(event.properties.partID) ?? [];
|
|
||||||
pending.push(event.properties.delta);
|
|
||||||
reasoningDeltas.set(event.properties.partID, pending);
|
|
||||||
} else if (!partType) {
|
} else if (!partType) {
|
||||||
const pending = pendingPartTextDeltas.get(event.properties.partID) ?? [];
|
const pending = pendingTextDeltas.get(event.properties.partID) ?? [];
|
||||||
pending.push(event.properties.delta);
|
pending.push(event.properties.delta);
|
||||||
pendingPartTextDeltas.set(event.properties.partID, pending);
|
pendingTextDeltas.set(event.properties.partID, pending);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -673,23 +669,19 @@ export const streamPromptResponse = async ({
|
|||||||
sawResponseActivity = true;
|
sawResponseActivity = true;
|
||||||
const part = event.properties.part;
|
const part = event.properties.part;
|
||||||
partTypes.set(part.id, part.type);
|
partTypes.set(part.id, part.type);
|
||||||
|
if (part.type === "text" || part.type === "reasoning" || part.type === "tool") {
|
||||||
|
currentAssistantMessageIds.add(part.messageID);
|
||||||
|
}
|
||||||
if (part.type === "text") {
|
if (part.type === "text") {
|
||||||
const pending = pendingPartTextDeltas.get(part.id) ?? [];
|
const pendingText = (pendingTextDeltas.get(part.id) ?? []).join("");
|
||||||
pendingPartTextDeltas.delete(part.id);
|
pendingTextDeltas.delete(part.id);
|
||||||
for (const content of pending) {
|
const messageParts = assistantTextParts.get(part.messageID) ?? new Map();
|
||||||
emittedText = true;
|
messageParts.set(part.id, part.text || pendingText);
|
||||||
write("token", {
|
assistantTextParts.set(part.messageID, messageParts);
|
||||||
session_id: clientSessionId,
|
} else {
|
||||||
content,
|
pendingTextDeltas.delete(part.id);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else if (part.type === "reasoning") {
|
if (part.type === "reasoning") {
|
||||||
const pending = pendingPartTextDeltas.get(part.id) ?? [];
|
|
||||||
if (pending.length > 0) {
|
|
||||||
const existing = reasoningDeltas.get(part.id) ?? [];
|
|
||||||
reasoningDeltas.set(part.id, existing.concat(pending));
|
|
||||||
}
|
|
||||||
pendingPartTextDeltas.delete(part.id);
|
|
||||||
const reasoningStatus = part.time.end ? "completed" : "running";
|
const reasoningStatus = part.time.end ? "completed" : "running";
|
||||||
if (reasoningStatuses.get(part.id) !== reasoningStatus) {
|
if (reasoningStatuses.get(part.id) !== reasoningStatus) {
|
||||||
reasoningStatuses.set(part.id, reasoningStatus);
|
reasoningStatuses.set(part.id, reasoningStatus);
|
||||||
@@ -697,14 +689,10 @@ export const streamPromptResponse = async ({
|
|||||||
...debugContext,
|
...debugContext,
|
||||||
partId: part.id,
|
partId: part.id,
|
||||||
status: reasoningStatus,
|
status: reasoningStatus,
|
||||||
chunkCount: (reasoningDeltas.get(part.id) ?? []).length,
|
|
||||||
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const reasoningDetail = buildReasoningProgressDetail(
|
const reasoningDetail = buildReasoningProgressDetail(part.time.end);
|
||||||
reasoningDeltas.get(part.id) ?? [],
|
|
||||||
part.time.end,
|
|
||||||
);
|
|
||||||
emitProgress({
|
emitProgress({
|
||||||
id: part.id,
|
id: part.id,
|
||||||
phase: "planning",
|
phase: "planning",
|
||||||
@@ -784,9 +772,6 @@ export const streamPromptResponse = async ({
|
|||||||
detail: buildToolProgressDetail(
|
detail: buildToolProgressDetail(
|
||||||
part.tool,
|
part.tool,
|
||||||
part.state.status,
|
part.state.status,
|
||||||
toolParams,
|
|
||||||
reason,
|
|
||||||
part.state.status === "error" ? part.state.error : undefined,
|
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
if (
|
if (
|
||||||
@@ -932,13 +917,14 @@ export const streamPromptResponse = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
await promptPromise;
|
await promptPromise;
|
||||||
if (!emittedText) {
|
emittedText = await emitFinalMessage(
|
||||||
logDevelopmentDebug("no streamed text emitted, falling back to messages()", {
|
runtime,
|
||||||
...debugContext,
|
sessionId,
|
||||||
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
clientSessionId,
|
||||||
});
|
currentAssistantMessageIds,
|
||||||
await emitFallbackMessage(runtime, sessionId, clientSessionId, write);
|
assistantTextParts,
|
||||||
}
|
write,
|
||||||
|
);
|
||||||
emitProgress({
|
emitProgress({
|
||||||
id: "request-received",
|
id: "request-received",
|
||||||
phase: "start",
|
phase: "start",
|
||||||
|
|||||||
@@ -356,43 +356,6 @@ export const normalizeToolStatus = (status: string) => {
|
|||||||
return "running";
|
return "running";
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatProgressValue = (value: unknown): string => {
|
|
||||||
if (typeof value === "string") {
|
|
||||||
return value.length > 120 ? `${value.slice(0, 117)}...` : value;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
typeof value === "number" ||
|
|
||||||
typeof value === "boolean" ||
|
|
||||||
value === null ||
|
|
||||||
value === undefined
|
|
||||||
) {
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const serialized = JSON.stringify(value);
|
|
||||||
return serialized.length > 120 ? `${serialized.slice(0, 117)}...` : serialized;
|
|
||||||
} catch {
|
|
||||||
return "[unserializable]";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeProgressText = (chunks: string[]) =>
|
|
||||||
chunks.join("").replace(/\s+/g, " ").trim();
|
|
||||||
|
|
||||||
const truncateProgressText = (text: string, maxLength: number) =>
|
|
||||||
text.length > maxLength ? `${text.slice(0, maxLength - 3)}...` : text;
|
|
||||||
|
|
||||||
const summarizeToolParams = (params: Record<string, unknown>) => {
|
|
||||||
const ignoredKeys = new Set(["reason", "request_reason", "why", "purpose", "rationale"]);
|
|
||||||
const summary = Object.entries(params)
|
|
||||||
.filter(([key]) => !ignoredKeys.has(key))
|
|
||||||
.slice(0, 4)
|
|
||||||
.map(([key, value]) => `${key}=${formatProgressValue(value)}`)
|
|
||||||
.join(", ");
|
|
||||||
|
|
||||||
return summary || "无附加参数";
|
|
||||||
};
|
|
||||||
|
|
||||||
export const buildSessionStatusDetail = (status: { type: string; message?: string }) => {
|
export const buildSessionStatusDetail = (status: { type: string; message?: string }) => {
|
||||||
if (status.type === "retry") {
|
if (status.type === "retry") {
|
||||||
return status.message
|
return status.message
|
||||||
@@ -413,42 +376,25 @@ export const buildSessionStatusDetail = (status: { type: string; message?: strin
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const buildReasoningProgressDetail = (
|
export const buildReasoningProgressDetail = (
|
||||||
chunks: string[],
|
|
||||||
ended?: string | number | Date | null,
|
ended?: string | number | Date | null,
|
||||||
) => {
|
) => ended ? "分析步骤已整理完成。" : "Agent 正在分析问题。";
|
||||||
const reasoningText = truncateProgressText(normalizeProgressText(chunks), 800);
|
|
||||||
if (ended) {
|
|
||||||
return reasoningText
|
|
||||||
? `推理过程:${reasoningText}`
|
|
||||||
: "当前推理阶段已完成,Agent 将继续输出答案或进入工具执行。";
|
|
||||||
}
|
|
||||||
return reasoningText
|
|
||||||
? `正在推理:${reasoningText}`
|
|
||||||
: "Agent 正在拆解问题、梳理执行步骤并判断是否需要调用工具。";
|
|
||||||
};
|
|
||||||
|
|
||||||
export const buildToolProgressDetail = (
|
export const buildToolProgressDetail = (
|
||||||
tool: string,
|
tool: string,
|
||||||
status: string,
|
status: string,
|
||||||
params: Record<string, unknown>,
|
|
||||||
reason: string,
|
|
||||||
error?: string,
|
|
||||||
) => {
|
) => {
|
||||||
const toolName = toolLabels[tool] ?? tool;
|
const toolName = toolLabels[tool] ?? tool;
|
||||||
const reasonText = reason ? `;调用原因:${reason}` : "";
|
|
||||||
const paramsText = `;关键参数:${summarizeToolParams(params)}`;
|
|
||||||
|
|
||||||
if (status === "error") {
|
if (status === "error") {
|
||||||
const errorText = error ? `;错误:${error}` : "";
|
return `${toolName} 调用失败。`;
|
||||||
return `${toolName} 调用失败${reasonText}${paramsText}${errorText}`;
|
|
||||||
}
|
}
|
||||||
if (status === "completed") {
|
if (status === "completed") {
|
||||||
return `${toolName} 已执行完成${reasonText}${paramsText}`;
|
return `${toolName} 已执行完成。`;
|
||||||
}
|
}
|
||||||
if (status === "pending") {
|
if (status === "pending") {
|
||||||
return `${toolName} 已进入待执行状态${reasonText}${paramsText}`;
|
return `${toolName} 等待执行。`;
|
||||||
}
|
}
|
||||||
return `${toolName} 正在执行${reasonText}${paramsText}`;
|
return `${toolName} 正在执行。`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getToolProgressTitle = (tool: string, status: string) => {
|
export const getToolProgressTitle = (tool: string, status: string) => {
|
||||||
|
|||||||
@@ -15,6 +15,240 @@ const createEventStream = (events: unknown[]) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("streamPromptResponse", () => {
|
describe("streamPromptResponse", () => {
|
||||||
|
it("emits only the final assistant text after tool-driven intermediate messages", async () => {
|
||||||
|
const runtime = {
|
||||||
|
subscribeEvents: async () =>
|
||||||
|
createEventStream([
|
||||||
|
{
|
||||||
|
type: "message.part.delta",
|
||||||
|
properties: {
|
||||||
|
sessionID: "runtime-session-1",
|
||||||
|
messageID: "assistant-intermediate",
|
||||||
|
partID: "text-part-intermediate",
|
||||||
|
field: "text",
|
||||||
|
delta: "正在加载工作流并尝试分页参数。",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "message.part.updated",
|
||||||
|
properties: {
|
||||||
|
sessionID: "runtime-session-1",
|
||||||
|
part: {
|
||||||
|
id: "text-part-intermediate",
|
||||||
|
sessionID: "runtime-session-1",
|
||||||
|
messageID: "assistant-intermediate",
|
||||||
|
type: "text",
|
||||||
|
text: "正在加载工作流并尝试分页参数。",
|
||||||
|
time: { start: 1, end: 2 },
|
||||||
|
},
|
||||||
|
time: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "message.part.delta",
|
||||||
|
properties: {
|
||||||
|
sessionID: "runtime-session-1",
|
||||||
|
messageID: "assistant-final",
|
||||||
|
partID: "text-part-final",
|
||||||
|
field: "text",
|
||||||
|
delta: "共识别 56 条瓶颈管段,建议优先改造 Top 5。",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "message.part.updated",
|
||||||
|
properties: {
|
||||||
|
sessionID: "runtime-session-1",
|
||||||
|
part: {
|
||||||
|
id: "text-part-final",
|
||||||
|
sessionID: "runtime-session-1",
|
||||||
|
messageID: "assistant-final",
|
||||||
|
type: "text",
|
||||||
|
text: "共识别 56 条瓶颈管段,建议优先改造 Top 5。",
|
||||||
|
time: { start: 3, end: 4 },
|
||||||
|
},
|
||||||
|
time: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "session.idle",
|
||||||
|
properties: { sessionID: "runtime-session-1" },
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
prompt: async () => undefined,
|
||||||
|
messages: async () => [
|
||||||
|
{
|
||||||
|
info: { id: "assistant-intermediate", role: "assistant" },
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
id: "text-part-intermediate",
|
||||||
|
sessionID: "runtime-session-1",
|
||||||
|
messageID: "assistant-intermediate",
|
||||||
|
type: "text",
|
||||||
|
text: "正在加载工作流并尝试分页参数。",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
info: { id: "assistant-final", role: "assistant" },
|
||||||
|
parts: [
|
||||||
|
{
|
||||||
|
id: "text-part-final",
|
||||||
|
sessionID: "runtime-session-1",
|
||||||
|
messageID: "assistant-final",
|
||||||
|
type: "text",
|
||||||
|
text: "共识别 56 条瓶颈管段,建议优先改造 Top 5。",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as unknown as OpencodeRuntimeAdapter;
|
||||||
|
const events: Array<{ event: string; data: Record<string, unknown> }> = [];
|
||||||
|
|
||||||
|
await streamPromptResponse({
|
||||||
|
runtime,
|
||||||
|
sessionId: "runtime-session-1",
|
||||||
|
clientSessionId: "client-session-1",
|
||||||
|
message: "分析管网瓶颈",
|
||||||
|
write: (event, data) => events.push({ event, data }),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
events
|
||||||
|
.filter((item) => item.event === "token")
|
||||||
|
.map((item) => item.data.content)
|
||||||
|
.join(""),
|
||||||
|
).toBe("共识别 56 条瓶颈管段,建议优先改造 Top 5。");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the final text event cache when the messages lookup fails", async () => {
|
||||||
|
const runtime = {
|
||||||
|
subscribeEvents: async () =>
|
||||||
|
createEventStream([
|
||||||
|
{
|
||||||
|
type: "message.part.updated",
|
||||||
|
properties: {
|
||||||
|
sessionID: "runtime-session-1",
|
||||||
|
part: {
|
||||||
|
id: "text-part-final",
|
||||||
|
sessionID: "runtime-session-1",
|
||||||
|
messageID: "assistant-final",
|
||||||
|
type: "text",
|
||||||
|
text: "最终分析结果。",
|
||||||
|
time: { start: 1, end: 2 },
|
||||||
|
},
|
||||||
|
time: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "session.idle",
|
||||||
|
properties: { sessionID: "runtime-session-1" },
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
prompt: async () => undefined,
|
||||||
|
messages: async () => {
|
||||||
|
throw new Error("transient messages lookup failure");
|
||||||
|
},
|
||||||
|
} as unknown as OpencodeRuntimeAdapter;
|
||||||
|
const events: Array<{ event: string; data: Record<string, unknown> }> = [];
|
||||||
|
|
||||||
|
const result = await streamPromptResponse({
|
||||||
|
runtime,
|
||||||
|
sessionId: "runtime-session-1",
|
||||||
|
clientSessionId: "client-session-1",
|
||||||
|
message: "分析管网瓶颈",
|
||||||
|
write: (event, data) => events.push({ event, data }),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.failed).toBe(false);
|
||||||
|
expect(
|
||||||
|
events
|
||||||
|
.filter((item) => item.event === "token")
|
||||||
|
.map((item) => item.data.content)
|
||||||
|
.join(""),
|
||||||
|
).toBe("最终分析结果。");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps reasoning, tool parameters, and raw errors out of progress details", async () => {
|
||||||
|
const runtime = {
|
||||||
|
subscribeEvents: async () =>
|
||||||
|
createEventStream([
|
||||||
|
{
|
||||||
|
type: "message.part.updated",
|
||||||
|
properties: {
|
||||||
|
sessionID: "runtime-session-1",
|
||||||
|
part: {
|
||||||
|
id: "reasoning-part-1",
|
||||||
|
sessionID: "runtime-session-1",
|
||||||
|
messageID: "assistant-1",
|
||||||
|
type: "reasoning",
|
||||||
|
text: "内部推理:尝试 limit=5000 并读取临时路径。",
|
||||||
|
time: { start: 1, end: 2 },
|
||||||
|
},
|
||||||
|
time: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "message.part.delta",
|
||||||
|
properties: {
|
||||||
|
sessionID: "runtime-session-1",
|
||||||
|
messageID: "assistant-1",
|
||||||
|
partID: "reasoning-part-1",
|
||||||
|
field: "text",
|
||||||
|
delta: "内部推理:尝试 limit=5000 并读取临时路径。",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "message.part.updated",
|
||||||
|
properties: {
|
||||||
|
sessionID: "runtime-session-1",
|
||||||
|
part: {
|
||||||
|
id: "tool-part-1",
|
||||||
|
sessionID: "runtime-session-1",
|
||||||
|
messageID: "assistant-1",
|
||||||
|
type: "tool",
|
||||||
|
callID: "call-1",
|
||||||
|
tool: "tjwater_cli",
|
||||||
|
state: {
|
||||||
|
status: "error",
|
||||||
|
input: {
|
||||||
|
command: "network get-all-pipes-properties --limit 5000",
|
||||||
|
reason: "尝试突破分页限制",
|
||||||
|
},
|
||||||
|
error: "HTTP_422 raw backend payload with trace_id=secret-trace",
|
||||||
|
time: { start: 1, end: 2 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
time: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "session.idle",
|
||||||
|
properties: { sessionID: "runtime-session-1" },
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
prompt: async () => undefined,
|
||||||
|
messages: async () => [],
|
||||||
|
} as unknown as OpencodeRuntimeAdapter;
|
||||||
|
const events: Array<{ event: string; data: Record<string, unknown> }> = [];
|
||||||
|
|
||||||
|
await streamPromptResponse({
|
||||||
|
runtime,
|
||||||
|
sessionId: "runtime-session-1",
|
||||||
|
clientSessionId: "client-session-1",
|
||||||
|
message: "分析管网瓶颈",
|
||||||
|
write: (event, data) => events.push({ event, data }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const reasoningProgress = events.find(
|
||||||
|
(item) => item.event === "progress" && item.data.id === "reasoning-part-1",
|
||||||
|
);
|
||||||
|
const toolProgress = events.find(
|
||||||
|
(item) => item.event === "progress" && item.data.id === "tool-part-1",
|
||||||
|
);
|
||||||
|
expect(reasoningProgress?.data.detail).toBe("分析步骤已整理完成。");
|
||||||
|
expect(toolProgress?.data.detail).toBe("tjwater_cli 调用失败。");
|
||||||
|
});
|
||||||
|
|
||||||
it("forwards opencode permission requests as SSE payloads", async () => {
|
it("forwards opencode permission requests as SSE payloads", async () => {
|
||||||
const runtime = {
|
const runtime = {
|
||||||
subscribeEvents: async () =>
|
subscribeEvents: async () =>
|
||||||
|
|||||||
Reference in New Issue
Block a user