@zhushanwen/pi-subagent-workflow 8.11.0 → 8.13.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.
@@ -40,7 +40,7 @@ export { SLUG_MAX_LENGTH };
40
40
  // 反映必填性。勿在此基础上继续堆 action 条件逻辑——要加就拆 tool。
41
41
  export const SubagentParams = Type.Object({
42
42
  action: StringEnum(["start", "list", "cancel", "message", "close", "fork-from"], {
43
- description: "Operation: 'start' runs a subagent, 'list' shows subagents, 'cancel' stops a background subagent, 'message' sends a follow-up to a running subagent (one-shot subagents are auto-upgraded to conversation mode on first message), 'close' ends a running subagent (conversation-mode or one-shot), 'fork-from' spawns a NEW subagent inheriting an older one's history (recovery for restart-disconnected subagents; the old record is untouched).",
43
+ description: "Operation: 'start' runs a subagent, 'list' shows subagents, 'cancel' stops a background subagent, 'message' sends a follow-up to any of your subagents (running or idle — an idle one transparently revives and continues on its original session file; one-shot subagents are auto-upgraded to conversation mode on first message), 'close' archives a subagent (immediately when idle; after the current round, or immediately with force:true, when running), 'fork-from' spawns a NEW subagent inheriting an older one's history (recovery for restart-disconnected subagents; the old record is untouched).",
44
44
  }),
45
45
  // ── action:"start" fields (flattened to top level). task/slug REQUIRED for start. ──
46
46
  // Missing/empty task or slug throws at runtime (startHandler).
@@ -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
  })),
@@ -138,24 +145,29 @@ export const SubagentParams = Type.Object({
138
145
  description: "REQUIRED for action:'cancel'. The subagentId to cancel. Throws if missing. Only background subagents can be cancelled.",
139
146
  }),
140
147
  })),
141
- // action:"message" → messageParam.subagentId + text REQUIRED. Any RUNNING subagent works —
142
- // one-shot subagents are auto-upgraded to conversation mode on first message (SP-5); ended ones throw.
148
+ // action:"message" → messageParam.subagentId + text REQUIRED. Any reachable subagent works —
149
+ // running joins the in-flight round (D2 打断入队);idle transparently revives on the same
150
+ // session file([U4 §3.2.3] 万物可续——形态枚举 gate 消亡);one-shot auto-upgrades to
151
+ // conversation mode on first message (SP-5)。description 与实现锚点见 messageHandler。
143
152
  messageParam: Type.Optional(Type.Object({
144
153
  subagentId: Type.String({
145
- description: "REQUIRED for action:'message'. The subagentId to message (any running subagent; a one-shot subagent is auto-upgraded to conversation mode on first message, so you may also message one-shot subagents that are still running).",
154
+ description: "REQUIRED for action:'message'. The subagentId to message. Any subagent reachable in this session tree works, running or idle: an idle subagent transparently revives on the same id and continues writing its original session file (a one-shot is auto-upgraded to conversation mode on first message); a running subagent has your message interrupt-and-join its in-flight round. Rejections: unknown id, session file held by another live process, a record from a different session tree, workflow-origin records (their results belong to the workflow run), and one-shot records on engines that do not support conversation upgrade.",
146
155
  }),
147
156
  text: Type.String({
148
157
  description: "REQUIRED for action:'message'. The message to send. Whitespace-only throws.",
149
158
  }),
150
159
  interrupt: Type.Optional(Type.Boolean({
151
- description: "If true, interrupt the subagent's current work immediately (in-progress output stops, it switches to your new message). If false (default), the message is queued and processed after the current round completes. When the subagent is idle (between rounds), interrupt has no effect — the message always starts a new round.",
160
+ // [H1 U6 / D2] 参数已退役(messageHandler 零消费):在途轮存在即打断入队,
161
+ // 不区分抢占/排队——字段保留防存量调用 schema 报错,description 据实声明 no-op。
162
+ description: "Deprecated, no effect: a message to a running subagent always interrupts its in-flight round (the round aborts and your message is processed next) regardless of this flag; an idle subagent always starts a new round.",
152
163
  })),
153
164
  })),
