@bolloon/bolloon-agent 0.1.35 → 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 (194) hide show
  1. package/.playwright-mcp/page-2026-06-16T07-51-45-706Z.yml +130 -0
  2. package/.playwright-mcp/page-2026-06-16T07-56-44-116Z.yml +131 -0
  3. package/Bolloon.md +45 -4
  4. package/Dive-into/CITATION.cff +17 -0
  5. package/Dive-into/LICENSE +25 -0
  6. package/Dive-into/README.md +598 -0
  7. package/Dive-into/README_zh.md +599 -0
  8. package/Dive-into/assets/context.png +0 -0
  9. package/Dive-into/assets/extensibility.png +0 -0
  10. package/Dive-into/assets/iteration.png +0 -0
  11. package/Dive-into/assets/layered_architecture.png +0 -0
  12. package/Dive-into/assets/main_structure.png +0 -0
  13. package/Dive-into/assets/permission.png +0 -0
  14. package/Dive-into/assets/session_compact.png +0 -0
  15. package/Dive-into/assets/subagent.png +0 -0
  16. package/Dive-into/paper/Dive_into_Claude_Code.pdf +0 -0
  17. package/README.md +1 -1
  18. package/dist/agents/p2p-chat-tools.js +6 -6
  19. package/dist/agents/permission-mode.js +115 -0
  20. package/dist/agents/pi-sdk.js +435 -25
  21. package/dist/agents/pre-tool-validator.js +194 -0
  22. package/dist/agents/workflow-pivot-loop.js +113 -12
  23. package/dist/bollharness/src/scripts/checks/check_doc_freshness.js +1 -1
  24. package/dist/bollharness/src/scripts/checks/check_doc_links.js +1 -1
  25. package/dist/bollharness/src/scripts/context_router.js +1 -1
  26. package/dist/bollharness/src/scripts/deploy-guard.js +1 -1
  27. package/dist/bollharness/src/scripts/guard_router.js +1 -1
  28. package/dist/bollharness/src/scripts/hooks/_hook_output.js +1 -1
  29. package/dist/bollharness/src/scripts/hooks/risk-tracker.js +1 -1
  30. package/dist/bollharness-integration/context-router.js +2 -2
  31. package/dist/bollharness-integration/guard-checker.js +1 -1
  32. package/dist/bootstrap/bootstrap.js +3 -0
  33. package/dist/bootstrap/context-collector.js +17 -1
  34. package/dist/bootstrap/context-hierarchy.js +218 -0
  35. package/dist/bootstrap/lifecycle-hooks.js +86 -24
  36. package/dist/bootstrap/project-context.js +10 -3
  37. package/dist/context-compaction/auto-compact.js +144 -0
  38. package/dist/context-compaction/budget-gate.js +28 -0
  39. package/dist/context-compaction/budget-reduce.js +35 -0
  40. package/dist/context-compaction/context-collapse.js +66 -0
  41. package/dist/context-compaction/index.js +21 -0
  42. package/dist/context-compaction/microcompact.js +51 -0
  43. package/dist/context-compaction/pipeline.js +123 -0
  44. package/dist/context-compaction/snip.js +45 -0
  45. package/dist/context-compaction/token-estimator.js +35 -0
  46. package/dist/context-compaction/types.js +19 -0
  47. package/dist/heartbeat/HealthMonitor.js +3 -2
  48. package/dist/index.js +38 -2
  49. package/dist/llm/llm-judgment-client.js +32 -30
  50. package/dist/llm/pi-ai.js +103 -16
  51. package/dist/llm/system-prompt/health.js +129 -0
  52. package/dist/llm/system-prompt/registry.js +246 -0
  53. package/dist/llm/system-prompt/strip-hibsml.js +51 -0
  54. package/dist/llm/tool-manifest/ask_user_input.js +35 -0
  55. package/dist/llm/tool-manifest/bash.js +23 -0
  56. package/dist/llm/tool-manifest/create_file.js +24 -0
  57. package/dist/llm/tool-manifest/fetch_sports_data.js +26 -0
  58. package/dist/llm/tool-manifest/image_search.js +24 -0
  59. package/dist/llm/tool-manifest/index.js +69 -0
  60. package/dist/llm/tool-manifest/mcp.js +43 -0
  61. package/dist/llm/tool-manifest/message_compose.js +31 -0
  62. package/dist/llm/tool-manifest/places.js +83 -0
  63. package/dist/llm/tool-manifest/present_files.js +21 -0
  64. package/dist/llm/tool-manifest/recipe.js +40 -0
  65. package/dist/llm/tool-manifest/recommend_apps.js +23 -0
  66. package/dist/llm/tool-manifest/str_replace.js +27 -0
  67. package/dist/llm/tool-manifest/types.js +7 -0
  68. package/dist/llm/tool-manifest/view.js +24 -0
  69. package/dist/llm/tool-manifest/weather.js +27 -0
  70. package/dist/llm/tool-manifest/web.js +51 -0
  71. package/dist/network/p2p-direct.js +23 -0
  72. package/dist/network/source-intent-broadcaster.js +203 -0
  73. package/dist/network/source-intent.js +100 -0
  74. package/dist/pi-ecosystem-judgment/adaptive-scan.js +48 -0
  75. package/dist/pi-ecosystem-judgment/injection-gate.js +54 -12
  76. package/dist/pi-ecosystem-judgment/value-injection.js +8 -2
  77. package/dist/security/context-router-tool.js +1 -1
  78. package/dist/security/input-scanner.js +223 -0
  79. package/dist/web/client.js +3677 -4161
  80. package/dist/web/components/p2p/P2PModal.js +188 -0
  81. package/dist/web/components/p2p/index.js +264 -226
  82. package/dist/web/components/p2p/p2p-modal.js +657 -0
  83. package/dist/web/components/p2p/p2p-tools.js +248 -0
  84. package/dist/web/index.html +58 -67
  85. package/dist/web/server.js +299 -117
  86. package/dist/web/style.css +531 -249
  87. package/dist/web/ui/message-renderer.js +463 -0
  88. package/dist/web/ui/step-timeline.js +375 -0
  89. package/lefthook.yml +4 -0
  90. package/package.json +1 -1
  91. package/scripts/auto-evolve-loop.ts +105 -0
  92. package/scripts/build-web.ts +35 -1
  93. package/scripts/validate-system-prompt.ts +142 -0
  94. package/src/agents/p2p-chat-tools.ts +6 -6
  95. package/src/agents/permission-mode.ts +127 -0
  96. package/src/agents/pi-sdk.ts +461 -28
  97. package/src/agents/pre-tool-validator.ts +213 -0
  98. package/src/agents/workflow-pivot-loop.ts +110 -19
  99. package/src/bollharness/CLAUDE.md +1 -1
  100. package/src/bollharness/README.md +2 -2
  101. package/src/bollharness/README.zh-CN.md +2 -2
  102. package/src/bollharness/reference/boll-reference/scripts/hooks/find-boll-root.sh +2 -2
  103. package/src/bollharness/scripts/context-fragments/truth-source-hierarchy.md +2 -2
  104. package/src/bollharness/scripts/context-fragments/version-sources.md +1 -1
  105. package/src/bollharness/scripts/hooks/find-project-root.sh +4 -4
  106. package/src/bollharness/src/scripts/checks/check_doc_freshness.ts +1 -1
  107. package/src/bollharness/src/scripts/checks/check_doc_links.ts +1 -1
  108. package/src/bollharness/src/scripts/context_router.ts +1 -1
  109. package/src/bollharness/src/scripts/deploy-guard.ts +1 -1
  110. package/src/bollharness/src/scripts/guard_router.ts +1 -1
  111. package/src/bollharness/src/scripts/hooks/_hook_output.ts +1 -1
  112. package/src/bollharness/src/scripts/hooks/risk-tracker.ts +1 -1
  113. package/src/bollharness/templates/scaffold/.gitignore.append +1 -1
  114. package/src/bollharness/templates/scaffold/CLAUDE.md +2 -2
  115. package/src/bollharness-integration/context-router.ts +2 -2
  116. package/src/bollharness-integration/guard-checker.ts +1 -1
  117. package/src/bootstrap/bootstrap.ts +3 -0
  118. package/src/bootstrap/context-collector.ts +30 -1
  119. package/src/bootstrap/context-hierarchy.ts +283 -0
  120. package/src/bootstrap/lifecycle-hooks.ts +139 -26
  121. package/src/bootstrap/project-context.ts +11 -3
  122. package/src/context-compaction/auto-compact.ts +153 -0
  123. package/src/context-compaction/budget-gate.ts +32 -0
  124. package/src/context-compaction/budget-reduce.ts +37 -0
  125. package/src/context-compaction/context-collapse.ts +72 -0
  126. package/src/context-compaction/index.ts +24 -0
  127. package/src/context-compaction/microcompact.ts +54 -0
  128. package/src/context-compaction/pipeline.ts +133 -0
  129. package/src/context-compaction/snip.ts +51 -0
  130. package/src/context-compaction/token-estimator.ts +36 -0
  131. package/src/context-compaction/types.ts +99 -0
  132. package/src/heartbeat/HealthMonitor.ts +3 -2
  133. package/src/index.ts +36 -2
  134. package/src/llm/llm-judgment-client.ts +36 -35
  135. package/src/llm/pi-ai.ts +111 -16
  136. package/src/llm/system-prompt/health.ts +159 -0
  137. package/src/llm/system-prompt/layers/channel/local.md +14 -0
  138. package/src/llm/system-prompt/layers/channel/p2p-agent.md +18 -0
  139. package/src/llm/system-prompt/layers/channel/p2p-visitor.md +19 -0
  140. package/src/llm/system-prompt/layers/core/artifacts_storage.md +89 -0
  141. package/src/llm/system-prompt/layers/core/evenhandedness.md +21 -0
  142. package/src/llm/system-prompt/layers/core/hibs_reminders.md +15 -0
  143. package/src/llm/system-prompt/layers/core/identity.md +37 -0
  144. package/src/llm/system-prompt/layers/core/knowledge.md +17 -0
  145. package/src/llm/system-prompt/layers/core/memory_system.md +12 -0
  146. package/src/llm/system-prompt/layers/core/network_filesystem.md +28 -0
  147. package/src/llm/system-prompt/layers/core/refusal.md +37 -0
  148. package/src/llm/system-prompt/layers/core/tone.md +31 -0
  149. package/src/llm/system-prompt/layers/core/tools.thin.md +13 -0
  150. package/src/llm/system-prompt/layers/core/wellbeing.md +41 -0
  151. package/src/llm/system-prompt/layers/role/architect.md +20 -0
  152. package/src/llm/system-prompt/layers/role/expert.md +19 -0
  153. package/src/llm/system-prompt/layers/role/implementer.md +15 -0
  154. package/src/llm/system-prompt/layers/role/security.md +15 -0
  155. package/src/llm/system-prompt/layers/tool/artifacts.md +72 -0
  156. package/src/llm/system-prompt/layers/tool/bash.md +25 -0
  157. package/src/llm/system-prompt/layers/tool/hibs_api.md +171 -0
  158. package/src/llm/system-prompt/layers/tool/image_search.md +70 -0
  159. package/src/llm/system-prompt/layers/tool/manifest.md +89 -0
  160. package/src/llm/system-prompt/layers/tool/mcp_apps.md +53 -0
  161. package/src/llm/system-prompt/layers/tool/web_search.md +83 -0
  162. package/src/llm/system-prompt/registry.ts +325 -0
  163. package/src/llm/system-prompt/strip-hibsml.ts +52 -0
  164. package/src/llm/tool-manifest/ask_user_input.ts +37 -0
  165. package/src/llm/tool-manifest/bash.ts +25 -0
  166. package/src/llm/tool-manifest/create_file.ts +26 -0
  167. package/src/llm/tool-manifest/fetch_sports_data.ts +28 -0
  168. package/src/llm/tool-manifest/image_search.ts +26 -0
  169. package/src/llm/tool-manifest/index.ts +88 -0
  170. package/src/llm/tool-manifest/mcp.ts +46 -0
  171. package/src/llm/tool-manifest/message_compose.ts +33 -0
  172. package/src/llm/tool-manifest/places.ts +86 -0
  173. package/src/llm/tool-manifest/present_files.ts +23 -0
  174. package/src/llm/tool-manifest/recipe.ts +42 -0
  175. package/src/llm/tool-manifest/recommend_apps.ts +25 -0
  176. package/src/llm/tool-manifest/str_replace.ts +29 -0
  177. package/src/llm/tool-manifest/types.ts +52 -0
  178. package/src/llm/tool-manifest/view.ts +26 -0
  179. package/src/llm/tool-manifest/weather.ts +29 -0
  180. package/src/llm/tool-manifest/web.ts +54 -0
  181. package/src/network/p2p-direct.ts +22 -0
  182. package/src/network/source-intent-broadcaster.ts +242 -0
  183. package/src/network/source-intent.ts +167 -0
  184. package/src/security/context-router-tool.ts +1 -1
  185. package/src/security/input-scanner.ts +287 -0
  186. package/src/web/client.js +271 -2817
  187. package/src/web/client.ts +4326 -0
  188. package/src/web/index.html +58 -67
  189. package/src/web/server.ts +301 -141
  190. package/src/web/style.css +531 -249
  191. package/src/web/ui/message-renderer.ts +540 -0
  192. package/src/web/ui/step-timeline.ts +394 -0
  193. package/test-results/.last-run.json +6 -0
  194. 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
