ppxans-harness 2.4.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 (106) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +265 -0
  3. package/bin/ppx-channels.js +3 -0
  4. package/bin/ppx-serve.js +6 -0
  5. package/bin/ppx.js +3 -0
  6. package/config/identity.md +6 -0
  7. package/config/ishiki.md +16 -0
  8. package/config/ppx.json +151 -0
  9. package/package.json +69 -0
  10. package/src/agent/context.js +179 -0
  11. package/src/agent/index.js +717 -0
  12. package/src/agent/prompts.js +107 -0
  13. package/src/aml-server.js +151 -0
  14. package/src/ans/eviction.js +144 -0
  15. package/src/ans/guard.js +120 -0
  16. package/src/ans/lifecycle.js +93 -0
  17. package/src/ans/proactive.js +129 -0
  18. package/src/ans/reward.js +112 -0
  19. package/src/ans/values.js +15 -0
  20. package/src/audit/audit-chain.js +167 -0
  21. package/src/audit/verifier.js +120 -0
  22. package/src/bus/circuit-breaker.js +115 -0
  23. package/src/bus/runtime-bus.js +94 -0
  24. package/src/channels/base.js +35 -0
  25. package/src/channels/feishu.js +127 -0
  26. package/src/channels/http.js +592 -0
  27. package/src/channels/index.js +110 -0
  28. package/src/channels/log.js +29 -0
  29. package/src/channels/wechat-crypto.js +74 -0
  30. package/src/channels/wechat.js +197 -0
  31. package/src/channels-cli.js +124 -0
  32. package/src/cli.js +120 -0
  33. package/src/config/channels.js +170 -0
  34. package/src/config/index.js +224 -0
  35. package/src/config/providers.js +189 -0
  36. package/src/config/settings.js +182 -0
  37. package/src/core/policy.js +272 -0
  38. package/src/core/trace.js +89 -0
  39. package/src/evolve/playbook.js +194 -0
  40. package/src/llm/client.js +446 -0
  41. package/src/llm/dsml.js +74 -0
  42. package/src/llm/embedder.js +35 -0
  43. package/src/llm/fence.js +105 -0
  44. package/src/llm/index.js +4 -0
  45. package/src/llm/retry.js +73 -0
  46. package/src/llm/router.js +98 -0
  47. package/src/mcp/client.js +375 -0
  48. package/src/mcp/index.js +116 -0
  49. package/src/memory/asset-hub.js +131 -0
  50. package/src/memory/canvas.js +131 -0
  51. package/src/memory/compaction.js +28 -0
  52. package/src/memory/experience.js +122 -0
  53. package/src/memory/fact-store.js +699 -0
  54. package/src/memory/failure-episode.js +99 -0
  55. package/src/memory/fork.js +83 -0
  56. package/src/memory/index.js +7 -0
  57. package/src/memory/l0.js +52 -0
  58. package/src/memory/l2.js +131 -0
  59. package/src/memory/l3.js +112 -0
  60. package/src/memory/memory-ticker.js +240 -0
  61. package/src/memory/session.js +398 -0
  62. package/src/mode/blackboard.js +49 -0
  63. package/src/mode/graph.js +41 -0
  64. package/src/mode/index.js +64 -0
  65. package/src/mode/legion.js +51 -0
  66. package/src/mode/plan-exec.js +50 -0
  67. package/src/mode/router.js +40 -0
  68. package/src/orchestrator/agent-worker.js +70 -0
  69. package/src/orchestrator/dag.js +83 -0
  70. package/src/orchestrator/index.js +2 -0
  71. package/src/orchestrator/legion.js +188 -0
  72. package/src/orchestrator/supervisor.js +177 -0
  73. package/src/persona/index.js +29 -0
  74. package/src/plugin/builtin.js +212 -0
  75. package/src/plugin/context.js +79 -0
  76. package/src/plugin/index.js +62 -0
  77. package/src/seam/registry.js +98 -0
  78. package/src/seam/shell.js +55 -0
  79. package/src/selfheal/evolve.js +68 -0
  80. package/src/selfheal/healer.js +167 -0
  81. package/src/selfheal/run.js +9 -0
  82. package/src/server.js +60 -0
  83. package/src/services/learning-service.js +177 -0
  84. package/src/services/memory-health.js +99 -0
  85. package/src/services/memory-service.js +160 -0
  86. package/src/skills/loader.js +150 -0
  87. package/src/skills/verify.js +100 -0
  88. package/src/tools/advanced.js +353 -0
  89. package/src/tools/builtin.js +298 -0
  90. package/src/tools/catalog.js +159 -0
  91. package/src/tools/command-guard.js +112 -0
  92. package/src/tools/custom.js +47 -0
  93. package/src/tools/delegate.js +297 -0
  94. package/src/tools/document.js +253 -0
  95. package/src/tools/governance.js +260 -0
  96. package/src/tools/index.js +11 -0
  97. package/src/tools/methods.js +178 -0
  98. package/src/tools/ocr.js +59 -0
  99. package/src/tools/seam.js +125 -0
  100. package/src/tools/selfmod.js +176 -0
  101. package/src/utils/logger.js +17 -0
  102. package/src/utils/pii.js +42 -0
  103. package/src/utils/store.js +108 -0
  104. package/src/utils/text.js +16 -0
  105. package/src/utils/trace.js +153 -0
  106. package/src/utils/winutf8.js +15 -0
