@bolloon/bolloon-agent 0.5.3 → 0.5.5
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 +38 -32
- package/dist/agents/pi-sdk-session-manager.js +72 -3
- package/dist/agents/pi-sdk-tools.js +268 -47
- package/dist/agents/pi-sdk.js +470 -22
- package/dist/agents/plan-store.js +36 -0
- package/dist/agents/process-runner.js +64 -2
- package/dist/agents/session-store.js +144 -3
- 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/agents/wallet-tools.js +63 -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 +231 -32
- package/dist/cli/input-history.js +119 -0
- package/dist/cli/input-paste.js +218 -0
- package/dist/cli/loading-tui.js +69 -7
- package/dist/cli/log-gate.js +46 -1
- package/dist/cli/mention-data.js +30 -0
- package/dist/cli/reasoning-view.js +103 -0
- package/dist/cli/session-summary.js +129 -0
- package/dist/cli/session-tree.js +141 -0
- package/dist/cli/startup-notice.js +25 -0
- package/dist/cli/status-segments.js +155 -0
- package/dist/cli/stores.js +16 -0
- package/dist/cli/trace-line.js +54 -0
- package/dist/cli/wallet-store.js +159 -0
- package/dist/cli/wallet-table.js +46 -0
- package/dist/cli-entry.js +22 -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 +38 -32
- package/dist/electron-build/agents/parse-tool-call.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 +268 -46
- 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 +471 -22
- 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/session-store.js +144 -3
- package/dist/electron-build/agents/session-store.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/agents/wallet-tools.js +102 -0
- package/dist/electron-build/agents/wallet-tools.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/cli/wallet-store.js +200 -0
- package/dist/electron-build/cli/wallet-store.js.map +1 -0
- 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/llm/pi-ai.js +25 -0
- package/dist/electron-build/llm/pi-ai.js.map +1 -1
- 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 +1324 -173
- package/dist/llm/pi-ai.js +23 -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 +3 -2
- package/dist/web/mobile-core.js +7762 -6289
- package/dist/web/routes-mobile-tasks.js +2 -1
- package/dist/web/server-storage.js +26 -5
- package/dist/web/server.js +2 -0
- package/dist/web/sw.js +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工具调用遥测 (2026-10-01, 用户: 「开始吧, 把这些事儿做完」—— 优化方向 #3)。
|
|
3
|
+
*
|
|
4
|
+
* 问题: 我们要"让工具调用变好", 但**没有任何度量** ✗ ⇒ 全靠感觉。这条先把"浪费在哪"变成数字。
|
|
5
|
+
* 记录 (每行一条 JSONL): 工具名 · 参数指纹 · 耗时 · 成功/失败 · 结果字符数 · **是否与上一次同签名**。
|
|
6
|
+
* 由此可算: 每任务调用次数 · **重复率** · 结果体量分布 · 慢工具排行 —— 后面每一条优化的效果都可证伪 ✓。
|
|
7
|
+
* 纪律: **fire-and-forget**(写失败绝不影响工具执行) · 只落本机数据目录 · 不记参数正文(只记指纹,
|
|
8
|
+
* 免得把用户的密钥/正文写进日志 ✓)。
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
export function telemetryDir(home = os.homedir()) {
|
|
15
|
+
return path.join(home, '.bolloon', 'logs');
|
|
16
|
+
}
|
|
17
|
+
export function telemetryPath(home = os.homedir()) {
|
|
18
|
+
return path.join(telemetryDir(home), 'tool-calls.jsonl');
|
|
19
|
+
}
|
|
20
|
+
/** 参数指纹: 只留结构, 不留值 (工具名 + 参数键名 + 值长度) —— 避免把秘密写进日志 */
|
|
21
|
+
export function argsFingerprint(args) {
|
|
22
|
+
try {
|
|
23
|
+
const o = (args && typeof args === 'object' ? args : {});
|
|
24
|
+
const shape = Object.keys(o).sort().map((k) => {
|
|
25
|
+
const v = o[k];
|
|
26
|
+
const kind = Array.isArray(v) ? `arr${v.length}` : typeof v === 'string' ? `str${v.length}` : typeof v;
|
|
27
|
+
return `${k}:${kind}`;
|
|
28
|
+
}).join(',');
|
|
29
|
+
return createHash('sha256').update(`${shape}`).digest('hex').slice(0, 12);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return 'unknown';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** 追加一条 (绝不影响调用方) */
|
|
36
|
+
export function recordToolCall(rec) {
|
|
37
|
+
try {
|
|
38
|
+
const home = rec.home || os.homedir();
|
|
39
|
+
const dir = telemetryDir(home);
|
|
40
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
41
|
+
const row = {
|
|
42
|
+
ts: new Date().toISOString(),
|
|
43
|
+
tool: String(rec.tool || '?'),
|
|
44
|
+
sig: String(rec.sig || '?'),
|
|
45
|
+
ms: Math.max(0, Math.round(Number(rec.ms) || 0)),
|
|
46
|
+
ok: !!rec.ok,
|
|
47
|
+
resultChars: Math.max(0, Math.round(Number(rec.resultChars) || 0)),
|
|
48
|
+
repeatOfPrev: !!rec.prevSig && rec.prevSig === rec.sig,
|
|
49
|
+
...(rec.errorClass ? { errorClass: String(rec.errorClass).slice(0, 40) } : {}),
|
|
50
|
+
};
|
|
51
|
+
fs.appendFileSync(telemetryPath(home), JSON.stringify(row) + '\n', 'utf-8');
|
|
52
|
+
return row;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** 汇总 (给"优化前/后"对比用): 读回 JSONL, 算重复率/体量/慢工具 */
|
|
59
|
+
export function summarizeTelemetry(home = os.homedir(), limit = 5000) {
|
|
60
|
+
const empty = { calls: 0, repeats: 0, repeatRate: 0, okRate: 0, resultChars: 0, avgMs: 0, errorsByClass: [], topTools: [], slowest: [], biggestResults: [] };
|
|
61
|
+
try {
|
|
62
|
+
const file = telemetryPath(home);
|
|
63
|
+
if (!fs.existsSync(file))
|
|
64
|
+
return empty;
|
|
65
|
+
const lines = fs.readFileSync(file, 'utf-8').split('\n').filter(Boolean).slice(-limit);
|
|
66
|
+
const rows = [];
|
|
67
|
+
for (const l of lines) {
|
|
68
|
+
try {
|
|
69
|
+
rows.push(JSON.parse(l));
|
|
70
|
+
}
|
|
71
|
+
catch { /* 跳过坏行 */ }
|
|
72
|
+
}
|
|
73
|
+
if (!rows.length)
|
|
74
|
+
return empty;
|
|
75
|
+
const byTool = new Map();
|
|
76
|
+
for (const r of rows)
|
|
77
|
+
byTool.set(r.tool, (byTool.get(r.tool) || 0) + 1);
|
|
78
|
+
const sum = (f) => rows.reduce((a, r) => a + f(r), 0);
|
|
79
|
+
const calls = rows.length;
|
|
80
|
+
const repeats = rows.filter((r) => r.repeatOfPrev).length;
|
|
81
|
+
return {
|
|
82
|
+
calls,
|
|
83
|
+
repeats,
|
|
84
|
+
repeatRate: Number((repeats / calls).toFixed(3)),
|
|
85
|
+
okRate: Number((rows.filter((r) => r.ok).length / calls).toFixed(3)),
|
|
86
|
+
errorsByClass: [...rows.filter((r) => !r.ok).reduce((m, r) => m.set(r.errorClass || '未分类', (m.get(r.errorClass || '未分类') || 0) + 1), new Map()).entries()].map(([cls, n]) => ({ cls, n })).sort((a, b) => b.n - a.n).slice(0, 8),
|
|
87
|
+
resultChars: sum((r) => r.resultChars),
|
|
88
|
+
avgMs: Math.round(sum((r) => r.ms) / calls),
|
|
89
|
+
topTools: [...byTool.entries()].map(([tool, n]) => ({ tool, n })).sort((a, b) => b.n - a.n).slice(0, 10),
|
|
90
|
+
slowest: [...rows].sort((a, b) => b.ms - a.ms).slice(0, 5).map((r) => ({ tool: r.tool, ms: r.ms })),
|
|
91
|
+
biggestResults: [...rows].sort((a, b) => b.resultChars - a.resultChars).slice(0, 5).map((r) => ({ tool: r.tool, chars: r.resultChars })),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return empty;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wallet-tools.ts — 钱包工具的**双布局加载器** (2026-09-30)
|
|
3
|
+
*
|
|
4
|
+
* 为什么要有这个文件:
|
|
5
|
+
* 钱包真身是 `constraint-runtime` 里的 `tools/WalletTools/*`。构建 (`npm run build:main` =
|
|
6
|
+
* `tsc && node scripts/copy-constraint-runtime.mjs`) 把 `src/constraint-runtime/dist/*`
|
|
7
|
+
* 复制成 `dist/constraint-runtime/*` —— **少一层 `dist/`**。于是同一句相对导入:
|
|
8
|
+
* `../constraint-runtime/dist/tools/WalletTools/createWallet.js`
|
|
9
|
+
* 在源码态 (src/agents) 能解析 ✓, 构建后 (dist/agents) 解析到 `dist/constraint-runtime/dist/…` ✗
|
|
10
|
+
* ⇒ `wallet_create` 报 `Cannot find module '…/dist/constraint-runtime/src/tools/…'` (leo 2026-09-30 贴的屏),
|
|
11
|
+
* 而 `/wallet` 又因为"从来没写过文件"显示「钱包 (0)」—— 两个毛病叠在一起, 看着像"钱包功能不存在"。
|
|
12
|
+
*
|
|
13
|
+
* 规则: **按存在的路径挑**, 挑不到就抛错并**列出试过的路径** (不许含糊成"未知错误")。
|
|
14
|
+
* 注意: 这里用**变量**给 `import()` ⇒ 不做静态解析 (跨布局本来就不能静态解析)。
|
|
15
|
+
*/
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import { pathToFileURL } from 'url';
|
|
18
|
+
// 2026-09-30: 不用 import.meta —— 本文件会被 electron 那条 CJS 链间接引到,
|
|
19
|
+
// `tsc -p tsconfig.electron.json` 会报 TS1343 (module: CommonJS 下不允许 import.meta),
|
|
20
|
+
// 而 `npm publish` 的 prepublishOnly (build:all → build:electron) 会因此挂住。
|
|
21
|
+
// 仓里现成的替代: cjsModuleDir() (CJS/ESM 都能用) + currentPackageRoot() + firstExisting()。
|
|
22
|
+
import { cjsModuleDir, firstExisting } from '../utils/module-context.js';
|
|
23
|
+
import { currentPackageRoot } from '../utils/version-info.js';
|
|
24
|
+
/** 候选路径 (先近后远; 覆盖 dev / dist 两种布局) */
|
|
25
|
+
export function walletToolCandidates(name) {
|
|
26
|
+
const root = (() => { try {
|
|
27
|
+
return currentPackageRoot();
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return process.cwd();
|
|
31
|
+
} })();
|
|
32
|
+
const here = cjsModuleDir() || path.join(root, 'dist', 'agents'); // src/agents 或 dist/agents
|
|
33
|
+
return [
|
|
34
|
+
// ① 构建后布局: dist/constraint-runtime/tools/… (从 dist/agents 出发)
|
|
35
|
+
path.join(here, '..', 'constraint-runtime', 'tools', 'WalletTools', `${name}.js`),
|
|
36
|
+
// ② 源码态布局: src/constraint-runtime/dist/tools/… (从 src/agents 出发)
|
|
37
|
+
path.join(here, '..', 'constraint-runtime', 'dist', 'tools', 'WalletTools', `${name}.js`),
|
|
38
|
+
// ③ 兜底: 从仓根分别按两种布局找
|
|
39
|
+
path.join(root, 'dist', 'constraint-runtime', 'tools', 'WalletTools', `${name}.js`),
|
|
40
|
+
path.join(root, 'src', 'constraint-runtime', 'dist', 'tools', 'WalletTools', `${name}.js`),
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
/** 加载一个钱包工具模块 (挑第一个真实存在的; 都不在就带着"试过哪些"报错) */
|
|
44
|
+
export async function loadWalletTool(name) {
|
|
45
|
+
const tried = walletToolCandidates(name);
|
|
46
|
+
const hit = firstExisting(tried);
|
|
47
|
+
if (hit)
|
|
48
|
+
return (await import(pathToFileURL(hit).href));
|
|
49
|
+
throw new Error(`找不到钱包工具 ${name} (试过 ${tried.length} 个路径):\n ${tried.join('\n ')}`);
|
|
50
|
+
}
|
|
51
|
+
/** 给诊断用: 当前各自解析到哪 (打印用的字符串, 不加载) */
|
|
52
|
+
export function walletToolLayout() {
|
|
53
|
+
const names = ['createWallet', 'getBalance', 'signMessage'];
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const n of names) {
|
|
56
|
+
for (const c of walletToolCandidates(n)) {
|
|
57
|
+
if (out.some(o => o.pattern === c))
|
|
58
|
+
continue;
|
|
59
|
+
out.push({ pattern: c, exists: firstExisting([c]) !== null });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
@@ -83,23 +83,53 @@ async function collectGit(cwd, limit) {
|
|
|
83
83
|
uncommittedChanges: statusOut ? statusOut.split('\n').filter(Boolean).length : 0,
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
/**
|
|
87
|
+
* 按 agent 取 persona (2026-10-01)。
|
|
88
|
+
*
|
|
89
|
+
* 用户实测: 233 会话的 get_identity 已经是「小龙」✓, 但**项目上下文**里仍显示 `## Persona: 小宝` ✗
|
|
90
|
+
* —— 那是**全局** `~/.bolloon/persona.json`(8/10 的老文件), 这里原先**无条件**读它 ✗。
|
|
91
|
+
* 规矩 (与 session-manager 的分流一致):
|
|
92
|
+
* ① 该 agent 有自己的 persona.json ⇒ 用它;
|
|
93
|
+
* ② 该 agent 有身份文档目录(persona/<agentId>/*.md) ⇒ **不注入 persona** (身份由文档承担, 别把全局名漏进来 ✗);
|
|
94
|
+
* ③ 既没 scope 又什么都没有 ⇒ 才回落全局 (兼容老流程)。
|
|
95
|
+
*/
|
|
96
|
+
export async function resolvePersonaForScope(home, scopeId) {
|
|
97
|
+
const scope = String(scopeId || '').trim();
|
|
98
|
+
if (scope) {
|
|
99
|
+
const safe = scope.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
100
|
+
const dir = path.join(home, '.bolloon', 'persona', safe);
|
|
101
|
+
const own = await safeReadFile(path.join(dir, 'persona.json'), 5000);
|
|
102
|
+
if (own) {
|
|
103
|
+
try {
|
|
104
|
+
const p = JSON.parse(own);
|
|
105
|
+
return { name: String(p.name || 'unknown'), description: String(p.description || ''), personality: String(p.personality || '') };
|
|
106
|
+
}
|
|
107
|
+
catch { /* 坏了就往下走 */ }
|
|
108
|
+
}
|
|
109
|
+
// 有身份文档 ⇒ 不注入 persona (否则全局名会盖过该 agent 的身份 ✗)
|
|
110
|
+
try {
|
|
111
|
+
const entries = await fs.readdir(dir);
|
|
112
|
+
if (entries.some((f) => f.endsWith('.md')))
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
catch { /* 目录不存在 ⇒ 往下走 */ }
|
|
116
|
+
}
|
|
88
117
|
const raw = await safeReadFile(path.join(home, '.bolloon', 'persona.json'), 5000);
|
|
89
118
|
if (!raw)
|
|
90
119
|
return null;
|
|
91
120
|
try {
|
|
92
121
|
const p = JSON.parse(raw);
|
|
93
|
-
return {
|
|
94
|
-
name: String(p.name || 'unknown'),
|
|
95
|
-
description: String(p.description || ''),
|
|
96
|
-
personality: String(p.personality || ''),
|
|
97
|
-
};
|
|
122
|
+
return { name: String(p.name || 'unknown'), description: String(p.description || ''), personality: String(p.personality || '') };
|
|
98
123
|
}
|
|
99
124
|
catch {
|
|
100
125
|
return null;
|
|
101
126
|
}
|
|
102
127
|
}
|
|
128
|
+
async function collectPersona() {
|
|
129
|
+
const home = process.env.HOME || os.homedir() || '/tmp';
|
|
130
|
+
// 当前 agent 由会话侧注入 (CLI 建/切 agent 时设置) ⇒ 项目上下文里的 persona 与之对齐
|
|
131
|
+
return resolvePersonaForScope(home, process.env.BOLLOON_ACTIVE_AGENT_ID);
|
|
132
|
+
}
|
|
103
133
|
async function collectJudgmentsSummary(topN) {
|
|
104
134
|
// 用动态 import 避免循环依赖
|
|
105
135
|
const { loadAllJudgments } = await import('../pi-ecosystem-judgment/human-value-store.js');
|
|
@@ -11,14 +11,23 @@ import { getCachedBolloonContext, clearBolloonContextCache } from './context-col
|
|
|
11
11
|
import { formatContextForSystemPrompt } from './project-context.js';
|
|
12
12
|
import { loadPersonaDocs, formatPersonaForSystemPrompt, loadPersonaJudgmentDeclaration, formatJudgmentDeclaration } from './persona-loader.js';
|
|
13
13
|
let lastSessionStartAt = 0;
|
|
14
|
-
|
|
14
|
+
let lastIdentityKey = ''; // 上一次计算时的身份键 (agentId + channelId)
|
|
15
|
+
const MIN_INTERVAL_MS = 5000; // 同一身份 5s 内最多触发一次, 防止循环
|
|
15
16
|
export async function onSessionStart(opts = {}) {
|
|
16
17
|
const start = Date.now();
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
// 节流必须**按身份**生效 (2026-10-01 修复):
|
|
19
|
+
// 旧实现只按时间节流 —— 5s 内第二次调用直接 `return { systemAddition: '' }`,
|
|
20
|
+
// 而 /channel 切换后重建 agent 只要几毫秒 ⇒ 新 agent 几乎必然落在窗口里,
|
|
21
|
+
// 拿到的**身份文档是空的** (persona 一个字都没有), 用户看到的就是
|
|
22
|
+
// 「切了智能体, 身份文档没换」。切了 agent/channel = 身份变了 ⇒ 必须重算。
|
|
23
|
+
// 同一身份连来两次仍走节流 (原意: 防循环); 上下文本身还有 cwd 级缓存兜底。
|
|
24
|
+
const identityKey = `${opts.agentId || ''}\u0000${opts.channelId || ''}`;
|
|
25
|
+
if (identityKey === lastIdentityKey && start - lastSessionStartAt < MIN_INTERVAL_MS) {
|
|
26
|
+
// 限流: 身份没变, 调用方此刻手里的就是这份身份的加法 (不需要重算)
|
|
19
27
|
return { systemAddition: '', collectMs: 0, truncated: false };
|
|
20
28
|
}
|
|
21
29
|
lastSessionStartAt = start;
|
|
30
|
+
lastIdentityKey = identityKey;
|
|
22
31
|
try {
|
|
23
32
|
const ctx = await getCachedBolloonContext({ cwd: opts.cwd ?? process.cwd() }, opts.force ?? false);
|
|
24
33
|
let systemAddition = formatContextForSystemPrompt(ctx, { maxChars: opts.maxChars });
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* persona-init.ts — **确保每个 agent 名下有身份文档** (2026-10-01)
|
|
3
|
+
*
|
|
4
|
+
* 用户报: 「切换之后应该知道加载智能体初始化文档, 为什么目前是无, 所有回复都是一个智能体人格?」
|
|
5
|
+
* 真因: `~/.bolloon/persona/<agentId>/` 下**一个文件都没有** (用户四个 agent 全缺),
|
|
6
|
+
* `loadPersonaDocs(agentId)` 读回来 6 个字段全空 ⇒ 系统提示里只剩 `# Persona (agentId=…)` 这个头
|
|
7
|
+
* + 所有 agent 共用的 INJECT 工作纪律 ⇒ 换谁都一样。
|
|
8
|
+
* 而且仓里此前**没有任何"建身份文档"的路径** (只有读的 persona-loader + 一个模板 json + 测试助手)。
|
|
9
|
+
*
|
|
10
|
+
* 这里补上那条路: 按 `persona-loader` 认的 6 个文件名生成**起步文档**, 幂等 (已存在不覆盖 ——
|
|
11
|
+
* 用户写过的内容绝不能被模板盖掉)。
|
|
12
|
+
*
|
|
13
|
+
* 拿的是"能立刻用"的起步稿, 不是空壳: 每份都带该 agent 的名字与 agentId, 并**明确标注是模板**,
|
|
14
|
+
* 提示用户去编辑; 不同 agent 生成的正文必然不同 (有名字/agentId), 所以"所有人格一个样"从结构上就不成立。
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from 'node:fs/promises';
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
import * as os from 'node:os';
|
|
19
|
+
import * as path from 'node:path';
|
|
20
|
+
/** 与 `persona-loader.ts` 的 FILE_KEYS 对齐 (读侧是唯一权威, 这里只复述文件名) */
|
|
21
|
+
export const PERSONA_DOC_FILES = ['soul', 'identity', 'project', 'user', 'agent', 'wiki'];
|
|
22
|
+
/** 与 persona-loader 的 sanitizeAgentId 同口径 */
|
|
23
|
+
export function sanitizePersonaAgentId(agentId) {
|
|
24
|
+
return String(agentId || '').replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
|
|
25
|
+
}
|
|
26
|
+
export function personaDirOf(agentId, home) {
|
|
27
|
+
const root = home || os.homedir();
|
|
28
|
+
return path.join(root, '.bolloon', 'persona', sanitizePersonaAgentId(agentId));
|
|
29
|
+
}
|
|
30
|
+
/** 该 agent 自己的 persona.json 里的名字 (同步读; 没有就 undefined) */
|
|
31
|
+
export function readPersonaNameSync(agentId, home) {
|
|
32
|
+
try {
|
|
33
|
+
const p = path.join(personaDirOf(agentId, home), 'persona.json');
|
|
34
|
+
const j = JSON.parse(readFileSync(p, 'utf-8'));
|
|
35
|
+
return String(j?.name || '').trim() || undefined;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const TITLES = {
|
|
42
|
+
soul: 'soul — 我是谁 / 我认什么',
|
|
43
|
+
identity: 'identity — 身份与边界',
|
|
44
|
+
project: 'project — 我在做的项目',
|
|
45
|
+
user: 'user — 我在服务的人',
|
|
46
|
+
agent: 'agent — 我的工作方式',
|
|
47
|
+
wiki: 'wiki — 我的知识索引',
|
|
48
|
+
};
|
|
49
|
+
function starter(file, agentId, name) {
|
|
50
|
+
const head = `---\nagentId: ${sanitizePersonaAgentId(agentId)}\ntitle: ${TITLES[file]}\nstage: starter\n---\n\n`;
|
|
51
|
+
const body = {
|
|
52
|
+
soul: `# ${name} 的 soul\n\n`
|
|
53
|
+
+ `> 这是**起步模板** (由 bolloon 在建/切 agent 时自动生成, 只生成一次)。请把它改成你自己的。\n\n`
|
|
54
|
+
// 2026-10-01: 标题行必须与**该 agent 自己的 persona.json** 一致 ——
|
|
55
|
+
// 之前用渠道名生成, 于是 soul.md 写「我是 233」而 persona.json 写「小龙」⇒ 用户看到"身份没匹配上" ✗
|
|
56
|
+
+ `我是 **${name}**。\n\n`
|
|
57
|
+
+ `- 我认什么: (待写 — 我的判断准则、我拒绝做什么)\n`
|
|
58
|
+
+ `- 我说话的方式: (待写 — 语气/详略/爱用的结构)\n`
|
|
59
|
+
+ `- 我不知道时会怎么做: (待写 — 先问 / 先查 / 先给假设并标注)\n`,
|
|
60
|
+
identity: `# ${name} 的身份与边界\n\n`
|
|
61
|
+
+ `- agentId: \`${sanitizePersonaAgentId(agentId)}\`\n`
|
|
62
|
+
+ `- 名字: ${name}\n`
|
|
63
|
+
+ `- 归属: (待写 — 属于谁 / 代表谁)\n`
|
|
64
|
+
+ `- 边界: (待写 — 哪些事我不能替人决定)\n`,
|
|
65
|
+
project: `# ${name} 在做的事\n\n`
|
|
66
|
+
+ `- 当前目标: (待写)\n`
|
|
67
|
+
+ `- 进行中的线: (待写)\n`
|
|
68
|
+
+ `- 不做的事: (待写)\n`,
|
|
69
|
+
user: `# ${name} 服务的人\n\n`
|
|
70
|
+
+ `- 偏好: (待写 — 语言/格式/节奏)\n`
|
|
71
|
+
+ `- 忌讳: (待写)\n`,
|
|
72
|
+
agent: `# ${name} 的工作方式\n\n`
|
|
73
|
+
+ `1. 先确认现状 (读真文件/真输出), 不靠记忆猜\n`
|
|
74
|
+
+ `2. 改动前后都给出可核验的证据 (命令、数字、路径)\n`
|
|
75
|
+
+ `3. 拿不准就说不确定, 并给出下一步怎么确认\n`,
|
|
76
|
+
wiki: `# ${name} 的知识索引\n\n`
|
|
77
|
+
+ `- 稳定结论放这里; 过程留在会话/日志里\n`
|
|
78
|
+
+ `- (待写) 我负责的领域与其唯一出处\n`,
|
|
79
|
+
};
|
|
80
|
+
return head + body[file];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 确保该 agent 名下有 6 份身份文档。**已存在的一律不碰** (幂等; 用户写过的内容不可被模板覆盖)。
|
|
84
|
+
* 失败不抛 —— 调用方 (getAgent 等) 不该因为写文档失败而崩; 返回已建/已保留的清单供如实汇报。
|
|
85
|
+
*/
|
|
86
|
+
export async function ensurePersonaDocs(agentId, opts = {}) {
|
|
87
|
+
const id = String(agentId || '').trim();
|
|
88
|
+
const dir = personaDirOf(id, opts.home);
|
|
89
|
+
const out = { agentId: sanitizePersonaAgentId(id), dir, created: [], kept: [] };
|
|
90
|
+
if (!id)
|
|
91
|
+
return out;
|
|
92
|
+
const name = (opts.name || '').trim() || id;
|
|
93
|
+
try {
|
|
94
|
+
await fs.mkdir(dir, { recursive: true });
|
|
95
|
+
for (const f of PERSONA_DOC_FILES) {
|
|
96
|
+
const file = path.join(dir, `${f}.md`);
|
|
97
|
+
let exists = false;
|
|
98
|
+
try {
|
|
99
|
+
await fs.access(file);
|
|
100
|
+
exists = true;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
exists = false;
|
|
104
|
+
}
|
|
105
|
+
if (exists) {
|
|
106
|
+
out.kept.push(f);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
await fs.writeFile(file, starter(f, id, name), 'utf-8');
|
|
110
|
+
out.created.push(f);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// 静默: 写不进去不影响本次对话 (但调用方可通过 created/kept 看出没建起来)
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
/** agent 自己写的人格在文档里的标记区 (手写的部分在标记外, 永不覆盖) */
|
|
119
|
+
export const PERSONA_AUTO_BEGIN = '<!-- persona:auto:begin -->';
|
|
120
|
+
export const PERSONA_AUTO_END = '<!-- persona:auto:end -->';
|
|
121
|
+
/**
|
|
122
|
+
* **写透**: 把 agent 自己设的 persona 落进它的身份文档 (2026-10-01)。
|
|
123
|
+
*
|
|
124
|
+
* 为什么必须做: 有身份文档的 agent 不再套用 persona.json ⇒ 身份由 6 份文档承担。
|
|
125
|
+
* 而 `set_persona` 原先只写 persona.json ⇒ **agent 自己改的人格根本不进系统提示**,
|
|
126
|
+
* 用户的感受就是"每次让智能体改, 都是同一个"(各 agent 的文档还都是同一份模板)。
|
|
127
|
+
*
|
|
128
|
+
* 规矩: 只重写 `<!-- persona:auto -->` 标记**之内**的内容 —— 标记外是你手写的, 一个字都不动。
|
|
129
|
+
* 落在 `soul.md` (人格/基调) 与 `identity.md` (身份/边界) 两份里。
|
|
130
|
+
*/
|
|
131
|
+
export async function applyPersonaToDocs(agentId, persona, opts = {}) {
|
|
132
|
+
const id = String(agentId || '').trim();
|
|
133
|
+
if (!id)
|
|
134
|
+
return [];
|
|
135
|
+
const dir = personaDirOf(id, opts.home);
|
|
136
|
+
// 目录可能还不存在 (该 agent 还没生成过身份文档) ⇒ 先建, 否则两份都写失败、静默返回空
|
|
137
|
+
try {
|
|
138
|
+
await fs.mkdir(dir, { recursive: true });
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
const block = [
|
|
144
|
+
PERSONA_AUTO_BEGIN,
|
|
145
|
+
`- 名字: ${persona.name || id}`,
|
|
146
|
+
persona.description ? `- 定位: ${persona.description}` : '',
|
|
147
|
+
persona.personality ? `- 性恪/基调: ${persona.personality}` : '',
|
|
148
|
+
persona.greeting ? `- 打招呼: ${persona.greeting}` : '',
|
|
149
|
+
persona.capabilities?.length ? `- 我能做: ${persona.capabilities.join(' · ')}` : '',
|
|
150
|
+
persona.interests?.length ? `- 我关心: ${persona.interests.join(' · ')}` : '',
|
|
151
|
+
PERSONA_AUTO_END,
|
|
152
|
+
].filter(Boolean).join('\n');
|
|
153
|
+
const written = [];
|
|
154
|
+
for (const name of ['soul', 'identity']) {
|
|
155
|
+
const file = path.join(dir, `${name}.md`);
|
|
156
|
+
let cur = '';
|
|
157
|
+
try {
|
|
158
|
+
cur = await fs.readFile(file, 'utf-8');
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
cur = '';
|
|
162
|
+
}
|
|
163
|
+
const wrapped = /<!-- persona:auto:begin -->[\s\S]*?<!-- persona:auto:end -->/;
|
|
164
|
+
const next = wrapped.test(cur)
|
|
165
|
+
? cur.replace(wrapped, block)
|
|
166
|
+
: `${cur.trimEnd()}\n\n${block}\n`;
|
|
167
|
+
try {
|
|
168
|
+
await fs.writeFile(file, next, 'utf-8');
|
|
169
|
+
written.push(name);
|
|
170
|
+
}
|
|
171
|
+
catch { /* 单份写失败不影响另一份 */ }
|
|
172
|
+
}
|
|
173
|
+
return written;
|
|
174
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* - formatPersonaForSystemPrompt 固定追加 INJECT 工作纪律段 (Context OS 读取协议),
|
|
12
12
|
* 任何 channel 即使无 persona 文件也有纪律约束.
|
|
13
13
|
*/
|
|
14
|
+
import { sanitizeForLlm } from '../agents/egress-sanitize.js';
|
|
14
15
|
import * as fs from 'fs/promises';
|
|
15
16
|
import * as os from 'os';
|
|
16
17
|
import * as path from 'path';
|
|
@@ -136,6 +137,9 @@ const OUTPUT_ORDER = ['identity', 'soul', 'project', 'user', 'agent', 'wiki'];
|
|
|
136
137
|
* 纪律段不参与动态预算 — 即使没有 persona 文件也有纪律约束.
|
|
137
138
|
*/
|
|
138
139
|
export function formatPersonaForSystemPrompt(docs, maxChars) {
|
|
140
|
+
// 2026-10-01 (落实③): 身份文档最终会**拼进给模型的提示** ⇒ 过**唯一出口**: 先脱敏(凭证一律打码)
|
|
141
|
+
// 再按上限中间省略。文档是用户/智能体自己写的, 里面完全可能出现私钥/助记词/URL 带密码。
|
|
142
|
+
const __sanitize = sanitizeForLlm;
|
|
139
143
|
const cap = maxChars ?? DEFAULT_MAX_CHARS;
|
|
140
144
|
// 段: 标识 + 字段值 (空字段跳过)
|
|
141
145
|
const sections = [];
|
|
@@ -173,5 +177,5 @@ export function formatPersonaForSystemPrompt(docs, maxChars) {
|
|
|
173
177
|
const truncateMarker = '\n... (截断)';
|
|
174
178
|
result = result.substring(0, Math.max(0, cap - truncateMarker.length)) + truncateMarker;
|
|
175
179
|
}
|
|
176
|
-
return result;
|
|
180
|
+
return __sanitize(String(result), 8000);
|
|
177
181
|
}
|