@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.
- package/package.json +6 -6
- package/src/host/__tests__/inflight-reporter.test.ts +18 -12
- package/src/host/inflight-reporter.ts +60 -28
- package/src/host/pi-host.ts +1 -1
- package/src/index.ts +32 -13
- package/src/injectors/__tests__/engine-awareness.test.ts +1 -1
- package/src/injectors/__tests__/engine-section-stability.test.ts +1 -4
- package/src/injectors/engine-awareness.ts +1 -1
- package/src/injectors/subagent-list-injector.ts +1 -1
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +1 -1
- package/src/interface/commands.ts +14 -3
- package/src/interface/format.ts +29 -5
- package/src/interface/gui-mappers.ts +6 -2
- package/src/interface/list-component.ts +9 -3
- package/src/interface/list-view.ts +1 -1
- package/src/interface/subagent-actions.ts +1 -1
- package/src/interface/subagent-tool-schema.ts +21 -9
- package/src/interface/subagent-tool.ts +5 -5
- package/src/interface/subagents.ts +1 -1
- package/src/interface/tool-workflow.ts +5 -3
- package/src/interface/views/WorkflowsView.ts +128 -48
- package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +82 -95
- package/src/interface/views/__tests__/record-progress.test.ts +235 -0
- package/src/interface/views/detail-content.ts +72 -26
- package/src/jsonl-run-store.ts +1 -1
- package/src/session-lifecycle.ts +23 -7
|
@@ -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
|
|
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
|
|
142
|
-
//
|
|
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
|
|
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
|
-
|
|
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.
|
|
155
|
-
//
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
167
|
-
- action:"close" —
|
|
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
|
|
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.
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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 经
|
|
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
|
|
133
|
-
* | 秒桶 Math.floor(now/1000) | renderHeader
|
|
134
|
-
* | completed/total(节点 status 推导)| renderHeader
|
|
135
|
-
* | budget 量化值(tokens round-k + cost toFixed(4)=BUDGET_COST_DECIMALS,与
|
|
136
|
-
* | run.state.errorLogs 指纹 = length+末条 level:message(errorLogs 仅经 push + slice(-MAX_ERROR_LOGS) 变异——append-only + 前向淘汰、条目不可变;封顶后 length 不变内容移,单取 length 会漏失效,而任何可见变化必伴随 length 变或末条变,故 length+末条是完备且最小的指纹)| detail-content.ts
|
|
137
|
-
* | 节点 stepIndex | nodeParts 首字段(trace 数组序参与拼接,节点重排→签名变)/ saveTraceToFile
|
|
138
|
-
* | 节点 sessionFile | detail-content.ts
|
|
139
|
-
* | 节点 status | 节点行
|
|
140
|
-
* | live.totalTokens |
|
|
141
|
-
* |
|
|
142
|
-
* | live.elapsedSeconds | 节点行
|
|
143
|
-
* | live.turns | detail
|
|
144
|
-
* | live.eventLog.length(append-only,长度变化即尾部窗口右移出新事件)| detail
|
|
145
|
-
* | live.currentActivity(type+label 均入签名)| detail
|
|
146
|
-
* | live.lastError(内容入签名,防同存在性不同文本漏绘)| detail
|
|
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(
|
|
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
|
|
170
|
-
return `${n.stepIndex}:${n.status}:${n.sessionFile ?? "-"}:${
|
|
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
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
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 路径优先(运行中从
|
|
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 (
|
|
817
|
-
const live = projectLiveProgress(node.live);
|
|
894
|
+
if (liveView) {
|
|
818
895
|
return {
|
|
819
|
-
tokStr:
|
|
820
|
-
tcCount:
|
|
821
|
-
elapsed: formatElapsedSeconds(
|
|
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) {
|