feat(opencode): migrate agent runtime to v2
This commit is contained in:
+240
-394
@@ -1,15 +1,13 @@
|
||||
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 { logger } from "../logger.js";
|
||||
import {
|
||||
type PermissionReply,
|
||||
type OpencodeRuntimeAdapter,
|
||||
type RuntimeEvent as OpencodeEvent,
|
||||
} from "../runtime/opencode.js";
|
||||
import {
|
||||
buildPermissionDetail,
|
||||
buildPermissionV2Detail,
|
||||
buildReasoningProgressDetail,
|
||||
buildSessionStatusDetail,
|
||||
buildToolProgressDetail,
|
||||
@@ -22,23 +20,15 @@ import {
|
||||
hasToolParams,
|
||||
isPermissionAskedEvent,
|
||||
isPermissionRepliedEvent,
|
||||
isPermissionV2AskedEvent,
|
||||
isPermissionV2RepliedEvent,
|
||||
isQuestionAskedEvent,
|
||||
isObjectRecord,
|
||||
isQuestionRejectedEvent,
|
||||
isQuestionRepliedEvent,
|
||||
isQuestionV2AskedEvent,
|
||||
isQuestionV2RejectedEvent,
|
||||
isQuestionV2RepliedEvent,
|
||||
isSessionEvent,
|
||||
isSkillEvent,
|
||||
logDevelopmentDebug,
|
||||
normalizeQuestionAnswers,
|
||||
normalizeQuestionPayload,
|
||||
normalizeQuestionToolPayload,
|
||||
normalizeTodoPriority,
|
||||
normalizeTodoStatus,
|
||||
normalizeToolParams,
|
||||
normalizeToolStatus,
|
||||
type PermissionRequestPayload,
|
||||
@@ -150,10 +140,7 @@ export const streamPromptResponse = async ({
|
||||
const progressStartedAtMap = new Map<string, number>();
|
||||
const finalizedProgressIds = new Set<string>();
|
||||
const emittedToolParts = new Set<string>();
|
||||
const emittedQuestionToolParts = new Set<string>();
|
||||
const emittedQuestionRequestIds = new Set<string>();
|
||||
const partTypes = new Map<string, Part["type"]>();
|
||||
const pendingPartTextDeltas = new Map<string, string[]>();
|
||||
const reasoningDeltas = new Map<string, string[]>();
|
||||
const reasoningStatuses = new Map<string, "running" | "completed">();
|
||||
const toolStatuses = new Map<string, string>();
|
||||
@@ -171,6 +158,10 @@ export const streamPromptResponse = async ({
|
||||
let promptSettled = false;
|
||||
let aborted = signal?.aborted ?? false;
|
||||
let failed = false;
|
||||
let pendingNextEvent: Promise<{
|
||||
type: "event";
|
||||
result: IteratorResult<OpencodeEvent>;
|
||||
}> | null = null;
|
||||
const debugContext = {
|
||||
sessionId,
|
||||
clientSessionId,
|
||||
@@ -279,7 +270,7 @@ export const streamPromptResponse = async ({
|
||||
break;
|
||||
}
|
||||
|
||||
const nextEvent = iterator
|
||||
pendingNextEvent ??= iterator
|
||||
.next()
|
||||
.then((result) => ({ type: "event" as const, result }));
|
||||
const nextPrompt = promptSettled
|
||||
@@ -290,7 +281,7 @@ export const streamPromptResponse = async ({
|
||||
);
|
||||
const next = await Promise.race(
|
||||
[
|
||||
...(nextPrompt ? [nextEvent, nextPrompt] : [nextEvent]),
|
||||
...(nextPrompt ? [pendingNextEvent, nextPrompt] : [pendingNextEvent]),
|
||||
...(abortPromise ? [abortPromise] : []),
|
||||
],
|
||||
);
|
||||
@@ -306,6 +297,7 @@ export const streamPromptResponse = async ({
|
||||
if (next.type === "prompt") {
|
||||
continue;
|
||||
}
|
||||
pendingNextEvent = null;
|
||||
if (next.result.done) {
|
||||
break;
|
||||
}
|
||||
@@ -326,11 +318,10 @@ export const streamPromptResponse = async ({
|
||||
}
|
||||
|
||||
if (event.type === "session.status") {
|
||||
const nextStatus = event.properties.status.type;
|
||||
const nextStatus = event.status.type;
|
||||
const nextStatusMessage =
|
||||
"message" in event.properties.status &&
|
||||
typeof event.properties.status.message === "string"
|
||||
? event.properties.status.message
|
||||
"message" in event.status && typeof event.status.message === "string"
|
||||
? event.status.message
|
||||
: null;
|
||||
if (
|
||||
nextStatus !== lastSessionStatus ||
|
||||
@@ -348,14 +339,14 @@ export const streamPromptResponse = async ({
|
||||
emitProgress({
|
||||
id: "session-status",
|
||||
phase: "session",
|
||||
status: event.properties.status.type === "idle" ? "completed" : "running",
|
||||
status: event.status.type === "idle" ? "completed" : "running",
|
||||
title:
|
||||
event.properties.status.type === "retry"
|
||||
? `模型请求重试中:${event.properties.status.message}`
|
||||
: event.properties.status.type === "busy"
|
||||
event.status.type === "retry"
|
||||
? `模型请求重试中:${event.status.message}`
|
||||
: event.status.type === "busy"
|
||||
? "Agent 正在处理请求"
|
||||
: "Agent 已空闲",
|
||||
detail: buildSessionStatusDetail(event.properties.status),
|
||||
detail: buildSessionStatusDetail(event.status),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -374,13 +365,13 @@ export const streamPromptResponse = async ({
|
||||
sawResponseActivity = true;
|
||||
logDevelopmentDebug("permission request received", {
|
||||
...debugContext,
|
||||
requestId: event.properties.id,
|
||||
permission: event.properties.permission,
|
||||
patterns: event.properties.patterns,
|
||||
requestId: event.request.id,
|
||||
permission: event.request.action,
|
||||
patterns: event.request.resources,
|
||||
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
||||
});
|
||||
emitProgress({
|
||||
id: `permission-${event.properties.id}`,
|
||||
id: `permission-${event.request.id}`,
|
||||
phase: "permission",
|
||||
status: approvalMode === "always" ? "completed" : "running",
|
||||
title: approvalMode === "always" ? "已自动允许权限请求" : "等待权限确认",
|
||||
@@ -391,70 +382,25 @@ export const streamPromptResponse = async ({
|
||||
});
|
||||
if (approvalMode === "always") {
|
||||
await runtime.replyPermission({
|
||||
requestId: event.properties.id,
|
||||
requestId: event.request.id,
|
||||
sessionId,
|
||||
reply: "always",
|
||||
});
|
||||
write("permission_response", {
|
||||
session_id: clientSessionId,
|
||||
request_id: event.properties.id,
|
||||
request_id: event.request.id,
|
||||
reply: "always" satisfies PermissionReply,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
write("permission_request", {
|
||||
session_id: clientSessionId,
|
||||
request_id: event.properties.id,
|
||||
permission: event.properties.permission,
|
||||
patterns: event.properties.patterns,
|
||||
target: getPermissionTarget(event.properties.metadata),
|
||||
always: event.properties.always,
|
||||
tool: event.properties.tool,
|
||||
created_at: Date.now(),
|
||||
} satisfies PermissionRequestPayload);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isPermissionV2AskedEvent(event)) {
|
||||
sawResponseActivity = true;
|
||||
logDevelopmentDebug("permission v2 request received", {
|
||||
...debugContext,
|
||||
requestId: event.properties.id,
|
||||
action: event.properties.action,
|
||||
resources: event.properties.resources,
|
||||
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
||||
});
|
||||
emitProgress({
|
||||
id: `permission-${event.properties.id}`,
|
||||
phase: "permission",
|
||||
status: approvalMode === "always" ? "completed" : "running",
|
||||
title: approvalMode === "always" ? "已自动允许权限请求" : "等待权限确认",
|
||||
detail:
|
||||
approvalMode === "always"
|
||||
? "当前批准模式为始终允许,已自动允许本次权限请求。"
|
||||
: buildPermissionV2Detail(event),
|
||||
});
|
||||
if (approvalMode === "always") {
|
||||
await runtime.replyPermission({
|
||||
requestId: event.properties.id,
|
||||
sessionId,
|
||||
reply: "always",
|
||||
});
|
||||
write("permission_response", {
|
||||
session_id: clientSessionId,
|
||||
request_id: event.properties.id,
|
||||
reply: "always" satisfies PermissionReply,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
write("permission_request", {
|
||||
session_id: clientSessionId,
|
||||
request_id: event.properties.id,
|
||||
permission: event.properties.action,
|
||||
patterns: event.properties.resources,
|
||||
target: getPermissionTarget(event.properties.metadata),
|
||||
always: event.properties.save ?? [],
|
||||
tool: undefined,
|
||||
request_id: event.request.id,
|
||||
permission: event.request.action,
|
||||
patterns: event.request.resources,
|
||||
target: getPermissionTarget(event.request.metadata),
|
||||
always: event.request.save ?? [],
|
||||
tool: event.request.tool,
|
||||
created_at: Date.now(),
|
||||
} satisfies PermissionRequestPayload);
|
||||
continue;
|
||||
@@ -464,78 +410,47 @@ export const streamPromptResponse = async ({
|
||||
sawResponseActivity = true;
|
||||
logDevelopmentDebug("permission request replied", {
|
||||
...debugContext,
|
||||
requestId: event.properties.requestID,
|
||||
reply: event.properties.reply,
|
||||
requestId: event.requestId,
|
||||
reply: event.reply,
|
||||
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
||||
});
|
||||
emitProgress({
|
||||
id: `permission-${event.properties.requestID}`,
|
||||
id: `permission-${event.requestId}`,
|
||||
phase: "permission",
|
||||
status: event.properties.reply === "reject" ? "error" : "completed",
|
||||
status: event.reply === "reject" ? "error" : "completed",
|
||||
title:
|
||||
event.properties.reply === "reject"
|
||||
event.reply === "reject"
|
||||
? "权限请求已拒绝"
|
||||
: "权限请求已允许",
|
||||
detail:
|
||||
event.properties.reply === "always"
|
||||
event.reply === "always"
|
||||
? "已允许本次请求,并记住同类权限。"
|
||||
: event.properties.reply === "once"
|
||||
: event.reply === "once"
|
||||
? "已允许本次请求。"
|
||||
: "已拒绝本次请求。",
|
||||
});
|
||||
write("permission_response", {
|
||||
session_id: clientSessionId,
|
||||
request_id: event.properties.requestID,
|
||||
reply: event.properties.reply satisfies PermissionReply,
|
||||
request_id: event.requestId,
|
||||
reply: event.reply satisfies PermissionReply,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isPermissionV2RepliedEvent(event)) {
|
||||
sawResponseActivity = true;
|
||||
logDevelopmentDebug("permission v2 request replied", {
|
||||
...debugContext,
|
||||
requestId: event.properties.requestID,
|
||||
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,
|
||||
reply: event.properties.reply satisfies PermissionReply,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isQuestionAskedEvent(event) || isQuestionV2AskedEvent(event)) {
|
||||
if (isQuestionAskedEvent(event)) {
|
||||
sawResponseActivity = true;
|
||||
logDevelopmentDebug("question request received", {
|
||||
...debugContext,
|
||||
requestId: event.properties.id,
|
||||
questionCount: event.properties.questions.length,
|
||||
requestId: event.request.id,
|
||||
questionCount: event.request.questions.length,
|
||||
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
||||
});
|
||||
emitProgress({
|
||||
id: `question-${event.properties.id}`,
|
||||
id: `question-${event.request.id}`,
|
||||
phase: "question",
|
||||
status: "running",
|
||||
title: "等待用户补充信息",
|
||||
detail: event.properties.questions
|
||||
detail: event.request.questions
|
||||
.map((question) => question.question)
|
||||
.join("\n"),
|
||||
});
|
||||
@@ -545,40 +460,40 @@ export const streamPromptResponse = async ({
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isQuestionRepliedEvent(event) || isQuestionV2RepliedEvent(event)) {
|
||||
if (isQuestionRepliedEvent(event)) {
|
||||
sawResponseActivity = true;
|
||||
logDevelopmentDebug("question request replied", {
|
||||
...debugContext,
|
||||
requestId: event.properties.requestID,
|
||||
requestId: event.requestId,
|
||||
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
||||
});
|
||||
emitProgress({
|
||||
id: `question-${event.properties.requestID}`,
|
||||
id: `question-${event.requestId}`,
|
||||
phase: "question",
|
||||
status: "completed",
|
||||
title: "已收到补充信息",
|
||||
detail: normalizeQuestionAnswers(event.properties.answers)
|
||||
detail: normalizeQuestionAnswers(event.answers)
|
||||
.map((answer) => answer.join("、"))
|
||||
.filter(Boolean)
|
||||
.join("\n"),
|
||||
});
|
||||
write("question_response", {
|
||||
session_id: clientSessionId,
|
||||
request_id: event.properties.requestID,
|
||||
answers: normalizeQuestionAnswers(event.properties.answers),
|
||||
request_id: event.requestId,
|
||||
answers: normalizeQuestionAnswers(event.answers),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isQuestionRejectedEvent(event) || isQuestionV2RejectedEvent(event)) {
|
||||
if (isQuestionRejectedEvent(event)) {
|
||||
sawResponseActivity = true;
|
||||
logDevelopmentDebug("question request rejected", {
|
||||
...debugContext,
|
||||
requestId: event.properties.requestID,
|
||||
requestId: event.requestId,
|
||||
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
||||
});
|
||||
emitProgress({
|
||||
id: `question-${event.properties.requestID}`,
|
||||
id: `question-${event.requestId}`,
|
||||
phase: "question",
|
||||
status: "completed",
|
||||
title: "已跳过补充信息",
|
||||
@@ -586,7 +501,7 @@ export const streamPromptResponse = async ({
|
||||
});
|
||||
write("question_response", {
|
||||
session_id: clientSessionId,
|
||||
request_id: event.properties.requestID,
|
||||
request_id: event.requestId,
|
||||
rejected: true,
|
||||
});
|
||||
continue;
|
||||
@@ -617,261 +532,209 @@ export const streamPromptResponse = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (event.type === "message.updated") {
|
||||
if (event.properties.info.role === "assistant") {
|
||||
sawResponseActivity = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.type === "message.part.delta" && event.properties.field === "text") {
|
||||
if (event.type === "text.delta") {
|
||||
sawResponseActivity = true;
|
||||
const partType = partTypes.get(event.properties.partID);
|
||||
if (partType === "text") {
|
||||
if (!firstTokenLogged) {
|
||||
firstTokenLogged = true;
|
||||
logDevelopmentDebug("first response token emitted", {
|
||||
...debugContext,
|
||||
partId: event.properties.partID,
|
||||
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
||||
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) {
|
||||
const pending = pendingPartTextDeltas.get(event.properties.partID) ?? [];
|
||||
pending.push(event.properties.delta);
|
||||
pendingPartTextDeltas.set(event.properties.partID, pending);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.type === "message.part.updated") {
|
||||
sawResponseActivity = true;
|
||||
const part = event.properties.part;
|
||||
partTypes.set(part.id, part.type);
|
||||
if (part.type === "text") {
|
||||
const pending = pendingPartTextDeltas.get(part.id) ?? [];
|
||||
pendingPartTextDeltas.delete(part.id);
|
||||
for (const content of pending) {
|
||||
emittedText = true;
|
||||
write("token", {
|
||||
session_id: clientSessionId,
|
||||
content,
|
||||
});
|
||||
}
|
||||
} else 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";
|
||||
if (reasoningStatuses.get(part.id) !== reasoningStatus) {
|
||||
reasoningStatuses.set(part.id, reasoningStatus);
|
||||
logDevelopmentDebug("reasoning part status changed", {
|
||||
...debugContext,
|
||||
partId: part.id,
|
||||
status: reasoningStatus,
|
||||
chunkCount: (reasoningDeltas.get(part.id) ?? []).length,
|
||||
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
||||
});
|
||||
}
|
||||
const reasoningDetail = buildReasoningProgressDetail(
|
||||
reasoningDeltas.get(part.id) ?? [],
|
||||
part.time.end,
|
||||
);
|
||||
emitProgress({
|
||||
id: part.id,
|
||||
phase: "planning",
|
||||
status: part.time.end ? "completed" : "running",
|
||||
title: part.time.end ? "分析规划完成" : "正在规划分析步骤",
|
||||
detail: reasoningDetail,
|
||||
if (!firstTokenLogged) {
|
||||
firstTokenLogged = true;
|
||||
logDevelopmentDebug("first response token emitted", {
|
||||
...debugContext,
|
||||
partId: event.partId,
|
||||
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
||||
sincePromptDispatchMs: Math.max(0, Date.now() - promptStartedAt),
|
||||
});
|
||||
}
|
||||
if (part.type === "tool") {
|
||||
if (!firstToolEventLogged) {
|
||||
firstToolEventLogged = true;
|
||||
logDevelopmentDebug("first tool event received", {
|
||||
...debugContext,
|
||||
partId: part.id,
|
||||
tool: part.tool,
|
||||
status: part.state.status,
|
||||
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
||||
sincePromptDispatchMs: Math.max(0, Date.now() - promptStartedAt),
|
||||
});
|
||||
}
|
||||
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);
|
||||
|
||||
if (toolStatuses.get(part.id) !== nextToolStatus) {
|
||||
toolStatuses.set(part.id, nextToolStatus);
|
||||
logDevelopmentDebug("tool part status changed", {
|
||||
...debugContext,
|
||||
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,
|
||||
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
||||
});
|
||||
}
|
||||
|
||||
const questionToolPayload = normalizeQuestionToolPayload(
|
||||
part,
|
||||
toolParams,
|
||||
clientSessionId,
|
||||
);
|
||||
if (questionToolPayload) {
|
||||
if (!emittedQuestionToolParts.has(part.id)) {
|
||||
emittedQuestionToolParts.add(part.id);
|
||||
emittedQuestionRequestIds.add(questionToolPayload.request_id);
|
||||
logDevelopmentDebug("question tool request received", {
|
||||
...debugContext,
|
||||
requestId: questionToolPayload.request_id,
|
||||
tool: part.tool,
|
||||
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 (
|
||||
!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",
|
||||
);
|
||||
}
|
||||
void writeLlmRequestAuditLog({
|
||||
kind: "tool",
|
||||
sessionId: sessionId,
|
||||
clientSessionId,
|
||||
traceId,
|
||||
projectId,
|
||||
target: part.tool,
|
||||
reason,
|
||||
reasonProvided: Boolean(reason),
|
||||
payload: toolParams,
|
||||
}).catch((error) => {
|
||||
logger.warn({ err: error }, "failed to write tool audit log");
|
||||
});
|
||||
write("tool_call", {
|
||||
session_id: clientSessionId,
|
||||
tool: part.tool,
|
||||
params: toolParams,
|
||||
reason,
|
||||
});
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.type === "todo.updated") {
|
||||
sawResponseActivity = true;
|
||||
const todos = event.properties.todos as Array<{
|
||||
content: string;
|
||||
status: string;
|
||||
priority: string;
|
||||
}>;
|
||||
const normalizedTodos = todos.map((todo, index) => ({
|
||||
id: `todo-${index}-${todo.content.slice(0, 24)}`,
|
||||
content: todo.content,
|
||||
status: normalizeTodoStatus(todo.status),
|
||||
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", {
|
||||
emittedText = true;
|
||||
write("token", {
|
||||
session_id: clientSessionId,
|
||||
todos: normalizedTodos,
|
||||
created_at: Date.now(),
|
||||
} satisfies TodoUpdatePayload);
|
||||
content: event.delta,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.type === "session.error") {
|
||||
if (event.type === "reasoning.updated") {
|
||||
sawResponseActivity = true;
|
||||
if (!firstReasoningLogged) {
|
||||
firstReasoningLogged = true;
|
||||
logDevelopmentDebug("first reasoning delta received", {
|
||||
...debugContext,
|
||||
partId: event.partId,
|
||||
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
||||
sincePromptDispatchMs: Math.max(0, Date.now() - promptStartedAt),
|
||||
});
|
||||
}
|
||||
const pending = reasoningDeltas.get(event.partId) ?? [];
|
||||
if (event.delta) {
|
||||
pending.push(event.delta);
|
||||
reasoningDeltas.set(event.partId, pending);
|
||||
}
|
||||
const reasoningStatus = event.completed ? "completed" : "running";
|
||||
if (reasoningStatuses.get(event.partId) !== reasoningStatus) {
|
||||
reasoningStatuses.set(event.partId, reasoningStatus);
|
||||
logDevelopmentDebug("reasoning part status changed", {
|
||||
...debugContext,
|
||||
partId: event.partId,
|
||||
status: reasoningStatus,
|
||||
chunkCount: pending.length,
|
||||
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
||||
});
|
||||
}
|
||||
emitProgress({
|
||||
id: event.partId,
|
||||
phase: "planning",
|
||||
status: event.completed ? "completed" : "running",
|
||||
title: event.completed ? "分析规划完成" : "正在规划分析步骤",
|
||||
detail: buildReasoningProgressDetail(
|
||||
reasoningDeltas.get(event.partId) ?? [],
|
||||
event.completed ? Date.now() : undefined,
|
||||
),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.type === "tool.updated") {
|
||||
sawResponseActivity = true;
|
||||
const part = event.part;
|
||||
if (!firstToolEventLogged) {
|
||||
firstToolEventLogged = true;
|
||||
logDevelopmentDebug("first tool event received", {
|
||||
...debugContext,
|
||||
partId: part.id,
|
||||
tool: part.tool,
|
||||
status: part.state.status,
|
||||
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
||||
sincePromptDispatchMs: Math.max(0, Date.now() - promptStartedAt),
|
||||
});
|
||||
}
|
||||
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);
|
||||
|
||||
if (toolStatuses.get(part.id) !== nextToolStatus) {
|
||||
toolStatuses.set(part.id, nextToolStatus);
|
||||
logDevelopmentDebug("tool part status changed", {
|
||||
...debugContext,
|
||||
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,
|
||||
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
||||
});
|
||||
}
|
||||
|
||||
if (part.tool === "question" || part.tool === "request_user_input") {
|
||||
// V2 emits the actionable form.created/question.asked event separately.
|
||||
// Do not expose the tool callID as a request ID: it cannot settle a form.
|
||||
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 (
|
||||
!emittedToolParts.has(part.id) &&
|
||||
(hasToolParams(toolParams) || isToolFinalState)
|
||||
) {
|
||||
emittedToolParts.add(part.id);
|
||||
toolCallCount += 1;
|
||||
if (!reason) {
|
||||
logger.warn(
|
||||
{
|
||||
tool: part.tool,
|
||||
sessionId,
|
||||
clientSessionId,
|
||||
},
|
||||
"llm tool request missing reason",
|
||||
);
|
||||
}
|
||||
void writeLlmRequestAuditLog({
|
||||
kind: "tool",
|
||||
sessionId,
|
||||
clientSessionId,
|
||||
traceId,
|
||||
projectId,
|
||||
target: part.tool,
|
||||
reason,
|
||||
reasonProvided: Boolean(reason),
|
||||
payload: toolParams,
|
||||
}).catch((error) => {
|
||||
logger.warn({ err: error }, "failed to write tool audit log");
|
||||
});
|
||||
write("tool_call", {
|
||||
session_id: clientSessionId,
|
||||
tool: part.tool,
|
||||
params: toolParams,
|
||||
reason,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.type === "session.execution.started") {
|
||||
emitProgress({
|
||||
id: "session-status",
|
||||
phase: "session",
|
||||
status: "running",
|
||||
title: "Agent 正在处理",
|
||||
detail: "OpenCode 已开始执行当前请求。",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.type === "session.execution.succeeded") {
|
||||
emitProgress({
|
||||
id: "session-status",
|
||||
phase: "session",
|
||||
status: "completed",
|
||||
title: "Agent 已完成处理",
|
||||
detail: "当前请求已执行完成,正在整理并返回结果。",
|
||||
});
|
||||
done = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.type === "session.execution.failed" || event.type === "session.error") {
|
||||
sawResponseActivity = true;
|
||||
const runtimeError = event.error;
|
||||
logDevelopmentDebug("session error received", {
|
||||
...debugContext,
|
||||
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
||||
error: event.properties.error
|
||||
? getErrorMessage(event.properties.error)
|
||||
error: runtimeError
|
||||
? getErrorMessage(runtimeError)
|
||||
: "opencode session error",
|
||||
});
|
||||
write("error", {
|
||||
session_id: clientSessionId,
|
||||
message: event.properties.error
|
||||
? getErrorMessage(event.properties.error)
|
||||
message: runtimeError
|
||||
? getErrorMessage(runtimeError)
|
||||
: "opencode session error",
|
||||
detail: event.properties.error?.name,
|
||||
detail: runtimeError?.name,
|
||||
total_duration_ms: Math.max(0, Date.now() - requestStartedAt),
|
||||
});
|
||||
failed = true;
|
||||
done = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.type === "session.execution.interrupted") {
|
||||
write("error", {
|
||||
session_id: clientSessionId,
|
||||
message: `OpenCode execution interrupted: ${event.reason}`,
|
||||
detail: "SessionExecutionInterrupted",
|
||||
total_duration_ms: Math.max(0, Date.now() - requestStartedAt),
|
||||
});
|
||||
failed = true;
|
||||
@@ -880,27 +743,10 @@ export const streamPromptResponse = async ({
|
||||
}
|
||||
|
||||
if (event.type === "session.idle") {
|
||||
if (!sawResponseActivity) {
|
||||
logDevelopmentDebug("ignoring session idle before response activity", {
|
||||
...debugContext,
|
||||
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
logDevelopmentDebug("session idle received", {
|
||||
logDevelopmentDebug("ignoring legacy session.idle event", {
|
||||
...debugContext,
|
||||
emittedText,
|
||||
toolCallCount,
|
||||
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
||||
});
|
||||
emitProgress({
|
||||
id: "session-status",
|
||||
phase: "session",
|
||||
status: "completed",
|
||||
title: "Agent 已完成处理",
|
||||
detail: "当前会话已无待执行任务,正在收尾并准备返回最终结果。",
|
||||
});
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user