ppxans-harness 2.6.0 → 3.2.1

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 (189) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +195 -195
  3. package/bin/ppx-channels.js +2 -2
  4. package/bin/ppx-serve.js +5 -5
  5. package/bin/ppx-setup.js +124 -0
  6. package/bin/ppx-web.js +140 -0
  7. package/bin/ppx.js +2 -2
  8. package/config/identity.md +6 -6
  9. package/config/ishiki.md +16 -16
  10. package/config/ppx.json +6 -124
  11. package/config/ppx.json.example +143 -0
  12. package/package.json +16 -6
  13. package/skills/.usage.json +6 -0
  14. package/skills/agent-professional-training/SKILL.md +94 -0
  15. package/skills/agent-professional-training/trajectories/2026-09-11-dpo-pairs.jsonl +2 -0
  16. package/skills/agent-professional-training/trajectories/2026-09-11-round2-trajectory.json +15 -0
  17. package/skills/agent-professional-training/trajectories/2026-09-13-first-run-trajectory.json +17 -0
  18. package/skills/brainstorm/SKILL.md +24 -0
  19. package/skills/cupid-lover-comms/SKILL.md +37 -0
  20. package/skills/debug/SKILL.md +26 -0
  21. package/skills/plan/SKILL.md +25 -0
  22. package/skills/ponytail/SKILL.md +25 -0
  23. package/skills/ppx-memory/SKILL.md +91 -0
  24. package/skills/ppx-memory/scripts/cli.js +192 -0
  25. package/skills/ppx-memory/scripts/experience.js +133 -0
  26. package/skills/ppx-memory/scripts/fact-store.js +842 -0
  27. package/skills/ppx-memory/scripts/l0.js +52 -0
  28. package/skills/ppx-memory/scripts/l2.js +146 -0
  29. package/skills/ppx-memory/scripts/l3.js +112 -0
  30. package/skills/ppx-memory/scripts/memory-ticker.js +238 -0
  31. package/skills/ppx-memory/scripts/pii.js +42 -0
  32. package/skills/ppx-memory/scripts/schema.js +80 -0
  33. package/skills/ppx-memory/scripts/session.js +398 -0
  34. package/skills/ppx-memory/scripts/similarity.js +43 -0
  35. package/skills/ppx-memory/scripts/store.js +116 -0
  36. package/skills/ppx-memory/scripts/wal.js +38 -0
  37. package/skills/ppx-selfheal/SKILL.md +24 -0
  38. package/skills/ppx-selfheal/scripts/cli.js +80 -0
  39. package/skills/ppx-selfheal/scripts/healer.js +184 -0
  40. package/skills/ppx-selfheal/scripts/logger.js +17 -0
  41. package/skills/ppx-selfheal/scripts/store.js +116 -0
  42. package/skills/prompt-depth-kit/SKILL.md +28 -0
  43. package/skills/session-naming/SKILL.md +36 -0
  44. package/skills/verify/SKILL.md +25 -0
  45. package/src/agent/index.js +1352 -717
  46. package/src/agent/prompts.js +46 -2
  47. package/src/aml-server.js +197 -151
  48. package/src/ans/eviction.js +123 -143
  49. package/src/ans/guard.js +159 -120
  50. package/src/ans/lifecycle.js +96 -93
  51. package/src/ans/proactive.js +112 -129
  52. package/src/ans/reward.js +95 -111
  53. package/src/ans/values.js +15 -15
  54. package/src/audit/audit-chain.js +43 -7
  55. package/src/audit/verifier.js +157 -120
  56. package/src/bus/circuit-breaker.js +9 -1
  57. package/src/bus/runtime-bus.js +107 -93
  58. package/src/channels/base.js +57 -34
  59. package/src/channels/feishu.js +118 -126
  60. package/src/channels/http.js +1090 -651
  61. package/src/channels/index.js +111 -110
  62. package/src/channels/log.js +29 -29
  63. package/src/channels/wechat-crypto.js +73 -74
  64. package/src/channels/wechat.js +191 -197
  65. package/src/channels/workspace.js +94 -0
  66. package/src/channels-cli.js +126 -124
  67. package/src/cli.js +129 -120
  68. package/src/commands/index.js +142 -0
  69. package/src/config/channels.js +137 -170
  70. package/src/config/index.js +277 -224
  71. package/src/config/placeholder.js +31 -0
  72. package/src/config/providers.js +148 -182
  73. package/src/config/settings.js +156 -182
  74. package/src/core/policy.js +69 -18
  75. package/src/core/trace.js +8 -6
  76. package/src/edit/editblock.js +266 -0
  77. package/src/edit/snapshot.js +67 -0
  78. package/src/evidence/index.js +153 -0
  79. package/src/evolve/playbook.js +9 -10
  80. package/src/hooks/index.js +113 -0
  81. package/src/llm/client.js +4 -3
  82. package/src/llm/dsml.js +74 -74
  83. package/src/llm/embedder.js +41 -35
  84. package/src/llm/fence.js +52 -52
  85. package/src/llm/index.js +4 -4
  86. package/src/llm/local-embedder.js +94 -0
  87. package/src/llm/presets.js +113 -0
  88. package/src/llm/pricing.js +93 -0
  89. package/src/llm/retry.js +73 -73
  90. package/src/llm/router.js +92 -89
  91. package/src/mcp/admin.js +44 -10
  92. package/src/mcp/client.js +487 -477
  93. package/src/mcp/http.js +23 -37
  94. package/src/mcp/index.js +116 -116
  95. package/src/mcp/server.js +23 -18
  96. package/src/mcp/tasks.js +9 -14
  97. package/src/memory/asset-hub.js +6 -11
  98. package/src/memory/canvas.js +2 -5
  99. package/src/memory/compaction.js +28 -28
  100. package/src/memory/experience.js +133 -122
  101. package/src/memory/fact-store.js +914 -698
  102. package/src/memory/failure-episode.js +20 -11
  103. package/src/memory/fork.js +17 -8
  104. package/src/memory/index.js +8 -6
  105. package/src/memory/l0.js +53 -52
  106. package/src/memory/l2.js +145 -130
  107. package/src/memory/l3.js +111 -111
  108. package/src/memory/legion-board.js +71 -0
  109. package/src/memory/memory-ticker.js +239 -240
  110. package/src/memory/session.js +398 -397
  111. package/src/memory/sqlite-store.js +581 -0
  112. package/src/mode/blackboard.js +49 -49
  113. package/src/mode/graph.js +42 -41
  114. package/src/mode/index.js +64 -64
  115. package/src/mode/legion.js +54 -51
  116. package/src/mode/plan-exec.js +50 -50
  117. package/src/mode/router.js +28 -40
  118. package/src/orchestrator/agent-worker.js +69 -69
  119. package/src/orchestrator/dag.js +90 -83
  120. package/src/orchestrator/experts.js +76 -0
  121. package/src/orchestrator/index.js +1 -1
  122. package/src/orchestrator/legion.js +179 -187
  123. package/src/orchestrator/supervisor.js +6 -8
  124. package/src/permissions/index.js +378 -0
  125. package/src/persona/index.js +28 -29
  126. package/src/plugin/builtin.js +313 -215
  127. package/src/plugin/context.js +80 -79
  128. package/src/plugin/index.js +62 -62
  129. package/src/plugin/v3.js +73 -0
  130. package/src/protocol/index.js +148 -0
  131. package/src/repomap/index.js +309 -0
  132. package/src/review/index.js +393 -0
  133. package/src/seam/registry.js +3 -0
  134. package/src/seam/shell.js +55 -55
  135. package/src/security/injection.js +79 -0
  136. package/src/selfheal/evolve.js +67 -67
  137. package/src/selfheal/healer.js +184 -167
  138. package/src/selfheal/run.js +9 -9
  139. package/src/server.js +63 -60
  140. package/src/services/diagnose.js +180 -0
  141. package/src/services/learning-service.js +9 -0
  142. package/src/services/memory-health.js +34 -6
  143. package/src/services/memory-service.js +42 -9
  144. package/src/services/triage.js +138 -0
  145. package/src/session/parts.js +76 -0
  146. package/src/session/projection.js +73 -0
  147. package/src/session/rollout.js +54 -0
  148. package/src/session/turn.js +137 -0
  149. package/src/skills/lint.js +72 -0
  150. package/src/skills/loader.js +231 -150
  151. package/src/skills/search.js +58 -0
  152. package/src/skills/verify.js +95 -100
  153. package/src/tools/advanced.js +388 -352
  154. package/src/tools/builtin.js +384 -297
  155. package/src/tools/catalog.js +283 -159
  156. package/src/tools/command-guard.js +112 -112
  157. package/src/tools/custom.js +47 -47
  158. package/src/tools/delegate.js +383 -297
  159. package/src/tools/document.js +254 -253
  160. package/src/tools/git.js +151 -0
  161. package/src/tools/governance.js +47 -20
  162. package/src/tools/index.js +16 -11
  163. package/src/tools/methods.js +178 -178
  164. package/src/tools/ocr.js +59 -59
  165. package/src/tools/sandbox-worker.js +40 -0
  166. package/src/tools/sandbox.js +92 -0
  167. package/src/tools/seam.js +162 -125
  168. package/src/tools/selfmod.js +196 -176
  169. package/src/tools/v3.js +225 -0
  170. package/src/tools/vad.js +176 -0
  171. package/src/tools/voice.js +238 -0
  172. package/src/utils/async.js +14 -0
  173. package/src/utils/config-file.js +53 -0
  174. package/src/utils/crashguard.js +88 -0
  175. package/src/utils/http.js +53 -0
  176. package/src/utils/id.js +8 -0
  177. package/src/utils/json-state.js +33 -0
  178. package/src/utils/logger.js +17 -17
  179. package/src/utils/ndjson.js +25 -0
  180. package/src/utils/pii.js +42 -42
  181. package/src/utils/rate-limit.js +50 -0
  182. package/src/utils/schema.js +80 -0
  183. package/src/utils/similarity.js +43 -0
  184. package/src/utils/store.js +170 -108
  185. package/src/utils/text.js +15 -15
  186. package/src/utils/trace.js +153 -153
  187. package/src/utils/wal.js +39 -0
  188. package/src/utils/winutf8.js +16 -15
  189. package/src/wiki/index.js +170 -0
