@bolloon/bolloon-agent 0.1.35 → 0.1.36
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/.playwright-mcp/page-2026-06-16T07-51-45-706Z.yml +130 -0
- package/.playwright-mcp/page-2026-06-16T07-56-44-116Z.yml +131 -0
- package/Bolloon.md +45 -4
- package/Dive-into/CITATION.cff +17 -0
- package/Dive-into/LICENSE +25 -0
- package/Dive-into/README.md +598 -0
- package/Dive-into/README_zh.md +599 -0
- package/Dive-into/assets/context.png +0 -0
- package/Dive-into/assets/extensibility.png +0 -0
- package/Dive-into/assets/iteration.png +0 -0
- package/Dive-into/assets/layered_architecture.png +0 -0
- package/Dive-into/assets/main_structure.png +0 -0
- package/Dive-into/assets/permission.png +0 -0
- package/Dive-into/assets/session_compact.png +0 -0
- package/Dive-into/assets/subagent.png +0 -0
- package/Dive-into/paper/Dive_into_Claude_Code.pdf +0 -0
- package/README.md +1 -1
- package/dist/agents/p2p-chat-tools.js +6 -6
- package/dist/agents/permission-mode.js +115 -0
- package/dist/agents/pi-sdk.js +435 -25
- package/dist/agents/pre-tool-validator.js +194 -0
- package/dist/agents/workflow-pivot-loop.js +113 -12
- package/dist/bollharness/src/scripts/checks/check_doc_freshness.js +1 -1
- package/dist/bollharness/src/scripts/checks/check_doc_links.js +1 -1
- package/dist/bollharness/src/scripts/context_router.js +1 -1
- package/dist/bollharness/src/scripts/deploy-guard.js +1 -1
- package/dist/bollharness/src/scripts/guard_router.js +1 -1
- package/dist/bollharness/src/scripts/hooks/_hook_output.js +1 -1
- package/dist/bollharness/src/scripts/hooks/risk-tracker.js +1 -1
- package/dist/bollharness-integration/context-router.js +2 -2
- package/dist/bollharness-integration/guard-checker.js +1 -1
- package/dist/bootstrap/bootstrap.js +3 -0
- package/dist/bootstrap/context-collector.js +17 -1
- package/dist/bootstrap/context-hierarchy.js +218 -0
- package/dist/bootstrap/lifecycle-hooks.js +86 -24
- package/dist/bootstrap/project-context.js +10 -3
- package/dist/context-compaction/auto-compact.js +144 -0
- package/dist/context-compaction/budget-gate.js +28 -0
- package/dist/context-compaction/budget-reduce.js +35 -0
- package/dist/context-compaction/context-collapse.js +66 -0
- package/dist/context-compaction/index.js +21 -0
- package/dist/context-compaction/microcompact.js +51 -0
- package/dist/context-compaction/pipeline.js +123 -0
- package/dist/context-compaction/snip.js +45 -0
- package/dist/context-compaction/token-estimator.js +35 -0
- package/dist/context-compaction/types.js +19 -0
- package/dist/heartbeat/HealthMonitor.js +3 -2
- package/dist/index.js +38 -2
- package/dist/llm/llm-judgment-client.js +32 -30
- package/dist/llm/pi-ai.js +103 -16
- package/dist/llm/system-prompt/health.js +129 -0
- package/dist/llm/system-prompt/registry.js +246 -0
- package/dist/llm/system-prompt/strip-hibsml.js +51 -0
- package/dist/llm/tool-manifest/ask_user_input.js +35 -0
- package/dist/llm/tool-manifest/bash.js +23 -0
- package/dist/llm/tool-manifest/create_file.js +24 -0
- package/dist/llm/tool-manifest/fetch_sports_data.js +26 -0
- package/dist/llm/tool-manifest/image_search.js +24 -0
- package/dist/llm/tool-manifest/index.js +69 -0
- package/dist/llm/tool-manifest/mcp.js +43 -0
- package/dist/llm/tool-manifest/message_compose.js +31 -0
- package/dist/llm/tool-manifest/places.js +83 -0
- package/dist/llm/tool-manifest/present_files.js +21 -0
- package/dist/llm/tool-manifest/recipe.js +40 -0
- package/dist/llm/tool-manifest/recommend_apps.js +23 -0
- package/dist/llm/tool-manifest/str_replace.js +27 -0
- package/dist/llm/tool-manifest/types.js +7 -0
- package/dist/llm/tool-manifest/view.js +24 -0
- package/dist/llm/tool-manifest/weather.js +27 -0
- package/dist/llm/tool-manifest/web.js +51 -0
- package/dist/network/p2p-direct.js +23 -0
- package/dist/network/source-intent-broadcaster.js +203 -0
- package/dist/network/source-intent.js +100 -0
- package/dist/pi-ecosystem-judgment/adaptive-scan.js +48 -0
- package/dist/pi-ecosystem-judgment/injection-gate.js +54 -12
- package/dist/pi-ecosystem-judgment/value-injection.js +8 -2
- package/dist/security/context-router-tool.js +1 -1
- package/dist/security/input-scanner.js +223 -0
- package/dist/web/client.js +3677 -4161
- package/dist/web/components/p2p/P2PModal.js +188 -0
- package/dist/web/components/p2p/index.js +264 -226
- package/dist/web/components/p2p/p2p-modal.js +657 -0
- package/dist/web/components/p2p/p2p-tools.js +248 -0
- package/dist/web/index.html +58 -67
- package/dist/web/server.js +299 -117
- package/dist/web/style.css +531 -249
- package/dist/web/ui/message-renderer.js +463 -0
- package/dist/web/ui/step-timeline.js +375 -0
- package/lefthook.yml +4 -0
- package/package.json +1 -1
- package/scripts/auto-evolve-loop.ts +105 -0
- package/scripts/build-web.ts +35 -1
- package/scripts/validate-system-prompt.ts +142 -0
- package/src/agents/p2p-chat-tools.ts +6 -6
- package/src/agents/permission-mode.ts +127 -0
- package/src/agents/pi-sdk.ts +461 -28
- package/src/agents/pre-tool-validator.ts +213 -0
- package/src/agents/workflow-pivot-loop.ts +110 -19
- package/src/bollharness/CLAUDE.md +1 -1
- package/src/bollharness/README.md +2 -2
- package/src/bollharness/README.zh-CN.md +2 -2
- package/src/bollharness/reference/boll-reference/scripts/hooks/find-boll-root.sh +2 -2
- package/src/bollharness/scripts/context-fragments/truth-source-hierarchy.md +2 -2
- package/src/bollharness/scripts/context-fragments/version-sources.md +1 -1
- package/src/bollharness/scripts/hooks/find-project-root.sh +4 -4
- package/src/bollharness/src/scripts/checks/check_doc_freshness.ts +1 -1
- package/src/bollharness/src/scripts/checks/check_doc_links.ts +1 -1
- package/src/bollharness/src/scripts/context_router.ts +1 -1
- package/src/bollharness/src/scripts/deploy-guard.ts +1 -1
- package/src/bollharness/src/scripts/guard_router.ts +1 -1
- package/src/bollharness/src/scripts/hooks/_hook_output.ts +1 -1
- package/src/bollharness/src/scripts/hooks/risk-tracker.ts +1 -1
- package/src/bollharness/templates/scaffold/.gitignore.append +1 -1
- package/src/bollharness/templates/scaffold/CLAUDE.md +2 -2
- package/src/bollharness-integration/context-router.ts +2 -2
- package/src/bollharness-integration/guard-checker.ts +1 -1
- package/src/bootstrap/bootstrap.ts +3 -0
- package/src/bootstrap/context-collector.ts +30 -1
- package/src/bootstrap/context-hierarchy.ts +283 -0
- package/src/bootstrap/lifecycle-hooks.ts +139 -26
- package/src/bootstrap/project-context.ts +11 -3
- package/src/context-compaction/auto-compact.ts +153 -0
- package/src/context-compaction/budget-gate.ts +32 -0
- package/src/context-compaction/budget-reduce.ts +37 -0
- package/src/context-compaction/context-collapse.ts +72 -0
- package/src/context-compaction/index.ts +24 -0
- package/src/context-compaction/microcompact.ts +54 -0
- package/src/context-compaction/pipeline.ts +133 -0
- package/src/context-compaction/snip.ts +51 -0
- package/src/context-compaction/token-estimator.ts +36 -0
- package/src/context-compaction/types.ts +99 -0
- package/src/heartbeat/HealthMonitor.ts +3 -2
- package/src/index.ts +36 -2
- package/src/llm/llm-judgment-client.ts +36 -35
- package/src/llm/pi-ai.ts +111 -16
- package/src/llm/system-prompt/health.ts +159 -0
- package/src/llm/system-prompt/layers/channel/local.md +14 -0
- package/src/llm/system-prompt/layers/channel/p2p-agent.md +18 -0
- package/src/llm/system-prompt/layers/channel/p2p-visitor.md +19 -0
- package/src/llm/system-prompt/layers/core/artifacts_storage.md +89 -0
- package/src/llm/system-prompt/layers/core/evenhandedness.md +21 -0
- package/src/llm/system-prompt/layers/core/hibs_reminders.md +15 -0
- package/src/llm/system-prompt/layers/core/identity.md +37 -0
- package/src/llm/system-prompt/layers/core/knowledge.md +17 -0
- package/src/llm/system-prompt/layers/core/memory_system.md +12 -0
- package/src/llm/system-prompt/layers/core/network_filesystem.md +28 -0
- package/src/llm/system-prompt/layers/core/refusal.md +37 -0
- package/src/llm/system-prompt/layers/core/tone.md +31 -0
- package/src/llm/system-prompt/layers/core/tools.thin.md +13 -0
- package/src/llm/system-prompt/layers/core/wellbeing.md +41 -0
- package/src/llm/system-prompt/layers/role/architect.md +20 -0
- package/src/llm/system-prompt/layers/role/expert.md +19 -0
- package/src/llm/system-prompt/layers/role/implementer.md +15 -0
- package/src/llm/system-prompt/layers/role/security.md +15 -0
- package/src/llm/system-prompt/layers/tool/artifacts.md +72 -0
- package/src/llm/system-prompt/layers/tool/bash.md +25 -0
- package/src/llm/system-prompt/layers/tool/hibs_api.md +171 -0
- package/src/llm/system-prompt/layers/tool/image_search.md +70 -0
- package/src/llm/system-prompt/layers/tool/manifest.md +89 -0
- package/src/llm/system-prompt/layers/tool/mcp_apps.md +53 -0
- package/src/llm/system-prompt/layers/tool/web_search.md +83 -0
- package/src/llm/system-prompt/registry.ts +325 -0
- package/src/llm/system-prompt/strip-hibsml.ts +52 -0
- package/src/llm/tool-manifest/ask_user_input.ts +37 -0
- package/src/llm/tool-manifest/bash.ts +25 -0
- package/src/llm/tool-manifest/create_file.ts +26 -0
- package/src/llm/tool-manifest/fetch_sports_data.ts +28 -0
- package/src/llm/tool-manifest/image_search.ts +26 -0
- package/src/llm/tool-manifest/index.ts +88 -0
- package/src/llm/tool-manifest/mcp.ts +46 -0
- package/src/llm/tool-manifest/message_compose.ts +33 -0
- package/src/llm/tool-manifest/places.ts +86 -0
- package/src/llm/tool-manifest/present_files.ts +23 -0
- package/src/llm/tool-manifest/recipe.ts +42 -0
- package/src/llm/tool-manifest/recommend_apps.ts +25 -0
- package/src/llm/tool-manifest/str_replace.ts +29 -0
- package/src/llm/tool-manifest/types.ts +52 -0
- package/src/llm/tool-manifest/view.ts +26 -0
- package/src/llm/tool-manifest/weather.ts +29 -0
- package/src/llm/tool-manifest/web.ts +54 -0
- package/src/network/p2p-direct.ts +22 -0
- package/src/network/source-intent-broadcaster.ts +242 -0
- package/src/network/source-intent.ts +167 -0
- package/src/security/context-router-tool.ts +1 -1
- package/src/security/input-scanner.ts +287 -0
- package/src/web/client.js +271 -2817
- package/src/web/client.ts +4326 -0
- package/src/web/index.html +58 -67
- package/src/web/server.ts +301 -141
- package/src/web/style.css +531 -249
- package/src/web/ui/message-renderer.ts +540 -0
- package/src/web/ui/step-timeline.ts +394 -0
- package/test-results/.last-run.json +6 -0
- package/test-results/src-test-web-loop-status-b-5734c-atus-bar-/346/230/276/347/244/272-spinner-/344/270/215/346/230/276/347/244/272/345/276/252/347/216/257/350/256/241/346/225/260/error-context.md +285 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Hierarchy — 4 级 Bolloon.md 查找 + 合并优先级
|
|
3
|
+
*
|
|
4
|
+
* 设计: 严格对齐 Claude Code 论文 4 级层次
|
|
5
|
+
* 1. Managed — /etc/bolloon/Bolloon.md (企业 IT 部署, 系统级)
|
|
6
|
+
* 2. User — ~/.bolloon/Bolloon.md (用户级, 跨项目)
|
|
7
|
+
* 3. Project — <cwd>/Bolloon.md 或 .claude/rules/*.md
|
|
8
|
+
* 4. Local — <cwd>/CLAUDE.local.md (个人覆盖, .gitignore)
|
|
9
|
+
*
|
|
10
|
+
* 注入约定 (论文):
|
|
11
|
+
* - 拼到 system prompt 顶部 (作为 user context, 概率性遵守)
|
|
12
|
+
* - 不混入 user 消息 (避免 prompt injection)
|
|
13
|
+
* - 截断时反向砍 local → project → user → managed (优先保 managed, 因为是 bolloon 自身约束)
|
|
14
|
+
*
|
|
15
|
+
* 兼容:
|
|
16
|
+
* - 同时识别 Bolloon.md 和 Bolloon.md (论文用前者, bolloon 历史用后者)
|
|
17
|
+
* - 文件名按优先级: Bolloon.md > Bolloon.md
|
|
18
|
+
*
|
|
19
|
+
* 失败静默: 任何 IO 错误 → 返回 null, 不阻塞主流程.
|
|
20
|
+
*/
|
|
21
|
+
import * as fs from 'fs/promises';
|
|
22
|
+
import * as os from 'os';
|
|
23
|
+
import * as path from 'path';
|
|
24
|
+
export const DEFAULT_PATHS = {
|
|
25
|
+
managed: '/etc/bolloon/Bolloon.md',
|
|
26
|
+
user: '', // 用 resolveUserPath 填
|
|
27
|
+
project: '', // 用 resolveProjectPaths 填
|
|
28
|
+
projectRulesDir: '',
|
|
29
|
+
local: '',
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* 单层字符上限 (P-Action 4 收紧, 阶段 0 把 4 级层次控制在 ≤ 2KB).
|
|
33
|
+
* 反向截断策略保证 managed 优先, 单层超限由 truncate() 截断.
|
|
34
|
+
*/
|
|
35
|
+
export const DEFAULT_MAX_CHARS = {
|
|
36
|
+
managed: 700, // 1500 → 700, 保 bolloon 自身约束
|
|
37
|
+
user: 500, // 1500 → 500, 跨项目偏好
|
|
38
|
+
project: 500, // 2500 → 500, 项目规则
|
|
39
|
+
local: 300, // 1500 → 300, 个人覆盖
|
|
40
|
+
};
|
|
41
|
+
// ============================================================
|
|
42
|
+
// 路径解析
|
|
43
|
+
// ============================================================
|
|
44
|
+
export function resolveUserPath(home) {
|
|
45
|
+
const h = home ?? process.env.HOME ?? os.homedir() ?? '/tmp';
|
|
46
|
+
return path.join(h, '.bolloon', 'Bolloon.md');
|
|
47
|
+
}
|
|
48
|
+
export function resolveProjectPaths(cwd) {
|
|
49
|
+
return {
|
|
50
|
+
project: path.join(cwd, 'Bolloon.md'),
|
|
51
|
+
projectRulesDir: path.join(cwd, '.claude', 'rules'),
|
|
52
|
+
local: path.join(cwd, 'CLAUDE.local.md'),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// ============================================================
|
|
56
|
+
// 单层读取 (优先 Bolloon.md, fallback Bolloon.md)
|
|
57
|
+
// ============================================================
|
|
58
|
+
/**
|
|
59
|
+
* 读 1 个文件路径, 失败/缺失返回 null.
|
|
60
|
+
* 字符上限由调用方截断 (这里只负责读全文).
|
|
61
|
+
*/
|
|
62
|
+
async function readRuleFile(filePath) {
|
|
63
|
+
try {
|
|
64
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
65
|
+
return content.length > 0 ? content : null;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 读 project 层: 优先 Bolloon.md, fallback Bolloon.md, 最后尝试 .claude/rules/*.md
|
|
73
|
+
*/
|
|
74
|
+
async function readProjectLayer(projectPath, projectRulesDir, maxChars) {
|
|
75
|
+
// 1. Bolloon.md
|
|
76
|
+
let content = await readRuleFile(projectPath);
|
|
77
|
+
if (content)
|
|
78
|
+
return truncate(content, maxChars);
|
|
79
|
+
// 2. Bolloon.md (向后兼容)
|
|
80
|
+
const bolloonMdPath = path.join(path.dirname(projectPath), 'Bolloon.md');
|
|
81
|
+
content = await readRuleFile(bolloonMdPath);
|
|
82
|
+
if (content)
|
|
83
|
+
return truncate(content, maxChars);
|
|
84
|
+
// 3. .claude/rules/*.md (合并所有)
|
|
85
|
+
try {
|
|
86
|
+
const entries = await fs.readdir(projectRulesDir, { withFileTypes: true });
|
|
87
|
+
const ruleFiles = entries
|
|
88
|
+
.filter((e) => e.isFile() && e.name.endsWith('.md'))
|
|
89
|
+
.map((e) => path.join(projectRulesDir, e.name))
|
|
90
|
+
.sort();
|
|
91
|
+
if (ruleFiles.length > 0) {
|
|
92
|
+
const parts = [];
|
|
93
|
+
let total = 0;
|
|
94
|
+
for (const f of ruleFiles) {
|
|
95
|
+
const c = await readRuleFile(f);
|
|
96
|
+
if (c) {
|
|
97
|
+
parts.push(`### ${path.basename(f, '.md')}\n${c}`);
|
|
98
|
+
total += c.length;
|
|
99
|
+
if (total >= maxChars)
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (parts.length > 0)
|
|
104
|
+
return truncate(parts.join('\n\n'), maxChars);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch { /* dir 不存在 */ }
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* 并行读 4 层, 任何一层失败 = null (不阻塞其他).
|
|
112
|
+
*/
|
|
113
|
+
export async function collectHierarchyLayers(opts) {
|
|
114
|
+
const limits = { ...DEFAULT_MAX_CHARS, ...(opts.limits?.maxChars ?? {}) };
|
|
115
|
+
const home = opts.home ?? process.env.HOME ?? os.homedir() ?? '/tmp';
|
|
116
|
+
const managedPath = opts.limits?.paths?.managed ?? DEFAULT_PATHS.managed;
|
|
117
|
+
const userPath = opts.limits?.paths?.user ?? resolveUserPath(home);
|
|
118
|
+
const projectPath = opts.limits?.paths?.project ?? path.join(opts.cwd, 'Bolloon.md');
|
|
119
|
+
const projectRulesDir = opts.limits?.paths?.projectRulesDir ?? path.join(opts.cwd, '.claude', 'rules');
|
|
120
|
+
const localPath = opts.limits?.paths?.local ?? path.join(opts.cwd, 'CLAUDE.local.md');
|
|
121
|
+
const [managed, user, project, local] = await Promise.all([
|
|
122
|
+
readRuleFile(managedPath).then((c) => c ? truncate(c, limits.managed) : null),
|
|
123
|
+
readRuleFile(userPath).then((c) => c ? truncate(c, limits.user) : null),
|
|
124
|
+
readProjectLayer(projectPath, projectRulesDir, limits.project),
|
|
125
|
+
readRuleFile(localPath).then((c) => c ? truncate(c, limits.local) : null),
|
|
126
|
+
]);
|
|
127
|
+
return { managed, user, project, local };
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* 4 级合并总字符上限 (P-Action 4 收紧, ≤ 2KB ≈ 500 tokens).
|
|
131
|
+
* 反向截断 (local → project → user → managed) 保证 managed 不丢.
|
|
132
|
+
*/
|
|
133
|
+
export const DEFAULT_MERGE_MAX_CHARS = 2000;
|
|
134
|
+
/**
|
|
135
|
+
* 按 4 级顺序 (managed → user → project → local) 拼接为 markdown 片段.
|
|
136
|
+
*
|
|
137
|
+
* 截断策略: 反向砍 (local → project → user → managed), 优先保 managed.
|
|
138
|
+
* - 总长 < maxChars: 不截断
|
|
139
|
+
* - 总长 ≥ maxChars: 按 (local, project, user) 顺序逐步砍到 firstParagraphs=1
|
|
140
|
+
* - 最后只保 managed (bolloon 自身约束不能丢)
|
|
141
|
+
*/
|
|
142
|
+
export function mergeHierarchyLayers(layers, opts = {}) {
|
|
143
|
+
const maxChars = opts.maxChars ?? DEFAULT_MERGE_MAX_CHARS;
|
|
144
|
+
const parts = [];
|
|
145
|
+
const labels = [
|
|
146
|
+
['managed', '管理规则 (Managed)'],
|
|
147
|
+
['user', '用户规则 (User)'],
|
|
148
|
+
['project', '项目规则 (Project)'],
|
|
149
|
+
['local', '本地规则 (Local)'],
|
|
150
|
+
];
|
|
151
|
+
for (const [key, label] of labels) {
|
|
152
|
+
const content = layers[key];
|
|
153
|
+
if (content) {
|
|
154
|
+
parts.push(`## ${label}\n\n${content}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (parts.length === 0)
|
|
158
|
+
return '';
|
|
159
|
+
let result = parts.join('\n\n---\n\n');
|
|
160
|
+
if (result.length <= maxChars)
|
|
161
|
+
return result;
|
|
162
|
+
// 超限: 反向砍 (local → project → user), 二级回退到 firstParagraphs=1
|
|
163
|
+
return truncateMerged(layers, maxChars);
|
|
164
|
+
}
|
|
165
|
+
function truncateMerged(layers, maxChars) {
|
|
166
|
+
const order = [
|
|
167
|
+
['local', '本地规则 (Local)'],
|
|
168
|
+
['project', '项目规则 (Project)'],
|
|
169
|
+
['user', '用户规则 (User)'],
|
|
170
|
+
['managed', '管理规则 (Managed)'],
|
|
171
|
+
];
|
|
172
|
+
const capped = { ...layers };
|
|
173
|
+
// 反复压缩, 直到总长 ≤ maxChars
|
|
174
|
+
let depth = 0; // 0=原始, 1=firstParagraphs=1, 2=砍半
|
|
175
|
+
while (depth < 3) {
|
|
176
|
+
const parts = [];
|
|
177
|
+
for (const [key, label] of order) {
|
|
178
|
+
const c = capped[key];
|
|
179
|
+
if (!c)
|
|
180
|
+
continue;
|
|
181
|
+
let text = c;
|
|
182
|
+
if (depth === 1)
|
|
183
|
+
text = firstParagraphs(c, 1);
|
|
184
|
+
if (depth === 2)
|
|
185
|
+
text = firstParagraphs(c, 1, Math.floor(maxChars / 4));
|
|
186
|
+
parts.push(`## ${label}\n\n${text}`);
|
|
187
|
+
}
|
|
188
|
+
const joined = parts.reverse().join('\n\n---\n\n'); // 恢复 managed → user → project → local
|
|
189
|
+
if (joined.length <= maxChars)
|
|
190
|
+
return joined;
|
|
191
|
+
depth++;
|
|
192
|
+
}
|
|
193
|
+
// 最后保 managed 一行
|
|
194
|
+
return `## 管理规则 (Managed)\n\n${firstParagraphs(layers.managed ?? '(无)', 1, maxChars - 50)}`;
|
|
195
|
+
}
|
|
196
|
+
// ============================================================
|
|
197
|
+
// 工具
|
|
198
|
+
// ============================================================
|
|
199
|
+
function truncate(s, maxChars) {
|
|
200
|
+
if (s.length <= maxChars)
|
|
201
|
+
return s;
|
|
202
|
+
return s.substring(0, maxChars) + '\n... (truncated)';
|
|
203
|
+
}
|
|
204
|
+
function firstParagraphs(text, count, maxLen) {
|
|
205
|
+
const paragraphs = text.split(/\n\s*\n/).filter((p) => p.trim().length > 0);
|
|
206
|
+
let result = paragraphs.slice(0, count).join('\n\n');
|
|
207
|
+
if (maxLen && result.length > maxLen) {
|
|
208
|
+
result = result.substring(0, maxLen) + '...';
|
|
209
|
+
}
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
// ============================================================
|
|
213
|
+
// 测试钩子
|
|
214
|
+
// ============================================================
|
|
215
|
+
/** 重置模块级状态 (供测试). 当前无状态, 保留为占位. */
|
|
216
|
+
export function _resetHierarchyForTest() {
|
|
217
|
+
// no-op (collect 是纯函数, 但保留 API 一致)
|
|
218
|
+
}
|
|
@@ -69,36 +69,29 @@ function sanitizeChannelId(id) {
|
|
|
69
69
|
// 防止路径穿越
|
|
70
70
|
return id.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
|
|
71
71
|
}
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
{ re: /\bdd\s+if=.*\s+of=\/dev\//, reason: '禁止 dd 覆盖块设备' },
|
|
78
|
-
{ re: /\bcurl\s+.*\|\s*(ba)?sh\b/, reason: '禁止 curl|sh 直执行' },
|
|
79
|
-
{ re: />\s*\/dev\/sd[a-z]/, reason: '禁止写裸设备' },
|
|
80
|
-
];
|
|
72
|
+
// ============================================================
|
|
73
|
+
// PreToolUse (P2: 4 步链式 validator + permission mode)
|
|
74
|
+
// ============================================================
|
|
75
|
+
import { resolvePermissionMode } from '../agents/permission-mode.js';
|
|
76
|
+
import { validatePreToolUse } from '../agents/pre-tool-validator.js';
|
|
81
77
|
/**
|
|
82
|
-
* PreToolUse:
|
|
83
|
-
*
|
|
84
|
-
*
|
|
78
|
+
* PreToolUse: 4 步链式 validator
|
|
79
|
+
* 1. modeGate (bypassPermissions + 非 shell 直接放行)
|
|
80
|
+
* 2. blacklistGate (6 模式危险命令)
|
|
81
|
+
* 3. shellGuardGate (路径黑名单, 绕过 mode 永远生效)
|
|
82
|
+
* 4. schemaGate (第一版 stub: always allow)
|
|
83
|
+
*
|
|
84
|
+
* 向后兼容: 不传 permissionMode → 默认 'default' (env BOLLOON_PERM_MODE 仍生效)
|
|
85
|
+
* 失败静默: 任何异常 → allowed: true (与原行为一致)
|
|
85
86
|
*/
|
|
86
87
|
export async function onPreToolUse(opts) {
|
|
87
88
|
try {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const cmd = String(opts.args.command || opts.args.cmd || '');
|
|
91
|
-
for (const { re, reason } of DANGEROUS_PATTERNS) {
|
|
92
|
-
if (re.test(cmd)) {
|
|
93
|
-
return { allowed: false, reason };
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return { allowed: true };
|
|
89
|
+
const mode = resolvePermissionMode({ permissionMode: opts.permissionMode });
|
|
90
|
+
return validatePreToolUse(opts.tool, opts.args || {}, mode);
|
|
98
91
|
}
|
|
99
92
|
catch (err) {
|
|
100
|
-
console.warn('[lifecycle-hooks] onPreToolUse failed (silent):', err);
|
|
101
|
-
return { allowed: true }; // 失败放行 (不阻塞)
|
|
93
|
+
console.warn('[lifecycle-hooks] onPreToolUse failed (silent, allowing):', err);
|
|
94
|
+
return { allowed: true, mode: 'default' }; // 失败放行 (不阻塞)
|
|
102
95
|
}
|
|
103
96
|
}
|
|
104
97
|
// ============================================================
|
|
@@ -107,3 +100,72 @@ export async function onPreToolUse(opts) {
|
|
|
107
100
|
export function getStopLogPathForTest() {
|
|
108
101
|
return STOP_LOG;
|
|
109
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* PostToolUse: 工具调用后审计
|
|
105
|
+
* 默认 always continue. 项目可挂 shell-guard audit log / judgment 注入门 / 监控门
|
|
106
|
+
* 失败静默: 任何异常 → continue: true (不阻塞主循环)
|
|
107
|
+
*/
|
|
108
|
+
export async function onPostToolUse(opts) {
|
|
109
|
+
try {
|
|
110
|
+
// 审计日志: 写 ~/.bolloon/sessions/post-tool-audit.jsonl
|
|
111
|
+
const fs = await import('fs/promises');
|
|
112
|
+
const os = await import('os');
|
|
113
|
+
const path = await import('path');
|
|
114
|
+
const file = path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'sessions', 'post-tool-audit.jsonl');
|
|
115
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
116
|
+
const entry = {
|
|
117
|
+
ts: new Date().toISOString(),
|
|
118
|
+
tool: opts.tool,
|
|
119
|
+
success: opts.result.success,
|
|
120
|
+
durationMs: opts.durationMs ?? 0,
|
|
121
|
+
// 不写 args (可能含密钥), 只写 result 的 success/error
|
|
122
|
+
error: opts.result.error?.substring(0, 200) || null,
|
|
123
|
+
};
|
|
124
|
+
await fs.appendFile(file, JSON.stringify(entry) + '\n', 'utf-8').catch(() => { });
|
|
125
|
+
return { continue: true };
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
console.warn('[lifecycle-hooks] onPostToolUse failed (silent, allowing):', err);
|
|
129
|
+
return { continue: true };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export async function onJudgmentInjected(opts) {
|
|
133
|
+
try {
|
|
134
|
+
const fs = await import('fs/promises');
|
|
135
|
+
const os = await import('os');
|
|
136
|
+
const path = await import('path');
|
|
137
|
+
const file = path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'sessions', 'judgment-injected.jsonl');
|
|
138
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
139
|
+
const entry = {
|
|
140
|
+
ts: new Date().toISOString(),
|
|
141
|
+
channelId: opts.channelId || 'unknown',
|
|
142
|
+
judgmentIds: opts.judgmentIds,
|
|
143
|
+
userInputSnippet: (opts.userInput || '').substring(0, 200),
|
|
144
|
+
};
|
|
145
|
+
await fs.appendFile(file, JSON.stringify(entry) + '\n', 'utf-8').catch(() => { });
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
console.warn('[lifecycle-hooks] onJudgmentInjected failed (silent):', err);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
export async function onMonitorViolation(opts) {
|
|
152
|
+
try {
|
|
153
|
+
const fs = await import('fs/promises');
|
|
154
|
+
const os = await import('os');
|
|
155
|
+
const path = await import('path');
|
|
156
|
+
const file = path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'sessions', 'monitor-violation.jsonl');
|
|
157
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
158
|
+
const entry = {
|
|
159
|
+
ts: new Date().toISOString(),
|
|
160
|
+
channelId: opts.channelId || 'unknown',
|
|
161
|
+
judgmentId: opts.judgmentId,
|
|
162
|
+
rule: opts.rule.substring(0, 200),
|
|
163
|
+
actualBehavior: opts.actualBehavior.substring(0, 200),
|
|
164
|
+
severity: opts.severity,
|
|
165
|
+
};
|
|
166
|
+
await fs.appendFile(file, JSON.stringify(entry) + '\n', 'utf-8').catch(() => { });
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
console.warn('[lifecycle-hooks] onMonitorViolation failed (silent):', err);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -6,14 +6,21 @@
|
|
|
6
6
|
* 字符上限默认 4000 (system prompt 不能太长).
|
|
7
7
|
* 超限时按优先级砍: TODO 列表 → 压缩 Bolloon.md → 砍 judgments top values
|
|
8
8
|
*/
|
|
9
|
-
const DEFAULT_MAX_CHARS = 4000
|
|
9
|
+
const DEFAULT_MAX_CHARS = 8000; // 从 4000 提到 8000, 给 4 级 Bolloon.md 留空间
|
|
10
10
|
const BOLLOON_MD_KEEP = 500; // 砍到这么长
|
|
11
11
|
export function formatContextForSystemPrompt(ctx, opts = {}) {
|
|
12
12
|
const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
|
|
13
13
|
const lines = [];
|
|
14
14
|
lines.push(`# 你的项目上下文 (自动 bootstrap, 时间: ${ctx.collectedAt})`);
|
|
15
15
|
lines.push('');
|
|
16
|
-
// 1.
|
|
16
|
+
// 1. Bolloon.md 4 级层次 (Claude Code 论文, 严格 1:1)
|
|
17
|
+
// managed → user → project → local, 已在 collector 层 merge
|
|
18
|
+
if (ctx.hierarchy && ctx.hierarchy.merged && ctx.hierarchy.merged.length > 0) {
|
|
19
|
+
lines.push('## 规则层次 (4 级, Claude Code 论文对齐)');
|
|
20
|
+
lines.push(ctx.hierarchy.merged);
|
|
21
|
+
lines.push('');
|
|
22
|
+
}
|
|
23
|
+
// 1b. 项目名 + Bolloon.md 摘要 (向后兼容, Bolloon.md 也算 project 层之一)
|
|
17
24
|
lines.push(`## 项目: ${ctx.projectName}`);
|
|
18
25
|
lines.push(`- 路径: ${ctx.projectRoot}`);
|
|
19
26
|
if (ctx.bolloonMd) {
|
|
@@ -40,7 +47,7 @@ export function formatContextForSystemPrompt(ctx, opts = {}) {
|
|
|
40
47
|
}
|
|
41
48
|
// 3. Persona
|
|
42
49
|
if (ctx.persona) {
|
|
43
|
-
lines.push(`##
|
|
50
|
+
lines.push(`## 用户身份 (persona)`);
|
|
44
51
|
lines.push(`- 名字: ${ctx.persona.name}`);
|
|
45
52
|
if (ctx.persona.description)
|
|
46
53
|
lines.push(`- 描述: ${ctx.persona.description}`);
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-Compact — 第 5 层: LLM 摘要 (最后手段)
|
|
3
|
+
*
|
|
4
|
+
* 论文:
|
|
5
|
+
* - 兜底层, budget > 100% 时才触发
|
|
6
|
+
* - 用 LLM 生成完整摘要, 替代前 K 对 user/assistant
|
|
7
|
+
* - 摘要 prompt 明确要求"必须保留出现过的 tool names"
|
|
8
|
+
* - 缓存到 ~/.bolloon/sessions/<channel>/compaction-cache.json
|
|
9
|
+
* - LLM 失败 → fallback 保留原文
|
|
10
|
+
*
|
|
11
|
+
* 关键不变量: 这是**破坏性**的 (折叠 N 对成 1 条), 调用方接受新 history
|
|
12
|
+
*
|
|
13
|
+
* 失败静默: LLM 失败 → 返回原 history (与现状一致)
|
|
14
|
+
*/
|
|
15
|
+
import * as crypto from 'crypto';
|
|
16
|
+
import * as fs from 'fs/promises';
|
|
17
|
+
import * as os from 'os';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
const DEFAULT_COLLAPSE_PAIRS = 5;
|
|
20
|
+
const CACHE_DIR = () => path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'sessions', 'compaction-cache');
|
|
21
|
+
function cacheKey(history, collapsePairs) {
|
|
22
|
+
const slice = history.slice(0, collapsePairs * 2);
|
|
23
|
+
const joined = slice.map((m) => `${m.role}:${m.content}`).join('|');
|
|
24
|
+
return crypto.createHash('sha1').update(joined).digest('hex').slice(0, 12);
|
|
25
|
+
}
|
|
26
|
+
async function readCache(scope, key) {
|
|
27
|
+
try {
|
|
28
|
+
const file = path.join(CACHE_DIR(), `${scope}_${key}.json`);
|
|
29
|
+
const raw = await fs.readFile(file, 'utf-8');
|
|
30
|
+
const obj = JSON.parse(raw);
|
|
31
|
+
if (typeof obj.summary === 'string' && Date.now() - obj.ts < 24 * 60 * 60 * 1000) {
|
|
32
|
+
return obj.summary;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch { /* cache miss or parse err */ }
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
async function writeCache(scope, key, summary) {
|
|
39
|
+
try {
|
|
40
|
+
await fs.mkdir(CACHE_DIR(), { recursive: true });
|
|
41
|
+
const file = path.join(CACHE_DIR(), `${scope}_${key}.json`);
|
|
42
|
+
await fs.writeFile(file, JSON.stringify({ ts: Date.now(), summary, key }), 'utf-8');
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
console.warn('[compactor] cache write failed (silent):', err);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const SUMMARIZE_SYSTEM = `你是一个对话历史压缩助手.
|
|
49
|
+
你的任务: 把给定的对话历史压缩成 1-2 句话的摘要, 保留所有出现过的 tool names.
|
|
50
|
+
要求:
|
|
51
|
+
- 保留所有用户的关键需求
|
|
52
|
+
- 保留所有调用过的 tool names (例如 read_file, shell_exec, use_skill 等)
|
|
53
|
+
- 保留关键决策和发现
|
|
54
|
+
- 不要添加原对话中没有的信息
|
|
55
|
+
- 输出纯文本, 不要 JSON, 不要 markdown 标题`;
|
|
56
|
+
export async function autoCompact(history, opts = {}) {
|
|
57
|
+
try {
|
|
58
|
+
if (opts.skip)
|
|
59
|
+
return { history, applied: false, detail: 'skipped' };
|
|
60
|
+
const collapsePairs = opts.autoCompactCollapsePairs ?? DEFAULT_COLLAPSE_PAIRS;
|
|
61
|
+
// 找前 K 对的边界
|
|
62
|
+
let pairsFound = 0;
|
|
63
|
+
let cutTo = -1;
|
|
64
|
+
for (let i = 0; i < history.length; i++) {
|
|
65
|
+
if (history[i].role === 'user') {
|
|
66
|
+
pairsFound++;
|
|
67
|
+
if (pairsPairsOver(pairsFound, collapsePairs)) {
|
|
68
|
+
cutTo = i;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (cutTo <= 0) {
|
|
74
|
+
return { history, applied: false, detail: 'history too short' };
|
|
75
|
+
}
|
|
76
|
+
const toCollapse = history.slice(0, cutTo);
|
|
77
|
+
const remaining = history.slice(cutTo);
|
|
78
|
+
const scope = opts.cacheScope || 'default';
|
|
79
|
+
// 1. 查缓存 (key 基于实际被切的消息, 不用理论 collapsePairs)
|
|
80
|
+
const key = crypto.createHash('sha1').update(toCollapse.map((m) => `${m.role}:${m.content}`).join('|')).digest('hex').slice(0, 12);
|
|
81
|
+
let summary = await readCache(scope, key);
|
|
82
|
+
// 2. 没缓存就调 LLM
|
|
83
|
+
if (!summary) {
|
|
84
|
+
if (!opts.llmChat) {
|
|
85
|
+
// 没有 LLM 就 fallback 保留原文
|
|
86
|
+
console.warn('[compactor] autoCompact: no llmChat injected, returning original');
|
|
87
|
+
return { history, applied: false, detail: 'no llmChat available' };
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const userPrompt = `以下是 ${cutTo} 条较早的对话历史, 请压缩为 1-2 句摘要:\n\n${formatForSummary(toCollapse)}`;
|
|
91
|
+
summary = await opts.llmChat(SUMMARIZE_SYSTEM, userPrompt);
|
|
92
|
+
if (!summary || summary.trim().length === 0) {
|
|
93
|
+
return { history, applied: false, detail: 'llm returned empty' };
|
|
94
|
+
}
|
|
95
|
+
// 写缓存 (静默, 但 await 确保测试可重现)
|
|
96
|
+
try {
|
|
97
|
+
await writeCache(scope, key, summary);
|
|
98
|
+
}
|
|
99
|
+
catch { /* ignored */ }
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
console.warn('[compactor] autoCompact LLM call failed (silent, fallback):', err);
|
|
103
|
+
return { history, applied: false, detail: 'llm call failed' };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// 3. 折叠: 1 条 summary 消息替代 N 条原文
|
|
107
|
+
const summaryMsg = {
|
|
108
|
+
role: 'system',
|
|
109
|
+
content: `[Auto-Compact Summary] ${summary}`,
|
|
110
|
+
};
|
|
111
|
+
return {
|
|
112
|
+
history: [summaryMsg, ...remaining],
|
|
113
|
+
applied: true,
|
|
114
|
+
detail: `collapsed ${cutTo} messages into 1 summary (cache key: ${key})`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
console.warn('[compactor] autoCompact failed (silent, returning original):', err);
|
|
119
|
+
return { history, applied: false, detail: 'error' };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function pairsPairsOver(found, target) {
|
|
123
|
+
return found > target;
|
|
124
|
+
}
|
|
125
|
+
function formatForSummary(messages) {
|
|
126
|
+
return messages.map((m) => {
|
|
127
|
+
if (m.role === 'user')
|
|
128
|
+
return `用户: ${m.content}`;
|
|
129
|
+
if (m.role === 'assistant')
|
|
130
|
+
return `助手: ${m.content.substring(0, 500)}`;
|
|
131
|
+
if (m.role === 'tool') {
|
|
132
|
+
const r = m.toolResult;
|
|
133
|
+
return `工具: ${typeof r === 'string' ? r : JSON.stringify(r).substring(0, 300)}`;
|
|
134
|
+
}
|
|
135
|
+
return m.content;
|
|
136
|
+
}).join('\n');
|
|
137
|
+
}
|
|
138
|
+
// ============================================================
|
|
139
|
+
// 测试钩子
|
|
140
|
+
// ============================================================
|
|
141
|
+
export function _resetAutoCompactCacheForTest() {
|
|
142
|
+
// 实际缓存由 readCache/writeCache 管理, 这里只暴露接口
|
|
143
|
+
// 测试可以走 fs.rm CACHE_DIR() 清理
|
|
144
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Budget Gate — 预算判定 (不修改消息)
|
|
3
|
+
*
|
|
4
|
+
* 论文: 这是判定阶段, 不是压缩阶段. 触发条件:
|
|
5
|
+
* - ratio < 0.8 → fit, 不压缩
|
|
6
|
+
* - 0.8 ≤ ratio ≤ 1.0 → triggerNextLayer (跑下一层)
|
|
7
|
+
* - ratio > 1.0 → triggerNextLayer, 全部层都跑
|
|
8
|
+
*
|
|
9
|
+
* 失败静默: 任何异常 → 视为 fit (与现状一致, 不冒险压缩)
|
|
10
|
+
*/
|
|
11
|
+
import { estimateTokens } from './token-estimator.js';
|
|
12
|
+
const FIT_THRESHOLD = 0.8;
|
|
13
|
+
export function budgetGate(history, maxTokens = 8000) {
|
|
14
|
+
try {
|
|
15
|
+
const estimatedTokens = estimateTokens(history);
|
|
16
|
+
const ratio = maxTokens > 0 ? estimatedTokens / maxTokens : 0;
|
|
17
|
+
return {
|
|
18
|
+
fit: ratio < FIT_THRESHOLD,
|
|
19
|
+
estimatedTokens,
|
|
20
|
+
ratio,
|
|
21
|
+
triggerNextLayer: ratio >= FIT_THRESHOLD,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
console.warn('[compactor] budgetGate failed (silent, treating as fit):', err);
|
|
26
|
+
return { fit: true, estimatedTokens: 0, ratio: 0, triggerNextLayer: false };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Budget Reduction — 第 1 层: 单条消息大小限制
|
|
3
|
+
*
|
|
4
|
+
* 论文: 总是跑. 截断 > 阈值的单条消息, 加 ... [truncated, original=N chars]
|
|
5
|
+
*
|
|
6
|
+
* 关键: 不破坏 messageHistory 内存结构, 只改 buildContext 输出
|
|
7
|
+
* 但因为 buildContext 需要最终字符串, 这里实际返回新 history
|
|
8
|
+
* (内容相同的引用 = 内存未真正变更)
|
|
9
|
+
*
|
|
10
|
+
* 失败静默: 异常 → 返回原 history
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_MAX_CHARS = 4000;
|
|
13
|
+
export function budgetReduce(history, opts = {}) {
|
|
14
|
+
try {
|
|
15
|
+
if (opts.skip)
|
|
16
|
+
return { history, applied: false, detail: 'skipped' };
|
|
17
|
+
const maxChars = opts.budgetReduceMaxChars ?? DEFAULT_MAX_CHARS;
|
|
18
|
+
let changed = false;
|
|
19
|
+
const out = history.map((m) => {
|
|
20
|
+
if (m.content && m.content.length > maxChars) {
|
|
21
|
+
changed = true;
|
|
22
|
+
return {
|
|
23
|
+
...m,
|
|
24
|
+
content: m.content.substring(0, maxChars) + `\n... [truncated, original=${m.content.length} chars]`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return m;
|
|
28
|
+
});
|
|
29
|
+
return { history: out, applied: changed, detail: changed ? `truncated >${maxChars} chars` : 'no oversized messages' };
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
console.warn('[compactor] budgetReduce failed (silent, returning original):', err);
|
|
33
|
+
return { history, applied: false, detail: 'error' };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Collapse — 第 4 层: 读时虚拟投影 (非破坏)
|
|
3
|
+
*
|
|
4
|
+
* 论文:
|
|
5
|
+
* - 不修改 messageHistory
|
|
6
|
+
* - 在 buildContext() 输出阶段, 前 K 对 user/assistant 替换为 [collapsed: <summary>]
|
|
7
|
+
* - feature flag BOLLOON_CONTEXT_COLLAPSE 默认关闭
|
|
8
|
+
*
|
|
9
|
+
* 实现策略:
|
|
10
|
+
* - 由于 buildContext 阶段才输出最终字符串, 这里"虚拟投影"实际意味着:
|
|
11
|
+
* 返回一个新 history, 前 K 对用一个虚拟的 "collapsed" summary 消息替代
|
|
12
|
+
* - 但 memory 层的 messageHistory 不变 (由调用方负责回滚或仅在 buildContext 内用)
|
|
13
|
+
*
|
|
14
|
+
* 实践: 本 stage 返回一个标记 collapsed=true 的 history, 调用方可以选择:
|
|
15
|
+
* (a) 仅在 buildContext 字符串拼接时替换 (理想)
|
|
16
|
+
* (b) 临时替换 (本次 LLM call 结束后回滚)
|
|
17
|
+
* (c) 永久替换 (但这违背"非破坏"原则)
|
|
18
|
+
*
|
|
19
|
+
* 本模块选 (b): 返回新 history, 标记 collapsedMeta 字段, 文档说明调用方应仅用于构建 LLM 输入
|
|
20
|
+
*
|
|
21
|
+
* 失败静默: 异常 → 返回原 history
|
|
22
|
+
*/
|
|
23
|
+
const DEFAULT_COLLAPSE_PAIRS = 5;
|
|
24
|
+
const DEFAULT_SUMMARY = '[collapsed: N earlier rounds — summary not yet generated]';
|
|
25
|
+
export function isContextCollapseEnabled() {
|
|
26
|
+
return process.env.BOLLOON_CONTEXT_COLLAPSE === '1';
|
|
27
|
+
}
|
|
28
|
+
export function contextCollapse(history, opts = {}) {
|
|
29
|
+
try {
|
|
30
|
+
if (opts.skip || !isContextCollapseEnabled()) {
|
|
31
|
+
return { history, applied: false, detail: isContextCollapseEnabled() ? 'skipped' : 'feature flag off' };
|
|
32
|
+
}
|
|
33
|
+
const collapsePairs = opts.contextCollapseCollapsePairs ?? DEFAULT_COLLAPSE_PAIRS;
|
|
34
|
+
// 找前 K 对 user/assistant 的边界
|
|
35
|
+
let pairsFound = 0;
|
|
36
|
+
let cutTo = -1;
|
|
37
|
+
for (let i = 0; i < history.length; i++) {
|
|
38
|
+
if (history[i].role === 'user') {
|
|
39
|
+
pairsFound++;
|
|
40
|
+
if (pairsFound > collapsePairs) {
|
|
41
|
+
cutTo = i;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (cutTo <= 0) {
|
|
47
|
+
return { history, applied: false, detail: 'history too short' };
|
|
48
|
+
}
|
|
49
|
+
// 把前 cutTo 条压成 1 条 virtual summary message
|
|
50
|
+
const collapsedCount = cutTo;
|
|
51
|
+
const summaryMsg = {
|
|
52
|
+
role: 'system',
|
|
53
|
+
content: `[collapsed: ${collapsedCount} earlier messages (${collapsePairs} pairs) — virtual projection, original history preserved]`,
|
|
54
|
+
};
|
|
55
|
+
const out = [summaryMsg, ...history.slice(cutTo)];
|
|
56
|
+
return {
|
|
57
|
+
history: out,
|
|
58
|
+
applied: true,
|
|
59
|
+
detail: `virtual-collapsed first ${collapsedCount} messages`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
console.warn('[compactor] contextCollapse failed (silent, returning original):', err);
|
|
64
|
+
return { history, applied: false, detail: 'error' };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Compaction — 公共导出 (barrel)
|
|
3
|
+
*
|
|
4
|
+
* 设计: 严格对齐 Claude Code 论文 5 层压缩流水线
|
|
5
|
+
* 1. Budget Reduction (单条消息大小限制)
|
|
6
|
+
* 2. Snip (裁掉老历史, feature flag 关闭)
|
|
7
|
+
* 3. Microcompact (cache-aware 细粒度压缩)
|
|
8
|
+
* 4. Context Collapse (读时虚拟投影, feature flag 关闭)
|
|
9
|
+
* 5. Auto-Compact (LLM 摘要, 兜底)
|
|
10
|
+
*
|
|
11
|
+
* 主要入口: `compactPipeline(history, opts)`
|
|
12
|
+
* 失败静默: 任何 stage 抛错 → pipeline 返回原 history
|
|
13
|
+
*/
|
|
14
|
+
export { compactPipeline, _resetPipelineForTest } from './pipeline.js';
|
|
15
|
+
export { budgetGate } from './budget-gate.js';
|
|
16
|
+
export { budgetReduce } from './budget-reduce.js';
|
|
17
|
+
export { snip, isSnipEnabled } from './snip.js';
|
|
18
|
+
export { microcompact } from './microcompact.js';
|
|
19
|
+
export { contextCollapse, isContextCollapseEnabled } from './context-collapse.js';
|
|
20
|
+
export { autoCompact, _resetAutoCompactCacheForTest } from './auto-compact.js';
|
|
21
|
+
export { estimateTokens, estimateStringTokens } from './token-estimator.js';
|