154
- // action:"close" → closeParam.subagentId REQUIRED. Ends a running subagent (conversation-mode
155
- // or one-shot closeSubagent behavior split covers both).
165
+ // action:"close" → closeParam.subagentId REQUIRED. 归档(archived):列表隐藏、可寻回、
166
+ // 非终态化([U5 §3.2.5])。idle 立即归档收口;running 默认等当前轮收口后归档,
167
+ // force:true 立即终止随即归档(closeHandler 头注行为分流)。
156
168
  closeParam: Type.Optional(Type.Object({
157
169
  subagentId: Type.String({
158
- description: "REQUIRED for action:'close'. The subagentId to close (any running subagent, conversation-mode or one-shot).",
170
+ description: "REQUIRED for action:'close'. The subagentId to close (any reachable subagent running or idle). Close archives the record: hidden from list, recoverable, not a terminal state. Idle subagents close immediately; running ones finish the current round first, or terminate immediately when force:true.",
159
171
  }),
160
172
  force: Type.Optional(Type.Boolean({
161
173
  description: "If true, terminate immediately even if mid-round (in-progress work is lost). If false (default), let the current round finish, then close. When idle, the subagent closes immediately regardless.",
@@ -166,7 +178,7 @@ export const SubagentParams = Type.Object({
166
178
  // 源文件只读不续写);旧记录/状态机不动。pi 引擎限定(非 pi 在 execute 层拒绝)。
167
179
  forkFromParam: Type.Optional(Type.Object({
168
180
  sourceSubagentId: Type.String({
169
- description: "REQUIRED for action:'fork-from'. The OLD subagentId whose conversation history becomes the inherited context of the new subagent. Works for records disconnected by a session restart or already finished; cancelled/worktree-bound/still-running ones are rejected with guidance.",
181
+ description: "REQUIRED for action:'fork-from'. The OLD subagentId whose conversation history becomes the inherited context of the new subagent. Works for any idle record — disconnected by a session restart, already finished, or previously closed/cancelled. Rejections: still-running sources (message them instead), sources held by another live process, worktree-bound sources, and unknown ids; an unparseable history anchor is guided to action:'message' (same-id reopen) instead.",
170
182
  }),
171
183
  prompt: Type.Optional(Type.String({
172
184
  description: "Continuation instruction for the new subagent (what to do next on top of the inherited history). When omitted, a standard handover frame is injected: reconstruct done/decided/remaining from the inherited history, then continue to completion. Whitespace-only treated as omitted.",
@@ -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 走
@@ -163,11 +163,11 @@ action:"list" before action:"start" — a reusable subagent may exist; compactio
163
163
  ## Actions
164
164
 
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
- - 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
- - 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.
166
+ - action:"message" — send a follow-up to any of your subagents — running or idle (idle revives in place; one-shots become conversation-mode); full context retained. REQUIRED messageParam: { subagentId, text }. The reply auto-notifies.
167
+ - action:"close" — archive a subagent (hidden from list, recoverable): idle closes immediately; running finishes the current round first unless force:true (then terminates mid-round). REQUIRED closeParam: { subagentId }.
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
- - 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.
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 still-running / foreign-live / worktree-bound sources; unparseable history anchors are guided to action:"message" (same-id reopen).
171
171
 
172
172
  ## Examples
173
173
 
@@ -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 ─────────────────────────────────────────
@@ -431,7 +431,7 @@ export async function actionRun(
431
431
  const args = params.args ?? {};
432
432
  const tokens = params.tokens;
433
433
  const time = params.time;
434
- // OR-1 入口 fail-fast(unbounded-wait-audit §7.2 T3①):schema 的 time 是
434
+ // OR-1 入口 fail-fast(crash-forensics-and-watchdog.md 附录 E(原 unbounded-wait-audit §7.2 T3①)):schema 的 time 是
435
435
  // Type.Number 直通(无上界)——超 setTimeout 安全域的值会穿透到 lifecycle 内层
436
436
  // 防线(assertSafeTimerDelay),而入口拦截让它永不进入副作用链。错误带合法上限
437
437
  // 与实际传入值,LLM 可据消息自纠(clamp 或省略走 unlimited 语义)。
@@ -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
  });
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * SDK 集成:ctx.ui.custom factory 返回 Component{render(width), handleInput(data),
13
13
  * invalidate},第二参数 `{overlay:true, overlayOptions}`(全屏 overlay,对齐 main +
14
- * subagents 扩展 + docs/pi-tui-development-guide.md §3.2)。按键经
14
+ * subagents 扩展 + docs/extensions/tui-rendering-pitfalls.md §3.2)。按键经
15
15
  * matchesKey(data, KeyId) 解析(兼容 xterm/iTerm/kitty 转义序列差异)。
16
16
  * escape/ctrl+c 在 keybindings 同映射到 exit。
17
17
  *
@@ -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) {