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
package/config/ppx.json
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
{
|
|
2
|
+
"agent": {
|
|
3
|
+
"name": "皮皮虾",
|
|
4
|
+
"yuan": "ppx",
|
|
5
|
+
"max_tool_rounds": 8,
|
|
6
|
+
"tool_result_budget": 4000,
|
|
7
|
+
"max_tool_error_retry": 2,
|
|
8
|
+
"proactive": {
|
|
9
|
+
"enabled": true,
|
|
10
|
+
"interval_ms": 3600000
|
|
11
|
+
},
|
|
12
|
+
"evolve": {
|
|
13
|
+
"enabled": true,
|
|
14
|
+
"every_calls": 20,
|
|
15
|
+
"min_interval_ms": 30000
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"user": {
|
|
19
|
+
"name": "兄弟"
|
|
20
|
+
},
|
|
21
|
+
"providers": [
|
|
22
|
+
{
|
|
23
|
+
"id": "dsh",
|
|
24
|
+
"backend": "deepseek",
|
|
25
|
+
"dsh_root": ".deps/deepseek-harness",
|
|
26
|
+
"timeout_ms": 180000
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "openai",
|
|
30
|
+
"base_url": "https://api.openai.com/v1",
|
|
31
|
+
"api_key_env": "OPENAI_API_KEY",
|
|
32
|
+
"model": "gpt-4o-mini",
|
|
33
|
+
"timeout_ms": 180000,
|
|
34
|
+
"context_window": 128000
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "deepseek",
|
|
38
|
+
"base_url": "https://api.deepseek.com/v1",
|
|
39
|
+
"api_key_env": "DEEPSEEK_API_KEY",
|
|
40
|
+
"model": "deepseek-chat",
|
|
41
|
+
"timeout_ms": 180000,
|
|
42
|
+
"context_window": 65536
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "dashscope",
|
|
46
|
+
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
47
|
+
"api_key_env": "DASHSCOPE_API_KEY",
|
|
48
|
+
"model": "qwen-turbo",
|
|
49
|
+
"timeout_ms": 180000,
|
|
50
|
+
"context_window": 131072
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": "qwen-vl",
|
|
54
|
+
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
55
|
+
"api_key_env": "DASHSCOPE_API_KEY",
|
|
56
|
+
"model": "qwen-vl-max",
|
|
57
|
+
"vision": true,
|
|
58
|
+
"timeout_ms": 180000,
|
|
59
|
+
"context_window": 32768
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"id": "lmstudio",
|
|
63
|
+
"base_url": "http://127.0.0.1:1234/v1",
|
|
64
|
+
"api_key": "lm-studio",
|
|
65
|
+
"model": "gemma-4-e2b-uncensored-hauhaucs-aggressive-q8_k_p",
|
|
66
|
+
"vision": false,
|
|
67
|
+
"timeout_ms": 180000
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"id": "zhipu",
|
|
71
|
+
"base_url": "https://open.bigmodel.cn/api/paas/v4",
|
|
72
|
+
"api_key_env": "ZHIPU_API_KEY",
|
|
73
|
+
"model": "glm-5v-turbo",
|
|
74
|
+
"vision": true,
|
|
75
|
+
"timeout_ms": 180000,
|
|
76
|
+
"context_window": 65536
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
"_optional_engines": {
|
|
80
|
+
"_note": "可选引擎。火山引擎 volcengine 需先填真实 endpoint(model 不能留 REPLACE_WITH_YOUR_ENDPOINT 占位), 把它加回 providers 数组并设 VOLCENGINE_API_KEY 即启用;本地引擎(openclaw/dsh)默认不启用, 要用时把对象移入 providers 并设对应路径环境变量。",
|
|
81
|
+
"openclaw": {
|
|
82
|
+
"id": "openclaw",
|
|
83
|
+
"backend": "openclaw",
|
|
84
|
+
"mjs": "",
|
|
85
|
+
"session_key": "ppx:main",
|
|
86
|
+
"timeout_ms": 180000
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"memory": {
|
|
90
|
+
"enabled": true,
|
|
91
|
+
"token_budget": 2500,
|
|
92
|
+
"decay_per_day": 0.02,
|
|
93
|
+
"hit_bonus": 5,
|
|
94
|
+
"base_importance": 10,
|
|
95
|
+
"compile_threshold": 4.5,
|
|
96
|
+
"forget_speed": 1,
|
|
97
|
+
"memory_ttl_days": 90
|
|
98
|
+
},
|
|
99
|
+
"audit": {
|
|
100
|
+
"enabled": true,
|
|
101
|
+
"_note": "工具调用审计哈希链 (吸收自 ppx-v2)。append-only + SHA-256 链式防篡改, 落 data/logs/audit.ndjson。设 false 可关闭 (性能敏感场景)。校验: ppx 对话里调 audit_verify 工具, 或 npm run audit:verify。"
|
|
102
|
+
},
|
|
103
|
+
"embedding": {
|
|
104
|
+
"base_url": "http://127.0.0.1:1234/v1",
|
|
105
|
+
"api_key": "lm-studio",
|
|
106
|
+
"model": "text-embedding-nomic-embed-text-v1.5"
|
|
107
|
+
},
|
|
108
|
+
"experience": {
|
|
109
|
+
"enabled": true
|
|
110
|
+
},
|
|
111
|
+
"selfheal": {
|
|
112
|
+
"enabled": true,
|
|
113
|
+
"check_interval_ms": 60000
|
|
114
|
+
},
|
|
115
|
+
"tools": {
|
|
116
|
+
"enabled": true
|
|
117
|
+
},
|
|
118
|
+
"channels": {
|
|
119
|
+
"http": {
|
|
120
|
+
"enabled": true,
|
|
121
|
+
"port": 8899,
|
|
122
|
+
"auth_token": ""
|
|
123
|
+
},
|
|
124
|
+
"log": {
|
|
125
|
+
"enabled": true,
|
|
126
|
+
"target": "console"
|
|
127
|
+
},
|
|
128
|
+
"feishu": {
|
|
129
|
+
"enabled": false,
|
|
130
|
+
"appId": "",
|
|
131
|
+
"appSecret": "",
|
|
132
|
+
"verifyToken": ""
|
|
133
|
+
},
|
|
134
|
+
"wechat": {
|
|
135
|
+
"enabled": false,
|
|
136
|
+
"path": "/wechat/webhook",
|
|
137
|
+
"token": "",
|
|
138
|
+
"encodingAESKey": "",
|
|
139
|
+
"corpId": "",
|
|
140
|
+
"corpSecret": "",
|
|
141
|
+
"agentId": ""
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"security": {
|
|
145
|
+
"allow_all": false,
|
|
146
|
+
"command_timeout_ms": 30000,
|
|
147
|
+
"deny": [
|
|
148
|
+
|
|
149
|
+
]
|
|
150
|
+
}
|
|
151
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ppxans-harness",
|
|
3
|
+
"version": "2.4.0",
|
|
4
|
+
"description": "PPXANS-Harness - 皮皮虾神经系 (ANS) + Harness 一体化智能体内核. 零运行时依赖纯Node. 自愈 + 自学习 + 五层记忆 + SHA-256 审计哈希链 + 多 Agent 军团 + 治理内核(deny-wins/熔断/seam) + 进化内核(Playbook/故障记忆) + 符号画布 + supervisor. Agent Nervous System harness in pure Node, zero runtime dependencies.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"author": "chen6896qqwee <chen6896qqwee@users.noreply.github.com>",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20"
|
|
10
|
+
},
|
|
11
|
+
"main": "src/agent/index.js",
|
|
12
|
+
"bin": {
|
|
13
|
+
"ppx": "bin/ppx.js",
|
|
14
|
+
"ppxans": "bin/ppx.js",
|
|
15
|
+
"ppx-serve": "bin/ppx-serve.js",
|
|
16
|
+
"ppx-channels": "bin/ppx-channels.js"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/chen6896qqwee/PPXANS-Harness.git"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/chen6896qqwee/PPXANS-Harness",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/chen6896qqwee/PPXANS-Harness/issues"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"start": "node src/agent/index.js",
|
|
28
|
+
"chat": "node src/cli.js",
|
|
29
|
+
"serve": "node src/server.js",
|
|
30
|
+
"web": "node scripts/start-web.js",
|
|
31
|
+
"web:build": "npm run build --prefix web",
|
|
32
|
+
"release": "node scripts/release.js",
|
|
33
|
+
"eval": "node scripts/eval.js",
|
|
34
|
+
"dsh": "cd .deps/deepseek-harness && pnpm dsh",
|
|
35
|
+
"dsh:install": "cd .deps/deepseek-harness && pnpm install",
|
|
36
|
+
"dsh:build": "cd .deps/deepseek-harness && pnpm run build",
|
|
37
|
+
"selfheal": "node scripts/selfheal-bench.js",
|
|
38
|
+
"selfheal:run": "node src/selfheal/run.js",
|
|
39
|
+
"audit:verify": "node scripts/audit-verify.js",
|
|
40
|
+
"bench": "node scripts/bench.js",
|
|
41
|
+
"test": "node --test --test-force-exit test/*.test.js",
|
|
42
|
+
"prepublishOnly": "npm run selfheal && npm test"
|
|
43
|
+
},
|
|
44
|
+
"keywords": [
|
|
45
|
+
"agent",
|
|
46
|
+
"ai",
|
|
47
|
+
"llm",
|
|
48
|
+
"memory",
|
|
49
|
+
"autonomous",
|
|
50
|
+
"self-healing",
|
|
51
|
+
"zero-dependency",
|
|
52
|
+
"audit-chain",
|
|
53
|
+
"memory-governance",
|
|
54
|
+
"agent-nervous-system"
|
|
55
|
+
],
|
|
56
|
+
"files": [
|
|
57
|
+
"bin/",
|
|
58
|
+
"src/",
|
|
59
|
+
"config/",
|
|
60
|
+
"README.md",
|
|
61
|
+
"LICENSE"
|
|
62
|
+
],
|
|
63
|
+
"exports": {
|
|
64
|
+
".": "./src/agent/index.js",
|
|
65
|
+
"./server": "./src/server.js",
|
|
66
|
+
"./cli": "./src/cli.js",
|
|
67
|
+
"./audit": "./src/audit/audit-chain.js"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// src/agent/context.js - Agent 历史/上下文管理 (从 index.js 拆分, mixin 挂回 prototype)
|
|
2
|
+
// 重构第三刀后续 (2026-09-15): 历史裁剪/token 预算/会话压缩从 PPXAgent 类中抽出,
|
|
3
|
+
// 方法以 mixin 方式挂回 prototype, 实例行为与调用方完全不变 (测试走 agent._xxx 不受影响)。
|
|
4
|
+
|
|
5
|
+
import { estimateTokens } from "../utils/text.js";
|
|
6
|
+
import { transcriptToText, buildCompactionMessages } from "../memory/compaction.js";
|
|
7
|
+
|
|
8
|
+
// 辅助 LLM 调用短超时 (压缩/提炼等非主对话调用):
|
|
9
|
+
// 模型不可用/网络不通时快速失败降级, 避免阻塞主对话
|
|
10
|
+
const AUX_LLM_TIMEOUT_MS = 10000;
|
|
11
|
+
// 上下文窗口感知: 未知窗口的保守默认 (绝不放大历史) + 历史占用窗口的安全比例上限
|
|
12
|
+
const DEFAULT_CONTEXT_WINDOW = 8192;
|
|
13
|
+
const DEFAULT_CONTEXT_RATIO = 0.6;
|
|
14
|
+
|
|
15
|
+
export const contextMethods = {
|
|
16
|
+
// ---- 多轮会话历史 (吸收 dsh "会话即事实源") ----
|
|
17
|
+
// 历史从事件日志投影, 再按预算裁剪 (裁剪只发生在投影层, 日志本身不可变)
|
|
18
|
+
// v0.6.6 优化: 信息量感知裁剪 (学自 Claude Code Microcompact 思路)
|
|
19
|
+
// 旧版: 纯按条数硬截 + 尾部 token 预算, 可能裁掉关键决策/工具结果轮次
|
|
20
|
+
// 新版: 优先保留"含关键信息"的轮次(指令/数字/路径/结论/工具结果), 纯寒暄让位
|
|
21
|
+
_historyPriority(m) {
|
|
22
|
+
const s = String(m?.content || "");
|
|
23
|
+
if (!s) return 0;
|
|
24
|
+
let p = 0;
|
|
25
|
+
// 长消息(含工具结果/详细决策)权重高
|
|
26
|
+
if (s.length > 120) p += 2;
|
|
27
|
+
// 含指令/结论/数字/路径/文件等关键信号
|
|
28
|
+
if (/[查|算|计算|写|建|改|创建|删除|修复|总结|分析|设置|配置|执行|运行|启动|停止|提交|部署|安装|生成|编译|测试]/.test(s)) p += 2;
|
|
29
|
+
if (/[0-9]{2,}|[%.¥$元%]|[::][0-9]/.test(s)) p += 1;
|
|
30
|
+
if (/[A-Za-z]:[\\\/]|\.(js|py|md|json|txt|ts|go|rs|cpp|java|log)\b/.test(s)) p += 2;
|
|
31
|
+
if (/失败|错误|报错|异常|成功|完成|结果|结论|决定|方案|建议/.test(s)) p += 2;
|
|
32
|
+
// 纯寒暄/简短确认权重低
|
|
33
|
+
if (/^(你好|hi|hello|在吗|谢谢|好的|ok|嗯|是的|对|收到|知道|了解|再见|拜拜)/i.test(s.trim())) p -= 3;
|
|
34
|
+
return p;
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// provider 上下文窗口 -> 历史 token 预算硬上限:
|
|
38
|
+
// 用 llm.context_window(未配置回退 config.memory.context_window) * 安全比例, 与显式预算取小。
|
|
39
|
+
// 目的: 本地小上下文模型即使没配 history_token_budget, 也不会把历史塞爆窗口。
|
|
40
|
+
_histTokenCap() {
|
|
41
|
+
const window = Number(this.llm?.context_window) || Number(this.config?.memory?.context_window) || DEFAULT_CONTEXT_WINDOW;
|
|
42
|
+
const ratio = Number(this.config?.memory?.context_window_ratio) || DEFAULT_CONTEXT_RATIO;
|
|
43
|
+
return Math.max(200, Math.floor(window * ratio));
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// 会话历史裁剪 (中心函数): 条数上限 + token 预算, 信息量感知, 必保最近一条。
|
|
47
|
+
// opts.budget / opts.maxItems 可覆盖 (溢出降档重试时传更紧预算)。
|
|
48
|
+
// token 预算 = min(显式 history_token_budget, 窗口硬上限), 两者都收紧, 取小者。
|
|
49
|
+
_trimHistory(hist, opts = {}) {
|
|
50
|
+
let h = [...hist];
|
|
51
|
+
const maxItems = opts.maxItems != null ? opts.maxItems : (Number(this.config.memory?.max_history_items) || 40);
|
|
52
|
+
const cfgBudget = Number(this.config.memory?.history_token_budget) || 4000;
|
|
53
|
+
const tokenBudget = opts.budget != null ? opts.budget : Math.min(cfgBudget, this._histTokenCap());
|
|
54
|
+
// 1) 条数上限: 超限时按信息量淘汰 (低信息量优先, 从旧到新)
|
|
55
|
+
if (h.length > maxItems) {
|
|
56
|
+
const scored = h.map((m, i) => ({ m, i, p: this._historyPriority(m) }));
|
|
57
|
+
scored.sort((a, b) => (a.p - b.p) || (a.i - b.i));
|
|
58
|
+
const drop = scored.length - maxItems;
|
|
59
|
+
const dropped = new Set(scored.slice(0, drop).map((x) => x.i));
|
|
60
|
+
scored.sort((a, b) => a.i - b.i);
|
|
61
|
+
h = scored.filter((x) => !dropped.has(x.i)).map((x) => x.m);
|
|
62
|
+
}
|
|
63
|
+
// 2) token 预算: 信息量感知裁剪 (替代旧版"丢最旧前缀")
|
|
64
|
+
// 必保最近一条, 其余按信息量从高到低补足, 低信息量轮次让位
|
|
65
|
+
let total = h.reduce((a, m) => a + estimateTokens(m.content), 0);
|
|
66
|
+
if (total > tokenBudget) {
|
|
67
|
+
const keep = new Set();
|
|
68
|
+
let used = 0;
|
|
69
|
+
const lastIdx = h.length - 1;
|
|
70
|
+
keep.add(lastIdx); used += estimateTokens(h[lastIdx].content);
|
|
71
|
+
const rest = h.slice(0, lastIdx)
|
|
72
|
+
.map((m, i) => ({ m, i, p: this._historyPriority(m) }))
|
|
73
|
+
.sort((a, b) => (b.p - a.p) || (a.i - b.i));
|
|
74
|
+
for (const { m, i } of rest) {
|
|
75
|
+
const t = estimateTokens(m.content);
|
|
76
|
+
if (used + t > tokenBudget) continue;
|
|
77
|
+
keep.add(i); used += t;
|
|
78
|
+
}
|
|
79
|
+
h = h.filter((_, i) => keep.has(i));
|
|
80
|
+
}
|
|
81
|
+
return h;
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
// 绝对硬裁剪兜底 (第九轮 review P1: 不依赖 LLM 也能保证历史放得下):
|
|
85
|
+
// 在 _trimHistory 基础上再加一道绝对底线 — 超条数则保留最近 N 轮,
|
|
86
|
+
// 超 token 则从最新向前贪心保留到预算内 (最近信息优先, 必保最后一条)。
|
|
87
|
+
// 即便 config 异常 (预算极大/极小的模型), 注入的历史也不会超过窗口安全比例。
|
|
88
|
+
_ensureContextFit(hist, { budget = this._histTokenCap(), maxItems } = {}) {
|
|
89
|
+
let h = [...hist];
|
|
90
|
+
const itemCap = maxItems != null ? maxItems : (Number(this.config.memory?.max_history_items) || 40);
|
|
91
|
+
// 环节 1: 条数绝对兜底 — 超上限只留最近 itemCap 条
|
|
92
|
+
if (h.length > itemCap) h = h.slice(-itemCap);
|
|
93
|
+
// 环节 2: token 绝对兜底 — 最近优先贪心直到塞满预算 (必保最后一条)
|
|
94
|
+
let total = h.reduce((a, m) => a + estimateTokens(m.content), 0);
|
|
95
|
+
if (total > budget && h.length) {
|
|
96
|
+
const keep = [];
|
|
97
|
+
let used = 0;
|
|
98
|
+
for (let i = h.length - 1; i >= 0; i--) {
|
|
99
|
+
const t = estimateTokens(h[i].content);
|
|
100
|
+
// 至少保留最后一条 (当前对话), 其余超预算跳过
|
|
101
|
+
if (keep.length === 0 || used + t <= budget) {
|
|
102
|
+
keep.unshift(h[i]); used += t;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
h = keep;
|
|
106
|
+
}
|
|
107
|
+
return h;
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
// 溢出降档: 把已组好的消息数组按「更紧历史预算」重建 (消息完整性安全版)。
|
|
111
|
+
// - 保留全部 system (角色/记忆/经验)
|
|
112
|
+
// - 自最后一条 user 起的一切消息原样保留 (含 in-flight 的 assistant tool_calls + tool 配对,
|
|
113
|
+
// 绝不剪切成"孤立的 tool 消息"导致 API 400)
|
|
114
|
+
// - 只对最后一条 user 之前的旧历史做最近优先硬裁剪到 budget 内
|
|
115
|
+
_shrinkMessagesForOverflow(messages, budget) {
|
|
116
|
+
let i = 0;
|
|
117
|
+
while (i < messages.length && messages[i] && messages[i].role === "system") i++;
|
|
118
|
+
const systems = messages.slice(0, i);
|
|
119
|
+
const rest = messages.slice(i);
|
|
120
|
+
if (!rest.length) return messages; // 只有 system, 无裁剪空间, 原样返回
|
|
121
|
+
// 从后向前找最后一条 user 作为「进行中单元」起点 (含其后的 tool 配对)
|
|
122
|
+
let lastUser = rest.length - 1;
|
|
123
|
+
while (lastUser > 0 && rest[lastUser].role !== "user") lastUser--;
|
|
124
|
+
const tail = rest.slice(lastUser); // 完整保留 (结束于 user 或 in-flight 工具单元)
|
|
125
|
+
const mid = rest.slice(0, lastUser); // 仅剪这里的历史
|
|
126
|
+
const itemCap = Math.max(2, Number(this.config.memory?.max_history_items) || 40);
|
|
127
|
+
const trimmed = this._ensureContextFit(mid, { budget, maxItems: itemCap });
|
|
128
|
+
return [...systems, ...trimmed, ...tail];
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
_getSession(sessionKey) {
|
|
132
|
+
// 先信息量感知裁剪, 再 + 绝对硬兜底: 即便 config 异常/压缩失败, 历史也放得下
|
|
133
|
+
const raw = this.sessionStore.deriveCompacted(sessionKey || "default");
|
|
134
|
+
return this._ensureContextFit(this._trimHistory(raw));
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
// 追加一轮对话为不可变事件 (append-only, 永不重写日志)
|
|
138
|
+
// v1.1.1: user+assistant 一次批量落盘 (skipFlush), 一轮对话只写一次磁盘而非两次
|
|
139
|
+
_pushTurn(sessionKey, userMsg, assistant) {
|
|
140
|
+
const k = sessionKey || "default";
|
|
141
|
+
this.sessionStore.append(k, "user/message", { content: String(userMsg) }, Date.now(), { skipFlush: true });
|
|
142
|
+
if (assistant) this.sessionStore.append(k, "assistant/message", { content: String(assistant) }, Date.now(), { skipFlush: true });
|
|
143
|
+
this.sessionStore.flush(k);
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
// 加载历史: 先尝试结构化压缩(超阈值), 再按预算裁剪
|
|
147
|
+
async _loadHistory(sessionKey) {
|
|
148
|
+
const k = sessionKey || "default";
|
|
149
|
+
await this._maybeCompact(k);
|
|
150
|
+
return this._getSession(k).map((m) => ({ ...m }));
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
// 会话压缩: 未压缩部分超 token 阈值时, 把最旧一半压成结构化摘要并持久化到日志
|
|
154
|
+
// (吸收 OpenClaw compaction: 摘要替换被压缩区间, 日志本身不可变)
|
|
155
|
+
async _maybeCompact(sessionKey) {
|
|
156
|
+
if (!this.llm) return;
|
|
157
|
+
if (!(await this._auxLlmReady())) return; // 模型不可用时跳过压缩 (交给 _trimHistory 硬裁剪)
|
|
158
|
+
const events = this.sessionStore.replay(sessionKey);
|
|
159
|
+
let upToSeq = 0;
|
|
160
|
+
for (const e of events) if (e.type === "compaction/summary") upToSeq = e.data?.upToSeq || 0;
|
|
161
|
+
const tail = events.filter((e) => e.seq > upToSeq && (e.type === "user/message" || e.type === "assistant/message"));
|
|
162
|
+
if (!tail.length) return;
|
|
163
|
+
const tokenBudget = Number(this.config.memory?.history_token_budget) || 4000;
|
|
164
|
+
const total = tail.reduce((a, e) => a + estimateTokens(e.data?.content), 0);
|
|
165
|
+
if (total <= tokenBudget * 1.5) return; // 未超阈值不压缩
|
|
166
|
+
const split = Math.floor(tail.length / 2);
|
|
167
|
+
const old = tail.slice(0, split);
|
|
168
|
+
if (old.length < 2) return; // 太少不值得压
|
|
169
|
+
const lastSeq = old[old.length - 1].seq;
|
|
170
|
+
const transcript = transcriptToText(old.map((e) => ({ role: e.type === "user/message" ? "user" : "assistant", content: e.data?.content })));
|
|
171
|
+
try {
|
|
172
|
+
const r = await this.llm.chat(buildCompactionMessages(transcript), { timeoutMs: AUX_LLM_TIMEOUT_MS, retryMax: 0 });
|
|
173
|
+
const summary = r?.content;
|
|
174
|
+
if (summary) this.sessionStore.append(sessionKey, "compaction/summary", { summary, upToSeq: lastSeq });
|
|
175
|
+
} catch {
|
|
176
|
+
// 压缩失败静默降级, 交给 _trimHistory 硬裁剪
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|