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,108 @@
|
|
|
1
|
+
// src/utils/store.js - 零依赖文件存储 (JSON + 原子写)
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
|
|
6
|
+
export function ensureDir(dir) {
|
|
7
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
8
|
+
return dir;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function atomicWrite(file, data) {
|
|
12
|
+
ensureDir(path.dirname(file));
|
|
13
|
+
const tmp = file + "." + crypto.randomBytes(4).toString("hex") + ".tmp";
|
|
14
|
+
fs.writeFileSync(tmp, data, "utf8");
|
|
15
|
+
// v1.0.8: rename 覆盖已存在文件在 Windows 并发下可能 EPERM/EEXIST (短窗口), 重试 3 次;
|
|
16
|
+
// 不再降级为非原子直接写 (并发双写可交错损坏文件), 重试仍失败则抛错由调用方处理
|
|
17
|
+
for (let attempt = 0; ; attempt++) {
|
|
18
|
+
try {
|
|
19
|
+
fs.renameSync(tmp, file);
|
|
20
|
+
return;
|
|
21
|
+
} catch (e) {
|
|
22
|
+
if (attempt >= 2) {
|
|
23
|
+
try { fs.unlinkSync(tmp); } catch {}
|
|
24
|
+
throw new Error(`原子写失败: ${file} (${e.message})`);
|
|
25
|
+
}
|
|
26
|
+
const end = Date.now() + 30;
|
|
27
|
+
while (Date.now() < end) {} // 短延迟后重试
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function readJson(file, fallback = null) {
|
|
33
|
+
try {
|
|
34
|
+
if (!fs.existsSync(file)) return fallback;
|
|
35
|
+
let raw = fs.readFileSync(file, "utf8");
|
|
36
|
+
if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1); // 去 BOM
|
|
37
|
+
return JSON.parse(raw);
|
|
38
|
+
} catch {
|
|
39
|
+
return fallback;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function writeJson(file, obj) {
|
|
44
|
+
atomicWrite(file, JSON.stringify(obj, null, 2));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 简单跨进程文件锁 (零依赖, 同步): 原子创建 .lock 文件, 临界区内执行 fn, finally 释放
|
|
48
|
+
// 用于跨 agent 共享文件的"读-改-写"临界区 (防并发覆盖, 如共享经验库)
|
|
49
|
+
// 锁是同步 busy-wait (低频写场景可接受); 超时视为陈旧锁 (崩溃残留) 强取
|
|
50
|
+
export function withFileLock(file, fn, { timeoutMs = 3000, pollMs = 20 } = {}) {
|
|
51
|
+
const lock = file + ".lock";
|
|
52
|
+
const acquire = () => {
|
|
53
|
+
try {
|
|
54
|
+
const fd = fs.openSync(lock, "wx"); // 'wx': 已存在则抛错 (原子)
|
|
55
|
+
try { fs.writeSync(fd, String(process.pid)); } catch {}
|
|
56
|
+
fs.closeSync(fd);
|
|
57
|
+
return true;
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const start = Date.now();
|
|
63
|
+
while (!acquire()) {
|
|
64
|
+
if (Date.now() - start > timeoutMs) {
|
|
65
|
+
try { fs.rmSync(lock, { force: true }); } catch {} // 陈旧锁强取
|
|
66
|
+
if (!acquire()) throw new Error("文件锁获取超时: " + lock);
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
const end = Date.now() + pollMs;
|
|
70
|
+
while (Date.now() < end) {} // 同步短暂等待
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
return fn();
|
|
74
|
+
} finally {
|
|
75
|
+
try { fs.rmSync(lock, { force: true }); } catch {}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function readText(file, fallback = "") {
|
|
80
|
+
try {
|
|
81
|
+
if (!fs.existsSync(file)) return fallback;
|
|
82
|
+
return fs.readFileSync(file, "utf8");
|
|
83
|
+
} catch {
|
|
84
|
+
return fallback;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function writeText(file, text) {
|
|
89
|
+
atomicWrite(file, text);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function appendLine(file, line) {
|
|
93
|
+
ensureDir(path.dirname(file));
|
|
94
|
+
fs.appendFileSync(file, line + "\n", "utf8");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function nowISO() {
|
|
98
|
+
return new Date().toISOString();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function logicalDay(d = new Date()) {
|
|
102
|
+
// 逻辑日 (本地时区): 今日/归档/按天检索统一用本地年月日
|
|
103
|
+
// 与 eventsByDay() 的本地解析保持一致, 避免 8 小时时区偏移导致按天检索错位
|
|
104
|
+
const y = d.getFullYear();
|
|
105
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
106
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
107
|
+
return `${y}-${m}-${day}`;
|
|
108
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// src/utils/text.js - 零依赖文本工具 (token 估算等, 供 agent/llm 等共用)
|
|
2
|
+
// 单一实现, 避免 agent 与 llm 各自维护一份 token 估算导致口径漂移。
|
|
3
|
+
|
|
4
|
+
// 估算 token 数: 中文字符约 1 字 ≈ 0.6 token, 1 token ≈ 4 字符; 统一按字符数 /1.6 估。
|
|
5
|
+
export function estimateTokens(s) {
|
|
6
|
+
return Math.ceil(String(s || "").length / 1.6);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// 按 token 预算截断字符串: 保留头部信息 (供 persona/角色设定等需保留 key 信息的场景),
|
|
10
|
+
// 超预算时在尾部标注截断来源与总量。与 estimateTokens 同口径 (字符/1.6)。
|
|
11
|
+
export function truncateByTokens(s, budget) {
|
|
12
|
+
const str = String(s || "");
|
|
13
|
+
const maxLen = Math.floor(budget * 1.6);
|
|
14
|
+
if (str.length <= maxLen) return str;
|
|
15
|
+
return str.slice(0, maxLen) + `\n...[persona 已按预算截断, 共 ${str.length} 字符]...`;
|
|
16
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// src/utils/trace.js - 结构化可观测轨迹 (Harness 第四层)
|
|
2
|
+
// 记录每次工具调用的完整轨迹: 工具/参数/结果摘要/耗时/成败
|
|
3
|
+
// 写 JSONL 到 data/logs/traces/YYYY-MM-DD.jsonl, 供事后复盘"哪一步坏了"
|
|
4
|
+
// v1.0.8: args/result 落盘前 PII 脱敏 (凭证不写日志); read(day) 支持按指定日期读取
|
|
5
|
+
// P0 (2026-09-15): 事件源不变量 (model-visible = logged) ——
|
|
6
|
+
// turn/step 边界事件落同一 JSONL + verifyReplay() 重放校验 (配对完整性/顺序性)。
|
|
7
|
+
// 对齐 dsh "会话日志是模型可见内容的唯一事实源" 思想, 用皮皮虾自有结构增量实现。
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { ensureDir, logicalDay } from "./store.js";
|
|
11
|
+
import { scrubPII } from "./pii.js";
|
|
12
|
+
|
|
13
|
+
const MAX_TRACE_BODY = 2000; // 单条结果保留上限, 防爆文件
|
|
14
|
+
|
|
15
|
+
// 事件类型常量 (turn/step 边界)
|
|
16
|
+
export const EVT = {
|
|
17
|
+
TURN_START: "turn/start",
|
|
18
|
+
TURN_END: "turn/end",
|
|
19
|
+
STEP_START: "step/start",
|
|
20
|
+
STEP_END: "step/end",
|
|
21
|
+
TOOL: "tool/call",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export class Traces {
|
|
25
|
+
constructor(dataDir) {
|
|
26
|
+
this.dir = path.join(dataDir, "logs", "traces");
|
|
27
|
+
ensureDir(this.dir);
|
|
28
|
+
this.sessionId = "s_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
29
|
+
this.count = 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_file(day = logicalDay()) { return path.join(this.dir, `${day}.jsonl`); }
|
|
33
|
+
|
|
34
|
+
// 记录一次工具调用轨迹
|
|
35
|
+
record({ tool, args, result, ok, durationMs, error }) {
|
|
36
|
+
const entry = this._entry({ evt: EVT.TOOL, tool, args, result, ok, durationMs, error });
|
|
37
|
+
fs.appendFileSync(this._file(), JSON.stringify(entry) + "\n", "utf8");
|
|
38
|
+
return entry;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---- P0: turn/step 边界事件 (事件源事实) ----
|
|
42
|
+
recordTurnStart({ sessionId, reason } = {}) {
|
|
43
|
+
return this._append({ evt: EVT.TURN_START, tsIso: new Date().toISOString(), reason: reason || "user", sessionId: sessionId || this.sessionId });
|
|
44
|
+
}
|
|
45
|
+
recordTurnEnd({ ok = true, rounds = 0, reason } = {}) {
|
|
46
|
+
return this._append({ evt: EVT.TURN_END, ok: !!ok, rounds, reason: reason || null });
|
|
47
|
+
}
|
|
48
|
+
recordStepStart({ round = 0, context } = {}) {
|
|
49
|
+
// context 快照 = 该 step 模型看到的上下文投影摘要 (验证 model-visible = logged)
|
|
50
|
+
const safe = context ? scrubPII(JSON.stringify(context)).cleaned : null;
|
|
51
|
+
return this._append({ evt: EVT.STEP_START, round, context: safe ? this._truncate(safe) : null });
|
|
52
|
+
}
|
|
53
|
+
recordStepEnd({ round = 0, ok = true, tool = null, error } = {}) {
|
|
54
|
+
return this._append({ evt: EVT.STEP_END, round, ok: !!ok, tool: tool || null, error: error || null });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
_append(extra) {
|
|
58
|
+
this.count += 1;
|
|
59
|
+
const entry = { ...this._base(), ...extra };
|
|
60
|
+
fs.appendFileSync(this._file(), JSON.stringify(entry) + "\n", "utf8");
|
|
61
|
+
return entry;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_base() {
|
|
65
|
+
return { ts: new Date().toISOString(), sessionId: this.sessionId, seq: this.count };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_entry({ evt, tool, args, result, ok, durationMs, error }) {
|
|
69
|
+
const safeArgs = scrubPII(JSON.stringify(args ?? {})).cleaned;
|
|
70
|
+
const safeResult = scrubPII(String(result ?? "")).cleaned;
|
|
71
|
+
return {
|
|
72
|
+
...this._base(),
|
|
73
|
+
evt: evt || EVT.TOOL,
|
|
74
|
+
tool,
|
|
75
|
+
args: this._truncate(safeArgs),
|
|
76
|
+
result: this._truncate(safeResult),
|
|
77
|
+
ok: !!ok,
|
|
78
|
+
durationMs: Math.round(durationMs || 0),
|
|
79
|
+
error: error || null,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
_truncate(s) {
|
|
84
|
+
const str = String(s ?? "");
|
|
85
|
+
return str.length > MAX_TRACE_BODY ? str.slice(0, MAX_TRACE_BODY) + "…" : str;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---- P0: 事件源不变量断言 (model-visible = logged) ----
|
|
89
|
+
// 重放校验: 1) 每行可解析 2) turn/step 配对完整 (start 必有 end, 嵌套合法) 3) step round 递增无重复
|
|
90
|
+
// 返回 { ok, total, errors: [] }。任何不变量破坏 → ok=false + 具体错误行, 供自愈/测试定位。
|
|
91
|
+
verifyReplay(day = logicalDay()) {
|
|
92
|
+
const lines = this.readRaw(day);
|
|
93
|
+
const errors = [];
|
|
94
|
+
const turnStack = [];
|
|
95
|
+
const stepRounds = new Set();
|
|
96
|
+
let total = 0;
|
|
97
|
+
for (let i = 0; i < lines.length; i++) {
|
|
98
|
+
let e;
|
|
99
|
+
try { e = JSON.parse(lines[i]); } catch (err) { errors.push({ line: i + 1, kind: "parse", detail: err.message }); continue; }
|
|
100
|
+
total++;
|
|
101
|
+
if (!e || !e.evt) { errors.push({ line: i + 1, kind: "no-evt" }); continue; }
|
|
102
|
+
if (e.evt === EVT.TURN_START) turnStack.push(i);
|
|
103
|
+
else if (e.evt === EVT.TURN_END) {
|
|
104
|
+
if (!turnStack.length) errors.push({ line: i + 1, kind: "turn-end-orphan" });
|
|
105
|
+
else turnStack.pop();
|
|
106
|
+
} else if (e.evt === EVT.STEP_START) {
|
|
107
|
+
const r = Number(e.round);
|
|
108
|
+
if (stepRounds.has(r)) errors.push({ line: i + 1, kind: "step-round-duplicate", detail: `round=${r}` });
|
|
109
|
+
stepRounds.add(r);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (turnStack.length) errors.push({ kind: "turn-unclosed", detail: `剩余 ${turnStack.length} 个未闭合 turn` });
|
|
113
|
+
return { ok: errors.length === 0, total, errors };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 读原始行 (verifyReplay 用, 不跳过坏行)
|
|
117
|
+
readRaw(day = logicalDay()) {
|
|
118
|
+
const file = this._file(day);
|
|
119
|
+
if (!fs.existsSync(file)) return [];
|
|
120
|
+
return fs.readFileSync(file, "utf8").split("\n").filter(Boolean);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 读取某天轨迹 (最近 N 条); day 缺省 = 今天
|
|
124
|
+
read(day, limit = 100) {
|
|
125
|
+
const file = this._file(day);
|
|
126
|
+
if (!fs.existsSync(file)) return [];
|
|
127
|
+
return fs.readFileSync(file, "utf8").split("\n").filter(Boolean).slice(-limit)
|
|
128
|
+
.map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 统计: 失败率/平均耗时/慢工具
|
|
132
|
+
stats(day) {
|
|
133
|
+
const all = this.read(day, 10000);
|
|
134
|
+
if (!all.length) return { count: 0, failed: 0, failRate: "0%", slowTools: [] };
|
|
135
|
+
const failed = all.filter((t) => !t.ok);
|
|
136
|
+
const byTool = {};
|
|
137
|
+
for (const t of all) {
|
|
138
|
+
(byTool[t.tool] = byTool[t.tool] || { calls: 0, fails: 0, totalMs: 0 });
|
|
139
|
+
byTool[t.tool].calls++;
|
|
140
|
+
if (!t.ok) byTool[t.tool].fails++;
|
|
141
|
+
byTool[t.tool].totalMs += t.durationMs;
|
|
142
|
+
}
|
|
143
|
+
const slow = Object.entries(byTool)
|
|
144
|
+
.map(([tool, v]) => ({ tool, ...v, avgMs: Math.round(v.totalMs / v.calls) }))
|
|
145
|
+
.sort((a, b) => b.avgMs - a.avgMs).slice(0, 5);
|
|
146
|
+
return {
|
|
147
|
+
count: all.length,
|
|
148
|
+
failed: failed.length,
|
|
149
|
+
failRate: all.length ? (failed.length / all.length * 100).toFixed(1) + "%" : "0%",
|
|
150
|
+
slowTools: slow,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/utils/winutf8.js - Windows 控制台 UTF-8 引导:强制 chcp 65001 + stdout/stderr 用 utf8
|
|
2
|
+
// 根治 PowerShell/GBK 控制台把 Node UTF-8 输出解成乱码(鐨尰铏?/鑳藉姏... 那种)
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
export function ensureUTF8Console() {
|
|
6
|
+
if (process.platform !== "win32") return;
|
|
7
|
+
try {
|
|
8
|
+
// 改当前控制台活动代码页为 UTF-8(仅当是真实终端时;stdout 非 TTY 时跳过,避免写日志管道报错)
|
|
9
|
+
if (process.stdout?.isTTY) {
|
|
10
|
+
execSync("chcp 65001", { stdio: "ignore", windowsHide: true });
|
|
11
|
+
}
|
|
12
|
+
} catch { /* 非交互/无控制台,忽略 */ }
|
|
13
|
+
try { process.stdout.setDefaultEncoding?.("utf8"); } catch {}
|
|
14
|
+
try { process.stderr.setDefaultEncoding?.("utf8"); } catch {}
|
|
15
|
+
}
|