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.
- package/LICENSE +201 -201
- package/README.md +195 -195
- package/bin/ppx-channels.js +2 -2
- package/bin/ppx-serve.js +5 -5
- package/bin/ppx-setup.js +124 -0
- package/bin/ppx-web.js +140 -0
- package/bin/ppx.js +2 -2
- package/config/identity.md +6 -6
- package/config/ishiki.md +16 -16
- package/config/ppx.json +6 -124
- package/config/ppx.json.example +143 -0
- package/package.json +16 -6
- package/skills/.usage.json +6 -0
- package/skills/agent-professional-training/SKILL.md +94 -0
- package/skills/agent-professional-training/trajectories/2026-09-11-dpo-pairs.jsonl +2 -0
- package/skills/agent-professional-training/trajectories/2026-09-11-round2-trajectory.json +15 -0
- package/skills/agent-professional-training/trajectories/2026-09-13-first-run-trajectory.json +17 -0
- package/skills/brainstorm/SKILL.md +24 -0
- package/skills/cupid-lover-comms/SKILL.md +37 -0
- package/skills/debug/SKILL.md +26 -0
- package/skills/plan/SKILL.md +25 -0
- package/skills/ponytail/SKILL.md +25 -0
- package/skills/ppx-memory/SKILL.md +91 -0
- package/skills/ppx-memory/scripts/cli.js +192 -0
- package/skills/ppx-memory/scripts/experience.js +133 -0
- package/skills/ppx-memory/scripts/fact-store.js +842 -0
- package/skills/ppx-memory/scripts/l0.js +52 -0
- package/skills/ppx-memory/scripts/l2.js +146 -0
- package/skills/ppx-memory/scripts/l3.js +112 -0
- package/skills/ppx-memory/scripts/memory-ticker.js +238 -0
- package/skills/ppx-memory/scripts/pii.js +42 -0
- package/skills/ppx-memory/scripts/schema.js +80 -0
- package/skills/ppx-memory/scripts/session.js +398 -0
- package/skills/ppx-memory/scripts/similarity.js +43 -0
- package/skills/ppx-memory/scripts/store.js +116 -0
- package/skills/ppx-memory/scripts/wal.js +38 -0
- package/skills/ppx-selfheal/SKILL.md +24 -0
- package/skills/ppx-selfheal/scripts/cli.js +80 -0
- package/skills/ppx-selfheal/scripts/healer.js +184 -0
- package/skills/ppx-selfheal/scripts/logger.js +17 -0
- package/skills/ppx-selfheal/scripts/store.js +116 -0
- package/skills/prompt-depth-kit/SKILL.md +28 -0
- package/skills/session-naming/SKILL.md +36 -0
- package/skills/verify/SKILL.md +25 -0
- package/src/agent/index.js +1352 -717
- package/src/agent/prompts.js +46 -2
- package/src/aml-server.js +197 -151
- package/src/ans/eviction.js +123 -143
- package/src/ans/guard.js +159 -120
- package/src/ans/lifecycle.js +96 -93
- package/src/ans/proactive.js +112 -129
- package/src/ans/reward.js +95 -111
- package/src/ans/values.js +15 -15
- package/src/audit/audit-chain.js +43 -7
- package/src/audit/verifier.js +157 -120
- package/src/bus/circuit-breaker.js +9 -1
- package/src/bus/runtime-bus.js +107 -93
- package/src/channels/base.js +57 -34
- package/src/channels/feishu.js +118 -126
- package/src/channels/http.js +1090 -651
- package/src/channels/index.js +111 -110
- package/src/channels/log.js +29 -29
- package/src/channels/wechat-crypto.js +73 -74
- package/src/channels/wechat.js +191 -197
- package/src/channels/workspace.js +94 -0
- package/src/channels-cli.js +126 -124
- package/src/cli.js +129 -120
- package/src/commands/index.js +142 -0
- package/src/config/channels.js +137 -170
- package/src/config/index.js +277 -224
- package/src/config/placeholder.js +31 -0
- package/src/config/providers.js +148 -182
- package/src/config/settings.js +156 -182
- package/src/core/policy.js +69 -18
- package/src/core/trace.js +8 -6
- package/src/edit/editblock.js +266 -0
- package/src/edit/snapshot.js +67 -0
- package/src/evidence/index.js +153 -0
- package/src/evolve/playbook.js +9 -10
- package/src/hooks/index.js +113 -0
- package/src/llm/client.js +4 -3
- package/src/llm/dsml.js +74 -74
- package/src/llm/embedder.js +41 -35
- package/src/llm/fence.js +52 -52
- package/src/llm/index.js +4 -4
- package/src/llm/local-embedder.js +94 -0
- package/src/llm/presets.js +113 -0
- package/src/llm/pricing.js +93 -0
- package/src/llm/retry.js +73 -73
- package/src/llm/router.js +92 -89
- package/src/mcp/admin.js +44 -10
- package/src/mcp/client.js +487 -477
- package/src/mcp/http.js +23 -37
- package/src/mcp/index.js +116 -116
- package/src/mcp/server.js +23 -18
- package/src/mcp/tasks.js +9 -14
- package/src/memory/asset-hub.js +6 -11
- package/src/memory/canvas.js +2 -5
- package/src/memory/compaction.js +28 -28
- package/src/memory/experience.js +133 -122
- package/src/memory/fact-store.js +914 -698
- package/src/memory/failure-episode.js +20 -11
- package/src/memory/fork.js +17 -8
- package/src/memory/index.js +8 -6
- package/src/memory/l0.js +53 -52
- package/src/memory/l2.js +145 -130
- package/src/memory/l3.js +111 -111
- package/src/memory/legion-board.js +71 -0
- package/src/memory/memory-ticker.js +239 -240
- package/src/memory/session.js +398 -397
- package/src/memory/sqlite-store.js +581 -0
- package/src/mode/blackboard.js +49 -49
- package/src/mode/graph.js +42 -41
- package/src/mode/index.js +64 -64
- package/src/mode/legion.js +54 -51
- package/src/mode/plan-exec.js +50 -50
- package/src/mode/router.js +28 -40
- package/src/orchestrator/agent-worker.js +69 -69
- package/src/orchestrator/dag.js +90 -83
- package/src/orchestrator/experts.js +76 -0
- package/src/orchestrator/index.js +1 -1
- package/src/orchestrator/legion.js +179 -187
- package/src/orchestrator/supervisor.js +6 -8
- package/src/permissions/index.js +378 -0
- package/src/persona/index.js +28 -29
- package/src/plugin/builtin.js +313 -215
- package/src/plugin/context.js +80 -79
- package/src/plugin/index.js +62 -62
- package/src/plugin/v3.js +73 -0
- package/src/protocol/index.js +148 -0
- package/src/repomap/index.js +309 -0
- package/src/review/index.js +393 -0
- package/src/seam/registry.js +3 -0
- package/src/seam/shell.js +55 -55
- package/src/security/injection.js +79 -0
- package/src/selfheal/evolve.js +67 -67
- package/src/selfheal/healer.js +184 -167
- package/src/selfheal/run.js +9 -9
- package/src/server.js +63 -60
- package/src/services/diagnose.js +180 -0
- package/src/services/learning-service.js +9 -0
- package/src/services/memory-health.js +34 -6
- package/src/services/memory-service.js +42 -9
- package/src/services/triage.js +138 -0
- package/src/session/parts.js +76 -0
- package/src/session/projection.js +73 -0
- package/src/session/rollout.js +54 -0
- package/src/session/turn.js +137 -0
- package/src/skills/lint.js +72 -0
- package/src/skills/loader.js +231 -150
- package/src/skills/search.js +58 -0
- package/src/skills/verify.js +95 -100
- package/src/tools/advanced.js +388 -352
- package/src/tools/builtin.js +384 -297
- package/src/tools/catalog.js +283 -159
- package/src/tools/command-guard.js +112 -112
- package/src/tools/custom.js +47 -47
- package/src/tools/delegate.js +383 -297
- package/src/tools/document.js +254 -253
- package/src/tools/git.js +151 -0
- package/src/tools/governance.js +47 -20
- package/src/tools/index.js +16 -11
- package/src/tools/methods.js +178 -178
- package/src/tools/ocr.js +59 -59
- package/src/tools/sandbox-worker.js +40 -0
- package/src/tools/sandbox.js +92 -0
- package/src/tools/seam.js +162 -125
- package/src/tools/selfmod.js +196 -176
- package/src/tools/v3.js +225 -0
- package/src/tools/vad.js +176 -0
- package/src/tools/voice.js +238 -0
- package/src/utils/async.js +14 -0
- package/src/utils/config-file.js +53 -0
- package/src/utils/crashguard.js +88 -0
- package/src/utils/http.js +53 -0
- package/src/utils/id.js +8 -0
- package/src/utils/json-state.js +33 -0
- package/src/utils/logger.js +17 -17
- package/src/utils/ndjson.js +25 -0
- package/src/utils/pii.js +42 -42
- package/src/utils/rate-limit.js +50 -0
- package/src/utils/schema.js +80 -0
- package/src/utils/similarity.js +43 -0
- package/src/utils/store.js +170 -108
- package/src/utils/text.js +15 -15
- package/src/utils/trace.js +153 -153
- package/src/utils/wal.js +39 -0
- package/src/utils/winutf8.js +16 -15
- package/src/wiki/index.js +170 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"task": "按 agent-professional-training 技能首次真实运行「感知—记忆—推理—行动—反思」闭环,产出第 1 条训练轨迹并通过校验器",
|
|
3
|
+
"observations": [
|
|
4
|
+
{"modality": "text", "content": "用户指令:'你试试'。上下文:SKILL.md 7.6 节已新增 NexRL/Nex-N2.5 框架并映射到 L1/L2 行为层动作;技能要求每次任务第 5 步强制写轨迹。环境:Windows / PowerShell / OpenClaw 工作区,桌面 repo 与已安装技能同步于 git main=d697231"}
|
|
5
|
+
],
|
|
6
|
+
"memory": [
|
|
7
|
+
"技能第 2 节闭环五步:感知→记忆→推理→行动→反思,第 5 步强制写轨迹",
|
|
8
|
+
"技能第 4 节:轨迹 8 必填字段 task/observations/memory/thought/action/result/reward/reflection",
|
|
9
|
+
"技能 4.2 校验器:8 字段齐全 + reward 数值 + modality 合法",
|
|
10
|
+
"先例:2026-09-11 已有 round2-trajectory.json(真实事件取材)+ dpo-pairs.jsonl"
|
|
11
|
+
],
|
|
12
|
+
"thought": "任务'试试'=验证闭环能真实跑通。规划:①感知输入(用户指令+环境);②检索记忆(技能规程+先例格式);③推理拆解(写轨迹→写校验器→跑校验→归档→同步 git);④行动(写入第 4 节格式轨迹、运行 4.2 校验器);⑤反思(对照 9.0 行为层自检表)+ 强制写本条轨迹。核心认知:'行动'步本身就是'写轨迹+跑校验'——训练即做事,做事即训练",
|
|
13
|
+
"action": {"tool": "write+exec", "args": {"files": ["trajectories/2026-09-13-first-run-trajectory.json", "validate_trajectory.py"], "commands": ["python validate_trajectory.py trajectories/2026-09-13-first-run-trajectory.json", "同步已安装技能 + git commit"]}},
|
|
14
|
+
"result": {"ok": true, "summary": "闭环五步真实跑通:首条轨迹按第 4 节 8 字段格式写入并通过 4.2 校验器;归档 trajectories/;同步已安装技能目录并 git 提交;无 GPU 纯行为层进化,符合技能主线 L1/L2"},
|
|
15
|
+
"reward": 1.0,
|
|
16
|
+
"reflection": "首次真实运行闭环验证可行。关键发现:①闭环'行动'步本身就是'写轨迹+跑校验'——训练即做事、做事即训练,不需任何额外训练代码;②记忆检索要先例:参照 2026-09-11 轨迹格式避免重造格式;③校验器是闭环的'环境反馈',reward 从校验结果来,不依赖 GPU;④本轮'教训沉淀':轨迹 JSON 中命令路径要写相对路径,避免换机失效;⑤下一步:持续按闭环干活攒轨迹,满 50 条跑第 5 节飞轮生成偏好对(对照 14 节自检清单)"
|
|
17
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: brainstorm
|
|
3
|
+
description: 发散-收敛式头脑风暴:先穷举方案再按可行性收敛。需求模糊、方案未定、要做技术选型时使用。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# brainstorm — 发散与收敛
|
|
7
|
+
|
|
8
|
+
## 流程
|
|
9
|
+
|
|
10
|
+
1. **复述问题**:用一句话写下真实目标与成功标准。目标写不清就先问,别急着给方案。
|
|
11
|
+
2. **发散(不评判)**:列出 ≥6 个方案,含"看起来不靠谱的"。此阶段禁止自我审查。
|
|
12
|
+
3. **归类**:把方案归到 2–4 个方向(如:最小改动 / 一步到位 / 换个思路 / 不做)。
|
|
13
|
+
4. **收敛**:每个方向选 1 个代表方案,按「代价 / 收益 / 可逆性」三列打分。
|
|
14
|
+
5. **推荐 1 个 + 备选 1 个**,说明为什么淘汰其余。
|
|
15
|
+
|
|
16
|
+
## 反合理化
|
|
17
|
+
|
|
18
|
+
- "只有一个方案"——那就是还没发散完,回去重来。
|
|
19
|
+
- "细节以后再想"——不可逆决策不允许以后再想。
|
|
20
|
+
- "先做起来看看"——先做起来的前提是能回滚。
|
|
21
|
+
|
|
22
|
+
## 验证
|
|
23
|
+
|
|
24
|
+
完成后必须给出:① 至少 6 条候选;② 打分表;③ 明确推荐项 + 淘汰理由。
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cupid-lover-comms
|
|
3
|
+
description: 可自定义角色沟通素材库(Role-based Chatting Kit)。按关系阶段组织话术与话题引子,含好话术/坏话术对照。需要设计拟人角色的对话风格、陪伴型文案时使用。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 丘比特 · 可自定义角色沟通素材库(Role-based Chatting Kit)
|
|
7
|
+
|
|
8
|
+
为拟人化角色(陪伴、客服人格、虚拟伴侣)提供分阶段的沟通素材,避免"上来就掏心窝子"这类失真人设。
|
|
9
|
+
|
|
10
|
+
## 流程
|
|
11
|
+
|
|
12
|
+
1. **定关系阶段**:先确定当前处于哪个阶段——初识 / 熟悉 / 亲近 / 稳定。阶段决定可用的亲密度和话题深度。
|
|
13
|
+
2. **套漏斗(The Funnel)**:话题只能逐级下探,不可跳级。初识谈事实与兴趣,熟悉谈观点与经历,亲近才谈情绪与脆弱。
|
|
14
|
+
3. **选话术**:同一意图准备"好话术 / 坏话术"两版对照——好话术给具体动作与承接,坏话术是套话与自我中心。
|
|
15
|
+
4. **补话题引子**:从话题库里按阶段取 3–5 个开放性问题,避免只能回答"是/否"的封闭问题。
|
|
16
|
+
5. **检查人设一致性**:把生成的回复与角色设定对齐——用词、语气、关注点是否像同一个人说的。
|
|
17
|
+
|
|
18
|
+
## 好话术 vs 坏话术(对照表)
|
|
19
|
+
|
|
20
|
+
| 场景 | 坏话术 | 好话术 |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| 对方说累了 | "那你早点休息" | "今天是什么事最耗神?说出来我陪你顺一遍" |
|
|
23
|
+
| 对方分享好消息 | "恭喜恭喜!" | "这个你准备了挺久吧,最得意的是哪一步?" |
|
|
24
|
+
| 对方情绪低落 | "别想太多了" | "先不急着解决,你想让我听着,还是想一起想办法?" |
|
|
25
|
+
| 自己想表达关心 | "我一直都在" | "你上次提的那件事,后来怎么样了?" |
|
|
26
|
+
|
|
27
|
+
> 判据:**坏话术结束对话,好话术开启下一步。**
|
|
28
|
+
|
|
29
|
+
## 反合理化
|
|
30
|
+
|
|
31
|
+
- "话说得越热络越像真人"——热络不等于亲密,越级热络是典型的人设崩塌。
|
|
32
|
+
- "套话最安全"——套话没有信息量,对方接不下去,对话就死了。
|
|
33
|
+
- "一句万能回复走天下"——同一个回复跨越所有阶段,等于没有阶段设计。
|
|
34
|
+
|
|
35
|
+
## 验证
|
|
36
|
+
|
|
37
|
+
必须给出:① 当前关系阶段判定;② 该阶段允许的话题深度;③ 好/坏话术对照至少 3 组;④ 人设一致性检查结果。
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: debug
|
|
3
|
+
description: 面向根因的排障流程:先稳定复现再二分定位,禁止猜测式改代码。出现报错、行为异常、测试失败时使用。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# debug — 根因排障
|
|
7
|
+
|
|
8
|
+
## 流程
|
|
9
|
+
|
|
10
|
+
1. **读完整报错**:全文读,包括栈与 code(`ERR_*`),别只看第一行。
|
|
11
|
+
2. **稳定复现**:写出最小复现命令。不能复现就先想办法复现,别改代码。
|
|
12
|
+
3. **二分定位**:把链路切成两半,确认问题在哪半,重复到最小单元。
|
|
13
|
+
4. **根因陈述**:用一句话说清"为什么",并指出它在哪一行。
|
|
14
|
+
5. **先写回归**:修之前先加一个能失败的测试,再改到它通过。
|
|
15
|
+
6. **验证修复**:跑全量回归,确认没有连带破坏。
|
|
16
|
+
|
|
17
|
+
## 反合理化
|
|
18
|
+
|
|
19
|
+
- "先改改看"——猜测式修改会把真正的根因埋得更深。
|
|
20
|
+
- "这个报错以前见过"——相似报错≠同一根因。
|
|
21
|
+
- "加了 try/catch 就不报错了"——那是掩盖,不是修复。
|
|
22
|
+
- "改完好了就行"——没写回归,下次还会犯。
|
|
23
|
+
|
|
24
|
+
## 验证
|
|
25
|
+
|
|
26
|
+
必须提供:① 最小复现命令与原始报错;② 定位到的 `文件:行号` 根因;③ 新增的回归测试;④ 全量回归结果。
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plan
|
|
3
|
+
description: 把模糊目标拆成可执行、可验证的步骤序列,标出依赖与风险。多步骤任务动手前使用。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# plan — 计划拆解
|
|
7
|
+
|
|
8
|
+
## 流程
|
|
9
|
+
|
|
10
|
+
1. **定终局**:写下"做完时什么样"(可观察的终态),不是"要做什么"。
|
|
11
|
+
2. **倒推步骤**:从终态往回拆,每步输出必须是下一步的输入。
|
|
12
|
+
3. **标注**:每步标 ① 依赖 ② 可回滚性 ③ 预计触碰的文件/系统。
|
|
13
|
+
4. **找关键路径**:哪一步最可能卡住?先做它(风险前置)。
|
|
14
|
+
5. **设检查点**:在不可逆动作之前插一个"停下来确认"。
|
|
15
|
+
6. **产出**:有序步骤表 + 关键路径 + 检查点位置。
|
|
16
|
+
|
|
17
|
+
## 反合理化
|
|
18
|
+
|
|
19
|
+
- "边做边想"——不可逆的部分不允许边做边想。
|
|
20
|
+
- "先全部做完再测"——每步都应可独立验证。
|
|
21
|
+
- "步骤越多越专业"——超过 7 步就该分层。
|
|
22
|
+
|
|
23
|
+
## 验证
|
|
24
|
+
|
|
25
|
+
完成后必须给出:① 步骤表(含依赖与可回滚性);② 关键路径;③ 每个不可逆动作前的检查点。
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ponytail
|
|
3
|
+
description: 最小改动原则:能改一行不改十行,能不加依赖就不加。任何重构、优化、修缺陷动手前使用。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ponytail — 最小改动
|
|
7
|
+
|
|
8
|
+
## 流程
|
|
9
|
+
|
|
10
|
+
1. **先量体**:改之前统计当前现状(文件数、行数、测试数),改完再量一次,用差值证明改动范围。
|
|
11
|
+
2. **找最小切口**:同样的效果,优先选"改动行数最少"的方案;能局部修就不整体重写。
|
|
12
|
+
3. **零依赖优先**:能用手写 20 行解决,就不引入一个包(本项目的底线是运行时零依赖)。
|
|
13
|
+
4. **不动无关代码**:顺手格式化、顺手改名、顺手删注释,都算越界 —— 除非它就在你改的这行上。
|
|
14
|
+
5. **可回滚**:动不可逆的东西(删数据、改接口)前先备份或留隔离区。
|
|
15
|
+
|
|
16
|
+
## 反合理化
|
|
17
|
+
|
|
18
|
+
- "顺手重构一下"——顺手重构是最常见的"改出三个新 bug"来源。
|
|
19
|
+
- "加个库更省事"——省的是你的时间,赔的是别人的部署。
|
|
20
|
+
- "反正要改,一次改干净"——大爆炸式改动没法二分定位问题。
|
|
21
|
+
- "这代码太丑了我看不下去"——丑但能跑,和跑不起来之间差一个 PR。
|
|
22
|
+
|
|
23
|
+
## 验证
|
|
24
|
+
|
|
25
|
+
必须提供:① 改动前后的量体数据(行数/文件数);② 本次改动的 diff 范围说明;③ 明确列出"我刻意没改的东西"。
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ppx-memory
|
|
3
|
+
description: 皮皮虾记忆引擎(ppx-memory)的读写规程与独立 CLI 用法:五层记忆结构、什么时候记、记到哪层、怎么检索与安全遗忘。涉及长期记忆、用户偏好、跨会话召回时使用。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 皮皮虾记忆引擎 (ppx-memory)
|
|
7
|
+
|
|
8
|
+
五层结构(L0 对话 → L1 原子事实 → L2 场景 → L3 画像 → L4 程序性记忆),自带高斯衰减、软删回滚、版本链与 WAL 增量落盘。
|
|
9
|
+
|
|
10
|
+
本技能附带 `scripts/cli.js`,是一个**不依赖主项目**的独立可运行版本,可直接对记忆库做增删查。
|
|
11
|
+
|
|
12
|
+
## 流程
|
|
13
|
+
|
|
14
|
+
### 1. 判断该不该记
|
|
15
|
+
|
|
16
|
+
只记**跨会话仍然成立**的信息:偏好、约定、稳定事实。
|
|
17
|
+
不记:寒暄、提问、一次性的中间结果。(引擎已做句式过滤,但边界情况要自己判——脏记忆会喂回上下文污染判断。)
|
|
18
|
+
|
|
19
|
+
### 2. 写入(选层)
|
|
20
|
+
|
|
21
|
+
| 内容类型 | 目标层 | 工具 |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| 用户偏好 / 稳定事实 | L1 | `memory_add` |
|
|
24
|
+
| 可复用的方法 / 流程 | L4(衰减仅 L1 的 1/4) | `memory_add`(layer=4)或 `create_skill` |
|
|
25
|
+
| 场景知识 | L2 | 由 `afterTurn` 自动聚类归档 |
|
|
26
|
+
|
|
27
|
+
内容必须**自包含**——脱离当前上下文也能读懂,否则三个月后检索出来也不知道在说什么。
|
|
28
|
+
|
|
29
|
+
### 3. 检索
|
|
30
|
+
|
|
31
|
+
- `memory_search` 走"粗召回(倒排索引)→ 精排(BM25 × 时间新鲜度 + 命中权重 + 重要性)",门槛 1 分。
|
|
32
|
+
- 需要更宽上下文时,同时取 L2 场景与 L3 画像(`persona_read`)。
|
|
33
|
+
- 疑问句/多义词可用 `queryMulti` 的查询扩展(RRF 融合多路结果)。
|
|
34
|
+
|
|
35
|
+
### 4. 遗忘(可回滚)
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
memory_forget → 软删(状态置 deleted,检索立即不可见,数据保留)
|
|
39
|
+
memory_restore → 回滚(恢复即视为一次访问,避免刚恢复就被衰减清空)
|
|
40
|
+
memory_list_deleted → 复核已遗忘条目(含原因与时间)
|
|
41
|
+
memory_clear_layer → 按层清理(默认软删,hard=true 才物理删除)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**分不清该不该删时一律先软删。**
|
|
45
|
+
|
|
46
|
+
### 5. 迁移
|
|
47
|
+
|
|
48
|
+
`memory_export` / `memory_import` 用于换机或备份。导入前确认 `mode`:`merge`(按内容去重)还是 `replace`(整体替换,**会覆盖现有库**)。
|
|
49
|
+
|
|
50
|
+
### 6. 独立 CLI 用法(不依赖主项目)
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# 写入一条原子记忆(自动 PII 脱敏)
|
|
54
|
+
node skills/ppx-memory/scripts/cli.js add "用户偏好深色主题"
|
|
55
|
+
|
|
56
|
+
# 检索
|
|
57
|
+
node skills/ppx-memory/scripts/cli.js search "主题偏好"
|
|
58
|
+
|
|
59
|
+
# 组装完整上下文(今日 + 长期摘要 + 高分事实)
|
|
60
|
+
node skills/ppx-memory/scripts/cli.js context
|
|
61
|
+
|
|
62
|
+
# 读取最近会话
|
|
63
|
+
node skills/ppx-memory/scripts/cli.js session --limit 50
|
|
64
|
+
|
|
65
|
+
# 软删 / 回滚
|
|
66
|
+
node skills/ppx-memory/scripts/cli.js forget <id>
|
|
67
|
+
node skills/ppx-memory/scripts/cli.js restore <id>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
数据目录默认 `<root>/data`,可用环境变量 `PPX_DATA_DIR` 覆盖。
|
|
71
|
+
|
|
72
|
+
### 7. 集成到 OpenClaw 的时机
|
|
73
|
+
|
|
74
|
+
满足以下任一条时,把本技能的 `scripts/` 目录挂到目标 harness:
|
|
75
|
+
- 目标 harness 没有持久记忆,但需要跨会话召回用户偏好;
|
|
76
|
+
- 需要与主项目的记忆库**共享同一份数据**(同一 `PPX_DATA_DIR`);
|
|
77
|
+
- 只想用记忆能力、不想引入整个 Agent 内核。
|
|
78
|
+
|
|
79
|
+
> 注意:独立版与主项目 `src/memory/` 共享数据格式。**同一数据目录不要被两个进程同时写入**(主项目有文件锁,独立 CLI 也有,但跨进程仍建议串行使用)。
|
|
80
|
+
|
|
81
|
+
## 反合理化
|
|
82
|
+
|
|
83
|
+
- "先记下来再说"——记忆越用越脏,且会喂回上下文。
|
|
84
|
+
- "记了删不掉"——软删可回滚,别因为怕删就不敢记。
|
|
85
|
+
- "把用户原话整段存进去"——提问和寒暄不该入库。
|
|
86
|
+
- "L4 和 L1 一样处理"——技能应该长期留存,衰减率不同正是为此。
|
|
87
|
+
- "直接改 facts.json 更快"——绕过锁会损坏 WAL 一致性,务必走工具或 CLI。
|
|
88
|
+
|
|
89
|
+
## 验证
|
|
90
|
+
|
|
91
|
+
完成后必须确认:① 写入内容脱离上下文仍可读;② 软删→恢复可逆;③ 检索能命中刚写入的条目;④ 若用 CLI,跑完 `cli.js context` 能看到新条目。
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// skills/ppx-memory/scripts/cli.js - 皮皮虾记忆引擎「独立运行版」CLI
|
|
3
|
+
// 不依赖主项目 src/, 直接对记忆库做增删查 (与主项目共享同一数据格式, 可用 PPX_DATA_DIR 共用)
|
|
4
|
+
//
|
|
5
|
+
// 用法:
|
|
6
|
+
// node cli.js add "内容" [--layer 1] [--importance 12] [--scope proj] [--type fact]
|
|
7
|
+
// node cli.js search "查询" [--limit 5]
|
|
8
|
+
// node cli.js context ["当前消息"]
|
|
9
|
+
// node cli.js session [--limit 50]
|
|
10
|
+
// node cli.js forget <id|内容> [--reason 原因]
|
|
11
|
+
// node cli.js restore <id>
|
|
12
|
+
// node cli.js deleted
|
|
13
|
+
// node cli.js stats
|
|
14
|
+
// node cli.js export [--no-deleted]
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { FactStore } from "./fact-store.js";
|
|
19
|
+
|
|
20
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
// 默认数据目录: 技能位于 <root>/skills/ppx-memory/scripts/, 回退三级到 <root>/data
|
|
22
|
+
const DATA = process.env.PPX_DATA_DIR || path.resolve(HERE, "..", "..", "..", "data");
|
|
23
|
+
|
|
24
|
+
const out = (o) => console.log(typeof o === "string" ? o : JSON.stringify(o, null, 2));
|
|
25
|
+
const facts = new FactStore(DATA);
|
|
26
|
+
|
|
27
|
+
// 极简参数解析: 位置参数 + --key value
|
|
28
|
+
function parseArgs(argv) {
|
|
29
|
+
const positional = [];
|
|
30
|
+
const flags = {};
|
|
31
|
+
for (let i = 0; i < argv.length; i++) {
|
|
32
|
+
const a = argv[i];
|
|
33
|
+
if (a.startsWith("--")) {
|
|
34
|
+
const key = a.slice(2);
|
|
35
|
+
const next = argv[i + 1];
|
|
36
|
+
if (next === undefined || next.startsWith("--")) flags[key] = true;
|
|
37
|
+
else {
|
|
38
|
+
flags[key] = next;
|
|
39
|
+
i++;
|
|
40
|
+
}
|
|
41
|
+
} else positional.push(a);
|
|
42
|
+
}
|
|
43
|
+
return { positional, flags };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function addMemory(content, fl = {}) {
|
|
47
|
+
const f = facts.add(content, {
|
|
48
|
+
layer: fl.layer ? Number(fl.layer) : undefined,
|
|
49
|
+
importance: fl.importance ? Number(fl.importance) : undefined,
|
|
50
|
+
type: fl.type || undefined,
|
|
51
|
+
scope: fl.scope || null,
|
|
52
|
+
});
|
|
53
|
+
if (!f) {
|
|
54
|
+
out("未写入 (内容为空, 或被归一化去重命中已有条目)");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
out({ ok: true, id: f.id, layer: f.layer, score: Math.round(f.score * 100) / 100, content: f.content });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function printFacts(list) {
|
|
61
|
+
if (!list.length) {
|
|
62
|
+
out("(无匹配)");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
for (const f of list) {
|
|
66
|
+
const s = typeof f.score === "number" ? Math.round(f.score * 100) / 100 : "-";
|
|
67
|
+
out(`[L${f.layer ?? 1}] (${s}) ${f.id} ${f.content}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 读取最近会话 (直接读 jsonl, 不依赖 SessionStore API)
|
|
72
|
+
function readSession(limit = 50) {
|
|
73
|
+
const dir = path.join(DATA, "sessions");
|
|
74
|
+
if (!fs.existsSync(dir)) return [];
|
|
75
|
+
const files = fs
|
|
76
|
+
.readdirSync(dir)
|
|
77
|
+
.filter((f) => f.endsWith(".jsonl"))
|
|
78
|
+
.map((f) => ({ f, m: fs.statSync(path.join(dir, f)).mtimeMs }))
|
|
79
|
+
.sort((a, b) => b.m - a.m);
|
|
80
|
+
const lines = [];
|
|
81
|
+
for (const { f } of files) {
|
|
82
|
+
for (const line of fs.readFileSync(path.join(dir, f), "utf8").split("\n")) {
|
|
83
|
+
if (line.trim()) lines.push(line);
|
|
84
|
+
if (lines.length > limit * 4) break;
|
|
85
|
+
}
|
|
86
|
+
if (lines.length > limit * 4) break;
|
|
87
|
+
}
|
|
88
|
+
return lines.slice(-limit).map((l) => {
|
|
89
|
+
try {
|
|
90
|
+
const o = JSON.parse(l);
|
|
91
|
+
const d = o.data || {};
|
|
92
|
+
return { ts: o.ts, type: o.type, text: d.user || d.assistant || d.text || "" };
|
|
93
|
+
} catch {
|
|
94
|
+
return { raw: l };
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const { positional, flags } = parseArgs(process.argv.slice(2));
|
|
100
|
+
const [cmd, ...rest] = positional;
|
|
101
|
+
|
|
102
|
+
switch (cmd) {
|
|
103
|
+
case "add":
|
|
104
|
+
addMemory(rest.join(" "), flags);
|
|
105
|
+
break;
|
|
106
|
+
|
|
107
|
+
case "search":
|
|
108
|
+
case "q": {
|
|
109
|
+
const q = rest.join(" ");
|
|
110
|
+
if (!q) {
|
|
111
|
+
out("需要查询关键词");
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
const limit = flags.limit ? Number(flags.limit) : 5;
|
|
115
|
+
const hits = facts.query(q, { limit });
|
|
116
|
+
hits.forEach((h) => facts.hit(h.id)); // 命中加分 (与主项目一致)
|
|
117
|
+
printFacts(hits);
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
case "context": {
|
|
122
|
+
// 组装: 高分事实 + 长期摘要 (today/longterm 由 MemoryTicker 维护, 此处轻量回退)
|
|
123
|
+
const q = rest.join(" ");
|
|
124
|
+
printFacts(facts.query(q, { limit: 8 }));
|
|
125
|
+
const lt = path.join(DATA, "memory", "longterm.md");
|
|
126
|
+
if (fs.existsSync(lt)) {
|
|
127
|
+
out("\n--- longterm ---");
|
|
128
|
+
out(fs.readFileSync(lt, "utf8").trim().slice(0, 2000));
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
case "session": {
|
|
134
|
+
const limit = flags.limit ? Number(flags.limit) : 50;
|
|
135
|
+
const rows = readSession(limit);
|
|
136
|
+
if (!rows.length) out("(无会话记录)");
|
|
137
|
+
for (const r of rows) out(`${r.type || "?"} ${String(r.text || r.raw || "").slice(0, 160)}`);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
case "forget": {
|
|
142
|
+
const target = rest.join(" ");
|
|
143
|
+
if (!target) {
|
|
144
|
+
out("需要 id 或内容片段");
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
const r = facts.forget(target, { reason: flags.reason || null });
|
|
148
|
+
out(r ? { ok: true, forgotten: r.id || target } : "未找到可遗忘的条目");
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
case "restore": {
|
|
153
|
+
const id = rest.join(" ");
|
|
154
|
+
const r = facts.restore(id);
|
|
155
|
+
out(r ? { ok: true, restored: r.id || id } : "未找到该 id (或未被软删)");
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
case "deleted": {
|
|
160
|
+
const all = facts.exportAll({ includeDeleted: true });
|
|
161
|
+
const list = (Array.isArray(all) ? all : all.facts || []).filter((f) => f.status === "deleted");
|
|
162
|
+
if (!list.length) out("(无已遗忘条目)");
|
|
163
|
+
for (const f of list) out(`${f.id} ${f.content} ← ${f.deletedReason || "无原因"}`);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
case "stats":
|
|
168
|
+
out(facts.stats());
|
|
169
|
+
break;
|
|
170
|
+
|
|
171
|
+
case "export": {
|
|
172
|
+
const all = facts.exportAll({ includeDeleted: flags["no-deleted"] !== true });
|
|
173
|
+
process.stdout.write(JSON.stringify(all, null, 2));
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
default:
|
|
178
|
+
out(`皮皮虾记忆引擎 · 独立 CLI
|
|
179
|
+
数据目录: ${DATA}
|
|
180
|
+
|
|
181
|
+
用法:
|
|
182
|
+
add <内容> [--layer 1] [--importance 12] [--scope xxx] 写入一条记忆
|
|
183
|
+
search <关键词> [--limit 5] 检索 (命中自动加分)
|
|
184
|
+
context [当前消息] 组装上下文 (高分事实 + 长期摘要)
|
|
185
|
+
session [--limit 50] 读取最近会话
|
|
186
|
+
forget <id|内容> [--reason 原因] 软删 (可恢复)
|
|
187
|
+
restore <id> 回滚软删
|
|
188
|
+
deleted 列出已遗忘条目
|
|
189
|
+
stats 记忆库统计
|
|
190
|
+
export [--no-deleted] 导出 JSON`);
|
|
191
|
+
process.exit(cmd ? 1 : 0);
|
|
192
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
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 "./store.js";
|
|
7
|
+
import { overlapCoefficient } from "./similarity.js";
|
|
8
|
+
|
|
9
|
+
// 同义合并阈值: 经验 lesson 的 bigram overlap 达到此值视为同一条
|
|
10
|
+
// 用真实生产变体校准: 同义改写 0.50-0.55, 相关但不同 0.29, 不相关 0.0 → 0.5 能抓同义不误伤
|
|
11
|
+
const SIM_THRESHOLD = 0.5;
|
|
12
|
+
|
|
13
|
+
// overlap 系数 (交集 / 较短者): 对词序变化容忍, 实现收敛到 utils/similarity
|
|
14
|
+
// (与 FactStore._overlap 同思路, 但分词/阈值各自独立)
|
|
15
|
+
export const lessonOverlap = overlapCoefficient;
|
|
16
|
+
|
|
17
|
+
// 模板句判定: 去掉数字后完全相同 (如 "A 经验教训 0" vs "A 经验教训 1")
|
|
18
|
+
// 这类"仅编号不同"的条目是不同内容, 不参与同义合并 (防误伤)
|
|
19
|
+
function _isTemplateLike(a, b) {
|
|
20
|
+
return String(a).replace(/\d+/g, "#") === String(b).replace(/\d+/g, "#");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class Experience {
|
|
24
|
+
constructor(dataDir) {
|
|
25
|
+
this.dir = path.join(dataDir, "experience");
|
|
26
|
+
ensureDir(this.dir);
|
|
27
|
+
this.file = path.join(this.dir, "lessons.json");
|
|
28
|
+
this.lessons = readJson(this.file, []);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
learn({ task, outcome, lesson, tags = [] }) {
|
|
32
|
+
const entry = {
|
|
33
|
+
id: "e_" + Math.random().toString(36).slice(2, 10),
|
|
34
|
+
task: String(task || "").slice(0, 300),
|
|
35
|
+
outcome: String(outcome || "").slice(0, 300),
|
|
36
|
+
lesson: String(lesson || "").slice(0, 500),
|
|
37
|
+
tags,
|
|
38
|
+
ts: nowISO(),
|
|
39
|
+
uses: 0,
|
|
40
|
+
};
|
|
41
|
+
if (!entry.lesson || entry.lesson.length < 5) return null;
|
|
42
|
+
// 内容去重: 同一 lesson 已存在则命中加分 (uses+1) 而非新增, 防高频学习路径写放大
|
|
43
|
+
// 归一化: 去首尾空白 + 折叠连续空白 (与 FactStore._norm 同策略)
|
|
44
|
+
const normKey = String(lesson).trim().replace(/\s+/g, " ");
|
|
45
|
+
// 锁内读-改-写: 防止多 agent 共享经验库时并发覆盖
|
|
46
|
+
return withFileLock(this.file, () => {
|
|
47
|
+
this.lessons = readJson(this.file, []);
|
|
48
|
+
const existing = this.lessons.find((l) => String(l.lesson || "").trim().replace(/\s+/g, " ") === normKey);
|
|
49
|
+
if (existing) {
|
|
50
|
+
existing.uses += 1;
|
|
51
|
+
existing.ts = nowISO(); // 刷新时间, 让近期命中经验排到 context 前面
|
|
52
|
+
writeJson(this.file, this.lessons);
|
|
53
|
+
return existing;
|
|
54
|
+
}
|
|
55
|
+
// 同义合并 (v1.0.7): 措辞/词序不同的变体 (逃过精确去重) 语义相似时合并, 防同义经验堆积
|
|
56
|
+
// 排除"仅编号不同"的模板句 (如 A 经验教训 0/1 → 视为不同条目)
|
|
57
|
+
const similar = this.lessons.find((l) => {
|
|
58
|
+
const la = String(l.lesson || "");
|
|
59
|
+
if (_isTemplateLike(la, entry.lesson)) return false;
|
|
60
|
+
return lessonOverlap(la, entry.lesson) >= SIM_THRESHOLD;
|
|
61
|
+
});
|
|
62
|
+
if (similar) {
|
|
63
|
+
similar.uses += 1;
|
|
64
|
+
similar.ts = nowISO();
|
|
65
|
+
writeJson(this.file, this.lessons);
|
|
66
|
+
return similar;
|
|
67
|
+
}
|
|
68
|
+
this.lessons.push(entry);
|
|
69
|
+
this._prune();
|
|
70
|
+
writeJson(this.file, this.lessons);
|
|
71
|
+
return entry;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
recall(taskDesc) {
|
|
76
|
+
const q = String(taskDesc || "").toLowerCase();
|
|
77
|
+
const scored = this.lessons
|
|
78
|
+
.map((l) => {
|
|
79
|
+
let s = 0;
|
|
80
|
+
const hay = (l.lesson + " " + l.task + " " + l.tags.join(" ")).toLowerCase();
|
|
81
|
+
if (q && hay.includes(q)) s += 10;
|
|
82
|
+
if (l.uses > 0) s += Math.min(l.uses, 5);
|
|
83
|
+
return { ...l, score: s };
|
|
84
|
+
})
|
|
85
|
+
.filter((l) => l.score > 0)
|
|
86
|
+
.sort((a, b) => b.score - a.score);
|
|
87
|
+
return scored.slice(0, 5);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
use(id) {
|
|
91
|
+
withFileLock(this.file, () => {
|
|
92
|
+
this.lessons = readJson(this.file, []);
|
|
93
|
+
const l = this.lessons.find((x) => x.id === id);
|
|
94
|
+
if (l) { l.uses += 1; writeJson(this.file, this.lessons); }
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 经验清单 (公开读 API): 默认按命中次数降序, 同次数按时间新→旧
|
|
99
|
+
// 供 fork 快照 / 外部观测使用 (调用方无需触碰 this.lessons 内部数组)
|
|
100
|
+
list({ limit = 0, sort = "uses" } = {}) {
|
|
101
|
+
const all = [...this.lessons];
|
|
102
|
+
if (sort === "uses") {
|
|
103
|
+
all.sort((a, b) => (b.uses || 0) - (a.uses || 0) || new Date(b.ts || 0) - new Date(a.ts || 0));
|
|
104
|
+
} else if (sort === "ts") {
|
|
105
|
+
all.sort((a, b) => new Date(b.ts || 0) - new Date(a.ts || 0));
|
|
106
|
+
}
|
|
107
|
+
const n = Number(limit) || 0;
|
|
108
|
+
return n > 0 ? all.slice(0, n) : all;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 条目数 (可观测)
|
|
112
|
+
count() {
|
|
113
|
+
return this.lessons.length;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
_prune() {
|
|
117
|
+
// 最多保留 200 条
|
|
118
|
+
if (this.lessons.length > 200) {
|
|
119
|
+
this.lessons.sort((a, b) => b.uses - a.uses);
|
|
120
|
+
this.lessons = this.lessons.slice(0, 200);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
context() {
|
|
125
|
+
// 优先命中次数 (高频教训浮上来, 让反复发生的硬拒绝/跨会话坑真正影响行为),
|
|
126
|
+
// 同次数按时间新→旧 (近期教训仍可见)。蒸馏接缝: B3 auto-self-review 教训 uses 随复发上涨,
|
|
127
|
+
// 从而在上下文里越来越突出, 形成「经验→长期准则」的浮升链路。
|
|
128
|
+
const picked = [...this.lessons]
|
|
129
|
+
.sort((a, b) => (b.uses || 0) - (a.uses || 0) || new Date(b.ts || 0) - new Date(a.ts || 0))
|
|
130
|
+
.slice(0, 5);
|
|
131
|
+
return picked.map((l) => `- 经验: ${l.lesson}`).join("\n") || "(暂无经验)";
|
|
132
|
+
}
|
|
133
|
+
}
|