@zhushanwen/pi-subagent-workflow 8.14.7 → 9.0.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.
Files changed (58) hide show
  1. package/README.md +6 -5
  2. package/package.json +11 -7
  3. package/relay/relay.mjs +45 -2
  4. package/skills/workflow-script-format/SKILL.md +2 -56
  5. package/src/host/__tests__/inflight-reporter.test.ts +26 -1
  6. package/src/host/__tests__/pi-host.test.ts +418 -4
  7. package/src/host/inflight-reporter.ts +13 -10
  8. package/src/host/pi-host.ts +205 -17
  9. package/src/index.ts +21 -72
  10. package/src/injectors/__tests__/engine-awareness.test.ts +236 -33
  11. package/src/injectors/__tests__/helpers/injector-test-mocks.ts +15 -7
  12. package/src/injectors/__tests__/workflow-list-injector.test.ts +447 -40
  13. package/src/injectors/resource-list-injector.ts +103 -32
  14. package/src/injectors/subagent-list-injector.ts +4 -28
  15. package/src/injectors/workflow-list-injector.ts +8 -4
  16. package/src/interface/__tests__/capture-tool.ts +56 -0
  17. package/src/interface/__tests__/commands-resume.test.ts +103 -0
  18. package/src/interface/__tests__/tool-prompt-contract.test.ts +361 -0
  19. package/src/interface/__tests__/tool-render.test.ts +105 -2
  20. package/src/interface/__tests__/tool-subagents.test.ts +24 -26
  21. package/src/interface/__tests__/tool-workflow-resume.test.ts +227 -0
  22. package/src/interface/__tests__/tool-workflow-script.test.ts +557 -0
  23. package/src/interface/__tests__/tool-workflow.test.ts +884 -0
  24. package/src/interface/bg-notify-render.ts +18 -9
  25. package/src/interface/command-actions.ts +22 -17
  26. package/src/interface/commands.ts +58 -12
  27. package/src/interface/format.ts +50 -16
  28. package/src/interface/id-preview.ts +4 -4
  29. package/src/interface/subagent-actions.ts +18 -50
  30. package/src/interface/subagent-tool.ts +26 -18
  31. package/src/interface/subagents.ts +5 -11
  32. package/src/interface/tool-render.ts +14 -30
  33. package/src/interface/tool-shared.ts +55 -12
  34. package/src/interface/tool-subagents.ts +21 -11
  35. package/src/interface/tool-workflow-script.ts +52 -72
  36. package/src/interface/tool-workflow.ts +345 -139
  37. package/src/interface/views/WorkflowsView.ts +10 -7
  38. package/src/interface/views/__tests__/{WorkflowsView-signature.test.ts → WorkflowsView.test.ts} +155 -11
  39. package/src/interface/views/detail-content.ts +3 -5
  40. package/src/jsonl-run-store.ts +656 -544
  41. package/src/model-events.ts +35 -0
  42. package/src/session-lifecycle.ts +376 -85
  43. package/src/subagents-events.ts +56 -0
  44. package/src/workflow-events.ts +285 -200
  45. package/src/workflow-notify.ts +456 -0
  46. package/src/injectors/__tests__/engine-section-stability.test.ts +0 -353
  47. package/src/injectors/__tests__/model-list-injector.test.ts +0 -136
  48. package/src/injectors/__tests__/subagent-list-injector.test.ts +0 -376
  49. package/src/interface/__tests__/detectors.test.ts +0 -161
  50. package/src/interface/__tests__/subagent-tool-prompt.test.ts +0 -183
  51. package/src/interface/__tests__/tool-workflow-run-builtin-name.test.ts +0 -219
  52. package/src/interface/__tests__/tool-workflow-script-generate.test.ts +0 -251
  53. package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +0 -215
  54. package/src/interface/__tests__/workflow-state-file-exposure.test.ts +0 -38
  55. package/src/interface/__tests__/workflow-tool-prompt.test.ts +0 -129
  56. package/src/interface/helpers.ts +0 -272
  57. package/src/interface/views/__tests__/detail-content-session-file.test.ts +0 -73
  58. package/src/interface/views/__tests__/record-progress.test.ts +0 -235
