@bolloon/bolloon-agent 0.1.34 → 0.1.36

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.
Files changed (226) hide show
  1. package/.auto-evolve-calls +1 -0
  2. package/.last-auto-evolve-baseline +1 -0
  3. package/.playwright-mcp/page-2026-06-16T07-51-45-706Z.yml +130 -0
  4. package/.playwright-mcp/page-2026-06-16T07-56-44-116Z.yml +131 -0
  5. package/Bolloon.md +144 -0
  6. package/Dive-into/CITATION.cff +17 -0
  7. package/Dive-into/LICENSE +25 -0
  8. package/Dive-into/README.md +598 -0
  9. package/Dive-into/README_zh.md +599 -0
  10. package/Dive-into/assets/context.png +0 -0
  11. package/Dive-into/assets/extensibility.png +0 -0
  12. package/Dive-into/assets/iteration.png +0 -0
  13. package/Dive-into/assets/layered_architecture.png +0 -0
  14. package/Dive-into/assets/main_structure.png +0 -0
  15. package/Dive-into/assets/permission.png +0 -0
  16. package/Dive-into/assets/session_compact.png +0 -0
  17. package/Dive-into/assets/subagent.png +0 -0
  18. package/Dive-into/paper/Dive_into_Claude_Code.pdf +0 -0
  19. package/README.md +1 -1
  20. package/dist/agents/p2p-chat-tools.js +6 -6
  21. package/dist/agents/permission-mode.js +115 -0
  22. package/dist/agents/pi-sdk.js +687 -25
  23. package/dist/agents/pre-tool-validator.js +194 -0
  24. package/dist/agents/workflow-pivot-loop.js +113 -12
  25. package/dist/bollharness/src/scripts/checks/check_doc_freshness.js +1 -1
  26. package/dist/bollharness/src/scripts/checks/check_doc_links.js +1 -1
  27. package/dist/bollharness/src/scripts/context_router.js +1 -1
  28. package/dist/bollharness/src/scripts/deploy-guard.js +1 -1
  29. package/dist/bollharness/src/scripts/guard_router.js +1 -1
  30. package/dist/bollharness/src/scripts/hooks/_hook_output.js +1 -1
  31. package/dist/bollharness/src/scripts/hooks/risk-tracker.js +1 -1
  32. package/dist/bollharness-integration/context-router.js +2 -2
  33. package/dist/bollharness-integration/guard-checker.js +1 -1
  34. package/dist/bootstrap/bootstrap.js +117 -0
  35. package/dist/bootstrap/context-collector.js +312 -0
  36. package/dist/bootstrap/context-hierarchy.js +218 -0
  37. package/dist/bootstrap/lifecycle-hooks.js +171 -0
  38. package/dist/bootstrap/project-context.js +158 -0
  39. package/dist/context-compaction/auto-compact.js +144 -0
  40. package/dist/context-compaction/budget-gate.js +28 -0
  41. package/dist/context-compaction/budget-reduce.js +35 -0
  42. package/dist/context-compaction/context-collapse.js +66 -0
  43. package/dist/context-compaction/index.js +21 -0
  44. package/dist/context-compaction/microcompact.js +51 -0
  45. package/dist/context-compaction/pipeline.js +123 -0
  46. package/dist/context-compaction/snip.js +45 -0
  47. package/dist/context-compaction/token-estimator.js +35 -0
  48. package/dist/context-compaction/types.js +19 -0
  49. package/dist/heartbeat/HealthMonitor.js +3 -2
  50. package/dist/index.js +49 -2
  51. package/dist/llm/llm-judgment-client.js +32 -30
  52. package/dist/llm/pi-ai.js +125 -28
  53. package/dist/llm/system-prompt/health.js +129 -0
  54. package/dist/llm/system-prompt/registry.js +246 -0
  55. package/dist/llm/system-prompt/strip-hibsml.js +51 -0
  56. package/dist/llm/tool-manifest/ask_user_input.js +35 -0
  57. package/dist/llm/tool-manifest/bash.js +23 -0
  58. package/dist/llm/tool-manifest/create_file.js +24 -0
  59. package/dist/llm/tool-manifest/fetch_sports_data.js +26 -0
  60. package/dist/llm/tool-manifest/image_search.js +24 -0
  61. package/dist/llm/tool-manifest/index.js +69 -0
  62. package/dist/llm/tool-manifest/mcp.js +43 -0
  63. package/dist/llm/tool-manifest/message_compose.js +31 -0
  64. package/dist/llm/tool-manifest/places.js +83 -0
  65. package/dist/llm/tool-manifest/present_files.js +21 -0
  66. package/dist/llm/tool-manifest/recipe.js +40 -0
  67. package/dist/llm/tool-manifest/recommend_apps.js +23 -0
  68. package/dist/llm/tool-manifest/str_replace.js +27 -0
  69. package/dist/llm/tool-manifest/types.js +7 -0
  70. package/dist/llm/tool-manifest/view.js +24 -0
  71. package/dist/llm/tool-manifest/weather.js +27 -0
  72. package/dist/llm/tool-manifest/web.js +51 -0
  73. package/dist/network/p2p-direct.js +23 -0
  74. package/dist/network/source-intent-broadcaster.js +203 -0
  75. package/dist/network/source-intent.js +100 -0
  76. package/dist/pi-ecosystem-judgment/adaptive-scan.js +279 -0
  77. package/dist/pi-ecosystem-judgment/causal-judge.js +449 -0
  78. package/dist/pi-ecosystem-judgment/detect-hook.js +168 -0
  79. package/dist/pi-ecosystem-judgment/distill-prompt.js +226 -0
  80. package/dist/pi-ecosystem-judgment/evolve-judgment.js +170 -0
  81. package/dist/pi-ecosystem-judgment/human-value-pipeline.js +21 -0
  82. package/dist/pi-ecosystem-judgment/human-value-store.js +283 -22
  83. package/dist/pi-ecosystem-judgment/injection-gate.js +208 -0
  84. package/dist/pi-ecosystem-judgment/monitor-gate.js +188 -0
  85. package/dist/pi-ecosystem-judgment/value-injection.js +8 -2
  86. package/dist/security/builtin-guards.js +124 -0
  87. package/dist/security/context-router-tool.js +106 -0
  88. package/dist/security/input-scanner.js +223 -0
  89. package/dist/security/react-harness.js +143 -0
  90. package/dist/security/tool-gate.js +235 -0
  91. package/dist/utils/auto-evolve-policy.js +117 -0
  92. package/dist/utils/clamp.js +7 -0
  93. package/dist/utils/double.js +6 -0
  94. package/dist/web/client.js +3810 -3830
  95. package/dist/web/components/p2p/P2PModal.js +188 -0
  96. package/dist/web/components/p2p/index.js +264 -226
  97. package/dist/web/components/p2p/p2p-modal.js +657 -0
  98. package/dist/web/components/p2p/p2p-tools.js +248 -0
  99. package/dist/web/index.html +60 -49
  100. package/dist/web/server.js +827 -124
  101. package/dist/web/style.css +531 -249
  102. package/dist/web/ui/message-renderer.js +463 -0
  103. package/dist/web/ui/step-timeline.js +375 -0
  104. package/lefthook.yml +33 -0
  105. package/package.json +3 -2
  106. package/scripts/auto-evolve-loop.ts +481 -0
  107. package/scripts/auto-evolve-oneshot.sh +155 -0
  108. package/scripts/auto-evolve-snapshot.sh +136 -0
  109. package/scripts/build-web.ts +35 -1
  110. package/scripts/detect-schema-changes.sh +48 -0
  111. package/scripts/diff-reviewer.ts +159 -0
  112. package/scripts/validate-system-prompt.ts +142 -0
  113. package/scripts/weekly-report.ts +364 -0
  114. package/src/agents/p2p-chat-tools.ts +6 -6
  115. package/src/agents/permission-mode.ts +127 -0
  116. package/src/agents/pi-sdk.ts +741 -30
  117. package/src/agents/pre-tool-validator.ts +213 -0
  118. package/src/agents/workflow-pivot-loop.ts +110 -19
  119. package/src/bollharness/CLAUDE.md +1 -1
  120. package/src/bollharness/README.md +2 -2
  121. package/src/bollharness/README.zh-CN.md +2 -2
  122. package/src/bollharness/reference/boll-reference/scripts/hooks/find-boll-root.sh +2 -2
  123. package/src/bollharness/scripts/context-fragments/truth-source-hierarchy.md +2 -2
  124. package/src/bollharness/scripts/context-fragments/version-sources.md +1 -1
  125. package/src/bollharness/scripts/hooks/find-project-root.sh +4 -4
  126. package/src/bollharness/src/scripts/checks/check_doc_freshness.ts +1 -1
  127. package/src/bollharness/src/scripts/checks/check_doc_links.ts +1 -1
  128. package/src/bollharness/src/scripts/context_router.ts +1 -1
  129. package/src/bollharness/src/scripts/deploy-guard.ts +1 -1
  130. package/src/bollharness/src/scripts/guard_router.ts +1 -1
  131. package/src/bollharness/src/scripts/hooks/_hook_output.ts +1 -1
  132. package/src/bollharness/src/scripts/hooks/risk-tracker.ts +1 -1
  133. package/src/bollharness/templates/scaffold/.gitignore.append +1 -1
  134. package/src/bollharness/templates/scaffold/CLAUDE.md +2 -2
  135. package/src/bollharness-integration/context-router.ts +2 -2
  136. package/src/bollharness-integration/guard-checker.ts +1 -1
  137. package/src/bootstrap/bootstrap.ts +135 -0
  138. package/src/bootstrap/context-collector.ts +371 -0
  139. package/src/bootstrap/context-hierarchy.ts +283 -0
  140. package/src/bootstrap/lifecycle-hooks.ts +289 -0
  141. package/src/bootstrap/project-context.ts +171 -0
  142. package/src/context-compaction/auto-compact.ts +153 -0
  143. package/src/context-compaction/budget-gate.ts +32 -0
  144. package/src/context-compaction/budget-reduce.ts +37 -0
  145. package/src/context-compaction/context-collapse.ts +72 -0
  146. package/src/context-compaction/index.ts +24 -0
  147. package/src/context-compaction/microcompact.ts +54 -0
  148. package/src/context-compaction/pipeline.ts +133 -0
  149. package/src/context-compaction/snip.ts +51 -0
  150. package/src/context-compaction/token-estimator.ts +36 -0
  151. package/src/context-compaction/types.ts +99 -0
  152. package/src/heartbeat/HealthMonitor.ts +3 -2
  153. package/src/index.ts +47 -2
  154. package/src/llm/llm-judgment-client.ts +36 -35
  155. package/src/llm/pi-ai.ts +135 -29
  156. package/src/llm/system-prompt/health.ts +159 -0
  157. package/src/llm/system-prompt/layers/channel/local.md +14 -0
  158. package/src/llm/system-prompt/layers/channel/p2p-agent.md +18 -0
  159. package/src/llm/system-prompt/layers/channel/p2p-visitor.md +19 -0
  160. package/src/llm/system-prompt/layers/core/artifacts_storage.md +89 -0
  161. package/src/llm/system-prompt/layers/core/evenhandedness.md +21 -0
  162. package/src/llm/system-prompt/layers/core/hibs_reminders.md +15 -0
  163. package/src/llm/system-prompt/layers/core/identity.md +37 -0
  164. package/src/llm/system-prompt/layers/core/knowledge.md +17 -0
  165. package/src/llm/system-prompt/layers/core/memory_system.md +12 -0
  166. package/src/llm/system-prompt/layers/core/network_filesystem.md +28 -0
  167. package/src/llm/system-prompt/layers/core/refusal.md +37 -0
  168. package/src/llm/system-prompt/layers/core/tone.md +31 -0
  169. package/src/llm/system-prompt/layers/core/tools.thin.md +13 -0
  170. package/src/llm/system-prompt/layers/core/wellbeing.md +41 -0
  171. package/src/llm/system-prompt/layers/role/architect.md +20 -0
  172. package/src/llm/system-prompt/layers/role/expert.md +19 -0
  173. package/src/llm/system-prompt/layers/role/implementer.md +15 -0
  174. package/src/llm/system-prompt/layers/role/security.md +15 -0
  175. package/src/llm/system-prompt/layers/tool/artifacts.md +72 -0
  176. package/src/llm/system-prompt/layers/tool/bash.md +25 -0
  177. package/src/llm/system-prompt/layers/tool/hibs_api.md +171 -0
  178. package/src/llm/system-prompt/layers/tool/image_search.md +70 -0
  179. package/src/llm/system-prompt/layers/tool/manifest.md +89 -0
  180. package/src/llm/system-prompt/layers/tool/mcp_apps.md +53 -0
  181. package/src/llm/system-prompt/layers/tool/web_search.md +83 -0
  182. package/src/llm/system-prompt/registry.ts +325 -0
  183. package/src/llm/system-prompt/strip-hibsml.ts +52 -0
  184. package/src/llm/tool-manifest/ask_user_input.ts +37 -0
  185. package/src/llm/tool-manifest/bash.ts +25 -0
  186. package/src/llm/tool-manifest/create_file.ts +26 -0
  187. package/src/llm/tool-manifest/fetch_sports_data.ts +28 -0
  188. package/src/llm/tool-manifest/image_search.ts +26 -0
  189. package/src/llm/tool-manifest/index.ts +88 -0
  190. package/src/llm/tool-manifest/mcp.ts +46 -0
  191. package/src/llm/tool-manifest/message_compose.ts +33 -0
  192. package/src/llm/tool-manifest/places.ts +86 -0
  193. package/src/llm/tool-manifest/present_files.ts +23 -0
  194. package/src/llm/tool-manifest/recipe.ts +42 -0
  195. package/src/llm/tool-manifest/recommend_apps.ts +25 -0
  196. package/src/llm/tool-manifest/str_replace.ts +29 -0
  197. package/src/llm/tool-manifest/types.ts +52 -0
  198. package/src/llm/tool-manifest/view.ts +26 -0
  199. package/src/llm/tool-manifest/weather.ts +29 -0
  200. package/src/llm/tool-manifest/web.ts +54 -0
  201. package/src/network/p2p-direct.ts +22 -0
  202. package/src/network/source-intent-broadcaster.ts +242 -0
  203. package/src/network/source-intent.ts +167 -0
  204. package/src/security/builtin-guards.ts +162 -0
  205. package/src/security/context-router-tool.ts +122 -0
  206. package/src/security/input-scanner.ts +287 -0
  207. package/src/security/react-harness.ts +177 -0
  208. package/src/security/tool-gate.ts +294 -0
  209. package/src/utils/auto-evolve-policy.ts +138 -0
  210. package/src/utils/clamp.ts +5 -0
  211. package/src/web/client.js +105 -2187
  212. package/src/web/client.ts +4326 -0
  213. package/src/web/index.html +60 -49
  214. package/src/web/server.ts +894 -148
  215. package/src/web/style.css +531 -249
  216. package/src/web/ui/message-renderer.ts +540 -0
  217. package/src/web/ui/step-timeline.ts +394 -0
  218. package/staging/auto-evolve/clean-001/.review-verdict +9 -0
  219. package/staging/auto-evolve/clean-001/clean-001.patch +14 -0
  220. package/staging/auto-evolve/e2e-001/.patch-id +1 -0
  221. package/staging/auto-evolve/e2e-001/.review-verdict +12 -0
  222. package/staging/auto-evolve/e2e-001/e2e-001.patch +11 -0
  223. package/staging/auto-evolve/test-bad/.review-verdict +12 -0
  224. package/staging/auto-evolve/test-bad/test-bad.patch +11 -0
  225. package/test-results/.last-run.json +6 -0
  226. package/test-results/src-test-web-loop-status-b-5734c-atus-bar-/346/230/276/347/244/272-spinner-/344/270/215/346/230/276/347/244/272/345/276/252/347/216/257/350/256/241/346/225/260/error-context.md +285 -0
