@bolloon/bolloon-agent 0.1.40 → 0.1.42
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/.comm/README.md +21 -0
- package/.comm/default/2026-06-17T08-23-00-017Z-8a735de8.md +7 -0
- package/CLAUDE.md +3 -0
- package/dist/agents/intent-classifier.js +99 -0
- package/dist/agents/pi-sdk.js +1364 -58
- package/dist/agents/pre-tool-validator.js +2 -1
- package/dist/agents/shell-guard.js +9 -3
- package/dist/agents/shell-tool.js +28 -13
- package/dist/agents/task-state.js +224 -0
- package/dist/agents/workflow-pivot-loop.js +30 -0
- package/dist/bollharness-integration/gate-state-machine.js +13 -0
- package/dist/bollharness-integration/integration.js +71 -0
- package/dist/bollharness-integration/skill-adapter.js +52 -127
- package/dist/bootstrap/context-hierarchy.js +6 -4
- package/dist/cli/interface.js +28 -0
- package/dist/documents/reader.js +14 -0
- package/dist/git-transport/chat-render.js +155 -0
- package/dist/git-transport/chat-repo.js +370 -0
- package/dist/git-transport/chat-types.js +23 -0
- package/dist/git-transport/chat-watch.js +102 -0
- package/dist/index.js +471 -25
- package/dist/llm/pi-ai.js +103 -27
- package/dist/network/local-inbox-bus.js +73 -0
- package/dist/network/p2p-direct.js +3 -4
- package/dist/pi-ecosystem-judgment/adaptive-scan.js +6 -2
- package/dist/pi-ecosystem-judgment/causal-judge.js +3 -3
- package/dist/pi-ecosystem-judgment/index.js +1 -1
- package/dist/security/tool-gate.js +11 -0
- package/dist/web/client.js +2876 -3770
- package/dist/web/components/p2p/index.js +226 -264
- package/dist/web/server.js +83 -15
- package/dist/web/ui/message-renderer.js +326 -442
- package/dist/web/ui/step-timeline.js +255 -351
- package/package.json +2 -2
- package/scripts/lefthook-helper.sh +69 -0
- package/dist/web/components/p2p/P2PModal.js +0 -188
- package/dist/web/components/p2p/p2p-modal.js +0 -657
- package/dist/web/components/p2p/p2p-tools.js +0 -248
|
@@ -116,7 +116,8 @@ function shellGuardGate(tool, args, mode) {
|
|
|
116
116
|
// 只对**写/删/改**类命令生效 (rm, cp, mv, sed -i, echo >); 读类命令 (cat, head, tail, ls) 跳过
|
|
117
117
|
const READ_BINS = new Set(['cat', 'head', 'tail', 'wc', 'ls', 'echo', 'pwd', 'date', 'mkdir', 'touch', 'find', 'grep', 'git', 'node', 'npm', 'npx', 'tsx', 'tsc', 'vitest']);
|
|
118
118
|
const FILENAME_DENYLIST = [
|
|
119
|
-
|
|
119
|
+
// 2026-06-17 (Q2-B): 解除 pi-sdk.ts — agent 可改自己源码
|
|
120
|
+
'shell-guard.ts', 'shell-tool.ts',
|
|
120
121
|
'.env', 'package.json', 'tsconfig.json',
|
|
121
122
|
];
|
|
122
123
|
const isReadOperation = READ_BINS.has(bin);
|
|
@@ -51,7 +51,10 @@ import * as os from 'os';
|
|
|
51
51
|
const FALLBACK_COMMAND_ALLOWLIST = new Set([
|
|
52
52
|
'git', 'node', 'npm', 'npx', 'tsx', 'tsc', 'vitest',
|
|
53
53
|
'cat', 'head', 'tail', 'wc', 'ls', 'echo', 'pwd', 'date',
|
|
54
|
-
'mkdir', 'touch'
|
|
54
|
+
'mkdir', 'touch',
|
|
55
|
+
// M3.5 (2026-06-17): Windows cmd 内置命令 (cmd.exe shell 模式)
|
|
56
|
+
// 'cat'/'ls' 在 Git Bash 才存在, Windows 原生 cmd 用 'type'/'dir'
|
|
57
|
+
'type', 'dir', 'cd', 'where', 'set', 'echo.',
|
|
55
58
|
]);
|
|
56
59
|
const FALLBACK_PATH_ALLOWLIST = [
|
|
57
60
|
// 自由区: AI 可以改
|
|
@@ -66,7 +69,9 @@ const FALLBACK_PATH_ALLOWLIST = [
|
|
|
66
69
|
'README.md'
|
|
67
70
|
];
|
|
68
71
|
const FALLBACK_PATH_DENYLIST = [
|
|
69
|
-
|
|
72
|
+
// 2026-06-17 (Q2-B): 解除 pi-sdk.ts denylist — 现在允许 agent 改自己源码以自进化
|
|
73
|
+
// 原屏蔽理由: "LLM 抽象层" — 但 owner 决定 auto-evolve 需要这个能力
|
|
74
|
+
// 仍保留: shell-guard.ts / shell-tool.ts — 改这两个等于改护栏本身, fail-open 风险太高
|
|
70
75
|
/(^|\/)src\/agents\/shell-guard\.ts$/, // 护栏本身
|
|
71
76
|
/(^|\/)src\/agents\/shell-tool\.ts$/, // shell 工具实现
|
|
72
77
|
/(^|\/)src\/heartbeat\//, // 心跳
|
|
@@ -79,7 +84,8 @@ const FALLBACK_PATH_DENYLIST = [
|
|
|
79
84
|
/(^|\/)\.git\//,
|
|
80
85
|
/(^|\/)\.bolloon\//, // 策略文件 / sessions / persona
|
|
81
86
|
/(^|\/)dist\//,
|
|
82
|
-
/
|
|
87
|
+
// 2026-06-17: node_modules 不再 denylist, 因为 M3.4 自动 commit 阶段需要 npm install / npx vitest
|
|
88
|
+
// 通过 allowlist 限定 agent 只能 npm install, 不能 rm node_modules (shell arg denylist 仍禁 rm -rf)
|
|
83
89
|
];
|
|
84
90
|
const FALLBACK_ARG_DENYLIST = [
|
|
85
91
|
/^\s*push\s+(-f|--force)/i,
|
|
@@ -9,8 +9,18 @@
|
|
|
9
9
|
* 禁区列表在 shell-guard.ts, AI 改不了那个文件.
|
|
10
10
|
*/
|
|
11
11
|
import { spawn } from 'child_process';
|
|
12
|
-
import
|
|
13
|
-
|
|
12
|
+
import { checkCommand, checkWritePath } from './shell-guard.js';
|
|
13
|
+
/**
|
|
14
|
+
* 把参数 quote 一下,避免 shell 元字符注入 (&& | ; ` > < 等).
|
|
15
|
+
* Windows: 用双引号包, 内部双引号转义.
|
|
16
|
+
*/
|
|
17
|
+
function shellQuoteArgs(arg) {
|
|
18
|
+
if (process.platform !== 'win32')
|
|
19
|
+
return arg; // POSIX shell 由 checkCommand 防护
|
|
20
|
+
if (!/[\s"&|<>^()%!`]/.test(arg))
|
|
21
|
+
return arg;
|
|
22
|
+
return `"${arg.replace(/"/g, '""')}"`;
|
|
23
|
+
}
|
|
14
24
|
/**
|
|
15
25
|
* 在沙箱里跑一条命令
|
|
16
26
|
* @param cmd 可执行文件名, 必须命中白名单
|
|
@@ -41,20 +51,25 @@ export async function shellExec(cmd, args = [], opts = {}) {
|
|
|
41
51
|
}
|
|
42
52
|
}
|
|
43
53
|
}
|
|
44
|
-
// 3.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
54
|
+
// 3. 确定运行 cwd
|
|
55
|
+
// M3.5 (2026-06-17 + 2026-06-18): 全部走 process.cwd() (即 agent 实际工作目录).
|
|
56
|
+
// 老逻辑写命令走 sandbox 反而出问题 — sandbox 是个独立 git 目录,
|
|
57
|
+
// 在里面 git add/commit/push 看不到外面的 working tree,
|
|
58
|
+
// agent 看到的 status 是 sandbox 自己的, 容易误判.
|
|
59
|
+
// 现在全部走 cwd. (注: 老逻辑 `getSandboxCwd()` 已废弃, 不再使用)
|
|
60
|
+
const cwd = process.cwd();
|
|
52
61
|
// 4. 跑命令
|
|
62
|
+
// M3.5 (2026-06-17): Windows 上 ls/cat/pwd 不在 PATH, 必须用 cmd 内置命令 (dir/type/cd)
|
|
63
|
+
// 但 cmd 内置命令不能在 shell: false 下跑, 切到 shell: true (Windows shell: cmd.exe, POSIX shell: /bin/sh)
|
|
64
|
+
// 风险: 元字符注入 — 通过 checkCommand + arg denylist 防护, 加 shellQuoteArgs() 转义
|
|
65
|
+
const isWindows = process.platform === 'win32';
|
|
66
|
+
const needsShell = isWindows; // Windows: 必须用 shell 才能跑 cmd 内置
|
|
67
|
+
const quotedArgs = isWindows ? args.map(shellQuoteArgs) : args;
|
|
53
68
|
return new Promise((resolve) => {
|
|
54
|
-
const proc = spawn(cmd,
|
|
55
|
-
cwd
|
|
69
|
+
const proc = spawn(cmd, quotedArgs, {
|
|
70
|
+
cwd,
|
|
56
71
|
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, // 禁止 git 弹交互
|
|
57
|
-
shell:
|
|
72
|
+
shell: needsShell,
|
|
58
73
|
windowsHide: true
|
|
59
74
|
});
|
|
60
75
|
let stdout = '';
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task State Machine — M3.2 (2026-06-17)
|
|
3
|
+
*
|
|
4
|
+
* 长期项目响应的核心: agent 能跟踪一个多步任务的进度, 跨 loop / 跨 session 持久化
|
|
5
|
+
*
|
|
6
|
+
* 设计:
|
|
7
|
+
* - 状态写入 ~/.bolloon/tasks/<task-id>.yaml
|
|
8
|
+
* - task-id 由 LLM 在 user request 进来时生成 (或由 intent-classifier 自动给)
|
|
9
|
+
* - 每个 plan 步骤有 status: pending | running | done | failed | skipped
|
|
10
|
+
* - 写盘后 agent 在每个 loop iter 读 plan, 选下一个 pending step
|
|
11
|
+
*
|
|
12
|
+
* 简化版: 不强制 LLM 生成 plan (M3.1 已把 task plan 作为可选项, LLM 走 multi_step intent 时
|
|
13
|
+
* 主动 plan), 只提供 plan 状态机 + 持久化 + 给 agent 的 API
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from 'fs/promises';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import * as os from 'os';
|
|
18
|
+
import * as crypto from 'crypto';
|
|
19
|
+
const TASK_DIR = path.join(os.homedir(), '.bolloon', 'tasks');
|
|
20
|
+
async function ensureDir() {
|
|
21
|
+
await fs.mkdir(TASK_DIR, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
export function generateTaskId() {
|
|
24
|
+
return `task-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* YAML 简单序列化 (避免拉 js-yaml 进 task 模块)
|
|
28
|
+
* 只支持 Task schema
|
|
29
|
+
*/
|
|
30
|
+
function serializeTask(t) {
|
|
31
|
+
const lines = [
|
|
32
|
+
`id: ${t.id}`,
|
|
33
|
+
`goal: ${JSON.stringify(t.goal)}`,
|
|
34
|
+
`createdAt: ${t.createdAt}`,
|
|
35
|
+
`updatedAt: ${t.updatedAt}`,
|
|
36
|
+
`status: ${t.status}`,
|
|
37
|
+
`branch: ${t.branch || ''}`,
|
|
38
|
+
`sessionKey: ${t.sessionKey || ''}`,
|
|
39
|
+
`steps:`,
|
|
40
|
+
];
|
|
41
|
+
for (const s of t.steps) {
|
|
42
|
+
lines.push(` - id: ${s.id}`);
|
|
43
|
+
lines.push(` description: ${JSON.stringify(s.description)}`);
|
|
44
|
+
lines.push(` status: ${s.status}`);
|
|
45
|
+
if (s.startedAt)
|
|
46
|
+
lines.push(` startedAt: ${s.startedAt}`);
|
|
47
|
+
if (s.finishedAt)
|
|
48
|
+
lines.push(` finishedAt: ${s.finishedAt}`);
|
|
49
|
+
if (s.resultSummary)
|
|
50
|
+
lines.push(` resultSummary: ${JSON.stringify(s.resultSummary)}`);
|
|
51
|
+
if (s.error)
|
|
52
|
+
lines.push(` error: ${JSON.stringify(s.error)}`);
|
|
53
|
+
}
|
|
54
|
+
return lines.join('\n') + '\n';
|
|
55
|
+
}
|
|
56
|
+
function parseTask(content) {
|
|
57
|
+
try {
|
|
58
|
+
const lines = content.split('\n');
|
|
59
|
+
const task = {
|
|
60
|
+
id: '', goal: '', createdAt: '', updatedAt: '', status: 'planning', steps: [],
|
|
61
|
+
};
|
|
62
|
+
let currentStep = null;
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
const trimmed = line.trimEnd();
|
|
65
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
66
|
+
continue;
|
|
67
|
+
if (trimmed.startsWith('- id:')) {
|
|
68
|
+
if (currentStep)
|
|
69
|
+
task.steps.push(currentStep);
|
|
70
|
+
currentStep = { id: trimmed.slice(6).trim(), description: '', status: 'pending' };
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (currentStep && trimmed.startsWith('description:')) {
|
|
74
|
+
currentStep.description = JSON.parse(trimmed.slice(12).trim());
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (currentStep && trimmed.startsWith('status:')) {
|
|
78
|
+
currentStep.status = trimmed.slice(7).trim();
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (currentStep && trimmed.startsWith('startedAt:')) {
|
|
82
|
+
currentStep.startedAt = trimmed.slice(10).trim();
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (currentStep && trimmed.startsWith('finishedAt:')) {
|
|
86
|
+
currentStep.finishedAt = trimmed.slice(12).trim();
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (currentStep && trimmed.startsWith('resultSummary:')) {
|
|
90
|
+
currentStep.resultSummary = JSON.parse(trimmed.slice(15).trim());
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (currentStep && trimmed.startsWith('error:')) {
|
|
94
|
+
currentStep.error = JSON.parse(trimmed.slice(7).trim());
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (trimmed.startsWith('id:')) {
|
|
98
|
+
task.id = trimmed.slice(3).trim();
|
|
99
|
+
}
|
|
100
|
+
else if (trimmed.startsWith('goal:')) {
|
|
101
|
+
task.goal = JSON.parse(trimmed.slice(5).trim());
|
|
102
|
+
}
|
|
103
|
+
else if (trimmed.startsWith('createdAt:')) {
|
|
104
|
+
task.createdAt = trimmed.slice(10).trim();
|
|
105
|
+
}
|
|
106
|
+
else if (trimmed.startsWith('updatedAt:')) {
|
|
107
|
+
task.updatedAt = trimmed.slice(10).trim();
|
|
108
|
+
}
|
|
109
|
+
else if (trimmed.startsWith('status:')) {
|
|
110
|
+
task.status = trimmed.slice(7).trim();
|
|
111
|
+
}
|
|
112
|
+
else if (trimmed.startsWith('branch:')) {
|
|
113
|
+
const v = trimmed.slice(7).trim();
|
|
114
|
+
if (v)
|
|
115
|
+
task.branch = v;
|
|
116
|
+
}
|
|
117
|
+
else if (trimmed.startsWith('sessionKey:')) {
|
|
118
|
+
const v = trimmed.slice(11).trim();
|
|
119
|
+
if (v)
|
|
120
|
+
task.sessionKey = v;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (currentStep)
|
|
124
|
+
task.steps.push(currentStep);
|
|
125
|
+
if (!task.id)
|
|
126
|
+
return null;
|
|
127
|
+
return task;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function taskPath(id) {
|
|
134
|
+
await ensureDir();
|
|
135
|
+
return path.join(TASK_DIR, `${id}.yaml`);
|
|
136
|
+
}
|
|
137
|
+
export async function createTask(opts) {
|
|
138
|
+
const now = new Date().toISOString();
|
|
139
|
+
const task = {
|
|
140
|
+
id: generateTaskId(),
|
|
141
|
+
goal: opts.goal,
|
|
142
|
+
createdAt: now,
|
|
143
|
+
updatedAt: now,
|
|
144
|
+
status: 'running',
|
|
145
|
+
branch: opts.branch,
|
|
146
|
+
sessionKey: opts.sessionKey,
|
|
147
|
+
steps: (opts.steps || []).map((desc, i) => ({
|
|
148
|
+
id: `step-${i + 1}`,
|
|
149
|
+
description: desc,
|
|
150
|
+
status: i === 0 ? 'running' : 'pending',
|
|
151
|
+
startedAt: i === 0 ? now : undefined,
|
|
152
|
+
})),
|
|
153
|
+
};
|
|
154
|
+
const p = await taskPath(task.id);
|
|
155
|
+
await fs.writeFile(p, serializeTask(task), 'utf-8');
|
|
156
|
+
return task;
|
|
157
|
+
}
|
|
158
|
+
export async function getTask(id) {
|
|
159
|
+
try {
|
|
160
|
+
const p = await taskPath(id);
|
|
161
|
+
const content = await fs.readFile(p, 'utf-8');
|
|
162
|
+
return parseTask(content);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export async function updateTask(id, patch) {
|
|
169
|
+
const t = await getTask(id);
|
|
170
|
+
if (!t)
|
|
171
|
+
return null;
|
|
172
|
+
Object.assign(t, patch, { updatedAt: new Date().toISOString() });
|
|
173
|
+
const p = await taskPath(id);
|
|
174
|
+
await fs.writeFile(p, serializeTask(t), 'utf-8');
|
|
175
|
+
return t;
|
|
176
|
+
}
|
|
177
|
+
export async function updateStep(taskId, stepId, patch) {
|
|
178
|
+
const t = await getTask(taskId);
|
|
179
|
+
if (!t)
|
|
180
|
+
return null;
|
|
181
|
+
const step = t.steps.find((s) => s.id === stepId);
|
|
182
|
+
if (!step)
|
|
183
|
+
return null;
|
|
184
|
+
Object.assign(step, patch);
|
|
185
|
+
// 推进下一步: 当前 step done → 下一个 pending 变 running
|
|
186
|
+
if (patch.status === 'done' || patch.status === 'failed' || patch.status === 'skipped') {
|
|
187
|
+
step.finishedAt = step.finishedAt || new Date().toISOString();
|
|
188
|
+
const nextPending = t.steps.find((s) => s.status === 'pending');
|
|
189
|
+
if (nextPending && patch.status === 'done') {
|
|
190
|
+
nextPending.status = 'running';
|
|
191
|
+
nextPending.startedAt = new Date().toISOString();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// 检查整 task 是否完成
|
|
195
|
+
if (t.steps.every((s) => s.status === 'done' || s.status === 'skipped')) {
|
|
196
|
+
t.status = 'completed';
|
|
197
|
+
}
|
|
198
|
+
else if (t.steps.some((s) => s.status === 'failed')) {
|
|
199
|
+
// 不立即置 failed, 留个机会让 LLM 调整
|
|
200
|
+
}
|
|
201
|
+
t.updatedAt = new Date().toISOString();
|
|
202
|
+
const p = await taskPath(taskId);
|
|
203
|
+
await fs.writeFile(p, serializeTask(t), 'utf-8');
|
|
204
|
+
return t;
|
|
205
|
+
}
|
|
206
|
+
export async function listTasks(limit = 20) {
|
|
207
|
+
try {
|
|
208
|
+
await ensureDir();
|
|
209
|
+
const files = await fs.readdir(TASK_DIR);
|
|
210
|
+
const tasks = [];
|
|
211
|
+
for (const f of files) {
|
|
212
|
+
if (!f.endsWith('.yaml'))
|
|
213
|
+
continue;
|
|
214
|
+
const content = await fs.readFile(path.join(TASK_DIR, f), 'utf-8');
|
|
215
|
+
const t = parseTask(content);
|
|
216
|
+
if (t)
|
|
217
|
+
tasks.push(t);
|
|
218
|
+
}
|
|
219
|
+
return tasks.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).slice(0, limit);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -123,10 +123,14 @@ export class WorkflowPivotLoop {
|
|
|
123
123
|
async execute(input, llm, systemPrompt, streamCallback) {
|
|
124
124
|
this.streamCallback = streamCallback;
|
|
125
125
|
this.state = this.createInitialState();
|
|
126
|
+
console.log(`[pivot] execute: input chars=${input.length}, systemPrompt chars=${systemPrompt.length}`);
|
|
126
127
|
this.messageHistory = [{ role: 'user', content: input }];
|
|
127
128
|
// Analyze task complexity and adapt config
|
|
128
129
|
const taskProfile = analyzeTaskComplexity(input);
|
|
129
130
|
const effectiveConfig = this.adaptConfigForTask(taskProfile);
|
|
131
|
+
// 2026-06-18 (supervisor): taskProfile 算的 tokenBudget 只看 input 长度, 但 systemPrompt 53K 时不够
|
|
132
|
+
// 把 effectiveConfig.maxTokenBudget 提到 systemPrompt * 1.2 留余量, 简单问题也走完
|
|
133
|
+
effectiveConfig.maxTokenBudget = Math.max(effectiveConfig.maxTokenBudget, Math.ceil(systemPrompt.length * 1.2));
|
|
130
134
|
this.emit({
|
|
131
135
|
type: 'status',
|
|
132
136
|
content: `🔍 任务复杂度: ${taskProfile.complexity} (预估 ${taskProfile.estimatedSteps} 步)`,
|
|
@@ -148,10 +152,13 @@ export class WorkflowPivotLoop {
|
|
|
148
152
|
// Build context for LLM
|
|
149
153
|
const context = this.buildContext();
|
|
150
154
|
const fullPrompt = `${systemPrompt}\n\n${context}`;
|
|
155
|
+
console.log(`[pivot] iter=${this.state.iteration} ctx=${context.length} fullPrompt=${fullPrompt.length} budget=${effectiveConfig.maxTokenBudget}`);
|
|
151
156
|
try {
|
|
152
157
|
// Call LLM
|
|
158
|
+
const t0 = Date.now();
|
|
153
159
|
const llmResponse = await llm.chat(context, systemPrompt);
|
|
154
160
|
const reply = llmResponse.reply.trim();
|
|
161
|
+
console.log(`[pivot] iter=${this.state.iteration} LLM took=${Date.now() - t0}ms reply=${reply.length} head=${reply.substring(0, 80).replace(/\n/g, ' ')}`);
|
|
155
162
|
this.emit({ type: 'token', content: reply.substring(0, 100) });
|
|
156
163
|
// Estimate token usage
|
|
157
164
|
this.state.totalTokens += this.estimateTokens(fullPrompt) + this.estimateTokens(reply);
|
|
@@ -377,6 +384,29 @@ export class WorkflowPivotLoop {
|
|
|
377
384
|
}
|
|
378
385
|
}
|
|
379
386
|
}
|
|
387
|
+
// 2026-06-18 (supervisor): Claude Code 风格 <tool_call><tool_name>...</tool_call>
|
|
388
|
+
// bolloon agent 之前一直被 model 教用这个格式, 但 pivot loop 只认 3 个旧 pattern.
|
|
389
|
+
// 现在加: <tool_call><invoke name="X"><parameter name="k">v</parameter></invoke></tool_call>
|
|
390
|
+
const toolCodeRe = /<tool_code>([\s\S]*?)<\/tool_code>/g;
|
|
391
|
+
while ((match = toolCodeRe.exec(content)) !== null) {
|
|
392
|
+
const block = match[1];
|
|
393
|
+
const invokeRe = /<invoke\s+name="(\w+)"\s*>([\s\S]*?)<\/invoke>/g;
|
|
394
|
+
let im;
|
|
395
|
+
while ((im = invokeRe.exec(block)) !== null) {
|
|
396
|
+
const name = im[1];
|
|
397
|
+
if (!this.tools.has(name))
|
|
398
|
+
continue;
|
|
399
|
+
const args = {};
|
|
400
|
+
const paramRe = /<parameter\s+name="(\w+)"\s*>([\s\S]*?)<\/parameter>/g;
|
|
401
|
+
let pm;
|
|
402
|
+
while ((pm = paramRe.exec(im[2])) !== null) {
|
|
403
|
+
args[pm[1]] = pm[2].trim().replace(/^["']|['"]$/g, '');
|
|
404
|
+
}
|
|
405
|
+
if (!pending.some(p => p.name === name)) {
|
|
406
|
+
pending.push({ name, args, description: '', parameters: {} });
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
380
410
|
// Pattern 1: Chinese format "调用工具: tool_name(args)"
|
|
381
411
|
const pattern1 = /调用工具[::]\s*(\w+)\s*\(([^)]*)\)/g;
|
|
382
412
|
while ((match = pattern1.exec(content)) !== null) {
|
|
@@ -106,6 +106,19 @@ export class GateStateMachine {
|
|
|
106
106
|
getState() {
|
|
107
107
|
return { ...this.state, artifacts: new Map(this.state.artifacts), conversationHistory: [...this.state.conversationHistory] };
|
|
108
108
|
}
|
|
109
|
+
restore(state) {
|
|
110
|
+
this.state = {
|
|
111
|
+
currentGate: state.currentGate,
|
|
112
|
+
entrySatisfied: state.entrySatisfied,
|
|
113
|
+
blockers: [...state.blockers],
|
|
114
|
+
requiredArtifact: state.requiredArtifact,
|
|
115
|
+
requiredNextSkill: state.requiredNextSkill,
|
|
116
|
+
requiredReviewSubstrate: state.requiredReviewSubstrate,
|
|
117
|
+
valueInjection: state.valueInjection,
|
|
118
|
+
artifacts: new Map(state.artifacts),
|
|
119
|
+
conversationHistory: [...state.conversationHistory],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
109
122
|
getCurrentGate() {
|
|
110
123
|
return this.state.currentGate;
|
|
111
124
|
}
|
|
@@ -11,6 +11,8 @@ import { GateStateMachine } from './gate-state-machine.js';
|
|
|
11
11
|
import { GuardChecker, runGuards } from './guard-checker.js';
|
|
12
12
|
import { ContextRouter } from './context-router.js';
|
|
13
13
|
import { createSkillAdapter } from './skill-adapter.js';
|
|
14
|
+
import * as fs from 'fs';
|
|
15
|
+
import * as path from 'path';
|
|
14
16
|
/**
|
|
15
17
|
* Main integration class
|
|
16
18
|
*/
|
|
@@ -29,11 +31,76 @@ export class BollharnessIntegration {
|
|
|
29
31
|
contextEnabled: config.contextEnabled ?? true,
|
|
30
32
|
skillsEnabled: config.skillsEnabled ?? true,
|
|
31
33
|
gatesEnabled: config.gatesEnabled ?? true,
|
|
34
|
+
persistencePath: config.persistencePath,
|
|
32
35
|
};
|
|
33
36
|
this.gateMachine = new GateStateMachine();
|
|
34
37
|
this.guardChecker = new GuardChecker();
|
|
35
38
|
this.contextRouter = new ContextRouter();
|
|
36
39
|
this.skillAdapter = createSkillAdapter();
|
|
40
|
+
if (this.config.persistencePath) {
|
|
41
|
+
this.loadState();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
saveState() {
|
|
45
|
+
if (!this.config.persistencePath)
|
|
46
|
+
return;
|
|
47
|
+
try {
|
|
48
|
+
const gateState = this.gateMachine.getState();
|
|
49
|
+
const data = JSON.stringify({
|
|
50
|
+
gateState: {
|
|
51
|
+
currentGate: gateState.currentGate,
|
|
52
|
+
entrySatisfied: gateState.entrySatisfied,
|
|
53
|
+
blockers: gateState.blockers,
|
|
54
|
+
requiredArtifact: gateState.requiredArtifact,
|
|
55
|
+
requiredNextSkill: gateState.requiredNextSkill,
|
|
56
|
+
valueInjection: gateState.valueInjection,
|
|
57
|
+
artifacts: Array.from(gateState.artifacts.entries()),
|
|
58
|
+
conversationHistory: gateState.conversationHistory,
|
|
59
|
+
},
|
|
60
|
+
sessionArchives: this.sessionArchives,
|
|
61
|
+
currentSessionId: this.currentSessionId,
|
|
62
|
+
}, null, 2);
|
|
63
|
+
const dir = path.dirname(this.config.persistencePath);
|
|
64
|
+
if (!fs.existsSync(dir)) {
|
|
65
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
fs.writeFileSync(this.config.persistencePath, data, 'utf-8');
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
console.warn(`[BollharnessIntegration] 持久化失败: ${err.message}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
loadState() {
|
|
74
|
+
if (!this.config.persistencePath)
|
|
75
|
+
return;
|
|
76
|
+
try {
|
|
77
|
+
if (!fs.existsSync(this.config.persistencePath))
|
|
78
|
+
return;
|
|
79
|
+
const raw = fs.readFileSync(this.config.persistencePath, 'utf-8');
|
|
80
|
+
const data = JSON.parse(raw);
|
|
81
|
+
if (data.gateState) {
|
|
82
|
+
this.gateMachine.restore({
|
|
83
|
+
currentGate: data.gateState.currentGate,
|
|
84
|
+
entrySatisfied: data.gateState.entrySatisfied,
|
|
85
|
+
blockers: data.gateState.blockers,
|
|
86
|
+
requiredArtifact: data.gateState.requiredArtifact,
|
|
87
|
+
requiredNextSkill: data.gateState.requiredNextSkill,
|
|
88
|
+
requiredReviewSubstrate: undefined,
|
|
89
|
+
valueInjection: data.gateState.valueInjection || '',
|
|
90
|
+
artifacts: new Map(data.gateState.artifacts || []),
|
|
91
|
+
conversationHistory: data.gateState.conversationHistory || [],
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (data.sessionArchives) {
|
|
95
|
+
this.sessionArchives = data.sessionArchives;
|
|
96
|
+
}
|
|
97
|
+
if (data.currentSessionId) {
|
|
98
|
+
this.currentSessionId = data.currentSessionId;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
console.warn(`[BollharnessIntegration] 状态恢复失败: ${err.message}`);
|
|
103
|
+
}
|
|
37
104
|
}
|
|
38
105
|
/**
|
|
39
106
|
* Process a file edit with all enabled checks
|
|
@@ -128,12 +195,14 @@ export class BollharnessIntegration {
|
|
|
128
195
|
*/
|
|
129
196
|
submitGateArtifact(name, artifact) {
|
|
130
197
|
this.gateMachine.submitArtifact(name, artifact);
|
|
198
|
+
this.saveState();
|
|
131
199
|
}
|
|
132
200
|
/**
|
|
133
201
|
* Attempt gate transition
|
|
134
202
|
*/
|
|
135
203
|
async transitionGate(reviewResult) {
|
|
136
204
|
const transition = await this.gateMachine.transition(reviewResult);
|
|
205
|
+
this.saveState();
|
|
137
206
|
return {
|
|
138
207
|
success: transition.blockers.length === 0,
|
|
139
208
|
transition,
|
|
@@ -246,6 +315,7 @@ export class BollharnessIntegration {
|
|
|
246
315
|
};
|
|
247
316
|
this.sessionArchives.push(archive);
|
|
248
317
|
this.currentSessionId = id;
|
|
318
|
+
this.saveState();
|
|
249
319
|
return archive;
|
|
250
320
|
}
|
|
251
321
|
/**
|
|
@@ -333,6 +403,7 @@ export class BollharnessIntegration {
|
|
|
333
403
|
*/
|
|
334
404
|
startNewSession() {
|
|
335
405
|
this.currentSessionId = `session_${Date.now()}`;
|
|
406
|
+
this.saveState();
|
|
336
407
|
return this.currentSessionId;
|
|
337
408
|
}
|
|
338
409
|
/**
|