@bolloon/bolloon-agent 0.2.15 → 0.3.1

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.
@@ -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 = 4500;
163
+ const TOTAL_BUDGET = 8000;
146
164
  export async function assembleSystemPrompt(ctx) {
147
165
  // 1. 收集所有 layer
148
166
  const allLayers = [];
@@ -125,6 +125,13 @@
125
125
  node.appendChild(marker);
126
126
  node.appendChild(label2);
127
127
  node.appendChild(args);
128
+ const errContainer = document.createElement("div");
129
+ errContainer.className = "step-timeline-error-wrap";
130
+ errContainer.style.display = "none";
131
+ const errEl = document.createElement("span");
132
+ errEl.className = "step-timeline-error";
133
+ errContainer.appendChild(errEl);
134
+ node.appendChild(errContainer);
128
135
  if (existing[htmlIdx] && existing[htmlIdx] !== node) {
129
136
  listEl.replaceChild(node, existing[htmlIdx]);
130
137
  } else {
@@ -142,6 +149,16 @@
142
149
  argsEl.textContent = argStr;
143
150
  argsEl.style.display = argStr ? "" : "none";
144
151
  }
152
+ const errWrap = node.querySelector(".step-timeline-error-wrap");
153
+ const errEl2 = node.querySelector(".step-timeline-error");
154
+ if (step.status === "error" && step.error && errWrap && errEl2) {
155
+ errEl2.textContent = String(step.error);
156
+ errEl2.setAttribute("title", String(step.error));
157
+ errWrap.style.display = "";
158
+ } else if (errWrap && errEl2) {
159
+ errWrap.style.display = "none";
160
+ errEl2.textContent = "";
161
+ }
145
162
  htmlIdx++;
146
163
  }
147
164
  while (listEl.children.length > htmlIdx) {
@@ -356,6 +373,31 @@
356
373
  function parseToolCall(content, ctx) {
357
374
  if (!content) return null;
358
375
  const strippedContent = content.replace(/<think[\s\S]*?<\/think/g, "");
376
+ try {
377
+ const result = /* @__PURE__ */ function _diagProbe() {
378
+ return null;
379
+ }();
380
+ const probe = function _doParse() {
381
+ const m1 = strippedContent.match(/<invoke\s+name=["']([\w]+)["']/);
382
+ if (m1) {
383
+ return { name: m1[1], args: { __probe_invoketag: "1" } };
384
+ }
385
+ const m2 = strippedContent.match(/<function_calls>[\s\S]*?<invoke\s+name=["']([\w]+)["']/);
386
+ if (m2) {
387
+ return { name: m2[1], args: { __probe_function_calls: "1" } };
388
+ }
389
+ const m3 = strippedContent.match(/\{[\s\S]*?"name"\s*:\s*["']([\w]+)["']/);
390
+ if (m3) {
391
+ return { name: m3[1], args: { __probe_json_name: "1" } };
392
+ }
393
+ return null;
394
+ }();
395
+ console.warn(
396
+ "[parseToolCall diag] rawLen=" + content.length + " strippedLen=" + strippedContent.length + " probeName=" + (probe?.name ?? "null") + " rawHead=" + JSON.stringify(content.slice(0, 500))
397
+ );
398
+ } catch (diagErr) {
399
+ console.warn("[parseToolCall diag] diag-self-failed:", String(diagErr));
400
+ }
359
401
  const jsonPatterns = [
360
402
  // markdown json code block + OpenAI 块, 同时匹配 arguments/input 字段
361
403
  /(?:```(?:json|json5)?\s*\n?)?\{[\s\S]*?"name"\s*:\s*["'](\w+)["']\s*,\s*["']?(?:arguments|input)["']?\s*:\s*(\{[\s\S]*?\})\s*\}/
@@ -4740,3 +4740,30 @@ body:has(> .api-config-page) {
4740
4740
  }
4741
4741
  .step-timeline-more:hover { text-decoration: underline; }
4742
4742
 
4743
+
4744
+ /* 2026-07-12: 工具原始 error 展示 — 之前 step.error 数据存了但没渲染, 用户只看到
4745
+ LLM 改写的"X 必填"误导调试. 现在展示原始 error (mono 字体 + 红色 + 缩进).
4746
+ 默认 hidden, error 状态时 step-timeline.ts 显示出来. */
4747
+ .step-timeline-error-wrap {
4748
+ flex-basis: 100%;
4749
+ margin: 2px 0 0 0;
4750
+ padding-left: 16px;
4751
+ }
4752
+ .step-timeline-error {
4753
+ display: inline-block;
4754
+ font-family: 'JetBrains Mono', monospace;
4755
+ font-size: 10.5px;
4756
+ color: var(--warning);
4757
+ background: rgba(255, 165, 0, 0.08);
4758
+ border-left: 2px solid var(--warning);
4759
+ padding: 2px 6px;
4760
+ border-radius: 0 3px 3px 0;
4761
+ word-break: break-word;
4762
+ white-space: pre-wrap;
4763
+ max-width: 100%;
4764
+ }
4765
+ .step-timeline-node[data-status="error"] .step-timeline-error {
4766
+ /* error 状态下加粗, 用户视线引导 */
4767
+ font-weight: 500;
4768
+ }
4769
+
@@ -98,6 +98,13 @@ function render(timelineEl) {
98
98
  node.appendChild(marker);
99
99
  node.appendChild(label2);
100
100
  node.appendChild(args);
101
+ const errContainer = document.createElement("div");
102
+ errContainer.className = "step-timeline-error-wrap";
103
+ errContainer.style.display = "none";
104
+ const errEl = document.createElement("span");
105
+ errEl.className = "step-timeline-error";
106
+ errContainer.appendChild(errEl);
107
+ node.appendChild(errContainer);
101
108
  if (existing[htmlIdx] && existing[htmlIdx] !== node) {
102
109
  listEl.replaceChild(node, existing[htmlIdx]);
103
110
  } else {
@@ -115,6 +122,16 @@ function render(timelineEl) {
115
122
  argsEl.textContent = argStr;
116
123
  argsEl.style.display = argStr ? "" : "none";
117
124
  }
125
+ const errWrap = node.querySelector(".step-timeline-error-wrap");
126
+ const errEl2 = node.querySelector(".step-timeline-error");
127
+ if (step.status === "error" && step.error && errWrap && errEl2) {
128
+ errEl2.textContent = String(step.error);
129
+ errEl2.setAttribute("title", String(step.error));
130
+ errWrap.style.display = "";
131
+ } else if (errWrap && errEl2) {
132
+ errWrap.style.display = "none";
133
+ errEl2.textContent = "";
134
+ }
118
135
  htmlIdx++;
119
136
  }
120
137
  while (listEl.children.length > htmlIdx) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.2.15",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli-entry.js",
@@ -48,7 +48,7 @@
48
48
  "src/constraint-runtime"
49
49
  ],
50
50
  "dependencies": {
51
- "@bolloon/bolloon-agent": "^0.2.15",
51
+ "@bolloon/bolloon-agent": "^0.3.1",
52
52
  "@bolloon/constraint-runtime": "0.1.0",
53
53
  "@capacitor/core": "^8.4.1",
54
54
  "@capacitor/ios": "^8.4.1",