@bolloon/bolloon-agent 0.3.27 → 0.3.28
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/decision-store.js +247 -0
- package/dist/agents/pi-sdk-tools.js +344 -0
- package/dist/bootstrap/context-os.js +213 -0
- package/dist/bootstrap/lifecycle-hooks.js +8 -2
- package/dist/bootstrap/memory-compressor.js +126 -1
- package/dist/bootstrap/persona-loader.js +88 -5
- package/dist/pi-ecosystem-mcp/index.js +153 -37
- package/dist/security/tool-gate.js +8 -0
- package/dist/web/server.js +56 -2
- package/package.json +1 -1
|
@@ -4,15 +4,16 @@
|
|
|
4
4
|
* Bridges MCP (Model Context Protocol) servers with Bolloon's tool system.
|
|
5
5
|
* Based on the pi-mcp-adapter philosophy: on-demand tool loading with minimal token overhead.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* 2026-08-03 (验证修复): sendMcpRequest 从 simulated 占位 → 真实 stdio JSON-RPC 通信.
|
|
8
|
+
* - 协议: initialize → notifications/initialized → tools/list → tools/call
|
|
9
|
+
* - 请求/响应按 id 配对, 30s 超时, server 崩溃时 pending 全部 reject
|
|
10
|
+
* - discoverMcpServers 修复重复读 mcpServers 键 (同一个键被读两遍)
|
|
11
11
|
*/
|
|
12
12
|
import * as fs from 'fs/promises';
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import { spawn } from 'child_process';
|
|
15
15
|
import { EventEmitter } from 'events';
|
|
16
|
+
import * as readline from 'readline';
|
|
16
17
|
// MCP adapter state
|
|
17
18
|
let tools = new Map();
|
|
18
19
|
let servers = new Map();
|
|
@@ -22,6 +23,8 @@ let toolCallLog = [];
|
|
|
22
23
|
class McpEventEmitter extends EventEmitter {
|
|
23
24
|
}
|
|
24
25
|
const mcpEvents = new McpEventEmitter();
|
|
26
|
+
const MCP_REQUEST_TIMEOUT_MS = 30_000;
|
|
27
|
+
let mcpRequestSeq = 1;
|
|
25
28
|
/**
|
|
26
29
|
* Discover MCP servers from standard config locations
|
|
27
30
|
*/
|
|
@@ -37,24 +40,17 @@ export async function discoverMcpServers() {
|
|
|
37
40
|
try {
|
|
38
41
|
const content = await fs.readFile(loc, 'utf-8');
|
|
39
42
|
const mcpJson = JSON.parse(content);
|
|
40
|
-
if
|
|
41
|
-
|
|
43
|
+
// 2026-08-03 fix: mcpServers 只读一次 (之前 if + if['mcpServers'] 重复读同一键)
|
|
44
|
+
const serversConfig = (mcpJson.mcpServers ?? mcpJson['mcpServers']);
|
|
45
|
+
if (serversConfig && typeof serversConfig === 'object') {
|
|
46
|
+
for (const [name, config] of Object.entries(serversConfig)) {
|
|
42
47
|
const serverConfig = config;
|
|
48
|
+
if (!serverConfig || typeof serverConfig.command !== 'string')
|
|
49
|
+
continue;
|
|
43
50
|
configs.push({
|
|
44
51
|
name,
|
|
45
52
|
command: serverConfig.command,
|
|
46
|
-
args: serverConfig.args,
|
|
47
|
-
env: serverConfig.env,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
if (mcpJson['mcpServers']) {
|
|
52
|
-
for (const [name, config] of Object.entries(mcpJson['mcpServers'])) {
|
|
53
|
-
const serverConfig = config;
|
|
54
|
-
configs.push({
|
|
55
|
-
name,
|
|
56
|
-
command: serverConfig.command,
|
|
57
|
-
args: serverConfig.args,
|
|
53
|
+
args: Array.isArray(serverConfig.args) ? serverConfig.args : undefined,
|
|
58
54
|
env: serverConfig.env,
|
|
59
55
|
});
|
|
60
56
|
}
|
|
@@ -64,7 +60,15 @@ export async function discoverMcpServers() {
|
|
|
64
60
|
// File doesn't exist, skip
|
|
65
61
|
}
|
|
66
62
|
}
|
|
67
|
-
|
|
63
|
+
// 去重 (同 name 同 command)
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
return configs.filter((c) => {
|
|
66
|
+
const key = `${c.name}::${c.command}`;
|
|
67
|
+
if (seen.has(key))
|
|
68
|
+
return false;
|
|
69
|
+
seen.add(key);
|
|
70
|
+
return true;
|
|
71
|
+
});
|
|
68
72
|
}
|
|
69
73
|
/**
|
|
70
74
|
* Initialize the MCP adapter
|
|
@@ -76,15 +80,60 @@ export async function initializeMcpAdapter() {
|
|
|
76
80
|
console.log(`[McpAdapter] Discovered ${discoveredServers.length} MCP servers`);
|
|
77
81
|
for (const server of discoveredServers) {
|
|
78
82
|
registerServer(server);
|
|
83
|
+
// 2026-08-03: 启动后立即握手 + 发现工具 (真实 stdio 协议)
|
|
84
|
+
await connectAndDiscover(server.name).catch((e) => {
|
|
85
|
+
console.warn(`[McpAdapter] connect ${server.name} 失败:`, e?.message?.slice(0, 120));
|
|
86
|
+
});
|
|
79
87
|
}
|
|
80
88
|
initialized = true;
|
|
81
89
|
mcpEvents.emit('initialized');
|
|
82
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* 连接 MCP server + 握手 + 发现并注册工具 (2026-08-03)
|
|
93
|
+
* 协议序: startServer → initialize → notifications/initialized → tools/list
|
|
94
|
+
*/
|
|
95
|
+
export async function connectAndDiscover(serverName) {
|
|
96
|
+
const server = servers.get(serverName);
|
|
97
|
+
if (!server)
|
|
98
|
+
return [];
|
|
99
|
+
if (!server.running || !server.process) {
|
|
100
|
+
const started = await startServer(serverName);
|
|
101
|
+
if (!started)
|
|
102
|
+
return [];
|
|
103
|
+
// 等 server 就绪
|
|
104
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
105
|
+
}
|
|
106
|
+
await sendMcpRequest(serverName, 'initialize', {
|
|
107
|
+
protocolVersion: '2024-11-05',
|
|
108
|
+
capabilities: {},
|
|
109
|
+
clientInfo: { name: 'bolloon', version: '0.3.27' },
|
|
110
|
+
});
|
|
111
|
+
await sendMcpRequest(serverName, 'notifications/initialized');
|
|
112
|
+
const result = await sendMcpRequest(serverName, 'tools/list');
|
|
113
|
+
const toolsList = Array.isArray(result?.tools) ? result.tools : [];
|
|
114
|
+
const discovered = [];
|
|
115
|
+
for (const t of toolsList) {
|
|
116
|
+
const tool = {
|
|
117
|
+
name: String(t.name || ''),
|
|
118
|
+
description: String(t.description || ''),
|
|
119
|
+
inputSchema: t.inputSchema || {},
|
|
120
|
+
serverName,
|
|
121
|
+
};
|
|
122
|
+
if (!tool.name)
|
|
123
|
+
continue;
|
|
124
|
+
discovered.push(tool);
|
|
125
|
+
registerTool(tool);
|
|
126
|
+
}
|
|
127
|
+
console.log(`[McpAdapter] ${serverName}: 发现 ${discovered.length} 个工具 (${discovered.map((t) => t.name).join(', ')})`);
|
|
128
|
+
return discovered;
|
|
129
|
+
}
|
|
83
130
|
/**
|
|
84
131
|
* Register an MCP server configuration
|
|
85
132
|
*/
|
|
86
133
|
export function registerServer(config) {
|
|
87
|
-
servers.
|
|
134
|
+
if (servers.has(config.name))
|
|
135
|
+
return;
|
|
136
|
+
servers.set(config.name, { config, process: null, running: false, pending: new Map() });
|
|
88
137
|
console.log(`[McpAdapter] Registered server: ${config.name}`);
|
|
89
138
|
}
|
|
90
139
|
/**
|
|
@@ -156,10 +205,12 @@ export async function executeTool(name, args) {
|
|
|
156
205
|
toolCallLog = toolCallLog.slice(-50);
|
|
157
206
|
}
|
|
158
207
|
if (result && typeof result === 'object' && 'content' in result) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
208
|
+
// 2026-08-03: 提取 content 数组里的文本 (agent 直接可用)
|
|
209
|
+
const content = result.content;
|
|
210
|
+
const text = Array.isArray(content)
|
|
211
|
+
? content.map((c) => c?.text ?? '').filter(Boolean).join('\n')
|
|
212
|
+
: JSON.stringify(result);
|
|
213
|
+
return { success: true, content: [{ type: 'text', text }] };
|
|
163
214
|
}
|
|
164
215
|
return { success: true, content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
165
216
|
}
|
|
@@ -170,25 +221,78 @@ export async function executeTool(name, args) {
|
|
|
170
221
|
}
|
|
171
222
|
}
|
|
172
223
|
/**
|
|
173
|
-
* Send MCP request to a server
|
|
224
|
+
* Send MCP request to a server via real stdio JSON-RPC (2026-08-03).
|
|
225
|
+
* - 写 JSON-RPC 行到 server stdin, 从 stdout 按 id 配对响应
|
|
226
|
+
* - 30s 超时; server 进程退出时挂起请求全部 reject
|
|
174
227
|
*/
|
|
175
228
|
async function sendMcpRequest(serverName, method, params) {
|
|
176
229
|
const server = servers.get(serverName);
|
|
177
230
|
if (!server) {
|
|
178
231
|
throw new Error(`Server not registered: ${serverName}`);
|
|
179
232
|
}
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
233
|
+
// 确保 server 进程在跑
|
|
234
|
+
if (!server.running || !server.process || !server.process.stdin?.writable) {
|
|
235
|
+
const started = await startServer(serverName);
|
|
236
|
+
if (!started)
|
|
237
|
+
throw new Error(`无法启动 MCP server: ${serverName}`);
|
|
238
|
+
// 等 500ms 让 server 就绪
|
|
239
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
240
|
+
}
|
|
241
|
+
const child = server.process;
|
|
242
|
+
if (!child.stdin?.writable)
|
|
243
|
+
throw new Error(`MCP server stdin 不可写: ${serverName}`);
|
|
244
|
+
const id = mcpRequestSeq++;
|
|
245
|
+
const isNotification = method.startsWith('notifications/');
|
|
246
|
+
// 通知 (notifications/*) 是 fire-and-forget: 无 id, server 不响应
|
|
247
|
+
const line = JSON.stringify(isNotification
|
|
248
|
+
? { jsonrpc: '2.0', method, params: params ?? {} }
|
|
249
|
+
: { jsonrpc: '2.0', id, method, params: params ?? {} });
|
|
250
|
+
if (isNotification) {
|
|
251
|
+
child.stdin.write(line + '\n');
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
return new Promise((resolve, reject) => {
|
|
255
|
+
const timer = setTimeout(() => {
|
|
256
|
+
server.pending.delete(id);
|
|
257
|
+
reject(new Error(`MCP request timeout (${MCP_REQUEST_TIMEOUT_MS}ms): ${method}`));
|
|
258
|
+
}, MCP_REQUEST_TIMEOUT_MS);
|
|
259
|
+
server.pending.set(id, { resolve, reject, timer });
|
|
260
|
+
child.stdin.write(line + '\n');
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
/** 挂上 stdout 行读取器 (按 id 分发响应) */
|
|
264
|
+
function attachStdoutReader(serverName, child) {
|
|
265
|
+
const server = servers.get(serverName);
|
|
266
|
+
if (!server)
|
|
267
|
+
return;
|
|
268
|
+
const rl = readline.createInterface({ input: child.stdout });
|
|
269
|
+
rl.on('line', (line) => {
|
|
270
|
+
const trimmed = line.trim();
|
|
271
|
+
if (!trimmed)
|
|
272
|
+
return;
|
|
273
|
+
let msg;
|
|
274
|
+
try {
|
|
275
|
+
msg = JSON.parse(trimmed);
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
console.log(`[McpAdapter][${serverName}] non-JSON stdout:`, trimmed.slice(0, 200));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
// 服务端主动推送 (无 id) → 忽略
|
|
282
|
+
if (msg.id === undefined || msg.id === null)
|
|
283
|
+
return;
|
|
284
|
+
const entry = server.pending.get(msg.id);
|
|
285
|
+
if (!entry)
|
|
286
|
+
return;
|
|
287
|
+
clearTimeout(entry.timer);
|
|
288
|
+
server.pending.delete(msg.id);
|
|
289
|
+
if (msg.error) {
|
|
290
|
+
entry.reject(new Error(`MCP error ${msg.error.code}: ${msg.error.message}`));
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
entry.resolve(msg.result);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
192
296
|
}
|
|
193
297
|
/**
|
|
194
298
|
* Start an MCP server process
|
|
@@ -215,16 +319,20 @@ export async function startServer(serverName) {
|
|
|
215
319
|
});
|
|
216
320
|
child.on('error', (err) => {
|
|
217
321
|
console.error(`[McpAdapter][${serverName}] error:`, err);
|
|
322
|
+
rejectAllPending(server, `MCP server process error: ${err.message}`);
|
|
218
323
|
server.running = false;
|
|
219
324
|
server.process = null;
|
|
220
325
|
});
|
|
221
326
|
child.on('exit', (code) => {
|
|
222
327
|
console.log(`[McpAdapter][${serverName}] exited with code:`, code);
|
|
328
|
+
rejectAllPending(server, `MCP server exited with code ${code}`);
|
|
223
329
|
server.running = false;
|
|
224
330
|
server.process = null;
|
|
225
331
|
});
|
|
226
332
|
server.process = child;
|
|
227
333
|
server.running = true;
|
|
334
|
+
// 2026-08-03: 挂 stdout 行读取器, 按 id 分发 JSON-RPC 响应
|
|
335
|
+
attachStdoutReader(serverName, child);
|
|
228
336
|
console.log(`[McpAdapter] Started server: ${serverName}`);
|
|
229
337
|
return true;
|
|
230
338
|
}
|
|
@@ -233,6 +341,14 @@ export async function startServer(serverName) {
|
|
|
233
341
|
return false;
|
|
234
342
|
}
|
|
235
343
|
}
|
|
344
|
+
/** server 退出时把挂起请求全部 reject, 避免调用方永远等待 */
|
|
345
|
+
function rejectAllPending(server, reason) {
|
|
346
|
+
for (const [, entry] of server.pending) {
|
|
347
|
+
clearTimeout(entry.timer);
|
|
348
|
+
entry.reject(new Error(reason));
|
|
349
|
+
}
|
|
350
|
+
server.pending.clear();
|
|
351
|
+
}
|
|
236
352
|
/**
|
|
237
353
|
* Stop an MCP server
|
|
238
354
|
*/
|
|
@@ -46,6 +46,14 @@ const TOOL_WHITELIST = new Set([
|
|
|
46
46
|
'create_skill', 'update_skill', 'list_skill_candidates', 'promote_skill',
|
|
47
47
|
// 2026-08-02: plan/todo/review 工具 (plan-store.ts)
|
|
48
48
|
'create_plan', 'update_plan', 'review_plan', 'list_plans',
|
|
49
|
+
// 2026-08-03: 决策协议工具 (decision-store.ts, Context OS §7 九要素)
|
|
50
|
+
'create_decision', 'decide_decision', 'rollback_decision', 'list_decisions',
|
|
51
|
+
// 2026-08-03: Context OS 资产层工具 (context-os.ts, 12+3 层文件夹体系)
|
|
52
|
+
'list_context_layers', 'write_context_asset', 'read_context_assets',
|
|
53
|
+
// 2026-08-03: MCP 工具 (pi-ecosystem-mcp, 真实 stdio JSON-RPC)
|
|
54
|
+
'mcp_list_tools', 'mcp_tool',
|
|
55
|
+
// 2026-08-03: DID 发布到 IPFS+IPNS (自动安装 Kubo)
|
|
56
|
+
'publish_did',
|
|
49
57
|
// 2026-08-02: 远端 channel 工具 (本地智能体 @ 远程交流)
|
|
50
58
|
'list_remote_channels', 'send_to_remote_channel',
|
|
51
59
|
]);
|
package/dist/web/server.js
CHANGED
|
@@ -1614,6 +1614,47 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1614
1614
|
catch (err) {
|
|
1615
1615
|
console.warn('[createWebServer] bootstrap 失败 (非致命):', err);
|
|
1616
1616
|
}
|
|
1617
|
+
// 2026-08-03 (Context OS P5): 初始化资产层 12+3 目录 (幂等, 每层 README 声明职责边界)
|
|
1618
|
+
try {
|
|
1619
|
+
const { ensureContextOsDirs } = await import('../bootstrap/context-os.js');
|
|
1620
|
+
await ensureContextOsDirs();
|
|
1621
|
+
console.log('[createWebServer] Context OS 资产层就绪 (~/.bolloon/context-os/, 12+3 层)');
|
|
1622
|
+
}
|
|
1623
|
+
catch (err) {
|
|
1624
|
+
console.warn('[createWebServer] Context OS 资产层初始化失败 (非致命):', err);
|
|
1625
|
+
}
|
|
1626
|
+
// 2026-08-03: 后台自动安装/启动本地 Kubo (IPFS+IPNS 发布用, 全自动零手动).
|
|
1627
|
+
// fire-and-forget, 不阻塞 server 启动; 首次下载 ~100MB 需 1-2 分钟.
|
|
1628
|
+
// 装好后 publish_did / DID 发布自动走 IPFS+IPNS; 没装好则降级本地模式.
|
|
1629
|
+
(async () => {
|
|
1630
|
+
try {
|
|
1631
|
+
const sdk = await import('@diap/sdk');
|
|
1632
|
+
const checkKuboSetup = sdk.checkKuboSetup;
|
|
1633
|
+
if (typeof checkKuboSetup !== 'function')
|
|
1634
|
+
return;
|
|
1635
|
+
const setup = await checkKuboSetup(true, true);
|
|
1636
|
+
if (setup?.ready && setup?.daemonRunning) {
|
|
1637
|
+
console.log('[ipfs] 本地 Kubo 就绪 (自动安装/启动) → DID 发布支持 IPFS+IPNS');
|
|
1638
|
+
}
|
|
1639
|
+
else {
|
|
1640
|
+
console.log('[ipfs] Kubo 不可用, DID 发布降级本地模式 (可用 publish_did 工具重试)');
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
catch (e) {
|
|
1644
|
+
console.warn('[ipfs] Kubo 自动安装失败 (非致命):', e?.message?.slice(0, 120));
|
|
1645
|
+
}
|
|
1646
|
+
})();
|
|
1647
|
+
// 2026-08-03: 初始化 MCP 适配器 (读 ~/.mcp.json, 自动握手发现工具).
|
|
1648
|
+
// 后台异步: 不阻塞启动, 失败静默 (agent 调 mcp_list_tools 时再触发).
|
|
1649
|
+
(async () => {
|
|
1650
|
+
try {
|
|
1651
|
+
const { initializeMcpAdapter } = await import('../pi-ecosystem-mcp/index.js');
|
|
1652
|
+
await initializeMcpAdapter();
|
|
1653
|
+
}
|
|
1654
|
+
catch (e) {
|
|
1655
|
+
console.warn('[mcp] 初始化失败 (非致命):', e?.message?.slice(0, 120));
|
|
1656
|
+
}
|
|
1657
|
+
})();
|
|
1617
1658
|
// 重置旧的 agent session,确保使用新的 LLM 配置
|
|
1618
1659
|
const { resetAgentSession } = await import('../agents/pi-sdk.js');
|
|
1619
1660
|
resetAgentSession();
|
|
@@ -3303,7 +3344,7 @@ ${goalDesc}
|
|
|
3303
3344
|
}
|
|
3304
3345
|
if (summariesToRead.length > 0) {
|
|
3305
3346
|
const memBlock = summariesToRead.map(s => s.trim().slice(-1500)).join('\n\n---\n\n');
|
|
3306
|
-
contextHint += `[系统上下文] 本 channel
|
|
3347
|
+
contextHint += `[系统上下文] 动态状态层 · chat-worksite (上次做到哪 — 本 channel 历史记忆, 来自 memory-compressor 摘要, 引用而非复述):\n${memBlock.slice(-2500)}\n\n`;
|
|
3307
3348
|
}
|
|
3308
3349
|
}
|
|
3309
3350
|
}
|
|
@@ -3318,12 +3359,25 @@ ${goalDesc}
|
|
|
3318
3359
|
const plans = await listActivePlans();
|
|
3319
3360
|
if (plans.length > 0) {
|
|
3320
3361
|
const plansBlock = plans.slice(0, 3).map(p => planToContext(p)).join('\n\n');
|
|
3321
|
-
contextHint += `[系统上下文]
|
|
3362
|
+
contextHint += `[系统上下文] 动态状态层 · focus (此刻优先级与时间边界 — 进行中的计划, 来自 plan-store, 执行中每完成一步调 update_plan 勾选):\n${plansBlock}\n\n`;
|
|
3322
3363
|
}
|
|
3323
3364
|
}
|
|
3324
3365
|
catch (planErr) {
|
|
3325
3366
|
// 静默失败
|
|
3326
3367
|
}
|
|
3368
|
+
// 2026-08-03 (Context OS P5): 资产层目录注入 — LLM 知道有 12+3 层资产,
|
|
3369
|
+
// 需要细节时用 read_context_assets 按层路由读取, 不全仓扫描 (Context OS §4).
|
|
3370
|
+
try {
|
|
3371
|
+
const { readContextAssets, formatLayerListing } = await import('../bootstrap/context-os.js');
|
|
3372
|
+
const listings = await readContextAssets();
|
|
3373
|
+
const total = listings.reduce((s, l) => s + l.fileCount, 0);
|
|
3374
|
+
if (total > 0) {
|
|
3375
|
+
contextHint += `[系统上下文] 资产层 (Context OS 12+3 层, ${total} 篇资产 — 任务需要细节时用 read_context_assets 按层读取, 不要假装知道没读过的内容):\n${formatLayerListing(listings)}`;
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
catch (ctxErr) {
|
|
3379
|
+
// 静默失败
|
|
3380
|
+
}
|
|
3327
3381
|
const linkedIds = channelForJudgment?.linkedDocumentIds;
|
|
3328
3382
|
if (Array.isArray(linkedIds) && linkedIds.length > 0) {
|
|
3329
3383
|
try {
|