@@ -48,6 +48,8 @@ import { injectJudgmentGate, recordJudgmentUsage } from '../pi-ecosystem-judgmen
48
48
  import { monitorAfterReply } from '../pi-ecosystem-judgment/monitor-gate.js';
49
49
  // Bootstrap 生命周期 hook (SessionStart / Stop / PreToolUse)
50
50
  import { onSessionStart, onStop, onPreToolUse } from '../pi-ecosystem-judgment/human-value-pipeline.js';
51
+ import { onPostToolUse, onJudgmentInjected, onMonitorViolation } from '../bootstrap/lifecycle-hooks.js';
52
+ import { budgetReduce, snip, microcompact } from '../context-compaction/index.js';
51
53
  // React Harness: 8-gate + 4-guard (防越权 / 防 prompt 注入)
52
54
  import { ReactHarness } from '../security/react-harness.js';
53
55
 
@@ -480,10 +482,18 @@ export interface StreamCallback {
480
482
  }
481
483
 
482
484
  export interface StreamEvent {
483
- type: 'status' | 'thinking' | 'tool' | 'token' | 'done' | 'error';
485
+ type: 'status' | 'thinking' | 'tool' | 'token' | 'done' | 'error'
486
+ | 'step_start' | 'step_done' | 'step_error';
484
487
  content: string;
485
488
  tool?: string;
486
489
  data?: unknown;
490
+ // step_* 专用: success / output / error
491
+ success?: boolean;
492
+ output?: string;
493
+ error?: string;
494
+ args?: Record<string, unknown>;
495
+ // step_* 可选: 步骤耗时 (server 端用来展示 in 状态条 + 性能分析)
496
+ durationMs?: number;
487
497
  }
