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
package/src/config/settings.js
CHANGED
|
@@ -1,182 +1,156 @@
|
|
|
1
|
-
// src/config/settings.js - 通用设置读写 (HTTP API 后端)
|
|
2
|
-
// 覆盖 config/ppx.json 顶层字段: user / channels.http / security / agent 预设 (values/system_extra/citation_rule/mode)
|
|
3
|
-
// 设计 (与 providers.js 对齐):
|
|
4
|
-
// - 唯一事实源 = config/ppx.json
|
|
5
|
-
// - 写盘: 备份原文件 → 原子写 (.tmp + rename) → 防配置丢失
|
|
6
|
-
// - 读取: 深度合并默认值 (loadConfig), 保证前端总能拿到完整结构
|
|
7
|
-
// - 热重载: 写盘后由调用方调 agent.reload() 重建内存
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
//
|
|
94
|
-
function
|
|
95
|
-
if (!
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
for (const k of MCP_SERVER_KEYS) if (s[k] !== undefined) norm[k] = s[k];
|
|
158
|
-
clean.push(norm);
|
|
159
|
-
}
|
|
160
|
-
cfg.mcp.servers = clean;
|
|
161
|
-
}
|
|
162
|
-
// tools.disabled: 必须是字符串数组
|
|
163
|
-
if (cfg.tools.disabled != null) {
|
|
164
|
-
if (!Array.isArray(cfg.tools.disabled)) throw new Error("tools.disabled 必须是字符串数组");
|
|
165
|
-
if (cfg.tools.disabled.some((t) => typeof t !== "string")) throw new Error("tools.disabled 必须是字符串数组");
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
writeConfigAtomic(root, cfg);
|
|
169
|
-
return sanitizeSettings(cfg);
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// 应用 tools.disabled: 返回需要禁用的工具名列表 (供启动时/热重载时禁用)
|
|
174
|
-
export function disabledTools(root) {
|
|
175
|
-
const { tools } = getSettings(root);
|
|
176
|
-
return Array.isArray(tools?.disabled) ? tools.disabled : [];
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// 可编辑字段元数据 (供前端渲染表单, 免前端硬编码结构)
|
|
180
|
-
export function settingsSchema() {
|
|
181
|
-
return SETTINGS_FIELDS;
|
|
182
|
-
}
|
|
1
|
+
// src/config/settings.js - 通用设置读写 (HTTP API 后端)
|
|
2
|
+
// 覆盖 config/ppx.json 顶层字段: user / channels.http / security / agent 预设 (values/system_extra/citation_rule/mode)
|
|
3
|
+
// 设计 (与 providers.js 对齐):
|
|
4
|
+
// - 唯一事实源 = config/ppx.json
|
|
5
|
+
// - 写盘: 备份原文件 → 原子写 (.tmp + rename) → 防配置丢失
|
|
6
|
+
// - 读取: 深度合并默认值 (loadConfig), 保证前端总能拿到完整结构
|
|
7
|
+
// - 热重载: 写盘后由调用方调 agent.reload() 重建内存
|
|
8
|
+
import { loadConfig, DEFAULT_CONFIG } from "./index.js";
|
|
9
|
+
import { withFileLock } from "../utils/store.js";
|
|
10
|
+
import { configFilePath, readPpxConfig, writeConfigAtomic } from "../utils/config-file.js";
|
|
11
|
+
|
|
12
|
+
const getConfigPath = configFilePath;
|
|
13
|
+
|
|
14
|
+
// 读取当前 config (磁盘原文), 缺文件返回空
|
|
15
|
+
export function readConfigRaw(root) {
|
|
16
|
+
return readPpxConfig(root, {});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 通用设置字段白名单 (GET 返回 + PUT 可改的顶层键)
|
|
20
|
+
// 每个键对应 config 里的一个可编辑分区, 值里的字段再细分
|
|
21
|
+
export const SETTINGS_FIELDS = {
|
|
22
|
+
user: ["name"],
|
|
23
|
+
http: ["port", "auth_token"],
|
|
24
|
+
security: ["allow_all", "command_timeout_ms", "code_act", "deny"],
|
|
25
|
+
agent: ["name", "mode", "citation_rule", "system_extra", "values"],
|
|
26
|
+
mcp: ["servers", "auto_connect"],
|
|
27
|
+
tools: ["disabled"],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// MCP 服务器字段白名单 (防注入任意字段)
|
|
31
|
+
export const MCP_SERVER_KEYS = ["command", "args", "env", "prefix", "url", "headers", "timeout", "name"];
|
|
32
|
+
|
|
33
|
+
// 安全视图: 抹掉敏感明文 (auth_token / api_key / mcp headers), 只暴露 set 标志
|
|
34
|
+
function sanitizeSettings(cfg) {
|
|
35
|
+
const out = {
|
|
36
|
+
user: { name: cfg.user?.name || "兄弟" },
|
|
37
|
+
http: {
|
|
38
|
+
port: cfg.channels?.http?.port ?? 8899,
|
|
39
|
+
auth_token_set: !!(cfg.channels?.http?.auth_token),
|
|
40
|
+
},
|
|
41
|
+
security: {
|
|
42
|
+
allow_all: !!cfg.security?.allow_all,
|
|
43
|
+
command_timeout_ms: cfg.security?.command_timeout_ms ?? 30000,
|
|
44
|
+
code_act: !!cfg.security?.code_act,
|
|
45
|
+
deny: Array.isArray(cfg.security?.deny) ? cfg.security.deny : [],
|
|
46
|
+
},
|
|
47
|
+
agent: {
|
|
48
|
+
name: cfg.agent?.name || "皮皮虾",
|
|
49
|
+
mode: cfg.agent?.mode || "react",
|
|
50
|
+
citation_rule: cfg.agent?.citation_rule ?? "",
|
|
51
|
+
system_extra: cfg.agent?.system_extra ?? "",
|
|
52
|
+
values: Array.isArray(cfg.agent?.values) ? cfg.agent.values : (DEFAULT_CONFIG.agent?.values || []),
|
|
53
|
+
},
|
|
54
|
+
mcp: {
|
|
55
|
+
auto_connect: !!cfg.mcp?.auto_connect,
|
|
56
|
+
servers: Array.isArray(cfg.mcp?.servers) ? cfg.mcp.servers.map((s) => sanitizeMcpServer(s)) : [],
|
|
57
|
+
},
|
|
58
|
+
tools: {
|
|
59
|
+
disabled: Array.isArray(cfg.tools?.disabled) ? cfg.tools.disabled : [],
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// MCP 服务器安全视图: 只暴露白名单字段, headers 抹掉明文只回 set 标志
|
|
66
|
+
function sanitizeMcpServer(s) {
|
|
67
|
+
if (!s || typeof s !== "object") return {};
|
|
68
|
+
const out = {};
|
|
69
|
+
for (const k of MCP_SERVER_KEYS) {
|
|
70
|
+
if (k === "headers") {
|
|
71
|
+
if (s.headers && typeof s.headers === "object") out.headers_set = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (k === "env") {
|
|
75
|
+
if (s.env && typeof s.env === "object") out.env_set = Object.keys(s.env).length > 0;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (s[k] !== undefined) out[k] = s[k];
|
|
79
|
+
}
|
|
80
|
+
if (!out.name) out.name = s.command || s.url || "";
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// GET /api/settings: 返回可编辑设置的安全视图
|
|
85
|
+
export function getSettings(root) {
|
|
86
|
+
// 用 loadConfig 深合并默认值, 保证缺字段也有结构; 但 write 需基于磁盘原文避免覆盖默认
|
|
87
|
+
const merged = loadConfig(root);
|
|
88
|
+
return sanitizeSettings(merged);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// PUT /api/settings: 只更新白名单字段, 返回更新后的安全视图
|
|
92
|
+
// patch = { user?, http?, security?, agent?, mcp?, tools? }
|
|
93
|
+
// v1.0.8: patch 顶层/分区字段都按 SETTINGS_FIELDS 白名单过滤 (防注入任意字段); 写盘在文件锁内读-改-写 (防并发丢更新)
|
|
94
|
+
export function updateSettings(root, patch) {
|
|
95
|
+
if (!patch || typeof patch !== "object") throw new Error("patch 必须是对象");
|
|
96
|
+
const p = getConfigPath(root);
|
|
97
|
+
return withFileLock(p, () => {
|
|
98
|
+
const cfg = readConfigRaw(root); // 锁内重读: 拿最新磁盘状态再改
|
|
99
|
+
// 只取白名单字段 (SETTINGS_FIELDS 定义每个分区可改的键)
|
|
100
|
+
const pick = (obj, keys) => {
|
|
101
|
+
const out = {};
|
|
102
|
+
for (const k of keys) if (obj && typeof obj === "object" && obj[k] !== undefined) out[k] = obj[k];
|
|
103
|
+
return out;
|
|
104
|
+
};
|
|
105
|
+
cfg.user = { ...(cfg.user || {}), ...pick(patch.user, SETTINGS_FIELDS.user) };
|
|
106
|
+
cfg.channels = cfg.channels || {};
|
|
107
|
+
cfg.channels.http = { ...(cfg.channels.http || {}), ...pick(patch.http, SETTINGS_FIELDS.http) };
|
|
108
|
+
cfg.security = { ...(cfg.security || {}), ...pick(patch.security, SETTINGS_FIELDS.security) };
|
|
109
|
+
cfg.agent = { ...(cfg.agent || {}), ...pick(patch.agent, SETTINGS_FIELDS.agent) };
|
|
110
|
+
cfg.mcp = { ...(cfg.mcp || {}), ...pick(patch.mcp, SETTINGS_FIELDS.mcp) };
|
|
111
|
+
cfg.tools = { ...(cfg.tools || {}), ...pick(patch.tools, SETTINGS_FIELDS.tools) };
|
|
112
|
+
|
|
113
|
+
// 校验
|
|
114
|
+
const port = cfg.channels.http.port;
|
|
115
|
+
if (port != null && (!Number.isInteger(port) || port < 1 || port > 65535)) throw new Error("HTTP 端口必须是 1-65535 的整数");
|
|
116
|
+
const timeout = cfg.security.command_timeout_ms;
|
|
117
|
+
if (timeout != null && (!Number.isFinite(timeout) || timeout < 1000)) throw new Error("命令超时至少 1000ms");
|
|
118
|
+
if (cfg.security.deny != null && !Array.isArray(cfg.security.deny)) throw new Error("额外拒绝规则必须是字符串数组");
|
|
119
|
+
if (cfg.security.deny && cfg.security.deny.some((d) => typeof d !== "string")) throw new Error("额外拒绝规则必须是字符串数组");
|
|
120
|
+
if (cfg.agent.values != null && !Array.isArray(cfg.agent.values)) throw new Error("核心价值必须是字符串数组");
|
|
121
|
+
if (cfg.agent.values && cfg.agent.values.some((v) => typeof v !== "string")) throw new Error("核心价值必须是字符串数组");
|
|
122
|
+
if (cfg.agent.mode != null && !/^[a-z-]{2,30}$/.test(String(cfg.agent.mode))) throw new Error("编排模式只含小写字母/横线");
|
|
123
|
+
// mcp.servers: 必须是对象数组, 每项至少 command 或 url, 只保留白名单字段
|
|
124
|
+
if (cfg.mcp.servers != null && !Array.isArray(cfg.mcp.servers)) throw new Error("MCP servers 必须是数组");
|
|
125
|
+
if (Array.isArray(cfg.mcp.servers)) {
|
|
126
|
+
const clean = [];
|
|
127
|
+
for (const s of cfg.mcp.servers) {
|
|
128
|
+
if (!s || typeof s !== "object") continue;
|
|
129
|
+
if (!s.command && !s.url) throw new Error("MCP 服务器至少需要 command(stdio) 或 url(http)");
|
|
130
|
+
const norm = {};
|
|
131
|
+
for (const k of MCP_SERVER_KEYS) if (s[k] !== undefined) norm[k] = s[k];
|
|
132
|
+
clean.push(norm);
|
|
133
|
+
}
|
|
134
|
+
cfg.mcp.servers = clean;
|
|
135
|
+
}
|
|
136
|
+
// tools.disabled: 必须是字符串数组
|
|
137
|
+
if (cfg.tools.disabled != null) {
|
|
138
|
+
if (!Array.isArray(cfg.tools.disabled)) throw new Error("tools.disabled 必须是字符串数组");
|
|
139
|
+
if (cfg.tools.disabled.some((t) => typeof t !== "string")) throw new Error("tools.disabled 必须是字符串数组");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
writeConfigAtomic(root, cfg);
|
|
143
|
+
return sanitizeSettings(cfg);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 应用 tools.disabled: 返回需要禁用的工具名列表 (供启动时/热重载时禁用)
|
|
148
|
+
export function disabledTools(root) {
|
|
149
|
+
const { tools } = getSettings(root);
|
|
150
|
+
return Array.isArray(tools?.disabled) ? tools.disabled : [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 可编辑字段元数据 (供前端渲染表单, 免前端硬编码结构)
|
|
154
|
+
export function settingsSchema() {
|
|
155
|
+
return SETTINGS_FIELDS;
|
|
156
|
+
}
|
package/src/core/policy.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// 循环何时停、降档、重试、注入方向盘, 全由本模块决策。
|
|
6
6
|
// 依赖全部注入 (llm/tools/runTool/shrinkMessages/...), 无 agent 引用, 可独立测试。
|
|
7
7
|
import { TOOL_ERROR_PREFIX } from "../tools/index.js";
|
|
8
|
-
import { warn } from "../utils/logger.js";
|
|
8
|
+
import { warn, debug } from "../utils/logger.js";
|
|
9
9
|
|
|
10
10
|
// ---- 阈值默认值 (config.agent.* 可覆盖) ----
|
|
11
11
|
export const DEFAULT_MAX_TOOL_ROUNDS = 8;
|
|
@@ -78,7 +78,7 @@ export async function callWithTimeoutRetry({
|
|
|
78
78
|
budgetMs = null, // 工具超时预算 (toolTimeoutOf 注入, 事件采集用)
|
|
79
79
|
onEvent = null,
|
|
80
80
|
}) {
|
|
81
|
-
const ev = (type, payload) => { if (onEvent) { try { onEvent(type, payload); } catch {} } };
|
|
81
|
+
const ev = (type, payload) => { if (onEvent) { try { onEvent(type, payload); } catch (e) { debug(`[core/policy] 已忽略异常: ${e && e.message ? e.message : e}`); } } };
|
|
82
82
|
const t0 = Date.now();
|
|
83
83
|
let result = await runTool(name, args);
|
|
84
84
|
let elapsedMs = Date.now() - t0;
|
|
@@ -112,6 +112,8 @@ export class ToolLoopPolicy {
|
|
|
112
112
|
this.exploreBreak = Number(c.explore_break_limit) || DEFAULT_EXPLORE_BREAK;
|
|
113
113
|
this.repeatFlag = Number(c.repeat_flag_limit) || DEFAULT_REPEAT_FLAG;
|
|
114
114
|
this.overflowShrinkMax = DEFAULT_OVERFLOW_SHRINK_MAX;
|
|
115
|
+
// 同轮独立工具调用并发执行 (2026-10-01 优化): 默认开, agent.parallel_tool_calls=false 回退串行
|
|
116
|
+
this.parallelToolCalls = c.parallel_tool_calls !== false;
|
|
115
117
|
// 运行时状态 (每轮循环实例持有, 重启归零)
|
|
116
118
|
this.errorRetries = 0;
|
|
117
119
|
this.exploreStreak = 0;
|
|
@@ -142,7 +144,7 @@ export class ToolLoopPolicy {
|
|
|
142
144
|
let repeatHit = false;
|
|
143
145
|
for (const tc of called) {
|
|
144
146
|
let a = {};
|
|
145
|
-
try { a = JSON.parse(tc.function.arguments || "{}"); } catch {}
|
|
147
|
+
try { a = JSON.parse(tc.function.arguments || "{}"); } catch (e) { debug(`[core/policy] 已忽略异常: ${e && e.message ? e.message : e}`); }
|
|
146
148
|
const sig = (tc.function?.name || "") + "::" + JSON.stringify(a).slice(0, 120);
|
|
147
149
|
this.seenSig.set(sig, (this.seenSig.get(sig) || 0) + 1);
|
|
148
150
|
if (this.seenSig.get(sig) >= this.repeatFlag) repeatHit = true;
|
|
@@ -166,6 +168,24 @@ export class ToolLoopPolicy {
|
|
|
166
168
|
this.errorRetries++;
|
|
167
169
|
return true;
|
|
168
170
|
}
|
|
171
|
+
|
|
172
|
+
// ---- 自省裁决 (Reflective 内核): 对工具失败做语义分类, 决定重试策略 ----
|
|
173
|
+
// 在机械次数重试之上加一道闸门: 硬拒绝类错误 (黑名单/审批拒/权限/deny/DENY_HINT)
|
|
174
|
+
// 不许盲目改写命令绕过, 直接拦停; 可修正类错误才走次数重试。
|
|
175
|
+
// 返回 { action: "stop"|"retry", reason } — 纯方法, 可独立测试。
|
|
176
|
+
selfReviewError(errors) {
|
|
177
|
+
if (!errors || !errors.length) return null;
|
|
178
|
+
const text = errors.join("\n");
|
|
179
|
+
// 硬拒绝特征: 命中命令守卫拦截 / 审批被拒 / 权限拒绝 / 黑名单
|
|
180
|
+
if (/命中后不要重试|改造命令绕过|硬黑名单|审批被拒绝|审批拒绝|权限.*拒|deny|DENY|拦截/i.test(text)) {
|
|
181
|
+
return {
|
|
182
|
+
action: "stop",
|
|
183
|
+
reason: "这是硬性拒绝类错误, 盲目重试或改写命令会绕过安全闸门 — 停下不重试, 说明原因或请用户调整配置。",
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
// 可修正类错误 (命令不存在/文件缺失/参数错等): 走次数重试道
|
|
187
|
+
return { action: "retry", reason: "可修正错误, 喂回模型重试 (仍受次数上限约束)" };
|
|
188
|
+
}
|
|
169
189
|
}
|
|
170
190
|
|
|
171
191
|
// ---- 工具循环主驱动 (原 PPXAgent._llmWithTools) ----
|
|
@@ -198,11 +218,11 @@ export async function runToolLoop({
|
|
|
198
218
|
}) {
|
|
199
219
|
const policy = new ToolLoopPolicy(config.agent || config);
|
|
200
220
|
let messages = [...seedMessages];
|
|
201
|
-
const ev = (type, payload) => { if (onEvent) { try { onEvent(type, payload); } catch {} } };
|
|
221
|
+
const ev = (type, payload) => { if (onEvent) { try { onEvent(type, payload); } catch (e) { debug(`[core/policy] 已忽略异常: ${e && e.message ? e.message : e}`); } } };
|
|
202
222
|
|
|
203
223
|
for (let round = 0; round < policy.maxRounds; round++) {
|
|
204
224
|
if (isInterrupted()) return "[皮皮虾] 任务已被中断 (operator cancelled).";
|
|
205
|
-
if (onStep) { try { onStep({ type: "step", round, maxRounds: policy.maxRounds, ts: Date.now() }); } catch {} }
|
|
225
|
+
if (onStep) { try { onStep({ type: "step", round, maxRounds: policy.maxRounds, ts: Date.now() }); } catch (e) { debug(`[core/policy] 已忽略异常: ${e && e.message ? e.message : e}`); } }
|
|
206
226
|
|
|
207
227
|
let resp;
|
|
208
228
|
try {
|
|
@@ -232,25 +252,56 @@ export async function runToolLoop({
|
|
|
232
252
|
}
|
|
233
253
|
|
|
234
254
|
// 工具错误重试: 若本轮有工具失败, 汇总错误喂回模型修正后重试 (最多 maxErrorRetry 次)
|
|
235
|
-
|
|
255
|
+
// 并发执行 (2026-10-01 优化, 审计 P1 遗留项): 同一轮的 tool_calls 相互独立
|
|
256
|
+
// (OpenAI 语义: 数组内无依赖), 串行会让 N 个独立调用的延迟线性叠加。
|
|
257
|
+
// Promise.all 保序: messages 回传顺序与 errors 汇总顺序仍与 tool_calls 一致,
|
|
258
|
+
// 下游 (recordTurn 重复检测 / 错误喂回) 语义不变。agent.parallel_tool_calls=false 回退串行。
|
|
259
|
+
const callable = [];
|
|
236
260
|
for (const tc of toolCalls) {
|
|
237
261
|
if (tc.type === "function" && tc.function) {
|
|
238
262
|
let args = {};
|
|
239
|
-
try { args = JSON.parse(tc.function.arguments || "{}"); } catch {}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
263
|
+
try { args = JSON.parse(tc.function.arguments || "{}"); } catch (e) { debug(`[core/policy] 已忽略异常: ${e && e.message ? e.message : e}`); }
|
|
264
|
+
callable.push({ tc, args });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// v1.6.0 第四刀语义保留: 超时检测 + 幂等重试一次 (tool/timeout 事件采集 P50/P95/P99 数据基础)
|
|
268
|
+
const execOne = ({ tc, args }) => callWithTimeoutRetry({
|
|
269
|
+
name: tc.function.name,
|
|
270
|
+
args,
|
|
271
|
+
runTool,
|
|
272
|
+
isIdempotent: isIdempotentTool(tc.function.name),
|
|
273
|
+
budgetMs: toolTimeoutOf(tc.function.name),
|
|
274
|
+
onEvent,
|
|
275
|
+
}).then((r) => ({ tc, ...r }));
|
|
276
|
+
const errors = [];
|
|
277
|
+
const collect = (tc, result) => {
|
|
278
|
+
// 2026-10-03 修复 (P1): 原 tool 消息只有非标 `_id`, 缺 OpenAI 规范要求的 tool_call_id
|
|
279
|
+
// → 严格校验后端 (OpenAI 官方 / vLLM) 第二轮必 400。补上标准字段; `_id` 保留兼容内部消费方
|
|
280
|
+
messages.push({ role: "tool", tool_call_id: tc.id, _id: tc.id, content: toToolContent(result, policy.resultBudget) });
|
|
281
|
+
if (result.startsWith(TOOL_ERROR_PREFIX)) errors.push(result);
|
|
282
|
+
};
|
|
283
|
+
if (policy.parallelToolCalls) {
|
|
284
|
+
const settled = await Promise.all(callable.map(execOne));
|
|
285
|
+
for (const { tc, result } of settled) collect(tc, result);
|
|
286
|
+
} else {
|
|
287
|
+
// 串行回退路径 (旧行为): 逐个执行 + 逐个回传
|
|
288
|
+
for (const item of callable) {
|
|
289
|
+
const { result } = await execOne(item);
|
|
290
|
+
collect(item.tc, result);
|
|
251
291
|
}
|
|
252
292
|
}
|
|
253
293
|
if (policy.shouldRetryErrors(errors)) {
|
|
294
|
+
// 自省裁决 (Reflective 内核): 硬拒绝类错误直接拦停, 不得盲目改写命令绕过
|
|
295
|
+
// 语义闸门在次数闸门之前: 即使重试次数未满, 硬拒绝也不重试 (安全红线)
|
|
296
|
+
const verdict = policy.selfReviewError(errors);
|
|
297
|
+
if (verdict && verdict.action === "stop") {
|
|
298
|
+
ev("tool/self_review_stop", { round, reason: verdict.reason, retries: policy.errorRetries });
|
|
299
|
+
messages.push({
|
|
300
|
+
role: "user",
|
|
301
|
+
content: "自省裁决: " + verdict.reason + "\n失败详情:\n" + errors.join("\n") + "\n请停止重试, 直接基于已有信息给出结论, 或向用户说明原因。",
|
|
302
|
+
});
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
254
305
|
ev("tool/error_retry", { round, errors: errors.length, retries: policy.errorRetries, max: policy.maxErrorRetry });
|
|
255
306
|
messages.push({
|
|
256
307
|
role: "user",
|
package/src/core/trace.js
CHANGED
|
@@ -11,14 +11,15 @@
|
|
|
11
11
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
12
12
|
import fs from "node:fs";
|
|
13
13
|
import path from "node:path";
|
|
14
|
-
import {
|
|
14
|
+
import { logsDir, logicalDay } from "../utils/store.js";
|
|
15
|
+
import { shortId } from "../utils/id.js";
|
|
15
16
|
import { scrubPII } from "../utils/pii.js";
|
|
16
17
|
|
|
17
18
|
const als = new AsyncLocalStorage();
|
|
18
19
|
const MAX_PAYLOAD = 2000; // 单条事件载荷上限, 防爆文件
|
|
19
20
|
|
|
20
21
|
export function genTraceId() {
|
|
21
|
-
return "t_"
|
|
22
|
+
return shortId("t_", 8);
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
// 入口包装: 生成新 traceId, 所有异步子调用自动继承
|
|
@@ -41,9 +42,8 @@ export function hasTrace() {
|
|
|
41
42
|
// ---- 事件流写入器 (agent 构造时实例化一个, 全局复用) ----
|
|
42
43
|
export class EventTracer {
|
|
43
44
|
constructor(dataDir) {
|
|
44
|
-
this.dir =
|
|
45
|
-
|
|
46
|
-
this.sessionId = "s_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
45
|
+
this.dir = logsDir(dataDir); // 与 utils/trace.js 同一目录 (logs/traces)
|
|
46
|
+
this.sessionId = shortId("s_", 6);
|
|
47
47
|
this.count = 0;
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -61,8 +61,10 @@ export class EventTracer {
|
|
|
61
61
|
sessionId: this.sessionId,
|
|
62
62
|
traceId: store?.traceId || null,
|
|
63
63
|
seq: this.count,
|
|
64
|
-
type
|
|
64
|
+
// ...safe 在 type 之前 (2026-09-18 修复): payload 自带 type 键时不得覆盖埋点事件类型,
|
|
65
|
+
// 否则 events-*.jsonl 中 type 与实际语义不符, 基于 type 的检索/统计失真
|
|
65
66
|
...safe,
|
|
67
|
+
type,
|
|
66
68
|
};
|
|
67
69
|
if (opts.durationMs != null) entry.durationMs = Math.round(opts.durationMs);
|
|
68
70
|
if (opts.error != null) entry.error = String(opts.error).slice(0, 500);
|