@bolloon/bolloon-agent 0.3.8 → 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.
@@ -8,6 +8,7 @@ import { checkWritePath } from './shell-guard.js';
8
8
  import { p2pDocumentTools } from './p2p-document-tools.js';
9
9
  import { runSelfImproveLoop } from './pi-sdk-session-factory.js';
10
10
  import { getMinimax } from '../constraints/index.js';
11
+ import { delegateToEngine } from '../external-engines/delegate.js';
11
12
  /**
12
13
  * Tools 模块 — 从 pi-sdk.ts 抽出的 registerTools() / _registerWalletTools() / _setupInboxListener()
13
14
  * / wrapToolsWithIdempotency() (2026-07-06).
@@ -267,6 +268,37 @@ export function registerBuiltinTools(ctx) {
267
268
  return { success: true, output: `📞 RPC 任务已发送给 ${peerId.substring(0, 16)}...\n requestId=${requestId}` };
268
269
  }
269
270
  });
271
+ // delegate_to_engine — 把编码任务委派给本机已安装的其他 AI 编码智能体 CLI
272
+ // (codex / claude-code / opencode / openclaw / hermes). 它们必须已安装且可达 PATH.
273
+ // 实验 API 引擎 (experiment:xxx) 是供应商不是 CLI, 不支持委派, 工具会提示改用 import.
274
+ ctx.tools.set('delegate_to_engine', {
275
+ name: 'delegate_to_engine',
276
+ description: '把编码任务委派给本机已安装的其他 AI 编码智能体 (子智能体) 执行: codex / claude-code / opencode / openclaw / hermes. 引擎需已安装且在 PATH 上. 返回其执行输出. 注意: 各工具 CLI 的非交互参数随版本变化, 若报错请检查该工具版本对应 flag.',
277
+ parameters: {
278
+ engine: "引擎 id: codex / claude-code / opencode / openclaw / hermes (实验 API 不支持委派)",
279
+ prompt: '派发的任务描述 (作为单参数传给该引擎 CLI)',
280
+ model: '可选, 强制指定模型 (如 deepseek/deepseek-v4-flash), 需引擎支持',
281
+ cwd: '可选, 工作目录, 默认当前目录'
282
+ },
283
+ execute: async (args) => {
284
+ const engine = String(args.engine || '').trim();
285
+ const prompt = String(args.prompt || '').trim();
286
+ if (!engine)
287
+ return { success: false, error: 'engine 必填' };
288
+ if (!prompt)
289
+ return { success: false, error: 'prompt 必填' };
290
+ const cwd = args.cwd ? String(args.cwd).trim() : undefined;
291
+ const model = args.model ? String(args.model).trim() : undefined;
292
+ const result = await delegateToEngine(engine, prompt, { cwd: cwd || undefined, ...(model ? { model } : {}) });
293
+ if (!result.success) {
294
+ return { success: false, error: result.error, output: result.output };
295
+ }
296
+ return {
297
+ success: true,
298
+ output: `🤖 ${engine} 执行结果 (exitCode=${result.exitCode}):\n${result.output || '(无输出)'}`
299
+ };
300
+ }
301
+ });
270
302
  ctx.tools.set('get_identity', {
271
303
  name: 'get_identity',
272
304
  description: '获取当前智能体身份信息',
@@ -1217,12 +1249,26 @@ export function registerWalletTools(ctx) {
1217
1249
  });
1218
1250
  ctx.tools.set('polymarket_get_orders', {
1219
1251
  name: 'polymarket_get_orders',
1220
- description: '查询 Polymarket 订单.',
1221
- parameters: { marketId: '可选 按市场 ID 过滤' },
1252
+ description: '查询 Polymarket 开放订单. 需要钱包私钥 privateKey 做 API key 鉴权.',
1253
+ parameters: {
1254
+ privateKey: '钱包私钥 0x... (必填)',
1255
+ marketId: '可选 按市场 ID 过滤',
1256
+ apiKey: '可选, 已存在的 API key',
1257
+ apiSecret: '可选, API secret',
1258
+ apiPassphrase: '可选, API passphrase',
1259
+ funder: '可选, 资金地址',
1260
+ },
1222
1261
  execute: async (args) => {
1223
1262
  try {
1224
1263
  const { getOrders } = await import('../constraint-runtime/dist/tools/PolymarketSDK/getOrders.js').catch(() => import('../constraint-runtime/src/tools/PolymarketSDK/getOrders.js'));
1225
- const orders = await getOrders(args.marketId ? { marketId: String(args.marketId) } : {});
1264
+ const orders = await getOrders({
1265
+ privateKey: String(args.privateKey),
1266
+ marketId: args.marketId ? String(args.marketId) : undefined,
1267
+ apiKey: args.apiKey ? String(args.apiKey) : undefined,
1268
+ apiSecret: args.apiSecret ? String(args.apiSecret) : undefined,
1269
+ apiPassphrase: args.apiPassphrase ? String(args.apiPassphrase) : undefined,
1270
+ funder: args.funder ? String(args.funder) : undefined,
1271
+ });
1226
1272
  return { success: true, output: `📋 订单列表: ${JSON.stringify(orders, null, 2).substring(0, 1500)}` };
1227
1273
  }
1228
1274
  catch (e) {
@@ -1232,19 +1278,40 @@ export function registerWalletTools(ctx) {
1232
1278
  });
1233
1279
  ctx.tools.set('polymarket_create_order', {
1234
1280
  name: 'polymarket_create_order',
1235
- description: '在 Polymarket 下单 (BUY/SELL).',
1236
- parameters: { marketId: '市场 ID (必填)', side: 'BUY 或 SELL (必填)', price: '价格 0-1 (必填)', size: '数量 USDC (必填)' },
1281
+ description: '在 Polymarket 下单 (BUY/SELL). 需要钱包私钥 privateKey 做 EIP-712 订单签名与 API key 派生.',
1282
+ parameters: {
1283
+ privateKey: '下单钱包私钥 0x... (必填)',
1284
+ marketId: '市场 ID (必填)',
1285
+ side: 'BUY 或 SELL (必填)',
1286
+ price: '价格 0-1 (必填, 需符合 tickSize)',
1287
+ size: '数量 USDC (必填)',
1288
+ outcome: '可选, Yes/No 或索引 0/1, 默认第一个 (通常 Yes)',
1289
+ tokenId: '可选, 显式条件代币 tokenID (优先于 outcome)',
1290
+ orderType: '可选, GTC (默认) 或 GTD',
1291
+ apiKey: '可选, 已存在的 API key (需配合 apiSecret/apiPassphrase)',
1292
+ apiSecret: '可选, API secret',
1293
+ apiPassphrase: '可选, API passphrase',
1294
+ funder: '可选, 资金地址 (默认=私钥地址)',
1295
+ },
1237
1296
  execute: async (args) => {
1238
1297
  try {
1239
1298
  const { createOrder } = await import('../constraint-runtime/dist/tools/PolymarketSDK/createOrder.js').catch(() => import('../constraint-runtime/src/tools/PolymarketSDK/createOrder.js'));
1240
1299
  const r = await createOrder({
1300
+ privateKey: String(args.privateKey),
1241
1301
  marketId: String(args.marketId),
1242
1302
  side: String(args.side).toUpperCase() === 'SELL' ? 'SELL' : 'BUY',
1243
1303
  price: Number(args.price),
1244
1304
  size: Number(args.size),
1305
+ outcome: args.outcome !== undefined ? args.outcome : undefined,
1306
+ tokenId: args.tokenId ? String(args.tokenId) : undefined,
1307
+ orderType: args.orderType ? String(args.orderType) : undefined,
1308
+ apiKey: args.apiKey ? String(args.apiKey) : undefined,
1309
+ apiSecret: args.apiSecret ? String(args.apiSecret) : undefined,
1310
+ apiPassphrase: args.apiPassphrase ? String(args.apiPassphrase) : undefined,
1311
+ funder: args.funder ? String(args.funder) : undefined,
1245
1312
  });
1246
1313
  if (r.success)
1247
- return { success: true, output: `✅ 订单: ${r.message}` };
1314
+ return { success: true, output: `✅ 订单已提交: orderId=${r.orderId} status=${r.status}` };
1248
1315
  return { success: false, error: r.message, output: r.message };
1249
1316
  }
1250
1317
  catch (e) {
@@ -1254,12 +1321,26 @@ export function registerWalletTools(ctx) {
1254
1321
  });
1255
1322
  ctx.tools.set('polymarket_cancel_order', {
1256
1323
  name: 'polymarket_cancel_order',
1257
- description: '取消 Polymarket 订单.',
1258
- parameters: { orderId: '订单 ID (必填)' },
1324
+ description: '取消 Polymarket 订单. 需要钱包私钥 privateKey 做 API key 鉴权.',
1325
+ parameters: {
1326
+ privateKey: '钱包私钥 0x... (必填)',
1327
+ orderId: '订单 ID (必填)',
1328
+ apiKey: '可选, 已存在的 API key',
1329
+ apiSecret: '可选, API secret',
1330
+ apiPassphrase: '可选, API passphrase',
1331
+ funder: '可选, 资金地址',
1332
+ },
1259
1333
  execute: async (args) => {
1260
1334
  try {
1261
1335
  const { cancelOrder } = await import('../constraint-runtime/dist/tools/PolymarketSDK/cancelOrder.js').catch(() => import('../constraint-runtime/src/tools/PolymarketSDK/cancelOrder.js'));
1262
- const r = await cancelOrder({ orderId: String(args.orderId) });
1336
+ const r = await cancelOrder({
1337
+ privateKey: String(args.privateKey),
1338
+ orderId: String(args.orderId),
1339
+ apiKey: args.apiKey ? String(args.apiKey) : undefined,
1340
+ apiSecret: args.apiSecret ? String(args.apiSecret) : undefined,
1341
+ apiPassphrase: args.apiPassphrase ? String(args.apiPassphrase) : undefined,
1342
+ funder: args.funder ? String(args.funder) : undefined,
1343
+ });
1263
1344
  if (r.success)
1264
1345
  return { success: true, output: `✅ 取消订单: ${r.message}` };
1265
1346
  return { success: false, error: r.message, output: r.message };
@@ -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
- const gate = await injectJudgmentGate(input, {}, { maxChars: 1500 });
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
- if (gate.usedIds.length > 0) {
154
- safePhase('gate_done', { usedCount: gate.usedIds.length, didInject: gate.didInject, skipReason: gate.skipReason });
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,
@@ -1,6 +1,21 @@
1
+ import { buildClobClient } from './clobShared';
1
2
  export async function cancelOrder(params) {
2
- return {
3
- success: false,
4
- message: 'Polymarket order cancellation requires CLOB client with authentication. Use the Polymarket web interface to cancel orders.',
5
- };
3
+ if (!params.privateKey) {
4
+ return { success: false, message: '取消订单需要提供钱包私钥 privateKey' };
5
+ }
6
+ if (!params.orderId) {
7
+ return { success: false, message: 'orderId 必填' };
8
+ }
9
+ try {
10
+ const { client } = await buildClobClient({
11
+ privateKey: params.privateKey,
12
+ creds: { apiKey: params.apiKey, apiSecret: params.apiSecret, apiPassphrase: params.apiPassphrase },
13
+ funder: params.funder,
14
+ });
15
+ const resp = await client.cancelOrder({ orderID: params.orderId });
16
+ return { success: true, message: '已提交取消', raw: resp };
17
+ }
18
+ catch (e) {
19
+ return { success: false, message: `取消失败: ${e?.message || e}` };
20
+ }
6
21
  }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Polymarket CLOB 下单/查单/撤单 共享依赖
3
+ *
4
+ * 真实实现基于 @polymarket/clob-client (ClobClient):
5
+ * - 签名用 viem WalletClient (privateKeyToAccount + polygon + http transport)
6
+ * - 下单前需 ApiKeyCreds (key/secret/passphrase), 由 createOrDeriveApiKey() 从签名派生
7
+ * - tokenID / tickSize / negRisk 来自 Gamma 市场元数据 (https://gamma-api.polymarket.com/markets/:id)
8
+ *
9
+ * chainId = 137 (Polygon), host = https://clob.polymarket.com
10
+ */
11
+ import { ClobClient, OrderType, Side } from '@polymarket/clob-client';
12
+ import { createWalletClient, http } from 'viem';
13
+ import { privateKeyToAccount } from 'viem/accounts';
14
+ import { polygon } from 'viem/chains';
15
+ export const CLOB_HOST = 'https://clob.polymarket.com';
16
+ export const CHAIN_ID = 137;
17
+ export function normalizePrivateKey(pk) {
18
+ const s = pk.startsWith('0x') ? pk : `0x${pk}`;
19
+ return s;
20
+ }
21
+ function safeParseArray(v) {
22
+ if (Array.isArray(v))
23
+ return v;
24
+ if (typeof v === 'string') {
25
+ try {
26
+ const parsed = JSON.parse(v);
27
+ return Array.isArray(parsed) ? parsed : [];
28
+ }
29
+ catch {
30
+ return [];
31
+ }
32
+ }
33
+ return [];
34
+ }
35
+ /**
36
+ * 从 Gamma 取市场元数据: clobTokenIds / outcomes / tickSize / negRisk.
37
+ * 优先用 Gamma 路径端点 (带 tickSize/negRisk), 失败回退 polymarket-sdk listMarkets({id}).
38
+ */
39
+ export async function fetchMarketMeta(marketId) {
40
+ try {
41
+ const res = await fetch(`https://gamma-api.polymarket.com/markets/${encodeURIComponent(marketId)}`);
42
+ if (res.ok) {
43
+ const m = await res.json();
44
+ return {
45
+ clobTokenIds: safeParseArray(m.clobTokenIds),
46
+ outcomes: safeParseArray(m.outcomes),
47
+ tickSize: typeof m.tickSize === 'string' ? m.tickSize : '0.01',
48
+ negRisk: !!m.negRisk,
49
+ };
50
+ }
51
+ }
52
+ catch {
53
+ // fall through to sdk
54
+ }
55
+ const { listMarkets } = await import('polymarket-sdk').catch(() => ({ listMarkets: async () => [] }));
56
+ const ms = await listMarkets({ id: marketId });
57
+ const m = ms?.[0];
58
+ if (!m)
59
+ throw new Error(`未找到市场: ${marketId}`);
60
+ return {
61
+ clobTokenIds: safeParseArray(m.clobTokenIds),
62
+ outcomes: safeParseArray(m.outcomes),
63
+ tickSize: '0.01',
64
+ negRisk: false,
65
+ };
66
+ }
67
+ /**
68
+ * 由 outcome (如 "Yes"/"No" 或索引 0/1) 或显式 tokenId 解析出要交易的 tokenID.
69
+ * 都不给 → 默认取第一个 clobTokenId (通常是 "Yes").
70
+ */
71
+ export function resolveTokenId(meta, tokenId, outcome) {
72
+ if (tokenId)
73
+ return tokenId;
74
+ if (!meta.clobTokenIds || meta.clobTokenIds.length === 0)
75
+ return undefined;
76
+ if (outcome === undefined || outcome === null)
77
+ return meta.clobTokenIds[0];
78
+ if (typeof outcome === 'number')
79
+ return meta.clobTokenIds[outcome];
80
+ const idx = meta.outcomes.findIndex((o) => String(o).toLowerCase() === String(outcome).toLowerCase());
81
+ return idx >= 0 ? meta.clobTokenIds[idx] : undefined;
82
+ }
83
+ /**
84
+ * 构造已鉴权的 ClobClient.
85
+ * - 若提供完整 apiKey/apiSecret/apiPassphrase → 直接用
86
+ * - 否则用签名派生 ApiKey (createOrDeriveApiKey, 需联网)
87
+ * signatureType = 0 (EOA / 浏览器钱包, 对应原始私钥签名)
88
+ */
89
+ export async function buildClobClient(params) {
90
+ const account = privateKeyToAccount(normalizePrivateKey(params.privateKey));
91
+ const signer = createWalletClient({ account, chain: polygon, transport: http() });
92
+ const address = account.address;
93
+ let creds;
94
+ if (params.creds?.apiKey && params.creds?.apiSecret && params.creds?.apiPassphrase) {
95
+ creds = { key: params.creds.apiKey, secret: params.creds.apiSecret, passphrase: params.creds.apiPassphrase };
96
+ }
97
+ if (!creds) {
98
+ const tmp = new ClobClient(CLOB_HOST, CHAIN_ID, signer);
99
+ creds = await tmp.createOrDeriveApiKey();
100
+ }
101
+ const client = new ClobClient(CLOB_HOST, CHAIN_ID, signer, creds, 0, params.funder ?? address);
102
+ return { client, address };
103
+ }
104
+ export { OrderType, Side };
@@ -1,6 +1,36 @@
1
+ import { buildClobClient, fetchMarketMeta, resolveTokenId } from './clobShared';
1
2
  export async function createOrder(params) {
2
- return {
3
- success: false,
4
- message: 'Polymarket order creation requires CLOB client with authentication. Use the Polymarket web interface to create orders.',
5
- };
3
+ if (!params.privateKey) {
4
+ return { success: false, message: '下单需要提供钱包私钥 privateKey (用于 EIP-712 订单签名)' };
5
+ }
6
+ if (!params.marketId) {
7
+ return { success: false, message: 'marketId 必填' };
8
+ }
9
+ try {
10
+ const meta = await fetchMarketMeta(params.marketId);
11
+ const tokenID = resolveTokenId(meta, params.tokenId, params.outcome);
12
+ if (!tokenID) {
13
+ return { success: false, message: '无法解析 tokenID (outcome 不匹配或市场无 clobTokenIds)' };
14
+ }
15
+ const { client } = await buildClobClient({
16
+ privateKey: params.privateKey,
17
+ creds: { apiKey: params.apiKey, apiSecret: params.apiSecret, apiPassphrase: params.apiPassphrase },
18
+ funder: params.funder,
19
+ });
20
+ const resp = await client.createAndPostOrder({
21
+ tokenID,
22
+ price: Number(params.price),
23
+ size: Number(params.size),
24
+ side: params.side,
25
+ }, { tickSize: meta.tickSize, negRisk: meta.negRisk }, (params.orderType ?? 'GTC'));
26
+ return {
27
+ success: true,
28
+ orderId: resp?.orderID ?? resp?.id,
29
+ status: resp?.status,
30
+ raw: resp,
31
+ };
32
+ }
33
+ catch (e) {
34
+ return { success: false, message: `下单失败: ${e?.message || e}` };
35
+ }
6
36
  }
@@ -1,6 +1,19 @@
1
+ import { buildClobClient } from './clobShared';
1
2
  export async function getOrders(params = {}) {
2
- return {
3
- orders: [],
4
- message: 'Polymarket order retrieval requires CLOB client with authentication. Use the Polymarket web interface to view orders.',
5
- };
3
+ if (!params.privateKey) {
4
+ return { orders: [], message: '查询订单需要提供钱包私钥 privateKey' };
5
+ }
6
+ try {
7
+ const { client } = await buildClobClient({
8
+ privateKey: params.privateKey,
9
+ creds: { apiKey: params.apiKey, apiSecret: params.apiSecret, apiPassphrase: params.apiPassphrase },
10
+ funder: params.funder,
11
+ });
12
+ const resp = await client.getOpenOrders(params.marketId ? { market: params.marketId } : {});
13
+ const orders = Array.isArray(resp) ? resp : resp?.orders ?? [];
14
+ return { orders, message: 'OK' };
15
+ }
16
+ catch (e) {
17
+ return { orders: [], message: `查询失败: ${e?.message || e}` };
18
+ }
6
19
  }