import { Type, type Static } from "typebox"; import { StringEnum } from "@earendil-works/pi-ai"; import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import { getMarkdownTheme } from "@earendil-works/pi-coding-agent"; import { Container, Key, Markdown, matchesKey, Text } from "@earendil-works/pi-tui"; import { getGlobalConfigPath, loadClusterConfig, readOrCreateClusterConfig, writeClusterConfig } from "./config.ts"; import { clearLearningEvidence, getLearningProjectKey, loadLearningEvidence, normalizeTaskType, recordLearningEvidence, selectInitialLevel } from "./learning.ts"; import { ClusterDashboard } from "./dashboard.ts"; import { formatDuration } from "./duration.ts"; import { ClusterSettingsComponent, type AvailableModel } from "./settings.ts"; import { ClusterScheduler } from "./scheduler.ts"; import { renderStatusPanel } from "./status-panel.ts"; import { aggregateUsage, clusterUsage, formatCacheHitRate, formatTokenCount, taskUsage } from "./usage.ts"; import { cloneState, createRunPersistence, deleteHistoricalRun, finalizeInterruptedSnapshot, isClusterSnapshot, loadHistoricalSnapshots, summarizeState } from "./state.ts"; import { isActiveClusterStatus } from "./types.ts"; import type { ClusterDetails, ClusterRunResult, ClusterSnapshot, ClusterState, ClusterTaskInput, TaskStatus, UserDecision, UserDecisionRequest, } from "./types.ts"; const ClusterTaskSchema = Type.Object({ id: Type.String({ description: "稳定且唯一的任务 ID" }), title: Type.String({ description: "任务标题" }), taskType: Type.String({ minLength: 1, description: "稳定任务类型,建议使用技术栈/任务性质/作用范围格式,例如 typescript/bugfix/cross-module" }), task: Type.String({ description: "worker 要执行的具体任务" }), acceptanceCriteria: Type.Array(Type.String(), { description: "审核器逐条检查的验收标准" }), level: StringEnum(["low", "medium", "high"] as const, { description: "初始 worker 等级" }), dependsOn: Type.Optional(Type.Array(Type.String(), { description: "必须先完成的任务 ID" })), cwd: Type.Optional(Type.String({ description: "该任务的工作目录" })), }); const ClusterParamsSchema = Type.Object({ goal: Type.String({ description: "整个集群要完成的目标" }), tasks: Type.Array(ClusterTaskSchema, { description: "由主 agent 拆解的任务图" }), }); type ClusterParams = Static; type ClusterTaskParams = Static; let activeScheduler: ClusterScheduler | undefined; let lastSnapshot: ClusterSnapshot | undefined; let sessionGeneration = 0; let dashboardOpen = false; let settingsOpen = false; let dashboardRefresh: (() => void) | undefined; let statusPanelRefresh: (() => void) | undefined; let statusPanelDispose: (() => void) | undefined; let uiRefreshTimer: ReturnType | undefined; let elapsedRefreshTimer: ReturnType | undefined; const CLUSTER_OVERLAY_OPTIONS = { overlay: true, overlayOptions: { width: "88%", maxHeight: "80%", anchor: "center", margin: 2, }, } as const; interface DecisionPrompt { scheduler: ClusterScheduler; request: UserDecisionRequest; } let decisionPromptQueue: DecisionPrompt[] = []; let activeDecisionPrompt: DecisionPrompt | undefined; function currentState(): ClusterState | undefined { return activeScheduler?.state ?? lastSnapshot?.state; } function taskInputs(params: ClusterParams): ClusterTaskInput[] { return params.tasks.map((task: ClusterTaskParams) => ({ id: task.id, title: task.title, taskType: normalizeTaskType(task.taskType), task: task.task, acceptanceCriteria: [...task.acceptanceCriteria], level: task.level, dependsOn: [...(task.dependsOn ?? [])], cwd: task.cwd, })); } function validateTasks(tasks: ClusterTaskInput[], maxTasks: number): void { if (tasks.length === 0) throw new Error("subagent_cluster 至少需要一个任务"); if (tasks.length > maxTasks) throw new Error(`任务数量 ${tasks.length} 超过配置上限 ${maxTasks}`); const ids = new Set(); for (const task of tasks) { if (ids.has(task.id)) throw new Error(`任务 ID 重复:${task.id}`); ids.add(task.id); if (task.taskType.trim() === "") throw new Error(`任务 ${task.id} 的 taskType 不能为空`); if (task.acceptanceCriteria.length === 0) throw new Error(`任务 ${task.id} 缺少验收标准`); } for (const task of tasks) { for (const dependency of task.dependsOn ?? []) { if (!ids.has(dependency)) throw new Error(`任务 ${task.id} 依赖了不存在的任务 ${dependency}`); if (dependency === task.id) throw new Error(`任务 ${task.id} 不能依赖自己`); } } } function makeDetails(state: ClusterState, result?: ClusterRunResult): ClusterDetails { const counts = summarizeState(state); return { runId: state.runId, status: state.status, taskCount: state.tasks.length, completedCount: counts.completedCount, failedCount: counts.failedCount, pausedCount: counts.pausedCount, state, result, }; } function statusText(status: ClusterState["status"]): string { return { running: "运行中", paused: "已暂停", completed: "完成", failed: "失败", cancelled: "已取消" }[status]; } function thinkingLevelsForModel(model: { reasoning: boolean; thinkingLevelMap?: Partial> }): string[] { if (!model.reasoning) return ["off"]; const levels = ["minimal", "low", "medium", "high", "xhigh", "max"]; return ["off", ...levels.filter((level) => model.thinkingLevelMap?.[level] !== null)]; } function taskStatusText(status: TaskStatus): string { return { queued: "排队中", running: "运行中", reviewing: "审核中", paused_for_user: "等待用户决策", timed_out: "已超时", completed: "完成", failed: "失败", blocked: "阻塞", cancelled: "已取消", }[status]; } function indentText(text: string, prefix: string): string { return text .split(/\r?\n/) .map((line) => `${prefix}${line}`) .join("\n"); } function resultText(result: ClusterRunResult, state: ClusterState): string { const completedCount = result.tasks.filter((task) => task.status === "completed").length; const failedCount = result.tasks.filter((task) => ["failed", "blocked", "cancelled", "timed_out"].includes(task.status)).length; const taskDetails = result.tasks .map((task, index) => { const reason = task.review?.reason || task.error; const totalUsage = taskUsage(task); const workerUsage = aggregateUsage(task.attempts.map((attempt) => attempt.worker?.usage)); const reviewerUsage = aggregateUsage(task.attempts.map((attempt) => attempt.reviewer?.usage)); return [ ` ${index + 1}. ${task.id}`, ` 任务类型:${task.taskType}`, ` 等级:请求 ${task.requestedLevel},实际初始 ${task.initialLevel},当前 ${task.level}`, ` ${task.requestedLevel === task.initialLevel ? "等级选择说明" : "等级调整原因"}:${task.levelSelectionReason}`, ` 状态:${taskStatusText(task.status)}`, ` 运行时长:${formatDuration(task.startedAt, task.finishedAt, state.pausePeriods)}`, ` Token 总量:${formatTokenCount(totalUsage.totalTokens)}`, ` Token 明细:输入 ${formatTokenCount(totalUsage.input)},输出 ${formatTokenCount(totalUsage.output)}`, ` worker Token:${formatTokenCount(workerUsage.totalTokens)}`, ` 审核器 Token:${formatTokenCount(reviewerUsage.totalTokens)}`, reason ? " 结果:" : "", reason ? indentText(reason, " ") : "", ] .filter(Boolean) .join("\n"); }) .join("\n\n"); return [ "集群后台结果", "", "集群信息", ` 状态:${statusText(result.status)}`, ` ID:${result.runId}`, " 目标:", indentText(state.goal, " "), "", "执行概览", ` 总运行时长:${formatDuration(state.startedAt, state.finishedAt, state.pausePeriods)}`, ` 任务总数:${result.tasks.length}`, ` 已完成:${completedCount}`, ` 失败或阻塞:${failedCount}`, ` 总 Token:${formatTokenCount(result.usage.totalTokens)}`, ` Token 明细:输入 ${formatTokenCount(result.usage.input)},输出 ${formatTokenCount(result.usage.output)}`, ` 缓存命中率:${formatCacheHitRate(result.usage)}`, "", "任务明细", taskDetails, ].join("\n"); } function flushUiRefresh(): void { uiRefreshTimer = undefined; statusPanelRefresh?.(); dashboardRefresh?.(); } function clearUiRefresh(): void { if (uiRefreshTimer !== undefined) { clearTimeout(uiRefreshTimer); uiRefreshTimer = undefined; } if (elapsedRefreshTimer !== undefined) { clearInterval(elapsedRefreshTimer); elapsedRefreshTimer = undefined; } } function updateStatus(_ctx: ExtensionContext, state: ClusterState): void { if (!statusPanelRefresh && !dashboardRefresh) return; if (isActiveClusterStatus(state.status)) { if (elapsedRefreshTimer === undefined) { elapsedRefreshTimer = setInterval(() => { const current = currentState(); if (!current || !isActiveClusterStatus(current.status)) { if (elapsedRefreshTimer !== undefined) clearInterval(elapsedRefreshTimer); elapsedRefreshTimer = undefined; return; } flushUiRefresh(); }, 1_000); } } else if (elapsedRefreshTimer !== undefined) { clearInterval(elapsedRefreshTimer); elapsedRefreshTimer = undefined; } if (uiRefreshTimer !== undefined) return; uiRefreshTimer = setTimeout(flushUiRefresh, 100); } async function openDashboard(ctx: ExtensionContext, pi: ExtensionAPI): Promise { if (ctx.mode !== "tui") { ctx.ui.notify("集群 dashboard 只能在交互式 TUI 中打开", "warning"); return; } if (dashboardOpen) return; const historicalSnapshots = (await loadHistoricalSnapshots(ctx.cwd)).map((snapshot) => finalizeInterruptedSnapshot(snapshot)); const scheduler = activeScheduler; const activeState = scheduler?.state; const sessionSnapshot = lastSnapshot && lastSnapshot.state.runId !== activeState?.runId ? finalizeInterruptedSnapshot(lastSnapshot) : undefined; const clusters = [ ...(activeState ? [{ state: activeState, active: true }] : []), ...(sessionSnapshot ? [{ state: sessionSnapshot.state, active: false }] : []), ...historicalSnapshots .filter((snapshot) => snapshot.state.runId !== activeState?.runId && snapshot.state.runId !== sessionSnapshot?.state.runId) .map((snapshot) => ({ state: snapshot.state, active: false })), ]; if (clusters.length === 0) { ctx.ui.notify("当前 session 还没有集群运行记录", "info"); return; } dashboardOpen = true; try { await ctx.ui.custom( (tui, theme, _keybindings, done) => { dashboardRefresh = () => tui.requestRender(); const dashboard = new ClusterDashboard(tui, theme, clusters, { cancel: () => { if (!scheduler) return; clearDecisionPrompts(scheduler); scheduler.cancel(); }, togglePause: () => scheduler?.togglePause(), decide: (taskId, action) => { if (!scheduler) return; if (activeDecisionPrompt?.scheduler === scheduler && activeDecisionPrompt.request.taskId === taskId) activeDecisionPrompt = undefined; decisionPromptQueue = decisionPromptQueue.filter((prompt) => prompt.scheduler !== scheduler || prompt.request.taskId !== taskId); scheduler.decide(taskId, action); showNextDecisionPrompt(pi); }, deleteHistory: async (runId) => { if (scheduler?.state.runId === runId && (scheduler.state.status === "running" || scheduler.state.status === "paused")) { ctx.ui.notify("运行中的集群不能删除", "warning"); return false; } const confirmed = await ctx.ui.confirm( "删除集群历史", `确认删除集群历史 ${runId}?此操作不可撤销。`, ); if (!confirmed) return false; try { const deleted = await deleteHistoricalRun(ctx.cwd, runId); if (lastSnapshot?.state.runId === runId) { lastSnapshot = undefined; statusPanelRefresh?.(); } ctx.ui.notify(deleted ? `已删除集群历史 ${runId}` : `集群历史 ${runId} 已不存在,已从列表移除`, "info"); return true; } catch (error) { ctx.ui.notify(`删除集群历史失败:${error instanceof Error ? error.message : String(error)}`, "error"); return false; } }, close: done, }); return dashboard; }, CLUSTER_OVERLAY_OPTIONS, ); } finally { clearUiRefresh(); dashboardRefresh = undefined; dashboardOpen = false; statusPanelRefresh?.(); } } async function openSettings(ctx: ExtensionContext): Promise { if (ctx.mode !== "tui") { ctx.ui.notify("subagent-cluster 设置页只能在交互式 TUI 中打开", "warning"); return; } const models = ctx.modelRegistry.getAvailable(); if (models.length === 0) { ctx.ui.notify("当前没有可用模型,请先配置 provider 或登录 Pi", "error"); return; } const availableModels: AvailableModel[] = models.map((model) => ({ value: `${model.provider}/${model.id}`, label: `${model.provider}/${model.id}`, description: `${model.name || model.id} · thinking: ${thinkingLevelsForModel(model).join(", ")}`, thinkingLevels: thinkingLevelsForModel(model), })); const globalPath = getGlobalConfigPath(); const baseConfig = (await readOrCreateClusterConfig(globalPath, `${models[0].provider}/${models[0].id}`)).config; const config = JSON.parse(JSON.stringify(baseConfig)); settingsOpen = true; try { await ctx.ui.custom( (tui, theme, _keybindings, done) => new ClusterSettingsComponent(tui, theme, config, availableModels, thinkingLevelsForModel(ctx.model ?? models[0]), { onSave: async (nextConfig) => writeClusterConfig(globalPath, nextConfig), onClearLearningEvidence: async () => { const confirmed = await ctx.ui.confirm( "清除全部全局学习证据", "确认清除所有项目共享的全部学习证据?此操作不可撤销。", ); if (!confirmed) return "cancelled"; const cleared = await clearLearningEvidence(); return cleared ? "cleared" : "empty"; }, onClose: () => done(false), }), CLUSTER_OVERLAY_OPTIONS, ); } finally { settingsOpen = false; } } function installStatusPanel(ctx: ExtensionContext, pi: ExtensionAPI): void { let focusedRunId: string | undefined; const widgetKey = "subagent-cluster-status"; ctx.ui.setWidget( widgetKey, (tui, theme) => { statusPanelRefresh = () => tui.requestRender(); return { render: (width: number) => { const state = currentState(); return renderStatusPanel(state, theme, width, state?.runId === focusedRunId); }, invalidate: () => undefined, }; }, { placement: "belowEditor" }, ); const unsubscribe = ctx.ui.onTerminalInput((data) => { const state = currentState(); if (dashboardOpen || settingsOpen || !state || !isActiveClusterStatus(state.status)) return undefined; const focused = focusedRunId === state.runId; if (!focused && matchesKey(data, Key.down) && ctx.ui.getEditorText().length === 0) { focusedRunId = state.runId; statusPanelRefresh?.(); return { consume: true }; } if (!focused) return undefined; if (matchesKey(data, Key.enter)) { focusedRunId = undefined; statusPanelRefresh?.(); void openDashboard(ctx, pi); return { consume: true }; } if (matchesKey(data, Key.up) || matchesKey(data, Key.escape)) { focusedRunId = undefined; statusPanelRefresh?.(); return { consume: true }; } if (matchesKey(data, Key.down)) return { consume: true }; return undefined; }); statusPanelDispose = () => { unsubscribe(); ctx.ui.setWidget(widgetKey, undefined); statusPanelRefresh = undefined; }; } function decisionOptions(request: UserDecisionRequest): string[] { const options = ["1 接受当前结果", "2 重试当前等级"]; if (request.level !== "high") options.push("3 升级 worker 等级"); options.push(request.level === "high" ? "3 放弃任务" : "4 放弃任务"); return options; } function decisionPromptText(request: UserDecisionRequest): string { return [ `任务 ${request.taskId}(${request.title},${request.level} 级)需要用户决策。`, `审核意见:${request.review.reason}`, "请直接在主输入框回复选项编号或动作:", ...decisionOptions(request).map((option) => ` ${option}`), ].join("\n"); } function parseUserDecision(text: string, request: UserDecisionRequest): UserDecision["action"] | undefined { const value = text.trim().toLowerCase(); if (/^(1|a|accept|接受|通过|采用)(?:[ .::].*)?$/.test(value)) return "accept"; if (/^(2|r|retry|重试)(?:[ .::].*)?$/.test(value)) return "retry"; if (request.level !== "high" && /^(3|e|escalate|升级)(?:[ .::].*)?$/.test(value)) return "escalate"; const abandonOption = request.level === "high" ? "3" : "4"; if (new RegExp(`^(${abandonOption}|x|abandon|放弃|取消)(?:[ .::].*)?$`).test(value)) return "abandon"; return undefined; } function showNextDecisionPrompt(pi: ExtensionAPI): void { if (activeDecisionPrompt || decisionPromptQueue.length === 0) return; const next = decisionPromptQueue.shift(); if (!next) return; if (next.scheduler.signal.aborted) { next.scheduler.decide(next.request.taskId, "abandon"); showNextDecisionPrompt(pi); return; } activeDecisionPrompt = next; try { pi.sendMessage( { customType: "subagent-cluster-question", content: decisionPromptText(next.request), display: true, details: next.request, }, { triggerTurn: false }, ); } catch { activeDecisionPrompt = undefined; next.scheduler.decide(next.request.taskId, "abandon"); showNextDecisionPrompt(pi); } } function enqueueDecisionPrompt(pi: ExtensionAPI, scheduler: ClusterScheduler, request: UserDecisionRequest): void { decisionPromptQueue.push({ scheduler, request }); showNextDecisionPrompt(pi); } function clearDecisionPrompts(scheduler?: ClusterScheduler): void { if (!scheduler || activeDecisionPrompt?.scheduler === scheduler) activeDecisionPrompt = undefined; decisionPromptQueue = scheduler ? decisionPromptQueue.filter((prompt) => prompt.scheduler !== scheduler) : []; } function deliverBackgroundResult(pi: ExtensionAPI, result: ClusterRunResult, state: ClusterState): void { const details = makeDetails(state, result); pi.sendMessage( { customType: "subagent-cluster-background", content: resultText(result, state), display: true, details, }, { triggerTurn: true, deliverAs: "followUp" }, ); } export default function (pi: ExtensionAPI) { pi.on("session_start", async (_event, ctx) => { sessionGeneration += 1; activeScheduler = undefined; lastSnapshot = undefined; activeDecisionPrompt = undefined; decisionPromptQueue = []; for (const entry of ctx.sessionManager.getBranch()) { if (entry.type !== "custom" || entry.customType !== "subagent-cluster-run") continue; if (isClusterSnapshot(entry.data)) lastSnapshot = finalizeInterruptedSnapshot(entry.data); } if (ctx.mode === "tui") installStatusPanel(ctx, pi); if (lastSnapshot && ctx.mode === "tui") updateStatus(ctx, lastSnapshot.state); }); pi.on("session_shutdown", async (_event, _ctx) => { sessionGeneration += 1; clearUiRefresh(); clearDecisionPrompts(); const scheduler = activeScheduler; scheduler?.cancel(); await scheduler?.waitForCompletion(); activeScheduler = undefined; statusPanelDispose?.(); statusPanelDispose = undefined; }); pi.registerEntryRenderer("subagent-cluster-run", (entry, _options, theme) => { const data = entry.data as ClusterSnapshot; const counts = summarizeState(data.state); return new Text( theme.fg("accent", `集群 ${statusText(data.state.status)}`) + theme.fg("muted", ` · ${counts.completedCount}/${data.state.tasks.length} 完成 · 运行 ${formatDuration(data.state.startedAt, data.state.finishedAt, data.state.pausePeriods)} · ${data.state.goal}`), 0, 0, ); }); pi.registerMessageRenderer("subagent-cluster-background", (message, _options, theme) => { return new Text(theme.fg("accent", "集群后台结果\n") + String(message.content), 0, 0); }); pi.registerMessageRenderer("subagent-cluster-question", (message, _options, theme) => { return new Text(theme.fg("warning", "需要用户决策\n") + String(message.content), 0, 0); }); pi.on("input", (event, ctx) => { if (!activeDecisionPrompt || event.source === "extension") return { action: "continue" as const }; const prompt = activeDecisionPrompt; const action = parseUserDecision(event.text, prompt.request); if (!action) { ctx.ui.notify(`无法识别决策,请回复:${decisionOptions(prompt.request).join("、")}`, "warning"); return { action: "handled" as const }; } activeDecisionPrompt = undefined; prompt.scheduler.decide(prompt.request.taskId, action); showNextDecisionPrompt(pi); return { action: "handled" as const }; }); pi.registerCommand("subagent-cluster", { description: "打开 subagent 集群 workflow dashboard", handler: async (_args, ctx) => { await openDashboard(ctx, pi); }, }); pi.registerCommand("subagent-settings", { description: "配置 subagent-cluster 的模型、工具、调度和学习参数", handler: async (_args, ctx) => { await openSettings(ctx); }, }); pi.registerTool({ name: "subagent_cluster", label: "Subagent Cluster", description: [ "调度隔离的 Pi subagent 集群完成多步骤任务。", "主 agent 负责分析需求并提交结构化任务图;调度器负责依赖、并行、审核和 low→medium→high 自动升级。", "简单任务不要使用此工具;只有需要拆分、并行或独立审核的复杂任务才使用。", ].join(" "), promptSnippet: "将复杂的多步骤任务拆成带验收标准和依赖关系的任务图并交给 subagent 集群", promptGuidelines: [ "复杂且可以拆成独立子任务的编码需求使用 subagent_cluster。", "调用 subagent_cluster 前必须为每个任务定义非空 taskType 和具体的 acceptanceCriteria;taskType 使用技术栈/任务性质/作用范围格式。", "简单的单文件改动或直接问答不要使用 subagent_cluster。", ], parameters: ClusterParamsSchema, executionMode: "sequential", async execute(_toolCallId, params, _signal, _onUpdate, ctx) { if (activeScheduler) throw new Error(`已有集群正在运行:${activeScheduler.state.runId},请使用 /subagent-cluster 查看`); const loaded = loadClusterConfig(ctx.cwd); const tasks = taskInputs(params); validateTasks(tasks, loaded.config.maxTasks); const learningEvidence = loaded.config.learning.enabled ? await loadLearningEvidence(loaded.config) : []; const levelSelections = new Map(tasks.map((task) => [ task.id, selectInitialLevel(task, learningEvidence, loaded.config, getLearningProjectKey(task.cwd ?? ctx.cwd)), ])); if (!ctx.model) throw new Error("无法读取当前主 agent 模型,不能创建同模型审核器"); const runId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const generation = sessionGeneration; const persistence = await createRunPersistence(ctx.cwd, runId); let scheduler!: ClusterScheduler; scheduler = new ClusterScheduler({ runId, input: { goal: params.goal, tasks }, cwd: ctx.cwd, config: loaded.config, reviewerModel: loaded.config.reviewer.model ?? `${ctx.model.provider}/${ctx.model.id}`, reviewerThinkingLevel: loaded.config.reviewer.thinkingLevel ?? ctx.thinkingLevel, persistence, levelSelections, onRunFinished: async (state) => { await recordLearningEvidence(state, loaded.config); }, onChange: (state) => { if (generation === sessionGeneration) updateStatus(ctx, state); }, onPersist: (state, result) => { if (generation !== sessionGeneration) return; lastSnapshot = { version: 2, state: cloneState(state), result }; }, onUserDecision: ctx.mode === "tui" ? (request) => { if (generation === sessionGeneration) enqueueDecisionPrompt(pi, scheduler, request); } : undefined, }); activeScheduler = scheduler; updateStatus(ctx, scheduler.state); if (ctx.mode !== "tui") { const result = await scheduler.run(); if (activeScheduler === scheduler) activeScheduler = undefined; lastSnapshot = { version: 2, state: cloneState(scheduler.state), result }; return { content: [{ type: "text", text: resultText(result, scheduler.state) }], details: makeDetails(scheduler.state, result), }; } void scheduler.run().then((result) => { if (generation !== sessionGeneration) return; if (activeScheduler === scheduler) activeScheduler = undefined; lastSnapshot = { version: 2, state: cloneState(scheduler.state), result }; updateStatus(ctx, scheduler.state); deliverBackgroundResult(pi, result, scheduler.state); }).catch((error) => { if (generation !== sessionGeneration) return; if (activeScheduler === scheduler) activeScheduler = undefined; scheduler.cancel(); const result: ClusterRunResult = { runId: scheduler.state.runId, status: "failed", summary: `集群运行异常:${error instanceof Error ? error.message : String(error)}`, tasks: scheduler.state.tasks, usage: clusterUsage(scheduler.state.tasks), }; lastSnapshot = { version: 2, state: cloneState(scheduler.state), result }; updateStatus(ctx, scheduler.state); deliverBackgroundResult(pi, result, scheduler.state); }); return { content: [{ type: "text", text: `集群已在后台启动:${runId}。可按 ↓ 后 Enter,或使用 /subagent-cluster 查看。` }], details: makeDetails(scheduler.state), terminate: true, }; }, renderCall(args, theme) { return new Text(theme.fg("toolTitle", theme.bold("subagent_cluster ")) + theme.fg("accent", `${args.tasks.length} tasks`) + theme.fg("muted", ` · ${args.goal}`), 0, 0); }, renderResult(result, { expanded }, theme) { const details = result.details as ClusterDetails | undefined; if (!details) return new Text(result.content[0]?.type === "text" ? result.content[0].text : "(无结果)", 0, 0); const summary = result.content[0]?.type === "text" ? result.content[0].text : "(无结果)"; if (!expanded) return new Text(theme.fg(details.status === "failed" ? "error" : "success", summary), 0, 0); const container = new Container(); container.addChild(new Markdown(summary, 0, 0, getMarkdownTheme())); const clusterTokenUsage = details.result?.usage ?? clusterUsage(details.state.tasks); container.addChild(new Text(theme.fg("dim", `集群 Token:${formatTokenCount(clusterTokenUsage.totalTokens)}(输入 ${formatTokenCount(clusterTokenUsage.input)},输出 ${formatTokenCount(clusterTokenUsage.output)})· 缓存命中率 ${formatCacheHitRate(clusterTokenUsage)}`), 0, 0)); for (const task of details.state.tasks) { const totalUsage = taskUsage(task); const workerUsage = aggregateUsage(task.attempts.map((attempt) => attempt.worker?.usage)); const reviewerUsage = aggregateUsage(task.attempts.map((attempt) => attempt.reviewer?.usage)); const label = `${task.id} · ${taskStatusText(task.status)} · ${task.level} · ${task.attempts.length} attempts · Token ${formatTokenCount(totalUsage.totalTokens)} · 运行 ${formatDuration(task.startedAt, task.finishedAt, details.state.pausePeriods)}`; container.addChild(new Text(theme.fg(task.status === "completed" ? "success" : "error", label), 0, 0)); container.addChild(new Text(theme.fg("dim", ` worker Token:${formatTokenCount(workerUsage.totalTokens)} · 审核器 Token:${formatTokenCount(reviewerUsage.totalTokens)} · 输入 ${formatTokenCount(totalUsage.input)} · 输出 ${formatTokenCount(totalUsage.output)}`), 0, 0)); if (task.review) container.addChild(new Text(theme.fg("dim", `审核:${task.review.reason}`), 0, 0)); if (task.outputPath) container.addChild(new Text(theme.fg("dim", task.outputPath), 0, 0)); } return container; }, }); }