@zhushanwen/pi-subagent-workflow 8.11.0 → 8.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-subagent-workflow",
3
- "version": "8.11.0",
3
+ "version": "8.12.0",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "Unified subagent execution and multi-agent workflow orchestration for Pi — spawned-process agent runtime with sync/background modes, stateful workflow management with persistence, state machine, and execution tracing.",
@@ -58,9 +58,9 @@
58
58
  "@xyz-agent/session-delivery": "0.3.1",
59
59
  "@zhushanwen/pi-ext-guards": "0.3.0",
60
60
  "@zhushanwen/pi-extension-logger": "0.6.0",
61
- "@zhushanwen/subagent-core": "0.8.0",
62
- "@zhushanwen/pi-subagent-cli": "0.1.2",
63
- "@zhushanwen/zcode-subagent-cli": "0.2.0"
61
+ "@zhushanwen/subagent-core": "0.9.0",
62
+ "@zhushanwen/pi-subagent-cli": "0.2.0",
63
+ "@zhushanwen/zcode-subagent-cli": "0.2.1"
64
64
  },
65
65
  "peerDependencies": {
66
66
  "@earendil-works/pi-ai": "^0.84.4",
package/src/index.ts CHANGED
@@ -5,10 +5,13 @@
5
5
  * 注册项:3 tool(subagent + workflow + workflow-script)+ 2 command(subagents + workflows)
6
6
  * + messageRenderer(subagent-bg-notify)+ pi.__workflowRun + session 事件。
7
7
  *
8
- * 三层架构:
9
- * interface/ → 注册胶水(tools/commands/tui
10
- * orchestration/ workflow engine(launcher/lifecycle/error-recovery)
11
- * execution/ → subagents 执行运行时(SubagentService/session-runner/concurrency-pool)
8
+ * 包内结构(执行运行时已迁 packages/subagent-core,本包只留注册面与宿主适配):
9
+ * interface/ → 注册胶水(tools / commands / TUI 渲染 / GUI mappers
10
+ * host/ pi 宿主端口实现(HostServices / NotifyDomain 的 pi 侧兑现)
11
+ * injectors/ → 提示注入器(engine-awareness / model-list / resource-list …)
12
+ * session-lifecycle.ts → 会话生命周期装配 seam(测试可注入 fake 依赖)
13
+ *
14
+ * 架构导航见 docs/extensions/subagents/architecture.md。
12
15
  *
13
16
  * 设计基线:D-004(旧包不动)/ ADR-025(进程内执行)/ D-8(pi.__workflowRun 签名)。
14
17
  */
@@ -26,7 +29,7 @@ import { createPiHostServices, createPiNotifyDomainPorts } from "./host/pi-host.
26
29
  import { createInFlightReporter } from "./host/inflight-reporter.ts";
27
30
 
28
31
  import { bestEffort } from "@zhushanwen/subagent-core";
29
- // ═══ execution/ 层(subagents 核心 + 运行时) ═══
32
+ // ═══ core barrel 消费执行域(执行运行时住 packages/subagent-core) ═══
30
33
  // [U7] 引擎列表状态文件(registry → engines.json,GUI 引擎选择器数据源)
31
34
  import { syncEnginesFile } from "@zhushanwen/subagent-core";
32
35
  // [W11/DoD#5] registerPiEngine(inproc 'pi' 注册)已随内建引擎删除:registry 'pi'
@@ -53,7 +56,7 @@ import { registerSubagentTool } from "./interface/subagent-tool.ts";
53
56
  import { registerSubagentsCommand } from "./interface/subagents.ts";
54
57
  import { registerWorkflowTool } from "./interface/tool-workflow.ts";
55
58
  import { registerWorkflowScriptTool } from "./interface/tool-workflow-script.ts";
56
- // ═══ orchestration/ 层(workflow engine + infra) ═══
59
+ // ═══ core barrel 消费 workflow 域(引擎与 worker 住 packages/subagent-core) ═══
57
60
  import type { LauncherDeps } from "@zhushanwen/subagent-core";
58
61
  import { executeNestedWorkflow, runAndWait, type WorkflowRunResult } from "@zhushanwen/subagent-core";
59
62
  import {
@@ -273,7 +276,18 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
273
276
  scheduleTimeBudget(runId, deps, budgetTimeMs),
274
277
  onWorkflowCall: (name: string, args: Record<string, unknown>, parentRun: WorkflowRun) =>
275
278
  executeNestedWorkflow(name, args, parentRun, deps),
276
- streamSink: getSubagentService()?.getStreamSink() ?? undefined,
279
+ // [H2 W3] workflow agent() 统一派发入口(设计 §3.5):pump 侧 dispatchAgentCall
280
+ // 经此转调 SubagentService.executeWorkflowAgent——真实 record(origin:"workflow"
281
+ // + parentRunId)进 store、共享池/守护/journal 归 service 编排;parentRunId 由
282
+ // pump 补 run.runId。service 单例在 session_start 后必在(run 只能于 session 内
283
+ // 派发);null 时抛错由 pump 的 dispatchCall catch 兜底回发 failed result。
284
+ workflowAgentDispatch: (opts, parentRunId, signal) => {
285
+ const service = getSubagentService();
286
+ if (!service) {
287
+ throw new Error("workflow agent dispatch unavailable: subagent service not initialized");
288
+ }
289
+ return service.executeWorkflowAgent(opts, parentRunId, signal);
290
+ },
277
291
  log,
278
292
  };
279
293
  return deps;
@@ -486,9 +500,11 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
486
500
  // - beforeExit 是退出前最后事件,不 exit(自然退出)。
487
501
  // - idempotent guard(reapSpawnedChildrenOnShutdown 内)防多信号叠加重复 kill。
488
502
  //
489
- // 防线 iii(activate 互斥)已接入:subagent-service.ts 冷路径 resume 调
490
- // acquireActivateLock(含 30s 超时兜底,见 lifecycle-manager.ts ACTIVATE_LOCK_TIMEOUT_MS)。
491
- // 防线 ii(启动 scanOrphanProcesses)骨架就位,启动时接入待实现。
503
+ // 防线 iii(activate 互斥):未接线——acquireActivateLock 机制已随简化清扫删除
504
+ // (历史接线点随协议化重构消失,仅余自持单测)。当前的双写者防护由
505
+ // subagent-service resumesInFlight 集合守卫承担。
506
+ // 防线 ii(启动孤儿扫描):未接线,骨架已随 L2 死代码清扫删除(当前 piped stdio
507
+ // 下 stdin-EOF 自灭链覆盖崩溃路径,见 docs/design/v2-defense-ii-iii-resolution.md)。
492
508
  // ════════════════════════════════════════════════════════════
493
509
  process.on("SIGTERM", () => {
494
510
  reapSpawnedChildrenOnShutdown();
@@ -110,9 +110,6 @@ function fakeEngine(id: string, models: Array<{ id: string; name?: string }>): E
110
110
  run: async () => {
111
111
  throw new Error("not used in this test");
112
112
  },
113
- interact: async () => {
114
- throw new Error("not used in this test");
115
- },
116
113
  read: async () => ({ engineId: id, turns: [], source: "outcome-only" }),
117
114
  // 每次调用返回新数组实例(模拟 registry 每 turn 现值)——渲染必须与实例无关
118
115
  listModels: () => models.map((m) => ({ ...m })),
@@ -22,11 +22,12 @@
22
22
  import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
23
23
 
24
24
  import type { LauncherDeps } from "@zhushanwen/subagent-core";
25
- import { abortRun } from "@zhushanwen/subagent-core";
25
+ import { abortRun, getSubagentService } from "@zhushanwen/subagent-core";
26
26
  import type { WorkflowRun } from "@zhushanwen/subagent-core";
27
27
  import { parseWorkflowRpcCommand, type WorkflowRpcAction } from "./command-actions.ts";
28
28
  import { createWorkflowsView, type ViewActions } from "./views/WorkflowsView.ts";
29
29
  import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
30
+ import { LIST_LIMIT } from "./list-shared.ts";
30
31
 
31
32
  /** runId 截断长度(显示用)。 */
32
33
  const RUNID_SHORT = 8;
@@ -248,6 +249,12 @@ function sortedRuns(runs: Map<string, WorkflowRun>): WorkflowRun[] {
248
249
  *
249
250
  * ViewActions 通过 deps 调 lifecycle(abort),与 view 解耦——
250
251
  * view 单测可注入 mock actions(见 views/__tests__/WorkflowsView-signature.test.ts)。
252
+ *
253
+ * [H2 W3] live 进度数据源(设计 D2 进度源切换):view 经 store 查询
254
+ * collectRecordsByParentRunId(run.runId)(内存 ∪ 磁盘重建 ∪ manifest,LIST_LIMIT
255
+ * 口径)配对 running trace node 渲染实时进度。service 单例未就绪(理论上 run 只能
256
+ * 于 session 内存在,session_start 后必在;防御分支)时不注入——view 走终态
257
+ * result 渲染路径。
251
258
  */
252
259
  async function openView(
253
260
  run: WorkflowRun,
@@ -256,7 +263,11 @@ async function openView(
256
263
  deps: LauncherDeps,
257
264
  ): Promise<void> {
258
265
  const actions: ViewActions = {
259
- abort: (runId: string) => abortRun(runId, deps),
266
+ abort: (runId) => abortRun(runId, deps),
260
267
  };
261
- await createWorkflowsView(run, theme, ctx, actions, deps.store.stateFilePath(run.runId));
268
+ const service = getSubagentService();
269
+ const liveRecords = service
270
+ ? () => service.queries.collectRecordsByParentRunId(run.runId, LIST_LIMIT)
271
+ : undefined;
272
+ await createWorkflowsView(run, theme, ctx, actions, deps.store.stateFilePath(run.runId), liveRecords);
262
273
  }
@@ -494,6 +494,22 @@ export function formatElapsed(startedAt?: string, now: number = Date.now()): str
494
494
  return formatElapsedSeconds(Math.max(0, secs));
495
495
  }
496
496
 
497
+ /**
498
+ * Run status 行的 elapsed([H2 A2] done 后冻结)。
499
+ *
500
+ * 与 formatElapsed 的差异仅在 now 基准:completedAt 有值(run 已终态)时以
501
+ * completedAt 为基准 → elapsed 恒等于 completedAt - startedAt,不再随墙钟增长;
502
+ * running(completedAt 缺省)沿用 Date.now() 缺省实时跳动。时钟异常
503
+ * (completedAt < startedAt)由 formatElapsed 钳 0 兜底("0s")。
504
+ */
505
+ export function formatRunStatusElapsed(
506
+ startedAt?: string,
507
+ completedAt?: string,
508
+ now: number = Date.now(),
509
+ ): string {
510
+ return formatElapsed(startedAt, completedAt ? new Date(completedAt).getTime() : now);
511
+ }
512
+
497
513
  /**
498
514
  * Format a live eventLog entry(live 路径 Activity 区用)。
499
515
  *
@@ -128,6 +128,13 @@ export const SubagentParams = Type.Object({
128
128
  includeFinished: Type.Optional(Type.Boolean({
129
129
  description: "Include finished (done/failed/cancelled) records. Default false (running only).",
130
130
  })),
131
+ includeWorkflow: Type.Optional(Type.Boolean({
132
+ description:
133
+ "Include records dispatched by workflow scripts (agent() calls). Default false — subagents " +
134
+ "spawned inside workflows are hidden from list output. Set true only when troubleshooting a " +
135
+ "workflow run's subagents (e.g. a workflow step failed and you need to inspect its records). " +
136
+ "Pair with includeFinished:true — finished workflow records stay hidden unless both flags are set.",
137
+ })),
131
138
  limit: Type.Optional(Type.Number({
132
139
  description: "Max items to return. Default 20, clamped to [1, 100].",
133
140
  })),
@@ -90,7 +90,7 @@ function isModelOverrideObj(a: unknown): a is { model?: unknown; thinkingLevel?:
90
90
  /**
91
91
  * start 路径类参数(skillPath / cwd)运行时守卫:绝对路径 + 禁 `..` 穿越。
92
92
  *
93
- * 校验链事实(pi 0.84.1 实装,登记 PS-20):pi agent-loop 对注册 typebox schema
93
+ * 校验链事实(pi 0.84.4 实装,登记 PS-20):pi agent-loop 对注册 typebox schema
94
94
  * 有运行时强校验——agent-loop.js:403-404 在 beforeToolCall / execute 之前调
95
95
  * validateToolArguments(pi-ai validation.js:247:Value.Convert :249 + Compile :210
96
96
  * + Check :265,失败 throw `Validation failed for tool` :272-273)→ catch 走
@@ -165,7 +165,7 @@ action:"list" before action:"start" — a reusable subagent may exist; compactio
165
165
  - action:"start" — run a subagent. Pass task and slug as top-level fields (REQUIRED). Optional: agent, model, thinkingLevel, engine, collect, skillPath, appendSystemPrompt, schema, maxTurns, graceTurns, fork, worktree, cwd, conversation, idleTimeoutMs. Background only: returns a subagentId immediately, notifies on completion.
166
166
  - action:"message" — send a follow-up to a running subagent (conversation-mode or one-shot); full context retained. REQUIRED messageParam: { subagentId, text }. Optional: interrupt (default false). The reply auto-notifies.
167
167
  - action:"close" — end a running subagent and release its resources. REQUIRED closeParam: { subagentId }. Optional: force (default false = let the current round finish; true terminates mid-round).
168
- - action:"list" — list subagents. listParam: { includeFinished?, limit? } (all optional). Read an item's sessionFile for full detail.
168
+ - action:"list" — list subagents. listParam: { includeFinished?, includeWorkflow?, limit? } (all optional; includeWorkflow defaults false — workflow-dispatched subagents are hidden unless true). Read an item's sessionFile for full detail.
169
169
  - action:"cancel" — stop a background subagent (for conversation-mode use close). REQUIRED cancelParam: { subagentId }.
170
170
  - action:"fork-from" — restart-disconnect recovery: spawn a NEW subagent inheriting the old one's history via --fork. REQUIRED forkFromParam: { sourceSubagentId }. Optional: prompt (continuation; default handover frame). Returns { newSubagentId, sourceSessionFile }. Rejects cancelled / worktree-bound / still-running sources.
171
171
 
@@ -41,7 +41,7 @@ export interface SubagentDirectiveDetails {
41
41
  /**
42
42
  * 定向消息留痕:向主 session 落 subagent-directive custom_message entry。
43
43
  *
44
- * 按主 agent streaming 状态分流 sendMessage options。pi 0.84.1 sendCustomMessage
44
+ * 按主 agent streaming 状态分流 sendMessage options。pi 0.84.4 sendCustomMessage
45
45
  * 实装(agent-session.js):isStreaming 且无 deliverAs 时默认 agent.steer()——会把
46
46
  * 定向消息注入正在运行的主 agent LLM turn,违反「不经主 agent LLM 直达 subagent」。
47
47
  * 故按调用时刻的权威 streaming 状态(ctx.isIdle(),与 sendCustomMessage 内部
@@ -52,7 +52,7 @@ import {
52
52
  type ReentryGuardRef,
53
53
  releaseReentryGuard,
54
54
  } from "./reentry-guard.ts";
55
- import { formatElapsed, renderTextFallback } from "./format.ts";
55
+ import { formatRunStatusElapsed, renderTextFallback } from "./format.ts";
56
56
  import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
57
57
 
58
58
  // ── Parameter schema ─────────────────────────────────────────
@@ -489,7 +489,9 @@ function actionStatus(deps: LauncherDeps): ToolResult {
489
489
  }
490
490
  const summaries = runs.map((r) => toRunSummary(r, deps.store));
491
491
  const lines = summaries.map((s) => {
492
- const duration = s.startedAt ? ` (${formatElapsed(s.startedAt)})` : "";
492
+ // [H2 A2] run done elapsed 冻结于 completedAt(formatRunStatusElapsed 内切
493
+ // now 基准),不再随每次 status 查询的墙钟增长。
494
+ const duration = s.startedAt ? ` (${formatRunStatusElapsed(s.startedAt, s.completedAt)})` : "";
493
495
  const reasonSuffix = s.reason && s.reason !== "completed" ? ` [${s.reason}]` : "";
494
496
  return `[${s.status}${reasonSuffix}] ${s.name} (${s.runId.slice(0, RUNID_SHORT)})${duration}${s.error ? ` error: ${s.error}` : ""}`;
495
497
  });
@@ -30,14 +30,12 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
30
30
  import { Key, matchesKey, visibleWidth } from "@earendil-works/pi-tui";
31
31
 
32
32
  import {
33
- countAllToolCalls,
34
- getAllToolCalls,
35
- projectLiveProgress,
33
+ displayAgentName,
34
+ saveWorkflow,
36
35
  } from "@zhushanwen/subagent-core";
37
36
  import type { ExecutionTraceNode } from "@zhushanwen/subagent-core";
37
+ import type { SubagentRecord } from "@zhushanwen/subagent-core";
38
38
  import type { WorkflowRun } from "@zhushanwen/subagent-core";
39
- import { saveWorkflow } from "@zhushanwen/subagent-core";
40
- import { displayAgentName } from "@zhushanwen/subagent-core";
41
39
  import {
42
40
  buildPhaseGroups,
43
41
  ELLIPSIS,
@@ -65,7 +63,15 @@ import {
65
63
  } from "./view-constants.ts";
66
64
 
67
65
  // L2 详情内容构建 + 滚动按键(纯函数)抽到 detail-content.ts(view 与其测试直接 import)。
68
- import { buildDetailContent, detailContentLength, type DetailScrollContext, processDetailKey } from "./detail-content.ts";
66
+ // [H2 W3] live 进度数据源 = store record 投影(LiveProgressView,替代 node.live)。
67
+ import {
68
+ buildDetailContent,
69
+ detailContentLength,
70
+ projectRecordProgress,
71
+ type DetailScrollContext,
72
+ type LiveProgressView,
73
+ processDetailKey,
74
+ } from "./detail-content.ts";
69
75
 
70
76
  // ── TUI layout constants ──────────────────────────────────────
71
77
 
@@ -111,10 +117,60 @@ export interface ViewActions {
111
117
 
112
118
  // ── 渲染签名(IF11/TC7/DM6:tick 条件失效的判据,渲染动态字段 SSOT)──────
113
119
 
120
+ // ── [H2 W3] live 数据源:store record 配对(设计 D2 进度源切换)─────────
121
+
122
+ /**
123
+ * 查询 + 配对:本 run 的 store records → running trace node 的 live 进度投影。
124
+ *
125
+ * **record ↔ trace node 对应关系(opts 标签映射 + 时间最近邻)**:
126
+ * - 候选 = 查询结果中 status==="running" 的 record(执行期 record 恒在内存源;
127
+ * archive 后的终态 record 从磁盘 light 重建,无 eventLog——但终态节点的渲染走
128
+ * node.result 终态摘要路径,不消费 record 投影);
129
+ * - 标签匹配 = `record.task === node.task`(record.task = opts.prompt,与
130
+ * node.task 同源——workflowCallToExecuteOptions 的 task 映射单点);
131
+ * - 多候选(parallel 同 prompt)时取 startedAt 与 node.startedAt 差绝对值最小者
132
+ * (record 创建紧随 dispatch,pi 快路径下仅隔数个 microtask;非 pi 引擎 probe
133
+ * 异步时仍是最优最近邻);配对后从候选池移除(贪心一一)。
134
+ *
135
+ * 精度语义:不同 prompt/agent 的节点精确匹配;完全同构(prompt+agent 均同)的
136
+ * parallel 孪生节点匹配到两者之一(数值可能在孪生间互换,形态不变)。running
137
+ * node 无匹配 record(重试间隙——上个 record 已 failed、新 record 未创建)时
138
+ * 走终态 fallback 渲染(与旧 live 缺失路径同形态)。
139
+ */
140
+ export function collectNodeLiveProgress(
141
+ run: WorkflowRun,
142
+ records: SubagentRecord[],
143
+ ): Map<number, LiveProgressView> {
144
+ const candidates = records.filter((r) => r.status === "running");
145
+ const result = new Map<number, LiveProgressView>();
146
+ for (const node of run.state.trace.toArray()) {
147
+ if (node.status !== "running") continue;
148
+ const nodeStarted = node.startedAt !== undefined ? Date.parse(node.startedAt) : Number.NaN;
149
+ let bestIdx = -1;
150
+ let bestDelta = Number.POSITIVE_INFINITY;
151
+ for (let i = 0; i < candidates.length; i++) {
152
+ const rec = candidates[i]!;
153
+ if (rec.task !== node.task) continue;
154
+ // startedAt 解析失败(防御,正常不可达)排最后——不与有限值比较出假精度。
155
+ const delta = Number.isFinite(nodeStarted)
156
+ ? Math.abs(rec.startedAt - nodeStarted)
157
+ : Number.POSITIVE_INFINITY;
158
+ if (delta < bestDelta) {
159
+ bestDelta = delta;
160
+ bestIdx = i;
161
+ }
162
+ }
163
+ if (bestIdx === -1) continue;
164
+ const [matched] = candidates.splice(bestIdx, 1);
165
+ if (matched) result.set(node.stepIndex, projectRecordProgress(matched));
166
+ }
167
+ return result;
168
+ }
169
+
114
170
  /**
115
171
  * computeRenderSignature — 渲染输出中全部动态决定字段的签名(now 参数化,供测试)。
116
172
  *
117
- * 纯度说明:now 是显式参数,但 live 节点的 elapsedSeconds 经 projectLiveProgress
173
+ * 纯度说明:now 是显式参数,但 live 节点的 elapsedSeconds 经 projectRecordProgress
118
174
  * 内的 computeElapsedSeconds 现算(record.endedAt 缺省时取 Date.now())——即使两次
119
175
  * 调用传同一 now,跨秒的真实时钟也会使签名变。该内含实时源只朝「多失效」方向
120
176
  * 偏离(多失效、不多绘、更不漏绘),对「签名同 → 跳过重绘」的单向判据安全。
@@ -125,25 +181,26 @@ export interface ViewActions {
125
181
  * ES7;签名取原始值时粒度细于显示量化值,只多失效不多绘不漏绘)。
126
182
  *
127
183
  * **字段核对表**(每字段 ↔ 渲染消费点 file:line,完备性唯一证据——测试只能证
128
- * 「已入字段变 → 签名变」,不能证无漏字段):
184
+ * 「已入字段变 → 签名变」,不能证无漏字段)。live 七字段经 collectNodeLiveProgress
185
+ * 的 store record 投影([H2 W3] 数据源从 node.live 切换,字段口径不变):
129
186
  *
130
187
  * | 签名字段 | 消费点 |
131
188
  * |---|---|
132
- * | run.state.status | WorkflowsView.ts renderHeader :629 / detail-content.ts :75(statusDotStr+statusLabel)/ renderFooter :666-678 |
133
- * | 秒桶 Math.floor(now/1000) | renderHeader :628 formatElapsed(现算 elapsed)+ detail-content.ts :69-72(now 参与未完成节点 elapsed)|
134
- * | completed/total(节点 status 推导)| renderHeader :626-629 |
135
- * | budget 量化值(tokens round-k + cost toFixed(4)=BUDGET_COST_DECIMALS,与 :631 同精度——第 3-4 位小数变化是可见变化)| renderHeader :631 / saveTraceToFile :980 |
136
- * | run.state.errorLogs 指纹 = length+末条 level:message(errorLogs 仅经 push + slice(-MAX_ERROR_LOGS) 变异——append-only + 前向淘汰、条目不可变;封顶后 length 不变内容移,单取 length 会漏失效,而任何可见变化必伴随 length 变或末条变,故 length+末条是完备且最小的指纹)| detail-content.ts :174-191(renderWorkerLogSection:total 标签 + 末 20 条)|
137
- * | 节点 stepIndex | nodeParts 首字段(trace 数组序参与拼接,节点重排→签名变)/ saveTraceToFile :987(trace 导出 `### [#stepIndex]` 标题)|
138
- * | 节点 sessionFile | detail-content.ts :314-317(renderSessionSection)|
139
- * | 节点 status | 节点行 :844 statusDotStr / detail :75 |
140
- * | live.totalTokens | 节点行 :819 / detail :79 / :275 |
141
- * | 工具计数(签名路径用 countAllToolCalls 免克隆计数,与 getAllToolCalls(node.live).length 恒等——projectLiveProgress 投影无 toolCallCount 字段,不得引用)| 节点行 :820 / detail :80 / :223 |
142
- * | live.elapsedSeconds | 节点行 :821 / detail :81 / :276 |
143
- * | live.turns | detail :224 / :276 |
144
- * | live.eventLog.length(append-only,长度变化即尾部窗口右移出新事件)| detail :222(filter turn_end)+ :231-235 |
145
- * | live.currentActivity(type+label 均入签名)| detail :227-228 |
146
- * | live.lastError(内容入签名,防同存在性不同文本漏绘)| detail :277-278 |
189
+ * | run.state.status | WorkflowsView.ts renderHeader(statusDotStr+statusLabel 同串源)/ detail-content.ts statusLabel(statusDotStr 调用处)/ renderFooter |
190
+ * | 秒桶 Math.floor(now/1000) | renderHeader formatElapsed(现算 elapsed)+ detail-content.ts(now 参与未完成节点 elapsed)|
191
+ * | completed/total(节点 status 推导)| renderHeader |
192
+ * | budget 量化值(tokens round-k + cost toFixed(4)=BUDGET_COST_DECIMALS,与 renderHeader budgetStr 同精度——第 3-4 位小数变化是可见变化)| renderHeader / saveTraceToFile |
193
+ * | run.state.errorLogs 指纹 = length+末条 level:message(errorLogs 仅经 push + slice(-MAX_ERROR_LOGS) 变异——append-only + 前向淘汰、条目不可变;封顶后 length 不变内容移,单取 length 会漏失效,而任何可见变化必伴随 length 变或末条变,故 length+末条是完备且最小的指纹)| detail-content.ts(renderWorkerLogSection:total 标签 + 末 20 条)|
194
+ * | 节点 stepIndex | nodeParts 首字段(trace 数组序参与拼接,节点重排→签名变)/ saveTraceToFile(trace 导出 `### [#stepIndex]` 标题)|
195
+ * | 节点 sessionFile | detail-content.ts(renderSessionSection)|
196
+ * | 节点 status | 节点行 statusDotStr / detail statusLabel |
197
+ * | live.totalTokens | 节点行(level1AgentCells)/ detail(buildDetailContent 用量行)|
198
+ * | live.toolCallCount(store 投影:eventLog tool_start 计数,与 getAllToolCalls(record).length 恒等——LiveProgressView 无别的计数字段,不得另引)| 节点行 / detail(用量行 + Activity 标签)|
199
+ * | live.elapsedSeconds | 节点行 / detail(用量行 + Outcome Running 行)|
200
+ * | live.turns | detail(Activity 标签 + Outcome Running 行)|
201
+ * | live.eventLog.length(append-only,长度变化即尾部窗口右移出新事件)| detail(Activity filter turn_end + 最近 N 条)|
202
+ * | live.currentActivity(type+label 均入签名)| detail(Activity 当前活动行)|
203
+ * | live.lastError(内容入签名,防同存在性不同文本漏绘)| detail(Outcome 行)|
147
204
  *
148
205
  * 维护约定(DS8):WorkflowsView/detail-content 新增任何动态展示字段必须同步
149
206
  * 并入本签名并更新上表,否则该字段渲染滞后一拍。本文件编辑会使表内 WorkflowsView
@@ -152,7 +209,11 @@ export interface ViewActions {
152
209
  /** 秒桶宽度(ms):签名取 Math.floor(now / SECOND_MS),秒桶推进 = 可见 elapsed 变化。 */
153
210
  const SECOND_MS = 1000;
154
211
 
155
- export function computeRenderSignature(run: WorkflowRun, now: number): string {
212
+ export function computeRenderSignature(
213
+ run: WorkflowRun,
214
+ liveProgress: Map<number, LiveProgressView>,
215
+ now: number,
216
+ ): string {
156
217
  const traceArr = run.state.trace.toArray();
157
218
  const completed = traceArr.filter((n) => n.status === "completed").length;
158
219
  const budget = run.state.budget;
@@ -166,8 +227,8 @@ export function computeRenderSignature(run: WorkflowRun, now: number): string {
166
227
  const lastLog = logs && logs.length > 0 ? `${logs[logs.length - 1].level}:${logs[logs.length - 1].message}` : "-";
167
228
  const errorLogsPart = `${logs?.length ?? 0}:${lastLog}`;
168
229
  const nodeParts = traceArr.map((n) => {
169
- const live = n.live ? projectLiveProgress(n.live) : undefined;
170
- return `${n.stepIndex}:${n.status}:${n.sessionFile ?? "-"}:${live?.totalTokens ?? -1}:${n.live ? countAllToolCalls(n.live) : -1}:${live?.elapsedSeconds ?? -1}:${live?.turns ?? -1}:${live?.eventLog.length ?? -1}:${live?.currentActivity ? `${live.currentActivity.type}:${live.currentActivity.label}` : "-"}:${live?.lastError ?? "-"}`;
230
+ const l = liveProgress.get(n.stepIndex);
231
+ return `${n.stepIndex}:${n.status}:${n.sessionFile ?? "-"}:${l?.totalTokens ?? -1}:${l?.toolCallCount ?? -1}:${l?.elapsedSeconds ?? -1}:${l?.turns ?? -1}:${l?.eventLog.length ?? -1}:${l?.currentActivity ? `${l.currentActivity.type}:${l.currentActivity.label}` : "-"}:${l?.lastError ?? "-"}`;
171
232
  });
172
233
  return [run.state.status, Math.floor(now / SECOND_MS), `${completed}/${traceArr.length}`, budgetPart, errorLogsPart, ...nodeParts].join("|");
173
234
  }
@@ -224,6 +285,13 @@ function createInitialState(): ViewState {
224
285
  * @param theme ThemeLike(避免直接 import Pi runtime)
225
286
  * @param ctx ExtensionContext(调 ui.custom 渲染 + ui.notify 错误反馈)
226
287
  * @param actions lifecycle 操作(abort),由调用方注入
288
+ * @param runStateFile run 状态快照路径(可选,header 展示)
289
+ * @param liveRecords 本 run 的 store record 查询([H2 W3] 设计 D2 进度源切换:
290
+ * 经 SubagentService.queries.collectRecordsByParentRunId 查询(内存 ∪ 磁盘重建 ∪
291
+ * manifest,LIST_LIMIT 口径),view 在 200ms tick 与 render 时重查并按
292
+ * collectNodeLiveProgress 配对到 running trace node。未注入(无 service 的测试
293
+ * 环境)时 live 投影恒空,渲染走终态 result 路径。终态 record archive 出内存后
294
+ * 由下次重查从磁盘重建面取终态快照——无需带 payload 的新事件类型(D2 实现锚点)。
227
295
  */
228
296
  export function createWorkflowsView(
229
297
  run: WorkflowRun,
@@ -231,11 +299,17 @@ export function createWorkflowsView(
231
299
  ctx: ExtensionContext,
232
300
  actions: ViewActions,
233
301
  runStateFile?: string,
302
+ liveRecords?: () => SubagentRecord[],
234
303
  ): Promise<void> {
235
304
  return ctx.ui.custom<void>((_tui: unknown, _t: unknown, _kb: unknown, done: (result: void) => void) => {
236
305
  const state = createInitialState();
237
306
  const tui = _tui as TuiLike;
238
307
 
308
+ /** 查询 + 配对:本 run running 节点的 live 进度投影(liveRecords 未注入时恒空)。 */
309
+ function collectLive(): Map<number, LiveProgressView> {
310
+ return liveRecords === undefined ? new Map() : collectNodeLiveProgress(run, liveRecords());
311
+ }
312
+
239
313
  function currentPhaseAgents() {
240
314
  const live = buildPhaseGroups([...run.state.trace.toArray()]);
241
315
  const pg = live[state.phaseIdx];
@@ -253,16 +327,17 @@ export function createWorkflowsView(
253
327
  const cache = { key: undefined as string | undefined, lines: undefined as string[] | undefined };
254
328
  const requestRender = () => tui.requestRender();
255
329
 
256
- // ── 轮询 tick:engine 无事件推送,view 自轮询 trace 变化 ──
257
- // 每 200ms 条件失效(IF11/TC7):先算渲染签名(computeRenderSignature,
258
- // 覆盖 header/节点行/L2 detail 全部动态字段),与上次相同 → 该帧内容无可见
259
- // 变化,直接 return(不清 cache、不 requestRender——纯等待场景零重建零重绘);
260
- // 不同 → 现状路径(清缓存 + requestRender)。lastSignature 初始 undefined,
261
- // tick 必失效(保证首绘)。签名计算 O(N)(N=trace 节点数)远低于全量 render。
330
+ // ── 轮询 tick:engine 无事件推送,view 自轮询 trace + store record 变化 ──
331
+ // 每 200ms 条件失效(IF11/TC7):先查 store(parentRunId 查询域)配对 live 投影,
332
+ // 再算渲染签名(computeRenderSignature,覆盖 header/节点行/L2 detail 全部动态
333
+ // 字段),与上次相同 → 该帧内容无可见变化,直接 return(不清 cache、不
334
+ // requestRender——纯等待场景零重建零重绘);不同 → 现状路径(清缓存 +
335
+ // requestRender)。lastSignature 初始 undefined,首 tick 必失效(保证首绘)。
336
+ // 签名计算 O(N)(N=trace 节点数)远低于全量 render。
262
337
  let lastSignature: string | undefined;
263
338
  const tick = setInterval(() => {
264
339
  if (state.disposed) return;
265
- const signature = computeRenderSignature(run, Date.now());
340
+ const signature = computeRenderSignature(run, collectLive(), Date.now());
266
341
  if (signature === lastSignature) return;
267
342
  lastSignature = signature;
268
343
  cache.key = undefined;
@@ -332,7 +407,7 @@ export function createWorkflowsView(
332
407
  if (!node) return false;
333
408
  const detailCtx: DetailScrollContext = {
334
409
  viewportHeight: detailViewportHeight(),
335
- contentLines: detailContentLength(node, state, run, theme),
410
+ contentLines: detailContentLength(node, state, run, theme, collectLive().get(node.stepIndex)),
336
411
  isRunning: node.status === "running",
337
412
  };
338
413
  const r = processDetailKey(
@@ -514,9 +589,10 @@ export function createWorkflowsView(
514
589
  if (cache.lines && cache.key === key) return cache.lines;
515
590
  clampSelections();
516
591
  // 缺陷 #1 修复:每次 render 从 run.state.trace 实时读(toArray 返回内部数组引用,
517
- // 后续 trace.append 会反映到 view),不再用 factory 时的冻结快照。
592
+ // 后续 trace.append 会反映到 view),不再用 factory 时的冻结快照。live 进度同源
593
+ // 重查 store([H2 W3]:进度源 = collectRecordsByParentRunId 查询域)。
518
594
  const liveGroups = buildPhaseGroups([...run.state.trace.toArray()]);
519
- const raw = renderLayout(run, state, liveGroups, theme, width, height, runStateFile);
595
+ const raw = renderLayout(run, state, liveGroups, theme, width, height, runStateFile, collectLive());
520
596
  // Pad to terminal height so the overlay fills the screen (matches main 行为)
521
597
  const lines = raw.length < height
522
598
  ? [...raw, ...Array.from({ length: height - raw.length }, () => "")]
@@ -567,6 +643,7 @@ function renderLayout(
567
643
  screenWidth: number,
568
644
  screenHeight: number,
569
645
  runStateFile?: string,
646
+ live?: Map<number, LiveProgressView>,
570
647
  ): string[] {
571
648
  const lines: string[] = [];
572
649
  const contentWidth = screenWidth - BOX_BORDER_CHARS;
@@ -585,10 +662,10 @@ function renderLayout(
585
662
  if (state.level === 0) {
586
663
  renderLevel0(lines, run, phaseGroups, state, theme, mainWidth, now, viewH);
587
664
  } else if (state.level === 1) {
588
- renderLevel1(lines, run, phaseGroups, state, theme, mainWidth, now, viewH);
665
+ renderLevel1(lines, run, phaseGroups, state, theme, mainWidth, now, viewH, live);
589
666
  } else {
590
667
  // L2 详情走固定高度 viewport(右侧滚动),高度 = minBody(与 L0/L1 最小一致)
591
- renderLevel2(lines, run, agents, state, theme, mainWidth, now, viewH);
668
+ renderLevel2(lines, run, agents, state, theme, mainWidth, now, viewH, live);
592
669
  }
593
670
 
594
671
  // Padding 已在各 renderLevel 内部处理,无需额外 padding
@@ -807,18 +884,18 @@ function pushLevel1Header(
807
884
  }
808
885
  }
809
886
 
810
- /** L1 agent 行统计三元组:live 路径优先(运行中从 node.live 读实时 token/tool + elapsed),
887
+ /** L1 agent 行统计三元组:live 路径优先(运行中从 store record 投影读实时 token/tool + elapsed),
811
888
  * 终态回退 result 统计。 */
812
889
  function level1AgentCells(
813
890
  node: ExecutionTraceNode,
891
+ liveView: LiveProgressView | undefined,
814
892
  now: number,
815
893
  ): { tokStr: string; tcCount: number; elapsed: string } {
816
- if (node.live) {
817
- const live = projectLiveProgress(node.live);
894
+ if (liveView) {
818
895
  return {
819
- tokStr: live.totalTokens > 0 ? `${Math.round(live.totalTokens / BUDGET_TOKENS_DIVISOR)}k tok` : "",
820
- tcCount: getAllToolCalls(node.live).length,
821
- elapsed: formatElapsedSeconds(live.elapsedSeconds),
896
+ tokStr: liveView.totalTokens > 0 ? `${Math.round(liveView.totalTokens / BUDGET_TOKENS_DIVISOR)}k tok` : "",
897
+ tcCount: liveView.toolCallCount,
898
+ elapsed: formatElapsedSeconds(liveView.elapsedSeconds),
822
899
  };
823
900
  }
824
901
  const elapsed = formatElapsed(
@@ -837,12 +914,13 @@ function level1AgentCells(
837
914
  function formatLevel1AgentLine(
838
915
  node: ExecutionTraceNode,
839
916
  theme: ThemeLike,
917
+ liveView: LiveProgressView | undefined,
840
918
  now: number,
841
919
  selected: boolean,
842
920
  ): string {
843
921
  const pointer = selected ? "❯ " : " ";
844
922
  const dot = statusDotStr(node.status, theme);
845
- const { tokStr, tcCount, elapsed } = level1AgentCells(node, now);
923
+ const { tokStr, tcCount, elapsed } = level1AgentCells(node, liveView, now);
846
924
  return `${pointer}${dot} ${displayAgentName(node.agent)} ${node.model} ${tokStr} · ${tcCount} tools · ${elapsed}`;
847
925
  }
848
926
 
@@ -855,6 +933,7 @@ function renderLevel1(
855
933
  mainWidth: number,
856
934
  now: number,
857
935
  bodyH: number,
936
+ live?: Map<number, LiveProgressView>,
858
937
  ): void {
859
938
  const leftLines = buildLevelSidebar(phases, state, theme, bodyH);
860
939
 
@@ -865,7 +944,7 @@ function renderLevel1(
865
944
  const agents = currentPhase?.nodes ?? [];
866
945
  const { startIdx: agentStart, viewportH: agentViewportH } = computeViewport(agents.length, state.agentIdx, bodyH);
867
946
  for (let i = agentStart; i < agentStart + agentViewportH && i < agents.length; i++) {
868
- rightLines.push(formatLevel1AgentLine(agents[i], theme, now, i === state.agentIdx));
947
+ rightLines.push(formatLevel1AgentLine(agents[i], theme, live?.get(agents[i]!.stepIndex), now, i === state.agentIdx));
869
948
  }
870
949
  state.agentScrollOffset = agentStart;
871
950
  while (rightLines.length < bodyH) rightLines.push("");
@@ -886,6 +965,7 @@ function renderLevel2(
886
965
  mainWidth: number,
887
966
  now: number,
888
967
  viewH: number,
968
+ live?: Map<number, LiveProgressView>,
889
969
  ): void {
890
970
  const leftLines: string[] = [];
891
971
  const rightLines: string[] = [];
@@ -908,7 +988,7 @@ function renderLevel2(
908
988
  // Right: full detail(viewport 截断 + 滚动,对齐 subagents renderRightDetail)
909
989
  const node = agents[state.agentIdx];
910
990
  if (node) {
911
- const content = buildDetailContent(node, state, run, theme, mainWidth, now);
991
+ const content = buildDetailContent(node, state, run, theme, mainWidth, now, live?.get(node.stepIndex));
912
992
  const maxOff = Math.max(0, content.length - viewH);
913
993
  // running 且 followTail → 钉底部(最新输出始终可见,用户 PgUp 后停止跟随)
914
994
  if (node.status === "running" && state.followTail) {