@@ -1,73 +0,0 @@
1
- // src/interface/views/__tests__/detail-content-session-file.test.ts
2
- //
3
- // TUI 渲染测试:buildDetailContent 在 agent 终态时渲染 session: 路径行。
4
- //
5
- // 防的 bug:node.sessionFile 有值但 TUI detail 不显示——用户在交互面板里
6
- // 看不到 agent 的 session jsonl 路径,无法定位文件做后续查看。
7
-
8
- import { describe, expect, it } from "vitest";
9
-
10
- import { buildDetailContent } from "../detail-content.ts";
11
- import type { ThemeLike } from "../../format.ts";
12
- import type { ExecutionTraceNode } from "@zhushanwen/subagent-core";
13
- import type { WorkflowRun } from "@zhushanwen/subagent-core";
14
-
15
- /** plain theme: 着色方法直接返回原文,无 ANSI 色码——测试断言纯文本。
16
- * bg/underline 为宽 ThemeLike 接口的 no-op 实现(workflow 渲染面只用 fg/bold)。 */
17
- const plainTheme: ThemeLike = {
18
- bg: (_color: string, text: string) => text,
19
- fg: (_tag: string, text: string) => text,
20
- bold: (text: string) => text,
21
- underline: (text: string) => text,
22
- };
23
-
24
- function makeNode(overrides: Partial<ExecutionTraceNode> = {}): ExecutionTraceNode {
25
- return {
26
- stepIndex: 0,
27
- agent: "coder",
28
- task: "do something",
29
- model: "default",
30
- status: "completed",
31
- ...overrides,
32
- };
33
- }
34
-
35
- /** 最小 WorkflowRun——buildDetailContent 只读 run.state.errorLogs。 */
36
- function makeRun(): WorkflowRun {
37
- return {
38
- state: { errorLogs: [] },
39
- } as unknown as WorkflowRun;
40
- }
41
-
42
- describe("buildDetailContent session 路径行渲染", () => {
43
- it("node.sessionFile 有值 → 详情末尾渲染 session: <路径>", () => {
44
- const sessionPath = "/abs/.pi/agent/subagents/enc/sessions/2026-07-15T_session-abc.jsonl";
45
- const node = makeNode({
46
- status: "completed",
47
- sessionFile: sessionPath,
48
- result: { content: "done" },
49
- });
50
- const lines = buildDetailContent(node, { promptExpanded: false }, makeRun(), plainTheme, 120, Date.now());
51
- const sessionLine = lines.find((l) => l.includes("session:"));
52
- expect(sessionLine).toBeDefined();
53
- expect(sessionLine).toContain(sessionPath);
54
- });
55
-
56
- it("node.sessionFile undefined(窗口期)→ 不渲染 session: 行", () => {
57
- const node = makeNode({ status: "completed", sessionFile: undefined, result: { content: "done" } });
58
- const lines = buildDetailContent(node, { promptExpanded: false }, makeRun(), plainTheme, 120, Date.now());
59
- const sessionLine = lines.find((l) => l.includes("session:"));
60
- expect(sessionLine).toBeUndefined();
61
- });
62
-
63
- it("长路径截断到 mainWidth(防溢出)", () => {
64
- const longPath = "/very/long/path/" + "x".repeat(200) + "/session.jsonl";
65
- const node = makeNode({ status: "completed", sessionFile: longPath, result: { content: "done" } });
66
- const narrowWidth = 60;
67
- const lines = buildDetailContent(node, { promptExpanded: false }, makeRun(), plainTheme, narrowWidth, Date.now());
68
- const sessionLine = lines.find((l) => l.includes("session:"));
69
- expect(sessionLine).toBeDefined();
70
- // 截断后行宽不超过 mainWidth + 边框余量
71
- expect(sessionLine!.length).toBeLessThanOrEqual(narrowWidth);
72
- });
73
- });
@@ -1,235 +0,0 @@
1
- /**
2
- * [H2 W3] store record live 进度投影单测(设计 D2 进度源切换)。
3
- *
4
- * 三组面:
5
- * 1. 双路径对齐(S1 等价表依据):同一 ExecutionRecord(同构造事件序列累积出的
6
- * turns 形态)→ 旧投影 projectLiveProgress(record) + getAllToolCalls(record).length
7
- * vs 新投影 projectRecordProgress(SubagentRecord 投影面)。逐字段断言相等
8
- * ——「数据源换 store、字段口径不变」的可证伪锁定。
9
- * 2. 派生恒等:toolCallCount(eventLog tool_start 计数)=== getAllToolCalls 长度
10
- * (getEventLog 对每个 toolCall 恰产一条 tool_start);lastError(eventLog 末条
11
- * error label)=== record.lastError(getEventLog 仅 lastError 非空时追加末条)。
12
- * 3. collectNodeLiveProgress 配对:task 标签匹配 + startedAt 最近邻 + 贪心唯一;
13
- * 终态 node 不配;无匹配 record(重试间隙)返回空。
14
- *
15
- * ExecutionRecord 经 duck-typed 构造(对齐 WorkflowsView-signature.test.ts 先例);
16
- * 新路径输入 SubagentRecord 的 eventLog/currentActivity/turns/totalTokens 取自
17
- * recordToSubagent 的同源投影(getEventLog/getCurrentActivity/turnCount/totalTokens
18
- * ——其投影恒等由 core 侧 record-store 测试锁定,此处消费投影面)。
19
- */
20
- import { describe, it, expect } from "vitest";
21
-
22
- import { computeElapsedSeconds, getAllToolCalls } from "@zhushanwen/subagent-core";
23
- import { projectLiveProgress } from "@zhushanwen/subagent-core/execution/persistence/execution-record.ts";
24
- import type { ExecutionRecord, SubagentRecord, WorkflowRun } from "@zhushanwen/subagent-core";
25
- import { collectNodeLiveProgress } from "../WorkflowsView.ts";
26
- import { projectRecordProgress } from "../detail-content.ts";
27
-
28
- // ── Fixtures ──────────────────────────────────────────────────
29
-
30
- const T0 = 1_700_000_000_000;
31
-
32
- /** duck-typed ExecutionRecord(turns 形态对齐 updateFromEvent 累积产物)。 */
33
- function makeRecord(overrides: Partial<ExecutionRecord> = {}): ExecutionRecord {
34
- return {
35
- id: "sa-x",
36
- agent: "worker",
37
- model: "default",
38
- thinkingLevel: undefined,
39
- mode: "background",
40
- task: "调研 A",
41
- slug: "research-a",
42
- startedAt: T0,
43
- rootSessionId: undefined,
44
- parentRecordId: undefined,
45
- depth: 0,
46
- status: "running",
47
- turns: [],
48
- turnCount: 0,
49
- totalTokens: 0,
50
- lastError: undefined,
51
- endedAt: undefined,
52
- result: undefined,
53
- error: undefined,
54
- agentResult: undefined,
55
- ...overrides,
56
- } as unknown as ExecutionRecord;
57
- }
58
-
59
- /** recordToSubagent 同源投影:SubagentRecord 的实时字段面(eventLog/currentActivity
60
- * 在真实链经 getEventLog/getCurrentActivity 派生,此处消费旧投影输出(同一函数产物)。 */
61
- function toSubagentFace(record: ExecutionRecord, oldView: ReturnType<typeof projectLiveProgress>): SubagentRecord {
62
- return {
63
- id: record.id,
64
- agent: record.agent,
65
- task: record.task,
66
- slug: record.slug,
67
- status: record.status,
68
- mode: record.mode,
69
- startedAt: record.startedAt,
70
- rootSessionId: undefined,
71
- parentRecordId: undefined,
72
- depth: 0,
73
- endedAt: record.endedAt,
74
- turns: oldView.turns,
75
- totalTokens: oldView.totalTokens,
76
- model: record.model,
77
- thinkingLevel: undefined,
78
- eventLog: oldView.eventLog,
79
- displayItems: [],
80
- currentActivity: oldView.currentActivity,
81
- result: undefined,
82
- error: undefined,
83
- } as SubagentRecord;
84
- }
85
-
86
- /** 双 turn + running tool + lastError 的复合形态(覆盖七字段全维度)。 */
87
- function compositeRecord(over: Partial<ExecutionRecord> = {}): ExecutionRecord {
88
- return makeRecord({
89
- turnCount: 2,
90
- totalTokens: 12345,
91
- lastError: "EPIPE: broken pipe",
92
- turns: [
93
- {
94
- text: "turn one done",
95
- thinking: "",
96
- closed: true,
97
- closedTs: T0 + 1000,
98
- toolCalls: [
99
- { toolName: "read", args: { path: "/a/b.ts" }, result: "ok", isError: false, _status: "completed", startedTs: T0 + 100 },
100
- { toolName: "bash", args: { command: "ls" }, result: "out", isError: false, _status: "completed", startedTs: T0 + 300 },
101
- ],
102
- usageDelta: undefined,
103
- },
104
- {
105
- text: "",
106
- thinking: "planning next",
107
- closed: false,
108
- toolCalls: [
109
- { toolName: "write", args: { path: "/c.ts" }, result: "", isError: false, _status: "running", startedTs: T0 + 2000 },
110
- ],
111
- usageDelta: undefined,
112
- },
113
- ],
114
- ...over,
115
- });
116
- }
117
-
118
- // ── 1. 双路径对齐 ─────────────────────────────────────────────
119
-
120
- describe("projectRecordProgress × projectLiveProgress 双路径对齐", () => {
121
- it("复合形态(双 turn + running tool + lastError)七字段逐项相等", () => {
122
- const record = compositeRecord();
123
- const oldView = projectLiveProgress(record);
124
- const newView = projectRecordProgress(toSubagentFace(record, oldView));
125
-
126
- expect(newView.totalTokens).toBe(oldView.totalTokens);
127
- expect(newView.toolCallCount).toBe(getAllToolCalls(record).length); // 工具计数恒等(旧路径口径)
128
- expect(newView.elapsedSeconds).toBe(oldView.elapsedSeconds);
129
- expect(newView.turns).toBe(oldView.turns);
130
- expect(newView.eventLog).toEqual(oldView.eventLog);
131
- expect(newView.currentActivity).toEqual(oldView.currentActivity);
132
- expect(newView.lastError).toBe(oldView.lastError);
133
- });
134
-
135
- it("空形态(零 turn 零 token)两边同退化为零值", () => {
136
- const record = makeRecord();
137
- const oldView = projectLiveProgress(record);
138
- const newView = projectRecordProgress(toSubagentFace(record, oldView));
139
- expect(newView.totalTokens).toBe(0);
140
- expect(newView.toolCallCount).toBe(0);
141
- expect(newView.turns).toBe(0);
142
- expect(newView.eventLog).toEqual([]);
143
- expect(newView.currentActivity).toBeUndefined();
144
- expect(newView.lastError).toBeUndefined();
145
- });
146
-
147
- it("elapsedSeconds 同源:endedAt 固定时两边都由 computeElapsedSeconds 现算", () => {
148
- const record = compositeRecord({ endedAt: T0 + 9000 });
149
- const oldView = projectLiveProgress(record);
150
- const newView = projectRecordProgress(toSubagentFace(record, oldView));
151
- const direct = computeElapsedSeconds({ startedAt: T0, endedAt: T0 + 9000 });
152
- expect(newView.elapsedSeconds).toBe(direct);
153
- expect(oldView.elapsedSeconds).toBe(direct);
154
- });
155
- });
156
-
157
- // ── 2. 派生恒等(构造性论证的行为锁定) ────────────────────────
158
-
159
- describe("projectRecordProgress 派生恒等", () => {
160
- it("toolCallCount = eventLog tool_start 计数 = getAllToolCalls 长度", () => {
161
- const record = compositeRecord();
162
- const oldView = projectLiveProgress(record);
163
- const starts = oldView.eventLog.filter((e) => e.type === "tool_start").length;
164
- expect(starts).toBe(3); // read + bash + write
165
- expect(projectRecordProgress(toSubagentFace(record, oldView)).toolCallCount).toBe(getAllToolCalls(record).length);
166
- });
167
-
168
- it("lastError 从 eventLog 末条 error 派生;无 error 条目 → undefined", () => {
169
- const withErr = compositeRecord();
170
- const oldErr = projectLiveProgress(withErr);
171
- expect(oldErr.eventLog.at(-1)?.type).toBe("error"); // 前置:getEventLog 把 lastError 追加为末条
172
- expect(projectRecordProgress(toSubagentFace(withErr, oldErr)).lastError).toBe("EPIPE: broken pipe");
173
-
174
- const noErr = compositeRecord({ lastError: undefined });
175
- const oldNo = projectLiveProgress(noErr);
176
- expect(oldNo.eventLog.some((e) => e.type === "error")).toBe(false);
177
- expect(projectRecordProgress(toSubagentFace(noErr, oldNo)).lastError).toBeUndefined();
178
- });
179
- });
180
-
181
- // ── 3. collectNodeLiveProgress 配对(record ↔ trace node) ─────
182
-
183
- function makeRunShape(nodes: { stepIndex: number; task: string; status: string; startedAt: string }[]): WorkflowRun {
184
- return {
185
- state: { trace: { toArray: () => nodes } },
186
- } as unknown as WorkflowRun;
187
- }
188
-
189
- function makeSub(over: Partial<SubagentRecord> = {}): SubagentRecord {
190
- return {
191
- id: "sa-match",
192
- task: "调研 A",
193
- status: "running",
194
- startedAt: T0,
195
- turns: 0,
196
- totalTokens: 0,
197
- eventLog: [],
198
- ...over,
199
- } as SubagentRecord;
200
- }
201
-
202
- describe("collectNodeLiveProgress 配对", () => {
203
- it("task 全等匹配:running node ↔ running record,投影挂到 stepIndex", () => {
204
- const run = makeRunShape([{ stepIndex: 0, task: "调研 A", status: "running", startedAt: new Date(T0).toISOString() }]);
205
- const live = collectNodeLiveProgress(run, [makeSub({ totalTokens: 42 })]);
206
- expect(live.get(0)?.totalTokens).toBe(42);
207
- });
208
-
209
- it("多候选(parallel 同 prompt)取 startedAt 最近邻,贪心一一不重复消费", () => {
210
- const run = makeRunShape([
211
- { stepIndex: 0, task: "调研 A", status: "running", startedAt: new Date(T0).toISOString() },
212
- { stepIndex: 1, task: "调研 A", status: "running", startedAt: new Date(T0 + 5000).toISOString() },
213
- ]);
214
- const near0 = makeSub({ id: "sa-near0", startedAt: T0 + 10, totalTokens: 100 });
215
- const near1 = makeSub({ id: "sa-near1", startedAt: T0 + 5010, totalTokens: 200 });
216
- const live = collectNodeLiveProgress(run, [near1, near0]);
217
- expect(live.get(0)?.totalTokens).toBe(100); // node0(startedAt=T0) 最近 = near0
218
- expect(live.get(1)?.totalTokens).toBe(200); // node1(T0+5s) 最近 = near1
219
- });
220
-
221
- it("终态 node 不配对;task 不匹配(不同 prompt)不配对", () => {
222
- const run = makeRunShape([
223
- { stepIndex: 0, task: "调研 A", status: "completed", startedAt: new Date(T0).toISOString() },
224
- { stepIndex: 1, task: "写总结", status: "running", startedAt: new Date(T0).toISOString() },
225
- ]);
226
- const live = collectNodeLiveProgress(run, [makeSub({ task: "调研 A", totalTokens: 99 })]);
227
- expect(live.size).toBe(0); // node0 终态跳过;node1 task 不匹配
228
- });
229
-
230
- it("重试间隙(无 running record)→ 空 map(views 走终态 fallback 渲染)", () => {
231
- const run = makeRunShape([{ stepIndex: 0, task: "调研 A", status: "running", startedAt: new Date(T0).toISOString() }]);
232
- const live = collectNodeLiveProgress(run, [makeSub({ status: "closed" })]);
233
- expect(live.size).toBe(0);
234
- });
235
- });