@@ -0,0 +1,177 @@
1
+ // src/orchestrator/supervisor.js - supervisor 编排模式 (P3⑨)
2
+ // 吸收 LangGraph supervisor 拓扑 + OpenAI Agents SDK handoff 思想 (仅思想, 无源码复制):
3
+ // 监督者(S) 分解任务 → 派发给多个专家子 agent (E1..En) → 收集结果 →
4
+ // S 综合/评审 → 决定: 接受最终答案 / 发现分歧 → 重新派发修正 / 升级人工。
5
+ // 皮皮虾自研实现, 构建在现有 Legion (多进程) + delegate 仲裁之上。
6
+ // 与 delegate.js 的 arbitrate (一次性聚合) 互补: supervisor 是完整编排循环 (可多轮修正)。
7
+
8
+ // 带超时等待 (防子 agent 卡死) — 定时器必须清理
9
+ export function withTimeout(p, ms, label) {
10
+ let timer;
11
+ const timeout = new Promise((_, rej) => {
12
+ timer = setTimeout(() => rej(new Error(`${label}超时 (${ms / 1000}s)`)), ms);
13
+ });
14
+ return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
15
+ }
16
+
17
+ export const SUPERVISOR_DEFAULTS = {
18
+ maxRounds: 3, // 最多几轮修正
19
+ timeoutMs: 120000, // 每轮子 agent 超时
20
+ minConsensus: 0.6, // 一致率低于此值视为分歧 (需重新派发)
21
+ };
22
+
23
+ // ---- 纯函数: 从子结果中识别分歧 ----
24
+ // results: [{agent, reply}] reply 为文本
25
+ // 返回 { consensus: 0~1, clusters: [{key, members, replies}], divergent: boolean }
26
+ // 用词法相似度聚类 (零依赖, 可升级 embedding)
27
+ export function findDisagreement(results, { minConsensus = SUPERVISOR_DEFAULTS.minConsensus } = {}) {
28
+ const list = (results || []).filter((r) => r && typeof r.reply === "string" && r.reply.trim());
29
+ if (list.length < 2) return { consensus: 1, clusters: [{ key: "solo", members: list, replies: list.map((r) => r.reply) }], divergent: false };
30
+
31
+ // 贪心聚类: 相似度 > 0.4 归一组
32
+ const clusters = [];
33
+ const assigned = new Set();
34
+ for (let i = 0; i < list.length; i++) {
35
+ if (assigned.has(i)) continue;
36
+ const members = [list[i]];
37
+ assigned.add(i);
38
+ for (let j = i + 1; j < list.length; j++) {
39
+ if (assigned.has(j)) continue;
40
+ if (lexSim(list[i].reply, list[j].reply) > 0.4) {
41
+ members.push(list[j]);
42
+ assigned.add(j);
43
+ }
44
+ }
45
+ clusters.push({ key: "c" + clusters.length, members, replies: members.map((m) => m.reply) });
46
+ }
47
+ const largest = Math.max(...clusters.map((c) => c.members.length));
48
+ const consensus = largest / list.length;
49
+ return { consensus, clusters, divergent: consensus < minConsensus };
50
+ }
51
+
52
+ function lexSim(a, b) {
53
+ // 中文连续串用字符 bigram (词级 Jaccard 对中文短句失效), 与 playbook.js 同策略
54
+ const ta = String(a || "");
55
+ const tb = String(b || "");
56
+ if (!ta || !tb) return 0;
57
+ const tk = (s) => {
58
+ const words = s.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
59
+ if (words.length > 1) return words;
60
+ const chars = s.replace(/\s+/g, "").toLowerCase();
61
+ const out = [];
62
+ for (let i = 0; i < chars.length - 1; i++) out.push(chars.slice(i, i + 2));
63
+ return out.length ? out : chars.split("");
64
+ };
65
+ const A = new Set(tk(ta));
66
+ const B = new Set(tk(tb));
67
+ if (!A.size || !B.size) return 0;
68
+ let inter = 0;
69
+ for (const w of A) if (B.has(w)) inter++;
70
+ return (2 * inter) / (A.size + B.size);
71
+ }
72
+
73
+ // ---- 纯函数: 组装监督者修正提示词 (把分歧/缺陷反馈给子 agent) ----
74
+ export function buildRevisionPrompt(task, feedback) {
75
+ const lines = (feedback || []).map((f, i) => `${i + 1}. ${f}`).join("\n");
76
+ return `${task}\n\n【监督者反馈】以下问题需修正:\n${lines}\n\n请针对反馈逐条修正, 只解决反馈问题, 不要引入新内容。`;
77
+ }
78
+
79
+ // ---- 监督者循环 ----
80
+ // opts: { legion, agents: [name...], task, judge, maxRounds, timeoutMs, minConsensus, onRound }
81
+ // 返回 { answer, rounds, consensus, divergent, history: [{round, results, feedback}] }
82
+ export async function runSupervisor({ legion, agents = [], task = "", judge = "", maxRounds = SUPERVISOR_DEFAULTS.maxRounds, timeoutMs = SUPERVISOR_DEFAULTS.timeoutMs, minConsensus = SUPERVISOR_DEFAULTS.minConsensus, onRound = null, llm = null, finalize = null } = {}) {
83
+ const history = [];
84
+ let currentTask = task;
85
+ let answer = "";
86
+ let rounds = 0;
87
+
88
+ for (let r = 0; r < maxRounds; r++) {
89
+ rounds = r + 1;
90
+ // 派发给所有专家
91
+ const results = [];
92
+ for (const name of agents) {
93
+ try {
94
+ const resp = await withTimeout(
95
+ legion.send(name, { type: "chat", message: currentTask }, { timeout: timeoutMs + 5000 }),
96
+ timeoutMs,
97
+ `监督者派发→${name}`
98
+ );
99
+ results.push({ agent: name, reply: String(resp?.reply || "").trim() || "(无回复)" });
100
+ } catch (e) {
101
+ results.push({ agent: name, reply: `[失败] ${e.message}` });
102
+ }
103
+ }
104
+
105
+ // 分歧检测
106
+ const { consensus, divergent, clusters } = findDisagreement(results, { minConsensus });
107
+ const roundRec = { round: r + 1, results, consensus, divergent, feedback: [] };
108
+
109
+ // 监督者评审: 有 LLM 且有多结果时, 让监督者决定是否接受或给反馈
110
+ let accept = !divergent;
111
+ let feedback = [];
112
+ if (llm && results.length >= 2) {
113
+ const verdict = await judgeRound(llm, task, results, judge);
114
+ accept = verdict.accept;
115
+ feedback = verdict.feedback;
116
+ } else if (divergent) {
117
+ feedback = [`各方结果存在分歧 (一致率 ${(consensus * 100).toFixed(0)}%), 请重新给出更一致的结论。`];
118
+ }
119
+ roundRec.feedback = feedback;
120
+ history.push(roundRec);
121
+ onRound?.(roundRec);
122
+
123
+ if (accept) {
124
+ // 监督者 (或 finalize) 产出最终答案
125
+ if (llm) {
126
+ const final = await finalizeRound(llm, task, results, judge);
127
+ answer = final;
128
+ } else {
129
+ answer = results[0]?.reply || "";
130
+ }
131
+ return { answer, rounds, consensus, divergent: false, history };
132
+ }
133
+ // 不通过: 带反馈重新派发
134
+ currentTask = buildRevisionPrompt(task, feedback.length ? feedback : ["请给出更精确、一致的答案。"]);
135
+ }
136
+ // 达到轮数上限: 返回最近一轮结果 + 分歧标记
137
+ const last = history[history.length - 1];
138
+ answer = last.results.map((r) => `【${r.agent}】${r.reply}`).join("\n\n");
139
+ return { answer, rounds, consensus: last.consensus, divergent: true, history };
140
+ }
141
+
142
+ // 监督者评审: 综合各结果, 决定接受/打回 + 反馈
143
+ async function judgeRound(llm, task, results, judge) {
144
+ const input = results.map((r) => `【${r.agent}】\n${String(r.reply).slice(0, 1500)}`).join("\n\n");
145
+ try {
146
+ const r = await llm.chat([
147
+ { role: "system", content: "你是监督者。评估各专家子 agent 的结果是否满足任务要求。输出 JSON: {\"accept\": true/false, \"feedback\": [\"问题1\", \"问题2\"]}。accept=true 时 feedback 为空数组。只输出 JSON。" },
148
+ { role: "user", content: `【任务】${String(task).slice(0, 1000)}\n${judge ? `【评审要求】${judge}\n` : ""}【各方结果】\n${input.slice(0, 5000)}` },
149
+ ]);
150
+ const text = String(r?.content || "").replace(/```(?:json)?\s*/g, "").replace(/```/g, "").trim();
151
+ const m = text.match(/\{[\s\S]*\}/);
152
+ if (!m) return { accept: false, feedback: ["监督者输出格式错误, 请重新回答"] };
153
+ const parsed = JSON.parse(m[0]);
154
+ return {
155
+ accept: parsed.accept === true,
156
+ feedback: Array.isArray(parsed.feedback) ? parsed.feedback.slice(0, 5).map(String) : [],
157
+ };
158
+ } catch {
159
+ return { accept: false, feedback: ["监督者评审失败, 请重新回答"] };
160
+ }
161
+ }
162
+
163
+ // 监督者定稿: 综合各结果给最终答案
164
+ async function finalizeRound(llm, task, results, judge) {
165
+ const input = results.map((r) => `【${r.agent}】\n${String(r.reply).slice(0, 2000)}`).join("\n\n");
166
+ try {
167
+ const r = await llm.chat([
168
+ { role: "system", content: "你是监督者。综合各专家结果, 给出最终整合答案。直接输出最终答案, 不要复述过程。" },
169
+ { role: "user", content: `【任务】${String(task).slice(0, 1000)}\n${judge ? `【评审要求】${judge}\n` : ""}【各方结果】\n${input.slice(0, 6000)}` },
170
+ ]);
171
+ return String(r?.content || "").trim() || "(监督者无输出)";
172
+ } catch {
173
+ return results.map((r) => `【${r.agent}】${r.reply}`).join("\n\n");
174
+ }
175
+ }
176
+
177
+ export default { runSupervisor, findDisagreement, buildRevisionPrompt };
@@ -0,0 +1,29 @@
1
+ // src/persona/index.js - 人格系统
2
+ // 从 identity.md + ishiki.md 组装人格 prompt (参考 openhanako)
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { readText } from "../utils/store.js";
6
+
7
+ export class Persona {
8
+ constructor(rootDir) {
9
+ this.configDir = path.join(rootDir, "config");
10
+ this.identity = readText(path.join(this.configDir, "identity.md"), "皮皮虾");
11
+ this.ishiki = readText(path.join(this.configDir, "ishiki.md"), "- 直接、务实");
12
+ }
13
+
14
+ // 组装 system prompt
15
+ systemPrompt(userName = "兄弟") {
16
+ return `${this.identity}
17
+
18
+ ## 人格定义
19
+ ${this.ishiki}
20
+
21
+ ## 你面对的用户
22
+ ${userName}`;
23
+ }
24
+
25
+ reload() {
26
+ this.identity = readText(path.join(this.configDir, "identity.md"), "皮皮虾");
27
+ this.ishiki = readText(path.join(this.configDir, "ishiki.md"), "- 直接、务实");
28
+ }
29
+ }
@@ -0,0 +1,212 @@
1
+ // src/plugin/builtin.js - 皮皮虾内置插件集 (一切皆插件)
2
+ // 借鉴 deepseek-harness 的 "everything is a plugin": 每个模块是一个插件, 通过 ctx.provide 注册服务。
3
+ // 装配顺序即依赖顺序 (依赖在前), 任何插件都可被用户插件替换或扩展。
4
+ import path from "node:path";
5
+ import { Healer } from "../selfheal/healer.js";
6
+ import { Persona } from "../persona/index.js";
7
+ import { FactStore, MemoryTicker, Experience, L0Recorder, SceneStore, PersonaStore } from "../memory/index.js";
8
+ import { SessionStore } from "../memory/session.js";
9
+ import {
10
+ ToolCatalog, registerBuiltinTools, registerAdvancedTools, Scheduler,
11
+ registerMethodTools, registerSelfmodTools, registerCustomTools, registerDocumentTools,
12
+ registerGovernanceTools,
13
+ } from "../tools/index.js";
14
+ import { embedderFromConfig } from "../llm/embedder.js";
15
+ import { LocalShellProvider } from "../seam/shell.js";
16
+ import { registerDelegateTools } from "../tools/delegate.js";
17
+ import { Traces } from "../utils/trace.js";
18
+ import { AuditLog } from "../audit/audit-chain.js";
19
+ import { RuntimeBus } from "../bus/runtime-bus.js";
20
+ import { PlaybookStore } from "../evolve/playbook.js";
21
+ import { MemoryHealthMonitor } from "../services/memory-health.js";
22
+ import { FailureEpisodeStore } from "../memory/failure-episode.js";
23
+ import { CanvasStore } from "../memory/canvas.js";
24
+ import { AssetHub } from "../memory/asset-hub.js";
25
+ import { exportMemorySnapshot, mergeSnapshotBack, hasSnapshot } from "../memory/fork.js";
26
+
27
+ // fork 工具 (供 ctx.consume("fork") 取用)
28
+ const forkTools = { exportMemorySnapshot, mergeSnapshotBack, hasSnapshot };
29
+ export { isUsableProvider, resolveLLM, resolveAllLLMs } from "../llm/router.js";
30
+ import { ModeRegistry, registerDefaultModes } from "../mode/index.js";
31
+ import { planExecExecutor } from "../mode/plan-exec.js";
32
+ import { routerExecutor } from "../mode/router.js";
33
+ import { blackboardExecutor } from "../mode/blackboard.js";
34
+ import { graphExecutor } from "../mode/graph.js";
35
+ import { legionExecutor } from "../mode/legion.js";
36
+
37
+ // ---- LLM 路由 (唯一真相源已迁至 src/llm/router.js, 此文件仅 re-export 向后兼容) ----
38
+ // router.js 负责: 占位死配置过滤 / 云端真key优先 / 本地零配置兜底 / 健康排序 / PPX_PROVIDER 强制
39
+ // 此处保留导出名, agent/热重载/插件继续用 builtin.resolveLLM 等旧引用。
40
+
41
+ // ---- 内置插件: 每个 (ctx) => void, 用 ctx.provide 注册服务 ----
42
+
43
+
44
+ export const busPlugin = (ctx) => {
45
+ // ②循环系: 全局 Runtime 总线。必须最先装配, 其他插件可 ctx.consume("bus") 挂订阅/注册命令。
46
+ const bus = new RuntimeBus();
47
+ ctx.provide("bus", bus);
48
+ // 状态槽: sessionKey 归属 (给观测/审计看当前活跃会话)
49
+ bus.set("bootedAt", Date.now());
50
+ return bus;
51
+ };
52
+
53
+ export const healerPlugin = (ctx) => {
54
+ const healer = new Healer(ctx.consume("root"));
55
+ healer.markDirty();
56
+ const health = healer.heal();
57
+ ctx.provide("healer", healer);
58
+ ctx.provide("health", health);
59
+ };
60
+
61
+ export const personaPlugin = (ctx) => {
62
+ ctx.provide("persona", new Persona(ctx.consume("root")));
63
+ };
64
+
65
+ export const factsPlugin = (ctx) => {
66
+ const config = ctx.consume("config");
67
+ ctx.provide("facts", new FactStore(ctx.consume("dataDir"), config.memory || {}));
68
+ };
69
+
70
+ export const experiencePlugin = (ctx) => {
71
+ // 经验库走全局共享目录 (ANS 全局记忆): 跨 agent 共享经验, 写入用文件锁防并发覆盖
72
+ ctx.provide("experience", new Experience(ctx.consume("globalDataDir") || ctx.consume("dataDir")));
73
+ };
74
+
75
+ export const sessionPlugin = (ctx) => {
76
+ const config = ctx.consume("config");
77
+ const sessions = new SessionStore(ctx.consume("dataDir"));
78
+ // 启动时清理过期会话 (config.memory.session_max_age_days, 默认 30; 0/负=不清理)
79
+ const maxAge = Number(config.memory?.session_max_age_days ?? 30);
80
+ if (maxAge > 0) {
81
+ const removed = sessions.pruneOld({ maxAgeDays: maxAge });
82
+ if (removed.length) info(`[sessions] 清理过期会话 ${removed.length} 个: ${removed.join(", ")}`);
83
+ }
84
+ ctx.provide("sessions", sessions);
85
+ };
86
+
87
+ export const memoryPlugin = (ctx) => {
88
+ const facts = ctx.consume("facts");
89
+ const sessions = ctx.consume("sessions");
90
+ ctx.provide("memory", new MemoryTicker(ctx.consume("dataDir"), facts, null, sessions));
91
+ };
92
+
93
+ export const llmPlugin = (ctx) => {
94
+ const config = ctx.consume("config");
95
+ ctx.provide("llm", resolveLLM(config));
96
+ ctx.provide("allProviders", resolveAllLLMs(config));
97
+ };
98
+
99
+ export const memoryLayersPlugin = (ctx) => {
100
+ const sessions = ctx.consume("sessions");
101
+ const dataDir = ctx.consume("dataDir");
102
+ const userName = ctx.consume("userName") || "兄弟";
103
+ ctx.provide("l0", new L0Recorder(sessions, dataDir));
104
+ ctx.provide("scenes", new SceneStore(dataDir));
105
+ ctx.provide("personaStore", new PersonaStore(dataDir, { userName }));
106
+ };
107
+
108
+ export const tracesPlugin = (ctx) => {
109
+ ctx.provide("traces", new Traces(ctx.consume("dataDir")));
110
+ };
111
+
112
+ export const auditPlugin = (ctx) => {
113
+ // 审计哈希链 (吸收自 ppx-v2): append-only + SHA-256 链式防篡改账本。
114
+ // 与 core/trace.js 的事件流互补 —— trace 面向"可观测", audit 面向"可追责/防篡改"。
115
+ // config.audit.enabled === false 时关闭 (供性能敏感场景), 默认开启。
116
+ const config = ctx.consume("config");
117
+ if (config?.audit?.enabled === false) {
118
+ ctx.provide("audit", null);
119
+ return null;
120
+ }
121
+ const audit = new AuditLog(ctx.consume("dataDir"));
122
+ ctx.provide("audit", audit);
123
+ return audit;
124
+ };
125
+
126
+ export const toolsPlugin = (ctx) => {
127
+ const root = ctx.consume("root");
128
+ const dataDir = ctx.consume("dataDir");
129
+ const config = ctx.consume("config");
130
+ const facts = ctx.consume("facts");
131
+ const memory = ctx.consume("memory");
132
+ const tools = new ToolCatalog();
133
+ registerBuiltinTools(tools, { rootDir: root, facts, memory });
134
+ const scheduler = new Scheduler(dataDir);
135
+ ctx.provide("scheduler", scheduler);
136
+ registerAdvancedTools(tools, { dataDir, scheduler, onMemoryNote: (note) => facts.add(note, { source: "schedule" }) });
137
+ registerMethodTools(tools);
138
+ registerSelfmodTools(tools, { skillsDir: path.join(root, "skills") });
139
+ // 用户自定义工具 (不改源码扩展能力)
140
+ const customDir = path.join(root, (config.tools && config.tools.custom_dir) || "custom-tools");
141
+ registerCustomTools(tools, customDir);
142
+ // 文档加载器 (RAG: read_document / ingest_document)
143
+ registerDocumentTools(tools, { rootDir: root });
144
+ // 向量化: 配了 config.embedding 则自动注入 embedder, 检索切 dense+BM25 RRF; 否则纯 BM25 兜底
145
+ const embedder = embedderFromConfig(config);
146
+ if (embedder) facts.setEmbedder(embedder);
147
+ // 多 agent 自主协作: spawn_agent 工具 (agent 自主派生子 agent 分工)
148
+ registerDelegateTools(tools, {});
149
+ // Shell 能力 seam: 命令执行解耦为可替换 provider (本地/未来沙箱/Docker)
150
+ ctx.provide("shell", new LocalShellProvider());
151
+ // 审计哈希链接入工具执行收口 (未启用时为 null, catalog 内部零开销跳过)
152
+ tools.setAudit(ctx.consume("audit"));
153
+ // 记忆治理 + 审计校验 + 运维工具 (吸收自 ppx-v2, 共 10 个)
154
+ registerGovernanceTools(tools, {
155
+ rootDir: root,
156
+ dataDir,
157
+ facts,
158
+ audit: ctx.consume("audit"),
159
+ personaStore: ctx.consume("personaStore"),
160
+ healer: ctx.consume("healer"),
161
+ experience: ctx.consume("experience"),
162
+ });
163
+ ctx.provide("tools", tools);
164
+ ctx.provide("toolsEnabled", config.tools?.enabled !== false);
165
+ };
166
+
167
+ export const modePlugin = (ctx) => {
168
+ const registry = new ModeRegistry();
169
+ registerDefaultModes(registry);
170
+ // 更多编排模式 (可插拔): plan-exec / router / blackboard / graph
171
+ registry.register("plan-exec", planExecExecutor);
172
+ registry.register("router", routerExecutor);
173
+ registry.register("blackboard", blackboardExecutor);
174
+ registry.register("graph", graphExecutor);
175
+ registry.register("legion", legionExecutor);
176
+ ctx.provide("modes", registry);
177
+ };
178
+
179
+ // P2⑧: 内置插件权限位 (函数外赋值, compose 调用前即可读)
180
+ // tools/shell 属敏感服务 (SENSITIVE_SERVICES), 需 full-access
181
+ toolsPlugin.access = "full-access";
182
+
183
+ export const evolvePlugin = (ctx) => {
184
+ // P1④⑤⑥: 进化系插件 —— Playbook 引擎 / 记忆健康监控 / 故障记忆
185
+ // P2⑥⑦: 符号画布 / 会话 fork 基线
186
+ // 提供服务, 消费方按需取用; 未接线时零开销 (不强制)。
187
+ const dataDir = ctx.consume("dataDir");
188
+ ctx.provide("playbook", new PlaybookStore(dataDir));
189
+ ctx.provide("memoryHealth", new MemoryHealthMonitor());
190
+ ctx.provide("failures", new FailureEpisodeStore(dataDir));
191
+ ctx.provide("canvas", new CanvasStore(dataDir));
192
+ ctx.provide("fork", forkTools);
193
+ ctx.provide("assets", new AssetHub(dataDir)); // P3⑩: 记忆资产中枢
194
+ };
195
+
196
+ // 默认内置插件装配顺序 (依赖在前)
197
+ export const builtinPlugins = [
198
+ busPlugin, // ②循环系: 全局总线必须最先 (依赖在前)
199
+ healerPlugin,
200
+ personaPlugin,
201
+ factsPlugin,
202
+ experiencePlugin,
203
+ sessionPlugin,
204
+ memoryPlugin,
205
+ llmPlugin,
206
+ memoryLayersPlugin,
207
+ tracesPlugin,
208
+ auditPlugin, // 审计哈希链 (toolsPlugin 依赖它注入 catalog, 必须在前)
209
+ toolsPlugin,
210
+ evolvePlugin, // P1: playbook / memoryHealth / failures (tools 之后, 依赖 dataDir)
211
+ modePlugin,
212
+ ];
@@ -0,0 +1,79 @@
1
+ // src/plugin/context.js - 轻量服务注册表 (零依赖插件系统核心)
2
+ // 借鉴 deepseek-harness 的 Cordis "everything is a plugin" 理念, 但零依赖、同步、极简:
3
+ // - 插件 = (ctx) => void 函数, 通过 ctx.provide(key, value) 注册服务, ctx.consume(key) 消费服务
4
+ // - 注册是可逆效果: ctx.onDispose(fn) 挂卸载钩子, ctx.dispose() 逆序执行
5
+ // - 支持父子 context: 子 context 的 consume 会向父查找 (用于 agent 内 scope 隔离)
6
+ // P2⑧ (2026-09-15): 插件两级权限 (吸收 HanaAgent restricted/full-access 思想) ——
7
+ // 敏感服务 (路由/生命周期/页面/工具链) 只允许 full-access 插件注册; restricted 插件
8
+ // 只能注册普通服务。compose 时校验, 违规注册直接拒绝 (代码强制, 不靠约定)。
9
+ export const PLUGIN_ACCESS = { RESTRICTED: "restricted", FULL: "full-access" };
10
+
11
+ // 敏感服务: 仅 full-access 插件可注册 (注册到这些 key 会触发权限校验)
12
+ const SENSITIVE_SERVICES = new Set([
13
+ "routes", // HTTP 路由
14
+ "lifecycle", // 生命周期钩子
15
+ "tools", // 工具链
16
+ "shell", // 命令执行
17
+ "pages", // 页面
18
+ "providers", // LLM provider
19
+ "extensions", // 扩展
20
+ ]);
21
+
22
+ export class Context {
23
+ constructor(parent = null, { access = PLUGIN_ACCESS.RESTRICTED } = {}) {
24
+ this.parent = parent;
25
+ this.access = access; // P2⑧: 当前插件权限位
26
+ this._services = new Map(); // key -> value
27
+ this._disposers = []; // 可逆效果 (dispose 时逆序执行)
28
+ }
29
+
30
+ // P2⑧: 切换权限位 —— 返回共享同一存储的包装 (原型继承), 仅 access 不同。
31
+ // 这样 restricted 插件注册的普通服务, 顶层 ctx.consume 也能查到 (服务仍全局可见),
32
+ // 但敏感 key 注册会被拒绝。
33
+ withAccess(access) {
34
+ const wrapper = Object.create(this);
35
+ wrapper.access = access;
36
+ return wrapper;
37
+ }
38
+
39
+ // 注册服务 (返回 value 便于链式)
40
+ provide(key, value) {
41
+ // P2⑧: 敏感服务权限校验 —— restricted 插件注册敏感 key 被拒
42
+ if (SENSITIVE_SERVICES.has(key) && this.access !== PLUGIN_ACCESS.FULL) {
43
+ throw new Error(`[插件权限] 注册 ${key} 需要 full-access, 当前 ${this.access}`);
44
+ }
45
+ this._services.set(key, value);
46
+ return value;
47
+ }
48
+
49
+ // 消费服务: 先查自己, 再向上查父 context
50
+ consume(key) {
51
+ if (this._services.has(key)) return this._services.get(key);
52
+ if (this.parent) return this.parent.consume(key);
53
+ return undefined;
54
+ }
55
+
56
+ has(key) {
57
+ if (this._services.has(key)) return true;
58
+ if (this.parent) return this.parent.has(key);
59
+ return false;
60
+ }
61
+
62
+ // 注册可逆效果 (卸载时执行), 返回取消函数
63
+ onDispose(fn) {
64
+ this._disposers.push(fn);
65
+ return () => {
66
+ const i = this._disposers.indexOf(fn);
67
+ if (i >= 0) this._disposers.splice(i, 1);
68
+ };
69
+ }
70
+
71
+ // 卸载: 逆序执行所有 disposer
72
+ async dispose() {
73
+ for (const fn of [...this._disposers].reverse()) {
74
+ try { await fn(); } catch {}
75
+ }
76
+ this._disposers = [];
77
+ this._services.clear();
78
+ }
79
+ }
@@ -0,0 +1,62 @@
1
+ // src/plugin/index.js - 插件装配器
2
+ // compose(ctx, plugins): 按顺序装配插件 (同步), 返回 ctx 便于链式
3
+ // 插件 = (ctx) => void, 在函数内用 ctx.provide 注册服务、ctx.consume 消费依赖、ctx.onDispose 挂卸载钩子
4
+ // P2⑧: 插件可声明权限 (plugin.access = 'full-access'), 声明方式: 插件函数挂 access 属性 或 导出对象带 access。
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { createRequire } from "node:module";
8
+ import { info, warn } from "../utils/logger.js";
9
+ import { Context, PLUGIN_ACCESS } from "./context.js";
10
+
11
+ export { Context } from "./context.js";
12
+
13
+ // 解析插件权限: 函数自身属性 > 导出对象 access 字段; 默认 restricted
14
+ export function pluginAccess(plugin) {
15
+ if (typeof plugin === "function" && plugin.access) return plugin.access;
16
+ if (plugin && plugin.access) return plugin.access;
17
+ return PLUGIN_ACCESS.RESTRICTED;
18
+ }
19
+
20
+ // 装配插件树: 顺序执行每个插件的 setup
21
+ // v1.0.9: 单个插件 setup 抛错不中断整条装配链 (隔离失败, 其余插件照常)
22
+ // P2⑧: 每个插件在自己的权限 context 里运行 (敏感服务注册受控)。
23
+ // full-access 插件直接用父 ctx (服务注册到顶层, agent.consume 可见);
24
+ // restricted 插件用 withAccess 子 context (注册到子, 父链向上可查, 但敏感 key 被拒)。
25
+ export function compose(ctx, plugins = []) {
26
+ for (const p of plugins) {
27
+ try {
28
+ const access = pluginAccess(p);
29
+ const pCtx = access === PLUGIN_ACCESS.FULL ? ctx : ctx.withAccess(access);
30
+ if (typeof p === "function") p(pCtx);
31
+ else if (p && typeof p.setup === "function") p.setup(pCtx);
32
+ } catch (e) {
33
+ warn(`[plugins] setup 失败已隔离: ${(e && e.message) || e}`);
34
+ }
35
+ }
36
+ return ctx;
37
+ }
38
+
39
+ // 扫描 plugins/ 目录加载用户插件 (声明式, 不改源码扩展 agent)
40
+ // 每个 .cjs 导出插件函数 (ctx) => void 或 { setup(ctx) }
41
+ export function loadPlugins(pluginsDir) {
42
+ if (!pluginsDir || !fs.existsSync(pluginsDir)) return [];
43
+ const require = createRequire(import.meta.url);
44
+ let files = [];
45
+ try { files = fs.readdirSync(pluginsDir); } catch { return []; }
46
+ const candidates = files.filter((f) => f.endsWith(".cjs") || f.endsWith(".js")).sort();
47
+ const plugins = [];
48
+ for (const f of candidates) {
49
+ const full = path.join(pluginsDir, f);
50
+ try {
51
+ const mod = require(full);
52
+ const plugin = mod && mod.default ? mod.default : mod;
53
+ if (typeof plugin === "function") plugins.push(plugin);
54
+ else if (plugin && typeof plugin.setup === "function") plugins.push(plugin);
55
+ else warn(`[plugins] ${f} 需导出插件函数 (ctx) => void, 跳过`);
56
+ } catch (e) {
57
+ warn(`[plugins] 加载 ${f} 失败: ${e.message}`);
58
+ }
59
+ }
60
+ if (plugins.length) info(`[plugins] 已加载 ${plugins.length} 个自定义插件`);
61
+ return plugins;
62
+ }
@@ -0,0 +1,98 @@
1
+ // src/seam/registry.js - 通用能力缝注册表 (P0③ seam 骨架)
2
+ // 吸收 deepseek-harness 的 "Service Definition / Service Provider / Consumer" 三分法 + dsh 的 "一个 provider 换整个产品"。
3
+ // 与 src/tools/seam.js (工具级能力缝) 互补: 这里管"服务级可替换能力" (shell/fs/embedder/...),
4
+ // 工具级管"单个工具的 Definition/Provider/Consumer"。
5
+ // 零依赖, 纯 Node 原生。用法:
6
+ // const reg = createSeamRegistry();
7
+ // reg.define("shell", { def: "命令执行接口", impl: LocalShellProvider, consumers: ["run_command", "code_act"] });
8
+ // reg.provide("shell", new LocalShellProvider()); // 注册实现
9
+ // reg.resolve("shell"); // 取实现 (消费方)
10
+ // reg.swap("shell", new SandboxShellProvider()); // 一行换实现, 消费方跟着切
11
+ // reg.status(); // 可观测: 哪些 seam 有实现/缺实现
12
+ export class SeamRegistry {
13
+ constructor() {
14
+ this.seams = new Map(); // key -> { def, implClass, impl, consumers: [], swapped: false }
15
+ }
16
+
17
+ // ---- Service Definition 声明 ----
18
+ // def: { desc, interface: [方法名], consumers: [消费方标识] } 或字符串描述
19
+ define(key, def = {}) {
20
+ if (!key || typeof key !== "string") throw new Error("seam.define: 需 key");
21
+ const existing = this.seams.get(key);
22
+ if (existing) {
23
+ // 幂等: 已存在则合并元数据, 不覆盖已装实现
24
+ if (def.consumers) existing.consumers = [...new Set([...existing.consumers, ...def.consumers])];
25
+ if (def.desc) existing.desc = def.desc;
26
+ return existing;
27
+ }
28
+ const seam = {
29
+ key,
30
+ desc: def.desc || "",
31
+ interface: Array.isArray(def.interface) ? def.interface : [],
32
+ consumers: Array.isArray(def.consumers) ? def.consumers : [],
33
+ impl: null,
34
+ implName: null,
35
+ swapped: false,
36
+ };
37
+ this.seams.set(key, seam);
38
+ return seam;
39
+ }
40
+
41
+ // ---- Service Provider 提供 ----
42
+ // impl: 实现实例 (或工厂函数返回实例)
43
+ provide(key, impl, { name = "" } = {}) {
44
+ if (!this.seams.has(key)) this.define(key);
45
+ const seam = this.seams.get(key);
46
+ seam.impl = typeof impl === "function" ? impl() : impl;
47
+ seam.implName = name || (seam.impl && seam.impl.constructor ? seam.impl.constructor.name : "anonymous");
48
+ return seam;
49
+ }
50
+
51
+ // ---- Consumer 消费 ----
52
+ resolve(key) {
53
+ const seam = this.seams.get(key);
54
+ return seam && seam.impl ? seam.impl : null;
55
+ }
56
+
57
+ // 解析 + 缺失报错 (消费方友好)
58
+ require(key) {
59
+ const impl = this.resolve(key);
60
+ if (!impl) throw new Error(`seam 缺失实现: ${key} (已声明, 未 provide)`);
61
+ return impl;
62
+ }
63
+
64
+ // ---- 热替换: 一行换实现, 消费方跟着切 ----
65
+ swap(key, impl, { name = "" } = {}) {
66
+ const seam = this.define(key);
67
+ seam.impl = typeof impl === "function" ? impl() : impl;
68
+ seam.implName = name || (seam.impl && seam.impl.constructor ? seam.impl.constructor.name : "anonymous");
69
+ seam.swapped = true;
70
+ seam.swappedAt = Date.now();
71
+ return seam;
72
+ }
73
+
74
+ // ---- 可观测 ----
75
+ status() {
76
+ const out = [];
77
+ for (const seam of this.seams.values()) {
78
+ out.push({
79
+ key: seam.key,
80
+ desc: seam.desc,
81
+ impl: seam.implName,
82
+ ready: !!seam.impl,
83
+ swapped: seam.swapped,
84
+ consumers: [...seam.consumers],
85
+ });
86
+ }
87
+ return out;
88
+ }
89
+
90
+ has(key) { return this.seams.has(key) && !!this.seams.get(key).impl; }
91
+ keys() { return [...this.seams.keys()]; }
92
+ }
93
+
94
+ export function createSeamRegistry() {
95
+ return new SeamRegistry();
96
+ }
97
+
98
+ export default SeamRegistry;