@bolloon/bolloon-agent 0.3.9 → 0.3.10
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/dist/agents/pi-sdk.js +26 -5
- package/dist/bootstrap/exhaust-scrubber.js +233 -0
- package/dist/bootstrap/memory-compressor.js +11 -0
- package/dist/pi-ecosystem-judgment/injection-gate.js +85 -1
- package/dist/web/client.js +32 -10
- package/dist/web/index.html +47 -23
- package/dist/web/routes-judgments.js +8 -2
- package/dist/web/server.js +11 -0
- package/dist/web/style.css +30 -0
- package/package.json +1 -1
package/dist/agents/pi-sdk.js
CHANGED
|
@@ -31,7 +31,8 @@ import { registerBuiltinTools, registerWalletTools, setupInboxListener, Idempote
|
|
|
31
31
|
export { createAgentSession, getAgentSession, resetAgentSession, runSelfImproveLoop, } from './pi-sdk-session-factory.js';
|
|
32
32
|
// Judgment 注入门 (P0): 在主对话 LLM 调起前自动拼入 Top 3 判断力
|
|
33
33
|
// 失败静默, 不阻塞主对话
|
|
34
|
-
import { injectJudgmentGate, recordJudgmentUsage } from '../pi-ecosystem-judgment/injection-gate.js';
|
|
34
|
+
import { injectJudgmentGate, injectNegativeGuard, recordJudgmentUsage } from '../pi-ecosystem-judgment/injection-gate.js';
|
|
35
|
+
import { getInjectionMaxChars } from '../bootstrap/exhaust-scrubber.js';
|
|
35
36
|
// 持续监控门 (P3): AI 回复后审计是否违反原则
|
|
36
37
|
import { monitorAfterReply } from '../pi-ecosystem-judgment/monitor-gate.js';
|
|
37
38
|
// Bootstrap 生命周期 hook (SessionStart / Stop / PreToolUse)
|
|
@@ -109,6 +110,8 @@ export class PiAgentSession {
|
|
|
109
110
|
*/
|
|
110
111
|
judgmentGateAddition = '';
|
|
111
112
|
judgmentGateUsedIds = [];
|
|
113
|
+
/** 2026-07-22 设计 B: 负向判断力 (避免清单) 注入用到的 judgment id */
|
|
114
|
+
judgmentGateNegativeUsedIds = [];
|
|
112
115
|
// 2026-06-18: 来自 web server markedPrompt 外的 contextHint (channel/judgment/distill/remote channels),
|
|
113
116
|
// 拼到 systemPrompt 末尾, 别再混进 user message
|
|
114
117
|
contextHintAddition = '';
|
|
@@ -147,11 +150,25 @@ export class PiAgentSession {
|
|
|
147
150
|
try {
|
|
148
151
|
// P-Action 4 (2026-06-15) 路径 1 整合: 透传 maxChars=1500 (≈ 375 tokens 硬上限)
|
|
149
152
|
// 路径 2/3 检测由 injection-gate 内部 alreadyInjectedSources 处理 (目前 assembleSystemPrompt 还没注入 value-store 标记, 所以这里不传)
|
|
150
|
-
|
|
153
|
+
// 2026-07-22 设计 C: maxChars 读背压动态值 (涡轮增压进气调参)
|
|
154
|
+
// 上下文紧张 (high) → 收紧 800; 宽裕 (idle/low) → 放宽 1800; 默认 medium 1500
|
|
155
|
+
const gate = await injectJudgmentGate(input, {}, { maxChars: getInjectionMaxChars() });
|
|
151
156
|
this.judgmentGateAddition = gate.systemAddition;
|
|
152
157
|
this.judgmentGateUsedIds = gate.usedIds;
|
|
153
|
-
|
|
154
|
-
|
|
158
|
+
// 2026-07-22 设计 B: 负向判断力回收 — "避免清单"注入 (显式, 进 prompt)
|
|
159
|
+
// 判断力负向是"判断力"非"废气", 可进 prompt 作为约束 (精准 = 正向指引 + 负向避免)
|
|
160
|
+
try {
|
|
161
|
+
const neg = await injectNegativeGuard(input, {}, { maxChars: 300 });
|
|
162
|
+
if (neg.didInject && neg.systemAddition) {
|
|
163
|
+
this.judgmentGateAddition += '\n' + neg.systemAddition;
|
|
164
|
+
this.judgmentGateNegativeUsedIds = neg.usedIds;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch (negErr) {
|
|
168
|
+
console.warn('[PiAgent] negative guard failed (non-fatal):', negErr);
|
|
169
|
+
}
|
|
170
|
+
if (this.judgmentGateUsedIds.length > 0 || this.judgmentGateNegativeUsedIds.length > 0) {
|
|
171
|
+
safePhase('gate_done', { usedCount: this.judgmentGateUsedIds.length, negativeCount: this.judgmentGateNegativeUsedIds.length, didInject: gate.didInject, skipReason: gate.skipReason });
|
|
155
172
|
}
|
|
156
173
|
}
|
|
157
174
|
catch (err) {
|
|
@@ -163,6 +180,7 @@ export class PiAgentSession {
|
|
|
163
180
|
clearJudgmentGate() {
|
|
164
181
|
this.judgmentGateAddition = '';
|
|
165
182
|
this.judgmentGateUsedIds = [];
|
|
183
|
+
this.judgmentGateNegativeUsedIds = [];
|
|
166
184
|
}
|
|
167
185
|
constructor(config) {
|
|
168
186
|
this.cwd = config.cwd;
|
|
@@ -491,7 +509,10 @@ export class PiAgentSession {
|
|
|
491
509
|
}
|
|
492
510
|
finally {
|
|
493
511
|
if (this.judgmentGateUsedIds.length > 0) {
|
|
494
|
-
recordJudgmentUsage(this.judgmentGateUsedIds, { userInput: input }).catch((err) => console.warn('[PiAgent] recordJudgmentUsage failed:', err));
|
|
512
|
+
recordJudgmentUsage(this.judgmentGateUsedIds, { userInput: input, polarity: 'positive' }).catch((err) => console.warn('[PiAgent] recordJudgmentUsage failed:', err));
|
|
513
|
+
}
|
|
514
|
+
if (this.judgmentGateNegativeUsedIds.length > 0) {
|
|
515
|
+
recordJudgmentUsage(this.judgmentGateNegativeUsedIds, { userInput: input, polarity: 'negative' }).catch((err) => console.warn('[PiAgent] recordJudgmentUsage (negative) failed:', err));
|
|
495
516
|
}
|
|
496
517
|
this.clearJudgmentGate();
|
|
497
518
|
this.currentSignal = null;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* exhaust-scrubber.ts — 上下文废气涡轮 (2026-07-22 设计 C)
|
|
3
|
+
*
|
|
4
|
+
* 思想锚点: 涡轮增压 (Turbocharger)
|
|
5
|
+
* 排气 (废气) = session-window dropped / memory-compressor skipped / compaction stage drops / truncation
|
|
6
|
+
* 涡轮 (本模块) = 采样丢弃事件, 聚合成"背压"指标
|
|
7
|
+
* 进气增压 = 背压反向调进气侧参数 (压缩阈值 / 检索 top-k / judgment 注入 maxChars)
|
|
8
|
+
* 燃烧室 (prompt) = 废气**不进**这里 (保持精准), 只让压力调参
|
|
9
|
+
*
|
|
10
|
+
* 拍板: 上下文废气 → 不进 prompt, 只调参, 进 log/memory, 隐式
|
|
11
|
+
*
|
|
12
|
+
* 三个职责:
|
|
13
|
+
* 1. recordExhaust(event): 采样丢弃事件 → 环形缓冲 + 落盘 ~/.bolloon/engine/backpressure.jsonl (log)
|
|
14
|
+
* 2. getBackpressure(): 算背压等级 (idle/low/medium/high) — 进气侧读它调参
|
|
15
|
+
* 3. getInjectionMaxChars(level): 背压 → judgment 注入 maxChars 映射 (进气增压)
|
|
16
|
+
* 4. maybeWriteExhaustMemory(): 背压高峰持续 → 模板摘要写 memory (月度滚动, 不调 LLM)
|
|
17
|
+
*
|
|
18
|
+
* 设计原则:
|
|
19
|
+
* - 零新数据源: 只订阅已有丢弃事件
|
|
20
|
+
* - 隐式: 用户看不到废气内容, 只看到压力等级 (可选背压表)
|
|
21
|
+
* - 静默: 任何失败 console.warn 不阻塞主流程
|
|
22
|
+
* - 不存原文: 只存 source + reason + 估算 token 数 (防隐私/膨胀)
|
|
23
|
+
*/
|
|
24
|
+
import * as fs from 'fs/promises';
|
|
25
|
+
import * as os from 'os';
|
|
26
|
+
import * as path from 'path';
|
|
27
|
+
// ============== 路径 ==============
|
|
28
|
+
function getEngineDir(home) {
|
|
29
|
+
return path.join(home || os.homedir(), '.bolloon', 'engine');
|
|
30
|
+
}
|
|
31
|
+
function getBackpressureLogPath(home) {
|
|
32
|
+
return path.join(getEngineDir(home), 'backpressure.jsonl');
|
|
33
|
+
}
|
|
34
|
+
function getMemoryEngineDir(agentId, home) {
|
|
35
|
+
// 跟 memory-compressor.ts 一致: ~/.bolloon/memory/<agentId>/engine/
|
|
36
|
+
const safe = agentId.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
|
|
37
|
+
return path.join(home || os.homedir(), '.bolloon', 'memory', safe, 'engine');
|
|
38
|
+
}
|
|
39
|
+
// ============== 状态 (模块级单例, 跟 chat-archiver 同模式) ==============
|
|
40
|
+
const RING_CAPACITY = 100;
|
|
41
|
+
const ringBuffer = [];
|
|
42
|
+
let droppedTokensTotal = 0;
|
|
43
|
+
const bySource = {};
|
|
44
|
+
let monthlyHighCount = 0; // 当月 high 事件累计 (触发 memory 摘要用)
|
|
45
|
+
// ============== 采样 (涡轮入口) ==============
|
|
46
|
+
/**
|
|
47
|
+
* 记录一个废气事件. 推入环形缓冲 + 落盘 jsonl (log). 静默失败.
|
|
48
|
+
*
|
|
49
|
+
* @param event 废气事件 (source + reason + 可选 droppedTokens; ts 不传则自动填)
|
|
50
|
+
* @param home 可选 home 目录 (测试注入)
|
|
51
|
+
*/
|
|
52
|
+
export async function recordExhaust(event, home) {
|
|
53
|
+
try {
|
|
54
|
+
const full = {
|
|
55
|
+
ts: event.ts || new Date().toISOString(),
|
|
56
|
+
source: event.source,
|
|
57
|
+
reason: event.reason,
|
|
58
|
+
droppedTokens: event.droppedTokens,
|
|
59
|
+
};
|
|
60
|
+
// 环形缓冲
|
|
61
|
+
ringBuffer.push(full);
|
|
62
|
+
if (ringBuffer.length > RING_CAPACITY) {
|
|
63
|
+
ringBuffer.shift();
|
|
64
|
+
}
|
|
65
|
+
// 计数
|
|
66
|
+
if (full.droppedTokens && full.droppedTokens > 0) {
|
|
67
|
+
droppedTokensTotal += full.droppedTokens;
|
|
68
|
+
}
|
|
69
|
+
bySource[full.source] = (bySource[full.source] || 0) + 1;
|
|
70
|
+
// 落盘 jsonl (log) — append 模式, 跟 chat-archiver 同
|
|
71
|
+
const logPath = getBackpressureLogPath(home);
|
|
72
|
+
await fs.mkdir(path.dirname(logPath), { recursive: true });
|
|
73
|
+
await fs.appendFile(logPath, JSON.stringify(full) + '\n', 'utf-8');
|
|
74
|
+
// high 事件累计 → 触发 memory 摘要 (火忘)
|
|
75
|
+
const snap = getBackpressure(home);
|
|
76
|
+
if (snap.level === 'high') {
|
|
77
|
+
monthlyHighCount++;
|
|
78
|
+
// 每 10 次 high 触发一次 memory 摘要 (节流, 防频繁写盘)
|
|
79
|
+
if (monthlyHighCount % 10 === 0) {
|
|
80
|
+
maybeWriteExhaustMemorySummary('default', home).catch(() => { });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
console.warn('[exhaust-scrubber] recordExhaust failed (non-fatal):', err);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 同步版本 (供不方便 await 的调用方; 只更新内存环形缓冲, 不落盘).
|
|
90
|
+
* 落盘由下次 async recordExhaust 或显式 flush 触发.
|
|
91
|
+
*/
|
|
92
|
+
export function recordExhaustSync(event) {
|
|
93
|
+
try {
|
|
94
|
+
const full = {
|
|
95
|
+
ts: event.ts || new Date().toISOString(),
|
|
96
|
+
source: event.source,
|
|
97
|
+
reason: event.reason,
|
|
98
|
+
droppedTokens: event.droppedTokens,
|
|
99
|
+
};
|
|
100
|
+
ringBuffer.push(full);
|
|
101
|
+
if (ringBuffer.length > RING_CAPACITY)
|
|
102
|
+
ringBuffer.shift();
|
|
103
|
+
if (full.droppedTokens && full.droppedTokens > 0)
|
|
104
|
+
droppedTokensTotal += full.droppedTokens;
|
|
105
|
+
bySource[full.source] = (bySource[full.source] || 0) + 1;
|
|
106
|
+
}
|
|
107
|
+
catch { /* 静默 */ }
|
|
108
|
+
}
|
|
109
|
+
// ============== 聚合 (背压等级) ==============
|
|
110
|
+
/**
|
|
111
|
+
* 算当前背压快照. 基于环形缓冲 + 最近 60s 事件速率.
|
|
112
|
+
*
|
|
113
|
+
* 等级映射 (dropRatePerMin = 最近 60s 内事件数 / 60 * 60... 简化为最近 60s 事件数本身):
|
|
114
|
+
* 0 → idle
|
|
115
|
+
* 1-2 → low
|
|
116
|
+
* 3-10 → medium
|
|
117
|
+
* > 10 → high
|
|
118
|
+
*/
|
|
119
|
+
export function getBackpressure(_home) {
|
|
120
|
+
const now = Date.now();
|
|
121
|
+
const recent = ringBuffer.filter((e) => {
|
|
122
|
+
const t = Date.parse(e.ts);
|
|
123
|
+
return Number.isFinite(t) && now - t < 60_000; // 最近 60s
|
|
124
|
+
});
|
|
125
|
+
const dropRatePerMin = recent.length;
|
|
126
|
+
let level;
|
|
127
|
+
if (dropRatePerMin === 0)
|
|
128
|
+
level = 'idle';
|
|
129
|
+
else if (dropRatePerMin <= 2)
|
|
130
|
+
level = 'low';
|
|
131
|
+
else if (dropRatePerMin <= 10)
|
|
132
|
+
level = 'medium';
|
|
133
|
+
else
|
|
134
|
+
level = 'high';
|
|
135
|
+
const srcCount = {};
|
|
136
|
+
for (const e of ringBuffer) {
|
|
137
|
+
srcCount[e.source] = (srcCount[e.source] || 0) + 1;
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
level,
|
|
141
|
+
dropCount: ringBuffer.length,
|
|
142
|
+
droppedTokensTotal,
|
|
143
|
+
dropRatePerMin,
|
|
144
|
+
lastTs: ringBuffer.length > 0 ? ringBuffer[ringBuffer.length - 1].ts : '',
|
|
145
|
+
bySource: srcCount,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
export function getPressureLevel(home) {
|
|
149
|
+
return getBackpressure(home).level;
|
|
150
|
+
}
|
|
151
|
+
// ============== 进气增压 (背压 → 调参) ==============
|
|
152
|
+
/**
|
|
153
|
+
* 背压 → judgment 注入 maxChars 映射 (进气增压核心).
|
|
154
|
+
*
|
|
155
|
+
* idle/low → 1800 (放宽注入, 上下文宽裕)
|
|
156
|
+
* medium → 1500 (默认, 现状)
|
|
157
|
+
* high → 800 (收紧注入, 上下文紧张, 留空间给主任务)
|
|
158
|
+
*
|
|
159
|
+
* 调用方: pi-sdk.ts computeJudgmentGate 的 maxChars 从固定 1500 改为读这个.
|
|
160
|
+
*/
|
|
161
|
+
export function getInjectionMaxChars(level, home) {
|
|
162
|
+
const lvl = level ?? getPressureLevel(home);
|
|
163
|
+
switch (lvl) {
|
|
164
|
+
case 'idle':
|
|
165
|
+
case 'low':
|
|
166
|
+
return 1800;
|
|
167
|
+
case 'medium':
|
|
168
|
+
return 1500;
|
|
169
|
+
case 'high':
|
|
170
|
+
return 800;
|
|
171
|
+
default:
|
|
172
|
+
return 1500;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* 背压 → 检索 top-k 映射 (进气增压, 检索侧).
|
|
177
|
+
* idle/low → 8, medium → 5, high → 3
|
|
178
|
+
*/
|
|
179
|
+
export function getRetrievalTopK(level, home) {
|
|
180
|
+
const lvl = level ?? getPressureLevel(home);
|
|
181
|
+
switch (lvl) {
|
|
182
|
+
case 'idle':
|
|
183
|
+
case 'low':
|
|
184
|
+
return 8;
|
|
185
|
+
case 'medium':
|
|
186
|
+
return 5;
|
|
187
|
+
case 'high':
|
|
188
|
+
return 3;
|
|
189
|
+
default:
|
|
190
|
+
return 5;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// ============== memory 落地 (月度摘要, 拍板要求"进 memory") ==============
|
|
194
|
+
/**
|
|
195
|
+
* 背压高峰持续 → 模板摘要写 memory (月度滚动, 不调 LLM).
|
|
196
|
+
* ~/.bolloon/memory/<agentId>/engine/exhaust-<YYYY-MM>.summary.md (append)
|
|
197
|
+
*
|
|
198
|
+
* 让"为什么这段时间上下文一直紧张"沉淀进 memory 供 agent 回看.
|
|
199
|
+
* 节流: 每 10 次 high 触发一次 (recordExhaust 内控制).
|
|
200
|
+
*/
|
|
201
|
+
export async function maybeWriteExhaustMemorySummary(agentId, home) {
|
|
202
|
+
try {
|
|
203
|
+
const snap = getBackpressure(home);
|
|
204
|
+
if (snap.level !== 'high')
|
|
205
|
+
return { written: false };
|
|
206
|
+
const now = new Date();
|
|
207
|
+
const yearMonth = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, '0')}`;
|
|
208
|
+
const dir = getMemoryEngineDir(agentId, home);
|
|
209
|
+
const file = path.join(dir, `exhaust-${yearMonth}.summary.md`);
|
|
210
|
+
await fs.mkdir(dir, { recursive: true });
|
|
211
|
+
const block = `\n\n---\n\n## 引擎背压高峰 @ ${now.toISOString()} (level=high)\n\n` +
|
|
212
|
+
`- 最近 60s 丢弃事件: ${snap.dropRatePerMin} 次/min\n` +
|
|
213
|
+
`- 环形缓冲事件总数: ${snap.dropCount}\n` +
|
|
214
|
+
`- 估算丢弃 token 累计: ${snap.droppedTokensTotal}\n` +
|
|
215
|
+
`- 按 source 分布: ${Object.entries(snap.bySource).map(([k, v]) => `${k}=${v}`).join(', ') || '(无)'}\n` +
|
|
216
|
+
`- 含义: 上下文持续紧张, 进气侧已自动收紧 (judgment 注入 maxChars=800, 检索 top-k=3)\n`;
|
|
217
|
+
await fs.appendFile(file, block, 'utf-8');
|
|
218
|
+
return { written: true, path: file };
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
console.warn('[exhaust-scrubber] maybeWriteExhaustMemorySummary failed (non-fatal):', err);
|
|
222
|
+
return { written: false };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// ============== 测试辅助 ==============
|
|
226
|
+
/** 重置模块状态 (仅测试用) */
|
|
227
|
+
export function __resetForTest() {
|
|
228
|
+
ringBuffer.length = 0;
|
|
229
|
+
droppedTokensTotal = 0;
|
|
230
|
+
for (const k of Object.keys(bySource))
|
|
231
|
+
delete bySource[k];
|
|
232
|
+
monthlyHighCount = 0;
|
|
233
|
+
}
|
|
@@ -161,6 +161,17 @@ export async function compressSessionToMemory(opts) {
|
|
|
161
161
|
await fs.mkdir(path.dirname(summaryPath), { recursive: true });
|
|
162
162
|
await fs.appendFile(summaryPath, block, 'utf-8');
|
|
163
163
|
await writeCursor(cursorPath, allMessages.length);
|
|
164
|
+
// 2026-07-22 设计 C: 废气采样 — 压缩成功 = 上下文需要压缩的信号, 记入涡轮 (隐式)
|
|
165
|
+
// 废气不进 prompt, 只调参 (背压高 → judgment 注入收紧). 落 log/memory.
|
|
166
|
+
try {
|
|
167
|
+
const { recordExhaust } = await import('./exhaust-scrubber.js');
|
|
168
|
+
recordExhaust({
|
|
169
|
+
source: 'memory-compressor',
|
|
170
|
+
reason: 'compress-summary-written',
|
|
171
|
+
droppedTokens: newMessages.length * 200, // 粗估 200 tokens/msg
|
|
172
|
+
}, opts.home).catch(() => { });
|
|
173
|
+
}
|
|
174
|
+
catch { /* 静默 */ }
|
|
164
175
|
return {
|
|
165
176
|
summaryPath,
|
|
166
177
|
cursorPath,
|
|
@@ -138,7 +138,7 @@ function formatInjection(values, mode, resolvedCount, maxChars = 1500) {
|
|
|
138
138
|
// 使用记录 (回溯): AI 实际"用了"哪些判断力
|
|
139
139
|
// ============================================================
|
|
140
140
|
const USAGE_LOG = (os.homedir() || '/tmp') + '/.bolloon/human-values/usage.jsonl';
|
|
141
|
-
export async function recordJudgmentUsage(usedIds, meta) {
|
|
141
|
+
export async function recordJudgmentUsage(usedIds, meta = {}) {
|
|
142
142
|
if (usedIds.length === 0)
|
|
143
143
|
return;
|
|
144
144
|
try {
|
|
@@ -147,6 +147,7 @@ export async function recordJudgmentUsage(usedIds, meta) {
|
|
|
147
147
|
channelId: meta.channelId ?? null,
|
|
148
148
|
userInputPreview: (meta.userInput ?? '').substring(0, 80),
|
|
149
149
|
usedIds,
|
|
150
|
+
polarity: meta.polarity ?? 'positive',
|
|
150
151
|
};
|
|
151
152
|
await fs.appendFile(USAGE_LOG, JSON.stringify(entry) + '\n', 'utf-8');
|
|
152
153
|
}
|
|
@@ -154,6 +155,89 @@ export async function recordJudgmentUsage(usedIds, meta) {
|
|
|
154
155
|
console.warn('[injection-gate] recordJudgmentUsage failed:', err);
|
|
155
156
|
}
|
|
156
157
|
}
|
|
158
|
+
// ============================================================
|
|
159
|
+
// 2026-07-22 设计 B: 负向判断力回收 — "避免清单"注入 (显式, 进 prompt)
|
|
160
|
+
//
|
|
161
|
+
// 涡轮增压锚点: 判断力的负向是"判断力"不是"上下文废气", 可进 prompt 作为约束
|
|
162
|
+
// (精准 = 正向指引 + 负向避免). 从 reject 类 + 高 stakes + 高 confidence 选 Top N,
|
|
163
|
+
// 以"避免清单"语义产出 systemAddition. maxChars=300 (远小于正向 1500, 防噪音).
|
|
164
|
+
// ============================================================
|
|
165
|
+
export const DEFAULT_NEGATIVE_CONFIG = {
|
|
166
|
+
topN: 3,
|
|
167
|
+
mode: 'concise',
|
|
168
|
+
skip: false,
|
|
169
|
+
maxChars: 300,
|
|
170
|
+
/** 最低置信度门槛 (只注入足够可信的否决) */
|
|
171
|
+
minConfidence: 0.7,
|
|
172
|
+
};
|
|
173
|
+
function emptyGateResult(skipReason) {
|
|
174
|
+
return { systemAddition: '', usedIds: [], matchedCount: 0, didInject: false, skipReason };
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* 负向判断力注入门: 给定用户输入, 返回"避免清单"追加文本 + 用到的负向 judgment id.
|
|
178
|
+
*
|
|
179
|
+
* 筛选: decision_type='reject' && status='active' && stakes∈{high,critical} && confidence>=minConfidence
|
|
180
|
+
* 排序: critical 优先, 再按 confidence desc
|
|
181
|
+
* 静默: 任意步骤失败返回空字符串, 不 throw (主对话不阻塞)
|
|
182
|
+
*/
|
|
183
|
+
export async function injectNegativeGuard(userInput, _ctx = {}, options = {}) {
|
|
184
|
+
const cfg = { ...DEFAULT_NEGATIVE_CONFIG, ...options };
|
|
185
|
+
if (cfg.skip)
|
|
186
|
+
return emptyGateResult('skip');
|
|
187
|
+
if (!userInput || userInput.trim().length === 0)
|
|
188
|
+
return emptyGateResult('no-input');
|
|
189
|
+
try {
|
|
190
|
+
const all = await loadAllJudgments();
|
|
191
|
+
const negatives = all.filter((j) => j.decision_type === 'reject' &&
|
|
192
|
+
(j.status ?? 'active') === 'active' &&
|
|
193
|
+
(j.context?.stakes === 'high' || j.context?.stakes === 'critical') &&
|
|
194
|
+
(j.metadata?.confidence ?? 0.5) >= (cfg.minConfidence ?? 0.7));
|
|
195
|
+
if (negatives.length === 0)
|
|
196
|
+
return emptyGateResult('empty-negatives');
|
|
197
|
+
// 排序: critical > high, 再按 confidence desc
|
|
198
|
+
negatives.sort((a, b) => {
|
|
199
|
+
const sa = a.context?.stakes === 'critical' ? 2 : 1;
|
|
200
|
+
const sb = b.context?.stakes === 'critical' ? 2 : 1;
|
|
201
|
+
const ca = a.metadata?.confidence ?? 0.5;
|
|
202
|
+
const cb = b.metadata?.confidence ?? 0.5;
|
|
203
|
+
return (sb - sa) || (cb - ca);
|
|
204
|
+
});
|
|
205
|
+
const top = negatives.slice(0, cfg.topN);
|
|
206
|
+
const usedIds = top.map((j) => j.id);
|
|
207
|
+
const systemAddition = formatNegativeInjection(top, cfg.maxChars);
|
|
208
|
+
return {
|
|
209
|
+
systemAddition,
|
|
210
|
+
usedIds,
|
|
211
|
+
matchedCount: negatives.length,
|
|
212
|
+
didInject: true,
|
|
213
|
+
skipReason: null,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
console.warn('[injection-gate] injectNegativeGuard failed (silent fallback):', err);
|
|
218
|
+
return emptyGateResult('exception');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function formatNegativeInjection(items, maxChars) {
|
|
222
|
+
if (items.length === 0)
|
|
223
|
+
return '';
|
|
224
|
+
const SOURCE_TAG = '<!-- source: injection-gate (negative) -->';
|
|
225
|
+
const lines = items.map((j, i) => {
|
|
226
|
+
const stakes = j.context?.stakes === 'critical' ? ' [关键风险]' : ' [高风险]';
|
|
227
|
+
const decision = (j.decision || '').slice(0, 80);
|
|
228
|
+
return `${i + 1}. 避免: ${decision}${stakes}`;
|
|
229
|
+
});
|
|
230
|
+
let result = `${SOURCE_TAG}\n` +
|
|
231
|
+
`# 避免清单 (负向判断力, 自动注入)\n` +
|
|
232
|
+
`- 以下行为已被明确否决, 执行时主动规避; 如情境冲突在回复中说明\n` +
|
|
233
|
+
`${lines.join('\n')}\n`;
|
|
234
|
+
if (maxChars > 0 && result.length > maxChars) {
|
|
235
|
+
result =
|
|
236
|
+
result.substring(0, maxChars) +
|
|
237
|
+
'\n[System Note: 避免清单因长度截断, 这是背景约束, 不影响用户实际请求.]\n';
|
|
238
|
+
}
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
157
241
|
/**
|
|
158
242
|
* 给定 channelId, 取最近 N 条 usage 记录 (UI 显示用)
|
|
159
243
|
*/
|
package/dist/web/client.js
CHANGED
|
@@ -3438,8 +3438,16 @@ ${data.error || "channel not found"}`, "error");
|
|
|
3438
3438
|
});
|
|
3439
3439
|
renderJudgments(lastJudgmentsCache);
|
|
3440
3440
|
}
|
|
3441
|
+
function switchPolarity(polarity) {
|
|
3442
|
+
currentPolarity = polarity;
|
|
3443
|
+
currentAdvancedFilter = null;
|
|
3444
|
+
document.querySelectorAll(".judgment-polarity-tab").forEach((btn) => {
|
|
3445
|
+
btn.classList.toggle("active", btn.dataset.polarity === polarity);
|
|
3446
|
+
});
|
|
3447
|
+
loadJudgments();
|
|
3448
|
+
}
|
|
3441
3449
|
function switchStatusFilter(status) {
|
|
3442
|
-
|
|
3450
|
+
currentAdvancedFilter = status;
|
|
3443
3451
|
document.querySelectorAll(".judgment-status-tab").forEach((btn) => {
|
|
3444
3452
|
btn.classList.toggle("active", btn.dataset.status === status);
|
|
3445
3453
|
});
|
|
@@ -3464,7 +3472,8 @@ ${data.error || "channel not found"}`, "error");
|
|
|
3464
3472
|
if (judgmentsModal) judgmentsModal.classList.remove("active");
|
|
3465
3473
|
}
|
|
3466
3474
|
var currentJudgmentTab = "channel";
|
|
3467
|
-
var
|
|
3475
|
+
var currentPolarity = "positive";
|
|
3476
|
+
var currentAdvancedFilter = null;
|
|
3468
3477
|
var lastJudgmentsCache = [];
|
|
3469
3478
|
function renderJudgments(items) {
|
|
3470
3479
|
if (!judgmentsList) return;
|
|
@@ -3554,7 +3563,7 @@ ${data.error || "channel not found"}`, "error");
|
|
|
3554
3563
|
async function loadJudgments() {
|
|
3555
3564
|
if (!judgmentsList) return;
|
|
3556
3565
|
try {
|
|
3557
|
-
if (
|
|
3566
|
+
if (currentAdvancedFilter === "violations") {
|
|
3558
3567
|
const res2 = await fetch("/api/judgments/violations?limit=50");
|
|
3559
3568
|
if (!res2.ok) throw new Error("HTTP " + res2.status);
|
|
3560
3569
|
const data2 = await res2.json();
|
|
@@ -3562,7 +3571,7 @@ ${data.error || "channel not found"}`, "error");
|
|
|
3562
3571
|
judgmentsLoaded = true;
|
|
3563
3572
|
return;
|
|
3564
3573
|
}
|
|
3565
|
-
if (
|
|
3574
|
+
if (currentAdvancedFilter === "adaptive") {
|
|
3566
3575
|
const res2 = await fetch("/api/judgments/adaptive-suggestions");
|
|
3567
3576
|
if (!res2.ok) throw new Error("HTTP " + res2.status);
|
|
3568
3577
|
const data2 = await res2.json();
|
|
@@ -3570,7 +3579,7 @@ ${data.error || "channel not found"}`, "error");
|
|
|
3570
3579
|
judgmentsLoaded = true;
|
|
3571
3580
|
return;
|
|
3572
3581
|
}
|
|
3573
|
-
if (
|
|
3582
|
+
if (currentAdvancedFilter === "causal") {
|
|
3574
3583
|
const res2 = await fetch("/api/judgments/causal/correlation?topN=10");
|
|
3575
3584
|
if (!res2.ok) throw new Error("HTTP " + res2.status);
|
|
3576
3585
|
const data2 = await res2.json();
|
|
@@ -3578,17 +3587,25 @@ ${data.error || "channel not found"}`, "error");
|
|
|
3578
3587
|
judgmentsLoaded = true;
|
|
3579
3588
|
return;
|
|
3580
3589
|
}
|
|
3581
|
-
const
|
|
3590
|
+
const POSITIVE_TYPES = ["approve", "modify", "escalate"];
|
|
3591
|
+
const fetchStatus = currentPolarity === "positive" ? "active" : "all";
|
|
3592
|
+
const res = await fetch("/api/judgments?status=" + encodeURIComponent(fetchStatus));
|
|
3582
3593
|
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
3583
3594
|
const data = await res.json();
|
|
3584
|
-
|
|
3585
|
-
|
|
3595
|
+
let list = data.judgments || [];
|
|
3596
|
+
if (currentPolarity === "positive") {
|
|
3597
|
+
list = list.filter((j) => POSITIVE_TYPES.includes(j.decision_type) && (j.status ?? "active") === "active");
|
|
3598
|
+
} else {
|
|
3599
|
+
list = list.filter((j) => j.decision_type === "reject" || ["rejected", "superseded"].includes(j.status ?? ""));
|
|
3600
|
+
}
|
|
3601
|
+
lastJudgmentsCache = list;
|
|
3602
|
+
renderJudgments(list);
|
|
3586
3603
|
if (judgmentsBadge) {
|
|
3587
3604
|
let activeCount;
|
|
3588
|
-
if (
|
|
3605
|
+
if (currentPolarity === "positive") {
|
|
3589
3606
|
activeCount = data.count;
|
|
3590
3607
|
} else {
|
|
3591
|
-
activeCount =
|
|
3608
|
+
activeCount = (data.judgments || []).filter((j) => POSITIVE_TYPES.includes(j.decision_type) && (j.status ?? "active") === "active").length;
|
|
3592
3609
|
}
|
|
3593
3610
|
if (activeCount > 0) {
|
|
3594
3611
|
judgmentsBadge.textContent = activeCount;
|
|
@@ -3863,6 +3880,9 @@ ${data.error || "channel not found"}`, "error");
|
|
|
3863
3880
|
document.querySelectorAll(".judgment-tab").forEach((btn) => {
|
|
3864
3881
|
btn.addEventListener("click", () => switchJudgmentTab(btn.dataset.tab));
|
|
3865
3882
|
});
|
|
3883
|
+
document.querySelectorAll(".judgment-polarity-tab").forEach((btn) => {
|
|
3884
|
+
btn.addEventListener("click", () => switchPolarity(btn.dataset.polarity));
|
|
3885
|
+
});
|
|
3866
3886
|
document.querySelectorAll(".judgment-status-tab").forEach((btn) => {
|
|
3867
3887
|
btn.addEventListener("click", () => switchStatusFilter(btn.dataset.status));
|
|
3868
3888
|
});
|
|
@@ -3980,12 +4000,14 @@ ${data.error || "channel not found"}`, "error");
|
|
|
3980
4000
|
judgmentSubmitBtn.disabled = true;
|
|
3981
4001
|
if (judgmentError) judgmentError.style.display = "none";
|
|
3982
4002
|
try {
|
|
4003
|
+
const polarity = document.querySelector('input[name="judgment-polarity"]:checked')?.value || "positive";
|
|
3983
4004
|
const res = await fetch("/api/judgments", {
|
|
3984
4005
|
method: "POST",
|
|
3985
4006
|
headers: { "Content-Type": "application/json" },
|
|
3986
4007
|
body: JSON.stringify({
|
|
3987
4008
|
decision,
|
|
3988
4009
|
reason: reason || void 0,
|
|
4010
|
+
decision_type: polarity === "negative" ? "reject" : "approve",
|
|
3989
4011
|
context: { domain: judgmentDomain?.value, stakes: judgmentStakes?.value }
|
|
3990
4012
|
})
|
|
3991
4013
|
});
|
package/dist/web/index.html
CHANGED
|
@@ -205,21 +205,39 @@
|
|
|
205
205
|
<input type="text" id="judgment-reason" placeholder="例: 信任 Bolloon 的判断存储">
|
|
206
206
|
</div>
|
|
207
207
|
<div class="form-group judgment-form-row">
|
|
208
|
-
<label
|
|
209
|
-
<
|
|
210
|
-
<
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
<
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
<
|
|
221
|
-
<
|
|
222
|
-
|
|
208
|
+
<label>分类</label>
|
|
209
|
+
<div class="judgment-polarity-toggle" id="judgment-polarity-toggle">
|
|
210
|
+
<label class="polarity-opt active" data-polarity="positive">
|
|
211
|
+
<input type="radio" name="judgment-polarity" value="positive" checked>
|
|
212
|
+
<span>▲ 正向 <small>采纳</small></span>
|
|
213
|
+
</label>
|
|
214
|
+
<label class="polarity-opt" data-polarity="negative">
|
|
215
|
+
<input type="radio" name="judgment-polarity" value="negative">
|
|
216
|
+
<span>▼ 负向 <small>否决/避免</small></span>
|
|
217
|
+
</label>
|
|
218
|
+
</div>
|
|
219
|
+
<details class="judgment-advanced-fold" style="margin-left:auto;">
|
|
220
|
+
<summary style="font-size:11px;color:#6b7280;cursor:pointer;list-style:none;">高级 (领域/风险)</summary>
|
|
221
|
+
<div style="display:flex;gap:10px;padding:6px 0 0;flex-wrap:wrap;font-size:11px;">
|
|
222
|
+
<label>领域
|
|
223
|
+
<select id="judgment-domain">
|
|
224
|
+
<option value="general">general</option>
|
|
225
|
+
<option value="code">code</option>
|
|
226
|
+
<option value="architecture">architecture</option>
|
|
227
|
+
<option value="security">security</option>
|
|
228
|
+
<option value="testing">testing</option>
|
|
229
|
+
</select>
|
|
230
|
+
</label>
|
|
231
|
+
<label>风险
|
|
232
|
+
<select id="judgment-stakes">
|
|
233
|
+
<option value="medium">medium</option>
|
|
234
|
+
<option value="low">low</option>
|
|
235
|
+
<option value="high">high</option>
|
|
236
|
+
<option value="critical">critical</option>
|
|
237
|
+
</select>
|
|
238
|
+
</label>
|
|
239
|
+
</div>
|
|
240
|
+
</details>
|
|
223
241
|
</div>
|
|
224
242
|
<div class="btn-group">
|
|
225
243
|
<button id="judgment-submit-btn" class="btn-primary">记录</button>
|
|
@@ -235,15 +253,21 @@
|
|
|
235
253
|
</button>
|
|
236
254
|
<button class="judgment-tab" data-tab="global">全局</button>
|
|
237
255
|
</div>
|
|
238
|
-
<!--
|
|
256
|
+
<!-- 2026-07-22 简化: 正向 / 负向 两个主分类 (替换原 6 个 status tab)
|
|
257
|
+
正向 = 采纳类 (approve/modify/escalate), 会注入 prompt 复用
|
|
258
|
+
负向 = 否决/被推翻 (reject/rejected/superseded), 负向回收为避免清单
|
|
259
|
+
高级分析 (违规/自适应/因果) 折叠保留, 数据/API 不删 -->
|
|
239
260
|
<div id="judgments-status-filter" class="judgment-status-bar">
|
|
240
|
-
<
|
|
241
|
-
<button class="judgment-
|
|
242
|
-
<
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
261
|
+
<button class="judgment-polarity-tab active" data-polarity="positive" title="采纳类: approve/modify/escalate, 会注入 prompt 复用">▲ 正向</button>
|
|
262
|
+
<button class="judgment-polarity-tab" data-polarity="negative" title="否决/被推翻: reject/rejected/superseded, 负向回收为避免清单">▼ 负向</button>
|
|
263
|
+
<details class="judgment-advanced-fold" style="margin-left:auto;">
|
|
264
|
+
<summary style="font-size:11px;color:#6b7280;cursor:pointer;list-style:none;">⋯ 高级分析</summary>
|
|
265
|
+
<div style="display:flex;gap:4px;padding:6px 0 0;flex-wrap:wrap;">
|
|
266
|
+
<button class="judgment-status-tab" data-status="violations" style="font-size:11px;">违规记录</button>
|
|
267
|
+
<button class="judgment-status-tab" data-status="adaptive" style="font-size:11px;">📊 自适应</button>
|
|
268
|
+
<button class="judgment-status-tab" data-status="causal" style="font-size:11px;">🔍 因果分析</button>
|
|
269
|
+
</div>
|
|
270
|
+
</details>
|
|
247
271
|
</div>
|
|
248
272
|
<h3 class="judgment-list-header">
|
|
249
273
|
<span id="judgments-list-title">本 channel 的判断力</span>
|
|
@@ -36,15 +36,21 @@ export function registerJudgmentsRoutes(app) {
|
|
|
36
36
|
// 极简版: 只记录 decision + reason; 其它字段可选
|
|
37
37
|
app.post('/api/judgments', async (req, res) => {
|
|
38
38
|
try {
|
|
39
|
-
const { decision, reason, context } = req.body;
|
|
39
|
+
const { decision, reason, context, decision_type } = req.body;
|
|
40
40
|
if (!decision || typeof decision !== 'string' || !decision.trim()) {
|
|
41
41
|
return res.status(400).json({ error: 'decision required' });
|
|
42
42
|
}
|
|
43
|
+
// 2026-07-22: 接受前端 polarity toggle 传来的 decision_type (正=approve / 负=reject)
|
|
44
|
+
// 不传或非法 → 默认 approve (向后兼容)
|
|
45
|
+
const allowedTypes = ['approve', 'reject', 'modify', 'escalate'];
|
|
46
|
+
const finalType = decision_type && allowedTypes.includes(decision_type)
|
|
47
|
+
? decision_type
|
|
48
|
+
: 'approve';
|
|
43
49
|
const { storeHumanJudgment, initializeValueStore } = await import('../pi-ecosystem-judgment/human-value-store.js');
|
|
44
50
|
await initializeValueStore();
|
|
45
51
|
const j = await storeHumanJudgment({
|
|
46
52
|
decision: decision.trim(),
|
|
47
|
-
decision_type:
|
|
53
|
+
decision_type: finalType,
|
|
48
54
|
reasons: reason ? [reason.trim()] : [],
|
|
49
55
|
values_derived: [],
|
|
50
56
|
context: {
|
package/dist/web/server.js
CHANGED
|
@@ -4484,6 +4484,17 @@ ${goalDesc}
|
|
|
4484
4484
|
}
|
|
4485
4485
|
});
|
|
4486
4486
|
// 获取 iroh 节点信息
|
|
4487
|
+
// 2026-07-22 设计 C: 引擎背压 API (涡轮增压表, 隐式可观测 — 废气内容不暴露, 只展示压力等级)
|
|
4488
|
+
app.get('/api/engine/backpressure', async (_req, res) => {
|
|
4489
|
+
try {
|
|
4490
|
+
const { getBackpressure } = await import('../bootstrap/exhaust-scrubber.js');
|
|
4491
|
+
const snap = getBackpressure();
|
|
4492
|
+
res.json({ ok: true, ...snap });
|
|
4493
|
+
}
|
|
4494
|
+
catch (err) {
|
|
4495
|
+
res.status(500).json({ error: err.message });
|
|
4496
|
+
}
|
|
4497
|
+
});
|
|
4487
4498
|
app.get('/api/iroh/info', async (_req, res) => {
|
|
4488
4499
|
if (!irohInitialized || !irohNodeInfo) {
|
|
4489
4500
|
res.json({ initialized: false });
|
package/dist/web/style.css
CHANGED
|
@@ -1795,6 +1795,36 @@ body {
|
|
|
1795
1795
|
background: var(--accent);
|
|
1796
1796
|
color: var(--bg);
|
|
1797
1797
|
}
|
|
1798
|
+
|
|
1799
|
+
/* 2026-07-22 设计 A: 正向/负向主分类 tab + 表单 toggle */
|
|
1800
|
+
.judgment-polarity-tab {
|
|
1801
|
+
background: var(--bg-hover);
|
|
1802
|
+
color: var(--text-secondary);
|
|
1803
|
+
border: none;
|
|
1804
|
+
padding: 4px 14px;
|
|
1805
|
+
border-radius: 4px;
|
|
1806
|
+
cursor: pointer;
|
|
1807
|
+
font-size: 12px;
|
|
1808
|
+
font-weight: 600;
|
|
1809
|
+
transition: var(--transition);
|
|
1810
|
+
}
|
|
1811
|
+
.judgment-polarity-tab:hover { background: var(--bg-active); color: var(--text-primary); }
|
|
1812
|
+
.judgment-polarity-tab[data-polarity="positive"].active { background: #059669; color: #fff; }
|
|
1813
|
+
.judgment-polarity-tab[data-polarity="negative"].active { background: #dc2626; color: #fff; }
|
|
1814
|
+
.judgment-advanced-fold > summary::-webkit-details-marker { display: none; }
|
|
1815
|
+
|
|
1816
|
+
.judgment-polarity-toggle { display: flex; gap: 4px; }
|
|
1817
|
+
.polarity-opt {
|
|
1818
|
+
display: flex; align-items: center; gap: 4px;
|
|
1819
|
+
padding: 3px 10px; border-radius: 4px; cursor: pointer;
|
|
1820
|
+
font-size: 12px; background: var(--bg-hover); color: var(--text-secondary);
|
|
1821
|
+
border: 1px solid transparent; transition: var(--transition);
|
|
1822
|
+
}
|
|
1823
|
+
.polarity-opt input { display: none; }
|
|
1824
|
+
.polarity-opt small { color: var(--text-muted); font-weight: 400; }
|
|
1825
|
+
.polarity-opt.active[data-polarity="positive"] { background: #ecfdf5; color: #047857; border-color: #6ee7b7; }
|
|
1826
|
+
.polarity-opt.active[data-polarity="negative"] { background: #fef2f2; color: #b91c1c; border-color: #fca5a5; }
|
|
1827
|
+
|
|
1798
1828
|
.judgment-form-row {
|
|
1799
1829
|
display: flex;
|
|
1800
1830
|
gap: 8px;
|