@@ -20,6 +20,17 @@ import { DiscoveredAgentsManager, createSocialHeartbeat } from '../social/heartb
20
20
  import { getGlobalSharedContext } from '../social/global-shared-context.js';
21
21
  import { Session, SkillRegistry, saveSession, loadSession } from '@bolloon/constraint-runtime';
22
22
  import { loadSkillsFromPaths, defaultSkillPaths, describeSkill } from './skill-loader.js';
23
+ // Judgment 注入门 (P0): 在主对话 LLM 调起前自动拼入 Top 3 判断力
24
+ // 失败静默, 不阻塞主对话
25
+ import { injectJudgmentGate, recordJudgmentUsage } from '../pi-ecosystem-judgment/injection-gate.js';
26
+ // 持续监控门 (P3): AI 回复后审计是否违反原则
27
+ import { monitorAfterReply } from '../pi-ecosystem-judgment/monitor-gate.js';
28
+ // Bootstrap 生命周期 hook (SessionStart / Stop / PreToolUse)
29
+ import { onSessionStart, onStop, onPreToolUse } from '../pi-ecosystem-judgment/human-value-pipeline.js';
30
+ import { onPostToolUse } from '../bootstrap/lifecycle-hooks.js';
31
+ import { budgetReduce, snip, microcompact } from '../context-compaction/index.js';
32
+ // React Harness: 8-gate + 4-guard (防越权 / 防 prompt 注入)
33
+ import { ReactHarness } from '../security/react-harness.js';
23
34
  const SHARED_SESSION_PATH = path.join(process.env.HOME || '/tmp', '.bolloon', 'sessions');
24
35
  const PERSONA_PATH = path.join(process.env.HOME || '/tmp', '.bolloon', 'persona.json');