488
498
 
489
499
  const TOOL_DEFINITIONS = `
@@ -572,9 +582,21 @@ class PiAgentSession implements AgentSession {
572
582
  private messageHistory: Message[] = [];
573
583
  private tools: Map<string, Tool> = new Map();
574
584
  private skillRegistry: SkillRegistry = new SkillRegistry();
575
- private readonly MAX_REACT_ITERATIONS = 100;
585
+ // 2026-06-16 修: 父要求把 ReAct loop 上限放大到 "几乎无限", 靠自动压缩上下文 + fail-safe 兜底
586
+ // 默认 10000 — 正常任务永远跑不到, 但作为防 LLM 死循环 / 防 OOM 的最后一道闸
587
+ // 旧默认 100 写死导致中等复杂度任务 (10-50 个 tool call + 多步反思) 会被误杀
588
+ private readonly MAX_REACT_ITERATIONS = 10_000;
576
589
  private readonly MAX_REFINE_ATTEMPTS = 3;
577
590
  private readonly QUALITY_THRESHOLD = 0.6;
591
+ /** P1: 上下文溢出阈值 (单轮估算 token 数, 超过则强制终止防止 prompt-too-long) */
592
+ private readonly MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD = 60_000; // 60K tokens 上限
593
+ /** 2026-06-16 新增: 累计错误总数兜底 (不管是否同工具, 累计 N 次就强制退出)
594
+ * 防 LLM 轮换工具名绕开 MAX_SAME_TOOL_FAILURES 的死循环攻击 */
595
+ private readonly MAX_TOTAL_ERRORS = 20;
596
+ /** 2026-06-16 新增: loop 内自动压缩触发阈值 (相对 60K 阈值的比例) */
597
+ private readonly LOOP_COMPACT_RATIO = 0.8;
598
+ /** P1: max output token 升级重试 (LLM 截断时重试, 最多 3 次) */
599
+ private readonly MAX_OUTPUT_TOKEN_ESCALATION_RETRIES = 3;
578
600
  private thinkingEngine = new DeepThinkingEngine(3);
579
601
  private coordinator = new AgentCoordinator(3);
580
602
  private harness: any = null;
@@ -583,6 +605,10 @@ class PiAgentSession implements AgentSession {
583
605
  private reactHarness: ReactHarness = new ReactHarness();
584
606
  private usePivotLoop: boolean = false;
585
607
  private pivotLoopConfig?: PivotLoopConfig;
608
+ /** P2: 当前会话的 permission mode (每次 promptStream 入口解析) */
609
+ private currentPermissionMode: import('./permission-mode.js').PermissionMode = 'default';
610
+ /** P1.2: Context Collapse 读时投影结果 (feature flag 开启时由 maybeAutoCompact 写入, buildContext 优先用) */
611
+ private projectedHistory: Message[] | null = null;
586
612
 
587
613
  /**
588
614
  * Judgment 注入门临时结果: 在 prompt / promptStream / promptWithPivotLoop 入口算一次, 拼到本轮 systemPrompt 末尾
@@ -620,11 +646,13 @@ class PiAgentSession implements AgentSession {
620
646
 
621
647
  safePhase('gate_compute', { detail: '正在检索相关判断力...' });
622
648
  try {
623
- const gate = await injectJudgmentGate(input);
649
+ // P-Action 4 (2026-06-15) 路径 1 整合: 透传 maxChars=1500 (≈ 375 tokens 硬上限)
650
+ // 路径 2/3 检测由 injection-gate 内部 alreadyInjectedSources 处理 (目前 assembleSystemPrompt 还没注入 value-store 标记, 所以这里不传)
651
+ const gate = await injectJudgmentGate(input, {}, { maxChars: 1500 });
624
652
  this.judgmentGateAddition = gate.systemAddition;
625
653
  this.judgmentGateUsedIds = gate.usedIds;
626
654
  if (gate.usedIds.length > 0) {
627
- safePhase('gate_done', { usedCount: gate.usedIds.length });
655
+ safePhase('gate_done', { usedCount: gate.usedIds.length, didInject: gate.didInject, skipReason: gate.skipReason });
628
656
  }
629
657
  } catch (err) {
630
658
  console.warn('[PiAgent] judgment gate failed (non-fatal):', err);
@@ -737,7 +765,7 @@ class PiAgentSession implements AgentSession {
737
765
  this.tools.set('read_document', {
738
766
  name: 'read_document',
739
767
  description: '读取文档内容,支持 .txt, .md, .pdf, .docx 格式',
740
- parameters: { path: '文件路径' },
768
+ parameters: { path: 'string' },
741
769
  execute: async (args) => {
742
770
  try {
743
771
  const content = await documentReader.read(args.path);
@@ -754,7 +782,7 @@ class PiAgentSession implements AgentSession {
754
782
  this.tools.set('summarize_document', {
755
783
  name: 'summarize_document',
756
784
  description: '总结文档内容,分析并生成摘要',
757
- parameters: { path: '文件路径', context: '可选上下文信息' },
785
+ parameters: { path: 'string', context: 'string' },
758
786
  execute: async (args) => {
759
787
  try {
760
788
  if (!this.minimaxAvailable) {
@@ -774,7 +802,7 @@ class PiAgentSession implements AgentSession {
774
802
  this.tools.set('improve_document', {
775
803
  name: 'improve_document',
776
804
  description: '根据要求改进文档内容',
777
- parameters: { path: '文件路径', requirements: '改进要求' },
805
+ parameters: { path: 'string', requirements: 'string' },
778
806
  execute: async (args) => {
779
807
  try {
780
808
  if (!this.minimaxAvailable) {
@@ -1094,8 +1122,20 @@ class PiAgentSession implements AgentSession {
1094
1122
  this.currentSignal = options?.signal ?? null;
1095
1123
  this.currentOnStream = options?.onStream ?? null;
1096
1124
  await this.computeJudgmentGate(input);
1125
+
1126
+ // P2: 解析当前 permission mode
1127
+ try {
1128
+ const { resolvePermissionMode } = await import('./permission-mode.js');
1129
+ this.currentPermissionMode = resolvePermissionMode();
1130
+ } catch (err) {
1131
+ console.warn('[PiAgent] resolvePermissionMode failed (non-fatal):', err);
1132
+ this.currentPermissionMode = 'default';
1133
+ }
1134
+
1097
1135
  try {
1098
- return await this.runReActLoop(undefined, options?.signal);
1136
+ // 2026-06-16: runReActLoop 现在返回 { reply, aiFailed, aiFailureReason } — 这里只需 reply 字符串
1137
+ const loopResult = await this.runReActLoop(undefined, options?.signal);
1138
+ return loopResult.reply;
1099
1139
  } finally {
1100
1140
  if (this.judgmentGateUsedIds.length > 0) {
1101
1141
  recordJudgmentUsage(this.judgmentGateUsedIds, { userInput: input }).catch((err) =>
@@ -1131,6 +1171,14 @@ class PiAgentSession implements AgentSession {
1131
1171
  this.currentSignal = signal ?? null;
1132
1172
  await this.computeJudgmentGate(input);
1133
1173
 
1174
+ // P1.1: 异步跑 Auto-Compact (LLM 摘要, 仅在 budget 超限时触发, 失败静默)
1175
+ // 复用 computeJudgmentGate 的 onStream 广播 phase, 跟 judgment 注入门风格一致
1176
+ try {
1177
+ await this.maybeAutoCompact(onStream, signal);
1178
+ } catch (err) {
1179
+ console.warn('[PiAgent] maybeAutoCompact failed (non-fatal):', err);
1180
+ }
1181
+
1134
1182
  // Bootstrap SessionStart: 收集项目 Context, 拼到 systemAddition 头部
1135
1183
  // (失败静默, 5s 限流防止循环)
1136
1184
  let bootstrapAddition = '';
@@ -1141,16 +1189,76 @@ class PiAgentSession implements AgentSession {
1141
1189
  console.warn('[PiAgent] onSessionStart failed (non-fatal):', err);
1142
1190
  }
1143
1191
  this.bootstrapAddition = bootstrapAddition;
1144
- this.promptStartTime = Date.now();
1145
1192
 
1146
- let result: string;
1193
+ // P2: 解析当前 permission mode (BootstrapOptions > env BOLLOON_PERM_MODE > default)
1147
1194
  try {
1148
- result = await this.runReActLoop(onStream, signal);
1149
- } catch (err: any) {
1150
- // abort 失败: 视作"已中断", 抛错让上层用 partial 兜底
1151
- this.currentOnStream = null;
1152
- this.currentSignal = null;
1153
- throw err;
1195
+ const { resolvePermissionMode } = await import('./permission-mode.js');
1196
+ this.currentPermissionMode = resolvePermissionMode();
1197
+ } catch (err) {
1198
+ console.warn('[PiAgent] resolvePermissionMode failed (non-fatal, using default):', err);
1199
+ this.currentPermissionMode = 'default';
1200
+ }
1201
+
1202
+ this.promptStartTime = Date.now();
1203
+
1204
+ // 2026-06-16: loop 自动重试 — runReActLoop 内部遇到 [AI 服务调用失败] sentinel 时,
1205
+ // 会设 aiFailed=true 并提前 break. 这里在外层重跑整个 loop (不是单次 LLM 调用),
1206
+ // 临时网络抖动 / 配额瞬时超限可自愈. 最多 3 次, 指数退避 1s/2s/4s.
1207
+ // 用户看到 status bar 显示 "自动重试中 X/N" — 不暴露按钮.
1208
+ const MAX_LOOP_RETRIES = 3;
1209
+ let attempt = 0;
1210
+ let result: string = '';
1211
+ let lastAiFailureReason = '';
1212
+ while (attempt <= MAX_LOOP_RETRIES) {
1213
+ try {
1214
+ const loopResult = await this.runReActLoop(onStream, signal);
1215
+ result = loopResult.reply;
1216
+ if (!loopResult.aiFailed) break; // 正常完成, 退出 retry 循环
1217
+ lastAiFailureReason = loopResult.aiFailureReason || 'AI 调用失败';
1218
+ } catch (err: any) {
1219
+ // abort 失败: 视作"已中断", 抛错让上层用 partial 兜底
1220
+ this.currentOnStream = null;
1221
+ this.currentSignal = null;
1222
+ throw err;
1223
+ }
1224
+ attempt++;
1225
+ if (attempt > MAX_LOOP_RETRIES) {
1226
+ console.warn(`[PiAgent] loop 自动重试 ${MAX_LOOP_RETRIES} 次后仍失败, 终止`);
1227
+ if (onStream) {
1228
+ onStream({ type: 'status', content: `⛔ loop 自动重试 ${MAX_LOOP_RETRIES} 次后仍失败: ${lastAiFailureReason}`, tool: 'system' });
1229
+ }
1230
+ result = lastAiFailureReason || 'AI 服务调用失败, 自动重试后仍不可用';
1231
+ break;
1232
+ }
1233
+ const backoffMs = 1000 * Math.pow(2, attempt - 1); // 1s, 2s, 4s
1234
+ console.log(`[PiAgent] loop 自动重试 ${attempt}/${MAX_LOOP_RETRIES}, 等待 ${backoffMs}ms`);
1235
+ if (onStream) {
1236
+ onStream({ type: 'status', content: `↻ 自动重试 loop ${attempt}/${MAX_LOOP_RETRIES} (${(backoffMs / 1000).toFixed(0)}s 后)`, tool: 'system' });
1237
+ }
1238
+ // 中途 abort 也要响应
1239
+ await new Promise<void>((resolve, reject) => {
1240
+ const t = setTimeout(() => {
1241
+ signal?.removeEventListener?.('abort', onAbort);
1242
+ resolve();
1243
+ }, backoffMs);
1244
+ const onAbort = () => {
1245
+ clearTimeout(t);
1246
+ reject(new Error('aborted during retry backoff'));
1247
+ };
1248
+ if (signal?.aborted) {
1249
+ clearTimeout(t);
1250
+ reject(new Error('aborted during retry backoff'));
1251
+ return;
1252
+ }
1253
+ signal?.addEventListener?.('abort', onAbort, { once: true });
1254
+ });
1255
+ // 重试时要把这条 user message 从 history 里移除 (避免下一次 runReActLoop 又重复加入),
1256
+ // 因为 messageHistory.push({role:'user'}) 在 promptStream 顶部已经做过, 重跑 runReActLoop 不会重复 push,
1257
+ // 但 assistant 失败那条也别留 (留了会污染下一轮 LLM context).
1258
+ // 简化: 重试前 pop 一次 assistant (如果最后一条是 assistant)
1259
+ if (this.messageHistory.length > 0 && this.messageHistory[this.messageHistory.length - 1].role === 'assistant') {
1260
+ this.messageHistory.pop();
1261
+ }
1154
1262
  }
1155
1263
  onStream({ type: 'done', content: '' });
1156
1264
 
@@ -1243,7 +1351,10 @@ ${this.getToolDefinitions()}
1243
1351
  - 当任务完成时,必须在回答末尾添加 <final gen> 标记表示结束
1244
1352
  - 如果需要更多信息,继续调用工具${this.judgmentGateAddition}`;
