@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 +4 -4
- package/src/index.ts +26 -10
- package/src/injectors/__tests__/engine-section-stability.test.ts +0 -3
- package/src/interface/commands.ts +14 -3
- package/src/interface/format.ts +16 -0
- package/src/interface/subagent-tool-schema.ts +7 -0
- package/src/interface/subagent-tool.ts +2 -2
- package/src/interface/subagents.ts +1 -1
- package/src/interface/tool-workflow.ts +4 -2
- package/src/interface/views/WorkflowsView.ts +127 -47
- package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +82 -95
- package/src/interface/views/__tests__/record-progress.test.ts +234 -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-subagent-workflow",
|
|
3
|
-
"version": "8.
|
|
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.
|
|
62
|
-
"@zhushanwen/pi-subagent-cli": "0.
|
|
63
|
-
"@zhushanwen/zcode-subagent-cli": "0.2.
|
|
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/
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
// ═══
|
|
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
|
-
// ═══
|
|
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
|
-
|
|
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
|
|
490
|
-
//
|
|
491
|
-
//
|
|
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
|
|
266
|
+
abort: (runId) => abortRun(runId, deps),
|
|
260
267
|
};
|
|
261
|
-
|
|
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
|
}
|
package/src/interface/format.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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 ─────────────────────────────────────────
|
|
@@ -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
|
});
|
|
@@ -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) {
|