@bolloon/bolloon-agent 0.5.4 → 0.6.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/dist/agents/agent-identity-store.js +41 -5
- package/dist/agents/background-notices.js +124 -0
- package/dist/agents/channel-identity.js +54 -0
- package/dist/agents/code-write-gate.js +47 -0
- package/dist/agents/deny-pipeline.js +11 -2
- package/dist/agents/egress-sanitize.js +69 -0
- package/dist/agents/error-classifier.js +67 -5
- package/dist/agents/experience-review.js +226 -0
- package/dist/agents/gateway-group.js +153 -18
- package/dist/agents/iteration-budget.js +64 -0
- package/dist/agents/lesson-to-skill.js +211 -0
- package/dist/agents/managed-services.js +64 -0
- package/dist/agents/parse-tool-call.js +43 -32
- package/dist/agents/pi-sdk-session-factory.js +36 -3
- package/dist/agents/pi-sdk-session-manager.js +72 -3
- package/dist/agents/pi-sdk-tools.js +201 -32
- package/dist/agents/pi-sdk.js +798 -206
- package/dist/agents/plan-store.js +36 -0
- package/dist/agents/process-runner.js +64 -2
- package/dist/agents/run-context.js +20 -0
- package/dist/agents/runner-resolver.js +2 -1
- package/dist/agents/shell-guard.js +109 -13
- package/dist/agents/skill-health.js +90 -0
- package/dist/agents/skill-ledger.js +129 -0
- package/dist/agents/tool-loop-guard.js +303 -0
- package/dist/agents/tool-path-args.js +53 -0
- package/dist/agents/tool-result-gate.js +50 -0
- package/dist/agents/tool-subset.js +109 -0
- package/dist/agents/tool-telemetry.js +97 -0
- package/dist/bootstrap/context-collector.js +37 -7
- package/dist/bootstrap/lifecycle-hooks.js +12 -3
- package/dist/bootstrap/persona-init.js +174 -0
- package/dist/bootstrap/persona-loader.js +5 -1
- package/dist/cli/ink-app.js +153 -6
- package/dist/cli/input-paste.js +218 -0
- package/dist/cli/loading-tui.js +3 -1
- package/dist/cli/log-gate.js +7 -0
- package/dist/cli/mention-data.js +30 -0
- package/dist/cli/reasoning-view.js +103 -0
- package/dist/cli/startup-notice.js +25 -0
- package/dist/cli/status-segments.js +87 -2
- package/dist/cli/trace-line.js +54 -0
- package/dist/context-compaction/auto-compact.js +37 -2
- package/dist/electron-build/agents/agent-identity-store.js +43 -5
- package/dist/electron-build/agents/agent-identity-store.js.map +1 -1
- package/dist/electron-build/agents/background-notices.js +138 -0
- package/dist/electron-build/agents/background-notices.js.map +1 -0
- package/dist/electron-build/agents/channel-identity.js +64 -0
- package/dist/electron-build/agents/channel-identity.js.map +1 -0
- package/dist/electron-build/agents/code-write-gate.js +55 -0
- package/dist/electron-build/agents/code-write-gate.js.map +1 -0
- package/dist/electron-build/agents/deny-pipeline.js +11 -2
- package/dist/electron-build/agents/deny-pipeline.js.map +1 -1
- package/dist/electron-build/agents/egress-sanitize.js +76 -0
- package/dist/electron-build/agents/egress-sanitize.js.map +1 -0
- package/dist/electron-build/agents/error-classifier.js +68 -5
- package/dist/electron-build/agents/error-classifier.js.map +1 -1
- package/dist/electron-build/agents/experience-review.js +243 -0
- package/dist/electron-build/agents/experience-review.js.map +1 -0
- package/dist/electron-build/agents/iteration-budget.js +73 -0
- package/dist/electron-build/agents/iteration-budget.js.map +1 -0
- package/dist/electron-build/agents/lesson-to-skill.js +228 -0
- package/dist/electron-build/agents/lesson-to-skill.js.map +1 -0
- package/dist/electron-build/agents/managed-services.js +69 -0
- package/dist/electron-build/agents/managed-services.js.map +1 -0
- package/dist/electron-build/agents/parse-tool-call.js +43 -32
- package/dist/electron-build/agents/parse-tool-call.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-session-factory.js +36 -3
- package/dist/electron-build/agents/pi-sdk-session-factory.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-session-manager.js +75 -3
- package/dist/electron-build/agents/pi-sdk-session-manager.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-tools.js +200 -31
- package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +799 -206
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/plan-store.js +37 -0
- package/dist/electron-build/agents/plan-store.js.map +1 -1
- package/dist/electron-build/agents/process-runner.js +71 -2
- package/dist/electron-build/agents/process-runner.js.map +1 -1
- package/dist/electron-build/agents/run-context.js +24 -0
- package/dist/electron-build/agents/run-context.js.map +1 -0
- package/dist/electron-build/agents/runner-resolver.js +2 -1
- package/dist/electron-build/agents/runner-resolver.js.map +1 -1
- package/dist/electron-build/agents/shell-guard.js +110 -14
- package/dist/electron-build/agents/shell-guard.js.map +1 -1
- package/dist/electron-build/agents/skill-health.js +103 -0
- package/dist/electron-build/agents/skill-health.js.map +1 -0
- package/dist/electron-build/agents/skill-ledger.js +146 -0
- package/dist/electron-build/agents/skill-ledger.js.map +1 -0
- package/dist/electron-build/agents/tool-loop-guard.js +319 -0
- package/dist/electron-build/agents/tool-loop-guard.js.map +1 -0
- package/dist/electron-build/agents/tool-path-args.js +63 -0
- package/dist/electron-build/agents/tool-path-args.js.map +1 -0
- package/dist/electron-build/agents/tool-result-gate.js +59 -0
- package/dist/electron-build/agents/tool-result-gate.js.map +1 -0
- package/dist/electron-build/agents/tool-subset.js +116 -0
- package/dist/electron-build/agents/tool-subset.js.map +1 -0
- package/dist/electron-build/agents/tool-telemetry.js +108 -0
- package/dist/electron-build/agents/tool-telemetry.js.map +1 -0
- package/dist/electron-build/bootstrap/context-collector.js +38 -7
- package/dist/electron-build/bootstrap/context-collector.js.map +1 -1
- package/dist/electron-build/bootstrap/lifecycle-hooks.js +12 -3
- package/dist/electron-build/bootstrap/lifecycle-hooks.js.map +1 -1
- package/dist/electron-build/bootstrap/persona-init.js +216 -0
- package/dist/electron-build/bootstrap/persona-init.js.map +1 -0
- package/dist/electron-build/bootstrap/persona-loader.js +5 -1
- package/dist/electron-build/bootstrap/persona-loader.js.map +1 -1
- package/dist/electron-build/context-compaction/auto-compact.js +38 -2
- package/dist/electron-build/context-compaction/auto-compact.js.map +1 -1
- package/dist/electron-build/external-engines/delegate.js +22 -0
- package/dist/electron-build/external-engines/delegate.js.map +1 -1
- package/dist/electron-build/kernel/channel-actor.js +314 -0
- package/dist/electron-build/kernel/channel-actor.js.map +1 -0
- package/dist/electron-build/orbitdb/cid-database.js +55 -0
- package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
- package/dist/electron-build/orbitdb/ipfs-node.js +36 -11
- package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
- package/dist/electron-build/web/server-storage.js +26 -5
- package/dist/electron-build/web/server-storage.js.map +1 -1
- package/dist/external-engines/delegate.js +21 -0
- package/dist/index.js +526 -95
- package/dist/kernel/channel-actor.js +297 -0
- package/dist/kernel/control.js +95 -0
- package/dist/kernel/gate-scan.js +1036 -0
- package/dist/kernel/model-runtime.js +472 -0
- package/dist/kernel/plan-channel-actor.js +298 -0
- package/dist/kernel/plan-constraint.js +165 -0
- package/dist/kernel/plan-deletion.js +32 -0
- package/dist/kernel/plan-modelruntime.js +60 -0
- package/dist/kernel/plan-runcontext.js +163 -0
- package/dist/kernel/plan.js +153 -0
- package/dist/kernel/roster.js +242 -0
- package/dist/orbitdb/cid-database.js +55 -0
- package/dist/orbitdb/event-index.js +370 -0
- package/dist/orbitdb/event-query.js +131 -0
- package/dist/orbitdb/event-shell.js +510 -0
- package/dist/orbitdb/group-access.js +381 -0
- package/dist/orbitdb/group-shards.js +140 -0
- package/dist/orbitdb/ipfs-node.js +36 -11
- package/dist/web/bolloon-web.json +3 -3
- package/dist/web/client.js +4 -2
- package/dist/web/mobile-core.js +7762 -6289
- package/dist/web/routes-mobile-tasks.js +2 -1
- package/dist/web/routes-tasks.js +5 -1
- package/dist/web/server-storage.js +26 -5
- package/dist/web/server.js +60 -18
- package/dist/web/sw.js +1 -1
- package/dist/web/ui/message-renderer.js +473 -29
- package/dist/web/ui/step-timeline.js +13 -12
- package/package.json +2 -1
- package/scripts/build-web.ts +74 -1
- package/dist/constraint-runtime/_archive_helper.d.ts +0 -1
- package/dist/constraint-runtime/_archive_helper.js +0 -9
- package/dist/constraint-runtime/assistant/index.d.ts +0 -11
- package/dist/constraint-runtime/assistant/index.js +0 -12
- package/dist/constraint-runtime/bootstrap/index.d.ts +0 -11
- package/dist/constraint-runtime/bootstrap/index.js +0 -12
- package/dist/constraint-runtime/bridge/index.d.ts +0 -11
- package/dist/constraint-runtime/bridge/index.js +0 -12
- package/dist/constraint-runtime/buddy/index.d.ts +0 -11
- package/dist/constraint-runtime/buddy/index.js +0 -12
- package/dist/constraint-runtime/cli/index.d.ts +0 -11
- package/dist/constraint-runtime/cli/index.js +0 -12
- package/dist/constraint-runtime/components/index.d.ts +0 -11
- package/dist/constraint-runtime/components/index.js +0 -12
- package/dist/constraint-runtime/constants/index.d.ts +0 -11
- package/dist/constraint-runtime/constants/index.js +0 -12
- package/dist/constraint-runtime/coordinator/index.d.ts +0 -11
- package/dist/constraint-runtime/coordinator/index.js +0 -12
- package/dist/constraint-runtime/cost_hook.d.ts +0 -2
- package/dist/constraint-runtime/cost_hook.js +0 -4
- package/dist/constraint-runtime/entrypoints/index.d.ts +0 -11
- package/dist/constraint-runtime/entrypoints/index.js +0 -12
- package/dist/constraint-runtime/execution_registry.d.ts +0 -20
- package/dist/constraint-runtime/execution_registry.js +0 -44
- package/dist/constraint-runtime/hooks/index.d.ts +0 -11
- package/dist/constraint-runtime/hooks/index.js +0 -12
- package/dist/constraint-runtime/ink.d.ts +0 -1
- package/dist/constraint-runtime/ink.js +0 -4
- package/dist/constraint-runtime/keybindings/index.d.ts +0 -11
- package/dist/constraint-runtime/keybindings/index.js +0 -12
- package/dist/constraint-runtime/memdir/index.d.ts +0 -11
- package/dist/constraint-runtime/memdir/index.js +0 -12
- package/dist/constraint-runtime/migrations/index.d.ts +0 -11
- package/dist/constraint-runtime/migrations/index.js +0 -12
- package/dist/constraint-runtime/moreright/index.d.ts +0 -11
- package/dist/constraint-runtime/moreright/index.js +0 -12
- package/dist/constraint-runtime/native_ts/index.d.ts +0 -11
- package/dist/constraint-runtime/native_ts/index.js +0 -12
- package/dist/constraint-runtime/output_styles/index.d.ts +0 -11
- package/dist/constraint-runtime/output_styles/index.js +0 -12
- package/dist/constraint-runtime/plugins/index.d.ts +0 -11
- package/dist/constraint-runtime/plugins/index.js +0 -12
- package/dist/constraint-runtime/port_manifest.d.ts +0 -7
- package/dist/constraint-runtime/port_manifest.js +0 -11
- package/dist/constraint-runtime/query.d.ts +0 -6
- package/dist/constraint-runtime/query.js +0 -1
- package/dist/constraint-runtime/remote/index.d.ts +0 -11
- package/dist/constraint-runtime/remote/index.js +0 -12
- package/dist/constraint-runtime/schemas/index.d.ts +0 -11
- package/dist/constraint-runtime/schemas/index.js +0 -12
- package/dist/constraint-runtime/screens/index.d.ts +0 -11
- package/dist/constraint-runtime/screens/index.js +0 -12
- package/dist/constraint-runtime/server/index.d.ts +0 -11
- package/dist/constraint-runtime/server/index.js +0 -12
- package/dist/constraint-runtime/services/index.d.ts +0 -11
- package/dist/constraint-runtime/services/index.js +0 -12
- package/dist/constraint-runtime/state/index.d.ts +0 -11
- package/dist/constraint-runtime/state/index.js +0 -12
- package/dist/constraint-runtime/system_init.d.ts +0 -1
- package/dist/constraint-runtime/system_init.js +0 -20
- package/dist/constraint-runtime/types/index.d.ts +0 -11
- package/dist/constraint-runtime/types/index.js +0 -12
- package/dist/constraint-runtime/upstream_proxy/index.d.ts +0 -11
- package/dist/constraint-runtime/upstream_proxy/index.js +0 -12
- package/dist/constraint-runtime/utils/index.d.ts +0 -11
- package/dist/constraint-runtime/utils/index.js +0 -12
- package/dist/constraint-runtime/vim/index.d.ts +0 -11
- package/dist/constraint-runtime/vim/index.js +0 -12
- package/dist/constraint-runtime/voice/index.d.ts +0 -11
- package/dist/constraint-runtime/voice/index.js +0 -12
|
@@ -162,10 +162,46 @@ export class AgentIdentityStore {
|
|
|
162
162
|
}));
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
// 2026-10-01 (数据事故修复): 单例**按 home 分桶**, 不再把 HOME 绑死在一个实例上。
|
|
166
|
+
//
|
|
167
|
+
// 事故经过 (真事, 用户丢了 4 个智能体): 测试 `channel-not-found.test.ts` 设了隔离 HOME
|
|
168
|
+
// (process.env.HOME = TMP_HOME), 但 vitest 复用 worker —— 同 worker 里**别的测试先**碰过
|
|
169
|
+
// getIdentityStore() ⇒ 单例已按**真实** HOME 构造 ⇒ 该测试的夹具 channel
|
|
170
|
+
// ("real test msg"/"test-agent") 直接写进真实的 ~/.bolloon/sessions/channels.json,
|
|
171
|
+
// 而且是**整表覆盖** ⇒ 用户 4 个 channel 只剩 1 条。
|
|
172
|
+
// 根因就是这里: `new AgentIdentityStore()` 的默认参数 `home = HOME()` 只在**构造那一刻**取值,
|
|
173
|
+
// 之后 process.env.HOME 再怎么变都影响不到它。
|
|
174
|
+
const _storesByHome = new Map();
|
|
175
|
+
/** 单例 (CLI / server 共用, **按当前 HOME 分桶**; 改过 HOME 的测试会自然拿到自己的实例) */
|
|
167
176
|
export function getIdentityStore() {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
177
|
+
const home = HOME();
|
|
178
|
+
let st = _storesByHome.get(home);
|
|
179
|
+
if (!st) {
|
|
180
|
+
st = new AgentIdentityStore(home);
|
|
181
|
+
_storesByHome.set(home, st);
|
|
182
|
+
}
|
|
183
|
+
return st;
|
|
184
|
+
}
|
|
185
|
+
/** 仅供测试: 丢弃所有缓存实例 (改完 HOME 后强制重建) */
|
|
186
|
+
export function resetIdentityStoreSingletons() {
|
|
187
|
+
_storesByHome.clear();
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* 测试期写盘硬保护 (2026-10-01, 数据事故后的第二道闸)。
|
|
191
|
+
*
|
|
192
|
+
* 事故: 测试夹具把用户的真实 channels.json 整表覆盖 (4 个智能体只剩 1 条)。
|
|
193
|
+
* 第一道修的是根因 (单例按 HOME 分桶); 这道是**兜底**: 只要进程带着测试标志,
|
|
194
|
+
* 就**只允许**写临时目录下的数据 —— 真实 HOME (~/.bolloon) 一律拒绝并抛错。
|
|
195
|
+
* 宁可让测试红, 也不能再动用户的数据。
|
|
196
|
+
*/
|
|
197
|
+
export function assertTestWriteTarget(home, what = '数据') {
|
|
198
|
+
const isTest = !!process.env.VITEST || process.env.NODE_ENV === 'test';
|
|
199
|
+
if (!isTest)
|
|
200
|
+
return;
|
|
201
|
+
const h = String(home || '').replace(/\/+$/, '');
|
|
202
|
+
const tmpLike = h.startsWith('/tmp/') || h.startsWith('/var/folders/') || h.startsWith('/private/var/folders/') || /bolloon[-_].*test/i.test(h);
|
|
203
|
+
if (!tmpLike) {
|
|
204
|
+
throw new Error(`[test-guard] 测试进程拒绝写非临时目录的${what}: ${h} —— ` +
|
|
205
|
+
`真实数据目录只能在正常 CLI/服务进程中写 (这道闸就是为防上次"夹具覆盖真实 channels.json"的事故)`);
|
|
206
|
+
}
|
|
171
207
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 后台委派的结果回灌 (2026-10-01, 用户: 「开展子智能体后,bolloon 没有回归」+「还是卡住了,学 hermes」)。
|
|
3
|
+
*
|
|
4
|
+
* 学的是"**起完就走, 结果自己回来**"这个形态:
|
|
5
|
+
* 起委派 ⇒ 立刻返回(带 session id ✓) ⇒ 父智能体继续干别的 ✓ ⇒ 它跑完后, 结果作为**一行提示**注入下一轮 ✓。
|
|
6
|
+
* 判据性质: 只对"我起的后台委派"发提示(命令里带标记 ✓) · **同一条只提示一次**(落盘记号 ✓) · 未跑完/已提示过 ⇒ 不提 ✓。
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { listSessions } from './process-runner.js';
|
|
12
|
+
/** 起后台委派时在命令前加的环境标记 ⇒ 便于识别"这是我起的委派" */
|
|
13
|
+
export const DELEGATE_ENV_MARK = 'BOLLOON_DELEGATE=1';
|
|
14
|
+
export function noticesPath(home = os.homedir()) {
|
|
15
|
+
return path.join(home, '.bolloon', 'delegate-notices.json');
|
|
16
|
+
}
|
|
17
|
+
function loadNoticed(home) {
|
|
18
|
+
try {
|
|
19
|
+
const raw = fs.readFileSync(noticesPath(home), 'utf-8');
|
|
20
|
+
const obj = JSON.parse(raw);
|
|
21
|
+
return obj && typeof obj === 'object' ? obj : {};
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function saveNoticed(map, home) {
|
|
28
|
+
try {
|
|
29
|
+
const p = noticesPath(home);
|
|
30
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
31
|
+
fs.writeFileSync(p, JSON.stringify(map), { mode: 0o600 });
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
/* best-effort: 提示记不下来最多重复一次, 不该打断主流程 */
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** 收集"已完成、且还没提示过"的后台委派, 每个一条中文提示; 同时落盘记号(只提示一次 ✓) */
|
|
38
|
+
export function collectDelegateNotices(home,
|
|
39
|
+
/** 便于门注入假 session(默认读真实的 ✓) */
|
|
40
|
+
injected) {
|
|
41
|
+
let sessions = [];
|
|
42
|
+
if (injected)
|
|
43
|
+
sessions = injected;
|
|
44
|
+
else {
|
|
45
|
+
try {
|
|
46
|
+
sessions = listSessions();
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const noticed = loadNoticed(home);
|
|
53
|
+
const lines = [];
|
|
54
|
+
let changed = false;
|
|
55
|
+
for (const s of sessions) {
|
|
56
|
+
if (!String(s.cmd || '').includes(DELEGATE_ENV_MARK))
|
|
57
|
+
continue; // 只认我起的委派
|
|
58
|
+
if (s.status === 'running')
|
|
59
|
+
continue; // 还没跑完 ⇒ 不提
|
|
60
|
+
if (noticed[s.id])
|
|
61
|
+
continue; // 提过 ⇒ 不提
|
|
62
|
+
const how = s.status === 'exited' ? `退出码 ${s.exitCode ?? 0}` : `状态 ${s.status}`;
|
|
63
|
+
lines.push(`[后台委派完成] session ${s.id} · ${how} —— 结果用 \`process poll ${s.id}\` 取; 若已达成目标就继续下一步, 别重发同样的委派。`);
|
|
64
|
+
noticed[s.id] = Date.now();
|
|
65
|
+
changed = true;
|
|
66
|
+
}
|
|
67
|
+
if (changed)
|
|
68
|
+
saveNoticed(noticed, home);
|
|
69
|
+
return lines;
|
|
70
|
+
}
|
|
71
|
+
/** 拼成一段(没有就返回空串 ✓) */
|
|
72
|
+
export function renderDelegateNotices(home) {
|
|
73
|
+
const lines = collectDelegateNotices(home);
|
|
74
|
+
return lines.length ? lines.join('\n') : '';
|
|
75
|
+
}
|
|
76
|
+
export function noticesQueuePath(home = os.homedir()) {
|
|
77
|
+
return path.join(home, '.bolloon', 'notices-queue.jsonl');
|
|
78
|
+
}
|
|
79
|
+
/** 投递一条(后台线程/异步回调都能安全调 ✓; 失败只吞不抛 ✓) */
|
|
80
|
+
export function pushNotice(kind, text, home = os.homedir()) {
|
|
81
|
+
try {
|
|
82
|
+
const p = noticesQueuePath(home);
|
|
83
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
84
|
+
fs.appendFileSync(p, JSON.stringify({ kind, text, at: Date.now() }) + '\n');
|
|
85
|
+
}
|
|
86
|
+
catch { /* 丢一条提示不该打断主流程 */ }
|
|
87
|
+
}
|
|
88
|
+
/** 空闲时排空(读走即清空 ✓): 返回"上一轮之后攒下的"通知, 供本轮开头一次性交代 ✓ */
|
|
89
|
+
export function drainNotices(home = os.homedir()) {
|
|
90
|
+
const p = noticesQueuePath(home);
|
|
91
|
+
let raw = '';
|
|
92
|
+
try {
|
|
93
|
+
raw = fs.readFileSync(p, 'utf-8');
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
const out = [];
|
|
99
|
+
for (const line of raw.split('\n')) {
|
|
100
|
+
if (!line.trim())
|
|
101
|
+
continue;
|
|
102
|
+
try {
|
|
103
|
+
const o = JSON.parse(line);
|
|
104
|
+
if (o && typeof o.text === 'string' && (o.kind === 'delegate' || o.kind === 'review'))
|
|
105
|
+
out.push(o);
|
|
106
|
+
}
|
|
107
|
+
catch { /* 坏行跳过 */ }
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
fs.writeFileSync(p, '', { mode: 0o600 });
|
|
111
|
+
}
|
|
112
|
+
catch { /* 清空失败 ⇒ 下轮可能重复一次, 可接受 */ }
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
/** 拼成一段"任务源块"(自包含 ✓: 说明是什么 + 现在该做什么 + 别重复) */
|
|
116
|
+
export function renderNoticeBlock(home = os.homedir()) {
|
|
117
|
+
const items = drainNotices(home);
|
|
118
|
+
if (!items.length)
|
|
119
|
+
return '';
|
|
120
|
+
const lines = items.map((n) => n.kind === 'review'
|
|
121
|
+
? `- [复盘产出] ${n.text}\n ⇒ 若它意味着要**改代码/改技能**, 现在就把这一步做掉(别只记下来); 若只是知识, 说明它已入库即可。`
|
|
122
|
+
: `- [后台任务完成] ${n.text}`);
|
|
123
|
+
return `【后台回流 · 这是新一轮】上一轮之后攒下 ${items.length} 条, 请在本轮一并处理:\n${lines.join('\n')}`;
|
|
124
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 每个 channel 的**自己的**身份 (2026-10-01)。
|
|
3
|
+
*
|
|
4
|
+
* 用户实测: 两个不同 channel (agent_18cece3f / test-agent) 都答「我叫小龙」, 且 get_identity 返回
|
|
5
|
+
* `DID: `(空) —— 症状是"读身份的还有问题"。
|
|
6
|
+
* 根因: index.ts 组装 identityDoc 时, 只有 channel 记录**同时**有 did 和 publicKey 才用它,
|
|
7
|
+
* 否则落到 `agentIdentity`(进程级**共享**的一个) ⇒ 多个 channel 共用一个身份 ✗;
|
|
8
|
+
* 而 channel 记录里还可能存着假 DID (`did:local:…`) ✗, 或干脆没 did (测试夹具建的) ✗。
|
|
9
|
+
* 规矩: 身份归属**以 channel 的 agentId 为准** —— 每个 agent 用自己的真 did:key (落盘密钥),
|
|
10
|
+
* 名字优先用该 agent 自己的 persona.json, 其次渠道名 (渠道名天然各不相同)。
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
/** 假的/垃圾 DID: 不该被当成身份用 (老代码自造过 did:local: / did:pi:) */
|
|
16
|
+
export function isUsableDid(did) {
|
|
17
|
+
const s = String(did ?? '').trim();
|
|
18
|
+
if (!s)
|
|
19
|
+
return false;
|
|
20
|
+
return !/^did:(local|pi):/i.test(s);
|
|
21
|
+
}
|
|
22
|
+
/** channel 记录里的身份够不够用 (did 真 + 有公钥) */
|
|
23
|
+
export function channelIdentityUsable(ch) {
|
|
24
|
+
return !!ch && isUsableDid(ch.did) && !!ch.publicKey;
|
|
25
|
+
}
|
|
26
|
+
/** 该 agent 自己的 persona.json 里的名字 (没有就 undefined) */
|
|
27
|
+
export function agentPersonaName(agentId, home = os.homedir()) {
|
|
28
|
+
const id = String(agentId || '').trim();
|
|
29
|
+
if (!id)
|
|
30
|
+
return undefined;
|
|
31
|
+
const safe = id.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
32
|
+
try {
|
|
33
|
+
const p = path.join(home, '.bolloon', 'persona', safe, 'persona.json');
|
|
34
|
+
const j = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
35
|
+
const n = String(j?.name ?? '').trim();
|
|
36
|
+
return n || undefined;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* channel 应该显示/使用什么名字。
|
|
44
|
+
* 顺序: 该 agent 的 persona.json → channel 记录里的 persona.name → channel 名 → agentId
|
|
45
|
+
* **绝不**回落到"某个全局身份的名字" —— 那正是"两个 channel 一个名"的来源。
|
|
46
|
+
*/
|
|
47
|
+
export function nameForChannel(ch, agentId, home = os.homedir()) {
|
|
48
|
+
const id = String(agentId || ch?.agentId || '').trim();
|
|
49
|
+
return ((id ? agentPersonaName(id, home) : undefined) ||
|
|
50
|
+
String(ch?.persona?.name ?? '').trim() ||
|
|
51
|
+
String(ch?.name ?? '').trim() ||
|
|
52
|
+
id ||
|
|
53
|
+
'agent');
|
|
54
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 代码写改的"类型门" (2026-10-01, 回应「tsc 抓错这个功能 bolloon 有吗」)。
|
|
3
|
+
*
|
|
4
|
+
* 查到的事实: bolloon **早就**有类型检查, 而且有三层 —— pre-commit 的 `tsc-check`(lefthook) ·
|
|
5
|
+
* `build:main = tsc && …` · 以及**给智能体的工具** `tsc_check`(pi-sdk-tools.ts:1450, 60s timeout)。
|
|
6
|
+
* 但**循环里没有"编辑后自动检查"** ✗ ⇒ `tsc_check` 是个"模型想得起来才会调"的工具 ✓
|
|
7
|
+
* ⇒ 和 #4(写操作读回)同一个病: **规矩写了, 没人执行** ✓。
|
|
8
|
+
* 这条把它变成机制: 本回合**改过 .ts/.tsx** ⇒ 回合收尾**自动**跑一次类型检查(一轮只跑一次, 不管改了 1 个还是 8 个文件 ✓),
|
|
9
|
+
* 结果以一行状态进对话流 —— "类型过没过"不再依赖模型自觉 ✓。
|
|
10
|
+
* 纪律: 每个回合最多一次(tsc 是秒级开销, 但也不该每改一个文件就跑) · 只对 TS 源码生效 · 失败绝不影响回合结果 ✓。
|
|
11
|
+
*/
|
|
12
|
+
/** 写/改类工具里, 会"产出源码"的那些 */
|
|
13
|
+
export const CODE_WRITE_TOOLS = new Set(['write_file', 'edit_file', 'patch', 'move_file', 'copy_file']);
|
|
14
|
+
export function isTypeScriptFile(p) {
|
|
15
|
+
return /\.(ts|tsx|mts|cts)$/i.test(String(p || '').trim());
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 这次写操作是否**产生了/改动了 TS 源码** ⇒ 返回那个路径(否则 null)。
|
|
19
|
+
* 只看路径, 不看内容(便宜且够用 ✓)。
|
|
20
|
+
*/
|
|
21
|
+
export function codeWriteTarget(toolName, args) {
|
|
22
|
+
if (!CODE_WRITE_TOOLS.has(String(toolName)))
|
|
23
|
+
return null;
|
|
24
|
+
const rel = String(args?.path ?? args?.to ?? args?.file ?? '').trim();
|
|
25
|
+
if (!rel)
|
|
26
|
+
return null;
|
|
27
|
+
return isTypeScriptFile(rel) ? rel : null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 回合收尾要不要跑类型检查: 改过 TS 且本回合还没跑过 ⇒ 跑。
|
|
31
|
+
* 抽成纯函数是为了**可测**: "一轮只跑一次"这条最容易写错(写成每个文件跑一次就把时间烧光 ✓)。
|
|
32
|
+
*/
|
|
33
|
+
export function decideTypecheck(touched, ranThisTurn) {
|
|
34
|
+
return !ranThisTurn && touched.length > 0;
|
|
35
|
+
}
|
|
36
|
+
/** 类型检查结果 ⇒ **一行**状态 (给对话流看; 错误多时只给前几条 + 总数) */
|
|
37
|
+
export function formatTypecheckResult(ok, output, maxLines = 6) {
|
|
38
|
+
const body = String(output || '').split('\n').map((l) => l.trim()).filter(Boolean);
|
|
39
|
+
if (ok)
|
|
40
|
+
return '✅ 类型检查通过 (tsc --noEmit, 本回合改过 TS 后自动跑)';
|
|
41
|
+
const errs = body.filter((l) => /error TS\d+/.test(l));
|
|
42
|
+
const head = (errs.length ? errs : body).slice(0, maxLines);
|
|
43
|
+
const more = (errs.length || body.length) - head.length;
|
|
44
|
+
return `❌ 类型检查没过: ${errs.length || '?'} 个错误\n` + head.map((l) => ` ${l.slice(0, 160)}`).join('\n')
|
|
45
|
+
+ (more > 0 ? `\n …还有 ${more} 条` : '')
|
|
46
|
+
+ '\n ⇒ **先修类型再继续**(别在同一轮里叠加更多改动)';
|
|
47
|
+
}
|
|
@@ -88,8 +88,17 @@ export class DenyPipeline {
|
|
|
88
88
|
* 实测日志 "write_file 被权限拦了" → LLM 只能绕道, 任务无法推进.
|
|
89
89
|
*/
|
|
90
90
|
static permissionChecker() {
|
|
91
|
+
/**
|
|
92
|
+
* 2026-10-01: **default 模式下只禁 `git_push`**(远端/不可逆那一个);
|
|
93
|
+
* `git_commit` / `git_branch` 放行 —— 它们本来是"指定安全路径"(有护栏、只提交已暂存的 ✓)。
|
|
94
|
+
* 为什么改: 用户现场实录里, `git_commit` 被权限拦了 ⇒ 智能体**绕道裸跑 shell**
|
|
95
|
+
* (`shell_exec` + **`git add -A`** ✗ —— 正是项目红线禁止的那条, 会卷走别的智能体在写的文件 ✗✗);
|
|
96
|
+
* 也就是说"拦住它"并没有更安全, 只是把它推去了**更危险**的路 ✗。
|
|
97
|
+
* (本文件上面那段注释早就记过同类教训: "被权限拦了 → LLM 只能绕道, 任务无法推进" ✓。)
|
|
98
|
+
* acceptEdits 模式仍按原样: shell/git 一律受限(那是**显式**收紧, 不是默认 ✗)。
|
|
99
|
+
*/
|
|
91
100
|
const DEFAULT_DENY_TOOLS = new Set([
|
|
92
|
-
'
|
|
101
|
+
'git_push',
|
|
93
102
|
]);
|
|
94
103
|
return (ctx) => {
|
|
95
104
|
if (ctx.permissionMode === 'bypassPermissions') {
|
|
@@ -106,7 +115,7 @@ export class DenyPipeline {
|
|
|
106
115
|
}
|
|
107
116
|
return { denied: false, reason: '', source: 'permission' };
|
|
108
117
|
}
|
|
109
|
-
// default:
|
|
118
|
+
// default: 只禁不可逆的远端操作(见上)
|
|
110
119
|
if (DEFAULT_DENY_TOOLS.has(ctx.toolName)) {
|
|
111
120
|
return {
|
|
112
121
|
denied: true,
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 出口净化 (2026-10-01 落实③)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么: 记忆/身份文档/历史片段这些"文本资产"最终都会**拼进给模型的提示**。秘密一旦混进去, 就等于
|
|
5
|
+
* 通过 API 送出去了 —— 而且会**反复**送(每一轮)。纪律上我们早就规定"凭证只落 0600 文件、绝不回显",
|
|
6
|
+
* 但那靠的是调用方自觉;出口必须是一道**闸**: 所有要进模型的文本都过同一个函数。
|
|
7
|
+
* 设计对齐 (只搬约束): **一个 choke point** · 先脱敏再截断 · 中间省略保留头尾 · 短文本原样返回(零成本)。
|
|
8
|
+
* 取舍: 宁可误伤(把长得像密钥的东西打码), 不可漏放 —— 误伤只是少几个字符, 漏放是事故。
|
|
9
|
+
*/
|
|
10
|
+
/** 常见凭证形态 (按"长得像"判, 不依赖具体厂商) */
|
|
11
|
+
const SECRET_PATTERNS = [
|
|
12
|
+
// 私钥 hex: 0x + 64 hex, 或裸 64 hex
|
|
13
|
+
{ re: /\b0x[0-9a-fA-F]{64}\b/g, tag: '[REDACTED:hex-key]' },
|
|
14
|
+
{ re: /\b[0-9a-fA-F]{64}\b/g, tag: '[REDACTED:hex64]' },
|
|
15
|
+
// 助记词: 12+ 个连续小写词 (空格分隔)
|
|
16
|
+
{ re: /\b(?:[a-z]{3,8}\s+){11,23}[a-z]{3,8}\b/g, tag: '[REDACTED:mnemonic]' },
|
|
17
|
+
// 常见 token 前缀
|
|
18
|
+
{ re: /\b(?:sk|pk|rk)-[A-Za-z0-9_-]{16,}\b/g, tag: '[REDACTED:api-key]' },
|
|
19
|
+
{ re: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}\b/g, tag: '[REDACTED:github-token]' },
|
|
20
|
+
{ re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, tag: '[REDACTED:slack-token]' },
|
|
21
|
+
{ re: /\bAKIA[0-9A-Z]{16}\b/g, tag: '[REDACTED:aws-key]' },
|
|
22
|
+
{ re: /\bnpm_[A-Za-z0-9]{30,}\b/g, tag: '[REDACTED:npm-token]' },
|
|
23
|
+
// JWT
|
|
24
|
+
{ re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{6,}\b/g, tag: '[REDACTED:jwt]' },
|
|
25
|
+
// Authorization: Bearer xxx
|
|
26
|
+
{ re: /\bBearer\s+[A-Za-z0-9._~+/-]{12,}=*/gi, tag: 'Bearer [REDACTED]' },
|
|
27
|
+
// URL 里的用户名:密码
|
|
28
|
+
{ re: /(https?:\/\/)[^\s/@:]+:[^\s/@]+@/gi, tag: '$1[REDACTED]@' },
|
|
29
|
+
// key: value 形态的敏感字段 (私钥/助记词/密钥/token/password)
|
|
30
|
+
{ re: /((?:privateKey|private_key|mnemonic|seedPhrase|seed_phrase|secret|apiKey|api_key|token|password|passwd|passphrase)"?\s*[:=]\s*)"[^"]{8,}"/gi, tag: '$1"[REDACTED]"' },
|
|
31
|
+
{ re: /((?:privateKey|private_key|mnemonic|seedPhrase|seed_phrase|secret|apiKey|api_key|token|password|passwd|passphrase)"?\s*[:=]\s*)'[^']{8,}'/gi, tag: "$1'[REDACTED]'" },
|
|
32
|
+
{ re: /((?:privateKey|private_key|mnemonic|seedPhrase|seed_phrase|secret|apiKey|api_key|token|password|passwd|passphrase)"?\s*[:=]\s*)[^\s,;"'}\]]{8,}/gi, tag: '$1[REDACTED]' },
|
|
33
|
+
];
|
|
34
|
+
/** 把长得像凭证的东西打码 (纯函数, 幂等 — 已打码的文本再跑不变) */
|
|
35
|
+
export function redactSecrets(text) {
|
|
36
|
+
let out = String(text ?? '');
|
|
37
|
+
for (const { re, tag } of SECRET_PATTERNS)
|
|
38
|
+
out = out.replace(re, tag);
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
/** 中间省略: 长度 <= maxChars 原样返回; 否则保留头 head、尾 tail, 中间插省略标记 */
|
|
42
|
+
export function elideMiddle(text, maxChars, headChars = 0, tailChars = 0) {
|
|
43
|
+
const s = String(text ?? '');
|
|
44
|
+
const max = Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) : 0;
|
|
45
|
+
if (!max || s.length <= max)
|
|
46
|
+
return s;
|
|
47
|
+
// 头/尾各留一部分, **给省略标记留出余量** —— 上一版 head+tail 正好等于 max ⇒ 落到"直接截断"分支,
|
|
48
|
+
// 省略标记根本不出现(门抓到: 输出里没有 "省略" ✗)。现在按 60%/25% 留, 剩下给标记。
|
|
49
|
+
const head = headChars > 0 ? Math.min(headChars, Math.floor(max * 0.8)) : Math.floor(max * 0.6);
|
|
50
|
+
const tail = tailChars > 0 ? Math.min(tailChars, Math.floor(max * 0.3)) : Math.floor(max * 0.25);
|
|
51
|
+
const marker = (n) => `\n…[省略 ${n} 字符]…\n`;
|
|
52
|
+
if (head <= 0 && tail <= 0)
|
|
53
|
+
return s.slice(0, max);
|
|
54
|
+
const omitted = Math.max(0, s.length - head - tail);
|
|
55
|
+
const out = `${s.slice(0, head)}${marker(omitted)}${tail > 0 ? s.slice(-tail) : ''}`;
|
|
56
|
+
return out.length <= max + marker(omitted).length ? out : out.slice(0, max + marker(omitted).length);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* **唯一出口**: 任何要拼进模型的文本都过这里。
|
|
60
|
+
* 顺序很重要 —— 先脱敏再截断: 否则截断可能把 key 的中间切掉, 让模式匹配失效(半截 key 照样是泄漏)。
|
|
61
|
+
*/
|
|
62
|
+
export function sanitizeForLlm(text, maxChars = 0) {
|
|
63
|
+
const redacted = redactSecrets(text);
|
|
64
|
+
return maxChars > 0 ? elideMiddle(redacted, maxChars) : redacted;
|
|
65
|
+
}
|
|
66
|
+
/** 一行内是否含可疑凭证 (给调用方做审计/告警用, 不改变文本) */
|
|
67
|
+
export function hasSuspiciousSecret(text) {
|
|
68
|
+
return redactSecrets(text) !== String(text ?? '');
|
|
69
|
+
}
|
|
@@ -12,12 +12,18 @@ const ERROR_SIGNATURES = [
|
|
|
12
12
|
{ pattern: /PreToolUse 拒绝|Harness.*拒绝|permission|not allowed|denied/i, cls: 'permission_denied', label: '权限拒绝' },
|
|
13
13
|
{ pattern: /ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENOTFOUND|network|connect.*fail|fetch.*fail/i, cls: 'network_error', label: '网络错误' },
|
|
14
14
|
{ pattern: /timeout|timed out/i, cls: 'timeout', label: '执行超时' },
|
|
15
|
-
|
|
15
|
+
// 2026-10-01: ENOENT 单独一条 —— 原先和 bad argument 挤在一起 ⇒ 路径不存在被报成"参数错误" ✗
|
|
16
|
+
// (用户实测: list_files '~/.bolloon' ⇒ ENOENT, 却被标成"参数错误", 真原因被标签盖住)
|
|
17
|
+
{ pattern: /ENOENT|no such file|does not exist|not exist/i, cls: 'bad_input', label: '路径/文件不存在' },
|
|
18
|
+
{ pattern: /invalid path|bad argument|ERR_INVALID|参数/i, cls: 'bad_input', label: '参数错误' },
|
|
19
|
+
// 2026-10-01: **策略拒绝**必须单列且排在前面 —— 否则长文本里的无关关键词会把它误判成鉴权类 ⇒ 熔断 ✗
|
|
20
|
+
// (现场: 「路径被护栏拒…」被当成"鉴权类错误不重试" ⇒ 停在 needs_human ✗)
|
|
21
|
+
{ pattern: /路径被护栏拒|不在白名单|被护栏拒|denied by policy/i, cls: 'policy', label: '策略拒绝(护栏)' },
|
|
16
22
|
{ pattern: /401|403|quota|rate limit|API key|unauthorized|authentication/i, cls: 'api_error', label: 'API 认证错误' },
|
|
17
23
|
];
|
|
18
24
|
export function classifyError(errorMsg) {
|
|
19
25
|
if (!errorMsg)
|
|
20
|
-
return { cls: 'unknown', label: '
|
|
26
|
+
return { cls: 'unknown', label: '', severity: 'medium', recoverable: true };
|
|
21
27
|
for (const sig of ERROR_SIGNATURES) {
|
|
22
28
|
if (sig.pattern.test(errorMsg)) {
|
|
23
29
|
const severity = sig.cls === 'api_error' ? 'high'
|
|
@@ -30,21 +36,44 @@ export function classifyError(errorMsg) {
|
|
|
30
36
|
return { cls: sig.cls, label: sig.label, severity, recoverable };
|
|
31
37
|
}
|
|
32
38
|
}
|
|
33
|
-
return { cls: 'unknown', label: '
|
|
39
|
+
return { cls: 'unknown', label: '', severity: 'medium', recoverable: true };
|
|
34
40
|
}
|
|
35
41
|
export function buildObservation(tool, args, result) {
|
|
36
42
|
const obs = { tool, args, success: result.success, summary: '' };
|
|
37
43
|
if (result.success) {
|
|
38
44
|
const output = result.output || '(无输出)';
|
|
39
45
|
obs.output = output;
|
|
40
|
-
|
|
46
|
+
// 2026-10-01 (用户: 「完整看一下这个反思模式,为啥一直出错?」):
|
|
47
|
+
// 原来只报 **字节数** ✗(`✅ terminal 成功 (1234B)`) ⇒ 模型不知道**发生了什么**, 只能猜 ⇒ 猜错 ✓
|
|
48
|
+
// 现在附一段**内容预览**(压平换行 ✓ 头部 160 字符 ✓) ⇒ 观察里就有事实 ✓。
|
|
49
|
+
const preview = output.replace(/\s+/g, ' ').trim().slice(0, 160);
|
|
50
|
+
obs.summary = `✅ ${tool} 成功 (${output.length}B)${preview ? `: ${preview}` : ''}`;
|
|
41
51
|
}
|
|
42
52
|
else {
|
|
43
53
|
const errMsg = result.error || '未知失败';
|
|
44
54
|
const cls = classifyError(errMsg);
|
|
45
55
|
obs.errorClass = cls.cls;
|
|
46
56
|
obs.errorLabel = cls.label;
|
|
47
|
-
|
|
57
|
+
// ② 2026-10-01: 退出码非零时**先不写"失败"** ✓(钩子/子命令常以非零退出而动作已生效);
|
|
58
|
+
// ③ 并附**输出尾部** —— 原来只截错误文本前 120 字符 ✗, 而 lefthook 那类"到底干了什么"恰在**尾部** ⇒ 必然误判 ✗
|
|
59
|
+
const __exit0 = Number((/exit\s+(\d+)/i.exec(result.error || '') || [])[1]);
|
|
60
|
+
const __tail = String(result.output || '').replace(/\s+/g, ' ').trim().slice(-200);
|
|
61
|
+
const __head = __exit0 && Number.isFinite(__exit0) && __exit0 !== 0 && tool === 'terminal'
|
|
62
|
+
? `⚠️ ${tool} 退出码 ${__exit0}(结果待核, 不等于失败)`
|
|
63
|
+
: `❌ ${tool} 失败: ${cls.label}`;
|
|
64
|
+
obs.summary = `${__head} — ${errMsg.slice(0, 160)}${__tail ? ` · 输出尾部: ${__tail}` : ''}`;
|
|
65
|
+
// 2026-10-01 (用户: 「反思还是有错误」—— 现场实测: git 提交**已经成功**, 但那一行却写着
|
|
66
|
+
// `Reflection: ❌ terminal 失败: — exit 1` ✗, 把 lefthook 的钩子退出码当成了任务失败 ✗):
|
|
67
|
+
// **退出码非零 ≠ 任务失败** ✓ —— 用命令核一次事实再下结论, 别直接写"失败"。
|
|
68
|
+
const __exit = Number((/exit\s+(\d+)/i.exec(errMsg) || [])[1]);
|
|
69
|
+
if (tool === 'terminal' && Number.isFinite(__exit) && __exit !== 0) {
|
|
70
|
+
obs.summary = `${obs.summary} · ⚠️ 退出码 ${__exit} **不等于任务失败**(钩子/子命令常以非零退出而动作已生效)`
|
|
71
|
+
+ ` —— 先核事实(git status / git log / read_file 看结果)再下结论, 别写成"失败"`;
|
|
72
|
+
}
|
|
73
|
+
// 2026-10-01 (优化 #6): 顺手给出"下一步该干什么" —— 光有标签模型会原地重试同一条命令
|
|
74
|
+
const __advice = suggestNextAction(`${cls.label} ${errMsg}`);
|
|
75
|
+
if (__advice)
|
|
76
|
+
obs.summary = `${obs.summary} · ${__advice}`;
|
|
48
77
|
}
|
|
49
78
|
return obs;
|
|
50
79
|
}
|
|
@@ -81,10 +110,24 @@ const ERROR_TO_STRATEGIES = {
|
|
|
81
110
|
{ action: 'retry', reason: '错误类型不确定', detail: '换个方式或参数再试一次' },
|
|
82
111
|
{ action: 'change_tool', reason: '原方法不可行', detail: '尝试用其他工具组合达成目标' },
|
|
83
112
|
],
|
|
113
|
+
// 2026-10-01: 护栏拒绝 ⇒ **别无脑重试同一条路径** ✓: 先改到允许的根下, 或问用户要目标位置 ✓
|
|
114
|
+
policy: [
|
|
115
|
+
{ action: 'change_params', reason: '策略拒绝(护栏)', detail: '别原样重试: ① 换到白名单内的根(docs/** · src/web/** · *.md · ~/.bolloon/**); ② 或先问用户"这个文件该放哪"; ③ 若是工具/命令不在白名单, 换等价的白名单内工具' },
|
|
116
|
+
{ action: 'change_tool', reason: '换等价工具', detail: '写文件用 write_file 到允许路径; 命令换白名单内的等价命令' },
|
|
117
|
+
],
|
|
84
118
|
};
|
|
85
119
|
export function buildReflection(toolName, errorMsg, errorCount, sameToolFailCount) {
|
|
86
120
|
if (!errorMsg)
|
|
87
121
|
return ERROR_TO_STRATEGIES.unknown.slice(0, 1);
|
|
122
|
+
// ④ 2026-10-01: 退出码非零 ⇒ **先核事实**, 而不是按规则表给重试 ✗
|
|
123
|
+
// (实测: git 提交已生效却因钩子退出码非零被判失败 ⇒ 模型又去重试/绕道, 越走越偏 ✗)
|
|
124
|
+
const __exitCode = Number((/exit\s+(\d+)/i.exec(errorMsg) || [])[1]);
|
|
125
|
+
if (Number.isFinite(__exitCode) && __exitCode !== 0) {
|
|
126
|
+
return [
|
|
127
|
+
{ action: 'change_params', reason: `退出码 ${__exitCode} —— 结果待核(不等于失败)`, detail: '先用 git status / git log / read_file 核事实, 确认到底成没成, 再决定下一步; 别直接当失败重试' },
|
|
128
|
+
{ action: 'change_tool', reason: '换法再试', detail: '若确未成功: 换参数/换工具/拆小步再执行' },
|
|
129
|
+
];
|
|
130
|
+
}
|
|
88
131
|
const cls = classifyError(errorMsg);
|
|
89
132
|
// 阶梯升档: 根据连续失败次数选择更激进的策略
|
|
90
133
|
if (sameToolFailCount >= 3) {
|
|
@@ -116,3 +159,22 @@ export function formatObservationWithReflection(obs, reflection) {
|
|
|
116
159
|
}
|
|
117
160
|
return lines.join('\n');
|
|
118
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* 错误 ⇒ **下一步动作建议** (2026-10-01 优化 #6)。
|
|
164
|
+
* 光有标签(路径不存在/参数错误)还不够 —— 模型得知道**接着该干什么**, 否则就原地重试同一条命令。
|
|
165
|
+
*/
|
|
166
|
+
export function suggestNextAction(labelOrMsg) {
|
|
167
|
+
const s = String(labelOrMsg || '');
|
|
168
|
+
const rules = [
|
|
169
|
+
[/路径|不存在|ENOENT/i, '用 list_files / glob_files 看**真实**路径再重试(注意 ~ 展开与工作目录)'],
|
|
170
|
+
[/超时|timeout/i, '拆小(分成多次/加 limit)或改用后台执行, 别原样重发'],
|
|
171
|
+
[/权限|EACCES|EPERM/i, '换可写目录(如工作区或 /tmp), 不要动系统目录'],
|
|
172
|
+
[/参数|invalid|missing|必填/i, '对照工具描述里的必填项补齐参数; 不确定就先 list_tools 看schema'],
|
|
173
|
+
[/网络|ECONN|fetch failed|不可达/i, '确认本机服务/代理可达; 自家域走 --resolve + --noproxy'],
|
|
174
|
+
[/缓冲区|maxBuffer|输出过大/i, '加过滤(只取需要的行)或改写到文件再按需读取'],
|
|
175
|
+
];
|
|
176
|
+
for (const [re, advice] of rules)
|
|
177
|
+
if (re.test(s))
|
|
178
|
+
return `下一步: ${advice}`;
|
|
179
|
+
return null;
|
|
180
|
+
}
|