1245
1353
 
1246
- const result = await loop.execute(input, llm, systemPrompt);
1354
+ // 2026-06-15: currentOnStream 传给 loop, step-timeline 在 pivot 循环里也能 emit step_start/done
1355
+ // 之前 loop.execute() 不接 streamCallback, 导致 step-timeline 只能看到老 runReActLoop 路径
1356
+ // promptWithPivotLoop 路径 0 step events — UI 显示 timeline 但永远是空
1357
+ const result = await loop.execute(input, llm, systemPrompt, this.currentOnStream ?? undefined);
1247
1358
 
1248
1359
  this.messageHistory.push({ role: 'user', content: input });
1249
1360
  if (result.response) {
@@ -1261,15 +1372,21 @@ ${this.getToolDefinitions()}
1261
1372
  return result;
1262
1373
  }
1263
1374
 
1264
- private async runReActLoop(onStream?: StreamCallback, signal?: AbortSignal): Promise<string> {
1375
+ private async runReActLoop(onStream?: StreamCallback, signal?: AbortSignal): Promise<{ reply: string; aiFailed: boolean; aiFailureReason?: string }> {
1265
1376
  const llm = getMinimax();
1266
1377
  let iteration = 0;
1267
1378
  let finalResponse = '';
1268
1379
  let lastQualityScore = 0;
1269
1380
  let refineAttempts = 0;
1270
1381
  let consecutiveErrors = 0;
1382
+ // 2026-06-16 新增: 累计错误数 (跨工具, 兜底防 LLM 轮换工具名死循环)
1383
+ let totalErrors = 0;
1271
1384
  let lastFailedTool = ''; // 跟踪最近一次失败的 tool name
1272
1385
  let lastFailedToolCount = 0; // 最近失败工具的连续失败次数
1386
+ // 2026-06-16: AI sentinel 标志 — runReActLoop 返回 aiFailed=true,
1387
+ // promptStream 据此自动重跑整个 loop 最多 N 次 (不是单次 LLM 重试)
1388
+ let aiFailed = false;
1389
+ let aiFailureReason = '';
1273
1390
  const MAX_CONSECUTIVE_ERRORS = 3;
1274
1391
  const MAX_SAME_TOOL_FAILURES = 3; // 同一工具连续失败 3 次, 强制让 LLM 给出最终答案
1275
1392
 
@@ -1289,6 +1406,55 @@ ${this.getToolDefinitions()}
1289
1406
  while (iteration < this.MAX_REACT_ITERATIONS) {
1290
1407
  iteration++;
1291
1408
 
1409
+ // 停止条件 1: max turns (fail-safe 10000, 正常任务永远跑不到)
1410
+ if (iteration >= this.MAX_REACT_ITERATIONS) {
1411
+ console.warn(`[PiAgent] 达到最大循环数 ${this.MAX_REACT_ITERATIONS}, 强制终止 (fail-safe)`);
1412
+ onStream?.({ type: 'error', content: `⏹️ 达到最大循环数 (${this.MAX_REACT_ITERATIONS}, fail-safe)`, tool: 'loop' });
1413
+ finalResponse = finalResponse || '(本轮 ReAct 循环达到最大步数, 强制结束)';
1414
+ break;
1415
+ }
1416
+
1417
+ // 停止条件 2: signal.aborted (显式 abort / 用户中断)
1418
+ if (signal?.aborted) {
1419
+ console.warn('[PiAgent] runReActLoop aborted by signal');
1420
+ onStream?.({ type: 'error', content: '⏹️ 用户中断', tool: 'loop' });
1421
+ finalResponse = finalResponse || '(用户中断)';
1422
+ break;
1423
+ }
1424
+
1425
+ // 2026-06-16 新增: 累计错误兜底 — 跨工具, 防 LLM 轮换工具名绕过 MAX_SAME_TOOL_FAILURES
1426
+ if (totalErrors >= this.MAX_TOTAL_ERRORS) {
1427
+ console.warn(`[PiAgent] 累计错误 ${totalErrors} >= ${this.MAX_TOTAL_ERRORS}, 强制终止 (防死循环)`);
1428
+ onStream?.({ type: 'error', content: `⛔ 累计 ${totalErrors} 次错误, 强制终止 (防止 LLM 死循环)`, tool: 'loop' });
1429
+ finalResponse = finalResponse || `(本轮 ReAct 循环累计 ${totalErrors} 次错误, 强制结束。请换个思路或简化任务重试。)`;
1430
+ break;
1431
+ }
1432
+
1433
+ // 2026-06-16 新增: loop 内自动压缩 — token 超 80% 阈值时跑一次
1434
+ // compact 失败走 C 路径: 不强行 break, 让现有 60K 阈值兜底 (后面有检查)
1435
+ const compactThreshold = this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD * this.LOOP_COMPACT_RATIO;
1436
+ const estimatedTokensBefore = this.estimateHistoryTokens();
1437
+ if (estimatedTokensBefore > compactThreshold) {
1438
+ const tokensBeforeCompact = estimatedTokensBefore;
1439
+ console.log(`[PiAgent] loop 入口 token ${tokensBeforeCompact} > ${compactThreshold}, 触发自动压缩`);
1440
+ onStream?.({ type: 'status', content: `🗜️ loop 自动压缩 (token ${tokensBeforeCompact} > ${compactThreshold})`, tool: 'compactor' });
1441
+ try {
1442
+ await this.maybeAutoCompact(onStream, signal);
1443
+ } catch (compactErr) {
1444
+ // C 路径: compact 失败不 break, 让 token 阈值检查兜底
1445
+ console.warn(`[PiAgent] loop 内 maybeAutoCompact 失败 (non-fatal, 继续走 token 阈值):`, compactErr);
1446
+ }
1447
+ }
1448
+
1449
+ // 停止条件 3: context overflow (compact 后还超, 强制终止)
1450
+ const estimatedTokens = this.estimateHistoryTokens();
1451
+ if (estimatedTokens > this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD) {
1452
+ console.warn(`[PiAgent] context overflow (${estimatedTokens} tokens > ${this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD})`);
1453
+ onStream?.({ type: 'error', content: `⏹️ 上下文溢出 (${estimatedTokens} tokens, 阈值 ${this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD})`, tool: 'loop' });
1454
+ finalResponse = finalResponse || '(本轮 ReAct 循环因上下文溢出终止)';
1455
+ break;
1456
+ }
1457
+
1292
1458
  // 调试日志:显示每次循环开始
1293
1459
  console.log(`[PiAgent] 循环 ${iteration}/${this.MAX_REACT_ITERATIONS} 开始`);
1294
1460
  if (onStream) {
@@ -1335,8 +1501,27 @@ ${toolDefs}
1335
1501
  - 当任务完成时,必须在回答末尾添加 <final gen> 标记表示结束
1336
1502
  - 如果需要更多信息,继续调用工具${this.judgmentGateAddition}`;
1337
1503
 
1338
- const response = await llm.chat(context, systemPrompt, signal);
1339
- const reply = response.reply.trim();
1504
+ // 3 个恢复机制 (Claude Code 论文 9-step pipeline 内部):
1505
+ // 1. max output token 升级 (最多 3 次, 每次 maxOutputTokens 翻倍)
1506
+ // 2. reactive compaction (prompt 估算超阈值, 跑压缩)
1507
+ // 3. prompt-too-long (LLM 报错 4xxx token 错误, 跑 reactive compaction 再试 1 次)
1508
+ // 失败静默: 全部重试失败 → 空 reply (上层用 no tool_use 终止)
1509
+ const response = await this.callLlmWithRecovery(llm, context, systemPrompt, signal, onStream);
1510
+ const reply = (response.reply || '').trim();
1511
+
1512
+ // 2026-06-16: 看到 [AI 服务调用失败] sentinel → 不再立即 break,
1513
+ // 而是设 aiFailed=true, 让外层 promptStream 自动重跑整个 loop 最多 N 次
1514
+ // (LLM API 401 / 网络错 / 配额满时, pi-ai 返回这个 prefix;
1515
+ // 自动 retry 兜底: 临时网络抖动可自愈, 真挂 N 次后才报失败)
1516
+ if (reply.startsWith('[AI 服务调用失败]')) {
1517
+ console.log(`[PiAgent] 收到 AI 错误 sentinel, 标记 aiFailed, 外层会自动重试整个 loop`);
1518
+ aiFailed = true;
1519
+ aiFailureReason = reply.length > 200 ? reply.substring(0, 200) : reply;
1520
+ if (onStream) {
1521
+ onStream({ type: 'status', content: `⚠️ AI 调用失败, 将自动重试整个 loop`, tool: 'system' });
1522
+ }
1523
+ break;
1524
+ }
1340
1525
 
1341
1526
  console.log(`[PiAgent] LLM 回复长度: ${reply.length}, 内容预览: "${reply.substring(0, 80)}..."`);
1342
1527
  console.log(`[PiAgent] LLM 完整回复:\n${reply}`);
@@ -1375,23 +1560,37 @@ ${toolDefs}
1375
1560
  if (toolCall.args && Object.keys(toolCall.args).length > 0) {
1376
1561
  onStream({ type: 'status', content: `📋 参数: ${JSON.stringify(toolCall.args)}`, tool: toolCall.name });
1377
1562
  }
1563
+ // 2026-06-15: step-timeline 状态机 — 开新节点
1564
+ onStream({
1565
+ type: 'step_start',
1566
+ content: `调用 ${toolCall.name}`,
1567
+ tool: toolCall.name,
1568
+ args: toolCall.args || {},
1569
+ });
1378
1570
  }
1379
1571
 
1380
1572
  const tool = this.tools.get(toolCall.name);
1381
1573
  if (!tool) {
1382
1574
  consecutiveErrors++;
1575
+ // 2026-06-16 新增: 未知工具也要累计 (LLM 幻觉高频场景)
1576
+ totalErrors++;
1383
1577
  const errorResult: ToolResult = { success: false, error: `未知工具: ${toolCall.name}` };
1384
1578
  this.messageHistory.push({ role: 'tool', content: JSON.stringify(errorResult), toolResult: errorResult });
1385
1579
  this.logToHarness(toolCall.name, toolCall.args, errorResult);
1386
- console.warn(`[PiAgent] 未知工具: ${toolCall.name},跳过并继续`);
1580
+ console.warn(`[PiAgent] 未知工具: ${toolCall.name} (累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}),跳过并继续`);
1387
1581
  continue;
1388
1582
  }
1389
1583
 
1390
1584
  // Bootstrap PreToolUse hook: 调工具前校验 (危险命令拦截)
1391
1585
  // 失败静默 — hook 自身挂掉 = 放行
1586
+ // P2: 透传 permissionMode (从 BootstrapOptions / env BOLLOON_PERM_MODE 解析)
1392
1587
  let toolToExecute = tool;
1393
1588
  try {
1394
- const pre = await onPreToolUse({ tool: toolCall.name, args: toolCall.args || {} });
1589
+ const pre = await onPreToolUse({
1590
+ tool: toolCall.name,
1591
+ args: toolCall.args || {},
1592
+ permissionMode: this.currentPermissionMode,
1593
+ });
1395
1594
  if (!pre.allowed) {
1396
1595
  const deniedResult: ToolResult = {
1397
1596
  success: false,
@@ -1409,6 +1608,13 @@ ${toolDefs}
1409
1608
  content: `🛡️ PreToolUse 拒绝 ${toolCall.name}: ${pre.reason || '安全校验失败'}`,
1410
1609
  tool: toolCall.name,
1411
1610
  });
1611
+ // 2026-06-15: step-timeline — 拦在 PreToolUse, 标 step_error
1612
+ onStream({
1613
+ type: 'step_error',
1614
+ content: `PreToolUse 拒绝 ${toolCall.name}`,
1615
+ tool: toolCall.name,
1616
+ error: pre.reason || '安全校验失败',
1617
+ });
1412
1618
  }
1413
1619
  console.warn(`[PiAgent] PreToolUse denied ${toolCall.name}: ${pre.reason}`);
1414
1620
  // 不调 tool.execute, 也不计 consecutiveErrors (这是用户级拒绝, 不是工具错)
@@ -1443,6 +1649,13 @@ ${toolDefs}
1443
1649
  content: `🛡️ Harness ${pre.details.rejectedBy} 拒绝 ${toolCall.name}: ${pre.reason || '安全校验失败'}`,
1444
1650
  tool: toolCall.name,
1445
1651
  });
1652
+ // 2026-06-15: step-timeline — Harness gate 拒绝, 标 step_error
1653
+ onStream({
1654
+ type: 'step_error',
1655
+ content: `Harness 拒绝 ${toolCall.name}`,
1656
+ tool: toolCall.name,
1657
+ error: pre.reason || '安全校验失败',
1658
+ });
1446
1659
  }
1447
1660
  console.warn(`[PiAgent] Harness denied ${toolCall.name} (${pre.details.rejectedBy}): ${pre.reason}`);
1448
1661
  continue;
@@ -1452,8 +1665,26 @@ ${toolDefs}
1452
1665
  }
1453
1666
 
1454
1667
  try {
1668
+ const toolStart = Date.now();
1455
1669
  let result = await tool.execute(toolCall.args);
1456
- console.log(`[PiAgent] 工具 ${toolCall.name} 执行完成: success=${result.success}`);
1670
+ const toolDurationMs = Date.now() - toolStart;
1671
+ console.log(`[PiAgent] 工具 ${toolCall.name} 执行完成: success=${result.success} (${toolDurationMs}ms)`);
1672
+
1673
+ // PostToolUse 审计 hook: 写 audit log, 默认 continue
1674
+ try {
1675
+ await onPostToolUse({
1676
+ tool: toolCall.name,
1677
+ args: toolCall.args || {},
1678
+ result: {
1679
+ success: result.success,
1680
+ output: result.output?.substring(0, 500),
1681
+ error: result.error,
1682
+ },
1683
+ durationMs: toolDurationMs,
1684
+ });
1685
+ } catch (postErr) {
1686
+ console.warn('[PiAgent] onPostToolUse failed (non-fatal):', postErr);
1687
+ }
1457
1688
 
1458
1689
  // Context router: 拿最近一次 preToolCall 算的 hint, 拼到 tool result messageHistory
1459
1690
  // (LLM 下次看到 tool result 时, 能"记得"这次调用的安全约束)
@@ -1506,8 +1737,23 @@ ${toolDefs}
1506
1737
  const outputPreview = result.output.substring(0, 200);
1507
1738
  onStream({ type: 'tool', content: `📤 结果: ${outputPreview}${result.output.length > 200 ? '...' : ''}`, tool: toolCall.name });
1508
1739
  }
1740
+ // 2026-06-15: step-timeline 状态机 — 关闭当前节点 (成功)
1741
+ onStream({
1742
+ type: 'step_done',
1743
+ content: `${toolCall.name} 执行成功`,
1744
+ tool: toolCall.name,
1745
+ success: true,
1746
+ output: result.output,
1747
+ });
1509
1748
  } else {
1510
1749
  onStream({ type: 'error', content: `❌ ${toolCall.name} 执行失败: ${result.error}`, tool: toolCall.name });
1750
+ // 2026-06-15: step-timeline 状态机 — 关闭当前节点 (失败)
1751
+ onStream({
1752
+ type: 'step_error',
1753
+ content: `${toolCall.name} 执行失败`,
1754
+ tool: toolCall.name,
1755
+ error: result.error,
1756
+ });
1511
1757
  }
1512
1758
  }
1513
1759
 
@@ -1530,6 +1776,8 @@ ${toolDefs}
1530
1776
  // 不 break,继续下一次循环
1531
1777
  } else {
1532
1778
  consecutiveErrors++;
1779
+ // 2026-06-16 新增: 累计错误 (跨工具, 兜底防 LLM 轮换工具名死循环)
1780
+ totalErrors++;
1533
1781
  // 跟踪同一工具连续失败次数
1534
1782
  if (toolCall.name === lastFailedTool) {
1535
1783
  lastFailedToolCount++;
@@ -1537,7 +1785,7 @@ ${toolDefs}
1537
1785
  lastFailedTool = toolCall.name;
1538
1786
  lastFailedToolCount = 1;
1539
1787
  }
1540
- console.warn(`[PiAgent] 工具 ${toolCall.name} 执行失败 (${lastFailedToolCount}/${MAX_SAME_TOOL_FAILURES}): ${result.error}`);
1788
+ console.warn(`[PiAgent] 工具 ${toolCall.name} 执行失败 (${lastFailedToolCount}/${MAX_SAME_TOOL_FAILURES}, 累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}): ${result.error}`);
1541
1789
 
1542
1790
  // 同一工具连续失败达到上限, 不再重试, 强制 LLM 给出最终答案
1543
1791
  if (lastFailedToolCount >= MAX_SAME_TOOL_FAILURES) {
@@ -1564,10 +1812,12 @@ ${toolDefs}
1564
1812
  }
1565
1813
  } catch (execError) {
1566
1814
  consecutiveErrors++;
1815
+ // 2026-06-16 新增: 异常分支也要累计
1816
+ totalErrors++;
1567
1817
  const errorResult: ToolResult = { success: false, error: String(execError) };
1568
1818
  this.messageHistory.push({ role: 'tool', content: JSON.stringify(errorResult), toolResult: errorResult });
1569
1819
  this.logToHarness(toolCall.name, toolCall.args, errorResult);
1570
- console.error(`[PiAgent] 工具执行异常: ${execError}`);
1820
+ console.error(`[PiAgent] 工具执行异常 (累计 ${totalErrors}/${this.MAX_TOTAL_ERRORS}): ${execError}`);
1571
1821
  }
1572
1822
  } else {
1573
1823
  // LLM 返回的不是 tool call 格式
@@ -1586,7 +1836,10 @@ ${toolDefs}
1586
1836
  const containsToolCallIntent = reply.includes('调用工具') || reply.includes('tool(') ||
1587
1837
  reply.includes('使用工具') || reply.includes('需要获取') || reply.includes('需要查看') ||
1588
1838
  // 兼容 LLM 用对象字面量输出 tool call (上轮没解析成功时, 至少要继续)
1589
- reply.includes('tool =>') || reply.includes('[TOOL_CALL]');
1839
+ reply.includes('tool =>') || reply.includes('[TOOL_CALL]') ||
1840
+ // 2026-06-15 修: 兼容 LLM 用 XML 标签输出 tool call (<shell_exec>...</shell_exec>)
1841
+ // 这时 parseToolCall 失败, 至少要让 loop 继续
1842
+ /<\w+>[\s\S]*?<\/\w+>/.test(reply);
1590
1843
  const hasError = ['不存在', '找不到', '无法找到', 'not found', 'does not exist',
1591
1844
  '错误', 'error', '失败', 'failed'].some(k => reply.includes(k));
1592
1845
  const isTooShort = reply.length < 50 && reply.length > 0;
@@ -1647,7 +1900,8 @@ Workspace root folder: ${this.cwd}
1647
1900
  console.warn('[PiAgent] reactHarness.onSessionEnd failed (non-fatal):', err);
1648
1901
  }
1649
1902
 
1650
- return finalResponse;
1903
+ // 2026-06-16: 暴露 aiFailed 标志 — promptStream 据此决定是否自动重试整个 loop
1904
+ return { reply: finalResponse, aiFailed, aiFailureReason: aiFailureReason || undefined };
1651
1905
  }
1652
1906
 
1653
1907
  async deepThink(prompt: string): Promise<{ result: ThinkResult; response: string }> {
@@ -1703,7 +1957,13 @@ Workspace root folder: ${this.cwd}
1703
1957
  }
1704
1958
 
1705
1959
  private buildContext(): string {
1706
- const recentMessages = this.messageHistory.slice(-10);
1960
+ // P1 接入: 同步跑前 3 层压缩 (Budget Reduction / Snip / Microcompact)
1961
+ // 异步层 (Context Collapse / Auto-Compact) 在 promptStream 入口处单独跑 (用 LLM)
1962
+ // 失败静默: 任何 stage 抛错 → 走老 slice(-10) 逻辑
1963
+ //
1964
+ // P1.2: 如果 maybeAutoCompact 算过 Context Collapse 投影, 用 this.projectedHistory (读时投影, 非破坏)
1965
+ const source = this.projectedHistory ?? this.messageHistory;
1966
+ const recentMessages = this.compressHistorySync(source).slice(-10);
1707
1967
  return recentMessages.map(m => {
1708
1968
  if (m.role === 'user') return `用户: ${m.content}`;
1709
1969
  if (m.role === 'assistant') return `助手: ${m.content}`;
@@ -1715,6 +1975,164 @@ Workspace root folder: ${this.cwd}
1715
1975
  }).join('\n');
1716
1976
  }
1717
1977
 
1978
+ /**
1979
+ * 估算 messageHistory 的 token 数 (4 字符 ≈ 1 token, 与 context-compaction 同步).
1980
+ * 失败静默: 任何异常 → 0 (不阻塞)
1981
+ */
1982
+ private estimateHistoryTokens(): number {
1983
+ try {
1984
+ const { estimateTokens } = require('../context-compaction/index.js') as typeof import('../context-compaction/index.js');
1985
+ return estimateTokens(this.messageHistory as any);
1986
+ } catch {
1987
+ return 0;
1988
+ }
1989
+ }
1990
+
1991
+ /**
1992
+ * 3 个恢复机制合一:
1993
+ * 1. max output token 升级: 最多 3 次, 每次 maxOutputTokens 翻倍 (如果 llm.chat 支持)
1994
+ * 2. reactive compaction: 估算 > 80% 阈值, 跑 sync compressHistorySync + 必要时 maybeAutoCompact
1995
+ * 3. prompt-too-long: LLM 报错 4xxx token 错误, 跑 reactive compaction 再试 1 次
1996
+ *
1997
+ * 失败静默: 全部失败 → 返回空 reply, 让上层 no-tool_use 终止
1998
+ */
1999
+ private async callLlmWithRecovery(
2000
+ llm: any,
2001
+ context: string,
2002
+ systemPrompt: string,
2003
+ signal: AbortSignal | undefined,
2004
+ onStream?: (chunk: any) => void
2005
+ ): Promise<{ reply: string }> {
2006
+ // Reactive compaction 预检: 估算 token 超 80% 阈值, 跑一次
2007
+ const estimated = this.estimateHistoryTokens();
2008
+ if (estimated > this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD * 0.8) {
2009
+ console.warn(`[PiAgent] reactive compaction pre-check (${estimated} tokens > 80% threshold)`);
2010
+ onStream?.({ type: 'status', content: '⚠️ reactive compaction 预检触发', tool: 'recovery' });
2011
+ try {
2012
+ const compacted = this.compressHistorySync(this.messageHistory);
2013
+ this.messageHistory = compacted;
2014
+ if (this.estimateHistoryTokens() > this.MAX_OUTPUT_TOKEN_ESCALATION_THRESHOLD * 0.8) {
2015
+ await this.maybeAutoCompact(onStream, signal);
2016
+ }
2017
+ } catch (err) {
2018
+ console.warn('[PiAgent] reactive compaction pre-check failed:', err);
2019
+ }
2020
+ }
2021
+
2022
+ // 主调用 + 3 个恢复路径
2023
+ let lastErr: any = null;
2024
+ for (let attempt = 0; attempt <= this.MAX_OUTPUT_TOKEN_ESCALATION_RETRIES; attempt++) {
2025
+ try {
2026
+ const response = await llm.chat(context, systemPrompt, signal);
2027
+ return { reply: response.reply || '' };
2028
+ } catch (err: any) {
2029
+ lastErr = err;
2030
+ const errMsg = String(err?.message || err || '');
2031
+ const isPromptTooLong = /token|too long|exceed|length|context|4000|413|429/i.test(errMsg);
2032
+ if (isPromptTooLong) {
2033
+ console.warn(`[PiAgent] prompt-too-long 触发 (attempt ${attempt + 1}), 跑 reactive compaction`);
2034
+ onStream?.({ type: 'status', content: `⚠️ prompt-too-long 触发 (attempt ${attempt + 1}/${this.MAX_OUTPUT_TOKEN_ESCALATION_RETRIES + 1})`, tool: 'recovery' });
2035
+ try {
2036
+ await this.maybeAutoCompact(onStream, signal);
2037
+ } catch (compactionErr) {
2038
+ console.warn('[PiAgent] reactive compaction on prompt-too-long failed:', compactionErr);
2039
+ }
2040
+ // 重新生成 context (compressHistorySync + projected)
2041
+ context = this.buildContext();
2042
+ } else {
2043
+ // 非 prompt-too-long 错误, 1 次重试就放弃
2044
+ if (attempt === 0) {
2045
+ console.warn(`[PiAgent] LLM 调用失败 (non-prompt-too-long), 1 次重试:`, err);
2046
+ continue;
2047
+ }
2048
+ break;
2049
+ }
2050
+ }
2051
+ }
2052
+ console.warn('[PiAgent] callLlmWithRecovery 全失败 (silent):', lastErr);
2053
+ return { reply: '' };
2054
+ }
2055
+
2056
+ /**
2057
+ * 同步压缩: 跑前 3 层 (Budget Reduction / Snip / Microcompact).
2058
+ * Context Collapse / Auto-Compact 是 async, 不在 buildContext 同步链里跑.
2059
+ * 失败静默: 任何 stage 抛错 → 返回原 history.
2060
+ */
2061
+ private compressHistorySync(history: Message[]): Message[] {
2062
+ try {
2063
+ // context-compaction 的 Message 与 pi-sdk 的 Message 字段兼容
2064
+ // 这里用 any cast 跳过 structural type 严格校验 (避免双向 import)
2065
+ let h: any = history;
2066
+ const r1 = budgetReduce(h);
2067
+ h = r1.history;
2068
+ const r2 = snip(h);
2069
+ h = r2.history;
2070
+ const r3 = microcompact(h);
2071
+ h = r3.history;
2072
+ return h as Message[];
2073
+ } catch (err) {
2074
+ console.warn('[PiAgent] compressHistorySync failed (silent, using original):', err);
2075
+ return history;
2076
+ }
2077
+ }
2078
+
2079
+ /**
2080
+ * P1.1: 异步跑 Auto-Compact (LLM 摘要).
2081
+ * 入口: promptStream 入口, 在 computeJudgmentGate 之后, onSessionStart 之前.
2082
+ *
2083
+ * 逻辑:
2084
+ * 1. 跑完整 compactPipeline (5 层, 异步)
2085
+ * 2. 第 5 层 (Auto-Compact) 需要 LLM, 通过 getMinimax().chat 注入
2086
+ * 3. 如果 budgetGate 不超限, 5 层短路在前 3 层, 不会调 LLM → 零开销
2087
+ * 4. 失败静默: 任何异常 → console.warn + 保留原 messageHistory
2088
+ *
2089
+ * onStream 广播: 跟 computeJudgmentGate 风格一致 (phase 事件供 UI timeline 显示)
2090
+ */
2091
+ private async maybeAutoCompact(
2092
+ onStream?: (chunk: any) => void,
2093
+ signal?: AbortSignal
2094
+ ): Promise<void> {
2095
+ if (this.messageHistory.length < 10) return; // 历史太短, 不值得压
2096
+
2097
+ onStream?.({ type: 'status', content: '🗜️ 评估是否需要压缩上下文...', tool: 'compactor' });
2098
+
2099
+ // 注入 LLM (用 getMinimax().chat, 与 judgment 注入门 / ReAct 循环同一来源)
2100
+ // 给 Context Collapse (虚拟投影) 和 Auto-Compact (摘要) 共用
2101
+ const llm = getMinimax();
2102
+ const llmChat = async (systemPrompt: string, userPrompt: string): Promise<string> => {
2103
+ const r = await llm.chat(userPrompt, systemPrompt, signal);
2104
+ return r.reply;
2105
+ };
2106
+
2107
+ const { compactPipeline, isContextCollapseEnabled } = await import('../context-compaction/index.js');
2108
+ const result = await compactPipeline(this.messageHistory as any, {
2109
+ maxTokens: 8000,
2110
+ llmChat,
2111
+ collapseLlmChat: llmChat, // P1.2: Context Collapse 投影也用同一 LLM
2112
+ cacheScope: this.currentChannelId || 'default',
2113
+ });
2114
+
2115
+ if (result.compacted && result.history.length < this.messageHistory.length) {
2116
+ const saved = this.messageHistory.length - result.history.length;
2117
+ const stagesApplied = result.stages.filter((s) => s.applied).map((s) => s.stage).join(' → ');
2118
+ onStream?.({
2119
+ type: 'status',
2120
+ content: `🗜️ 上下文压缩: ${stagesApplied || 'no-op'} | 节省 ${saved} 条 (剩余 ${result.history.length}, collapse=${isContextCollapseEnabled() ? 'on' : 'off'})`,
2121
+ tool: 'compactor',
2122
+ });
2123
+ // 关键: 第 4 层 (Context Collapse) 是读时投影 (非破坏)
2124
+ // 第 5 层 (Auto-Compact) 是破坏性折叠
2125
+ // 这里用 if-else 区分: collapse on → 仅 buildContext 用; collapse off → 真更新
2126
+ if (isContextCollapseEnabled()) {
2127
+ this.projectedHistory = result.history as Message[]; // buildContext 用
2128
+ // messageHistory 不变 (非破坏)
2129
+ } else {
2130
+ this.messageHistory = result.history as Message[]; // 真破坏性更新
2131
+ this.projectedHistory = null;
2132
+ }
2133
+ }
2134
+ }
2135
+
1718
2136
  private isFinalResponse(content: string): boolean {
1719
2137
  // 只有明确输出 <final gen> 才认为是最终回答
1720
2138
  return content.includes('<final gen>');
@@ -1755,6 +2173,9 @@ Workspace root folder: ${this.cwd}
1755
2173
  /\btool\s*=>\s*["'](\w+)["']/,
1756
2174
  // 兼容: [TOOL_CALL] 块内 JSON 形式 {"name": "x", "args": {...}}
1757
2175
  /\[TOOL_CALL\][\s\S]*?\{\s*"name"\s*:\s*"(\w+)"\s*,\s*"args"\s*:\s*(\{[\s\S]*?\})/i,
2176
+ // 2026-06-15 修: 兼容 LLM 输出的 XML 格式 <tool_name>...<arg>value</arg>...</tool_name>
2177
+ // 实际 LLM 习惯: <shell_exec>\n<command>ls</command>\n<args>["-la", "..."]</args>\n</shell_exec>
2178
+ /<(\w+)>([\s\S]*?)<\/\1>/,
1758
2179
  ];
1759
2180
 
1760
2181
  for (const pattern of patterns) {
@@ -1779,6 +2200,18 @@ Workspace root folder: ${this.cwd}
1779
2200
  if (key) args[key] = valueParts.join(':') || '';
1780
2201
  }
1781
2202
  }
2203
+ } else if (rawArgs && /<\w+>[\s\S]*<\/\w+>/.test(rawArgs)) {
2204
+ // 2026-06-15 修: XML 格式, 解析内嵌子标签 <argname>value</argname>
2205
+ // 例: <command>ls</command>\n<args>["-la","~/.bolloon/skills"]</args>
2206
+ const xmlArgPattern = /<(\w+)>([\s\S]*?)<\/\1>/g;
2207
+ let xmlMatch: RegExpExecArray | null;
2208
+ while ((xmlMatch = xmlArgPattern.exec(rawArgs)) !== null) {
2209
+ const argName = xmlMatch[1];
2210
+ const argValue = xmlMatch[2].trim();
2211
+ if (argName && argValue) {
2212
+ args[argName] = argValue;
2213
+ }
2214
+ }
1782
2215
  } else if (rawArgs) {
1783
2216
  // 形参串, 形如 key: value, key2: value2
1784
2217
  const argPairs = rawArgs.split(',').map(s => s.trim()).filter(Boolean);