@bolloon/bolloon-agent 0.1.34 → 0.1.35

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 (60) hide show
  1. package/.auto-evolve-calls +1 -0
  2. package/.last-auto-evolve-baseline +1 -0
  3. package/Bolloon.md +103 -0
  4. package/dist/agents/pi-sdk.js +264 -12
  5. package/dist/bootstrap/bootstrap.js +114 -0
  6. package/dist/bootstrap/context-collector.js +296 -0
  7. package/dist/bootstrap/lifecycle-hooks.js +109 -0
  8. package/dist/bootstrap/project-context.js +151 -0
  9. package/dist/index.js +11 -0
  10. package/dist/llm/pi-ai.js +31 -21
  11. package/dist/pi-ecosystem-judgment/adaptive-scan.js +231 -0
  12. package/dist/pi-ecosystem-judgment/causal-judge.js +449 -0
  13. package/dist/pi-ecosystem-judgment/detect-hook.js +168 -0
  14. package/dist/pi-ecosystem-judgment/distill-prompt.js +226 -0
  15. package/dist/pi-ecosystem-judgment/evolve-judgment.js +170 -0
  16. package/dist/pi-ecosystem-judgment/human-value-pipeline.js +21 -0
  17. package/dist/pi-ecosystem-judgment/human-value-store.js +283 -22
  18. package/dist/pi-ecosystem-judgment/injection-gate.js +166 -0
  19. package/dist/pi-ecosystem-judgment/monitor-gate.js +188 -0
  20. package/dist/security/builtin-guards.js +124 -0
  21. package/dist/security/context-router-tool.js +106 -0
  22. package/dist/security/react-harness.js +143 -0
  23. package/dist/security/tool-gate.js +235 -0
  24. package/dist/utils/auto-evolve-policy.js +117 -0
  25. package/dist/utils/clamp.js +7 -0
  26. package/dist/utils/double.js +6 -0
  27. package/dist/web/client.js +668 -204
  28. package/dist/web/index.html +24 -4
  29. package/dist/web/server.js +531 -10
  30. package/lefthook.yml +29 -0
  31. package/package.json +3 -2
  32. package/scripts/auto-evolve-loop.ts +376 -0
  33. package/scripts/auto-evolve-oneshot.sh +155 -0
  34. package/scripts/auto-evolve-snapshot.sh +136 -0
  35. package/scripts/detect-schema-changes.sh +48 -0
  36. package/scripts/diff-reviewer.ts +159 -0
  37. package/scripts/weekly-report.ts +364 -0
  38. package/src/agents/pi-sdk.ts +293 -15
  39. package/src/bootstrap/bootstrap.ts +132 -0
  40. package/src/bootstrap/context-collector.ts +342 -0
  41. package/src/bootstrap/lifecycle-hooks.ts +176 -0
  42. package/src/bootstrap/project-context.ts +163 -0
  43. package/src/index.ts +11 -0
  44. package/src/llm/pi-ai.ts +33 -22
  45. package/src/security/builtin-guards.ts +162 -0
  46. package/src/security/context-router-tool.ts +122 -0
  47. package/src/security/react-harness.ts +177 -0
  48. package/src/security/tool-gate.ts +294 -0
  49. package/src/utils/auto-evolve-policy.ts +138 -0
  50. package/src/utils/clamp.ts +5 -0
  51. package/src/web/client.js +668 -204
  52. package/src/web/index.html +24 -4
  53. package/src/web/server.ts +596 -10
  54. package/staging/auto-evolve/clean-001/.review-verdict +9 -0
  55. package/staging/auto-evolve/clean-001/clean-001.patch +14 -0
  56. package/staging/auto-evolve/e2e-001/.patch-id +1 -0
  57. package/staging/auto-evolve/e2e-001/.review-verdict +12 -0
  58. package/staging/auto-evolve/e2e-001/e2e-001.patch +11 -0
  59. package/staging/auto-evolve/test-bad/.review-verdict +12 -0
  60. package/staging/auto-evolve/test-bad/test-bad.patch +11 -0
