@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.
- package/.auto-evolve-calls +1 -0
- package/.last-auto-evolve-baseline +1 -0
- package/Bolloon.md +103 -0
- package/dist/agents/pi-sdk.js +264 -12
- package/dist/bootstrap/bootstrap.js +114 -0
- package/dist/bootstrap/context-collector.js +296 -0
- package/dist/bootstrap/lifecycle-hooks.js +109 -0
- package/dist/bootstrap/project-context.js +151 -0
- package/dist/index.js +11 -0
- package/dist/llm/pi-ai.js +31 -21
- package/dist/pi-ecosystem-judgment/adaptive-scan.js +231 -0
- package/dist/pi-ecosystem-judgment/causal-judge.js +449 -0
- package/dist/pi-ecosystem-judgment/detect-hook.js +168 -0
- package/dist/pi-ecosystem-judgment/distill-prompt.js +226 -0
- package/dist/pi-ecosystem-judgment/evolve-judgment.js +170 -0
- package/dist/pi-ecosystem-judgment/human-value-pipeline.js +21 -0
- package/dist/pi-ecosystem-judgment/human-value-store.js +283 -22
- package/dist/pi-ecosystem-judgment/injection-gate.js +166 -0
- package/dist/pi-ecosystem-judgment/monitor-gate.js +188 -0
- package/dist/security/builtin-guards.js +124 -0
- package/dist/security/context-router-tool.js +106 -0
- package/dist/security/react-harness.js +143 -0
- package/dist/security/tool-gate.js +235 -0
- package/dist/utils/auto-evolve-policy.js +117 -0
- package/dist/utils/clamp.js +7 -0
- package/dist/utils/double.js +6 -0
- package/dist/web/client.js +668 -204
- package/dist/web/index.html +24 -4
- package/dist/web/server.js +531 -10
- package/lefthook.yml +29 -0
- package/package.json +3 -2
- package/scripts/auto-evolve-loop.ts +376 -0
- package/scripts/auto-evolve-oneshot.sh +155 -0
- package/scripts/auto-evolve-snapshot.sh +136 -0
- package/scripts/detect-schema-changes.sh +48 -0
- package/scripts/diff-reviewer.ts +159 -0
- package/scripts/weekly-report.ts +364 -0
- package/src/agents/pi-sdk.ts +293 -15
- package/src/bootstrap/bootstrap.ts +132 -0
- package/src/bootstrap/context-collector.ts +342 -0
- package/src/bootstrap/lifecycle-hooks.ts +176 -0
- package/src/bootstrap/project-context.ts +163 -0
- package/src/index.ts +11 -0
- package/src/llm/pi-ai.ts +33 -22
- package/src/security/builtin-guards.ts +162 -0
- package/src/security/context-router-tool.ts +122 -0
- package/src/security/react-harness.ts +177 -0
- package/src/security/tool-gate.ts +294 -0
- package/src/utils/auto-evolve-policy.ts +138 -0
- package/src/utils/clamp.ts +5 -0
- package/src/web/client.js +668 -204
- package/src/web/index.html +24 -4
- package/src/web/server.ts +596 -10
- package/staging/auto-evolve/clean-001/.review-verdict +9 -0
- package/staging/auto-evolve/clean-001/clean-001.patch +14 -0
- package/staging/auto-evolve/e2e-001/.patch-id +1 -0
- package/staging/auto-evolve/e2e-001/.review-verdict +12 -0
- package/staging/auto-evolve/e2e-001/e2e-001.patch +11 -0
- package/staging/auto-evolve/test-bad/.review-verdict +12 -0
- package/staging/auto-evolve/test-bad/test-bad.patch +11 -0
package/src/agents/pi-sdk.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
+
}
|