25
36
  export class PiSessionManager {
@@ -334,15 +345,86 @@ class PiAgentSession {
334
345
  messageHistory = [];
335
346
  tools = new Map();
336
347
  skillRegistry = new SkillRegistry();
337
- MAX_REACT_ITERATIONS = 100;
348
+ // 2026-06-16 修: 父要求把 ReAct loop 上限放大到 "几乎无限", 靠自动压缩上下文 + fail-safe 兜底
349
+ // 默认 10000 — 正常任务永远跑不到, 但作为防 LLM 死循环 / 防 OOM 的最后一道闸
350
+ // 旧默认 100 写死导致中等复杂度任务 (10-50 个 tool call + 多步反思) 会被误杀
351
+ MAX_REACT_ITERATIONS = 10_000;
338
352
  MAX_REFINE_ATTEMPTS = 3;
339
353
  QUALITY_THRESHOLD = 0.6;
354
+ /** P1: 上下文溢出阈值 (单轮估算 token 数, 超过则强制终止防止 prompt-too-long) */
355
+ MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD = 60_000; // 60K tokens 上限
356
+ /** 2026-06-16 新增: 累计错误总数兜底 (不管是否同工具, 累计 N 次就强制退出)
357
+ * 防 LLM 轮换工具名绕开 MAX_SAME_TOOL_FAILURES 的死循环攻击 */
358
+ MAX_TOTAL_ERRORS = 20;
359
+ /** 2026-06-16 新增: loop 内自动压缩触发阈值 (相对 60K 阈值的比例) */
360
+ LOOP_COMPACT_RATIO = 0.8;
361
+ /** P1: max output token 升级重试 (LLM 截断时重试, 最多 3 次) */
362
+ MAX_OUTPUT_TOKEN_ESCALATION_RETRIES = 3;
340
363
  thinkingEngine = new DeepThinkingEngine(3);
341
364
  coordinator = new AgentCoordinator(3);
342
365
  harness = null;
343
366
  harnessEnabled = false;
367
+ /** 8-gate + 4-guard 集中调度 (防越权 / 防 prompt 注入) */
368
+ reactHarness = new ReactHarness();
344
369
  usePivotLoop = false;
345
370
  pivotLoopConfig;
371
+ /** P2: 当前会话的 permission mode (每次 promptStream 入口解析) */
372
+ currentPermissionMode = 'default';
373
+ /** P1.2: Context Collapse 读时投影结果 (feature flag 开启时由 maybeAutoCompact 写入, buildContext 优先用) */
374
+ projectedHistory = null;
375
+ /**
376
+ * Judgment 注入门临时结果: 在 prompt / promptStream / promptWithPivotLoop 入口算一次, 拼到本轮 systemPrompt 末尾
377
+ * 每次调用都会重置 (避免上一轮遗留)
378
+ */
379
+ judgmentGateAddition = '';
380
+ judgmentGateUsedIds = [];
381
+ /**
382
+ * 当前 onStream 引用 + abort signal (computeJudgmentGate 需要 onStream 广播 phase)
383
+ * 每次 prompt / promptStream / promptWithPivotLoop 入口设置, 用完即清
384
+ */
385
+ currentOnStream = null;
386
+ currentSignal = null;
387
+ /** Bootstrap SessionStart 拼的 system prompt 片段 (用完即清) */
388
+ bootstrapAddition = '';
389
+ /** 当前 prompt 开始时间 (供 Stop hook 计算 durationMs) */
390
+ promptStartTime = 0;
391
+ /** 当前 channel id (由 getAgentForChannel / prompt 4 参注入, 供 hook / log 使用) */
392
+ currentChannelId = '';
393
+ /**
394
+ * 算 judgment 注入门: 失败静默, 不阻塞主对话
395
+ * 期间通过 currentOnStream 广播 phase 事件, 前端可显示 "正在检索判断力..." 状态
396
+ * 调用方负责用完即清 (judgmentGateAddition='')
397
+ */
398
+ async computeJudgmentGate(input) {
399
+ const safePhase = (phase, extra = {}) => {
400
+ try {
401
+ if (this.currentOnStream) {
402
+ this.currentOnStream({ type: 'phase', phase, ...extra, content: '' });
403
+ }
404
+ }
405
+ catch { /* 静默 */ }
406
+ };
407
+ safePhase('gate_compute', { detail: '正在检索相关判断力...' });
408
+ try {
409
+ // P-Action 4 (2026-06-15) 路径 1 整合: 透传 maxChars=1500 (≈ 375 tokens 硬上限)
410
+ // 路径 2/3 检测由 injection-gate 内部 alreadyInjectedSources 处理 (目前 assembleSystemPrompt 还没注入 value-store 标记, 所以这里不传)
411
+ const gate = await injectJudgmentGate(input, {}, { maxChars: 1500 });
412
+ this.judgmentGateAddition = gate.systemAddition;
413
+ this.judgmentGateUsedIds = gate.usedIds;
414
+ if (gate.usedIds.length > 0) {
415
+ safePhase('gate_done', { usedCount: gate.usedIds.length, didInject: gate.didInject, skipReason: gate.skipReason });
416
+ }
417
+ }
418
+ catch (err) {
419
+ console.warn('[PiAgent] judgment gate failed (non-fatal):', err);
420
+ this.judgmentGateAddition = '';
421
+ this.judgmentGateUsedIds = [];
422
+ }
423
+ }
424
+ clearJudgmentGate() {
425
+ this.judgmentGateAddition = '';
426
+ this.judgmentGateUsedIds = [];
427
+ }
346
428
  constructor(config) {
347
429
  this.cwd = config.cwd;
348
430
  this.peerId = config.peerId || 'local';
@@ -428,17 +510,21 @@ class PiAgentSession {
428
510
  const { createBollharnessIntegration } = await import('../bollharness-integration/index.js');
429
511
  this.harness = createBollharnessIntegration();
430
512
  this.harnessEnabled = true;
513
+ // ReactHarness 已用 bollharness, 这里也记一份以供 archive 调用
514
+ this.reactHarness = new ReactHarness({ harnessEnabled: true, gateEnabled: true });
431
515
  }
432
516
  catch (e) {
433
517
  console.warn('[PiAgentSession] Harness initialization failed:', e);
434
518
  this.harnessEnabled = false;
519
+ // 失败 fallback: 走纯 8-gate (不带 bollharness 的 8-gate 工作流)
520
+ this.reactHarness = new ReactHarness({ harnessEnabled: false, gateEnabled: true });
435
521
  }
436
522
  }
437
523
  registerTools() {
438
524
  this.tools.set('read_document', {
439
525
  name: 'read_document',
440
526
  description: '读取文档内容,支持 .txt, .md, .pdf, .docx 格式',
441
- parameters: { path: '文件路径' },
527
+ parameters: { path: 'string' },
442
528
  execute: async (args) => {
443
529
  try {
444
530
  const content = await documentReader.read(args.path);
@@ -455,7 +541,7 @@ class PiAgentSession {
455
541
  this.tools.set('summarize_document', {
456
542
  name: 'summarize_document',
457
543
  description: '总结文档内容,分析并生成摘要',
458
- parameters: { path: '文件路径', context: '可选上下文信息' },
544
+ parameters: { path: 'string', context: 'string' },
459
545
  execute: async (args) => {
460
546
  try {
461
547
  if (!this.minimaxAvailable) {
@@ -475,7 +561,7 @@ class PiAgentSession {
475
561
  this.tools.set('improve_document', {
476
562
  name: 'improve_document',
477
563
  description: '根据要求改进文档内容',
478
- parameters: { path: '文件路径', requirements: '改进要求' },
564
+ parameters: { path: 'string', requirements: 'string' },
479
565
  execute: async (args) => {
480
566
  try {
481
567
  if (!this.minimaxAvailable) {
@@ -767,8 +853,9 @@ class PiAgentSession {
767
853
  return false;
768
854
  }
769
855
  }
770
- async prompt(input) {
856
+ async prompt(input, options) {
771
857
  this.minimaxAvailable = this.checkMinimax();
858
+ this.currentChannelId = options?.channelId ?? this.currentChannelId;
772
859
  this.messageHistory.push({
773
860
  role: 'user',
774
861
  content: input
@@ -778,10 +865,36 @@ class PiAgentSession {
778
865
  this.messageHistory.push({ role: 'assistant', content: response });
779
866
  return response;
780
867
  }
781
- return this.runReActLoop();
868
+ // P0 注入门
869
+ this.currentSignal = options?.signal ?? null;
870
+ this.currentOnStream = options?.onStream ?? null;
871
+ await this.computeJudgmentGate(input);
872
+ // P2: 解析当前 permission mode
873
+ try {
874
+ const { resolvePermissionMode } = await import('./permission-mode.js');
875
+ this.currentPermissionMode = resolvePermissionMode();
876
+ }
877
+ catch (err) {
878
+ console.warn('[PiAgent] resolvePermissionMode failed (non-fatal):', err);
879
+ this.currentPermissionMode = 'default';
880
+ }
881
+ try {
882
+ // 2026-06-16: runReActLoop 现在返回 { reply, aiFailed, aiFailureReason } — 这里只需 reply 字符串
883
+ const loopResult = await this.runReActLoop(undefined, options?.signal);
884
+ return loopResult.reply;
885
+ }
886
+ finally {
887
+ if (this.judgmentGateUsedIds.length > 0) {
888
+ recordJudgmentUsage(this.judgmentGateUsedIds, { userInput: input }).catch((err) => console.warn('[PiAgent] recordJudgmentUsage failed:', err));
889
+ }
890
+ this.clearJudgmentGate();
891
+ this.currentSignal = null;
892
+ this.currentOnStream = null;
893
+ }
782
894
  }
783
- async promptStream(input, onStream) {
895
+ async promptStream(input, onStream, signal, channelId) {
784
896
  this.minimaxAvailable = this.checkMinimax();
897
+ this.currentChannelId = channelId ?? this.currentChannelId;
785
898
  this.messageHistory.push({
786
899
  role: 'user',
787
900
  content: input
@@ -793,11 +906,129 @@ class PiAgentSession {
793
906
  onStream({ type: 'done', content: '' });
794
907
  return response;
795
908
  }
796
- const result = await this.runReActLoop(onStream);
909
+ // P0 注入门: 缓存 onStream + signal, computeJudgmentGate 用 currentOnStream 广播 phase
910
+ this.currentOnStream = onStream;
911
+ this.currentSignal = signal ?? null;
912
+ await this.computeJudgmentGate(input);
913
+ // P1.1: 异步跑 Auto-Compact (LLM 摘要, 仅在 budget 超限时触发, 失败静默)
914
+ // 复用 computeJudgmentGate 的 onStream 广播 phase, 跟 judgment 注入门风格一致
915
+ try {
916
+ await this.maybeAutoCompact(onStream, signal);
917
+ }
918
+ catch (err) {
919
+ console.warn('[PiAgent] maybeAutoCompact failed (non-fatal):', err);
920
+ }
921
+ // Bootstrap SessionStart: 收集项目 Context, 拼到 systemAddition 头部
922
+ // (失败静默, 5s 限流防止循环)
923
+ let bootstrapAddition = '';
924
+ try {
925
+ const ss = await onSessionStart({ channelId: this.currentChannelId || undefined });
926
+ bootstrapAddition = ss.systemAddition || '';
927
+ }
928
+ catch (err) {
929
+ console.warn('[PiAgent] onSessionStart failed (non-fatal):', err);
930
+ }
931
+ this.bootstrapAddition = bootstrapAddition;
932
+ // P2: 解析当前 permission mode (BootstrapOptions > env BOLLOON_PERM_MODE > default)
933
+ try {
934
+ const { resolvePermissionMode } = await import('./permission-mode.js');
935
+ this.currentPermissionMode = resolvePermissionMode();
936
+ }
937
+ catch (err) {
938
+ console.warn('[PiAgent] resolvePermissionMode failed (non-fatal, using default):', err);
939
+ this.currentPermissionMode = 'default';
940
+ }
941
+ this.promptStartTime = Date.now();
942
+ // 2026-06-16: loop 自动重试 — runReActLoop 内部遇到 [AI 服务调用失败] sentinel 时,
943
+ // 会设 aiFailed=true 并提前 break. 这里在外层重跑整个 loop (不是单次 LLM 调用),
944
+ // 临时网络抖动 / 配额瞬时超限可自愈. 最多 3 次, 指数退避 1s/2s/4s.
945
+ // 用户看到 status bar 显示 "自动重试中 X/N" — 不暴露按钮.
946
+ const MAX_LOOP_RETRIES = 3;
947
+ let attempt = 0;
948
+ let result = '';
949
+ let lastAiFailureReason = '';
950
+ while (attempt <= MAX_LOOP_RETRIES) {
951
+ try {
952
+ const loopResult = await this.runReActLoop(onStream, signal);
953
+ result = loopResult.reply;
954
+ if (!loopResult.aiFailed)
955
+ break; // 正常完成, 退出 retry 循环
956
+ lastAiFailureReason = loopResult.aiFailureReason || 'AI 调用失败';
957
+ }
958
+ catch (err) {
959
+ // abort 失败: 视作"已中断", 抛错让上层用 partial 兜底
960
+ this.currentOnStream = null;
961
+ this.currentSignal = null;
962
+ throw err;
963
+ }
964
+ attempt++;
965
+ if (attempt > MAX_LOOP_RETRIES) {
966
+ console.warn(`[PiAgent] loop 自动重试 ${MAX_LOOP_RETRIES} 次后仍失败, 终止`);
967
+ if (onStream) {
968
+ onStream({ type: 'status', content: `⛔ loop 自动重试 ${MAX_LOOP_RETRIES} 次后仍失败: ${lastAiFailureReason}`, tool: 'system' });
969
+ }
970
+ result = lastAiFailureReason || 'AI 服务调用失败, 自动重试后仍不可用';
971
+ break;
972
+ }
973
+ const backoffMs = 1000 * Math.pow(2, attempt - 1); // 1s, 2s, 4s
974
+ console.log(`[PiAgent] loop 自动重试 ${attempt}/${MAX_LOOP_RETRIES}, 等待 ${backoffMs}ms`);
975
+ if (onStream) {
976
+ onStream({ type: 'status', content: `↻ 自动重试 loop ${attempt}/${MAX_LOOP_RETRIES} (${(backoffMs / 1000).toFixed(0)}s 后)`, tool: 'system' });
977
+ }
978
+ // 中途 abort 也要响应
979
+ await new Promise((resolve, reject) => {
980
+ const t = setTimeout(() => {
981
+ signal?.removeEventListener?.('abort', onAbort);
982
+ resolve();
983
+ }, backoffMs);
984
+ const onAbort = () => {
985
+ clearTimeout(t);
986
+ reject(new Error('aborted during retry backoff'));
987
+ };
988
+ if (signal?.aborted) {
989
+ clearTimeout(t);
990
+ reject(new Error('aborted during retry backoff'));
991
+ return;
992
+ }
993
+ signal?.addEventListener?.('abort', onAbort, { once: true });
994
+ });
995
+ // 重试时要把这条 user message 从 history 里移除 (避免下一次 runReActLoop 又重复加入),
996
+ // 因为 messageHistory.push({role:'user'}) 在 promptStream 顶部已经做过, 重跑 runReActLoop 不会重复 push,
997
+ // 但 assistant 失败那条也别留 (留了会污染下一轮 LLM context).
998
+ // 简化: 重试前 pop 一次 assistant (如果最后一条是 assistant)
999
+ if (this.messageHistory.length > 0 && this.messageHistory[this.messageHistory.length - 1].role === 'assistant') {
1000
+ this.messageHistory.pop();
1001
+ }
1002
+ }
797
1003
  onStream({ type: 'done', content: '' });
1004
+ // 回溯: 异步记录 usage (不等)
1005
+ if (this.judgmentGateUsedIds.length > 0) {
1006
+ recordJudgmentUsage(this.judgmentGateUsedIds, { userInput: input }).catch((err) => console.warn('[PiAgent] recordJudgmentUsage failed:', err));
1007
+ // P0.5: 把 usedIds 通过 stream 事件回传给调用方 (server.ts 写到 session message)
1008
+ try {
1009
+ onStream({ type: 'used_judgments', usedIds: this.judgmentGateUsedIds, content: '' });
1010
+ }
1011
+ catch { }
1012
+ }
1013
+ // P3 监控门: fire-and-forget 审计 AI 回复是否违反原则
1014
+ monitorAfterReply(input, result);
1015
+ // Bootstrap Stop hook: fire-and-forget 写本次 session 摘要
1016
+ const stopStartTime = this.promptStartTime || Date.now();
1017
+ onStop({
1018
+ channelId: this.currentChannelId || 'unknown',
1019
+ durationMs: Date.now() - stopStartTime,
1020
+ usedJudgmentIds: [...this.judgmentGateUsedIds],
1021
+ }).catch((err) => console.warn('[PiAgent] onStop failed:', err));
1022
+ // 用完即清, 避免污染下一轮
1023
+ this.clearJudgmentGate();
1024
+ this.currentOnStream = null;
1025
+ this.currentSignal = null;
1026
+ this.bootstrapAddition = '';
1027
+ this.promptStartTime = 0;
798
1028
  return result;
799
1029
  }
800
- async promptWithPivotLoop(input, config) {
1030
+ async promptWithPivotLoop(input, config, channelId) {
1031
+ this.currentChannelId = channelId ?? this.currentChannelId;
801
1032
  if (!this.minimaxAvailable) {
802
1033
  const response = await this.handleFallback(input);
803
1034
  return {
@@ -824,12 +1055,14 @@ class PiAgentSession {
824
1055
  for (const tool of this.tools.values()) {
825
1056
  loop.registerTool(tool);
826
1057
  }
1058
+ // P0 注入门: 在构造 systemPrompt 之前算一次, 拼到末尾
1059
+ await this.computeJudgmentGate(input);
827
1060
  const personaSection = this.persona ? `
828
1061
  角色描述: ${this.persona.description || '无'}
829
1062
  性格特点: ${this.persona.personality || '无'}
830
1063
  问候语: ${this.persona.greeting || '无'}
831
1064
  ` : '';
832
- const systemPrompt = `你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${personaSection}
1065
+ const systemPrompt = `${this.bootstrapAddition}你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${personaSection}
833
1066
  当前工作目录: ${this.cwd}
834
1067
  当前身份: ${this.identity.name} (${this.identity.did})
835
1068
 
@@ -846,31 +1079,98 @@ ${this.getToolDefinitions()}
846
1079
  - 每次只调用一个工具
847
1080
  - 仔细分析工具返回结果
848
1081
  - 当任务完成时,必须在回答末尾添加 <final gen> 标记表示结束
849
- - 如果需要更多信息,继续调用工具`;
850
- const result = await loop.execute(input, llm, systemPrompt);
1082
+ - 如果需要更多信息,继续调用工具${this.judgmentGateAddition}`;
1083
+ // 2026-06-15: currentOnStream 传给 loop, step-timeline 在 pivot 循环里也能 emit step_start/done
1084
+ // 之前 loop.execute() 不接 streamCallback, 导致 step-timeline 只能看到老 runReActLoop 路径
1085
+ // promptWithPivotLoop 路径 0 step events — UI 显示 timeline 但永远是空
1086
+ const result = await loop.execute(input, llm, systemPrompt, this.currentOnStream ?? undefined);
851
1087
  this.messageHistory.push({ role: 'user', content: input });
852
1088
  if (result.response) {
853
1089
  this.messageHistory.push({ role: 'assistant', content: result.response });
854
1090
  }
1091
+ // 回溯 + 清场
1092
+ if (this.judgmentGateUsedIds.length > 0) {
1093
+ recordJudgmentUsage(this.judgmentGateUsedIds, { userInput: input }).catch((err) => console.warn('[PiAgent] recordJudgmentUsage failed:', err));
1094
+ }
1095
+ this.clearJudgmentGate();
855
1096
  return result;
856
1097
  }
857
- async runReActLoop(onStream) {
1098
+ async runReActLoop(onStream, signal) {
858
1099
  const llm = getMinimax();
859
1100
  let iteration = 0;
860
1101
  let finalResponse = '';
861
1102
  let lastQualityScore = 0;
862
1103
  let refineAttempts = 0;
863
1104
  let consecutiveErrors = 0;
1105
+ // 2026-06-16 新增: 累计错误数 (跨工具, 兜底防 LLM 轮换工具名死循环)
1106
+ let totalErrors = 0;
864
1107
  let lastFailedTool = ''; // 跟踪最近一次失败的 tool name
865
1108
  let lastFailedToolCount = 0; // 最近失败工具的连续失败次数
1109
+ // 2026-06-16: AI sentinel 标志 — runReActLoop 返回 aiFailed=true,
1110
+ // promptStream 据此自动重跑整个 loop 最多 N 次 (不是单次 LLM 重试)
1111
+ let aiFailed = false;
1112
+ let aiFailureReason = '';
866
1113
  const MAX_CONSECUTIVE_ERRORS = 3;
867
1114
  const MAX_SAME_TOOL_FAILURES = 3; // 同一工具连续失败 3 次, 强制让 LLM 给出最终答案
868
1115
  // 发送循环开始的事件
869
1116
  if (onStream) {
870
1117
  onStream({ type: 'status', content: '🔄 开始 ReAct 循环...', tool: 'system' });
871
1118
  }
1119
+ // React Harness: 循环开始 (重置 turn 计数 + 触发 harness sessionStart)
1120
+ // 失败静默 (fail-open), 不阻塞主循环
1121
+ try {
1122
+ await this.reactHarness.onSessionStart(this.currentChannelId || undefined);
1123
+ }
1124
+ catch (err) {
1125
+ console.warn('[PiAgent] reactHarness.onSessionStart failed (non-fatal):', err);
1126
+ }
872
1127
  while (iteration < this.MAX_REACT_ITERATIONS) {
873
1128
  iteration++;
1129
+ // 停止条件 1: max turns (fail-safe 10000, 正常任务永远跑不到)
1130
+ if (iteration >= this.MAX_REACT_ITERATIONS) {
1131
+ console.warn(`[PiAgent] 达到最大循环数 ${this.MAX_REACT_ITERATIONS}, 强制终止 (fail-safe)`);
1132
+ onStream?.({ type: 'error', content: `⏹️ 达到最大循环数 (${this.MAX_REACT_ITERATIONS}, fail-safe)`, tool: 'loop' });
1133
+ finalResponse = finalResponse || '(本轮 ReAct 循环达到最大步数, 强制结束)';
1134
+ break;
1135
+ }
1136
+ // 停止条件 2: signal.aborted (显式 abort / 用户中断)
1137
+ if (signal?.aborted) {
1138
+ console.warn('[PiAgent] runReActLoop aborted by signal');
1139
+ onStream?.({ type: 'error', content: '⏹️ 用户中断', tool: 'loop' });
1140
+ finalResponse = finalResponse || '(用户中断)';
1141
+ break;
1142
+ }
1143
+ // 2026-06-16 新增: 累计错误兜底 — 跨工具, 防 LLM 轮换工具名绕过 MAX_SAME_TOOL_FAILURES
1144
+ if (totalErrors >= this.MAX_TOTAL_ERRORS) {
1145
+ console.warn(`[PiAgent] 累计错误 ${totalErrors} >= ${this.MAX_TOTAL_ERRORS}, 强制终止 (防死循环)`);
1146
+ onStream?.({ type: 'error', content: `⛔ 累计 ${totalErrors} 次错误, 强制终止 (防止 LLM 死循环)`, tool: 'loop' });
1147
+ finalResponse = finalResponse || `(本轮 ReAct 循环累计 ${totalErrors} 次错误, 强制结束。请换个思路或简化任务重试。)`;
1148
+ break;
1149
+ }
1150
+ // 2026-06-16 新增: loop 内自动压缩 — token 超 80% 阈值时跑一次
1151
+ // compact 失败走 C 路径: 不强行 break, 让现有 60K 阈值兜底 (后面有检查)
1152
+ const compactThreshold = this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD * this.LOOP_COMPACT_RATIO;
1153
+ const estimatedTokensBefore = this.estimateHistoryTokens();
1154
+ if (estimatedTokensBefore > compactThreshold) {
1155
+ const tokensBeforeCompact = estimatedTokensBefore;
1156
+ console.log(`[PiAgent] loop 入口 token ${tokensBeforeCompact} > ${compactThreshold}, 触发自动压缩`);
1157
+ onStream?.({ type: 'status', content: `🗜️ loop 自动压缩 (token ${tokensBeforeCompact} > ${compactThreshold})`, tool: 'compactor' });
1158
+ try {
1159
+ await this.maybeAutoCompact(onStream, signal);
1160
+ }
1161
+ catch (compactErr) {
1162
+ // C 路径: compact 失败不 break, 让 token 阈值检查兜底
1163
+ console.warn(`[PiAgent] loop 内 maybeAutoCompact 失败 (non-fatal, 继续走 token 阈值):`, compactErr);
1164
+ }
1165
+ }
1166
+ // 停止条件 3: context overflow (compact 后还超, 强制终止)
1167
+ const estimatedTokens = this.estimateHistoryTokens();
1168
+ if (estimatedTokens > this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD) {
1169
+ console.warn(`[PiAgent] context overflow (${estimatedTokens} tokens > ${this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD})`);
1170
+ onStream?.({ type: 'error', content: `⏹️ 上下文溢出 (${estimatedTokens} tokens, 阈值 ${this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD})`, tool: 'loop' });
1171
+ finalResponse = finalResponse || '(本轮 ReAct 循环因上下文溢出终止)';
1172
+ break;
1173
+ }
874
1174
  // 调试日志:显示每次循环开始
875
1175
  console.log(`[PiAgent] 循环 ${iteration}/${this.MAX_REACT_ITERATIONS} 开始`);
876
1176
  if (onStream) {
@@ -892,7 +1192,7 @@ ${this.getToolDefinitions()}
892
1192
  性格特点: ${this.persona.personality || '无'}
893
1193
  问候语: ${this.persona.greeting || '无'}
894
1194
  ` : '';
895
- const systemPrompt = `你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${personaSection}
1195
+ const systemPrompt = `${this.bootstrapAddition}你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${personaSection}
896
1196
  当前工作目录: ${this.cwd}
897
1197
  当前身份: ${this.identity.name} (${this.identity.did})
898
1198
  ${refineContext}
@@ -910,9 +1210,27 @@ ${toolDefs}
910
1210
  - 每次只调用一个工具
911
1211
  - 仔细分析工具返回结果
912
1212
  - 当任务完成时,必须在回答末尾添加 <final gen> 标记表示结束
913
- - 如果需要更多信息,继续调用工具`;
914
- const response = await llm.chat(context, systemPrompt);
915
- const reply = response.reply.trim();
1213
+ - 如果需要更多信息,继续调用工具${this.judgmentGateAddition}`;
1214
+ // 3 个恢复机制 (Claude Code 论文 9-step pipeline 内部):
1215
+ // 1. max output token 升级 (最多 3 次, 每次 maxOutputTokens 翻倍)
1216
+ // 2. reactive compaction (prompt 估算超阈值, 跑压缩)
1217
+ // 3. prompt-too-long (LLM 报错 4xxx token 错误, 跑 reactive compaction 再试 1 次)
1218
+ // 失败静默: 全部重试失败 → 空 reply (上层用 no tool_use 终止)
1219
+ const response = await this.callLlmWithRecovery(llm, context, systemPrompt, signal, onStream);
1220
+ const reply = (response.reply || '').trim();
1221
+ // 2026-06-16: 看到 [AI 服务调用失败] sentinel → 不再立即 break,
1222
+ // 而是设 aiFailed=true, 让外层 promptStream 自动重跑整个 loop 最多 N 次
1223
+ // (LLM API 401 / 网络错 / 配额满时, pi-ai 返回这个 prefix;
1224
+ // 自动 retry 兜底: 临时网络抖动可自愈, 真挂 N 次后才报失败)
1225
+ if (reply.startsWith('[AI 服务调用失败]')) {
1226
+ console.log(`[PiAgent] 收到 AI 错误 sentinel, 标记 aiFailed, 外层会自动重试整个 loop`);
1227
+ aiFailed = true;
1228
+ aiFailureReason = reply.length > 200 ? reply.substring(0, 200) : reply;
1229
+ if (onStream) {
1230
+ onStream({ type: 'status', content: `⚠️ AI 调用失败, 将自动重试整个 loop`, tool: 'system' });
1231
+ }
1232
+ break;
1233
+ }
916
1234
  console.log(`[PiAgent] LLM 回复长度: ${reply.length}, 内容预览: "${reply.substring(0, 80)}..."`);
917
1235
  console.log(`[PiAgent] LLM 完整回复:\n${reply}`);
918
1236
  // 通知前端:收到 LLM 回复
@@ -944,19 +1262,160 @@ ${toolDefs}
944
1262
  if (toolCall.args && Object.keys(toolCall.args).length > 0) {
945
1263
  onStream({ type: 'status', content: `📋 参数: ${JSON.stringify(toolCall.args)}`, tool: toolCall.name });
946
1264
  }
1265
+ // 2026-06-15: step-timeline 状态机 — 开新节点
1266
+ onStream({
1267
+ type: 'step_start',
1268
+ content: `调用 ${toolCall.name}`,
1269
+ tool: toolCall.name,
1270
+ args: toolCall.args || {},
1271
+ });
947
1272
  }
948
1273
  const tool = this.tools.get(toolCall.name);
949
1274
  if (!tool) {
950
1275
  consecutiveErrors++;
1276
+ // 2026-06-16 新增: 未知工具也要累计 (LLM 幻觉高频场景)
1277
+ totalErrors++;
951
1278
  const errorResult = { success: false, error: `未知工具: ${toolCall.name}` };
952
1279
  this.messageHistory.push({ role: 'tool', content: JSON.stringify(errorResult), toolResult: errorResult });
953
1280
  this.logToHarness(toolCall.name, toolCall.args, errorResult);
954
- console.warn(`[PiAgent] 未知工具: ${toolCall.name},跳过并继续`);
1281
+ console.warn(`[PiAgent] 未知工具: ${toolCall.name} (累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}),跳过并继续`);
955
1282
  continue;
956
1283
  }
1284
+ // Bootstrap PreToolUse hook: 调工具前校验 (危险命令拦截)
1285
+ // 失败静默 — hook 自身挂掉 = 放行
1286
+ // P2: 透传 permissionMode (从 BootstrapOptions / env BOLLOON_PERM_MODE 解析)
1287
+ let toolToExecute = tool;
1288
+ try {
1289
+ const pre = await onPreToolUse({
1290
+ tool: toolCall.name,
1291
+ args: toolCall.args || {},
1292
+ permissionMode: this.currentPermissionMode,
1293
+ });
1294
+ if (!pre.allowed) {
1295
+ const deniedResult = {
1296
+ success: false,
1297
+ error: `PreToolUse 拒绝: ${pre.reason || '未通过安全校验'}`,
1298
+ };
1299
+ this.messageHistory.push({
1300
+ role: 'tool',
1301
+ content: JSON.stringify(deniedResult),
1302
+ toolResult: deniedResult,
1303
+ });
1304
+ this.logToHarness(toolCall.name, toolCall.args, deniedResult);
1305
+ if (onStream) {
1306
+ onStream({
1307
+ type: 'error',
1308
+ content: `🛡️ PreToolUse 拒绝 ${toolCall.name}: ${pre.reason || '安全校验失败'}`,
1309
+ tool: toolCall.name,
1310
+ });
1311
+ // 2026-06-15: step-timeline — 拦在 PreToolUse, 标 step_error
1312
+ onStream({
1313
+ type: 'step_error',
1314
+ content: `PreToolUse 拒绝 ${toolCall.name}`,
1315
+ tool: toolCall.name,
1316
+ error: pre.reason || '安全校验失败',
1317
+ });
1318
+ }
1319
+ console.warn(`[PiAgent] PreToolUse denied ${toolCall.name}: ${pre.reason}`);
1320
+ // 不调 tool.execute, 也不计 consecutiveErrors (这是用户级拒绝, 不是工具错)
1321
+ continue;
1322
+ }
1323
+ }
1324
+ catch (err) {
1325
+ console.warn('[PiAgent] onPreToolUse failed (non-fatal, allowing):', err);
1326
+ }
1327
+ // React Harness: 8-gate + builtin-guards 校验 (在 PreToolUse 之后, 串接双层)
1328
+ // 失败静默, 拒绝时不调 tool.execute
957
1329
  try {
958
- const result = await tool.execute(toolCall.args);
959
- console.log(`[PiAgent] 工具 ${toolCall.name} 执行完成: success=${result.success}`);
1330
+ const pre = await this.reactHarness.preToolCall(toolCall.name, toolCall.args || {}, this.currentChannelId || undefined);
1331
+ if (!pre.allowed) {
1332
+ const deniedResult = {
1333
+ success: false,
1334
+ error: `Harness gate 拒绝 (${pre.details.rejectedBy}): ${pre.reason || '未通过安全校验'}`,
1335
+ };
1336
+ this.messageHistory.push({
1337
+ role: 'tool',
1338
+ content: JSON.stringify(deniedResult),
1339
+ toolResult: deniedResult,
1340
+ });
1341
+ this.logToHarness(toolCall.name, toolCall.args, deniedResult);
1342
+ if (onStream) {
1343
+ onStream({
1344
+ type: 'error',
1345
+ content: `🛡️ Harness ${pre.details.rejectedBy} 拒绝 ${toolCall.name}: ${pre.reason || '安全校验失败'}`,
1346
+ tool: toolCall.name,
1347
+ });
1348
+ // 2026-06-15: step-timeline — Harness gate 拒绝, 标 step_error
1349
+ onStream({
1350
+ type: 'step_error',
1351
+ content: `Harness 拒绝 ${toolCall.name}`,
1352
+ tool: toolCall.name,
1353
+ error: pre.reason || '安全校验失败',
1354
+ });
1355
+ }
1356
+ console.warn(`[PiAgent] Harness denied ${toolCall.name} (${pre.details.rejectedBy}): ${pre.reason}`);
1357
+ continue;
1358
+ }
1359
+ }
1360
+ catch (err) {
1361
+ console.warn('[PiAgent] reactHarness.preToolCall failed (non-fatal, allowing):', err);
1362
+ }
1363
+ try {
1364
+ const toolStart = Date.now();
1365
+ let result = await tool.execute(toolCall.args);
1366
+ const toolDurationMs = Date.now() - toolStart;
1367
+ console.log(`[PiAgent] 工具 ${toolCall.name} 执行完成: success=${result.success} (${toolDurationMs}ms)`);
1368
+ // PostToolUse 审计 hook: 写 audit log, 默认 continue
1369
+ try {
1370
+ await onPostToolUse({
1371
+ tool: toolCall.name,
1372
+ args: toolCall.args || {},
1373
+ result: {
1374
+ success: result.success,
1375
+ output: result.output?.substring(0, 500),
1376
+ error: result.error,
1377
+ },
1378
+ durationMs: toolDurationMs,
1379
+ });
1380
+ }
1381
+ catch (postErr) {
1382
+ console.warn('[PiAgent] onPostToolUse failed (non-fatal):', postErr);
1383
+ }
1384
+ // Context router: 拿最近一次 preToolCall 算的 hint, 拼到 tool result messageHistory
1385
+ // (LLM 下次看到 tool result 时, 能"记得"这次调用的安全约束)
1386
+ const routeHint = this.reactHarness.getLastRouteHint();
1387
+ if (routeHint && routeHint.systemAddition) {
1388
+ this.messageHistory.push({
1389
+ role: 'system',
1390
+ content: `[Harness Router Hint: ${routeHint.reason}]\n${routeHint.systemAddition}`,
1391
+ });
1392
+ this.reactHarness.clearRouteHint();
1393
+ }
1394
+ // React Harness: post-tool call (output 审计: secret leak 等)
1395
+ // 拒绝时 result.output 含敏感 → 替换为 generic message, 不污染 messageHistory
1396
+ try {
1397
+ const post = await this.reactHarness.postToolCall(toolCall.name, String(result.output || ''), this.currentChannelId || undefined);
1398
+ if (!post.allowed) {
1399
+ if (onStream) {
1400
+ onStream({
1401
+ type: 'error',
1402
+ content: `🛡️ Harness output 拒绝 ${toolCall.name}: ${post.reason || '输出含敏感信息'}`,
1403
+ tool: toolCall.name,
1404
+ });
1405
+ }
1406
+ console.warn(`[PiAgent] Harness output denied ${toolCall.name}: ${post.reason}`);
1407
+ // 替换 result: success 仍保留 (tool 本身没错), 但 output 改成 generic
1408
+ // 这样 LLM 下轮看 output 不会拿到秘密, 但 success 标志让它知道 "工具执行了"
1409
+ result = {
1410
+ ...result,
1411
+ output: `[harness output gate: output 含敏感内容, 已屏蔽. 原因: ${post.reason || 'unknown'}]`,
1412
+ _harnessDenied: true,
1413
+ };
1414
+ }
1415
+ }
1416
+ catch (err) {
1417
+ console.warn('[PiAgent] reactHarness.postToolCall failed (non-fatal, allowing):', err);
1418
+ }
960
1419
  this.messageHistory.push({ role: 'tool', content: JSON.stringify(result), toolResult: result });
961
1420
  this.logToHarness(toolCall.name, toolCall.args, result);
962
1421
  // 通知前端工具执行结果
@@ -967,9 +1426,24 @@ ${toolDefs}
967
1426
  const outputPreview = result.output.substring(0, 200);
968
1427
  onStream({ type: 'tool', content: `📤 结果: ${outputPreview}${result.output.length > 200 ? '...' : ''}`, tool: toolCall.name });
969
1428
  }
1429
+ // 2026-06-15: step-timeline 状态机 — 关闭当前节点 (成功)
1430
+ onStream({
1431
+ type: 'step_done',
1432
+ content: `${toolCall.name} 执行成功`,
1433
+ tool: toolCall.name,
1434
+ success: true,
1435
+ output: result.output,
1436
+ });
970
1437
  }
971
1438
  else {
972
1439
  onStream({ type: 'error', content: `❌ ${toolCall.name} 执行失败: ${result.error}`, tool: toolCall.name });
1440
+ // 2026-06-15: step-timeline 状态机 — 关闭当前节点 (失败)
1441
+ onStream({
1442
+ type: 'step_error',
1443
+ content: `${toolCall.name} 执行失败`,
1444
+ tool: toolCall.name,
1445
+ error: result.error,
1446
+ });
973
1447
  }
974
1448
  }
975
1449
  if (result.success) {
@@ -991,6 +1465,8 @@ ${toolDefs}
991
1465
  }
992
1466
  else {
993
1467
  consecutiveErrors++;
1468
+ // 2026-06-16 新增: 累计错误 (跨工具, 兜底防 LLM 轮换工具名死循环)
1469
+ totalErrors++;
994
1470
  // 跟踪同一工具连续失败次数
995
1471
  if (toolCall.name === lastFailedTool) {
996
1472
  lastFailedToolCount++;
@@ -999,7 +1475,7 @@ ${toolDefs}
999
1475
  lastFailedTool = toolCall.name;
1000
1476
  lastFailedToolCount = 1;
1001
1477
  }
1002
- console.warn(`[PiAgent] 工具 ${toolCall.name} 执行失败 (${lastFailedToolCount}/${MAX_SAME_TOOL_FAILURES}): ${result.error}`);
1478
+ console.warn(`[PiAgent] 工具 ${toolCall.name} 执行失败 (${lastFailedToolCount}/${MAX_SAME_TOOL_FAILURES}, 累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}): ${result.error}`);
1003
1479
  // 同一工具连续失败达到上限, 不再重试, 强制 LLM 给出最终答案
1004
1480
  if (lastFailedToolCount >= MAX_SAME_TOOL_FAILURES) {
1005
1481
  console.log(`[PiAgent] 工具 ${toolCall.name} 连续 ${MAX_SAME_TOOL_FAILURES} 次失败, 放弃并要求直接回答`);
@@ -1025,10 +1501,12 @@ ${toolDefs}
1025
1501
  }
1026
1502
  catch (execError) {
1027
1503
  consecutiveErrors++;
1504
+ // 2026-06-16 新增: 异常分支也要累计
1505
+ totalErrors++;
1028
1506
  const errorResult = { success: false, error: String(execError) };
1029
1507
  this.messageHistory.push({ role: 'tool', content: JSON.stringify(errorResult), toolResult: errorResult });
1030
1508
  this.logToHarness(toolCall.name, toolCall.args, errorResult);
1031
- console.error(`[PiAgent] 工具执行异常: ${execError}`);
1509
+ console.error(`[PiAgent] 工具执行异常 (累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}): ${execError}`);
1032
1510
  }
1033
1511
  }
1034
1512
  else {
@@ -1046,7 +1524,10 @@ ${toolDefs}
1046
1524
  const containsToolCallIntent = reply.includes('调用工具') || reply.includes('tool(') ||
1047
1525
  reply.includes('使用工具') || reply.includes('需要获取') || reply.includes('需要查看') ||
1048
1526
  // 兼容 LLM 用对象字面量输出 tool call (上轮没解析成功时, 至少要继续)
1049
- reply.includes('tool =>') || reply.includes('[TOOL_CALL]');
1527
+ reply.includes('tool =>') || reply.includes('[TOOL_CALL]') ||
1528
+ // 2026-06-15 修: 兼容 LLM 用 XML 标签输出 tool call (<shell_exec>...</shell_exec>)
1529
+ // 这时 parseToolCall 失败, 至少要让 loop 继续
1530
+ /<\w+>[\s\S]*?<\/\w+>/.test(reply);
1050
1531
  const hasError = ['不存在', '找不到', '无法找到', 'not found', 'does not exist',
1051
1532
  '错误', 'error', '失败', 'failed'].some(k => reply.includes(k));
1052
1533
  const isTooShort = reply.length < 50 && reply.length > 0;
@@ -1092,7 +1573,15 @@ Workspace root folder: ${this.cwd}
1092
1573
  `;
1093
1574
  finalResponse = identityPrefix + finalResponse;
1094
1575
  this.messageHistory.push({ role: 'assistant', content: finalResponse });
1095
- return finalResponse;
1576
+ // React Harness: 循环结束
1577
+ try {
1578
+ await this.reactHarness.onSessionEnd();
1579
+ }
1580
+ catch (err) {
1581
+ console.warn('[PiAgent] reactHarness.onSessionEnd failed (non-fatal):', err);
1582
+ }
1583
+ // 2026-06-16: 暴露 aiFailed 标志 — promptStream 据此决定是否自动重试整个 loop
1584
+ return { reply: finalResponse, aiFailed, aiFailureReason: aiFailureReason || undefined };
1096
1585
  }
1097
1586
  async deepThink(prompt) {
1098
1587
  const result = await this.thinkingEngine.think(prompt);
@@ -1138,7 +1627,13 @@ Workspace root folder: ${this.cwd}
1138
1627
  return { outputs, success: allSuccess };
1139
1628
  }
1140
1629
  buildContext() {
1141
- const recentMessages = this.messageHistory.slice(-10);
1630
+ // P1 接入: 同步跑前 3 层压缩 (Budget Reduction / Snip / Microcompact)
1631
+ // 异步层 (Context Collapse / Auto-Compact) 在 promptStream 入口处单独跑 (用 LLM)
1632
+ // 失败静默: 任何 stage 抛错 → 走老 slice(-10) 逻辑
1633
+ //
1634
+ // P1.2: 如果 maybeAutoCompact 算过 Context Collapse 投影, 用 this.projectedHistory (读时投影, 非破坏)
1635
+ const source = this.projectedHistory ?? this.messageHistory;
1636
+ const recentMessages = this.compressHistorySync(source).slice(-10);
1142
1637
  return recentMessages.map(m => {
1143
1638
  if (m.role === 'user')
1144
1639
  return `用户: ${m.content}`;
@@ -1151,6 +1646,154 @@ Workspace root folder: ${this.cwd}
1151
1646
  return m.content;
1152
1647
  }).join('\n');
1153
1648
  }
1649
+ /**
1650
+ * 估算 messageHistory 的 token 数 (4 字符 ≈ 1 token, 与 context-compaction 同步).
1651
+ * 失败静默: 任何异常 → 0 (不阻塞)
1652
+ */
1653
+ estimateHistoryTokens() {
1654
+ try {
1655
+ const { estimateTokens } = require('../context-compaction/index.js');
1656
+ return estimateTokens(this.messageHistory);
1657
+ }
1658
+ catch {
1659
+ return 0;
1660
+ }
1661
+ }
1662
+ /**
1663
+ * 3 个恢复机制合一:
1664
+ * 1. max output token 升级: 最多 3 次, 每次 maxOutputTokens 翻倍 (如果 llm.chat 支持)
1665
+ * 2. reactive compaction: 估算 > 80% 阈值, 跑 sync compressHistorySync + 必要时 maybeAutoCompact
1666
+ * 3. prompt-too-long: LLM 报错 4xxx token 错误, 跑 reactive compaction 再试 1 次
1667
+ *
1668
+ * 失败静默: 全部失败 → 返回空 reply, 让上层 no-tool_use 终止
1669
+ */
1670
+ async callLlmWithRecovery(llm, context, systemPrompt, signal, onStream) {
1671
+ // Reactive compaction 预检: 估算 token 超 80% 阈值, 跑一次
1672
+ const estimated = this.estimateHistoryTokens();
1673
+ if (estimated > this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD * 0.8) {
1674
+ console.warn(`[PiAgent] reactive compaction pre-check (${estimated} tokens > 80% threshold)`);
1675
+ onStream?.({ type: 'status', content: '⚠️ reactive compaction 预检触发', tool: 'recovery' });
1676
+ try {
1677
+ const compacted = this.compressHistorySync(this.messageHistory);
1678
+ this.messageHistory = compacted;
1679
+ if (this.estimateHistoryTokens() > this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD * 0.8) {
1680
+ await this.maybeAutoCompact(onStream, signal);
1681
+ }
1682
+ }
1683
+ catch (err) {
1684
+ console.warn('[PiAgent] reactive compaction pre-check failed:', err);
1685
+ }
1686
+ }
1687
+ // 主调用 + 3 个恢复路径
1688
+ let lastErr = null;
1689
+ for (let attempt = 0; attempt <= this.MAX_OUTPUT_TOKEN_ESCALATION_RETRIES; attempt++) {
1690
+ try {
1691
+ const response = await llm.chat(context, systemPrompt, signal);
1692
+ return { reply: response.reply || '' };
1693
+ }
1694
+ catch (err) {
1695
+ lastErr = err;
1696
+ const errMsg = String(err?.message || err || '');
1697
+ const isPromptTooLong = /token|too long|exceed|length|context|4000|413|429/i.test(errMsg);
1698
+ if (isPromptTooLong) {
1699
+ console.warn(`[PiAgent] prompt-too-long 触发 (attempt ${attempt + 1}), 跑 reactive compaction`);
1700
+ onStream?.({ type: 'status', content: `⚠️ prompt-too-long 触发 (attempt ${attempt + 1}/${this.MAX_OUTPUT_TOKEN_ESCALATION_RETRIES + 1})`, tool: 'recovery' });
1701
+ try {
1702
+ await this.maybeAutoCompact(onStream, signal);
1703
+ }
1704
+ catch (compactionErr) {
1705
+ console.warn('[PiAgent] reactive compaction on prompt-too-long failed:', compactionErr);
1706
+ }
1707
+ // 重新生成 context (compressHistorySync + projected)
1708
+ context = this.buildContext();
1709
+ }
1710
+ else {
1711
+ // 非 prompt-too-long 错误, 1 次重试就放弃
1712
+ if (attempt === 0) {
1713
+ console.warn(`[PiAgent] LLM 调用失败 (non-prompt-too-long), 1 次重试:`, err);
1714
+ continue;
1715
+ }
1716
+ break;
1717
+ }
1718
+ }
1719
+ }
1720
+ console.warn('[PiAgent] callLlmWithRecovery 全失败 (silent):', lastErr);
1721
+ return { reply: '' };
1722
+ }
1723
+ /**
1724
+ * 同步压缩: 跑前 3 层 (Budget Reduction / Snip / Microcompact).
1725
+ * Context Collapse / Auto-Compact 是 async, 不在 buildContext 同步链里跑.
1726
+ * 失败静默: 任何 stage 抛错 → 返回原 history.
1727
+ */
1728
+ compressHistorySync(history) {
1729
+ try {
1730
+ // context-compaction 的 Message 与 pi-sdk 的 Message 字段兼容
1731
+ // 这里用 any cast 跳过 structural type 严格校验 (避免双向 import)
1732
+ let h = history;
1733
+ const r1 = budgetReduce(h);
1734
+ h = r1.history;
1735
+ const r2 = snip(h);
1736
+ h = r2.history;
1737
+ const r3 = microcompact(h);
1738
+ h = r3.history;
1739
+ return h;
1740
+ }
1741
+ catch (err) {
1742
+ console.warn('[PiAgent] compressHistorySync failed (silent, using original):', err);
1743
+ return history;
1744
+ }
1745
+ }
1746
+ /**
1747
+ * P1.1: 异步跑 Auto-Compact (LLM 摘要).
1748
+ * 入口: promptStream 入口, 在 computeJudgmentGate 之后, onSessionStart 之前.
1749
+ *
1750
+ * 逻辑:
1751
+ * 1. 跑完整 compactPipeline (5 层, 异步)
1752
+ * 2. 第 5 层 (Auto-Compact) 需要 LLM, 通过 getMinimax().chat 注入
1753
+ * 3. 如果 budgetGate 不超限, 5 层短路在前 3 层, 不会调 LLM → 零开销
1754
+ * 4. 失败静默: 任何异常 → console.warn + 保留原 messageHistory
1755
+ *
1756
+ * onStream 广播: 跟 computeJudgmentGate 风格一致 (phase 事件供 UI timeline 显示)
1757
+ */
1758
+ async maybeAutoCompact(onStream, signal) {
1759
+ if (this.messageHistory.length < 10)
1760
+ return; // 历史太短, 不值得压
1761
+ onStream?.({ type: 'status', content: '🗜️ 评估是否需要压缩上下文...', tool: 'compactor' });
1762
+ // 注入 LLM (用 getMinimax().chat, 与 judgment 注入门 / ReAct 循环同一来源)
1763
+ // 给 Context Collapse (虚拟投影) 和 Auto-Compact (摘要) 共用
1764
+ const llm = getMinimax();
1765
+ const llmChat = async (systemPrompt, userPrompt) => {
1766
+ const r = await llm.chat(userPrompt, systemPrompt, signal);
1767
+ return r.reply;
1768
+ };
1769
+ const { compactPipeline, isContextCollapseEnabled } = await import('../context-compaction/index.js');
1770
+ const result = await compactPipeline(this.messageHistory, {
1771
+ maxTokens: 8000,
1772
+ llmChat,
1773
+ collapseLlmChat: llmChat, // P1.2: Context Collapse 投影也用同一 LLM
1774
+ cacheScope: this.currentChannelId || 'default',
1775
+ });
1776
+ if (result.compacted && result.history.length < this.messageHistory.length) {
1777
+ const saved = this.messageHistory.length - result.history.length;
1778
+ const stagesApplied = result.stages.filter((s) => s.applied).map((s) => s.stage).join(' → ');
1779
+ onStream?.({
1780
+ type: 'status',
1781
+ content: `🗜️ 上下文压缩: ${stagesApplied || 'no-op'} | 节省 ${saved} 条 (剩余 ${result.history.length}, collapse=${isContextCollapseEnabled() ? 'on' : 'off'})`,
1782
+ tool: 'compactor',
1783
+ });
1784
+ // 关键: 第 4 层 (Context Collapse) 是读时投影 (非破坏)
1785
+ // 第 5 层 (Auto-Compact) 是破坏性折叠
1786
+ // 这里用 if-else 区分: collapse on → 仅 buildContext 用; collapse off → 真更新
1787
+ if (isContextCollapseEnabled()) {
1788
+ this.projectedHistory = result.history; // buildContext 用
1789
+ // messageHistory 不变 (非破坏)
1790
+ }
1791
+ else {
1792
+ this.messageHistory = result.history; // 真破坏性更新
1793
+ this.projectedHistory = null;
1794
+ }
1795
+ }
1796
+ }
1154
1797
  isFinalResponse(content) {
1155
1798
  // 只有明确输出 <final gen> 才认为是最终回答
1156
1799
  return content.includes('<final gen>');
@@ -1190,6 +1833,9 @@ Workspace root folder: ${this.cwd}
1190
1833
  /\btool\s*=>\s*["'](\w+)["']/,
1191
1834
  // 兼容: [TOOL_CALL] 块内 JSON 形式 {"name": "x", "args": {...}}
1192
1835
  /\[TOOL_CALL\][\s\S]*?\{\s*"name"\s*:\s*"(\w+)"\s*,\s*"args"\s*:\s*(\{[\s\S]*?\})/i,
1836
+ // 2026-06-15 修: 兼容 LLM 输出的 XML 格式 <tool_name>...<arg>value</arg>...</tool_name>
1837
+ // 实际 LLM 习惯: <shell_exec>\n<command>ls</command>\n<args>["-la", "..."]</args>\n</shell_exec>
1838
+ /<(\w+)>([\s\S]*?)<\/\1>/,
1193
1839
  ];
1194
1840
  for (const pattern of patterns) {
1195
1841
  const match = content.match(pattern);
@@ -1215,6 +1861,19 @@ Workspace root folder: ${this.cwd}
1215
1861
  }
1216
1862
  }
1217
1863
  }
1864
+ else if (rawArgs && /<\w+>[\s\S]*<\/\w+>/.test(rawArgs)) {
1865
+ // 2026-06-15 修: XML 格式, 解析内嵌子标签 <argname>value</argname>
1866
+ // 例: <command>ls</command>\n<args>["-la","~/.bolloon/skills"]</args>
1867
+ const xmlArgPattern = /<(\w+)>([\s\S]*?)<\/\1>/g;
1868
+ let xmlMatch;
1869
+ while ((xmlMatch = xmlArgPattern.exec(rawArgs)) !== null) {
1870
+ const argName = xmlMatch[1];
1871
+ const argValue = xmlMatch[2].trim();
1872
+ if (argName && argValue) {
1873
+ args[argName] = argValue;
1874
+ }
1875
+ }
1876
+ }
1218
1877
  else if (rawArgs) {
1219
1878
  // 形参串, 形如 key: value, key2: value2
1220
1879
  const argPairs = rawArgs.split(',').map(s => s.trim()).filter(Boolean);
@@ -1561,6 +2220,9 @@ ${this.extractOperationsFromRef(operationsRef)}
1561
2220
  updateIdentity(updates) {
1562
2221
  this.identity = { ...this.identity, ...updates };
1563
2222
  }
2223
+ setCurrentChannelId(channelId) {
2224
+ this.currentChannelId = channelId;
2225
+ }
1564
2226
  getSessionState() {
1565
2227
  return this.sessionManager.getState();
1566
2228
  }