@bolloon/bolloon-agent 0.1.41 → 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/server.js +59 -2
- package/package.json +2 -2
- package/scripts/lefthook-helper.sh +69 -0
package/dist/llm/pi-ai.js
CHANGED
|
@@ -20,21 +20,78 @@ export function getSystemPrependProvider() {
|
|
|
20
20
|
export class PiAIModel {
|
|
21
21
|
config;
|
|
22
22
|
provider;
|
|
23
|
+
/** 单次 LLM HTTP 请求硬上限 (ms), 防止上游卡住挂死整个 loop. 可通过 BOLLOON_LLM_TIMEOUT 覆盖. */
|
|
24
|
+
requestTimeoutMs;
|
|
23
25
|
constructor(config) {
|
|
24
26
|
this.config = config;
|
|
25
27
|
this.provider = config.provider;
|
|
28
|
+
const envTimeout = Number(process.env.BOLLOON_LLM_TIMEOUT);
|
|
29
|
+
this.requestTimeoutMs = Number.isFinite(envTimeout) && envTimeout > 0 ? envTimeout : 120_000;
|
|
26
30
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* 把外部 signal 和内部 timeout 合并: 任一触发都 abort.
|
|
33
|
+
* - 外部 signal 优先 (用户主动 abort)
|
|
34
|
+
* - 否则套 120s timeout
|
|
35
|
+
* - 任一不合法 (非 AbortSignal 实例) 时退到无 signal
|
|
36
|
+
*/
|
|
37
|
+
combinedSignal(external) {
|
|
38
|
+
const valid = external instanceof AbortSignal ? external : undefined;
|
|
39
|
+
// Node 18+ 支持 AbortSignal.timeout, 旧版本兜底用 setTimeout 构造
|
|
40
|
+
if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
|
|
41
|
+
const timeoutSignal = AbortSignal.timeout(this.requestTimeoutMs);
|
|
42
|
+
if (!valid)
|
|
43
|
+
return timeoutSignal;
|
|
44
|
+
// 合并两个 signal
|
|
45
|
+
const ctrl = new AbortController();
|
|
46
|
+
const onAbort = () => ctrl.abort();
|
|
47
|
+
valid.addEventListener('abort', onAbort, { once: true });
|
|
48
|
+
timeoutSignal.addEventListener('abort', onAbort, { once: true });
|
|
49
|
+
if (valid.aborted || timeoutSignal.aborted)
|
|
50
|
+
ctrl.abort();
|
|
51
|
+
return ctrl.signal;
|
|
52
|
+
}
|
|
53
|
+
return valid;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 与 LLM 对话.
|
|
57
|
+
*
|
|
58
|
+
* 支持三种调用形式:
|
|
59
|
+
* - 旧: chat(message: string, context?: string, signal?)
|
|
60
|
+
* 单一 user message + 附加 system context. 简单场景用.
|
|
61
|
+
* - 新: chat(messages: ChatMessage[], context?: string, signal?)
|
|
62
|
+
* 完整 messages 数组, 含 user/assistant/tool/system role.
|
|
63
|
+
* 工具调用场景必用 — 否则 LLM 看不到工具结果.
|
|
64
|
+
* - pivot loop 兼容: chat(context: string, systemPrompt: string, signal?)
|
|
65
|
+
* 第二参超过 2K 时视为 system prompt 覆盖, 避免把 46K context 当 user message 发送.
|
|
66
|
+
*
|
|
67
|
+
* 2026-06-17 (M3.5 调试): buildContext() 之前把所有 history 序列化成单字符串,
|
|
68
|
+
* LLM 看不到 tool 调用的真实结果,导致 CLI loop 卡死.
|
|
69
|
+
* 现在 messages 数组版本保留 role 语义, LLM 能正确看到工具返回.
|
|
70
|
+
*/
|
|
71
|
+
async chat(messageOrMessages, contextOrSystemPrompt, signal) {
|
|
72
|
+
const systemPrompt = await this.buildSystemPromptAsync(contextOrSystemPrompt);
|
|
73
|
+
let messages;
|
|
74
|
+
if (Array.isArray(messageOrMessages)) {
|
|
75
|
+
messages = [{ role: 'system', content: systemPrompt }, ...messageOrMessages];
|
|
76
|
+
}
|
|
77
|
+
else if (contextOrSystemPrompt && contextOrSystemPrompt.length > 2000) {
|
|
78
|
+
const baseSystem = await this.buildSystemPromptAsync(undefined);
|
|
79
|
+
messages = [
|
|
80
|
+
{ role: 'system', content: baseSystem + '\n\n' + contextOrSystemPrompt },
|
|
81
|
+
{ role: 'user', content: messageOrMessages }
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
messages = [
|
|
86
|
+
{ role: 'system', content: systemPrompt },
|
|
87
|
+
{ role: 'user', content: messageOrMessages }
|
|
88
|
+
];
|
|
89
|
+
}
|
|
33
90
|
try {
|
|
34
91
|
const response = await this.generateText({
|
|
35
92
|
messages,
|
|
36
93
|
temperature: 0.8,
|
|
37
|
-
maxTokens:
|
|
94
|
+
maxTokens: 16384, // 2026-06-17: 提到 16384 — agent 注入 16K+ system prompt + 8K tool defs 时, 8K 撞上限返回空 content (见 memory: bolloon-llm-empty-large-prompt)
|
|
38
95
|
signal,
|
|
39
96
|
});
|
|
40
97
|
return { reply: response };
|
|
@@ -215,23 +272,42 @@ export class PiAIModel {
|
|
|
215
272
|
temperature,
|
|
216
273
|
max_tokens: maxTokens
|
|
217
274
|
};
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
275
|
+
// 2026-06-19: 一次命中优化 — 收到空 content 时重试 2 次, 避免 minimax 上游网络抖动
|
|
276
|
+
// 经验: minimax 偶发返回 200 但 content="" (上游 retry 耗尽), 之前当作 sentinel
|
|
277
|
+
// 现在外层加 2 次重试 + 退避, 让 90%+ 的一次调用不出现错误
|
|
278
|
+
// 2026-06-19: 一次命中优化 — 收到空 content 时重试 2 次, 避免 minimax 上游网络抖动
|
|
279
|
+
// 经验: minimax 偶发返回 200 但 content="" (上游 retry 耗尽), 之前当作 sentinel
|
|
280
|
+
// 现在外层加 2 次重试 + 退避, 让 90%+ 的一次调用不出现错误
|
|
281
|
+
let lastFinishReason = '';
|
|
282
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
283
|
+
const response = await fetch(`${this.getBaseUrl()}/chat/completions`, {
|
|
284
|
+
method: 'POST',
|
|
285
|
+
headers: {
|
|
286
|
+
'Content-Type': 'application/json',
|
|
287
|
+
'Authorization': `Bearer ${apiKey}`
|
|
288
|
+
},
|
|
289
|
+
body: JSON.stringify(requestBody),
|
|
290
|
+
signal: this.combinedSignal(signal),
|
|
291
|
+
});
|
|
292
|
+
if (!response.ok) {
|
|
293
|
+
throw new Error(`OpenAI API error: ${response.status}`);
|
|
294
|
+
}
|
|
295
|
+
const data = await response.json();
|
|
296
|
+
const choice = data.choices?.[0];
|
|
297
|
+
const content = choice?.message?.content || '';
|
|
298
|
+
lastFinishReason = choice?.finish_reason || '';
|
|
299
|
+
if (content) {
|
|
300
|
+
if (lastFinishReason === 'length') {
|
|
301
|
+
console.warn(`[pi-ai] hit max_tokens ceiling (model=${this.mapModel()}, max_tokens=${maxTokens}) — caller should trim prompt or raise cap`);
|
|
302
|
+
}
|
|
303
|
+
return content;
|
|
304
|
+
}
|
|
305
|
+
// 空 content: 200 但 content="" → minimax 上游偶发, 退避后重试
|
|
306
|
+
console.warn(`[pi-ai] attempt ${attempt + 1}/3: 空 content (finish_reason=${lastFinishReason}), 退避 1.5s 重试`);
|
|
307
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
232
308
|
}
|
|
233
|
-
|
|
234
|
-
return
|
|
309
|
+
console.warn(`[pi-ai] 3 次重试都返回空 content (finish_reason=${lastFinishReason})`);
|
|
310
|
+
return ''; // 返回空让上层看到 [AI 服务调用失败]
|
|
235
311
|
}
|
|
236
312
|
async callAnthropic(messages, temperature, maxTokens, signal) {
|
|
237
313
|
const apiKey = this.getApiKey();
|
|
@@ -255,7 +331,7 @@ export class PiAIModel {
|
|
|
255
331
|
temperature,
|
|
256
332
|
max_tokens: maxTokens
|
|
257
333
|
}),
|
|
258
|
-
signal,
|
|
334
|
+
signal: this.combinedSignal(signal),
|
|
259
335
|
});
|
|
260
336
|
if (!response.ok) {
|
|
261
337
|
throw new Error(`Anthropic API error: ${response.status}`);
|
|
@@ -275,7 +351,7 @@ export class PiAIModel {
|
|
|
275
351
|
temperature,
|
|
276
352
|
stream: false
|
|
277
353
|
}),
|
|
278
|
-
signal,
|
|
354
|
+
signal: this.combinedSignal(signal),
|
|
279
355
|
});
|
|
280
356
|
if (!response.ok) {
|
|
281
357
|
throw new Error(`Ollama API error: ${response.status}`);
|
|
@@ -302,7 +378,7 @@ export class PiAIModel {
|
|
|
302
378
|
temperature,
|
|
303
379
|
max_tokens: maxTokens
|
|
304
380
|
}),
|
|
305
|
-
signal,
|
|
381
|
+
signal: this.combinedSignal(signal),
|
|
306
382
|
});
|
|
307
383
|
if (!response.ok) {
|
|
308
384
|
throw new Error(`OpenRouter API error: ${response.status}`);
|
|
@@ -335,7 +411,7 @@ export class PiAIModel {
|
|
|
335
411
|
maxOutputTokens: maxTokens
|
|
336
412
|
}
|
|
337
413
|
}),
|
|
338
|
-
signal,
|
|
414
|
+
signal: this.combinedSignal(signal),
|
|
339
415
|
});
|
|
340
416
|
if (!response.ok) {
|
|
341
417
|
throw new Error(`Gemini API error: ${response.status}`);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* local-inbox-bus.ts — 进程内 Agent 通信总线 (2026-06-19)
|
|
3
|
+
*
|
|
4
|
+
* 解决 bolloon "和另一个智能体发消息" 的本地场景:
|
|
5
|
+
* - 同进程内多个 PiAgentSession 实例 (例如 nodeA / nodeB 测试) 互发消息
|
|
6
|
+
* - 走 in-process EventEmitter, 不依赖 P2P 网络/Hyperswarm
|
|
7
|
+
* - 远端通信仍走 p2pNetwork.sendMessage + onMessage
|
|
8
|
+
*
|
|
9
|
+
* 用法:
|
|
10
|
+
* const bus = LocalInboxBus.getInstance();
|
|
11
|
+
* bus.subscribe('nodeA', (msg) => console.log('nodeA got:', msg));
|
|
12
|
+
* bus.deliver('nodeB', { from: 'nodeA', type: 'agent-message', payload: 'hi' });
|
|
13
|
+
*
|
|
14
|
+
* 角色标识: 默认从 process.env.BOLLOON_ROLE 读, 也可显式传
|
|
15
|
+
*/
|
|
16
|
+
import { EventEmitter } from 'events';
|
|
17
|
+
class LocalInboxBus {
|
|
18
|
+
static _instance = null;
|
|
19
|
+
emitter;
|
|
20
|
+
/** 记录已注册的角色, deliver 时找不到会返回 false (避免 silent drop) */
|
|
21
|
+
registeredRoles = new Set();
|
|
22
|
+
constructor() {
|
|
23
|
+
this.emitter = new EventEmitter();
|
|
24
|
+
// 防止 listener 太多导致 memory leak
|
|
25
|
+
this.emitter.setMaxListeners(1000);
|
|
26
|
+
}
|
|
27
|
+
static getInstance() {
|
|
28
|
+
if (!LocalInboxBus._instance) {
|
|
29
|
+
LocalInboxBus._instance = new LocalInboxBus();
|
|
30
|
+
}
|
|
31
|
+
return LocalInboxBus._instance;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 订阅角色 inbox — 投递到该角色的消息会回调 fn
|
|
35
|
+
* 返回 unsubscribe 函数
|
|
36
|
+
*/
|
|
37
|
+
subscribe(role, fn) {
|
|
38
|
+
this.registeredRoles.add(role);
|
|
39
|
+
this.emitter.on(`role:${role}`, fn);
|
|
40
|
+
return () => {
|
|
41
|
+
this.emitter.off(`role:${role}`, fn);
|
|
42
|
+
// 如果没人订阅, 移除 role (避免泄漏)
|
|
43
|
+
if (this.emitter.listenerCount(`role:${role}`) === 0) {
|
|
44
|
+
this.registeredRoles.delete(role);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 投递消息到指定角色
|
|
50
|
+
* 返回 true=已投递, false=角色未注册
|
|
51
|
+
*/
|
|
52
|
+
deliver(role, msg) {
|
|
53
|
+
if (!this.registeredRoles.has(role)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
this.emitter.emit(`role:${role}`, msg);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 列出当前进程所有已注册的角色
|
|
61
|
+
*/
|
|
62
|
+
listRoles() {
|
|
63
|
+
return Array.from(this.registeredRoles);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 检查某角色是否已订阅
|
|
67
|
+
*/
|
|
68
|
+
hasRole(role) {
|
|
69
|
+
return this.registeredRoles.has(role);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export { LocalInboxBus };
|
|
73
|
+
export default LocalInboxBus;
|
|
@@ -18,6 +18,8 @@ import { EventEmitter } from 'events';
|
|
|
18
18
|
// @ts-ignore — b4a 没官方 .d.ts
|
|
19
19
|
import b4a from 'b4a';
|
|
20
20
|
import { loadOrCreateKeyPair, writebackPublicKey } from './p2p-secret.js';
|
|
21
|
+
// 静态 import input-scanner (ESM, 替代 require 避免 "require is not defined" 报错)
|
|
22
|
+
import * as inputScanner from '../security/input-scanner.js';
|
|
21
23
|
export class P2PDirect extends EventEmitter {
|
|
22
24
|
swarm = null;
|
|
23
25
|
name;
|
|
@@ -67,10 +69,7 @@ export class P2PDirect extends EventEmitter {
|
|
|
67
69
|
// P-Action 3: untrusted-input scanner (默认 silence-on-fail + log-only)
|
|
68
70
|
// 包装层 跨机器 @-mention 代发 (已 WORKING) 不能 block-on-suspicion
|
|
69
71
|
try {
|
|
70
|
-
|
|
71
|
-
// (lazy require to keep cold-start fast)
|
|
72
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
73
|
-
const { scanInput, writeScanAudit, shouldHardBlock } = require('../security/input-scanner.js');
|
|
72
|
+
const { scanInput, writeScanAudit, shouldHardBlock } = inputScanner;
|
|
74
73
|
const result = scanInput(buf, { source: 'p2p' });
|
|
75
74
|
if (shouldHardBlock(result)) {
|
|
76
75
|
console.warn(`[P2PDirect:${this.name}] 阻断恶意消息 from ${remotePubKeyHex.substring(0, 12)}... (verdict=${result.verdict}, threats=${result.threats.length})`);
|
|
@@ -21,11 +21,15 @@ import * as path from 'path';
|
|
|
21
21
|
import { loadAllJudgments } from './human-value-store.js';
|
|
22
22
|
// 用 getter 函数而非模块顶层 const: process.env.HOME 在测试 beforeAll 才设,
|
|
23
23
|
// 模块顶层求值时还是真 home. 运行时求值才能正确响应测试 fixture.
|
|
24
|
+
// 跨平台: 优先 process.env.HOME (测试可控), 然后 os.homedir() (Windows 上读 USERPROFILE).
|
|
25
|
+
// 用 path.join 而不是字符串拼接, 保证 Windows 上用 \ 分隔.
|
|
24
26
|
function getUsageLogPath() {
|
|
25
|
-
|
|
27
|
+
const home = process.env.HOME || os.homedir() || '/tmp';
|
|
28
|
+
return path.join(home, '.bolloon', 'human-values', 'usage.jsonl');
|
|
26
29
|
}
|
|
27
30
|
function getEvolutionLogPath() {
|
|
28
|
-
|
|
31
|
+
const home = process.env.HOME || os.homedir() || '/tmp';
|
|
32
|
+
return path.join(home, '.bolloon', 'human-values', 'evolution.jsonl');
|
|
29
33
|
}
|
|
30
34
|
/**
|
|
31
35
|
* P-Action 1: next-action hint (4 仓库都强调的错误信息 = 修复指令 思想)
|
|
@@ -22,9 +22,9 @@ import * as os from 'os';
|
|
|
22
22
|
import * as path from 'path';
|
|
23
23
|
import { getModel } from '../llm/pi-ai.js';
|
|
24
24
|
import { loadAllJudgments } from './human-value-store.js';
|
|
25
|
-
const EVOLUTION_LOG = (process.env.HOME || os.homedir() || '/tmp'
|
|
26
|
-
const COUNTERFACTUAL_LOG = (process.env.HOME || os.homedir() || '/tmp'
|
|
27
|
-
const USAGE_LOG = (process.env.HOME || os.homedir() || '/tmp'
|
|
25
|
+
const EVOLUTION_LOG = path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'human-values', 'evolution.jsonl');
|
|
26
|
+
const COUNTERFACTUAL_LOG = path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'human-values', 'counterfactual-audit.jsonl');
|
|
27
|
+
const USAGE_LOG = path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'human-values', 'usage.jsonl');
|
|
28
28
|
async function readUsageLog() {
|
|
29
29
|
try {
|
|
30
30
|
const content = await fs.readFile(USAGE_LOG, 'utf-8');
|
|
@@ -266,7 +266,7 @@ export async function extractFromFragment(fragmentPath) {
|
|
|
266
266
|
type: 'rule',
|
|
267
267
|
content: ruleContent,
|
|
268
268
|
source: 'human',
|
|
269
|
-
confidence: 0.
|
|
269
|
+
confidence: 0.9,
|
|
270
270
|
context,
|
|
271
271
|
createdAt: new Date().toISOString(),
|
|
272
272
|
updatedAt: new Date().toISOString(),
|
|
@@ -21,6 +21,17 @@ const TOOL_WHITELIST = new Set([
|
|
|
21
21
|
'read', 'write', 'edit_file', 'list_files',
|
|
22
22
|
'list_sessions', 'get_session_state', 'list_messages',
|
|
23
23
|
'send_to_channel', 'create_channel',
|
|
24
|
+
// M2.1 (2026-06-17): 新增的 10 个 agent 工具 (跟 pi-sdk registerTools 同步)
|
|
25
|
+
'write_file', 'git_diff', 'git_commit', 'git_push', 'git_branch',
|
|
26
|
+
'create_task', 'update_task', 'get_task', 'list_tasks',
|
|
27
|
+
'use_skill', 'self_improve',
|
|
28
|
+
// 2026-06-19: M4 新增 (跟 pi-sdk.ts registerTools 同步)
|
|
29
|
+
'read_file', 'delete_file', 'mkdir', 'move_file',
|
|
30
|
+
'grep_files', 'glob_files',
|
|
31
|
+
'git_log', 'git_show', 'git_stash',
|
|
32
|
+
'vitest_run', 'tsc_check',
|
|
33
|
+
// 2026-06-19: Agent Mesh 通信工具 (跟 pi-sdk.ts registerTools 同步)
|
|
34
|
+
'check_inbox', 'send_to_peer', 'p2p_broadcast', 'send_to_local_agent', 'list_local_agents', 'agent_call',
|
|
24
35
|
// MCP 注册的工具
|
|
25
36
|
'mcp_tool',
|
|
26
37
|
]);
|
package/dist/web/server.js
CHANGED
|
@@ -769,6 +769,50 @@ async function handleV3P2PMessage(parsed, conn, comm) {
|
|
|
769
769
|
}
|
|
770
770
|
return;
|
|
771
771
|
}
|
|
772
|
+
// v3 新增: --collab CLI 派任务过来, 对方 web 收到后跑 LLM 干活, 回结果
|
|
773
|
+
if (op === 'agent.collab.run') {
|
|
774
|
+
const { requestId, task, fromPublicKey, fromRole, timeoutMs } = parsed.payload || {};
|
|
775
|
+
if (!requestId || !task) {
|
|
776
|
+
console.warn(`[v3-collab] agent.collab.run 缺少 requestId/task`);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
const senderKey = fromPublicKey || peerKey;
|
|
780
|
+
console.log(`[v3-collab] 收到 ${senderKey.substring(0, 12)}... (${fromRole}) 的协作任务: "${task.substring(0, 60)}..."`);
|
|
781
|
+
const startTs = Date.now();
|
|
782
|
+
try {
|
|
783
|
+
// 调 LLM: 走 getAgentForChannel 拿 agent, 用 prompt() 拿 string 结果
|
|
784
|
+
const visitorHint = `[系统上下文] 协作任务来源: 远端 peer (P2P, publicKey=${senderKey.substring(0, 12)}..., role=${fromRole || 'unknown'}). 这是通过 P2P 派过来的协作任务, 回答后会自动回到对方那里. 简洁完成, 不需要反问.\n\n`;
|
|
785
|
+
const fullPrompt = `【本轮协作任务】\n${task}\n【任务结束】\n\n${visitorHint}`;
|
|
786
|
+
// 临时用 channelId = "__collab__" 拿个一次性 agent
|
|
787
|
+
const collabChannelId = `__collab_${senderKey.substring(0, 8)}__`;
|
|
788
|
+
const agent = await getAgentForChannel(collabChannelId, '', `collab-${senderKey.substring(0, 8)}`, undefined);
|
|
789
|
+
const resultText = await agent.prompt(fullPrompt);
|
|
790
|
+
const reply = JSON.stringify({
|
|
791
|
+
v: 3,
|
|
792
|
+
op: 'agent.collab.reply',
|
|
793
|
+
payload: {
|
|
794
|
+
requestId,
|
|
795
|
+
fromPublicKey: v3P2PRef?.getPublicKey() || '',
|
|
796
|
+
result: resultText || '(empty)',
|
|
797
|
+
durationMs: Date.now() - startTs,
|
|
798
|
+
},
|
|
799
|
+
});
|
|
800
|
+
await comm.sendToConnection(conn.id, reply);
|
|
801
|
+
console.log(`[v3-collab] 已回 reply (${Date.now() - startTs}ms, ${(resultText || '').length} chars)`);
|
|
802
|
+
}
|
|
803
|
+
catch (e) {
|
|
804
|
+
console.error(`[v3-collab] 处理失败:`, e.message);
|
|
805
|
+
try {
|
|
806
|
+
const errReply = JSON.stringify({
|
|
807
|
+
v: 3, op: 'agent.collab.reply',
|
|
808
|
+
payload: { requestId, fromPublicKey: v3P2PRef?.getPublicKey() || '', error: e.message, result: '' }
|
|
809
|
+
});
|
|
810
|
+
await comm.sendToConnection(conn.id, errReply);
|
|
811
|
+
}
|
|
812
|
+
catch { }
|
|
813
|
+
}
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
772
816
|
// v3 新增: 收到远端发来的 @-mention 跨渠道消息, 存到本地 target channel
|
|
773
817
|
if (op === 'agent.cross.post') {
|
|
774
818
|
const { targetChannelId, targetChannelName, originChannelId, originChannelName, text, fromPublicKey } = parsed.payload || {};
|
|
@@ -936,7 +980,14 @@ async function getAgentForChannel(channelId, channelDid, channelName, channelDid
|
|
|
936
980
|
const session = await createAgentSession({
|
|
937
981
|
cwd: process.cwd(),
|
|
938
982
|
peerId: `channel-${channelId}:${currentSessionId}`,
|
|
939
|
-
identityDoc
|
|
983
|
+
identityDoc,
|
|
984
|
+
// M2.3 (2026-06-17): 构造时从 session JSON 回灌历史 — 服务重启后 LLM 仍记得前面对话
|
|
985
|
+
// key 跟 server.ts:240 写入路径保持一致: ~/.bolloon/sessions/cache/<key>.json
|
|
986
|
+
loadSessionKey: sessionKey,
|
|
987
|
+
// M3.1 (2026-06-17): 默认启用 WorkflowPivotLoop — web 路径以前死代码, 现在用
|
|
988
|
+
// pivot loop 自带 quality scoring + task complexity analysis + 30 iters cap
|
|
989
|
+
// (老 ReAct 10000 iter cap 容易跑飞)
|
|
990
|
+
usePivotLoop: true,
|
|
940
991
|
}, true); // forceNew: true 强制创建新实例
|
|
941
992
|
channelSessions.set(sessionKey, session);
|
|
942
993
|
if (channelDid) {
|
|
@@ -1665,13 +1716,19 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1665
1716
|
console.log(`[chat] aborted channel=${channelId}`);
|
|
1666
1717
|
}
|
|
1667
1718
|
else {
|
|
1668
|
-
|
|
1719
|
+
// M1.2 (2026-06-17): 不再 rethrow — 把错误塞到 fullResponse 让 broadcast 出来, 后续 session 仍保存
|
|
1720
|
+
// 旧行为: 抛到外层 catch, 触发 500 + 用户消息丢失 (session 不保存)
|
|
1721
|
+
console.error(`[chat] LLM 调用失败 channel=${channelId}:`, err);
|
|
1722
|
+
fullResponse = `[错误: LLM 调用失败] ${String(err?.message || err).slice(0, 300)}`;
|
|
1723
|
+
broadcast({ type: 'error', content: fullResponse }, channelId);
|
|
1669
1724
|
}
|
|
1670
1725
|
}
|
|
1671
1726
|
// abort 模式: 给 partial 拼后缀
|
|
1672
1727
|
if (runState.abortController?.signal.aborted && fullResponse.trim().length > 0) {
|
|
1673
1728
|
fullResponse = fullResponse + '\n\n_[生成已中断]_';
|
|
1674
1729
|
}
|
|
1730
|
+
// 2026-06-18: 修 lastFinalReply 没设 → /api/loop/inspect 永远返回空
|
|
1731
|
+
runState.lastFinalReply = fullResponse;
|
|
1675
1732
|
// v3 新增: 解析 LLM 回复里的 @-mentions, 转发到目标 channel
|
|
1676
1733
|
await routeMentionsInReply(channelId, fullResponse, localChannels, remoteChannels);
|
|
1677
1734
|
broadcast({ type: 'ai', content: fullResponse }, channelId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bolloon/bolloon-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.42",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
|
|
6
6
|
"main": "dist/cli.js",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"build:all": "npm run build:main && npm run build:web && npm run build:electron && npm run build:cli",
|
|
18
18
|
"start": "node dist/index.js",
|
|
19
19
|
"dev": "tsx -r dotenv/config src/index.ts",
|
|
20
|
-
"dev:web": "npm run build:web &&
|
|
20
|
+
"dev:web": "npm run build:web && tsx -r dotenv/config src/index.ts --web",
|
|
21
21
|
"dev:electron": "npm run build:electron && npm run electron:start",
|
|
22
22
|
"electron:start": "NODE_ENV=development electron dist/electron.js",
|
|
23
23
|
"electron:dev": "concurrently \"npm run dev:web\" \"npm run electron:start\"",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# 2026-06-17: lefthook helper — agent auto-evolve branch detection
|
|
3
|
+
# 写在独立文件里避免 lefthook 嵌套引号 + Windows bash 解析问题
|
|
4
|
+
# 用法: lefthook.yml 里 run: sh scripts/lefthook-helper.sh <command>
|
|
5
|
+
# <command> = commit | vitest-full | build-check | tag-baseline
|
|
6
|
+
|
|
7
|
+
CMD="$1"
|
|
8
|
+
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo none)
|
|
9
|
+
AUTO_EVOLVE_MSG=$(git log -1 --pretty=%B 2>/dev/null | head -1 | grep -c ^auto-evolve: || echo 0)
|
|
10
|
+
|
|
11
|
+
is_master_main() {
|
|
12
|
+
[ "$BRANCH" = "master" ] || [ "$BRANCH" = "main" ]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
is_auto_evolve() {
|
|
16
|
+
if [ "$BOLLOON_AUTO_EVOLVE" = "1" ]; then return 0; fi
|
|
17
|
+
if [ "$AUTO_EVOLVE_MSG" -gt 0 ] 2>/dev/null; then return 0; fi
|
|
18
|
+
return 1
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
case "$CMD" in
|
|
22
|
+
commit-vitest-bail)
|
|
23
|
+
if is_auto_evolve; then
|
|
24
|
+
echo "[skip] vitest-bail (auto-evolve mode, branch=$BRANCH)"
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
npx vitest run --bail=1 --reporter=dot
|
|
28
|
+
;;
|
|
29
|
+
commit-tsc-check)
|
|
30
|
+
if is_auto_evolve; then
|
|
31
|
+
echo "[skip] tsc-check (auto-evolve mode, branch=$BRANCH)"
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
npx tsc --noEmit
|
|
35
|
+
;;
|
|
36
|
+
push-vitest-full)
|
|
37
|
+
if is_master_main; then
|
|
38
|
+
npx vitest run --reporter=dot
|
|
39
|
+
else
|
|
40
|
+
echo "[skip] vitest-full (branch=$BRANCH, not master/main)"
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
;;
|
|
44
|
+
push-build-check)
|
|
45
|
+
if is_master_main; then
|
|
46
|
+
npm run build:main
|
|
47
|
+
else
|
|
48
|
+
echo "[skip] build-check (branch=$BRANCH, not master/main)"
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
;;
|
|
52
|
+
push-tag-baseline)
|
|
53
|
+
if is_master_main; then
|
|
54
|
+
if git tag -l 'auto-evolve-baseline-*' | grep -q .; then
|
|
55
|
+
exit 0
|
|
56
|
+
else
|
|
57
|
+
echo "[ERROR] push to master/main requires auto-evolve-baseline-* tag. Run: bash scripts/auto-evolve-snapshot.sh"
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
60
|
+
else
|
|
61
|
+
echo "[skip] tag-baseline (branch=$BRANCH, not master/main)"
|
|
62
|
+
exit 0
|
|
63
|
+
fi
|
|
64
|
+
;;
|
|
65
|
+
*)
|
|
66
|
+
echo "Unknown command: $CMD"
|
|
67
|
+
exit 1
|
|
68
|
+
;;
|
|
69
|
+
esac
|