import { type PermissionReply } from "../runtime/opencode.js"; import type { ResponseInputTracker } from "../chat/response-inputs.js"; import type { UIEnvelopePayload } from "../uiEnvelope/types.js"; import { type PermissionRequestPayload, type QuestionRequestPayload, type TodoUpdatePayload, } from "./chatStream.js"; export type RunStatus = "running" | "waiting_input" | "completed" | "error" | "aborted"; export type StreamSubscriber = { write: (event: string, data: Record) => void; close: () => void; }; export type ActiveRun = { inputs?: ResponseInputTracker; clientSessionId: string; controller: AbortController; messages: unknown[]; pendingPermissions: Map; pendingQuestions: Map; status: RunStatus; subscribers: Set; }; type ToolArtifactKind = "chart" | "map" | "panel" | "tool"; type ToolCallPayload = { session_id?: string; tool?: string; params?: unknown; reason?: string; }; export const isObjectRecord = (value: unknown): value is Record => typeof value === "object" && value !== null && !Array.isArray(value); const createFrontendMessageId = () => `msg-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; export const createInitialStreamingMessages = ( existingMessages: unknown[], userContent: string, ) => { const userMessage = { id: createFrontendMessageId(), role: "user", content: userContent, }; return [ ...existingMessages, { ...userMessage, branchRootId: userMessage.id, }, { id: createFrontendMessageId(), role: "assistant", content: "", progress: [ { id: "request-received", phase: "start", status: "running", title: "已收到请求,正在启动 Agent 分析", detail: "已接收用户消息,正在建立会话并准备进入分析、规划和工具调用阶段。", startedAt: Date.now(), elapsedMs: 0, elapsedSnapshotAt: Date.now(), }, ], }, ]; }; export const upsertBackendProgress = ( progress: unknown, payload: Record, ) => { const next = Array.isArray(progress) ? [...progress] : []; const id = typeof payload.id === "string" ? payload.id : `progress-${Date.now()}`; const index = next.findIndex((item) => isObjectRecord(item) && item.id === id); const existing = index >= 0 && isObjectRecord(next[index]) ? next[index] : undefined; const tool = isObjectRecord(payload.tool) && typeof payload.tool.name === "string" ? { name: payload.tool.name, callId: typeof payload.tool.call_id === "string" ? payload.tool.call_id : undefined, input: isObjectRecord(payload.tool.input) ? payload.tool.input : undefined } : existing?.tool; const nextItem = { id, tool, phase: typeof payload.phase === "string" ? payload.phase : "progress", status: payload.status === "completed" || payload.status === "error" ? payload.status : "running", title: typeof payload.title === "string" ? payload.title : "正在处理", detail: typeof payload.detail === "string" ? payload.detail : undefined, startedAt: typeof payload.started_at === "number" ? payload.started_at : undefined, endedAt: typeof payload.ended_at === "number" ? payload.ended_at : undefined, elapsedMs: typeof payload.elapsed_ms === "number" ? payload.elapsed_ms : undefined, elapsedSnapshotAt: typeof payload.elapsed_ms === "number" ? Date.now() : undefined, durationMs: typeof payload.duration_ms === "number" ? payload.duration_ms : undefined, }; if (index >= 0) { next[index] = nextItem; } else { next.push(nextItem); } return next; }; export const completeBackendProgress = (progress: unknown) => Array.isArray(progress) ? progress.map((item) => { if (!isObjectRecord(item) || item.status !== "running") { return item; } const endedAt = Date.now(); const startedAt = typeof item.startedAt === "number" ? item.startedAt : undefined; return { ...item, status: "completed", endedAt, elapsedMs: undefined, elapsedSnapshotAt: undefined, durationMs: typeof item.durationMs === "number" ? item.durationMs : startedAt !== undefined ? Math.max(0, endedAt - startedAt) : item.elapsedMs, }; }) : progress; export const cancelBackendTodos = (todos: unknown) => Array.isArray(todos) ? todos.map((todoUpdate) => { if (!isObjectRecord(todoUpdate) || !Array.isArray(todoUpdate.todos)) { return todoUpdate; } return { ...todoUpdate, todos: todoUpdate.todos.map((todo) => { if (!isObjectRecord(todo)) { return todo; } if (todo.status !== "pending" && todo.status !== "in_progress") { return todo; } return { ...todo, status: "cancelled", updatedAt: Date.now(), }; }), }; }) : todos; export const updateLastAssistantMessage = ( messages: unknown[], updater: (message: Record) => Record, ) => { for (let index = messages.length - 1; index >= 0; index -= 1) { const message = messages[index]; if (isObjectRecord(message) && message.role === "assistant") { const next = [...messages]; next[index] = updater(message); return next; } } return messages; }; export const updateLastAssistantPermission = ( messages: unknown[], requestId: string, updater: (permission: Record) => Record, ) => updateLastAssistantMessage(messages, (message) => { const permissions = Array.isArray(message.permissions) ? message.permissions : []; return { ...message, permissions: permissions.map((permission) => isObjectRecord(permission) && permission.requestId === requestId ? updater(permission) : permission, ), }; }); export const updateLastAssistantQuestion = ( messages: unknown[], requestId: string, updater: (question: Record) => Record, ) => updateLastAssistantMessage(messages, (message) => { const questions = Array.isArray(message.questions) ? message.questions : []; return { ...message, questions: questions.map((question) => isObjectRecord(question) && question.requestId === requestId ? updater(question) : question, ), }; }); const getToolArtifactKind = (tool: string): ToolArtifactKind => { if (tool === "render_scada_analysis" || tool === "clear_scada_analysis") { return "map"; } return "tool"; }; const getToolArtifactTitle = (tool: string, params: Record) => { if (typeof params.title === "string" && params.title.trim()) { return params.title.trim(); } return tool || "工具调用"; }; export const appendBackendToolArtifact = ( artifacts: unknown, payload: ToolCallPayload, ) => { const tool = typeof payload.tool === "string" ? payload.tool.trim() : ""; if (!tool) { return artifacts; } const params = isObjectRecord(payload.params) ? payload.params : {}; const next = Array.isArray(artifacts) ? [...artifacts] : []; next.push({ id: `${tool}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`, tool, kind: getToolArtifactKind(tool), title: getToolArtifactTitle(tool, params), description: typeof payload.reason === "string" && payload.reason.trim() ? payload.reason.trim() : undefined, params, }); return next; }; export const appendBackendUiEnvelope = ( uiEnvelopes: unknown, payload: UIEnvelopePayload, ) => { const next = Array.isArray(uiEnvelopes) ? [...uiEnvelopes] : []; next.push({ envelopeId: payload.envelope_id, createdAt: payload.created_at, envelope: payload.envelope, renderStatus: "pending", }); return next; }; export const toFrontendPermission = ( payload: PermissionRequestPayload, status: "pending" | "approved_once" | "approved_always" | "rejected" | "error" = "pending", ) => ({ requestId: payload.request_id, sessionId: payload.session_id, permission: payload.permission, patterns: payload.patterns, target: payload.target, always: payload.always, tool: payload.tool, createdAt: payload.created_at, status, }); const toFrontendQuestion = ( payload: QuestionRequestPayload, status: "pending" | "submitting" | "answered" | "rejected" | "error" = "pending", ) => ({ requestId: payload.request_id, sessionId: payload.session_id, questions: payload.questions, tool: payload.tool, createdAt: payload.created_at, status, }); export const toPermissionStatus = (reply: PermissionReply) => { if (reply === "always") return "approved_always"; if (reply === "once") return "approved_once"; return "rejected"; }; export const upsertBackendQuestion = ( questions: unknown, payload: QuestionRequestPayload, ) => { const next = Array.isArray(questions) ? [...questions] : []; const index = next.findIndex((item) => { if (!isObjectRecord(item)) return false; if (item.requestId === payload.request_id) return true; const tool = isObjectRecord(item.tool) ? item.tool : undefined; return Boolean( payload.tool?.callID && tool?.callID === payload.tool.callID, ); }); const nextItem = toFrontendQuestion(payload); if (index >= 0) { const current = next[index]; const currentRequestId = isObjectRecord(current) ? current.requestId : undefined; const currentTool = isObjectRecord(current) && isObjectRecord(current.tool) ? current.tool : undefined; const currentIsActionable = typeof currentRequestId === "string" && currentRequestId !== currentTool?.callID; const payloadIsToolPlaceholder = Boolean(payload.tool?.callID) && payload.request_id === payload.tool?.callID; next[index] = { ...(isObjectRecord(current) ? current : {}), ...(currentIsActionable && payloadIsToolPlaceholder ? { questions: nextItem.questions, tool: nextItem.tool, createdAt: nextItem.createdAt, } : nextItem), status: isObjectRecord(current) && current.status === "submitting" ? "submitting" : nextItem.status, }; } else { next.push(nextItem); } return next; }; export const upsertBackendTodoUpdate = ( _todos: unknown, payload: TodoUpdatePayload, ) => [ { sessionId: payload.session_id, messageId: payload.message_id, todos: payload.todos, createdAt: payload.created_at, }, ];