@@ -41,6 +41,16 @@ import {
41
41
  import { Session, SkillRegistry, saveSession, loadSession, type Skill, type StoredSession } from '@bolloon/constraint-runtime';
42
42
  import { loadSkillsFromPaths, defaultSkillPaths, describeSkill } from './skill-loader.js';
43
43
 
44
+ // Judgment 注入门 (P0): 在主对话 LLM 调起前自动拼入 Top 3 判断力
45
+ // 失败静默, 不阻塞主对话
46
+ import { injectJudgmentGate, recordJudgmentUsage } from '../pi-ecosystem-judgment/injection-gate.js';
47
+ // 持续监控门 (P3): AI 回复后审计是否违反原则
48
+ import { monitorAfterReply } from '../pi-ecosystem-judgment/monitor-gate.js';
49
+ // Bootstrap 生命周期 hook (SessionStart / Stop / PreToolUse)
50
+ import { onSessionStart, onStop, onPreToolUse } from '../pi-ecosystem-judgment/human-value-pipeline.js';
51
+ // React Harness: 8-gate + 4-guard (防越权 / 防 prompt 注入)
52
+ import { ReactHarness } from '../security/react-harness.js';
53
+
44
54
  // Pi Ecosystem Integration (lazy imports - initialized on demand)
45
55
  // Functions from: createGoal, getCurrentGoal, completeCurrentGoal, failCurrentGoal, getGoalStats, getQueueSummary
46
56
 
@@ -499,9 +509,9 @@ export interface HeartbeatConfig {
499
509
  }
500
510
 
501
511
  export interface AgentSession {
502
- prompt(input: string): Promise<string>;
503
- promptStream(input: string, onStream: StreamCallback): Promise<string>;
504
- promptWithPivotLoop(input: string, config?: PivotLoopConfig): Promise<LoopResult>;
512
+ prompt(input: string, options?: { onStream?: StreamCallback; signal?: AbortSignal; channelId?: string }): Promise<string>;
513
+ promptStream(input: string, onStream: StreamCallback, signal?: AbortSignal, channelId?: string): Promise<string>;
514
+ promptWithPivotLoop(input: string, config?: PivotLoopConfig, channelId?: string): Promise<LoopResult>;
505
515
  suggestRename(messages: { type: string; content: string }[]): Promise<string | null>;
506
516
  readDocument(filePath: string): Promise<string>;
507
517
  summarizeDocument(filePath: string, context?: string): Promise<{
@@ -520,6 +530,7 @@ export interface AgentSession {
520
530
  broadcast(message: string): Promise<void>;
521
531
  getIdentity(): IdentityDoc;
522
532
  updateIdentity(updates: Partial<IdentityDoc>): void;
533
+ setCurrentChannelId(channelId: string): void;
523
534
  getSessionState(): PiSessionState;
524
535
  getMemory(): PiMemory;
525
536
  getPersona(): PersonaDoc | null;
@@ -568,9 +579,65 @@ class PiAgentSession implements AgentSession {
568
579
  private coordinator = new AgentCoordinator(3);
569
580
  private harness: any = null;
570
581
  private harnessEnabled = false;
582
+ /** 8-gate + 4-guard 集中调度 (防越权 / 防 prompt 注入) */
583
+ private reactHarness: ReactHarness = new ReactHarness();
571
584
  private usePivotLoop: boolean = false;
572
585
  private pivotLoopConfig?: PivotLoopConfig;
573
586
 
587
+ /**
588
+ * Judgment 注入门临时结果: 在 prompt / promptStream / promptWithPivotLoop 入口算一次, 拼到本轮 systemPrompt 末尾
589
+ * 每次调用都会重置 (避免上一轮遗留)
590
+ */
591
+ private judgmentGateAddition: string = '';
592
+ private judgmentGateUsedIds: string[] = [];
593
+
594
+ /**
595
+ * 当前 onStream 引用 + abort signal (computeJudgmentGate 需要 onStream 广播 phase)
596
+ * 每次 prompt / promptStream / promptWithPivotLoop 入口设置, 用完即清
597
+ */
598
+ private currentOnStream: StreamCallback | null = null;
599
+ private currentSignal: AbortSignal | null = null;
600
+ /** Bootstrap SessionStart 拼的 system prompt 片段 (用完即清) */
601
+ private bootstrapAddition: string = '';
602
+ /** 当前 prompt 开始时间 (供 Stop hook 计算 durationMs) */
603
+ private promptStartTime: number = 0;
604
+ /** 当前 channel id (由 getAgentForChannel / prompt 4 参注入, 供 hook / log 使用) */
605
+ private currentChannelId: string = '';
606
+
607
+ /**
608
+ * 算 judgment 注入门: 失败静默, 不阻塞主对话
609
+ * 期间通过 currentOnStream 广播 phase 事件, 前端可显示 "正在检索判断力..." 状态
610
+ * 调用方负责用完即清 (judgmentGateAddition='')
611
+ */
612
+ private async computeJudgmentGate(input: string): Promise<void> {
613
+ const safePhase = (phase: string, extra: Record<string, unknown> = {}) => {
614
+ try {
615
+ if (this.currentOnStream) {
616
+ this.currentOnStream({ type: 'phase', phase, ...extra, content: '' } as any);
617
+ }
618
+ } catch { /* 静默 */ }
619
+ };
620
+
621
+ safePhase('gate_compute', { detail: '正在检索相关判断力...' });
622
+ try {
623
+ const gate = await injectJudgmentGate(input);
624
+ this.judgmentGateAddition = gate.systemAddition;
625
+ this.judgmentGateUsedIds = gate.usedIds;
626
+ if (gate.usedIds.length > 0) {
627
+ safePhase('gate_done', { usedCount: gate.usedIds.length });
628
+ }
629
+ } catch (err) {
630
+ console.warn('[PiAgent] judgment gate failed (non-fatal):', err);
631
+ this.judgmentGateAddition = '';
632
+ this.judgmentGateUsedIds = [];
633
+ }
634
+ }
635
+
636
+ private clearJudgmentGate(): void {
637
+ this.judgmentGateAddition = '';
638
+ this.judgmentGateUsedIds = [];
639
+ }
640
+
574
641
  constructor(config: AgentSessionConfig) {
575
642
  this.cwd = config.cwd;
576
643
  this.peerId = config.peerId || 'local';
@@ -656,9 +723,13 @@ class PiAgentSession implements AgentSession {
656
723
  const { createBollharnessIntegration } = await import('../bollharness-integration/index.js');
657
724
  this.harness = createBollharnessIntegration();
658
725
  this.harnessEnabled = true;
726
+ // ReactHarness 已用 bollharness, 这里也记一份以供 archive 调用
727
+ this.reactHarness = new ReactHarness({ harnessEnabled: true, gateEnabled: true });
659
728
  } catch (e) {
660
729
  console.warn('[PiAgentSession] Harness initialization failed:', e);
661
730
  this.harnessEnabled = false;
731
+ // 失败 fallback: 走纯 8-gate (不带 bollharness 的 8-gate 工作流)
732
+ this.reactHarness = new ReactHarness({ harnessEnabled: false, gateEnabled: true });
662
733
  }
663
734
  }
664
735
 
@@ -1004,8 +1075,9 @@ class PiAgentSession implements AgentSession {
1004
1075
  }
1005
1076
  }
1006
1077
 
1007
- async prompt(input: string): Promise<string> {
1078
+ async prompt(input: string, options?: { onStream?: StreamCallback; signal?: AbortSignal; channelId?: string }): Promise<string> {
1008
1079
  this.minimaxAvailable = this.checkMinimax();
1080
+ this.currentChannelId = options?.channelId ?? this.currentChannelId;
1009
1081
 
1010
1082
  this.messageHistory.push({
1011
1083
  role: 'user',
@@ -1018,11 +1090,27 @@ class PiAgentSession implements AgentSession {
1018
1090
  return response;
1019
1091
  }
1020
1092
 
1021
- return this.runReActLoop();
1093
+ // P0 注入门
1094
+ this.currentSignal = options?.signal ?? null;
1095
+ this.currentOnStream = options?.onStream ?? null;
1096
+ await this.computeJudgmentGate(input);
1097
+ try {
1098
+ return await this.runReActLoop(undefined, options?.signal);
1099
+ } finally {
1100
+ if (this.judgmentGateUsedIds.length > 0) {
1101
+ recordJudgmentUsage(this.judgmentGateUsedIds, { userInput: input }).catch((err) =>
1102
+ console.warn('[PiAgent] recordJudgmentUsage failed:', err)
1103
+ );
1104
+ }
1105
+ this.clearJudgmentGate();
1106
+ this.currentSignal = null;
1107
+ this.currentOnStream = null;
1108
+ }
1022
1109
  }
1023
1110
 
1024
- async promptStream(input: string, onStream: StreamCallback): Promise<string> {
1111
+ async promptStream(input: string, onStream: StreamCallback, signal?: AbortSignal, channelId?: string): Promise<string> {
1025
1112
  this.minimaxAvailable = this.checkMinimax();
1113
+ this.currentChannelId = channelId ?? this.currentChannelId;
1026
1114
 
1027
1115
  this.messageHistory.push({
1028
1116
  role: 'user',
@@ -1038,12 +1126,66 @@ class PiAgentSession implements AgentSession {
1038
1126
  return response;
1039
1127
  }
1040
1128
 
1041
- const result = await this.runReActLoop(onStream);
1129
+ // P0 注入门: 缓存 onStream + signal, computeJudgmentGate 用 currentOnStream 广播 phase
1130
+ this.currentOnStream = onStream;
1131
+ this.currentSignal = signal ?? null;
1132
+ await this.computeJudgmentGate(input);
1133
+
1134
+ // Bootstrap SessionStart: 收集项目 Context, 拼到 systemAddition 头部
1135
+ // (失败静默, 5s 限流防止循环)
1136
+ let bootstrapAddition = '';
1137
+ try {
1138
+ const ss = await onSessionStart({ channelId: this.currentChannelId || undefined });
1139
+ bootstrapAddition = ss.systemAddition || '';
1140
+ } catch (err) {
1141
+ console.warn('[PiAgent] onSessionStart failed (non-fatal):', err);
1142
+ }
1143
+ this.bootstrapAddition = bootstrapAddition;
1144
+ this.promptStartTime = Date.now();
1145
+
1146
+ let result: string;
1147
+ 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;
1154
+ }
1042
1155
  onStream({ type: 'done', content: '' });
1156
+
1157
+ // 回溯: 异步记录 usage (不等)
1158
+ if (this.judgmentGateUsedIds.length > 0) {
1159
+ recordJudgmentUsage(this.judgmentGateUsedIds, { userInput: input }).catch((err) =>
1160
+ console.warn('[PiAgent] recordJudgmentUsage failed:', err)
1161
+ );
1162
+ // P0.5: 把 usedIds 通过 stream 事件回传给调用方 (server.ts 写到 session message)
1163
+ try { onStream({ type: 'used_judgments', usedIds: this.judgmentGateUsedIds, content: '' } as any); } catch {}
1164
+ }
1165
+
1166
+ // P3 监控门: fire-and-forget 审计 AI 回复是否违反原则
1167
+ monitorAfterReply(input, result);
1168
+
1169
+ // Bootstrap Stop hook: fire-and-forget 写本次 session 摘要
1170
+ const stopStartTime = this.promptStartTime || Date.now();
1171
+ onStop({
1172
+ channelId: this.currentChannelId || 'unknown',
1173
+ durationMs: Date.now() - stopStartTime,
1174
+ usedJudgmentIds: [...this.judgmentGateUsedIds],
1175
+ }).catch((err) => console.warn('[PiAgent] onStop failed:', err));
1176
+
1177
+ // 用完即清, 避免污染下一轮
1178
+ this.clearJudgmentGate();
1179
+ this.currentOnStream = null;
1180
+ this.currentSignal = null;
1181
+ this.bootstrapAddition = '';
1182
+ this.promptStartTime = 0;
1183
+
1043
1184
  return result;
1044
1185
  }
1045
1186
 
1046
- async promptWithPivotLoop(input: string, config?: PivotLoopConfig): Promise<LoopResult> {
1187
+ async promptWithPivotLoop(input: string, config?: PivotLoopConfig, channelId?: string): Promise<LoopResult> {
1188
+ this.currentChannelId = channelId ?? this.currentChannelId;
1047
1189
  if (!this.minimaxAvailable) {
1048
1190
  const response = await this.handleFallback(input);
1049
1191
  return {
@@ -1073,13 +1215,16 @@ class PiAgentSession implements AgentSession {
1073
1215
  loop.registerTool(tool);
1074
1216
  }
1075
1217
 
1218
+ // P0 注入门: 在构造 systemPrompt 之前算一次, 拼到末尾
1219
+ await this.computeJudgmentGate(input);
1220
+
1076
1221
  const personaSection = this.persona ? `
1077
1222
  角色描述: ${this.persona.description || '无'}
1078
1223
  性格特点: ${this.persona.personality || '无'}
1079
1224
  问候语: ${this.persona.greeting || '无'}
1080
1225
  ` : '';
1081
1226
 
1082
- const systemPrompt = `你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${personaSection}
1227
+ const systemPrompt = `${this.bootstrapAddition}你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${personaSection}
1083
1228
  当前工作目录: ${this.cwd}
1084
1229
  当前身份: ${this.identity.name} (${this.identity.did})
1085
1230
 
@@ -1096,7 +1241,7 @@ ${this.getToolDefinitions()}
1096
1241
  - 每次只调用一个工具
1097
1242
  - 仔细分析工具返回结果
1098
1243
  - 当任务完成时,必须在回答末尾添加 <final gen> 标记表示结束
1099
- - 如果需要更多信息,继续调用工具`;
1244
+ - 如果需要更多信息,继续调用工具${this.judgmentGateAddition}`;
1100
1245
 
1101
1246
  const result = await loop.execute(input, llm, systemPrompt);
1102
1247
 
@@ -1105,10 +1250,18 @@ ${this.getToolDefinitions()}
1105
1250
  this.messageHistory.push({ role: 'assistant', content: result.response });
1106
1251
  }
1107
1252
 
1253
+ // 回溯 + 清场
1254
+ if (this.judgmentGateUsedIds.length > 0) {
1255
+ recordJudgmentUsage(this.judgmentGateUsedIds, { userInput: input }).catch((err) =>
1256
+ console.warn('[PiAgent] recordJudgmentUsage failed:', err)
1257
+ );
1258
+ }
1259
+ this.clearJudgmentGate();
1260
+
1108
1261
  return result;
1109
1262
  }
1110
1263
 
1111
- private async runReActLoop(onStream?: StreamCallback): Promise<string> {
1264
+ private async runReActLoop(onStream?: StreamCallback, signal?: AbortSignal): Promise<string> {
1112
1265
  const llm = getMinimax();
1113
1266
  let iteration = 0;
1114
1267
  let finalResponse = '';
@@ -1125,6 +1278,14 @@ ${this.getToolDefinitions()}
1125
1278
  onStream({ type: 'status', content: '🔄 开始 ReAct 循环...', tool: 'system' });
1126
1279
  }
1127
1280
 
1281
+ // React Harness: 循环开始 (重置 turn 计数 + 触发 harness sessionStart)
1282
+ // 失败静默 (fail-open), 不阻塞主循环
1283
+ try {
1284
+ await this.reactHarness.onSessionStart(this.currentChannelId || undefined);
1285
+ } catch (err) {
1286
+ console.warn('[PiAgent] reactHarness.onSessionStart failed (non-fatal):', err);
1287
+ }
1288
+
1128
1289
  while (iteration < this.MAX_REACT_ITERATIONS) {
1129
1290
  iteration++;
1130
1291
 
@@ -1154,7 +1315,7 @@ ${this.getToolDefinitions()}
1154
1315
  问候语: ${this.persona.greeting || '无'}
1155
1316
  ` : '';
1156
1317
 
1157
- const systemPrompt = `你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${personaSection}
1318
+ const systemPrompt = `${this.bootstrapAddition}你是 ${this.identity.name},基于ReAct (Reasoning + Acting)模式工作。${personaSection}
1158
1319
  当前工作目录: ${this.cwd}
1159
1320
  当前身份: ${this.identity.name} (${this.identity.did})
1160
1321
  ${refineContext}
@@ -1172,9 +1333,9 @@ ${toolDefs}
1172
1333
  - 每次只调用一个工具
1173
1334
  - 仔细分析工具返回结果
1174
1335
  - 当任务完成时,必须在回答末尾添加 <final gen> 标记表示结束
1175
- - 如果需要更多信息,继续调用工具`;
1336
+ - 如果需要更多信息,继续调用工具${this.judgmentGateAddition}`;
1176
1337
 
1177
- const response = await llm.chat(context, systemPrompt);
1338
+ const response = await llm.chat(context, systemPrompt, signal);
1178
1339
  const reply = response.reply.trim();
1179
1340
 
1180
1341
  console.log(`[PiAgent] LLM 回复长度: ${reply.length}, 内容预览: "${reply.substring(0, 80)}..."`);
@@ -1226,9 +1387,114 @@ ${toolDefs}
1226
1387
  continue;
1227
1388
  }
1228
1389
 
1390
+ // Bootstrap PreToolUse hook: 调工具前校验 (危险命令拦截)
1391
+ // 失败静默 — hook 自身挂掉 = 放行
1392
+ let toolToExecute = tool;
1393
+ try {
1394
+ const pre = await onPreToolUse({ tool: toolCall.name, args: toolCall.args || {} });
1395
+ if (!pre.allowed) {
1396
+ const deniedResult: ToolResult = {
1397
+ success: false,
1398
+ error: `PreToolUse 拒绝: ${pre.reason || '未通过安全校验'}`,
1399
+ };
1400
+ this.messageHistory.push({
1401
+ role: 'tool',
1402
+ content: JSON.stringify(deniedResult),
1403
+ toolResult: deniedResult,
1404
+ });
1405
+ this.logToHarness(toolCall.name, toolCall.args, deniedResult);
1406
+ if (onStream) {
1407
+ onStream({
1408
+ type: 'error',
1409
+ content: `🛡️ PreToolUse 拒绝 ${toolCall.name}: ${pre.reason || '安全校验失败'}`,
1410
+ tool: toolCall.name,
1411
+ });
1412
+ }
1413
+ console.warn(`[PiAgent] PreToolUse denied ${toolCall.name}: ${pre.reason}`);
1414
+ // 不调 tool.execute, 也不计 consecutiveErrors (这是用户级拒绝, 不是工具错)
1415
+ continue;
1416
+ }
1417
+ } catch (err) {
1418
+ console.warn('[PiAgent] onPreToolUse failed (non-fatal, allowing):', err);
1419
+ }
1420
+
1421
+ // React Harness: 8-gate + builtin-guards 校验 (在 PreToolUse 之后, 串接双层)
1422
+ // 失败静默, 拒绝时不调 tool.execute
1229
1423
  try {
1230
- const result = await tool.execute(toolCall.args);
1424
+ const pre = await this.reactHarness.preToolCall(
1425
+ toolCall.name,
1426
+ toolCall.args || {},
1427
+ this.currentChannelId || undefined
1428
+ );
1429
+ if (!pre.allowed) {
1430
+ const deniedResult: ToolResult = {
1431
+ success: false,
1432
+ error: `Harness gate 拒绝 (${pre.details.rejectedBy}): ${pre.reason || '未通过安全校验'}`,
1433
+ };
1434
+ this.messageHistory.push({
1435
+ role: 'tool',
1436
+ content: JSON.stringify(deniedResult),
1437
+ toolResult: deniedResult,
1438
+ });
1439
+ this.logToHarness(toolCall.name, toolCall.args, deniedResult);
1440
+ if (onStream) {
1441
+ onStream({
1442
+ type: 'error',
1443
+ content: `🛡️ Harness ${pre.details.rejectedBy} 拒绝 ${toolCall.name}: ${pre.reason || '安全校验失败'}`,
1444
+ tool: toolCall.name,
1445
+ });
1446
+ }
1447
+ console.warn(`[PiAgent] Harness denied ${toolCall.name} (${pre.details.rejectedBy}): ${pre.reason}`);
1448
+ continue;
1449
+ }
1450
+ } catch (err) {
1451
+ console.warn('[PiAgent] reactHarness.preToolCall failed (non-fatal, allowing):', err);
1452
+ }
1453
+
1454
+ try {
1455
+ let result = await tool.execute(toolCall.args);
1231
1456
  console.log(`[PiAgent] 工具 ${toolCall.name} 执行完成: success=${result.success}`);
1457
+
1458
+ // Context router: 拿最近一次 preToolCall 算的 hint, 拼到 tool result messageHistory
1459
+ // (LLM 下次看到 tool result 时, 能"记得"这次调用的安全约束)
1460
+ const routeHint = this.reactHarness.getLastRouteHint();
1461
+ if (routeHint && routeHint.systemAddition) {
1462
+ this.messageHistory.push({
1463
+ role: 'system',
1464
+ content: `[Harness Router Hint: ${routeHint.reason}]\n${routeHint.systemAddition}`,
1465
+ });
1466
+ this.reactHarness.clearRouteHint();
1467
+ }
1468
+
1469
+ // React Harness: post-tool call (output 审计: secret leak 等)
1470
+ // 拒绝时 result.output 含敏感 → 替换为 generic message, 不污染 messageHistory
1471
+ try {
1472
+ const post = await this.reactHarness.postToolCall(
1473
+ toolCall.name,
1474
+ String(result.output || ''),
1475
+ this.currentChannelId || undefined
1476
+ );
1477
+ if (!post.allowed) {
1478
+ if (onStream) {
1479
+ onStream({
1480
+ type: 'error',
1481
+ content: `🛡️ Harness output 拒绝 ${toolCall.name}: ${post.reason || '输出含敏感信息'}`,
1482
+ tool: toolCall.name,
1483
+ });
1484
+ }
1485
+ console.warn(`[PiAgent] Harness output denied ${toolCall.name}: ${post.reason}`);
1486
+ // 替换 result: success 仍保留 (tool 本身没错), 但 output 改成 generic
1487
+ // 这样 LLM 下轮看 output 不会拿到秘密, 但 success 标志让它知道 "工具执行了"
1488
+ result = {
1489
+ ...result,
1490
+ output: `[harness output gate: output 含敏感内容, 已屏蔽. 原因: ${post.reason || 'unknown'}]`,
1491
+ _harnessDenied: true,
1492
+ } as typeof result;
1493
+ }
1494
+ } catch (err) {
1495
+ console.warn('[PiAgent] reactHarness.postToolCall failed (non-fatal, allowing):', err);
1496
+ }
1497
+
1232
1498
  this.messageHistory.push({ role: 'tool', content: JSON.stringify(result), toolResult: result });
1233
1499
  this.logToHarness(toolCall.name, toolCall.args, result);
1234
1500
 
@@ -1373,6 +1639,14 @@ Workspace root folder: ${this.cwd}
1373
1639
  finalResponse = identityPrefix + finalResponse;
1374
1640
 
1375
1641
  this.messageHistory.push({ role: 'assistant', content: finalResponse });
1642
+
1643
+ // React Harness: 循环结束
1644
+ try {
1645
+ await this.reactHarness.onSessionEnd();
1646
+ } catch (err) {
1647
+ console.warn('[PiAgent] reactHarness.onSessionEnd failed (non-fatal):', err);
1648
+ }
1649
+
1376
1650
  return finalResponse;
1377
1651
  }
1378
1652
 
@@ -1885,6 +2159,10 @@ ${this.extractOperationsFromRef(operationsRef)}
1885
2159
  this.identity = { ...this.identity, ...updates };
1886
2160
  }
1887
2161
 
2162
+ setCurrentChannelId(channelId: string): void {
2163
+ this.currentChannelId = channelId;
2164
+ }
2165
+
1888
2166
  getSessionState(): PiSessionState {
1889
2167
  return this.sessionManager.getState();
1890
2168
  }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Bolloon Bootstrap — 启动入口
3
+ *
4
+ * web server 启动时 (或 CLI 模式) 调一次, 完成 3 件事:
5
+ * 1. 跑类 B 自适应扫描 (暖缓存 + 写 evolution.jsonl 启动事件)
6
+ * 2. 收集项目 Context (Bolloon.md / git / persona / judgments / skills)
7
+ * 3. 挂每天 0:00 定时任务
8
+ *
9
+ * 失败静默: 任意步骤失败 console.warn, 不抛错 (主流程不被阻塞)
10
+ */
11
+
12
+ import { runAdaptiveScan, logEvolution } from '../pi-ecosystem-judgment/adaptive-scan.js';
13
+ import { collectBolloonContext, type BolloonContext } from './context-collector.js';
14
+ import type { AdaptiveScanResult } from '../pi-ecosystem-judgment/adaptive-scan.js';
15
+
16
+ export interface BootstrapResult {
17
+ context: BolloonContext;
18
+ scanResult: AdaptiveScanResult;
19
+ durationMs: number;
20
+ // 失败的部分不影响主流程
21
+ errors: string[];
22
+ }
23
+
24
+ /**
25
+ * 入口: web server / CLI 启动时调一次
26
+ */
27
+ export async function bootstrapBolloon(opts: { cwd?: string } = {}): Promise<BootstrapResult> {
28
+ const start = Date.now();
29
+ const errors: string[] = [];
30
+
31
+ // 1. 类 B 启动扫描
32
+ let scanResult: AdaptiveScanResult = {
33
+ scannedAt: new Date().toISOString(),
34
+ judgmentsTotal: 0,
35
+ usageEntriesScanned: 0,
36
+ suggestions: [],
37
+ };
38
+ try {
39
+ scanResult = await runAdaptiveScan();
40
+ await logEvolution({
41
+ ts: new Date().toISOString(),
42
+ action: 'accept', // 用 accept 表示"系统记录" (跟 reject 区分)
43
+ suggestion: {
44
+ key: 'bootstrap-startup',
45
+ kind: 'unused', // 占位
46
+ judgmentId: '__bootstrap__',
47
+ decision: 'Bolloon 启动扫描',
48
+ reason: `本次启动扫描了 ${scanResult.judgmentsTotal} 条原则, ${scanResult.usageEntriesScanned} 条使用记录, 生成 ${scanResult.suggestions.length} 条建议`,
49
+ action: 'review',
50
+ metrics: { usage7d: 0, usage30d: 0, daysSinceLastUse: 0, totalUsage: 0 },
51
+ scannedAt: scanResult.scannedAt,
52
+ },
53
+ });
54
+ console.log(`[bootstrap] 类 B 启动扫描完成: ${scanResult.suggestions.length} 条建议`);
55
+ } catch (err) {
56
+ errors.push(`scan: ${(err as Error).message}`);
57
+ console.warn('[bootstrap] 启动扫描失败 (非致命):', err);
58
+ }
59
+
60
+ // 2. 收集项目 Context
61
+ let context: BolloonContext = {
62
+ projectRoot: opts.cwd ?? process.cwd(),
63
+ projectName: 'unknown',
64
+ bolloonMd: null,
65
+ git: null,
66
+ persona: null,
67
+ judgmentsSummary: { total: 0, active: 0, superseded: 0, rejected: 0, topValues: [] },
68
+ skills: [],
69
+ env: { os: 'unknown', nodeVersion: 'unknown', llmProvider: 'unknown' },
70
+ pending: { goals: [], todos: [] },
71
+ collectedAt: new Date().toISOString(),
72
+ };
73
+ try {
74
+ context = await collectBolloonContext({ cwd: opts.cwd ?? process.cwd() });
75
+ console.log(`[bootstrap] context 收集完成: ${context.judgmentsSummary.total} judgments, ${context.skills.length} skills`);
76
+ } catch (err) {
77
+ errors.push(`context: ${(err as Error).message}`);
78
+ console.warn('[bootstrap] context 收集失败 (非致命):', err);
79
+ }
80
+
81
+ // 3. 挂定时任务 (每天 0:00 跑扫描, server 重启时丢失可接受)
82
+ try {
83
+ scheduleAdaptiveScanDaily();
84
+ console.log('[bootstrap] 定时任务已挂: 每天 0:00 跑类 B 扫描');
85
+ } catch (err) {
86
+ errors.push(`schedule: ${(err as Error).message}`);
87
+ console.warn('[bootstrap] 定时任务挂载失败 (非致命):', err);
88
+ }
89
+
90
+ const durationMs = Date.now() - start;
91
+ console.log(`[bootstrap] 完成 (${durationMs}ms, ${errors.length} 个错误)`);
92
+
93
+ return { context, scanResult, durationMs, errors };
94
+ }
95
+
96
+ // ============================================================
97
+ // 定时任务: 每天 0:00 跑类 B 自适应扫描
98
+ // ============================================================
99
+
100
+ let scheduled = false;
101
+
102
+ function scheduleAdaptiveScanDaily(): void {
103
+ if (scheduled) return;
104
+ scheduled = true;
105
+
106
+ const now = new Date();
107
+ const next = new Date(now);
108
+ next.setHours(24, 0, 0, 0);
109
+ const msUntilMidnight = next.getTime() - now.getTime();
110
+
111
+ // 第一次: 等到明天 0:00
112
+ setTimeout(() => {
113
+ runAdaptiveScan().then((result) => {
114
+ console.log(`[bootstrap] 定时扫描完成: ${result.suggestions.length} 条建议`);
115
+ }).catch((err) => {
116
+ console.warn('[bootstrap] 定时扫描失败:', err);
117
+ });
118
+ // 之后: 每 24h
119
+ setInterval(() => {
120
+ runAdaptiveScan().then((result) => {
121
+ console.log(`[bootstrap] 定时扫描完成: ${result.suggestions.length} 条建议`);
122
+ }).catch((err) => {
123
+ console.warn('[bootstrap] 定时扫描失败:', err);
124
+ });
125
+ }, 24 * 60 * 60 * 1000);
126
+ }, msUntilMidnight);
127
+ }
128
+
129
+ /** 测试辅助: 重置 scheduled 标志 */
130
+ export function _resetScheduleForTest(): void {
131
+ scheduled = false;
132
+ }