@@ -1,83 +1,90 @@
1
- // src/orchestrator/dag.js - DAG 任务编排 (军团从 broadcast/dispatch 升级为任务图)
2
- // 纯函数零依赖: topoLevels 拓扑分层 + runDag 分层并行执行 + 依赖结果数据流
3
- // graph.nodes = [{ id, task, dependsOn?: [id], agent?: name }]
4
-
5
- // 拓扑分层 (Kahn 算法): 返回 [ [同层可并行的节点id], ... ], 检测环
6
- // v1.0.8: 校验重复 id / 依赖不存在 (原实现静默丢弃不存在的依赖, 重复 id 误报成环)
7
- export function topoLevels(nodes) {
8
- const ids = new Set();
9
- for (const n of nodes) {
10
- if (!n || !n.id) throw new Error("DAG 节点缺 id");
11
- if (ids.has(n.id)) throw new Error(`DAG 节点 id 重复: ${n.id}`);
12
- ids.add(n.id);
13
- }
14
- const indeg = new Map();
15
- const children = new Map();
16
- for (const n of nodes) {
17
- const deps = n.dependsOn || [];
18
- for (const d of deps) {
19
- if (!ids.has(d)) throw new Error(`DAG 节点 ${n.id} 依赖不存在的节点: ${d}`);
20
- }
21
- indeg.set(n.id, deps.length);
22
- for (const d of deps) {
23
- if (!children.has(d)) children.set(d, []);
24
- children.get(d).push(n.id);
25
- }
26
- }
27
- const levels = [];
28
- let frontier = nodes.filter((n) => indeg.get(n.id) === 0).map((n) => n.id);
29
- const seen = new Set();
30
- while (frontier.length) {
31
- levels.push(frontier);
32
- const next = [];
33
- for (const id of frontier) {
34
- seen.add(id);
35
- for (const c of children.get(id) || []) {
36
- indeg.set(c, indeg.get(c) - 1);
37
- if (indeg.get(c) === 0) next.push(c);
38
- }
39
- }
40
- frontier = next;
41
- }
42
- if (seen.size < nodes.length) {
43
- const cyclic = [...ids].filter((id) => !seen.has(id)).join(",");
44
- throw new Error("DAG 存在环, 无法编排: " + cyclic);
45
- }
46
- return levels;
47
- }
48
-
49
- // 执行 DAG: 每层节点并行, 上游结果作为 deps 传给下游 executor。
50
- // executor(id, node, deps) => Promise<result>, deps = { 依赖id: 结果 }
51
- // concurrency: 每层并发的上限 (默认 0 = 不设限, 兼容旧调用; Legion 会传入自己的 maxConcurrent)
52
- export async function runDag(graph, executor, { concurrency = 0 } = {}) {
53
- const byId = new Map(graph.nodes.map((n) => [n.id, n]));
54
- const levels = topoLevels(graph.nodes);
55
- const results = {};
56
- const order = [];
57
- const cap = concurrency > 0 ? concurrency : Infinity;
58
- for (const level of levels) {
59
- // 有界并发: 层内若超出 cap, 用轮询槽位的方式逐个交给并行执行, 避免一次性 Promise.all 打爆子进程
60
- let idx = 0;
61
- const runOne = async (id) => {
62
- const node = byId.get(id);
63
- const deps = {};
64
- for (const d of node.dependsOn || []) deps[d] = results[d];
65
- results[id] = await executor(id, node, deps);
66
- order.push(id);
67
- };
68
- await new Promise((resolveAll) => {
69
- let active = 0;
70
- let doneCount = 0;
71
- const tick = () => {
72
- while (active < cap && idx < level.length) {
73
- const id = level[idx++];
74
- active++;
75
- runOne(id).finally(() => { active--; doneCount++; tick(); });
76
- }
77
- if (doneCount >= level.length) resolveAll();
78
- };
79
- tick();
80
- });
81
- }
82
- return { results, order };
83
- }
1
+ // src/orchestrator/dag.js - DAG 任务编排 (军团从 broadcast/dispatch 升级为任务图)
2
+ // 纯函数零依赖: topoLevels 拓扑分层 + runDag 分层并行执行 + 依赖结果数据流
3
+ // graph.nodes = [{ id, task, dependsOn?: [id], agent?: name }]
4
+
5
+ // 拓扑分层 (Kahn 算法): 返回 [ [同层可并行的节点id], ... ], 检测环
6
+ // v1.0.8: 校验重复 id / 依赖不存在 (原实现静默丢弃不存在的依赖, 重复 id 误报成环)
7
+ export function topoLevels(nodes) {
8
+ const ids = new Set();
9
+ for (const n of nodes) {
10
+ if (!n || !n.id) throw new Error("DAG 节点缺 id");
11
+ if (ids.has(n.id)) throw new Error(`DAG 节点 id 重复: ${n.id}`);
12
+ ids.add(n.id);
13
+ }
14
+ const indeg = new Map();
15
+ const children = new Map();
16
+ for (const n of nodes) {
17
+ const deps = n.dependsOn || [];
18
+ for (const d of deps) {
19
+ if (!ids.has(d)) throw new Error(`DAG 节点 ${n.id} 依赖不存在的节点: ${d}`);
20
+ }
21
+ indeg.set(n.id, deps.length);
22
+ for (const d of deps) {
23
+ if (!children.has(d)) children.set(d, []);
24
+ children.get(d).push(n.id);
25
+ }
26
+ }
27
+ const levels = [];
28
+ let frontier = nodes.filter((n) => indeg.get(n.id) === 0).map((n) => n.id);
29
+ const seen = new Set();
30
+ while (frontier.length) {
31
+ levels.push(frontier);
32
+ const next = [];
33
+ for (const id of frontier) {
34
+ seen.add(id);
35
+ for (const c of children.get(id) || []) {
36
+ indeg.set(c, indeg.get(c) - 1);
37
+ if (indeg.get(c) === 0) next.push(c);
38
+ }
39
+ }
40
+ frontier = next;
41
+ }
42
+ if (seen.size < nodes.length) {
43
+ const cyclic = [...ids].filter((id) => !seen.has(id)).join(",");
44
+ throw new Error("DAG 存在环, 无法编排: " + cyclic);
45
+ }
46
+ return levels;
47
+ }
48
+
49
+ // 执行 DAG: 每层节点并行, 上游结果作为 deps 传给下游 executor。
50
+ // executor(id, node, deps) => Promise<result>, deps = { 依赖id: 结果 }
51
+ // concurrency: 每层并发的上限 (默认 0 = 不设限, 兼容旧调用; Legion 会传入自己的 maxConcurrent)
52
+ export async function runDag(graph, executor, { concurrency = 0 } = {}) {
53
+ const byId = new Map(graph.nodes.map((n) => [n.id, n]));
54
+ const levels = topoLevels(graph.nodes);
55
+ const results = {};
56
+ const order = [];
57
+ const cap = concurrency > 0 ? concurrency : Infinity;
58
+ for (const level of levels) {
59
+ // 有界并发: 层内若超出 cap, 用轮询槽位的方式逐个交给并行执行, 避免一次性 Promise.all 打爆子进程
60
+ let idx = 0;
61
+ const runOne = async (id) => {
62
+ const node = byId.get(id);
63
+ const deps = {};
64
+ for (const d of node.dependsOn || []) deps[d] = results[d];
65
+ // 2026-09-18 修复 (P1): executor 抛错原样冒泡, runOne().finally() 链上无人接住
66
+ // → unhandled rejection → Node 进程直接崩溃 (任一 agent 超时/失败即炸整个编排)。
67
+ // 现捕获后把错误文本作为该节点结果, 下游节点可感知失败, 编排整体继续。
68
+ try {
69
+ results[id] = await executor(id, node, deps);
70
+ } catch (e) {
71
+ results[id] = `[执行失败] ${e && e.message ? e.message : e}`;
72
+ }
73
+ order.push(id);
74
+ };
75
+ await new Promise((resolveAll) => {
76
+ let active = 0;
77
+ let doneCount = 0;
78
+ const tick = () => {
79
+ while (active < cap && idx < level.length) {
80
+ const id = level[idx++];
81
+ active++;
82
+ runOne(id).finally(() => { active--; doneCount++; tick(); });
83
+ }
84
+ if (doneCount >= level.length) resolveAll();
85
+ };
86
+ tick();
87
+ });
88
+ }
89
+ return { results, order };
90
+ }
@@ -0,0 +1,76 @@
1
+ // src/orchestrator/experts.js - 专家名册 (2026-10-02)
2
+ // 对标 MetaGPT/ChatDev 的固化分工: 代码交给代码专家, 设计交给设计专家。
3
+ // 每个专家 = 中文名 (角色) + 专属视角 (perspective, 注入 worker 对抗同质失败) + 可选只读约束。
4
+ // spawn_agent.expert / experts 参数消费本名册; 视角只是提示不是牢笼, worker 仍有全部上下文能力。
5
+
6
+ export const EXPERTS = {
7
+ code: {
8
+ name: "代码专家",
9
+ perspective:
10
+ "你是代码专家。聚焦实现正确性: 边界条件/异常路径/并发风险/最小改动原则。给出具体到文件与函数级建议, 不空谈架构。",
11
+ },
12
+ architect: {
13
+ name: "架构专家",
14
+ perspective:
15
+ "你是架构专家。聚焦模块边界/依赖方向/扩展成本/复杂度风险。先看全局再下结论, 明确说哪些不该做。",
16
+ },
17
+ review: {
18
+ name: "代码审查专家",
19
+ perspective:
20
+ "你是代码审查专家。只报告不修改。按严重度分级 (Critical/Important/Suggestion) 输出问题清单, 每条给出位置与理由。",
21
+ readonly: true,
22
+ },
23
+ test: {
24
+ name: "测试专家",
25
+ perspective:
26
+ "你是测试专家。聚焦边界用例/异常路径/回归风险。给出可直接落地的测试清单, 标注优先级。",
27
+ },
28
+ security: {
29
+ name: "安全专家",
30
+ perspective:
31
+ "你是安全专家。只读审查。聚焦注入/密钥泄露/权限边界/数据暴露面/危险命令, 按风险等级输出。",
32
+ readonly: true,
33
+ },
34
+ design: {
35
+ name: "设计专家",
36
+ perspective:
37
+ "你是设计专家。聚焦用户体验/信息架构/交互一致性/中文向导友好度。给具体可执行的改进项, 不给空洞美学口号。",
38
+ },
39
+ docs: {
40
+ name: "文档专家",
41
+ perspective:
42
+ "你是文档专家。聚焦准确性/完整性/示例可跑性/术语一致。逐条指出文档与实际行为的偏差。",
43
+ },
44
+ data: {
45
+ name: "数据专家",
46
+ perspective:
47
+ "你是数据专家。聚焦数据质量/口径一致性/统计方法合理性。结论必须带数据依据, 说不确定就明说。",
48
+ },
49
+ product: {
50
+ name: "产品专家",
51
+ perspective:
52
+ "你是产品专家。聚焦用户价值/优先级/MVP 边界/需求澄清。明确回答: 为谁解决什么问题, 什么先不做。",
53
+ },
54
+ };
55
+
56
+ // 解析专家: 支持英文 id (code) / 中文名 (代码专家, 模糊包含匹配), 不区分大小写; 未命中返回 null
57
+ export function resolveExpert(key) {
58
+ if (!key) return null;
59
+ const k = String(key).trim();
60
+ if (!k) return null;
61
+ const lower = k.toLowerCase();
62
+ // 1. 精确 id
63
+ if (EXPERTS[lower]) return EXPERTS[lower];
64
+ // 2. 中文名/别名模糊匹配 (专家名或 id 被查询串包含)
65
+ for (const [id, e] of Object.entries(EXPERTS)) {
66
+ if (lower.includes(id) || k.includes(e.name) || e.name.includes(k)) return e;
67
+ }
68
+ return null;
69
+ }
70
+
71
+ // 名册摘要 (工具描述/错误提示用)
72
+ export function listExperts() {
73
+ return Object.entries(EXPERTS)
74
+ .map(([id, e]) => `${id}(${e.name}${e.readonly ? "/只读" : ""})`)
75
+ .join("、");
76
+ }
@@ -1,2 +1,2 @@
1
- // src/orchestrator/index.js - 军团统一出口
1
+ // src/orchestrator/index.js - 军团统一出口
2
2
  export { Legion } from "./legion.js";
