@zhushanwen/pi-subagent-workflow 7.2.0 → 7.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-subagent-workflow",
3
- "version": "7.2.0",
3
+ "version": "7.3.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.",
@@ -0,0 +1,117 @@
1
+ // ManifestStore — parentRecordId 落盘测试(M3a)。
2
+ //
3
+ // 验证 M3a 三条契约:
4
+ // 1. 新 record 落盘含 parentRecordId(depth>=1 subagent 读回直接父 record id)
5
+ // 2. 旧 manifest JSON(无 parentRecordId 字段)readManifest 仍有效(向后兼容)
6
+ // 3. isValidManifest 校验不改(5 必填不变,parentRecordId optional)
7
+ //
8
+ // isValidManifest 私有未 export——通过 readManifest 等价验证:readManifest 内部
9
+ // `isValidManifest(parsed) ? parsed : null`,非 null ⟺ 校验通过。不 export 私有函数
10
+ // 避免扩大公共 API 表面(M3a C1 约束:不改 isValidManifest,仅 2 行代码改动)。
11
+
12
+ import * as fs from "node:fs";
13
+ import * as os from "node:os";
14
+ import * as path from "node:path";
15
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
16
+
17
+ import { ManifestStore, type ManifestRecord } from "../manifest-store.ts";
18
+
19
+ /** 构造最小合法 ManifestRecord(5 必填),optional 字段按 overrides 传入。 */
20
+ function makeBaseManifest(overrides: Partial<ManifestRecord> = {}): ManifestRecord {
21
+ return {
22
+ id: "rec-test",
23
+ rootSessionId: "session-main",
24
+ agentName: "worker",
25
+ status: "completed",
26
+ createdAt: 1000,
27
+ ...overrides,
28
+ };
29
+ }
30
+
31
+ describe("ManifestStore — parentRecordId 落盘 (M3a)", () => {
32
+ let tmpDir: string;
33
+ let store: ManifestStore;
34
+
35
+ beforeEach(() => {
36
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "manifest-parentid-"));
37
+ store = new ManifestStore(tmpDir);
38
+ });
39
+
40
+ afterEach(() => {
41
+ fs.rmSync(tmpDir, { recursive: true, force: true });
42
+ });
43
+
44
+ // ── TC-m3a-new-record-parentid ──
45
+ it("新 record 落盘含 parentRecordId(depth>=1 读回直接父 record id)", async () => {
46
+ const record = makeBaseManifest({
47
+ id: "rec-child",
48
+ parentRecordId: "sa-parent-1",
49
+ });
50
+ await store.writeManifest(record);
51
+
52
+ const readBack = await store.readManifest("rec-child");
53
+ expect(readBack).not.toBeNull();
54
+ expect(readBack?.parentRecordId).toBe("sa-parent-1");
55
+ });
56
+
57
+ it("顶层 record(parentRecordId 缺失)落盘读回仍 undefined", async () => {
58
+ // 顶层 subagent parentRecordId=undefined(父是 main,main 无 record)
59
+ const record = makeBaseManifest({ id: "rec-top" });
60
+ await store.writeManifest(record);
61
+
62
+ const readBack = await store.readManifest("rec-top");
63
+ expect(readBack).not.toBeNull();
64
+ expect(readBack?.parentRecordId).toBeUndefined();
65
+ });
66
+
67
+ // ── TC-m3a-old-manifest-compat ──
68
+ it("旧 manifest JSON(无 parentRecordId)readManifest 返有效(isValidManifest 通过)", async () => {
69
+ // 手写旧版本格式 manifest(无 parentRecordId 字段),模拟旧 subagent-workflow 写的磁盘文件
70
+ const oldManifest = {
71
+ id: "rec-old",
72
+ rootSessionId: "session-main",
73
+ agentName: "worker",
74
+ status: "completed",
75
+ createdAt: 2000,
76
+ // 无 parentRecordId —— 旧版本写的
77
+ };
78
+ fs.writeFileSync(
79
+ path.join(tmpDir, "rec-old.json"),
80
+ JSON.stringify(oldManifest),
81
+ "utf-8",
82
+ );
83
+
84
+ const readBack = await store.readManifest("rec-old");
85
+ // readManifest 内 isValidManifest(parsed) ? parsed : null —— 非 null 即 5 必填校验通过
86
+ expect(readBack).not.toBeNull();
87
+ expect(readBack?.id).toBe("rec-old");
88
+ expect(readBack?.parentRecordId).toBeUndefined();
89
+ });
90
+
91
+ // ── TC-m3a-isvalidmanifest-unchanged ──
92
+ it("isValidManifest 不改:含/不含 parentRecordId 均通过(5 必填不变)", async () => {
93
+ // isValidManifest 私有,通过 readManifest 等价验证(非 null ⟺ 校验通过)。
94
+ // 含 parentRecordId —— 新格式
95
+ const withParent = makeBaseManifest({
96
+ id: "rec-with-parent",
97
+ parentRecordId: "sa-x",
98
+ });
99
+ await store.writeManifest(withParent);
100
+ expect(await store.readManifest("rec-with-parent")).not.toBeNull();
101
+
102
+ // 不含 parentRecordId —— 仅 5 必填(旧格式 / 顶层 record)
103
+ const minimal = {
104
+ id: "rec-minimal",
105
+ rootSessionId: "session-main",
106
+ agentName: "worker",
107
+ status: "completed",
108
+ createdAt: 3000,
109
+ };
110
+ fs.writeFileSync(
111
+ path.join(tmpDir, "rec-minimal.json"),
112
+ JSON.stringify(minimal),
113
+ "utf-8",
114
+ );
115
+ expect(await store.readManifest("rec-minimal")).not.toBeNull();
116
+ });
117
+ });
@@ -144,6 +144,7 @@ export async function doFinalizeRecord(
144
144
  await deps.manifestStore.writeManifest({
145
145
  id: record.id,
146
146
  rootSessionId: record.rootSessionId ?? "",
147
+ parentRecordId: record.parentRecordId,
147
148
  agentName: record.agent,
148
149
  status: status === "done" ? "completed" : status,
149
150
  createdAt: record.startedAt,
@@ -7,6 +7,8 @@ import { bestEffort } from "./best-effort.ts";
7
7
  export interface ManifestRecord {
8
8
  id: string;
9
9
  rootSessionId: string;
10
+ /** 直接父 subagent record ID(层级树构建用)。顶层 record 缺失(undefined)。M3a 补字段。 */
11
+ parentRecordId?: string;
10
12
  agentName: string;
11
13
  /**
12
14
  * 终态枚举:finalizeRecord 写 running/completed/failed/cancelled 四态;cancelled 不再
@@ -25,6 +27,9 @@ export interface ManifestRecord {
25
27
  model?: string;
26
28
  }
27
29
 
30
+ /** JSON.stringify 缩进空格数(no-magic-numbers 合规)。 */
31
+ const MANIFEST_INDENT_SPACES = 2;
32
+
28
33
  /** 合法 manifest status 集合(4 态;运行时守卫用,磁盘文件可能陈旧/损坏)。crashed 不在其中。 */
29
34
  const VALID_MANIFEST_STATUSES: ReadonlySet<string> = new Set([
30
35
  "running",
@@ -69,7 +74,7 @@ export class ManifestStore {
69
74
  async writeManifest(record: ManifestRecord): Promise<void> {
70
75
  const filePath = path.join(this.dir, `${record.id}.json`);
71
76
  const tmpPath = `${filePath}.tmp.${process.pid}`;
72
- const content = JSON.stringify(record, null, 2);
77
+ const content = JSON.stringify(record, null, MANIFEST_INDENT_SPACES);
73
78
 
74
79
  let renamed = false;
75
80
  try {