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,131 @@
1
+ // src/memory/canvas.js - 符号画布记忆 (P2⑥)
2
+ // 吸收 TencentDB-Agent-Memory 的"符号化记忆"设计思想:
3
+ // 长任务上下文只放轻量 Mermaid 状态图 (节点=步骤, 边=状态转移, node_id=事件 seq),
4
+ // 细节按 node_id 从事件日志取 —— 上层存结构, 下层存证据, 完整 drill-down 路径。
5
+ // 皮皮虾自研实现: 从 trace.js 的事件流 (turn/step 边界事件) 归纳画布, 非 LLM、纯代码、可测。
6
+ // 触发条件: steps >= minSteps (默认 8) 或事件数超阈值时才开启, 防过度设计。
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { ensureDir, writeText, logicalDay } from "../utils/store.js";
10
+
11
+ export const CANVAS_DEFAULTS = { minSteps: 8, maxNodes: 30, maxEdges: 40 };
12
+
13
+ // ---- 从 trace 事件行归纳状态图 (纯函数) ----
14
+ // lines: trace.js 事件原始行 (已 JSON.parse 的数组)
15
+ // 返回 { nodes: [{id,label,kind}], edges: [{from,to,label}], startedAt, endedAt, steps }
16
+ export function buildCanvasFromEvents(events = [], opts = {}) {
17
+ const { maxNodes = CANVAS_DEFAULTS.maxNodes, maxEdges = CANVAS_DEFAULTS.maxEdges } = opts;
18
+ const nodes = [];
19
+ const edges = [];
20
+ const nodeById = new Map();
21
+ let steps = 0;
22
+ let startedAt = null;
23
+ let endedAt = null;
24
+ let lastNodeId = null;
25
+
26
+ const ensureNode = (id, label, kind) => {
27
+ if (nodeById.has(id)) return nodeById.get(id);
28
+ const n = { id, label: String(label || id).slice(0, 60), kind: kind || "step" };
29
+ nodes.push(n);
30
+ nodeById.set(id, n);
31
+ return n;
32
+ };
33
+
34
+ const addEdge = (from, to, label) => {
35
+ if (edges.length >= maxEdges) return;
36
+ edges.push({ from, to, label: String(label || "").slice(0, 40) });
37
+ };
38
+
39
+ for (const ev of events) {
40
+ if (!ev || !ev.evt) continue;
41
+ const seq = ev.seq != null ? "evt-" + ev.seq : null;
42
+ if (ev.evt === "turn/start") {
43
+ startedAt = ev.ts || startedAt;
44
+ const n = ensureNode(seq || "turn-start", "任务开始", "boundary");
45
+ lastNodeId = n.id;
46
+ } else if (ev.evt === "turn/end") {
47
+ endedAt = ev.ts || endedAt || new Date().toISOString();
48
+ const n = ensureNode(seq || "turn-end", ev.ok ? "任务完成" : "任务中断", "boundary");
49
+ if (lastNodeId) addEdge(lastNodeId, n.id, "");
50
+ lastNodeId = n.id;
51
+ } else if (ev.evt === "step/start") {
52
+ steps++;
53
+ const n = ensureNode(seq || "step-" + steps, `步骤 ${ev.round != null ? ev.round : steps}`, "step");
54
+ if (lastNodeId && lastNodeId !== n.id) addEdge(lastNodeId, n.id, "");
55
+ lastNodeId = n.id;
56
+ } else if (ev.evt === "step/end") {
57
+ const n = ensureNode(seq || "step-end-" + steps, `步骤结束 (${ev.ok ? "成功" : "失败"})`, ev.ok ? "ok" : "fail");
58
+ if (lastNodeId && lastNodeId !== n.id) addEdge(lastNodeId, n.id, ev.tool || "");
59
+ lastNodeId = n.id;
60
+ } else if (ev.evt === "tool/call" && ev.tool) {
61
+ const n = ensureNode(seq || "tool-" + steps + "-" + ev.tool, `${ev.tool}`, "tool");
62
+ if (lastNodeId && lastNodeId !== n.id) addEdge(lastNodeId, n.id, ev.ok === false ? "失败" : "");
63
+ lastNodeId = n.id;
64
+ }
65
+ if (nodes.length > maxNodes) break;
66
+ }
67
+ return { nodes, edges, steps, startedAt, endedAt };
68
+ }
69
+
70
+ // ---- Mermaid 渲染 ----
71
+ export function toMermaid(canvas) {
72
+ const lines = ["graph LR"];
73
+ for (const n of canvas.nodes) {
74
+ const cls = n.kind === "boundary" ? ":::" + n.kind : (n.kind === "fail" ? ":::" + n.kind : "");
75
+ lines.push(` ${n.id}["${escapeLabel(n.label)}"]${cls}`);
76
+ }
77
+ for (const e of canvas.edges) {
78
+ lines.push(` ${e.from} -->|${escapeLabel(e.label)}| ${e.to}`);
79
+ }
80
+ return lines.join("\n");
81
+ }
82
+
83
+ function escapeLabel(s) {
84
+ return String(s || "").replace(/["[\]]/g, "").slice(0, 50);
85
+ }
86
+
87
+ // ---- 画布存储 (按天, 增量追加) ----
88
+ export class CanvasStore {
89
+ constructor(dataDir) {
90
+ this.dir = path.join(dataDir, "memory", "canvas");
91
+ ensureDir(this.dir);
92
+ }
93
+
94
+ _file(day = logicalDay()) { return path.join(this.dir, `${day}.json`); }
95
+
96
+ // 保存某天画布 (整体覆盖当天, 画布是归纳视图非 append-only)
97
+ save(canvas, { day = logicalDay() } = {}) {
98
+ const payload = { day, updatedAt: Date.now(), ...canvas };
99
+ writeText(this._file(day), JSON.stringify(payload, null, 2));
100
+ return payload;
101
+ }
102
+
103
+ read(day = logicalDay()) {
104
+ const f = this._file(day);
105
+ if (!fs.existsSync(f)) return null;
106
+ try { return JSON.parse(fs.readFileSync(f, "utf8")); } catch { return null; }
107
+ }
108
+
109
+ // 从 trace 事件自动生成并保存 (若步骤数达标); 返回 null 表示未触发
110
+ async captureFromEvents(events, { minSteps = CANVAS_DEFAULTS.minSteps } = {}) {
111
+ const canvas = buildCanvasFromEvents(events);
112
+ if (canvas.steps < minSteps) return null;
113
+ return this.save(canvas);
114
+ }
115
+ }
116
+
117
+ // ---- 上下文注入片段: 只有画布 + 最近细节, 省 token ----
118
+ export function renderCanvasContext(canvas, { detailLines = 3 } = {}) {
119
+ if (!canvas || !canvas.nodes || !canvas.nodes.length) return "";
120
+ const mermaid = toMermaid(canvas);
121
+ const tail = canvas.nodes.slice(-detailLines).map((n) => `- ${n.id}: ${n.label}`).join("\n");
122
+ return `\n# 任务画布 (符号化)
123
+ ${mermaid}
124
+
125
+ 最近节点:
126
+ ${tail}
127
+ (需细节时用 node_id 取事件日志原文)
128
+ `;
129
+ }
130
+
131
+ export default CanvasStore;
@@ -0,0 +1,28 @@
1
+ // src/memory/compaction.js - 会话压缩层 (吸收 OpenClaw compaction 精华)
2
+ // 设计要点:
3
+ // - 超阈值时用 LLM 把旧对话压缩成「结构化摘要」(固定格式), 替换旧历史, 防上下文失控
4
+ // - 摘要可迭代: 摘要作为 system 消息进入下一轮, 下次压缩时摘要+新对话一起再压
5
+ // - 固定格式: 目标/进展/关键决策/待办/关键上下文, 保证可预测、可续写
6
+
7
+ export const COMPACTION_SYSTEM_PROMPT = `你是对话压缩器。把下面的对话历史压缩成一段结构化摘要,严格按以下格式输出(无内容的小节填「无」):
8
+ - 目标:用户想达成什么
9
+ - 进展:已完成的关键步骤
10
+ - 关键决策:已确定的重要决定/约定
11
+ - 待办:尚未完成的事项
12
+ - 关键上下文:后续对话必须知道的细节(数字/路径/偏好/约束)
13
+ 要求:简洁,≤300字,保留所有关键事实,不要客套,直接输出摘要。`;
14
+
15
+ // 组装压缩请求消息 (system + 待压缩对话)
16
+ export function buildCompactionMessages(transcript) {
17
+ return [
18
+ { role: "system", content: COMPACTION_SYSTEM_PROMPT },
19
+ { role: "user", content: String(transcript).slice(0, 8000) },
20
+ ];
21
+ }
22
+
23
+ // 把投影消息列表转成压缩用文本 (role → 中文标签)
24
+ export function transcriptToText(messages) {
25
+ return messages
26
+ .map((m) => (m.role === "user" ? "用户" : m.role === "assistant" ? "助手" : m.role) + ": " + String(m.content ?? ""))
27
+ .join("\n");
28
+ }
@@ -0,0 +1,122 @@
1
+ // src/memory/experience.js - 经验库 (自学习)
2
+ // 从每次任务结果中提炼经验, 供后续任务参考 (参考 openhanako experience)
3
+ // 跨 agent 共享: 支持全局经验目录 (globalDataDir), 写入用文件锁防并发覆盖
4
+ // v1.0.7: 同义经验相似合并 (bigram overlap, 词序/措辞变体命中合并而非新增)
5
+ import path from "node:path";
6
+ import { ensureDir, readJson, writeJson, nowISO, withFileLock } from "../utils/store.js";
7
+
8
+ // 同义合并阈值: 经验 lesson 的 bigram overlap 达到此值视为同一条
9
+ // 用真实生产变体校准: 同义改写 0.50-0.55, 相关但不同 0.29, 不相关 0.0 → 0.5 能抓同义不误伤
10
+ const SIM_THRESHOLD = 0.5;
11
+
12
+ // bigram 集合 (去空白, 中文按字符)
13
+ function _bigrams(s) {
14
+ const chars = Array.from(String(s).replace(/\s+/g, ""));
15
+ const out = new Set();
16
+ for (let i = 0; i < chars.length - 1; i++) out.add(chars[i] + chars[i + 1]);
17
+ return out;
18
+ }
19
+
20
+ // overlap 系数: 交集 / 较短者 bigram 数 (对词序变化容忍, 与 FactStore._overlap 同思路)
21
+ export function lessonOverlap(a, b) {
22
+ const A = _bigrams(a), B = _bigrams(b);
23
+ if (!A.size || !B.size) return 0;
24
+ let inter = 0;
25
+ for (const x of A) if (B.has(x)) inter++;
26
+ return inter / Math.min(A.size, B.size);
27
+ }
28
+
29
+ // 模板句判定: 去掉数字后完全相同 (如 "A 经验教训 0" vs "A 经验教训 1")
30
+ // 这类"仅编号不同"的条目是不同内容, 不参与同义合并 (防误伤)
31
+ function _isTemplateLike(a, b) {
32
+ return String(a).replace(/\d+/g, "#") === String(b).replace(/\d+/g, "#");
33
+ }
34
+
35
+ export class Experience {
36
+ constructor(dataDir) {
37
+ this.dir = path.join(dataDir, "experience");
38
+ ensureDir(this.dir);
39
+ this.file = path.join(this.dir, "lessons.json");
40
+ this.lessons = readJson(this.file, []);
41
+ }
42
+
43
+ learn({ task, outcome, lesson, tags = [] }) {
44
+ const entry = {
45
+ id: "e_" + Math.random().toString(36).slice(2, 10),
46
+ task: String(task || "").slice(0, 300),
47
+ outcome: String(outcome || "").slice(0, 300),
48
+ lesson: String(lesson || "").slice(0, 500),
49
+ tags,
50
+ ts: nowISO(),
51
+ uses: 0,
52
+ };
53
+ if (!entry.lesson || entry.lesson.length < 5) return null;
54
+ // 内容去重: 同一 lesson 已存在则命中加分 (uses+1) 而非新增, 防高频学习路径写放大
55
+ // 归一化: 去首尾空白 + 折叠连续空白 (与 FactStore._norm 同策略)
56
+ const normKey = String(lesson).trim().replace(/\s+/g, " ");
57
+ // 锁内读-改-写: 防止多 agent 共享经验库时并发覆盖
58
+ return withFileLock(this.file, () => {
59
+ this.lessons = readJson(this.file, []);
60
+ const existing = this.lessons.find((l) => String(l.lesson || "").trim().replace(/\s+/g, " ") === normKey);
61
+ if (existing) {
62
+ existing.uses += 1;
63
+ existing.ts = nowISO(); // 刷新时间, 让近期命中经验排到 context 前面
64
+ writeJson(this.file, this.lessons);
65
+ return existing;
66
+ }
67
+ // 同义合并 (v1.0.7): 措辞/词序不同的变体 (逃过精确去重) 语义相似时合并, 防同义经验堆积
68
+ // 排除"仅编号不同"的模板句 (如 A 经验教训 0/1 → 视为不同条目)
69
+ const similar = this.lessons.find((l) => {
70
+ const la = String(l.lesson || "");
71
+ if (_isTemplateLike(la, entry.lesson)) return false;
72
+ return lessonOverlap(la, entry.lesson) >= SIM_THRESHOLD;
73
+ });
74
+ if (similar) {
75
+ similar.uses += 1;
76
+ similar.ts = nowISO();
77
+ writeJson(this.file, this.lessons);
78
+ return similar;
79
+ }
80
+ this.lessons.push(entry);
81
+ this._prune();
82
+ writeJson(this.file, this.lessons);
83
+ return entry;
84
+ });
85
+ }
86
+
87
+ recall(taskDesc) {
88
+ const q = String(taskDesc || "").toLowerCase();
89
+ const scored = this.lessons
90
+ .map((l) => {
91
+ let s = 0;
92
+ const hay = (l.lesson + " " + l.task + " " + l.tags.join(" ")).toLowerCase();
93
+ if (q && hay.includes(q)) s += 10;
94
+ if (l.uses > 0) s += Math.min(l.uses, 5);
95
+ return { ...l, score: s };
96
+ })
97
+ .filter((l) => l.score > 0)
98
+ .sort((a, b) => b.score - a.score);
99
+ return scored.slice(0, 5);
100
+ }
101
+
102
+ use(id) {
103
+ withFileLock(this.file, () => {
104
+ this.lessons = readJson(this.file, []);
105
+ const l = this.lessons.find((x) => x.id === id);
106
+ if (l) { l.uses += 1; writeJson(this.file, this.lessons); }
107
+ });
108
+ }
109
+
110
+ _prune() {
111
+ // 最多保留 200 条
112
+ if (this.lessons.length > 200) {
113
+ this.lessons.sort((a, b) => b.uses - a.uses);
114
+ this.lessons = this.lessons.slice(0, 200);
115
+ }
116
+ }
117
+
118
+ context() {
119
+ const recent = [...this.lessons].sort((a, b) => new Date(b.ts) - new Date(a.ts)).slice(0, 5);
120
+ return recent.map((l) => `- 经验: ${l.lesson}`).join("\n") || "(暂无经验)";
121
+ }
122
+ }