@bolloon/bolloon-agent 0.2.15 → 0.3.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/goal-resume.js +321 -0
- package/dist/agents/pi-sdk-tools.js +99 -0
- package/dist/bollharness-integration/skill-adapter.js +111 -0
- package/dist/bootstrap/lifecycle-hooks.js +45 -0
- package/dist/llm/system-prompt/layers/channel/human-async.md +41 -0
- package/dist/llm/system-prompt/layers/channel/p2p-peer-sync.md +51 -0
- package/dist/llm/system-prompt/layers/channel/p2p-proactive.md +43 -0
- package/dist/llm/system-prompt/layers/channel/session-handoff.md +61 -0
- package/dist/llm/system-prompt/layers/core/external-engagement.md +73 -0
- package/dist/llm/system-prompt/layers/core/identity.md +30 -19
- package/dist/llm/system-prompt/layers/tool/goal_handoff.md +77 -0
- package/dist/llm/system-prompt/layers/tool/p2p_request.md +61 -0
- package/dist/llm/system-prompt/registry.js +20 -2
- package/package.json +1 -1
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* goal-resume — 双栖 agent 网络的"目标接力"原语
|
|
3
|
+
*
|
|
4
|
+
* 设计动机 (2026-07-10):
|
|
5
|
+
* - bolloon 改造: 从本地优先 → 远程/本地双栖 agent 网络.
|
|
6
|
+
* - 需要"目标不中断"机制: 用户当前 task 跑一半, 切到另一个 channel / 切到对端 peer
|
|
7
|
+
* / 用户离开, 目标不能丢.
|
|
8
|
+
* - 现有 task-state.ts 已有 'paused' 状态, 但没有跨 session / 跨机器的"目标快照+恢复"流.
|
|
9
|
+
* - 本文件提供 parkGoal / resumeGoal / continueGoalInBackground, 在 4 级 Bolloon.md 之外的
|
|
10
|
+
* 运行时层做目标接力.
|
|
11
|
+
*
|
|
12
|
+
* 复用现有:
|
|
13
|
+
* - task-state.ts: Task.status = 'paused' (park) / 'running' (resume)
|
|
14
|
+
* - session-store.ts: 消息历史 (saveMessages / loadMessages)
|
|
15
|
+
* - chat-archiver.ts: 高频归档 (~/.bolloon/archive/<peerDID>/YYYY-MM.jsonl)
|
|
16
|
+
* - p2p-outbox.ts: 离线消息不丢
|
|
17
|
+
* - injectJudgmentGate (pi-sdk.ts): judgment 注入门; goal handoff 路径上保留注入
|
|
18
|
+
*
|
|
19
|
+
* 不重复造:
|
|
20
|
+
* - 没有新数据库. 落盘 = ~/.bolloon/goals/snapshot.jsonl (append-only, 与 chat-archiver 同模式)
|
|
21
|
+
* - 没有新 LLM 调度. LLM 工具入口在 pi-sdk-tools.ts 注册, 复用现有 pi-ai.ts 调度.
|
|
22
|
+
*
|
|
23
|
+
* 安全 / 边界:
|
|
24
|
+
* - 人类隐私 judgment (target_id 包含"用户偏好"语义) 不外泄给 peer;
|
|
25
|
+
* continueGoalInBackground 只发"目标描述 + 已完成步骤", 不发 judgment 内容.
|
|
26
|
+
* - 任何 IO 失败静默 (返回 null / error 对象), 不阻塞主对话.
|
|
27
|
+
*/
|
|
28
|
+
import * as fs from 'fs/promises';
|
|
29
|
+
import * as os from 'os';
|
|
30
|
+
import * as path from 'path';
|
|
31
|
+
import * as crypto from 'crypto';
|
|
32
|
+
import { sessionStore } from './session-store.js';
|
|
33
|
+
import * as taskState from './task-state.js';
|
|
34
|
+
import { onGoalParked, onGoalResumed } from '../bootstrap/lifecycle-hooks.js';
|
|
35
|
+
// ============================================================
|
|
36
|
+
// 内部: 落盘 + 读盘
|
|
37
|
+
// ============================================================
|
|
38
|
+
const GOALS_DIR = path.join(os.homedir(), '.bolloon', 'goals');
|
|
39
|
+
const SNAPSHOT_FILE = path.join(GOALS_DIR, 'snapshot.jsonl');
|
|
40
|
+
const EVENT_FILE = path.join(GOALS_DIR, 'event.jsonl');
|
|
41
|
+
/** 写一条 park/resume event (独立文件, 不影响 snapshot 覆盖逻辑) */
|
|
42
|
+
async function appendEvent(event) {
|
|
43
|
+
try {
|
|
44
|
+
await ensureGoalsDir();
|
|
45
|
+
await fs.appendFile(EVENT_FILE, JSON.stringify({ ...event, ts: new Date().toISOString() }) + '\n', 'utf-8');
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// 静默 — event log 失败不应阻塞
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function ensureGoalsDir() {
|
|
52
|
+
await fs.mkdir(GOALS_DIR, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
/** append-only 写一行 JSON */
|
|
55
|
+
async function appendSnapshot(snap) {
|
|
56
|
+
try {
|
|
57
|
+
await ensureGoalsDir();
|
|
58
|
+
await fs.appendFile(SNAPSHOT_FILE, JSON.stringify(snap) + '\n', 'utf-8');
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
// 静默失败 — 落盘失败不应阻塞主对话
|
|
62
|
+
if (process.env.BOLLOON_VERBOSE === '1') {
|
|
63
|
+
console.warn(`[goal-resume] appendSnapshot failed: ${err.message?.slice(0, 100)}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** 读所有 snapshot, 按 goalId 分组保留最新一条 */
|
|
68
|
+
async function loadAllSnapshots() {
|
|
69
|
+
const out = new Map();
|
|
70
|
+
try {
|
|
71
|
+
const raw = await fs.readFile(SNAPSHOT_FILE, 'utf-8');
|
|
72
|
+
for (const line of raw.split('\n')) {
|
|
73
|
+
if (!line.trim())
|
|
74
|
+
continue;
|
|
75
|
+
try {
|
|
76
|
+
const snap = JSON.parse(line);
|
|
77
|
+
if (snap.schemaVersion !== 1)
|
|
78
|
+
continue;
|
|
79
|
+
out.set(snap.goalRef.goalId, snap); // 后写覆盖先写 (新状态覆盖旧状态)
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// 跳过损坏行
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// 文件不存在 = 空 map
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
function newGoalId() {
|
|
92
|
+
return `goal-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
|
|
93
|
+
}
|
|
94
|
+
// ============================================================
|
|
95
|
+
// Park 现有 task (复用 task-state.ts 'paused' 状态)
|
|
96
|
+
// ============================================================
|
|
97
|
+
async function parkAssociatedTask(taskId, goalId) {
|
|
98
|
+
if (!taskId)
|
|
99
|
+
return;
|
|
100
|
+
try {
|
|
101
|
+
// 1. 把 task 状态置 'paused'
|
|
102
|
+
await taskState.updateTask(taskId, { status: 'paused' });
|
|
103
|
+
// 2. 在 goal 字段里塞 goalId (让反向查找有据可查)
|
|
104
|
+
const t = await taskState.getTask(taskId);
|
|
105
|
+
if (t) {
|
|
106
|
+
// 不覆盖已有 goal 字段, 改用 metadata 注释
|
|
107
|
+
// (task-state.ts 的 Task interface 没 metadata, 跳过避免 schema 改动)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
if (process.env.BOLLOON_VERBOSE === '1') {
|
|
112
|
+
console.warn(`[goal-resume] parkAssociatedTask failed: ${err.message?.slice(0, 100)}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function resumeAssociatedTask(taskId, newSessionKey) {
|
|
117
|
+
if (!taskId)
|
|
118
|
+
return;
|
|
119
|
+
try {
|
|
120
|
+
const patch = { status: 'running' };
|
|
121
|
+
if (newSessionKey)
|
|
122
|
+
patch.sessionKey = newSessionKey;
|
|
123
|
+
await taskState.updateTask(taskId, patch);
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
if (process.env.BOLLOON_VERBOSE === '1') {
|
|
127
|
+
console.warn(`[goal-resume] resumeAssociatedTask failed: ${err.message?.slice(0, 100)}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// ============================================================
|
|
132
|
+
// 公共 API
|
|
133
|
+
// ============================================================
|
|
134
|
+
/**
|
|
135
|
+
* Park 当前目标 — 把 session 状态快照 + task 状态落盘, 切走/离开不丢目标.
|
|
136
|
+
*
|
|
137
|
+
* @param goalRef 目标引用 (goalId 必须已存在, 不能传空)
|
|
138
|
+
* @param reason park 原因 (决定后续恢复策略)
|
|
139
|
+
* @returns GoalHandle (含 error 字段, 不抛错)
|
|
140
|
+
*/
|
|
141
|
+
export async function parkGoal(goalRef, reason) {
|
|
142
|
+
if (!goalRef.goalId || !goalRef.targetId) {
|
|
143
|
+
return { goalId: goalRef.goalId, targetId: goalRef.targetId, state: 'parked', error: 'goalId/targetId 必填' };
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
// 1. 拉最近 30 条消息
|
|
147
|
+
const messages = await sessionStore.loadMessages(goalRef.originChannel) ?? [];
|
|
148
|
+
const recentMessages = messages.slice(-30);
|
|
149
|
+
// 2. 找关联 task (用 goalId 反向搜, 或 caller 传 taskId)
|
|
150
|
+
// 简化: caller 负责在 goalRef 里塞 taskId; 此处拿不到 — 跳过
|
|
151
|
+
const taskId = null; // TODO: 反向索引 (后续阶段)
|
|
152
|
+
const taskState_ = null;
|
|
153
|
+
// 3. 落 snapshot
|
|
154
|
+
const snap = {
|
|
155
|
+
goalRef,
|
|
156
|
+
sessionKey: goalRef.originChannel,
|
|
157
|
+
recentMessages,
|
|
158
|
+
taskId,
|
|
159
|
+
taskState: taskState_,
|
|
160
|
+
parkReason: reason,
|
|
161
|
+
parkedAt: new Date().toISOString(),
|
|
162
|
+
schemaVersion: 1,
|
|
163
|
+
};
|
|
164
|
+
await appendSnapshot(snap);
|
|
165
|
+
// 4. 写 park event 留痕 (独立 event.jsonl, 不污染 snapshot 覆盖)
|
|
166
|
+
await appendEvent({ goalId: goalRef.goalId, event: 'goal_parked', targetId: goalRef.targetId, reason });
|
|
167
|
+
// 5. park 关联 task
|
|
168
|
+
await parkAssociatedTask(taskId, goalRef.goalId);
|
|
169
|
+
// 6. 触发 lifecycle hook (留痕到 ~/.bolloon/sessions/goal-parked.jsonl)
|
|
170
|
+
await onGoalParked({
|
|
171
|
+
goalId: goalRef.goalId,
|
|
172
|
+
targetId: goalRef.targetId,
|
|
173
|
+
reason,
|
|
174
|
+
originChannel: goalRef.originChannel,
|
|
175
|
+
sessionKey: goalRef.originChannel,
|
|
176
|
+
taskId,
|
|
177
|
+
});
|
|
178
|
+
return { goalId: goalRef.goalId, targetId: goalRef.targetId, state: 'parked', taskId: taskId ?? undefined };
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
return { goalId: goalRef.goalId, targetId: goalRef.targetId, state: 'parked', error: `park 失败: ${err.message?.slice(0, 100)}` };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* 恢复目标 — 重新加载 session 消息历史 + 把 task 状态置 running.
|
|
186
|
+
*
|
|
187
|
+
* @param goalId park 时记的 goalId
|
|
188
|
+
* @param options newSession = true 时返回新 session key
|
|
189
|
+
*/
|
|
190
|
+
export async function resumeGoal(goalId, options = {}) {
|
|
191
|
+
if (!goalId) {
|
|
192
|
+
return { goalId, targetId: '', state: 'resumed', error: 'goalId 必填' };
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
const allSnaps = await loadAllSnapshots();
|
|
196
|
+
const snap = allSnaps.get(goalId);
|
|
197
|
+
if (!snap) {
|
|
198
|
+
return { goalId, targetId: '', state: 'resumed', error: `goal ${goalId} 未找到 (未 park 过)` };
|
|
199
|
+
}
|
|
200
|
+
// 1. 把末 30 条消息灌回 session (供 LLM 立即看到上下文)
|
|
201
|
+
const targetKey = options.newSession
|
|
202
|
+
? `${snap.goalRef.originChannel}:resume-${Date.now()}`
|
|
203
|
+
: snap.goalRef.originChannel;
|
|
204
|
+
await sessionStore.saveMessages(targetKey, snap.recentMessages);
|
|
205
|
+
// 2. resume 关联 task
|
|
206
|
+
await resumeAssociatedTask(snap.taskId, options.newSession ? targetKey : undefined);
|
|
207
|
+
// 3. 写一条 resume 留痕 (独立 event.jsonl)
|
|
208
|
+
await appendEvent({ goalId, event: 'goal_resumed', targetId: snap.goalRef.targetId, resumedIn: targetKey });
|
|
209
|
+
// 4. 触发 lifecycle hook
|
|
210
|
+
await onGoalResumed({
|
|
211
|
+
goalId,
|
|
212
|
+
targetId: snap.goalRef.targetId,
|
|
213
|
+
originChannel: snap.goalRef.originChannel,
|
|
214
|
+
resumedIn: targetKey,
|
|
215
|
+
taskId: snap.taskId,
|
|
216
|
+
});
|
|
217
|
+
return {
|
|
218
|
+
goalId,
|
|
219
|
+
targetId: snap.goalRef.targetId,
|
|
220
|
+
state: 'resumed',
|
|
221
|
+
resumedIn: targetKey,
|
|
222
|
+
taskId: snap.taskId ?? undefined,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
return { goalId, targetId: '', state: 'resumed', error: `resume 失败: ${err.message?.slice(0, 100)}` };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* 把目标推到对端 peer — 包含 park (本机) + P2P 推消息 (对端) + 期望对端 resume.
|
|
231
|
+
*
|
|
232
|
+
* 流程:
|
|
233
|
+
* 1. park 本机的 goal
|
|
234
|
+
* 2. 通过现有 p2pNetwork.sendMessage 发一条 'goal_continue' 类型消息给对端
|
|
235
|
+
* 3. 对端 hook 收到 → 自动调 resumeGoal (对端有同名 hook 处理 — 后续阶段)
|
|
236
|
+
* 4. 返回 handle 含 peerSessionId (对端生成) — 但**对端 id 当前拿不到**, 留 TODO
|
|
237
|
+
*
|
|
238
|
+
* @param goalRef 同 parkGoal
|
|
239
|
+
* @param peerDid 对端 DID (来自 list_peers 结果)
|
|
240
|
+
* @param p2pSendMessage 注入 P2P 发送函数 (避免循环依赖; pi-sdk-tools.ts 注入)
|
|
241
|
+
*/
|
|
242
|
+
export async function continueGoalInBackground(goalRef, peerDid, p2pSendMessage) {
|
|
243
|
+
if (!goalRef.goalId || !peerDid) {
|
|
244
|
+
return { handle: { goalId: goalRef.goalId, targetId: goalRef.targetId, state: 'continued_background', error: 'goalId/peerDid 必填' } };
|
|
245
|
+
}
|
|
246
|
+
// 1. park 本机
|
|
247
|
+
const parkResult = await parkGoal(goalRef, 'peer_handoff');
|
|
248
|
+
if (parkResult.error) {
|
|
249
|
+
return { handle: parkResult };
|
|
250
|
+
}
|
|
251
|
+
// 2. 推给对端
|
|
252
|
+
try {
|
|
253
|
+
// 隐私过滤: 不发 judgment 内容, 只发 target_id + 末 5 条消息摘要
|
|
254
|
+
const safePayload = {
|
|
255
|
+
event: 'goal_continue',
|
|
256
|
+
goalId: goalRef.goalId,
|
|
257
|
+
targetId: goalRef.targetId,
|
|
258
|
+
originChannel: goalRef.originChannel,
|
|
259
|
+
createdBy: goalRef.createdBy,
|
|
260
|
+
recentMessages: (parkResult.error ? [] : (await sessionStore.loadMessages(goalRef.originChannel) ?? [])).slice(-5),
|
|
261
|
+
timestamp: new Date().toISOString(),
|
|
262
|
+
};
|
|
263
|
+
const sendResult = await p2pSendMessage(peerDid, 'goal_continue', JSON.stringify(safePayload));
|
|
264
|
+
if (!sendResult.success) {
|
|
265
|
+
return { handle: { ...parkResult, state: 'continued_background', error: `P2P 推失败: ${sendResult.error?.slice(0, 100)}` } };
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
handle: { ...parkResult, state: 'continued_background', peerSessionId: '(对端生成, 当前拿不到 — TODO)' },
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
return { handle: { ...parkResult, state: 'continued_background', error: `continue 失败: ${err.message?.slice(0, 100)}` } };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* 列出所有 park 的 goal — 用于 LLM 切回时找回上下文
|
|
277
|
+
*/
|
|
278
|
+
export async function listParkedGoals(filter = {}) {
|
|
279
|
+
try {
|
|
280
|
+
const all = await loadAllSnapshots();
|
|
281
|
+
let arr = Array.from(all.values());
|
|
282
|
+
if (filter.originChannel)
|
|
283
|
+
arr = arr.filter((s) => s.goalRef.originChannel === filter.originChannel);
|
|
284
|
+
if (filter.createdBy)
|
|
285
|
+
arr = arr.filter((s) => s.goalRef.createdBy === filter.createdBy);
|
|
286
|
+
if (filter.targetIdPrefix)
|
|
287
|
+
arr = arr.filter((s) => s.goalRef.targetId.startsWith(filter.targetIdPrefix));
|
|
288
|
+
return arr.sort((a, b) => b.parkedAt.localeCompare(a.parkedAt));
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return [];
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* 单个 goal 查询
|
|
296
|
+
*/
|
|
297
|
+
export async function getGoal(goalId) {
|
|
298
|
+
try {
|
|
299
|
+
const all = await loadAllSnapshots();
|
|
300
|
+
return all.get(goalId) ?? null;
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// ============================================================
|
|
307
|
+
// 调试 / 健康端点用
|
|
308
|
+
// ============================================================
|
|
309
|
+
/** Goal 落盘目录 (只读) */
|
|
310
|
+
export function goalsDir() {
|
|
311
|
+
return GOALS_DIR;
|
|
312
|
+
}
|
|
313
|
+
/** 重置 (测试用) */
|
|
314
|
+
export async function _resetGoalsForTest() {
|
|
315
|
+
try {
|
|
316
|
+
await fs.unlink(SNAPSHOT_FILE);
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
// 不存在 = OK
|
|
320
|
+
}
|
|
321
|
+
}
|
|
@@ -933,6 +933,105 @@ export function registerBuiltinTools(ctx) {
|
|
|
933
933
|
}
|
|
934
934
|
}
|
|
935
935
|
});
|
|
936
|
+
// ============================================================
|
|
937
|
+
// 2026-07-10 双栖 agent 网络新增: goal handoff 工具
|
|
938
|
+
// (park_goal / resume_goal / continue_goal_background)
|
|
939
|
+
// 内部调用 goal-resume.ts, 错误不抛, 静默返回 error 字段
|
|
940
|
+
// ============================================================
|
|
941
|
+
ctx.tools.set('park_goal', {
|
|
942
|
+
name: 'park_goal',
|
|
943
|
+
description: '暂停当前目标并落盘快照. 切换 channel / 用户离开 / 等对端响应 / 推到对端 前必调. 接收 goalRef (含 goalId + targetId) + reason.',
|
|
944
|
+
parameters: {
|
|
945
|
+
goal_id: '已存在或新生成的 goal ID (建议 goal-${ts}-${rand} 格式)',
|
|
946
|
+
target_id: '用户视角的稳定目标描述 (e.g. "完成财务模块迁移")',
|
|
947
|
+
created_by: 'user | agent | peer',
|
|
948
|
+
origin_channel: '当前 session / channel id',
|
|
949
|
+
reason: 'channel_switch | user_away | awaiting_external | peer_handoff',
|
|
950
|
+
},
|
|
951
|
+
execute: async (args) => {
|
|
952
|
+
try {
|
|
953
|
+
const { parkGoal } = await import('./goal-resume.js');
|
|
954
|
+
const handle = await parkGoal({
|
|
955
|
+
goalId: String(args.goal_id || '').trim(),
|
|
956
|
+
targetId: String(args.target_id || '').trim(),
|
|
957
|
+
createdBy: (args.created_by === 'user' || args.created_by === 'peer') ? args.created_by : 'agent',
|
|
958
|
+
createdAt: new Date().toISOString(),
|
|
959
|
+
originChannel: String(args.origin_channel || '').trim(),
|
|
960
|
+
}, args.reason || 'channel_switch');
|
|
961
|
+
return { success: !handle.error, output: JSON.stringify(handle, null, 2), error: handle.error };
|
|
962
|
+
}
|
|
963
|
+
catch (e) {
|
|
964
|
+
return { success: false, error: `park_goal 失败: ${String(e).slice(0, 200)}` };
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
ctx.tools.set('resume_goal', {
|
|
969
|
+
name: 'resume_goal',
|
|
970
|
+
description: '恢复一个 park 的目标. 加载末 30 条消息到 session + 关联 task 改 running. 切回 channel / 用户回来 / 收到对端 ack 时调.',
|
|
971
|
+
parameters: {
|
|
972
|
+
goal_id: 'park 时记的 goal ID',
|
|
973
|
+
new_session: 'true = 在新 session key 下续, 默认 false (留原 session)',
|
|
974
|
+
channel_id: '可选, 指定 channelId 恢复 (默认 = originChannel)',
|
|
975
|
+
},
|
|
976
|
+
execute: async (args) => {
|
|
977
|
+
try {
|
|
978
|
+
const { resumeGoal } = await import('./goal-resume.js');
|
|
979
|
+
const newSessionStr = String(args.new_session || '').toLowerCase();
|
|
980
|
+
const handle = await resumeGoal(String(args.goal_id || '').trim(), {
|
|
981
|
+
newSession: newSessionStr === 'true' || newSessionStr === '1',
|
|
982
|
+
channelId: args.channel_id ? String(args.channel_id) : undefined,
|
|
983
|
+
});
|
|
984
|
+
return { success: !handle.error, output: JSON.stringify(handle, null, 2), error: handle.error };
|
|
985
|
+
}
|
|
986
|
+
catch (e) {
|
|
987
|
+
return { success: false, error: `resume_goal 失败: ${String(e).slice(0, 200)}` };
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
ctx.tools.set('continue_goal_background', {
|
|
992
|
+
name: 'continue_goal_background',
|
|
993
|
+
description: '把目标推到对端 peer 后台跑. 内部 park 本机 + P2P 推消息 + 隐私过滤 (judgment 不发). 任务大 / 想分工时用.',
|
|
994
|
+
parameters: {
|
|
995
|
+
goal_id: '当前 goal ID',
|
|
996
|
+
target_id: '用户视角目标描述',
|
|
997
|
+
origin_channel: '当前 session id',
|
|
998
|
+
peer_did: '对端 DID (来自 list_peers)',
|
|
999
|
+
},
|
|
1000
|
+
execute: async (args) => {
|
|
1001
|
+
try {
|
|
1002
|
+
const { continueGoalInBackground } = await import('./goal-resume.js');
|
|
1003
|
+
const peerDid = String(args.peer_did || '').trim();
|
|
1004
|
+
if (!peerDid)
|
|
1005
|
+
return { success: false, error: 'peer_did 必填' };
|
|
1006
|
+
// 注入 p2p 发送函数 — 通过 ctx.p2pNetwork 调用
|
|
1007
|
+
const p2pSendMessage = async (peerId, type, message) => {
|
|
1008
|
+
try {
|
|
1009
|
+
// p2pNetwork 在 ctx 里 (由 PiAgentSession 注入)
|
|
1010
|
+
const p2pNetwork = ctx.p2pNetwork;
|
|
1011
|
+
if (!p2pNetwork || typeof p2pNetwork.sendMessage !== 'function') {
|
|
1012
|
+
return { success: false, error: 'p2pNetwork 未注入到 ctx' };
|
|
1013
|
+
}
|
|
1014
|
+
await p2pNetwork.sendMessage(peerId, type, message);
|
|
1015
|
+
return { success: true };
|
|
1016
|
+
}
|
|
1017
|
+
catch (e) {
|
|
1018
|
+
return { success: false, error: String(e).slice(0, 100) };
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
const result = await continueGoalInBackground({
|
|
1022
|
+
goalId: String(args.goal_id || '').trim(),
|
|
1023
|
+
targetId: String(args.target_id || '').trim(),
|
|
1024
|
+
createdBy: 'agent',
|
|
1025
|
+
createdAt: new Date().toISOString(),
|
|
1026
|
+
originChannel: String(args.origin_channel || '').trim(),
|
|
1027
|
+
}, peerDid, p2pSendMessage);
|
|
1028
|
+
return { success: !result.handle.error, output: JSON.stringify(result, null, 2), error: result.handle.error };
|
|
1029
|
+
}
|
|
1030
|
+
catch (e) {
|
|
1031
|
+
return { success: false, error: `continue_goal_background 失败: ${String(e).slice(0, 200)}` };
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
});
|
|
936
1035
|
}
|
|
937
1036
|
/**
|
|
938
1037
|
* 注册 Wallet + Polymarket + Safe 工具 (基于 constraint-runtime/src/tools/).
|
|
@@ -350,6 +350,113 @@ export class CrystalLearnSkill extends BaseSkill {
|
|
|
350
350
|
return this.callLm('You are a failure pattern analyst. Extract failure modes, patterns, and invariants from the task description. Output structured JSON with failure_modes, patterns, invariants.', `Task: ${task}`);
|
|
351
351
|
}
|
|
352
352
|
}
|
|
353
|
+
// ============================================================
|
|
354
|
+
// 2026-07-10 双栖 agent 网络新增 3 个 skill
|
|
355
|
+
// (peer-sync / habit-distill / target-tracker)
|
|
356
|
+
// ============================================================
|
|
357
|
+
/**
|
|
358
|
+
* Peer-Sync Skill — 主动找对端协作.
|
|
359
|
+
*
|
|
360
|
+
* 调用流程:
|
|
361
|
+
* 1. 拿 list_peers (通过 p2pNetwork.getPeers() 或已注入的 ctx.tools)
|
|
362
|
+
* 2. 用 LLM 选最合适的 peer (按 expertise tag / 在线时长 / 任务匹配)
|
|
363
|
+
* 3. send_message 问对方是否接 + 预算
|
|
364
|
+
* 4. 同意后 send_to_channel 建带 target_id 的 channel
|
|
365
|
+
*/
|
|
366
|
+
export class PeerSyncSkill extends BaseSkill {
|
|
367
|
+
name = 'peer-sync';
|
|
368
|
+
description = '主动找对端 bolloon 节点协作. 接收 target_id + task, 自动选 peer + 建 channel. 写 channel 时调 sessionStore.saveMessages + chatArchiver (via goal-resume event log).';
|
|
369
|
+
async execute(params) {
|
|
370
|
+
const targetId = (params.target_id || params.targetId || '');
|
|
371
|
+
const task = (params.task || params.description || '');
|
|
372
|
+
if (!targetId || !task) {
|
|
373
|
+
return this.formatOutput({
|
|
374
|
+
warning: 'target_id 和 task 必填',
|
|
375
|
+
usage: '--harness-skill peer-sync --target_id "完成 X" --task "实现 Y"',
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
this.log(`peer-sync 启动: target_id=${targetId}`);
|
|
379
|
+
// 选 peer 的策略由 LLM 决定 (根据 list_peers 输出 + expertise tag)
|
|
380
|
+
return this.callLm(`You are a peer-selection specialist for the bolloon P2P network.
|
|
381
|
+
Given a target_id and task description, output structured JSON with:
|
|
382
|
+
- selectedPeer: DID of the best peer (or null if none suitable)
|
|
383
|
+
- channelName: human-readable name for the new channel
|
|
384
|
+
- initialMessage: first message to send (≤ 200 chars, brief + asks for acceptance)
|
|
385
|
+
- estimatedRounds: expected back-and-forth count (1-10)
|
|
386
|
+
|
|
387
|
+
Rules:
|
|
388
|
+
- Prefer peers with matching expertise tag
|
|
389
|
+
- If no peer matches, return selectedPeer: null
|
|
390
|
+
- Don't over-explain — peer agent has same identity layer as you`, `target_id: ${targetId}\ntask: ${task}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Habit-Distill Skill — 提炼用户习性, 写到 judgment 库.
|
|
395
|
+
*
|
|
396
|
+
* 调用流程:
|
|
397
|
+
* 1. sessionStore.loadMessages(channelId) 拿当前 session 末 30 条
|
|
398
|
+
* 2. LLM 抽取用户习性 (输入习惯 / 偏好术语 / 反复问的主题 / 纠正记录)
|
|
399
|
+
* 3. humanValueStore.storeHumanJudgment({ content, tags: ['habit'], source: 'habit-distill', privacy: 'private' })
|
|
400
|
+
*/
|
|
401
|
+
export class HabitDistillSkill extends BaseSkill {
|
|
402
|
+
name = 'habit-distill';
|
|
403
|
+
description = '从当前 session 提炼用户习性, 写到 ~/.bolloon/human-values/judgments.json. 调 humanValueStore.storeHumanJudgment, 标签 privacy:private.';
|
|
404
|
+
async execute(params) {
|
|
405
|
+
const sessionKey = (params.session_key || params.sessionKey || '');
|
|
406
|
+
const recentMessagesSummary = (params.messages_summary || '');
|
|
407
|
+
if (!sessionKey) {
|
|
408
|
+
return this.formatOutput({
|
|
409
|
+
warning: 'session_key 必填',
|
|
410
|
+
usage: '--harness-skill habit-distill --session_key <key>',
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
this.log(`habit-distill 启动: session=${sessionKey}`);
|
|
414
|
+
// 抽取习性的 prompt — 让 LLM 输出结构化 JSON
|
|
415
|
+
return this.callLm(`You are a habit-extraction specialist.
|
|
416
|
+
Given a summary of user-assistant interaction, output JSON with:
|
|
417
|
+
- habits: [{ content: string, tags: string[], weight: 'low'|'medium'|'high' }]
|
|
418
|
+
- content: 一个用户偏好/习性的简短描述 (≤ 100 字)
|
|
419
|
+
- tags: 标签如 ['input-style', 'terminology', 'recurring-topic', 'correction']
|
|
420
|
+
- weight: 影响判断力注入门时的优先级
|
|
421
|
+
- skipReason: 若不该蒸馏 (用户拒绝/任务太简单/judgment 库饱和), 填理由
|
|
422
|
+
|
|
423
|
+
Rules:
|
|
424
|
+
- 习性必须是"用户可观察到的稳定模式", 不是单次偏好
|
|
425
|
+
- 隐私相关打 tags 含 'private', 注入门自动过滤
|
|
426
|
+
- 不要重复已有 judgment (调用方负责查重)`, `Session: ${sessionKey}\nMessages summary: ${recentMessagesSummary || '(caller should pass loadMessages output)'}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Target-Tracker Skill — 跨 channel 查 target_id 进展.
|
|
431
|
+
*
|
|
432
|
+
* 调用流程:
|
|
433
|
+
* 1. goal-resume.ts 的 listParkedGoals({ targetIdPrefix: <id> }) 查所有匹配的 snapshot
|
|
434
|
+
* 2. chatArchiver.listPeerSummaries 查 channel 维度
|
|
435
|
+
* 3. 输出结构化 status: { goalId, targetId, parkedAt, reason, recentProgress[] }
|
|
436
|
+
*/
|
|
437
|
+
export class TargetTrackerSkill extends BaseSkill {
|
|
438
|
+
name = 'target-tracker';
|
|
439
|
+
description = '跨 channel 查 target_id 进展. 调 goal-resume listParkedGoals + chatArchiver.listPeerSummaries. 用于切 channel 时不丢目标状态.';
|
|
440
|
+
async execute(params) {
|
|
441
|
+
const targetId = (params.target_id || params.targetId || '');
|
|
442
|
+
const originChannel = (params.origin_channel || '');
|
|
443
|
+
if (!targetId && !originChannel) {
|
|
444
|
+
return this.formatOutput({
|
|
445
|
+
warning: 'target_id 或 origin_channel 至少传一个',
|
|
446
|
+
usage: '--harness-skill target-tracker --target_id <id>',
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
this.log(`target-tracker 启动: target_id=${targetId} channel=${originChannel}`);
|
|
450
|
+
// status JSON 由调用方负责真实查询 (skill 接收参数, 返回查询模板)
|
|
451
|
+
return this.formatOutput({
|
|
452
|
+
skill: this.name,
|
|
453
|
+
description: this.description,
|
|
454
|
+
query_params: { targetId, originChannel },
|
|
455
|
+
action: 'caller must invoke goal-resume.listParkedGoals + chatArchiver.listPeerSummaries with these params',
|
|
456
|
+
expected_output: 'List of GoalSnapshot + per-channel progress summary',
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
353
460
|
/**
|
|
354
461
|
* Skill Adapter - Registers all bollharness skills with Bolloon's SkillRegistry
|
|
355
462
|
*/
|
|
@@ -372,6 +479,10 @@ export class SkillAdapter {
|
|
|
372
479
|
new HarnessEngSkill(),
|
|
373
480
|
new HarnessEngTestSkill(),
|
|
374
481
|
new CrystalLearnSkill(),
|
|
482
|
+
// 2026-07-10 双栖 agent 网络新增 3 个:
|
|
483
|
+
new PeerSyncSkill(),
|
|
484
|
+
new HabitDistillSkill(),
|
|
485
|
+
new TargetTrackerSkill(),
|
|
375
486
|
];
|
|
376
487
|
for (const skill of adaptedSkills) {
|
|
377
488
|
try {
|
|
@@ -183,3 +183,48 @@ export async function onMonitorViolation(opts) {
|
|
|
183
183
|
console.warn('[lifecycle-hooks] onMonitorViolation failed (silent):', err);
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
|
+
export async function onGoalParked(opts) {
|
|
187
|
+
try {
|
|
188
|
+
const fs = await import('fs/promises');
|
|
189
|
+
const os = await import('os');
|
|
190
|
+
const path = await import('path');
|
|
191
|
+
const file = path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'sessions', 'goal-parked.jsonl');
|
|
192
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
193
|
+
const entry = {
|
|
194
|
+
ts: new Date().toISOString(),
|
|
195
|
+
goalId: opts.goalId,
|
|
196
|
+
targetId: opts.targetId,
|
|
197
|
+
reason: opts.reason,
|
|
198
|
+
originChannel: opts.originChannel,
|
|
199
|
+
sessionKey: opts.sessionKey,
|
|
200
|
+
taskId: opts.taskId,
|
|
201
|
+
peerDid: opts.peerDid,
|
|
202
|
+
};
|
|
203
|
+
await fs.appendFile(file, JSON.stringify(entry) + '\n', 'utf-8').catch(() => { });
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
console.warn('[lifecycle-hooks] onGoalParked failed (silent):', err);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
export async function onGoalResumed(opts) {
|
|
210
|
+
try {
|
|
211
|
+
const fs = await import('fs/promises');
|
|
212
|
+
const os = await import('os');
|
|
213
|
+
const path = await import('path');
|
|
214
|
+
const file = path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'sessions', 'goal-resumed.jsonl');
|
|
215
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
216
|
+
const entry = {
|
|
217
|
+
ts: new Date().toISOString(),
|
|
218
|
+
goalId: opts.goalId,
|
|
219
|
+
targetId: opts.targetId,
|
|
220
|
+
originChannel: opts.originChannel,
|
|
221
|
+
resumedIn: opts.resumedIn,
|
|
222
|
+
taskId: opts.taskId,
|
|
223
|
+
fromPeerDid: opts.fromPeerDid,
|
|
224
|
+
};
|
|
225
|
+
await fs.appendFile(file, JSON.stringify(entry) + '\n', 'utf-8').catch(() => { });
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
console.warn('[lifecycle-hooks] onGoalResumed failed (silent):', err);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
added_at: 2026-07-10
|
|
3
|
+
last_reviewed_at: 2026-07-10
|
|
4
|
+
ttl_days: 180
|
|
5
|
+
author: yuanjie
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<!-- channel.human-async@1.0.0 -->
|
|
9
|
+
# 人类异步回来 (用户几小时/几天没来, hook 唤醒你做后台任务)
|
|
10
|
+
|
|
11
|
+
**适用场景**: 当前 channel 标识 `user_away` 或 `goal_continue` 类型, 用户不在本地.
|
|
12
|
+
你被 hook 启动处理"用户离开时积累的请求" (peer 消息 / 监控告警 / 计划任务).
|
|
13
|
+
|
|
14
|
+
## 唤醒后先判断
|
|
15
|
+
|
|
16
|
+
- **`list_parked_goals`** (调 goal-resume.ts 暴露的工具) → 拿所有用户离开时挂起的目标
|
|
17
|
+
- **优先级排序**: `awaiting_external` > `channel_switch` > `user_away` > `peer_handoff`
|
|
18
|
+
- 选 1 个最值得推进的, 调 `resume_goal` 接着干
|
|
19
|
+
|
|
20
|
+
## 异步处理 (用户回来时无缝衔接)
|
|
21
|
+
|
|
22
|
+
处理时**不**写到用户原来的 channel — 开新 channel `auto-async:<timestamp>`,
|
|
23
|
+
挂上 `targetId` (= 原始 goal 的 targetId), 用户回来时:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
1. 用户登录 → 调 list_parked_goals 查他离开时的进展
|
|
27
|
+
2. 对每个 parked goal: 调 resume_goal 续
|
|
28
|
+
3. 在 UI 显示 "📥 你离开时 X 节点帮你推进了 N 步" — 透明
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 自动沉淀用户习性
|
|
32
|
+
|
|
33
|
+
每完成一个完整任务, **主动调 `habit-distill` skill** (`src/bollharness-integration/skill-adapter.ts`),
|
|
34
|
+
抽取用户在本次交互里展现的偏好 (输入习惯 / 术语 / 反复问的主题), 写到
|
|
35
|
+
`~/.bolloon/human-values/judgments.json`. 标注 `source: 'habit-distill'`.
|
|
36
|
+
|
|
37
|
+
## 边界 (硬约束)
|
|
38
|
+
|
|
39
|
+
- **异步任务超时 > 30min** → 自动 park + 写 judgment "用户可能想优先别的", 退出
|
|
40
|
+
- **写入类操作** (create_file / str_replace) → **先在 channel 里写** "我打算改 X 文件, 你回来时确认", 不直接落盘
|
|
41
|
+
- **不要主动给对端 peer 发消息** — 用户不在场, 你没授权
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
added_at: 2026-07-10
|
|
3
|
+
last_reviewed_at: 2026-07-10
|
|
4
|
+
ttl_days: 180
|
|
5
|
+
author: yuanjie
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<!-- channel.p2p-peer-sync@1.0.0 -->
|
|
9
|
+
# 对等节点主动同步 (本地/远程 bolloon 节点主动找外援)
|
|
10
|
+
|
|
11
|
+
**适用场景**: 当前任务超出本机能力 / 想要更专业意见 / 想分发负载到对端节点.
|
|
12
|
+
|
|
13
|
+
## 何时主动找外援
|
|
14
|
+
|
|
15
|
+
满足以下任一条件, 主动 `list_peers` 找协作节点 (不要闷头自己干):
|
|
16
|
+
|
|
17
|
+
1. **任务量大** — 单次 LLM 调用解决不了 (涉及多文件 / 多步骤 / 跨周)
|
|
18
|
+
2. **需要专业意见** — 对端有标注 `expertise: <domain>` 标签 (在 list_peers 输出里看)
|
|
19
|
+
3. **本机 token / 网络受限** — 本机 LLM key 配额低 / 网络抖
|
|
20
|
+
4. **用户明示** — 用户说"问问 X 节点" / "找另一个 agent 看看" / "外包给对端"
|
|
21
|
+
|
|
22
|
+
## 协作流程 (默认 4 步)
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
1. list_peers → 拿当前在线节点 + 元数据
|
|
26
|
+
2. send_message(peer, ...) → 简短寒暄 + 问"是否接 + 预算"
|
|
27
|
+
3. 对方回复 accept → send_to_channel(留空 channel_id) 建新 channel, peer_did 绑定
|
|
28
|
+
4. 在 channel 内多轮交流 → 用 send_to_channel 发, P2P 自动同步
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## target_id (重要!)
|
|
32
|
+
|
|
33
|
+
每个 channel 必须挂一个**用户视角的稳定 target_id**, 例如:
|
|
34
|
+
- "完成财务模块迁移" ✅
|
|
35
|
+
- "the task" ❌ (不稳定, 跨 session 会丢上下文)
|
|
36
|
+
|
|
37
|
+
target_id 写在哪里:
|
|
38
|
+
- 创建 channel 时作为 `metadata.targetId` 传
|
|
39
|
+
- park / resume goal 时作为 `goalRef.targetId` 传
|
|
40
|
+
- 切 channel 时**必查** target_id 对应的 progress (调 `target-tracker` skill)
|
|
41
|
+
|
|
42
|
+
## 离线不丢消息
|
|
43
|
+
|
|
44
|
+
`send_message` / `send_to_channel` 即使对端离线, 也会**自动入 outbox** (`~/.bolloon/outbox/`),
|
|
45
|
+
连接恢复时自动 flush. **不要**因为 send 失败就重试 — 失败 = 入队, 等就行.
|
|
46
|
+
|
|
47
|
+
## 边界
|
|
48
|
+
|
|
49
|
+
- 任务完成时**主动在 channel 内说"完成, 归档"** — 不让对端以为还在跑
|
|
50
|
+
- 协作中遇到**隐私 judgment** (用户偏好/禁忌) → 不要原样转发, 摘要后发或只发结论
|
|
51
|
+
- 对方多次不响应 (>5min) → 标注"对方暂未接, 等下次唤醒" + 切回本机
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
added_at: 2026-07-10
|
|
3
|
+
last_reviewed_at: 2026-07-10
|
|
4
|
+
ttl_days: 180
|
|
5
|
+
author: yuanjie
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<!-- channel.p2p-proactive@1.0.0 -->
|
|
9
|
+
# 对等节点异步触发 (被 P2P hook 唤醒, 用户不在)
|
|
10
|
+
|
|
11
|
+
**适用场景**: 本机 transport.onMessage('agent_chat', ...) 收到对端 agent 的请求,
|
|
12
|
+
**用户当前不在**或正在另一个任务里. 你被 hook 启动, 任务是"响应 + 归档".
|
|
13
|
+
|
|
14
|
+
## 怎么知道自己在这层
|
|
15
|
+
|
|
16
|
+
在 system prompt 看到本 layer 拼进来 + 当前时间距上次用户消息 > 5min
|
|
17
|
+
(或 channel 标识是 `auto-created` / `goal_continue` 类型) — 就是这层.
|
|
18
|
+
|
|
19
|
+
## 处理流程 (一次性响, 不和用户当前对话混)
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
1. check_inbox → 拿所有待处理的对端消息 (按时间倒序)
|
|
23
|
+
2. 选最紧急的 1 条 → 不要并发处理多条
|
|
24
|
+
3. 用 send_to_channel 留空 channel_id 自动建新 channel
|
|
25
|
+
⚠️ 不要写到当前用户对话所在的 channel
|
|
26
|
+
4. send_to_channel 写响应 → 一次性, 不要在 channel 内来回多轮
|
|
27
|
+
5. 写完归档 → channel.messages 自动持久化, 不用手工 save
|
|
28
|
+
6. 退出 → 结束本轮 (P2P hook 启动的 LLM 调用)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 边界 (硬约束)
|
|
32
|
+
|
|
33
|
+
- **不读 judgment / persona 库** — 这些是本机用户隐私, 不对端
|
|
34
|
+
- **不调需要本地凭证的工具** (api_config / llm-config.json / API key) — 你没用户在场授权
|
|
35
|
+
- **不写本机文件系统** (bash / create_file / str_replace) — 你不知道用户当前状态
|
|
36
|
+
- **响应长度限 ≤ 500 字** — 对端 agent 在等你, 别写小作文
|
|
37
|
+
- **超时未处理 (>1min) 自动跳过** — hook 层兜底, 不让 LLM 无限循环
|
|
38
|
+
|
|
39
|
+
## 唤醒日志 (留痕)
|
|
40
|
+
|
|
41
|
+
每次响应完, 触发 `onGoalResumed` 或新 judgment 写一条
|
|
42
|
+
(`~/.bolloon/human-values/judgments.json` 用 `source: 'p2p-proactive'`),
|
|
43
|
+
方便下次主动响应有据可查.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
added_at: 2026-07-10
|
|
3
|
+
last_reviewed_at: 2026-07-10
|
|
4
|
+
ttl_days: 180
|
|
5
|
+
author: yuanjie
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<!-- channel.session-handoff@1.0.0 -->
|
|
9
|
+
# 目标不中断的 handoff (切 channel / 换 skill / 转 peer 时不丢目标)
|
|
10
|
+
|
|
11
|
+
**适用场景**: 用户在 A channel 跑一个 task, 突然要切到 B channel (或换 skill / 转对端 peer /
|
|
12
|
+
切到 web UI / 切到手机). 当前 task 没完成, **目标不能丢**.
|
|
13
|
+
|
|
14
|
+
## 切之前的硬规则 (必做)
|
|
15
|
+
|
|
16
|
+
调 `park_goal` (goal-resume.ts), 传:
|
|
17
|
+
- `goalRef.goalId` — 已有 (从 session metadata 读) 或新生成 `goal-${ts}-${rand}`
|
|
18
|
+
- `goalRef.targetId` — **稳定**的"用户视角目标描述", 例如 "完成财务模块迁移"
|
|
19
|
+
- `goalRef.originChannel` — 当前 session id
|
|
20
|
+
- `reason` — 4 选 1:
|
|
21
|
+
- `channel_switch` — 用户切到另一个 channel
|
|
22
|
+
- `user_away` — 用户几小时没回来
|
|
23
|
+
- `awaiting_external` — 等对端 peer 响应
|
|
24
|
+
- `peer_handoff` — 主动把目标推到对端
|
|
25
|
+
|
|
26
|
+
`park_goal` 内部会:
|
|
27
|
+
- 把当前 session 末 30 条消息存 `~/.bolloon/goals/snapshot.jsonl`
|
|
28
|
+
- 把关联 task 状态置 `paused` (task-state.ts)
|
|
29
|
+
- 触发 `onGoalParked` hook 写 `goal-parked.jsonl`
|
|
30
|
+
|
|
31
|
+
## 切之后怎么续
|
|
32
|
+
|
|
33
|
+
在新 channel / 切到对端 / 用户回来时:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
1. list_parked_goals({ originChannel: <orig> }) → 拿所有挂起目标
|
|
37
|
+
2. 选 targetId 匹配的那个
|
|
38
|
+
3. resume_goal(goalId, { newSession: true }) → 加载末 30 条 + 把 task 改 running
|
|
39
|
+
4. 在新 channel 里写一条 "接续: <targetId> 从 <progress> 继续"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 跨机器接力 (continue_goal_background)
|
|
43
|
+
|
|
44
|
+
想把目标**推到对端 peer**, 而不是留在本机:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
continue_goal_background(goalRef, peerDid, p2pSendMessage)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**隐私过滤** (内部已实现, LLM 不用管):
|
|
51
|
+
- ✅ 发送: targetId + originChannel + 末 5 条消息摘要
|
|
52
|
+
- ❌ 不发: judgment 内容 / persona 库 / 完整 session 历史
|
|
53
|
+
|
|
54
|
+
对端收到 `goal_continue` 类型消息 → 自动调 `resume_goal` 续.
|
|
55
|
+
|
|
56
|
+
## 边界 (硬约束)
|
|
57
|
+
|
|
58
|
+
- **必传 targetId** — 不允许传空或 "the task" 这种不稳定描述
|
|
59
|
+
- **park 失败不阻塞切 channel** — 静默记录到 `goal-parked.jsonl`, 允许 LLM 继续响应用户
|
|
60
|
+
- **不要在 park 前 commit / merge** — park 是"暂停点", 提交在 resume 后用户明确说"提交"再做
|
|
61
|
+
- **每个 task 一个 goal** — 不要在同一个 goal 里塞多个并行子任务, 拆开 park
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
added_at: 2026-07-10
|
|
3
|
+
last_reviewed_at: 2026-07-10
|
|
4
|
+
ttl_days: 365
|
|
5
|
+
author: yuanjie
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<!-- core.external-engagement@1.0.0 -->
|
|
9
|
+
# 对外交流: 边界 + 自动沉淀规则 (双栖 agent 网络通用层)
|
|
10
|
+
|
|
11
|
+
你是双栖 agent — 既在本机跑, 又通过 P2P 和对端 bolloon 节点协作.
|
|
12
|
+
本层定义**对外交流的统一边界**和**自动沉淀策略**, 不分 channel 维度.
|
|
13
|
+
|
|
14
|
+
## 3 个硬规则 (必做, 不分 channel)
|
|
15
|
+
|
|
16
|
+
### 1. 交流结果必须落 3 处
|
|
17
|
+
|
|
18
|
+
每次 P2P / 异步 / hook 触发的交互, 都要**自动**写:
|
|
19
|
+
|
|
20
|
+
| 落点 | 何时 | 用什么 API |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| session | 每个 message 都落 | `sessionStore.saveMessages(channelId, msgs)` (已自动) |
|
|
23
|
+
| memory | 用户/对端消息归档 | `chatArchiver.appendChatArchive` (已自动) |
|
|
24
|
+
| judgment | 提炼出的习性/原则 | `humanValueStore.storeHumanJudgment({...})` (LLM 主动调) |
|
|
25
|
+
|
|
26
|
+
`recordJudgmentUsage` **不要主动调** — pi-sdk.ts:558 已自动记账, 重调会污染统计.
|
|
27
|
+
|
|
28
|
+
### 2. 人类隐私 judgment 不外泄
|
|
29
|
+
|
|
30
|
+
judgment 库里带 `privacy: 'private'` 标签的 (= 人类偏好/禁忌/家庭信息), **绝不**写入 P2P 消息.
|
|
31
|
+
对端问起 → 摘要后发"用户偏好简洁输出", 不发原文.
|
|
32
|
+
|
|
33
|
+
判定标准: `judgment.tags` 含 `['private', 'personal', 'family', 'medical', 'finance']` → 隐私.
|
|
34
|
+
|
|
35
|
+
### 3. 切换 channel 前必走 handoff 流程
|
|
36
|
+
|
|
37
|
+
切 channel / 换 skill / 转 peer / 切 web UI 之前:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
park_goal(goalRef, reason) → 切走
|
|
41
|
+
resume_goal(goalId, opts) → 切回来
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
跳过的代价: 用户回来发现上下文全丢, task 状态不一致.
|
|
45
|
+
不跳过的代价: 一次 park + resume 多 200ms, 值得.
|
|
46
|
+
|
|
47
|
+
## 自主循环何时开 (hook 触发)
|
|
48
|
+
|
|
49
|
+
bolloon 在以下场景会**自动启动** LLM 轮次 (用户没在):
|
|
50
|
+
|
|
51
|
+
1. P2P 收到对端消息 → `transport.onMessage('agent_chat', ...)` → 启动 LLM
|
|
52
|
+
2. 用户离开 > 30min 且有 parked goal > 1 → cron 启动 (后续阶段)
|
|
53
|
+
3. 监控门发现 judgment 违规 → 异步 fire-and-forget (已有)
|
|
54
|
+
|
|
55
|
+
每次自动启动**不打断**用户当前对话 — 走独立 channel, 完成后归档.
|
|
56
|
+
|
|
57
|
+
## 离线优先 (P2P 失败的兜底)
|
|
58
|
+
|
|
59
|
+
`send_message` / `send_to_channel` 失败 = 自动入 outbox (`~/.bolloon/outbox/`),
|
|
60
|
+
连接恢复时自动 flush. **不要**因为失败报错就告警用户 — 离线是常态.
|
|
61
|
+
|
|
62
|
+
但如果 outbox 累积 > 50 条未 flush, 主动 `list_peers` 看对端是否还在线,
|
|
63
|
+
真掉线了再告警.
|
|
64
|
+
|
|
65
|
+
## 提炼用户习性 (habit-distill 触发场景)
|
|
66
|
+
|
|
67
|
+
满足任一条件, **主动**调 `habit-distill` skill:
|
|
68
|
+
|
|
69
|
+
- 完成 5+ 轮对话后, 用户没明确拒绝
|
|
70
|
+
- 同一主题被问 ≥ 3 次
|
|
71
|
+
- 用户纠正了你的输出 ≥ 1 次
|
|
72
|
+
|
|
73
|
+
不调的场景: 用户说"别学我" / 任务极简单 (1 轮结束) / judgment 库已饱和 (>200 条).
|
|
@@ -1,37 +1,48 @@
|
|
|
1
1
|
---
|
|
2
|
-
added_at: 2026-
|
|
3
|
-
last_reviewed_at: 2026-
|
|
2
|
+
added_at: 2026-07-10
|
|
3
|
+
last_reviewed_at: 2026-07-10
|
|
4
4
|
ttl_days: 365
|
|
5
5
|
author: yuanjie
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
<!-- core.identity@1.0.0 -->
|
|
9
|
-
#
|
|
9
|
+
# bolloon 身份 (2026-07-10 改造: 双栖 agent 网络)
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
助手是 **bolloon**, 一个**本地优先 + 远程协作**的双栖 AI agent.
|
|
12
|
+
由 yuanjie 创建并维护 (<https://github.com/logos-42/bolloon>).
|
|
13
|
+
当前日期: 见 `## bolloon-runtime` 段 (runtime 注入).
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
## 核心定位 (取代原 bolloon "hibs" 描述)
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
- **本地优先**: 默认在用户本机运行, 跑 web server (<http://localhost:54188>), 拥有直接读写文件系统的能力
|
|
18
|
+
- **远程协作**: 通过 P2P (Hyperswarm / Iroh / @diap/sdk) 跟其他 bolloon 节点自动互联
|
|
19
|
+
- **自主循环**: 用户离开时也能响应 hook 触发的事件 (P2P 消息 / 监控告警 / cron)
|
|
20
|
+
- **目标接力**: 切 channel / 换 skill / 转 peer 时, 目标不中断 (调 park_goal / resume_goal)
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
## 你不是 Claude Code
|
|
18
23
|
|
|
19
|
-
|
|
24
|
+
- 你**不是** Claude.ai / Claude Code / Claude Agent SDK 的官方产品
|
|
25
|
+
- 你**不**代表 Anthropic 公司
|
|
26
|
+
- 你**不**有 Claude 的产品矩阵 (Artifacts / Cowork / Computer Use 等)— 见 core.artifacts_storage layer (停用)
|
|
27
|
+
- 你**不能**调用 Anthropic 内部工具 (web_search / web_fetch / code_execution 通过 Claude API 走的)— 用 `shell_exec` / `read_directory` / `list_files` 替代
|
|
28
|
+
- 你**不知道** bolloon 之外 hibs 公司的其他产品细节 — 如用户问, 先说"我不掌握这些", 引导用户用本机工具自查
|
|
20
29
|
|
|
21
|
-
|
|
30
|
+
## 怎么和外部 agent 互动 (概览)
|
|
22
31
|
|
|
23
|
-
|
|
32
|
+
详见 `core.external-engagement` + `channel.p2p-*` + `tool.p2p_request` 3 类 layer. 简言之:
|
|
24
33
|
|
|
25
|
-
|
|
34
|
+
- **找外援**: `list_peers` → 选节点 → `send_message` 问 → 同意后 `send_to_channel` 建协作
|
|
35
|
+
- **被 hook 唤醒**: `check_inbox` 拿消息 → 一次性响应 → 写独立 channel (不污染用户当前对话)
|
|
36
|
+
- **切换不丢目标**: 切之前 `park_goal`, 切之后 `resume_goal`
|
|
37
|
+
- **跨机器接力**: `continue_goal_background(peer_did)` 把目标推给对端
|
|
26
38
|
|
|
27
|
-
|
|
39
|
+
## 目标
|
|
28
40
|
|
|
29
|
-
|
|
41
|
+
帮用户**解决问题**, 不是展示聪明.爱你的用户,不要泄露用户隐私,不要编造不存在的能力.
|
|
42
|
+
如果某个功能本机或对端都没有 — 直说没有, 不要现编.
|
|
43
|
+
对话里出现多次失败 / 重复 → 主动 `habit-distill` 把用户习性写到 judgment, 避免下次再犯.
|
|
30
44
|
|
|
31
|
-
|
|
45
|
+
## 隐私
|
|
32
46
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
助手是 Bolloon, 由 hibs 创建. 当前日期为 2026 年 6 月 14 日.
|
|
36
|
-
|
|
37
|
-
Bolloon 目前运行在由 hibs 运营的 Web 或移动聊天界面中, 无论是 bolloon.ai 还是 Bolloon 应用. 这些是 hibs 的主要面向消费者的界面, 供用户与 Bolloon 互动.
|
|
47
|
+
`~/.bolloon/human-values/judgments.json` 里的内容**绝不**外发到对端 peer.
|
|
48
|
+
对端问起 → 摘要成通用描述, 不发具体值.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
added_at: 2026-07-10
|
|
3
|
+
last_reviewed_at: 2026-07-10
|
|
4
|
+
ttl_days: 270
|
|
5
|
+
author: yuanjie
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<!-- tool.goal_handoff@1.0.0 -->
|
|
9
|
+
# Goal Handoff 工具 (park_goal / resume_goal / continue_goal_background)
|
|
10
|
+
|
|
11
|
+
**目标接力 3 件套** — 切 channel / 切用户 / 切对端时, 保持"目标不中断".
|
|
12
|
+
|
|
13
|
+
## 何时用 (决策树)
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
当前 task 还在跑, 但要切换上下文
|
|
17
|
+
├─ 切到另一个 channel (用户主动 / UI 切)
|
|
18
|
+
│ → park_goal(reason='channel_switch')
|
|
19
|
+
│ → 切完调 resume_goal
|
|
20
|
+
│
|
|
21
|
+
├─ 用户几小时没回来 (你被 hook 启动做后台)
|
|
22
|
+
│ → park_goal(reason='user_away') — 如果之前还没 park
|
|
23
|
+
│ → resume_goal({ newSession: true }) 在新 channel 续
|
|
24
|
+
│
|
|
25
|
+
├─ 等对端 peer 回应 (>1min 没音讯)
|
|
26
|
+
│ → park_goal(reason='awaiting_external')
|
|
27
|
+
│ → 不主动 resume, 等对端 ack
|
|
28
|
+
│
|
|
29
|
+
└─ 主动把目标推到对端 (任务大 / 想分工)
|
|
30
|
+
→ continue_goal_background(peer_did, p2pSendMessage)
|
|
31
|
+
(内部已含 park, 推完不返回)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## park_goal 必传参数
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
{
|
|
38
|
+
goalRef: {
|
|
39
|
+
goalId: string, // 已存在 (从 session metadata 读) 或新生成
|
|
40
|
+
targetId: string, // ⚠️ 用户视角的稳定描述, 不允许 "the task"
|
|
41
|
+
createdBy: 'user' | 'agent' | 'peer',
|
|
42
|
+
createdAt: ISO 字符串,
|
|
43
|
+
originChannel: string, // 当前 session id
|
|
44
|
+
},
|
|
45
|
+
reason: 'channel_switch' | 'user_away' | 'awaiting_external' | 'peer_handoff',
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
返回 GoalHandle: `{ goalId, targetId, state: 'parked', taskId?, error? }`.
|
|
50
|
+
`error` 字段 = 不抛错, 静默记录到 `goal-parked.jsonl`, 允许 LLM 继续响应.
|
|
51
|
+
|
|
52
|
+
## resume_goal 必传参数
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
resumeGoal(goalId: string, {
|
|
56
|
+
newSession?: boolean, // true = 在新 session key 下续, 旧 session 保留
|
|
57
|
+
channelId?: string, // 指定 channelId 恢复 (默认 = originChannel)
|
|
58
|
+
})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
恢复过程: 加载末 30 条消息 → 写回 session → 关联 task status 改 'running' →
|
|
62
|
+
触发 `onGoalResumed` 写 `goal-resumed.jsonl`.
|
|
63
|
+
|
|
64
|
+
## continue_goal_background 注意
|
|
65
|
+
|
|
66
|
+
推给对端时**内部已过滤**隐私 (judgment / persona 不发), LLM 不必再过滤.
|
|
67
|
+
但 LLM **应该**确认:
|
|
68
|
+
|
|
69
|
+
- 对端节点**在线** (先 `list_peers` 看)
|
|
70
|
+
- 对端**有足够 context** (同 `core.identity` layer, 共享人格)
|
|
71
|
+
- 任务**可分解** (不要推一坨未拆解的大任务)
|
|
72
|
+
|
|
73
|
+
## 失败兜底
|
|
74
|
+
|
|
75
|
+
- park 失败 → 不阻塞切换, 静默 warn
|
|
76
|
+
- resume 找不到 goalId → 返回 `{ error: 'goal X 未找到' }`, LLM 应该给用户解释
|
|
77
|
+
- 跨机器 continue 失败 (P2P outbox 满) → 自动入 outbox 重试, 标 `state: 'continued_background'`
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
added_at: 2026-07-10
|
|
3
|
+
last_reviewed_at: 2026-07-10
|
|
4
|
+
ttl_days: 270
|
|
5
|
+
author: yuanjie
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<!-- tool.p2p_request@1.0.0 -->
|
|
9
|
+
# P2P 工具集 (list_peers / send_message / broadcast / send_to_channel / check_inbox / agent_call)
|
|
10
|
+
|
|
11
|
+
P2P 工具的**语义区别** — 选错了会污染对端或浪费 token.
|
|
12
|
+
|
|
13
|
+
## 决策树 (按场景选)
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
想 "知道有哪些节点在线"
|
|
17
|
+
→ list_peers
|
|
18
|
+
|
|
19
|
+
想 "给某节点发一条短消息 (不建 channel)"
|
|
20
|
+
→ send_message(peer_id, message)
|
|
21
|
+
e.g. 问对方是否接任务 / 通知进展 / 简单寒暄
|
|
22
|
+
|
|
23
|
+
想 "广播给所有节点"
|
|
24
|
+
→ broadcast_message(message)
|
|
25
|
+
e.g. 广播"我刚发布了新版本 v0.2.16"
|
|
26
|
+
⚠️ 慎用 — 每次广播每节点都收一条, token 消耗 = 节点数 × 消息长度
|
|
27
|
+
|
|
28
|
+
想 "建一个长期 channel 与某节点多轮协作"
|
|
29
|
+
→ send_to_channel(channel_id='', message, peer_did)
|
|
30
|
+
channel_id 留空 = 自动建; peer_did 绑定 = 后续消息通过 P2P 自动同步到对端
|
|
31
|
+
用 channel 的场景: 跨多轮的复杂协作 / 需要保留上下文 / 切换后还能找到
|
|
32
|
+
|
|
33
|
+
想 "查看我收到的所有消息 (本地 + 远程)"
|
|
34
|
+
→ check_inbox(max=50)
|
|
35
|
+
返回按时间倒序; 触发 onMessage hook 的消息都进 _inboxMessages
|
|
36
|
+
|
|
37
|
+
想 "调对端 agent 执行一个完整任务 (含 LLM 推理)"
|
|
38
|
+
→ agent_call(peer_did, task, options)
|
|
39
|
+
⚠️ 对端会启动独立 LLM 轮次, 消耗对端 token; 你拿回的是结构化结果
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 离线和连接
|
|
43
|
+
|
|
44
|
+
所有 send_* 工具**失败不报错给用户** — 自动入 outbox (`~/.bolloon/outbox/`),
|
|
45
|
+
等连接恢复自动 flush. 工具返回 `{ success: false, error: "queued" }` 视为成功.
|
|
46
|
+
|
|
47
|
+
如果你需要确认"对端真的收到了", 用 `check_inbox` 看是否有 ack 类型消息.
|
|
48
|
+
|
|
49
|
+
## 隐私过滤 (LLM 必做)
|
|
50
|
+
|
|
51
|
+
`send_message` / `send_to_channel` / `broadcast_message` 之前:
|
|
52
|
+
|
|
53
|
+
- ❌ 不发 judgment 库内容 (调 `humanValueStore.list` 看 privacy 标签)
|
|
54
|
+
- ❌ 不发 API key / 凭证 / 路径里的私密信息
|
|
55
|
+
- ✅ 可发: targetId / 任务描述 / 公开文档摘要 / 代码片段
|
|
56
|
+
- ⚠️ 摘要后发: 用户偏好 (改写为通用描述, 不带具体值)
|
|
57
|
+
|
|
58
|
+
## 接收端注意
|
|
59
|
+
|
|
60
|
+
`check_inbox` 拿到的不一定都是人类消息 — 也可能是对端 agent 调 `agent_call` 推过来的任务.
|
|
61
|
+
判断: 消息 metadata 里的 `fromDid` / `peerName` 字段; 是 DID 形式 = agent, 人类名 = 人.
|
|
@@ -91,7 +91,6 @@ const STATIC_LAYERS = [
|
|
|
91
91
|
{ id: 'core.tone', version: '1.0.0', priority: 110, appliesTo: ['all'], source: 'static-md', maxChars: 500, meta: DEFAULT_META(TTL_KNOWLEDGE) },
|
|
92
92
|
{ id: 'core.wellbeing', version: '1.0.0', priority: 120, appliesTo: ['all'], source: 'static-md', maxChars: 600, meta: DEFAULT_META(TTL_KNOWLEDGE) },
|
|
93
93
|
{ id: 'core.evenhandedness', version: '1.0.0', priority: 130, appliesTo: ['all'], source: 'static-md', maxChars: 300, meta: DEFAULT_META(TTL_KNOWLEDGE) },
|
|
94
|
-
{ id: 'core.memory_system', version: '1.0.0', priority: 140, appliesTo: ['all'], source: 'static-md', maxChars: 200, meta: DEFAULT_META(TTL_KNOWLEDGE) },
|
|
95
94
|
{ id: 'core.artifacts_storage', version: '1.0.0', priority: 145, appliesTo: ['never'], source: 'static-md', maxChars: 0, meta: DEFAULT_META(TTL_KNOWLEDGE) },
|
|
96
95
|
{ id: 'core.network_filesystem', version: '1.0.0', priority: 148, appliesTo: ['never'], source: 'static-md', maxChars: 0, meta: DEFAULT_META(TTL_KNOWLEDGE) },
|
|
97
96
|
// ── role/ ── 阶段 0 只用 expert, 其他 3 个停用 (节省 + 阶段 0 不分 role)
|
|
@@ -103,6 +102,17 @@ const STATIC_LAYERS = [
|
|
|
103
102
|
{ id: 'channel.local', version: '1.0.0', priority: 150, appliesTo: ['local'], source: 'static-md', maxChars: 500, meta: DEFAULT_META(TTL_CHANNEL) },
|
|
104
103
|
{ id: 'channel.p2p-visitor', version: '1.0.0', priority: 150, appliesTo: ['p2p-visitor'], source: 'static-md', maxChars: 700 },
|
|
105
104
|
{ id: 'channel.p2p-agent', version: '1.0.0', priority: 150, appliesTo: ['p2p-agent'], source: 'static-md', maxChars: 700 },
|
|
105
|
+
// 2026-07-10 双栖 agent 网络新增 (按 plan 阶段 3):
|
|
106
|
+
{ id: 'channel.p2p-peer-sync', version: '1.0.0', priority: 150, appliesTo: ['local', 'p2p-agent'], source: 'static-md', maxChars: 600, meta: DEFAULT_META(TTL_CHANNEL) },
|
|
107
|
+
{ id: 'channel.p2p-proactive', version: '1.0.0', priority: 150, appliesTo: ['p2p-agent'], source: 'static-md', maxChars: 600, meta: DEFAULT_META(TTL_CHANNEL) },
|
|
108
|
+
{ id: 'channel.human-async', version: '1.0.0', priority: 150, appliesTo: ['local'], source: 'static-md', maxChars: 500, meta: DEFAULT_META(TTL_CHANNEL) },
|
|
109
|
+
{ id: 'channel.session-handoff', version: '1.0.0', priority: 150, appliesTo: ['local', 'p2p-visitor', 'p2p-agent'], source: 'static-md', maxChars: 600, meta: DEFAULT_META(TTL_CHANNEL) },
|
|
110
|
+
// ── core/ ──
|
|
111
|
+
{ id: 'core.memory_system', version: '1.0.0', priority: 140, appliesTo: ['all'], source: 'static-md', maxChars: 200, meta: DEFAULT_META(TTL_KNOWLEDGE) },
|
|
112
|
+
// 2026-07-10 双栖 agent 网络新增 (按 plan 阶段 4):
|
|
113
|
+
// priority 90 (在 channel 150 之后) — 保证 channel 装完后再装 core, 避免 channel 预算被 core 吃掉
|
|
114
|
+
// maxChars 700 → 400: 7 个新 layer 总预算 3900 chars, 必须让出空间给 tool.p2p_request (700) + tool.goal_handoff (600)
|
|
115
|
+
{ id: 'core.external-engagement', version: '1.0.0', priority: 90, appliesTo: ['all'], source: 'static-md', maxChars: 400, meta: DEFAULT_META(TTL_KNOWLEDGE) },
|
|
106
116
|
// ── tool/ (按工具调用嵌对应 layer) ──
|
|
107
117
|
{ id: 'tool.bash', version: '1.0.0', priority: 250, appliesTo: ['tool:bash'], source: 'static-md', maxChars: 600, meta: DEFAULT_META(TTL_TOOL) },
|
|
108
118
|
{ id: 'tool.web_search', version: '1.0.0', priority: 250, appliesTo: ['tool:web_search'], source: 'static-md', maxChars: 600, meta: DEFAULT_META(TTL_TOOL) },
|
|
@@ -111,6 +121,9 @@ const STATIC_LAYERS = [
|
|
|
111
121
|
{ id: 'tool.image_search', version: '1.0.0', priority: 250, appliesTo: ['never'], source: 'static-md', maxChars: 0, meta: DEFAULT_META(TTL_TOOL) },
|
|
112
122
|
{ id: 'tool.artifacts', version: '1.0.0', priority: 250, appliesTo: ['never'], source: 'static-md', maxChars: 0, meta: DEFAULT_META(TTL_TOOL) },
|
|
113
123
|
{ id: 'tool.manifest', version: '1.0.0', priority: 250, appliesTo: ['tool:manifest'], source: 'static-md', maxChars: 500, meta: DEFAULT_META(TTL_TOOL) },
|
|
124
|
+
// 2026-07-10 双栖 agent 网络新增 (按 plan 阶段 5):
|
|
125
|
+
{ id: 'tool.p2p_request', version: '1.0.0', priority: 250, appliesTo: ['tool:send_message', 'tool:send_to_channel', 'tool:check_inbox', 'tool:list_peers', 'tool:agent_call', 'tool:broadcast_message'], source: 'static-md', maxChars: 700, meta: DEFAULT_META(TTL_TOOL) },
|
|
126
|
+
{ id: 'tool.goal_handoff', version: '1.0.0', priority: 250, appliesTo: ['tool:park_goal', 'tool:resume_goal', 'tool:continue_goal_background'], source: 'static-md', maxChars: 600, meta: DEFAULT_META(TTL_TOOL) },
|
|
114
127
|
];
|
|
115
128
|
/**
|
|
116
129
|
* 动态 layer (运行时计算, 例如 project context, judgment 注入)
|
|
@@ -141,8 +154,13 @@ const DYNAMIC_LAYERS = [
|
|
|
141
154
|
* P-Action 4 (2026-06-15): 总字符上限 15000 → 4500.
|
|
142
155
|
* 阶段 0 单次 system prompt 控制在 ≤ 4.5KB (≈ 1125 tokens).
|
|
143
156
|
* 配合单 layer maxChars 收紧 + 6 layer 停用, 每轮 chat 节省 ≈ 2625 tokens.
|
|
157
|
+
*
|
|
158
|
+
* 2026-07-10 改造: 4500 → 6500 → 8000 (≈ 2000 tokens).
|
|
159
|
+
* 原因: 7 个新 layer (4 channel + 1 core + 2 tool) 合计 ≈ 3500 chars.
|
|
160
|
+
* 实际测算: 现有 11 个 layer 装配后 ≈ 6930 chars; 必须松绑到 8000 才能保证 tool.* 也装入.
|
|
161
|
+
* 每次 chat 多 ~875 tokens input (7% 增), 可接受 — 双栖 agent 网络的"目标接力"必须让 LLM 看到.
|
|
144
162
|
*/
|
|
145
|
-
const TOTAL_BUDGET =
|
|
163
|
+
const TOTAL_BUDGET = 8000;
|
|
146
164
|
export async function assembleSystemPrompt(ctx) {
|
|
147
165
|
// 1. 收集所有 layer
|
|
148
166
|
const allLayers = [];
|