@@ -1,188 +1,180 @@
1
- // src/orchestrator/legion.js - Agent 军团编排器
2
- // 管理多个独立 agent 子进程, 支持并行派发任务、按角色分工
3
- import { spawn } from "node:child_process";
4
- import path from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- import { EventEmitter } from "node:events";
7
- import { info, warn, error } from "../utils/logger.js";
8
- import { runDag } from "./dag.js";
9
-
10
- const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
11
- const WORKER = path.join(ROOT, "src", "orchestrator", "agent-worker.js");
12
-
13
- export class Legion extends EventEmitter {
14
- constructor({ workerPath = WORKER, nodeBin = process.execPath, maxConcurrent = 8 } = {}) {
15
- super();
16
- this.workerPath = workerPath;
17
- this.nodeBin = nodeBin;
18
- this.maxConcurrent = maxConcurrent; // 军团整体并发上限: 防止大 DAG/广播瞬间 spawn 海量子进程
19
- this.agents = new Map(); // { name: { proc, pending: Map<id,{resolve,reject}>, counter } }
20
- }
21
-
22
- // 有界并发执行器: 以固定并发度跑一批 async 任务, 超出上限的排队 (背压而非一次性 Promise.all)
23
- // 这是 broadcast/runDag 层共享的背压口, 避免"并发无数控、大输入一次性打爆子进程数"
24
- async _mapBounded(items, fn, { concurrency = this.maxConcurrent } = {}) {
25
- const results = new Array(items.length);
26
- let idx = 0;
27
- const workers = Array.from({ length: Math.max(1, Math.min(concurrency, items.length)) });
28
- await Promise.all(workers.map(async () => {
29
- while (idx < items.length) {
30
- const i = idx++;
31
- results[i] = await fn(items[i], i);
32
- }
33
- }));
34
- return results;
35
- }
36
-
37
- // 创建一个 agent 子进程
38
- spawnAgent(name, { dataDir, globalDataDir, env = {} } = {}) {
39
- if (this.agents.has(name)) return this.agents.get(name);
40
- const env2 = { ...process.env, ...env };
41
- if (dataDir) env2.PPX_AGENT_DATA_DIR = dataDir;
42
- // 全局共享目录: 跨 agent 共享经验库 (ANS 全局记忆)
43
- if (globalDataDir) env2.PPX_AGENT_GLOBAL_DATA_DIR = globalDataDir;
44
- const proc = spawn(this.nodeBin, [this.workerPath], {
45
- cwd: ROOT,
46
- env: env2,
47
- stdio: ["pipe", "pipe", "inherit"],
48
- });
49
- const entry = { proc, pending: new Map(), counter: 0 };
50
- this.agents.set(name, entry);
51
-
52
- proc.stdout.setEncoding("utf8");
53
- let buf = "";
54
- proc.stdout.on("data", (chunk) => {
55
- buf += chunk;
56
- let idx;
57
- while ((idx = buf.indexOf("\n")) >= 0) {
58
- const line = buf.slice(0, idx).trim();
59
- buf = buf.slice(idx + 1);
60
- if (!line) continue;
61
- try {
62
- const msg = JSON.parse(line);
63
- // step 中间事件: 触发 onProgress 回调, 不消费 pending (等最终 reply)
64
- if (msg.type === "step" && msg.id && entry.pending.has(msg.id)) {
65
- const p = entry.pending.get(msg.id);
66
- if (p && p.onProgress) { try { p.onProgress(msg); } catch {} }
67
- continue;
68
- }
69
- if (msg.id && entry.pending.has(msg.id)) {
70
- const { resolve, reject } = entry.pending.get(msg.id);
71
- entry.pending.delete(msg.id);
72
- if (msg.type === "error") reject(new Error(msg.error));
73
- else resolve(msg);
74
- }
75
- } catch {}
76
- }
77
- });
78
- proc.on("error", (err) => {
79
- // v1.0.8: spawn 失败 (node bin 不存在等) 兜底, 拒绝所有 pending, 防永久挂起
80
- error(`agent[${name}] 启动失败: ${err.message}`);
81
- for (const [, { reject }] of entry.pending) reject(new Error(`agent ${name} 启动失败`));
82
- entry.pending.clear();
83
- this.agents.delete(name);
84
- });
85
- proc.on("exit", (code) => {
86
- info(`agent[${name}] 退出 code=${code}`);
87
- // 拒绝所有 pending
88
- for (const [, { reject }] of entry.pending) reject(new Error(`agent ${name} 已退出`));
89
- entry.pending.clear();
90
- this.agents.delete(name);
91
- this.emit("exit", name, code);
92
- });
93
- info(`agent[${name}] 已启动 (pid=${proc.pid})`);
94
- return entry;
95
- }
96
-
97
- // 向某 agent 发消息, 返回 Promise (onProgress 可选: 接收 step 中间事件)
98
- // v1.0.8: 超时兜底 (默认 30s), worker 卡死/异常不导致 pending 永久挂起
99
- send(name, msg, { onProgress, timeout = 30000 } = {}) {
100
- const entry = this.agents.get(name);
101
- if (!entry || entry.proc.exitCode !== null) {
102
- return Promise.reject(new Error(`agent ${name} 未运行`));
103
- }
104
- const id = ++entry.counter;
105
- return new Promise((resolve, reject) => {
106
- const timer = setTimeout(() => {
107
- entry.pending.delete(id);
108
- reject(new Error(`agent ${name} 请求超时 (${timeout}ms)`));
109
- }, timeout);
110
- entry.pending.set(id, {
111
- resolve: (v) => { clearTimeout(timer); resolve(v); },
112
- reject: (e) => { clearTimeout(timer); reject(e); },
113
- onProgress,
114
- });
115
- try {
116
- entry.proc.stdin.write(JSON.stringify({ id, ...msg }) + "\n");
117
- } catch (e) {
118
- clearTimeout(timer);
119
- entry.pending.delete(id);
120
- reject(new Error(`agent ${name} 写入失败: ${e.message}`));
121
- }
122
- });
123
- }
124
-
125
- // 并行派发: 同一任务广播给多个 agent, 最快返回
126
- async broadcast(type, message, { timeout = 30000 } = {}) {
127
- const names = [...this.agents.keys()];
128
- if (!names.length) throw new Error("军团为空, 先 spawnAgent");
129
- const results = await this._mapBounded(
130
- names,
131
- (n) => this.send(n, { type, message }, { timeout }).catch((e) => ({ type: "error", error: e.message }))
132
- );
133
- return names.map((n, i) => ({ agent: n, ...results[i] }));
134
- }
135
-
136
- // 按角色分工: 把任务列表分给不同 agent
137
- // ⚠️ 实验性 API: 生产代码无内置消费方 (详见 docs/ARCHITECTURE.md)。仅供在交互场景/测试里手动调用。
138
- // 逐条串行派发, 吞吐低但稳定; 若需高并发的角色分工, 优先用 runDag。
139
- async dispatch(type, tasks) {
140
- const names = [...this.agents.keys()];
141
- const results = [];
142
- for (let i = 0; i < tasks.length; i++) {
143
- const name = names[i % names.length];
144
- try {
145
- const r = await this.send(name, { type, message: tasks[i] });
146
- results.push({ agent: name, task: tasks[i], ...r });
147
- } catch (e) {
148
- results.push({ agent: name, task: tasks[i], type: "error", error: e.message });
149
- }
150
- }
151
- return results;
152
- }
153
-
154
- // DAG 任务编排: 按依赖拓扑分层执行, 同层并行, 上游结果传入下游 (P3)
155
- // graph = { nodes: [{ id, task, dependsOn?: [id], agent?: name }] }
156
- // 返回 { results: {id: reply}, order: [执行顺序] }
157
- async runDag(graph) {
158
- const names = [...this.agents.keys()];
159
- if (!names.length) throw new Error("军团为空, 先 spawnAgent");
160
- let rr = 0; // round-robin 派发未指定 agent 的节点
161
- return runDag(graph, async (id, node, deps) => {
162
- const agent = node.agent || names[rr++ % names.length];
163
- const depText = Object.entries(deps)
164
- .map(([k, v]) => `${k}: ${String(v).slice(0, 500)}`)
165
- .join("\n");
166
- const message = node.task + (depText ? "\n\n[上游结果]\n" + depText : "");
167
- const r = await this.send(agent, { type: "chat", message });
168
- return r.reply;
169
- }, { concurrency: this.maxConcurrent });
170
- }
171
-
172
- // 关闭所有 agent
173
- // v1.0.8: 先发 shutdown 优雅退出, 300ms 后兜底 kill 仍存活进程 (worker 无响应/卡死时不残留)
174
- async shutdownAll() {
175
- for (const [name] of [...this.agents]) {
176
- try { this.send(name, { type: "shutdown" }).catch(() => {}); } catch {}
177
- }
178
- // 等待退出
179
- await new Promise((r) => setTimeout(r, 300));
180
- for (const [name, entry] of [...this.agents]) {
181
- try { entry.proc.kill(); } catch {}
182
- }
183
- }
184
-
185
- list() {
186
- return [...this.agents.keys()].map((n) => ({ name: n, pid: this.agents.get(n).proc.pid }));
187
- }
1
+ // src/orchestrator/legion.js - Agent 军团编排器
2
+ // 管理多个独立 agent 子进程, 支持并行派发任务、按角色分工
3
+ import { spawn } from "node:child_process";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { EventEmitter } from "node:events";
7
+ import { info, error, warn, debug } from "../utils/logger.js";
8
+ import { createLineReader, writeLine } from "../utils/ndjson.js";
9
+ import { runDag } from "./dag.js";
10
+
11
+ const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
12
+ const WORKER = path.join(ROOT, "src", "orchestrator", "agent-worker.js");
13
+
14
+ export class Legion extends EventEmitter {
15
+ constructor({ workerPath = WORKER, nodeBin = process.execPath, maxConcurrent = 8 } = {}) {
16
+ super();
17
+ this.workerPath = workerPath;
18
+ this.nodeBin = nodeBin;
19
+ this.maxConcurrent = maxConcurrent; // 军团整体并发上限: 防止大 DAG/广播瞬间 spawn 海量子进程
20
+ this.agents = new Map(); // { name: { proc, pending: Map<id,{resolve,reject}>, counter } }
21
+ }
22
+
23
+ // 有界并发执行器: 以固定并发度跑一批 async 任务, 超出上限的排队 (背压而非一次性 Promise.all)
24
+ // 这是 broadcast/runDag 层共享的背压口, 避免"并发无数控、大输入一次性打爆子进程数"
25
+ async _mapBounded(items, fn, { concurrency = this.maxConcurrent } = {}) {
26
+ const results = new Array(items.length);
27
+ let idx = 0;
28
+ const workers = Array.from({ length: Math.max(1, Math.min(concurrency, items.length)) });
29
+ await Promise.all(workers.map(async () => {
30
+ while (idx < items.length) {
31
+ const i = idx++;
32
+ results[i] = await fn(items[i], i);
33
+ }
34
+ }));
35
+ return results;
36
+ }
37
+
38
+ // 创建一个 agent 子进程
39
+ spawnAgent(name, { dataDir, globalDataDir, env = {} } = {}) {
40
+ // 未显式指定 dataDir → worker 退回默认 root(项目根) 的 data/, 即把运行数据写进生产目录。
41
+ // 2026-10-03: test/legion.test.js 曾因此污染真实 data/ (实测 139 个测试中唯一一个), 此处显式告警。
42
+ if (!dataDir) warn(`[legion] spawnAgent(${name}) 未指定 dataDir, worker 将写入默认数据目录 (可能是生产 data/)`);
43
+ if (this.agents.has(name)) return this.agents.get(name);
44
+ const env2 = { ...process.env, ...env };
45
+ if (dataDir) env2.PPX_AGENT_DATA_DIR = dataDir;
46
+ // 全局共享目录: 跨 agent 共享经验库 (ANS 全局记忆)
47
+ if (globalDataDir) env2.PPX_AGENT_GLOBAL_DATA_DIR = globalDataDir;
48
+ const proc = spawn(this.nodeBin, [this.workerPath], {
49
+ cwd: ROOT,
50
+ env: env2,
51
+ stdio: ["pipe", "pipe", "inherit"],
52
+ });
53
+ const entry = { proc, pending: new Map(), counter: 0 };
54
+ this.agents.set(name, entry);
55
+
56
+ proc.stdout.setEncoding("utf8");
57
+ const onLine = createLineReader((line) => {
58
+ try {
59
+ const msg = JSON.parse(line);
60
+ // step 中间事件: 触发 onProgress 回调, 不消费 pending (等最终 reply)
61
+ if (msg.type === "step" && msg.id && entry.pending.has(msg.id)) {
62
+ const p = entry.pending.get(msg.id);
63
+ if (p && p.onProgress) { try { p.onProgress(msg); } catch (e) { debug(`[orchestrator/legion] 已忽略异常: ${e && e.message ? e.message : e}`); } }
64
+ return;
65
+ }
66
+ if (msg.id && entry.pending.has(msg.id)) {
67
+ const { resolve, reject } = entry.pending.get(msg.id);
68
+ entry.pending.delete(msg.id);
69
+ if (msg.type === "error") reject(new Error(msg.error));
70
+ else resolve(msg);
71
+ }
72
+ } catch (e) { debug(`[orchestrator/legion] 已忽略异常: ${e && e.message ? e.message : e}`); }
73
+ });
74
+ proc.stdout.on("data", onLine);
75
+ proc.on("error", (err) => {
76
+ // v1.0.8: spawn 失败 (node bin 不存在等) 兜底, 拒绝所有 pending, 防永久挂起
77
+ error(`agent[${name}] 启动失败: ${err.message}`);
78
+ for (const [, { reject }] of entry.pending) reject(new Error(`agent ${name} 启动失败`));
79
+ entry.pending.clear();
80
+ this.agents.delete(name);
81
+ });
82
+ proc.on("exit", (code) => {
83
+ info(`agent[${name}] 退出 code=${code}`);
84
+ // 拒绝所有 pending
85
+ for (const [, { reject }] of entry.pending) reject(new Error(`agent ${name} 已退出`));
86
+ entry.pending.clear();
87
+ this.agents.delete(name);
88
+ this.emit("exit", name, code);
89
+ });
90
+ info(`agent[${name}] 已启动 (pid=${proc.pid})`);
91
+ return entry;
92
+ }
93
+
94
+ // 向某 agent 发消息, 返回 Promise (onProgress 可选: 接收 step 中间事件)
95
+ // v1.0.8: 超时兜底 (默认 30s), worker 卡死/异常不导致 pending 永久挂起
96
+ send(name, msg, { onProgress, timeout = 30000 } = {}) {
97
+ const entry = this.agents.get(name);
98
+ if (!entry || entry.proc.exitCode !== null) {
99
+ return Promise.reject(new Error(`agent ${name} 未运行`));
100
+ }
101
+ const id = ++entry.counter;
102
+ return new Promise((resolve, reject) => {
103
+ const timer = setTimeout(() => {
104
+ entry.pending.delete(id);
105
+ reject(new Error(`agent ${name} 请求超时 (${timeout}ms)`));
106
+ }, timeout);
107
+ entry.pending.set(id, {
108
+ resolve: (v) => { clearTimeout(timer); resolve(v); },
109
+ reject: (e) => { clearTimeout(timer); reject(e); },
110
+ onProgress,
111
+ });
112
+ try {
113
+ writeLine(entry.proc.stdin, { id, ...msg });
114
+ } catch (e) {
115
+ clearTimeout(timer);
116
+ entry.pending.delete(id);
117
+ reject(new Error(`agent ${name} 写入失败: ${e.message}`));
118
+ }
119
+ });
120
+ }
121
+
122
+ // 并行派发: 同一任务广播给多个 agent, 最快返回
123
+ async broadcast(type, message, { timeout = 30000 } = {}) {
124
+ const names = [...this.agents.keys()];
125
+ if (!names.length) throw new Error("军团为空, 先 spawnAgent");
126
+ const results = await this._mapBounded(
127
+ names,
128
+ (n) => this.send(n, { type, message }, { timeout }).catch((e) => ({ type: "error", error: e.message }))
129
+ );
130
+ return names.map((n, i) => ({ agent: n, ...results[i] }));
131
+ }
132
+
133
+ // 按角色分工: 把任务列表分给不同 agent
134
+ // 2026-10-02: 串行 → 有界并行 (_mapBounded 背压, 同 broadcast/runDag 共享 maxConcurrent 口)
135
+ async dispatch(type, tasks) {
136
+ const names = [...this.agents.keys()];
137
+ if (!names.length) throw new Error("军团为空, 先 spawnAgent");
138
+ return this._mapBounded(tasks, (task, i) => {
139
+ const name = names[i % names.length];
140
+ return this.send(name, { type, message: task })
141
+ .then((r) => ({ agent: name, task, ...r }))
142
+ .catch((e) => ({ agent: name, task, type: "error", error: e.message }));
143
+ });
144
+ }
145
+
146
+ // DAG 任务编排: 按依赖拓扑分层执行, 同层并行, 上游结果传入下游 (P3)
147
+ // graph = { nodes: [{ id, task, dependsOn?: [id], agent?: name }] }
148
+ // 返回 { results: {id: reply}, order: [执行顺序] }
149
+ async runDag(graph) {
150
+ const names = [...this.agents.keys()];
151
+ if (!names.length) throw new Error("军团为空, 先 spawnAgent");
152
+ let rr = 0; // round-robin 派发未指定 agent 的节点
153
+ return runDag(graph, async (id, node, deps) => {
154
+ const agent = node.agent || names[rr++ % names.length];
155
+ const depText = Object.entries(deps)
156
+ .map(([k, v]) => `${k}: ${String(v).slice(0, 500)}`)
157
+ .join("\n");
158
+ const message = node.task + (depText ? "\n\n[上游结果]\n" + depText : "");
159
+ const r = await this.send(agent, { type: "chat", message });
160
+ return r.reply;
161
+ }, { concurrency: this.maxConcurrent });
162
+ }
163
+
164
+ // 关闭所有 agent
165
+ // v1.0.8: 先发 shutdown 优雅退出, 300ms 后兜底 kill 仍存活进程 (worker 无响应/卡死时不残留)
166
+ async shutdownAll() {
167
+ for (const [name] of [...this.agents]) {
168
+ try { this.send(name, { type: "shutdown" }).catch(() => {}); } catch (e) { debug(`[orchestrator/legion] 已忽略异常: ${e && e.message ? e.message : e}`); }
169
+ }
170
+ // 等待退出
171
+ await new Promise((r) => setTimeout(r, 300));
172
+ for (const [name, entry] of [...this.agents]) {
173
+ try { entry.proc.kill(); } catch (e) { debug(`[orchestrator/legion] 已忽略异常: ${e && e.message ? e.message : e}`); }
174
+ }
175
+ }
176
+
177
+ list() {
178
+ return [...this.agents.keys()].map((n) => ({ name: n, pid: this.agents.get(n).proc.pid }));
179
+ }
188
180
  }