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.
- package/LICENSE +201 -0
- package/README.md +265 -0
- package/bin/ppx-channels.js +3 -0
- package/bin/ppx-serve.js +6 -0
- package/bin/ppx.js +3 -0
- package/config/identity.md +6 -0
- package/config/ishiki.md +16 -0
- package/config/ppx.json +151 -0
- package/package.json +69 -0
- package/src/agent/context.js +179 -0
- package/src/agent/index.js +717 -0
- package/src/agent/prompts.js +107 -0
- package/src/aml-server.js +151 -0
- package/src/ans/eviction.js +144 -0
- package/src/ans/guard.js +120 -0
- package/src/ans/lifecycle.js +93 -0
- package/src/ans/proactive.js +129 -0
- package/src/ans/reward.js +112 -0
- package/src/ans/values.js +15 -0
- package/src/audit/audit-chain.js +167 -0
- package/src/audit/verifier.js +120 -0
- package/src/bus/circuit-breaker.js +115 -0
- package/src/bus/runtime-bus.js +94 -0
- package/src/channels/base.js +35 -0
- package/src/channels/feishu.js +127 -0
- package/src/channels/http.js +592 -0
- package/src/channels/index.js +110 -0
- package/src/channels/log.js +29 -0
- package/src/channels/wechat-crypto.js +74 -0
- package/src/channels/wechat.js +197 -0
- package/src/channels-cli.js +124 -0
- package/src/cli.js +120 -0
- package/src/config/channels.js +170 -0
- package/src/config/index.js +224 -0
- package/src/config/providers.js +189 -0
- package/src/config/settings.js +182 -0
- package/src/core/policy.js +272 -0
- package/src/core/trace.js +89 -0
- package/src/evolve/playbook.js +194 -0
- package/src/llm/client.js +446 -0
- package/src/llm/dsml.js +74 -0
- package/src/llm/embedder.js +35 -0
- package/src/llm/fence.js +105 -0
- package/src/llm/index.js +4 -0
- package/src/llm/retry.js +73 -0
- package/src/llm/router.js +98 -0
- package/src/mcp/client.js +375 -0
- package/src/mcp/index.js +116 -0
- package/src/memory/asset-hub.js +131 -0
- package/src/memory/canvas.js +131 -0
- package/src/memory/compaction.js +28 -0
- package/src/memory/experience.js +122 -0
- package/src/memory/fact-store.js +699 -0
- package/src/memory/failure-episode.js +99 -0
- package/src/memory/fork.js +83 -0
- package/src/memory/index.js +7 -0
- package/src/memory/l0.js +52 -0
- package/src/memory/l2.js +131 -0
- package/src/memory/l3.js +112 -0
- package/src/memory/memory-ticker.js +240 -0
- package/src/memory/session.js +398 -0
- package/src/mode/blackboard.js +49 -0
- package/src/mode/graph.js +41 -0
- package/src/mode/index.js +64 -0
- package/src/mode/legion.js +51 -0
- package/src/mode/plan-exec.js +50 -0
- package/src/mode/router.js +40 -0
- package/src/orchestrator/agent-worker.js +70 -0
- package/src/orchestrator/dag.js +83 -0
- package/src/orchestrator/index.js +2 -0
- package/src/orchestrator/legion.js +188 -0
- package/src/orchestrator/supervisor.js +177 -0
- package/src/persona/index.js +29 -0
- package/src/plugin/builtin.js +212 -0
- package/src/plugin/context.js +79 -0
- package/src/plugin/index.js +62 -0
- package/src/seam/registry.js +98 -0
- package/src/seam/shell.js +55 -0
- package/src/selfheal/evolve.js +68 -0
- package/src/selfheal/healer.js +167 -0
- package/src/selfheal/run.js +9 -0
- package/src/server.js +60 -0
- package/src/services/learning-service.js +177 -0
- package/src/services/memory-health.js +99 -0
- package/src/services/memory-service.js +160 -0
- package/src/skills/loader.js +150 -0
- package/src/skills/verify.js +100 -0
- package/src/tools/advanced.js +353 -0
- package/src/tools/builtin.js +298 -0
- package/src/tools/catalog.js +159 -0
- package/src/tools/command-guard.js +112 -0
- package/src/tools/custom.js +47 -0
- package/src/tools/delegate.js +297 -0
- package/src/tools/document.js +253 -0
- package/src/tools/governance.js +260 -0
- package/src/tools/index.js +11 -0
- package/src/tools/methods.js +178 -0
- package/src/tools/ocr.js +59 -0
- package/src/tools/seam.js +125 -0
- package/src/tools/selfmod.js +176 -0
- package/src/utils/logger.js +17 -0
- package/src/utils/pii.js +42 -0
- package/src/utils/store.js +108 -0
- package/src/utils/text.js +16 -0
- package/src/utils/trace.js +153 -0
- package/src/utils/winutf8.js +15 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// src/seam/shell.js - Shell 能力 seam (吸收 dsh Service|Provider|Consumer 三层)
|
|
2
|
+
// Service Definition: 命令执行能力, 接口 = exec(cmd, opts) -> Promise<{stdout, stderr, code, ok}>
|
|
3
|
+
// - Service: "shell" 这个能力 (key = "shell")
|
|
4
|
+
// - Provider: LocalShellProvider (本地 execFile, 默认) | 未来 SandboxShellProvider (Docker/MicroVM)
|
|
5
|
+
// - Consumer: run_command / code_act 工具, 通过 ctx.consume("shell") 获取实现, 换 provider 即换执行环境
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
|
|
9
|
+
const execFileP = promisify(execFile);
|
|
10
|
+
|
|
11
|
+
// 信号死亡可读化 (吸收 Hermes: 裸退出码 -> 人类可读说明)
|
|
12
|
+
// 负码/信号(子进程语义)确定性描述; shell 的 128+signum 约定用"通常"措辞
|
|
13
|
+
const SIGNAL_NOTES = {
|
|
14
|
+
SIGKILL: "通常内存耗尽(OOM)被内核强杀, 或显式 kill -9",
|
|
15
|
+
SIGTERM: "正常终止请求(可能超时被强杀)",
|
|
16
|
+
SIGSEGV: "段错误(访问非法内存)",
|
|
17
|
+
SIGABRT: "abort 异常终止",
|
|
18
|
+
SIGINT: "Ctrl+C 中断",
|
|
19
|
+
SIGPIPE: "管道断裂(读取端提前关闭)",
|
|
20
|
+
SIGBUS: "总线错误",
|
|
21
|
+
SIGFPE: "浮点异常",
|
|
22
|
+
SIGUSR1: "用户自定义信号 1",
|
|
23
|
+
SIGQUIT: "键盘退出信号",
|
|
24
|
+
};
|
|
25
|
+
function signalNote(sig) {
|
|
26
|
+
if (!sig) return "";
|
|
27
|
+
const extra = SIGNAL_NOTES[sig] ? " — " + SIGNAL_NOTES[sig] : "";
|
|
28
|
+
return ` (进程被信号 ${sig} 终止${extra})`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 默认本地 shell provider: 在本机 shell 直接执行命令
|
|
32
|
+
// 约定: 永不 throw, 错误编码进返回值 (code != 0 或 ok=false), 与 LLM 重试内核的"失败编码进结果"一致
|
|
33
|
+
export class LocalShellProvider {
|
|
34
|
+
async exec(cmd, { cwd, timeoutMs = 30000, maxBuffer = 1024 * 1024, env = null } = {}) {
|
|
35
|
+
const isWin = process.platform === "win32";
|
|
36
|
+
try {
|
|
37
|
+
const { stdout, stderr } = await execFileP(isWin ? "cmd.exe" : "/bin/sh", [isWin ? "/c" : "-c", cmd], {
|
|
38
|
+
cwd,
|
|
39
|
+
timeout: timeoutMs,
|
|
40
|
+
maxBuffer,
|
|
41
|
+
env: env || undefined,
|
|
42
|
+
windowsHide: true,
|
|
43
|
+
});
|
|
44
|
+
return { stdout: stdout || "", stderr: stderr || "", code: 0, ok: true };
|
|
45
|
+
} catch (e) {
|
|
46
|
+
const sig = e && e.signal;
|
|
47
|
+
return {
|
|
48
|
+
stdout: "",
|
|
49
|
+
stderr: String(e && e.message || e) + signalNote(sig),
|
|
50
|
+
code: typeof e.code === "number" ? e.code : (sig ? -1 : 1),
|
|
51
|
+
ok: false,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// src/selfheal/evolve.js - 自进化引擎
|
|
2
|
+
// 职责: 闭合「记录轨迹 → 提炼教训 → 注入上下文」的自进化闭环。
|
|
3
|
+
// 背景: refine()(失败→经验) 和 refineSkill()(成功→技能) 方法早已在 agent 上定义,
|
|
4
|
+
// 但全仓库没有任何触发点 → 皮皮虾天天记录工具轨迹却从不"停下来学"。
|
|
5
|
+
// 本引擎就是那个自动触发点: 每 N 次工具调用后, 异步提炼失败教训 + 沉淀重复成功技能。
|
|
6
|
+
// 约束: 异步 fire-and-forget, 绝不阻塞当前对话; 无 LLM 时安全跳过;
|
|
7
|
+
// 节流 (minIntervalMs) 防止高频提炼爆 LLM 成本; 可配置关闭 (agent.evolve.enabled=false)。
|
|
8
|
+
import { info, warn } from "../utils/logger.js";
|
|
9
|
+
|
|
10
|
+
export class EvolutionEngine {
|
|
11
|
+
constructor(agent, config = {}) {
|
|
12
|
+
this.agent = agent;
|
|
13
|
+
this.enabled = config.enabled !== false;
|
|
14
|
+
this.everyCalls = Number(config.every_calls) || 20; // 每 N 次工具调用触发一次提炼
|
|
15
|
+
this.minIntervalMs = Number(config.min_interval_ms) || 30000; // 两次提炼最小间隔
|
|
16
|
+
this.upgradeUses = Number(config.upgrade_uses) || 3; // 技能用满 N 次自动升级 (用中自进化)
|
|
17
|
+
this._calls = 0;
|
|
18
|
+
this._lastAt = 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 每轮对话收尾调用 (幂等, 轻量, 同步返回)
|
|
22
|
+
tick() {
|
|
23
|
+
if (!this.enabled) return;
|
|
24
|
+
const a = this.agent;
|
|
25
|
+
if (!a || !a.llm) return; // 无 LLM: 无法提炼
|
|
26
|
+
if (typeof a.refine !== "function") return;
|
|
27
|
+
this._calls++;
|
|
28
|
+
if (this._calls < this.everyCalls) return;
|
|
29
|
+
if (Date.now() - this._lastAt < this.minIntervalMs) return;
|
|
30
|
+
this._calls = 0;
|
|
31
|
+
this._lastAt = Date.now();
|
|
32
|
+
this._run(); // fire-and-forget, 不 await
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async _run() {
|
|
36
|
+
const a = this.agent;
|
|
37
|
+
try {
|
|
38
|
+
const traces = (typeof a.traces?.read === "function") ? a.traces.read(undefined, 50) : [];
|
|
39
|
+
const failed = traces.filter((t) => !t.ok);
|
|
40
|
+
const okCalls = traces.filter((t) => t.ok);
|
|
41
|
+
// 失败 → 提炼经验 (至少 2 条失败轨迹才有得教)
|
|
42
|
+
if (failed.length >= 2) {
|
|
43
|
+
const r = await a.refine({ limit: 20 });
|
|
44
|
+
if (r && r.distilled > 0) info(`[evolve] 失败→经验: ${String(r.lesson || "").slice(0, 120)}`);
|
|
45
|
+
}
|
|
46
|
+
// 重复成功 → 沉淀技能 (至少 3 条成功轨迹才有得沉淀)
|
|
47
|
+
if (okCalls.length >= 3) {
|
|
48
|
+
const r = await a.refineSkill({ limit: 50, minFreq: 2 });
|
|
49
|
+
if (r && r.created > 0) info(`[evolve] 成功→技能: ${r.name}`);
|
|
50
|
+
}
|
|
51
|
+
// 用中自进化: 扫描高频技能 (uses >= upgradeUses) 触发升级
|
|
52
|
+
if (typeof a.upgradeSkill === "function" && a.skills && typeof a.skills.usageAll === "function") {
|
|
53
|
+
try {
|
|
54
|
+
const usage = a.skills.usageAll() || {};
|
|
55
|
+
const ripe = Object.entries(usage).filter(([, u]) => (u && (u.uses || 0)) >= this.upgradeUses);
|
|
56
|
+
for (const [sid] of ripe) {
|
|
57
|
+
const r = await a.upgradeSkill(sid, { minUses: this.upgradeUses });
|
|
58
|
+
if (r && r.upgraded > 0) info(`[evolve] 用中升级技能: ${r.name}`);
|
|
59
|
+
}
|
|
60
|
+
} catch (e) {
|
|
61
|
+
warn(`[evolve] 技能升级失败: ${String(e?.message || e).slice(0, 120)}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch (e) {
|
|
65
|
+
warn(`[evolve] 提炼失败 (不影响当前对话): ${String(e?.message || e).slice(0, 160)}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// src/selfheal/healer.js - 自愈引擎
|
|
2
|
+
// 1. 启动检查: 修复缺失目录/损坏JSON/权限
|
|
3
|
+
// 2. 崩溃恢复: 检测上次异常退出, 清理残留
|
|
4
|
+
// 3. 数据一致性: 校验记忆文件
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { ensureDir, readJson } from "../utils/store.js";
|
|
8
|
+
import { info, warn, error } from "../utils/logger.js";
|
|
9
|
+
|
|
10
|
+
export class Healer {
|
|
11
|
+
constructor(rootDir) {
|
|
12
|
+
this.root = rootDir;
|
|
13
|
+
this.dataDir = path.join(rootDir, "data");
|
|
14
|
+
this.integrity = path.join(this.dataDir, "integrity.json");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 启动体检: 返回修复清单
|
|
18
|
+
runStartupChecks() {
|
|
19
|
+
const fixes = [];
|
|
20
|
+
ensureDir(this.dataDir);
|
|
21
|
+
for (const sub of ["memory", "memory/daily", "experience", "sessions", "logs"]) {
|
|
22
|
+
const d = path.join(this.dataDir, sub);
|
|
23
|
+
if (!fs.existsSync(d)) { fs.mkdirSync(d, { recursive: true }); fixes.push(`created dir: ${sub}`); }
|
|
24
|
+
}
|
|
25
|
+
// 校验关键 JSON 可解析
|
|
26
|
+
const facts = path.join(this.dataDir, "memory", "facts.json");
|
|
27
|
+
if (fs.existsSync(facts)) {
|
|
28
|
+
const parsed = readJson(facts, null);
|
|
29
|
+
if (parsed === null) {
|
|
30
|
+
// 损坏: 备份后重建
|
|
31
|
+
const bak = facts + ".corrupt-" + Date.now();
|
|
32
|
+
fs.renameSync(facts, bak);
|
|
33
|
+
fs.writeFileSync(facts, "[]", "utf8");
|
|
34
|
+
fixes.push(`facts.json corrupt -> backed up to ${path.basename(bak)}, reset`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return fixes;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 崩溃恢复: 检查上次是否干净退出
|
|
41
|
+
checkCrash() {
|
|
42
|
+
const state = readJson(this.integrity, { clean: true, pid: null });
|
|
43
|
+
const result = { crashed: false, detail: null };
|
|
44
|
+
if (state.clean === false) {
|
|
45
|
+
result.crashed = true;
|
|
46
|
+
result.detail = `上次进程 (pid=${state.pid}) 未干净退出, 可能残留临时文件`;
|
|
47
|
+
this._cleanupTmp();
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_cleanupTmp() {
|
|
53
|
+
const walk = (dir) => {
|
|
54
|
+
if (!fs.existsSync(dir)) return;
|
|
55
|
+
for (const f of fs.readdirSync(dir)) {
|
|
56
|
+
const p = path.join(dir, f);
|
|
57
|
+
let st;
|
|
58
|
+
try { st = fs.statSync(p); } catch { continue; } // 并发删除竞态容错
|
|
59
|
+
if (st.isDirectory()) walk(p);
|
|
60
|
+
else if (f.endsWith(".tmp")) { try { fs.unlinkSync(p); } catch {} info(`cleaned tmp: ${f}`); }
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
walk(this.dataDir);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
markDirty() {
|
|
67
|
+
ensureDir(this.dataDir);
|
|
68
|
+
fs.writeFileSync(this.integrity, JSON.stringify({ clean: false, pid: process.pid, ts: Date.now() }), "utf8");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
markClean() {
|
|
72
|
+
ensureDir(this.dataDir);
|
|
73
|
+
fs.writeFileSync(this.integrity, JSON.stringify({ clean: true, pid: process.pid, ts: Date.now() }), "utf8");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 清理历史 corrupt 备份: 保留最近 N 个, 更早自动删除 (默认保留 2)
|
|
77
|
+
cleanupCorruptBackups(keep = 2) {
|
|
78
|
+
const files = [];
|
|
79
|
+
const walk = (d) => {
|
|
80
|
+
if (!fs.existsSync(d)) return;
|
|
81
|
+
for (const f of fs.readdirSync(d)) {
|
|
82
|
+
const p = path.join(d, f);
|
|
83
|
+
let st;
|
|
84
|
+
try { st = fs.statSync(p); } catch { continue; }
|
|
85
|
+
if (st.isDirectory()) walk(p);
|
|
86
|
+
else if (f.includes(".corrupt-")) files.push({ p, mtime: st.mtimeMs });
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
walk(this.dataDir);
|
|
90
|
+
files.sort((a, b) => b.mtime - a.mtime);
|
|
91
|
+
const removed = [];
|
|
92
|
+
for (const f of files.slice(keep)) {
|
|
93
|
+
try { fs.unlinkSync(f.p); removed.push(path.basename(f.p)); }
|
|
94
|
+
catch (e) { warn("清理损坏备份失败: " + f.p + ": " + e.message); }
|
|
95
|
+
}
|
|
96
|
+
if (removed.length) info("selfheal: 清理旧 corrupt 备份 " + removed.length + " 个: " + removed.join(", "));
|
|
97
|
+
return removed;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 清理历史手动备份目录 (memory-backup-*): 保留最近 N 个, 更早自动删除 (默认保留 2)
|
|
101
|
+
// 手动全量备份目录不在 corrupt 备份机制内, 若无清理会持续累积磁盘占用
|
|
102
|
+
cleanupStaleBackupDirs(keep = 2) {
|
|
103
|
+
if (!fs.existsSync(this.dataDir)) return [];
|
|
104
|
+
const dirs = [];
|
|
105
|
+
for (const f of fs.readdirSync(this.dataDir)) {
|
|
106
|
+
if (!f.startsWith("memory-backup-")) continue;
|
|
107
|
+
const p = path.join(this.dataDir, f);
|
|
108
|
+
let st;
|
|
109
|
+
try { st = fs.statSync(p); } catch { continue; }
|
|
110
|
+
if (!st.isDirectory()) continue;
|
|
111
|
+
dirs.push({ p, mtime: st.mtimeMs });
|
|
112
|
+
}
|
|
113
|
+
dirs.sort((a, b) => b.mtime - a.mtime);
|
|
114
|
+
const removed = [];
|
|
115
|
+
for (const d of dirs.slice(keep)) {
|
|
116
|
+
try { fs.rmSync(d.p, { recursive: true, force: true }); removed.push(path.basename(d.p)); }
|
|
117
|
+
catch (e) { warn("清理过期备份目录失败: " + d.p + ": " + e.message); }
|
|
118
|
+
}
|
|
119
|
+
if (removed.length) info("selfheal: 清理旧手动备份目录 " + removed.length + " 个: " + removed.join(", "));
|
|
120
|
+
return removed;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 清理历史 .bak-* 文件 (去重/迁移等一次性工具留下的手动备份): 保留最近 N 个, 更早自动删除 (默认保留 2)
|
|
124
|
+
// 覆盖 facts.json.bak-* / lessons.json.bak-* 等, 防手动备份文件在 data/ 内无限累积
|
|
125
|
+
cleanupStaleBakFiles(keep = 2) {
|
|
126
|
+
if (!fs.existsSync(this.dataDir)) return [];
|
|
127
|
+
const files = [];
|
|
128
|
+
const walk = (d) => {
|
|
129
|
+
if (!fs.existsSync(d)) return;
|
|
130
|
+
for (const f of fs.readdirSync(d)) {
|
|
131
|
+
const p = path.join(d, f);
|
|
132
|
+
let st;
|
|
133
|
+
try { st = fs.statSync(p); } catch { continue; }
|
|
134
|
+
if (st.isDirectory()) walk(p);
|
|
135
|
+
else if (f.includes(".bak-") && !f.endsWith(".lock")) files.push({ p, mtime: st.mtimeMs });
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
walk(this.dataDir);
|
|
139
|
+
files.sort((a, b) => b.mtime - a.mtime);
|
|
140
|
+
const removed = [];
|
|
141
|
+
for (const f of files.slice(keep)) {
|
|
142
|
+
try { fs.unlinkSync(f.p); removed.push(path.basename(f.p)); }
|
|
143
|
+
catch (e) { warn("清理过期备份文件失败: " + f.p + ": " + e.message); }
|
|
144
|
+
}
|
|
145
|
+
if (removed.length) info("selfheal: 清理旧 .bak-* 备份文件 " + removed.length + " 个: " + removed.join(", "));
|
|
146
|
+
return removed;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 完整自愈入口
|
|
150
|
+
heal() {
|
|
151
|
+
const fixes = this.runStartupChecks();
|
|
152
|
+
const crash = this.checkCrash();
|
|
153
|
+
// 清理历史 corrupt 备份 (保留最近 2 个, 更早自动删除) — 之前漏调用导致 corrupt 持续累积
|
|
154
|
+
const cleanedCorrupt = this.cleanupCorruptBackups(2);
|
|
155
|
+
// 清理历史手动备份目录 (保留最近 2 个)
|
|
156
|
+
const cleanedBackupDirs = this.cleanupStaleBackupDirs(2);
|
|
157
|
+
// 清理历史 .bak-* 文件 (保留最近 2 个)
|
|
158
|
+
const cleanedBakFiles = this.cleanupStaleBakFiles(2);
|
|
159
|
+
// 崩溃残留清理后主动翻回 clean: 自愈闭环 - 清了毒就要"宣布痊愈", 否则下次启动还误报崩溃
|
|
160
|
+
if (crash.crashed) { this.markClean(); info("selfheal: 崩溃残留已清理, 状态置回 clean"); }
|
|
161
|
+
const report = { fixes, crashed: crash.crashed, crashDetail: crash.detail, cleanedCorrupt, cleanedBackupDirs, cleanedBakFiles };
|
|
162
|
+
if (fixes.length) info(`selfheal: 修复 ${fixes.length} 项: ${fixes.join("; ")}`);
|
|
163
|
+
else info("selfheal: 无异常");
|
|
164
|
+
if (crash.crashed) warn(`selfheal: 检测到崩溃残留 -> ${crash.detail}`);
|
|
165
|
+
return report;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// src/selfheal/run.js - 自愈命令行入口
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { Healer } from "./healer.js";
|
|
5
|
+
|
|
6
|
+
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
7
|
+
const healer = new Healer(root);
|
|
8
|
+
const report = healer.heal();
|
|
9
|
+
console.log(JSON.stringify(report, null, 2));
|
package/src/server.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// src/server.js - 皮皮虾 HTTP 服务入口
|
|
3
|
+
// 提供: /health, /message (HTTP通道), /feishu/webhook, /wechat/webhook
|
|
4
|
+
// 通道统一走 ChannelManager 注册表: connect + webhook 挂载由各通道 mount() 完成
|
|
5
|
+
import { ensureUTF8Console } from "./utils/winutf8.js";
|
|
6
|
+
import { PPXAgent } from "./agent/index.js";
|
|
7
|
+
import { ChannelManager } from "./channels/index.js";
|
|
8
|
+
|
|
9
|
+
ensureUTF8Console();
|
|
10
|
+
export async function startServer({ root = process.cwd(), port = 8899, host = "127.0.0.1", config = {}, llm = null } = {}) {
|
|
11
|
+
const agent = new PPXAgent({ root });
|
|
12
|
+
if (llm) {
|
|
13
|
+
// 测试注入 stub LLM, 避免依赖真实网络; 同步把 allProviders 替换为同源 stub,
|
|
14
|
+
// 让 /api/providers/test 等"按 id 取客户端"的路径也能命中 stub
|
|
15
|
+
agent.llm = llm;
|
|
16
|
+
if (Array.isArray(agent.allProviders)) agent.allProviders = [llm];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 统一走注册表: HTTP 通道始终启动 (port/host 参数覆盖配置), 其余按 config.channels
|
|
20
|
+
// 基础配置来自 agent.config (config/ppx.json 加载的), 调用方传入的 config.channels 只做覆盖
|
|
21
|
+
const baseChannels = agent.config.channels || {};
|
|
22
|
+
const channelsCfg = { ...baseChannels, ...((config.channels) || {}) };
|
|
23
|
+
channelsCfg.http = { ...(channelsCfg.http || {}), enabled: true, port, host }; // port/host 参数最高优先级
|
|
24
|
+
const manager = new ChannelManager(agent, channelsCfg);
|
|
25
|
+
await manager.start();
|
|
26
|
+
|
|
27
|
+
// 主动提醒 → 广播到所有已启用通道 (agent.proactive.enabled 才接; 默认关不打扰)
|
|
28
|
+
if (agent.config.agent?.proactive?.enabled) {
|
|
29
|
+
agent.startProactiveTicker((payload) => manager.broadcast(payload.text));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const server = manager.httpServer;
|
|
33
|
+
return {
|
|
34
|
+
agent,
|
|
35
|
+
server,
|
|
36
|
+
http: manager.get("http"),
|
|
37
|
+
feishu: manager.get("feishu"),
|
|
38
|
+
wechat: manager.get("wechat"),
|
|
39
|
+
manager,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// CLI 启动公共逻辑: 供 bin/ppx-serve 与 src/server.js 直接运行共用
|
|
44
|
+
export async function runServer({ root = process.cwd(), port = Number(process.env.PPX_PORT || 8899) } = {}) {
|
|
45
|
+
const { agent, server, manager } = await startServer({ root, port });
|
|
46
|
+
console.log(`皮皮虾 服务已启动: http://127.0.0.1:${port}`);
|
|
47
|
+
console.log(` /health 健康检查`);
|
|
48
|
+
console.log(` /message HTTP 对话通道 (POST {message})`);
|
|
49
|
+
console.log(` 通道: ${manager.list().filter((c) => c.enabled).map((c) => c.name).join(", ") || "(无)"}`);
|
|
50
|
+
console.log(` 工具: ${agent.tools.list().join(", ")}`);
|
|
51
|
+
console.log(" Ctrl+C 退出");
|
|
52
|
+
process.on("SIGINT", () => { agent.shutdown(); process.exit(0); });
|
|
53
|
+
return { agent, server, manager };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 直接运行 (node src/server.js, scripts/start-web.js)
|
|
57
|
+
const _entry = (process.argv[1] || "").replace(/\\/g, "/");
|
|
58
|
+
if (_entry.endsWith("src/server.js")) {
|
|
59
|
+
await runServer({ root: process.cwd() });
|
|
60
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// src/services/learning-service.js - 自我学习服务 (重构第二刀, 2026-09-14)
|
|
2
|
+
// 目的: 把 PPXAgent 上的自我进化闭环 (失败→经验 refine / 成功→技能 refineSkill /
|
|
3
|
+
// 使用中进化 upgradeSkill) 抽为独立服务, agent 只保留薄委托。
|
|
4
|
+
// 设计:
|
|
5
|
+
// - 依赖全部注入: llm (getLlm 闭包, 实时取当前 provider) / traces (工具轨迹) /
|
|
6
|
+
// skills (技能加载器) / experience (经验库) / lifecycle (进化计数) /
|
|
7
|
+
// auditor (验证账本) / tracer (事件流) / toolNames (工具名清单, 接地防幻觉) /
|
|
8
|
+
// runTool (create_skill 工具调用, 由 agent 提供 agent 上下文)。
|
|
9
|
+
// - 验证闸门 (verifyLesson / verifySkill / verifyUpgradeSkill) 保持不动:
|
|
10
|
+
// 经验/技能必须过确定性闸门才写回, 不信任模型自评。
|
|
11
|
+
import { TOOL_ERROR_PREFIX } from "../tools/index.js";
|
|
12
|
+
import { info, warn } from "../utils/logger.js";
|
|
13
|
+
import { Auditor, verifyLesson, heldOutSplit } from "../audit/verifier.js";
|
|
14
|
+
import { verifySkill, verifyUpgradeSkill } from "../skills/verify.js";
|
|
15
|
+
|
|
16
|
+
// 辅助 LLM 调用短超时 (经验/技能提炼): 模型不可用/网络不通时快速失败降级
|
|
17
|
+
const AUX_LLM_TIMEOUT_MS = 10000;
|
|
18
|
+
|
|
19
|
+
export class LearningService {
|
|
20
|
+
// deps: { getLlm, traces, skills, experience, lifecycle, auditor, tracer, toolNames, runTool }
|
|
21
|
+
constructor(deps) {
|
|
22
|
+
this.getLlm = deps.getLlm; // () => llm
|
|
23
|
+
this.traces = deps.traces; // 工具调用轨迹 (Traces)
|
|
24
|
+
this.skills = deps.skills; // SkillLoader
|
|
25
|
+
this.experience = deps.experience; // 经验库
|
|
26
|
+
this.lifecycle = deps.lifecycle; // ANS 生命周期
|
|
27
|
+
this.auditor = deps.auditor; // Auditor (验证账本)
|
|
28
|
+
this.tracer = deps.tracer; // 结构化事件流
|
|
29
|
+
this.toolNames = deps.toolNames; // () => string[], 工具名清单 (verifyLesson 接地)
|
|
30
|
+
this.runTool = deps.runTool; // (name, args) => Promise<string>, create_skill 调用
|
|
31
|
+
if (!(this.auditor instanceof Auditor)) {
|
|
32
|
+
// 兼容: 外部可能传普通对象 (测试/轻量装配), 不强求 Auditor 实例
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_llm() { return this.getLlm ? this.getLlm() : null; }
|
|
37
|
+
|
|
38
|
+
// 失败→经验: 回放近期失败轨迹, LLM 提炼经验教训进经验库 (原 agent.refine)
|
|
39
|
+
async refine({ limit = 20 } = {}) {
|
|
40
|
+
const llm = this._llm();
|
|
41
|
+
if (!llm) return { distilled: 0, reason: "无 LLM" };
|
|
42
|
+
const failed = this.traces.read(undefined, limit).filter((t) => !t.ok);
|
|
43
|
+
if (failed.length < 2) return { distilled: 0, reason: "失败轨迹不足" };
|
|
44
|
+
const summary = failed
|
|
45
|
+
.map((t) => `工具 ${t.tool}: ${String(t.error || t.result || "").slice(0, 160)}`)
|
|
46
|
+
.join("\n");
|
|
47
|
+
let lesson;
|
|
48
|
+
try {
|
|
49
|
+
const r = await llm.chat([
|
|
50
|
+
{ role: "system", content: "你是经验提炼器。从失败的工具调用轨迹中提炼一条可复用的经验教训, 一句话说清: 什么场景、为什么失败、下次怎么做。只输出这一句话, 不要解释。" },
|
|
51
|
+
{ role: "user", content: summary.slice(0, 2000) },
|
|
52
|
+
], { timeoutMs: AUX_LLM_TIMEOUT_MS, retryMax: 0 });
|
|
53
|
+
lesson = String(r.content || "").trim();
|
|
54
|
+
} catch { lesson = ""; }
|
|
55
|
+
if (!lesson) return { distilled: 0, reason: "LLM 未产出经验" };
|
|
56
|
+
// Auditor: 经验必须过确定性验证闸门才写回经验库 (不信任模型自评)
|
|
57
|
+
// 接地防幻觉(点名工具须有失败轨迹背书) + 可操作动词 + 单句精炼
|
|
58
|
+
const knownTools = this.toolNames ? this.toolNames() : [];
|
|
59
|
+
const g = await this.auditor.gate(
|
|
60
|
+
"lesson",
|
|
61
|
+
{ lesson, failedTraces: failed, knownTools },
|
|
62
|
+
verifyLesson,
|
|
63
|
+
(p) => {
|
|
64
|
+
this.experience.learn({ task: "自动提炼", lesson: p.lesson, tags: ["auto-refine"] });
|
|
65
|
+
if (this.lifecycle) this.lifecycle.evolve(); // 生命周期: 进化计数 (落盘)
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
if (!g.committed) {
|
|
69
|
+
warn(`[refine] 经验被验证闸门拒绝 (${g.reason}): ${lesson.slice(0, 80)}`);
|
|
70
|
+
return { distilled: 0, rejected: true, reason: g.reason, lesson };
|
|
71
|
+
}
|
|
72
|
+
info(`[refine] 学到经验: ${lesson}`);
|
|
73
|
+
this.tracer?.event("learning/refine", { lesson: lesson.slice(0, 120) });
|
|
74
|
+
return { distilled: 1, lesson };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 成功→技能: 从成功轨迹自动提炼可复用 Skill (原 agent.refineSkill)
|
|
78
|
+
// 轨迹 → 高频成功工具模式 → LLM 提炼 → create_skill 落盘 skills/<name>/SKILL.md
|
|
79
|
+
// 与 refine() (失败→经验) 互补, 形成「失败学教训 + 成功沉淀技能」完整闭环
|
|
80
|
+
async refineSkill({ limit = 50, minFreq = 2 } = {}) {
|
|
81
|
+
const llm = this._llm();
|
|
82
|
+
if (!llm) return { created: 0, reason: "无 LLM" };
|
|
83
|
+
const ok = this.traces.read(undefined, limit).filter((t) => t.ok);
|
|
84
|
+
if (ok.length < minFreq) return { created: 0, reason: "成功轨迹不足" };
|
|
85
|
+
// 找高频成功工具 (出现 >= minFreq 次)
|
|
86
|
+
const freq = {};
|
|
87
|
+
for (const t of ok) freq[t.tool] = (freq[t.tool] || 0) + 1;
|
|
88
|
+
const hot = Object.entries(freq).filter(([, n]) => n >= minFreq).map(([t]) => t);
|
|
89
|
+
if (!hot.length) return { created: 0, reason: "无重复成功工具模式" };
|
|
90
|
+
// 用 LLM 提炼 skill (name/description/content)
|
|
91
|
+
const summary = ok.slice(-20).map((t) => `工具 ${t.tool}: ${String(t.result || "").slice(0, 80)}`).join("\n");
|
|
92
|
+
let skill;
|
|
93
|
+
try {
|
|
94
|
+
const r = await llm.chat([
|
|
95
|
+
{ role: "system", content: "你是技能提炼器。根据成功的工具调用轨迹, 提炼一个可复用技能。只输出 JSON: {\"name\":\"技能名(仅字母数字横线)\",\"description\":\"一句话说明\",\"content\":\"SKILL正文, 含 ## 流程(逐步工作流) 和 ## 验证(完成后必须提供的证据)\"}。不要解释, 只输出 JSON。" },
|
|
96
|
+
{ role: "user", content: `高频工具: ${hot.join(", ")}\n成功轨迹:\n${summary.slice(0, 2000)}` },
|
|
97
|
+
], { timeoutMs: AUX_LLM_TIMEOUT_MS, retryMax: 0 });
|
|
98
|
+
const text = String(r.content || "").trim().replace(/```(?:json|JSON)?\s*/g, "").replace(/```/g, "").trim();
|
|
99
|
+
const m = text.match(/\{[\s\S]*\}/);
|
|
100
|
+
if (!m) return { created: 0, reason: "LLM 未产出有效 JSON" };
|
|
101
|
+
skill = JSON.parse(m[0]);
|
|
102
|
+
} catch { return { created: 0, reason: "LLM 提炼失败" }; }
|
|
103
|
+
if (!skill || !skill.content) return { created: 0, reason: "Skill 字段缺失" };
|
|
104
|
+
// name 归一化: 仅字母/数字/横线, 非法字符剔除, 空则兜底
|
|
105
|
+
const name = String(skill.name || "").replace(/[^a-zA-Z0-9-]/g, "").toLowerCase() || ("auto-" + Date.now().toString(36));
|
|
106
|
+
|
|
107
|
+
// self-evolution "reliable verification": gate before persist (no LLM)
|
|
108
|
+
// -> structure (## Process + ## Verify) + grounded (content references hot tool, trace-backed)
|
|
109
|
+
// held-out 回归: 样本够多时切出未见过的 held-out 子集, 要求接地工具在那也有背书, 防过拟合
|
|
110
|
+
const { heldOut } = heldOutSplit(ok, { ratio: 0.4, minTotal: 6 });
|
|
111
|
+
const v = verifySkill({
|
|
112
|
+
name,
|
|
113
|
+
content: String(skill.content || ""),
|
|
114
|
+
hotTools: hot,
|
|
115
|
+
okTraces: ok,
|
|
116
|
+
minFreq,
|
|
117
|
+
heldOutTraces: heldOut.length ? heldOut : undefined,
|
|
118
|
+
});
|
|
119
|
+
if (!v.ok) {
|
|
120
|
+
warn("[refineSkill] skill rejected by verify gate: " + name + " - " + v.reason);
|
|
121
|
+
return { created: 0, reason: v.reason, rejected: true, name };
|
|
122
|
+
}
|
|
123
|
+
const res = await this.runTool("create_skill", {
|
|
124
|
+
name,
|
|
125
|
+
description: String(skill.description || "自动提炼的技能"),
|
|
126
|
+
content: String(skill.content),
|
|
127
|
+
});
|
|
128
|
+
if (res.startsWith(TOOL_ERROR_PREFIX)) return { created: 0, reason: res };
|
|
129
|
+
if (this.auditor) this.auditor.record("skill_created", { lesson: `创建技能 ${name}` }); // 已过 verifySkill 闸门, 只记账
|
|
130
|
+
info(`[refineSkill] 生成技能: ${name}`);
|
|
131
|
+
this.tracer?.event("learning/refine_skill", { name });
|
|
132
|
+
return { created: 1, name };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 使用中进化: 技能用满 minUses 次后, 读当前内容 + 相关成功轨迹,
|
|
136
|
+
// LLM 改进 SKILL.md, 过 verifyUpgradeSkill 闸门(防退化) 后写回, 重置计数防连跑 (原 agent.upgradeSkill)
|
|
137
|
+
async upgradeSkill(id, { minUses = 3, limit = 40 } = {}) {
|
|
138
|
+
try {
|
|
139
|
+
const llm = this._llm();
|
|
140
|
+
if (!llm) return { upgraded: 0, reason: "无 LLM" };
|
|
141
|
+
if (!this.skills || typeof this.skills.read !== "function") return { upgraded: 0, reason: "无 skills加载器" };
|
|
142
|
+
const need = this.skills.get(id);
|
|
143
|
+
if (!need) return { upgraded: 0, reason: "未知技能: " + id };
|
|
144
|
+
const used = this.skills.useOf ? this.skills.useOf(id) : { uses: 0 };
|
|
145
|
+
if ((used.uses || 0) < minUses) return { upgraded: 0, reason: "使用不足", uses: used.uses, need: minUses, name: id };
|
|
146
|
+
const prev = this.skills.read(id);
|
|
147
|
+
if (prev === null) return { upgraded: 0, reason: "读取失败", name: id };
|
|
148
|
+
const ok = this.traces && typeof this.traces.read === "function" ? this.traces.read(undefined, limit).filter((t) => t.ok) : [];
|
|
149
|
+
const sample = ok.slice(-15).map((t) => "工具 " + t.tool + ": " + String(t.result || "").slice(0, 120)).join("\n");
|
|
150
|
+
let upgraded;
|
|
151
|
+
try {
|
|
152
|
+
const r = await llm.chat([
|
|
153
|
+
{ role: "system", content: "你是技能升级器。下面是一个已存在的技能全文 + 最近的成功工具轨迹。你的任务: 基于这些实际经验改进这个技能, 补充它的“## 流程”工作步骤/检查点和“## 反合理化”建议, 切勿删除“## 验证”段。只输出改进后的 SKILL.md 正文 (frontmatter 不用重复), 不要解释。" },
|
|
154
|
+
{ role: "user", content: "当前技能 (保留效果, 改进不足):\n\n" + String(prev).slice(0, 3000) + "\n\n最近成功轨迹:\n" + String(sample).slice(0, 2000) },
|
|
155
|
+
], { timeoutMs: AUX_LLM_TIMEOUT_MS, retryMax: 0 });
|
|
156
|
+
upgraded = String(r.content || "").trim().replace(/```(?:md|markdown)?\s*/g, "").replace(/```/g, "").trim();
|
|
157
|
+
} catch {
|
|
158
|
+
return { upgraded: 0, reason: "LLM 升级失败", name: id };
|
|
159
|
+
}
|
|
160
|
+
if (!upgraded) return { upgraded: 0, reason: "空升级结果", name: id };
|
|
161
|
+
const v = verifyUpgradeSkill({ content: upgraded, prevContent: prev });
|
|
162
|
+
if (!v.ok) {
|
|
163
|
+
warn("[upgradeSkill] 被升级闸门拦截: " + id + " - " + v.reason);
|
|
164
|
+
return { upgraded: 0, reason: v.reason, rejected: true, name: id };
|
|
165
|
+
}
|
|
166
|
+
const w = await this.runTool("create_skill", { name: id, description: (need.description || ""), content: upgraded });
|
|
167
|
+
if (typeof w === "string" && w.startsWith(TOOL_ERROR_PREFIX)) return { upgraded: 0, reason: w, name: id };
|
|
168
|
+
if (this.skills.resetUse) this.skills.resetUse(id);
|
|
169
|
+
info("[upgradeSkill] 升级技能: " + id + " (uses=" + used.uses + ")");
|
|
170
|
+
this.tracer?.event("learning/upgrade_skill", { id, uses: used.uses });
|
|
171
|
+
return { upgraded: 1, name: id, changed: v.changed, uses: used.uses };
|
|
172
|
+
} catch (e) {
|
|
173
|
+
warn("[upgradeSkill] 失败: " + String(e?.message || e).slice(0, 120));
|
|
174
|
+
return { upgraded: 0, reason: String(e?.message || e).slice(0, 120), name: id };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// src/services/memory-health.js - 记忆管线健康监控 (P1⑤)
|
|
2
|
+
// 吸收 HanaAgent (openhanako) 的记忆管线健康监控设计:
|
|
3
|
+
// healthy / degraded / unhealthy 三态 + 分步失败计数 + 降级策略。
|
|
4
|
+
// 皮皮虾自研实现: 监控记忆管线各步 (滚动归档 roll / 压缩 compact / LLM 提炼 extract),
|
|
5
|
+
// 通过 wrap() 包裹原方法统计成败; 不健康时给出降级建议 (只写不压, 防上下文污染)。
|
|
6
|
+
// 纯代码可测, 不依赖 LLM。
|
|
7
|
+
export const HEALTH = { HEALTHY: "healthy", DEGRADED: "degraded", UNHEALTHY: "unhealthy" };
|
|
8
|
+
|
|
9
|
+
export class MemoryHealthMonitor {
|
|
10
|
+
constructor({ degradeAfter = 3, windowMs = 5 * 60 * 1000 } = {}) {
|
|
11
|
+
this.degradeAfter = degradeAfter; // 连续失败多少次进入 degraded
|
|
12
|
+
this.windowMs = windowMs; // 统计窗口 (默认 5 分钟)
|
|
13
|
+
this.steps = new Map(); // stepName -> { ok, fail, lastError, lastAt, fails: [] }
|
|
14
|
+
this.degraded = false; // 降级开关 (只写不压)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
_step(name) {
|
|
18
|
+
if (!this.steps.has(name)) {
|
|
19
|
+
this.steps.set(name, { ok: 0, fail: 0, lastError: null, lastAt: 0, fails: [] });
|
|
20
|
+
}
|
|
21
|
+
return this.steps.get(name);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 记录一步成功/失败
|
|
25
|
+
record(name, { ok = true, error = null } = {}) {
|
|
26
|
+
const s = this._step(name);
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
s.ok += ok ? 1 : 0;
|
|
29
|
+
s.lastAt = now;
|
|
30
|
+
if (!ok) {
|
|
31
|
+
s.fail++;
|
|
32
|
+
s.lastError = error || "unknown";
|
|
33
|
+
s.fails.push(now);
|
|
34
|
+
// 滑动窗口裁剪
|
|
35
|
+
s.fails = s.fails.filter((t) => now - t < this.windowMs);
|
|
36
|
+
} else {
|
|
37
|
+
s.fails = [];
|
|
38
|
+
}
|
|
39
|
+
this._recompute();
|
|
40
|
+
return s;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 连续失败 -> degraded (滑动窗口内失败数 >= degradeAfter)
|
|
44
|
+
_recompute() {
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
let worst = 0;
|
|
47
|
+
for (const s of this.steps.values()) {
|
|
48
|
+
const recent = s.fails.filter((t) => now - t < this.windowMs).length;
|
|
49
|
+
if (recent > worst) worst = recent;
|
|
50
|
+
}
|
|
51
|
+
this.degraded = worst >= this.degradeAfter;
|
|
52
|
+
return this.degraded;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 包装原方法: 自动统计成败; ok 判定可自定义 (默认不抛错即成功)
|
|
56
|
+
wrap(name, fn, { isOk = null } = {}) {
|
|
57
|
+
return async (...args) => {
|
|
58
|
+
try {
|
|
59
|
+
const r = await fn(...args);
|
|
60
|
+
const good = isOk ? isOk(r) : true;
|
|
61
|
+
this.record(name, { ok: good });
|
|
62
|
+
return r;
|
|
63
|
+
} catch (e) {
|
|
64
|
+
this.record(name, { ok: false, error: e?.message || String(e) });
|
|
65
|
+
throw e;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 状态汇总
|
|
71
|
+
status() {
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
const stepList = [...this.steps.entries()].map(([name, s]) => ({
|
|
74
|
+
name,
|
|
75
|
+
ok: s.ok,
|
|
76
|
+
fail: s.fail,
|
|
77
|
+
lastError: s.lastError,
|
|
78
|
+
recentFails: s.fails.filter((t) => now - t < this.windowMs).length,
|
|
79
|
+
}));
|
|
80
|
+
const totalFail = stepList.reduce((a, s) => a + s.fail, 0);
|
|
81
|
+
const overall = this.degraded
|
|
82
|
+
? HEALTH.DEGRADED
|
|
83
|
+
: (totalFail === 0 ? HEALTH.HEALTHY : HEALTH.HEALTHY);
|
|
84
|
+
return { overall, degraded: this.degraded, steps: stepList, updatedAt: now };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 降级建议 (供 memory ticker / agent 消费)
|
|
88
|
+
advice() {
|
|
89
|
+
if (!this.degraded) return { action: "normal" };
|
|
90
|
+
return {
|
|
91
|
+
action: "degrade",
|
|
92
|
+
reason: "记忆管线连续失败, 建议降级为只写不压",
|
|
93
|
+
// 只写不压: 跳过 LLM 压缩/提炼, 仅保留原始会话写入
|
|
94
|
+
skip: ["compact", "extract"],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export default MemoryHealthMonitor;
|