minimal-agent 0.14.0 → 0.15.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minimal-agent",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Bun-first 的本地 Agent 内核,支持工具、插件、工作流、上下文压缩、长期记忆与 OpenAI 兼容接口;Tauri 桌面宿主和 Node.js npm 发布可复用同一内核",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Bill Wang <leiwang0359@gmail.com>",
@@ -58,7 +58,23 @@ function versionsOf(metadata) {
58
58
  evidenceVersion: metadata.evidenceVersion,
59
59
  };
60
60
  }
61
- function checkpointKey(checkpoint, versions) {
61
+ function checkpointKey(checkpoint, versions, signature) {
62
+ // pre-execution/pre-delivery 的意图是"本任务版本内只问一次"(见 beforeTool()
63
+ // 的 reason 文案"即将进行本任务版本的首次实质写入")。旧实现把 workVersion/
64
+ // evidenceVersion 也编进 key,而这两个计数器在几乎每次成功的工具调用(包括
65
+ // 纯 Read/Grep,见 observeToolResult)后都会变化——"首次"因此从未真正生效,
66
+ // 任务里的每一次不同写入都会重新触发一轮完整 Advisor+AdvisorDecision,同时
67
+ // 让 MAX_CONSECUTIVE_DEFERRALS 的连续暂缓降级网因为 key 很少连续重复而基本
68
+ // 失效。改成只用 taskVersion,让这两个检查点在一个任务版本内真正只问一次。
69
+ if (checkpoint === 'pre-execution' || checkpoint === 'pre-delivery') {
70
+ return `${versions.taskVersion}:${checkpoint}`;
71
+ }
72
+ // high-risk 的意图相反:每个不同的危险动作都该有自己的把关,但不能被"中间
73
+ // 插了一次无关的成功调用"误判成新动作——用动作自身的签名代替会漂移的全局
74
+ // 版本计数器,同一动作的重试才能被正确识别为"同一个"。
75
+ if (checkpoint === 'high-risk') {
76
+ return `${versions.taskVersion}:high-risk:${signature ?? 'unknown'}`;
77
+ }
62
78
  return `${versions.taskVersion}:${checkpoint}:${versions.workVersion}:${versions.evidenceVersion}`;
63
79
  }
64
80
  /**
@@ -166,6 +182,9 @@ export class AdvisorCoordinator {
166
182
  const successful = this.#successfulConsultationsByKey.get(this.#pending.checkpointKey) ?? 0;
167
183
  return successful <= MAX_FREE_RECONCILIATIONS_PER_CHECKPOINT;
168
184
  }
185
+ get hasActiveGate() {
186
+ return this.#required !== undefined || this.#softCheckpoint !== undefined;
187
+ }
169
188
  setAvailable(available) {
170
189
  this.#advisorAvailable = available;
171
190
  }
@@ -193,7 +212,15 @@ export class AdvisorCoordinator {
193
212
  return this.#deferPending('顾问意见已返回,但尚未调用 AdvisorDecision 记录主 Agent 的采纳判断。');
194
213
  }
195
214
  if (semantics.highRisk) {
196
- return this.requestCheckpoint('high-risk', semantics.riskReason ?? '即将执行不可逆操作');
215
+ // 用动作自身的签名(工具名+参数的短哈希)代替会被无关调用带动的全局版本
216
+ // 计数器:同一个危险动作的重试仍归到同一个 key,不同的危险动作各自独立
217
+ // 把关,两者都不受"中间插了一次成功的 Read"影响。
218
+ const signature = crypto
219
+ .createHash('sha1')
220
+ .update(`${call.function.name}:${call.function.arguments}`)
221
+ .digest('hex')
222
+ .slice(0, 16);
223
+ return this.requestCheckpoint('high-risk', semantics.riskReason ?? '即将执行不可逆操作', { signature });
197
224
  }
198
225
  if (semantics.mutatesWorkspace) {
199
226
  return this.requestCheckpoint('pre-execution', '即将进行本任务版本的首次实质写入');
@@ -204,7 +231,7 @@ export class AdvisorCoordinator {
204
231
  if (!this.#advisorAvailable)
205
232
  return { allow: true };
206
233
  this.#commitTask();
207
- const key = checkpointKey(checkpoint, this.versions);
234
+ const key = checkpointKey(checkpoint, this.versions, options.signature);
208
235
  if (this.#completed.has(key))
209
236
  return { allow: true };
210
237
  if (options.oneShot && this.#deferredOnce.has(key)) {
@@ -287,7 +314,13 @@ export class AdvisorCoordinator {
287
314
  : this.#required ??
288
315
  this.#softCheckpoint ?? {
289
316
  checkpoint: 'pre-execution',
290
- key: checkpointKey('pre-execution', this.versions),
317
+ // 刻意不用 checkpointKey('pre-execution', ...):这个分支只在没有任何
318
+ // 活跃检查点时才会走到,即模型是主动/复核性质地咨询。pre-execution
319
+ // 的 key 现在只随 taskVersion 变化(见 checkpointKey 头注),如果这里
320
+ // 复用同一个 key,一次纯主动咨询 + AdvisorDecision 就会把 #completed
321
+ // 里真正的首次写入检查点提前标记掉,导致它对本任务版本永久失效。
322
+ // 'voluntary' 是任何真实检查点都不会产生的命名空间,两者永远不会撞车。
323
+ key: `${this.versions.taskVersion}:voluntary`,
291
324
  };
292
325
  return {
293
326
  kind: 'advisor_consultation',
@@ -92,7 +92,24 @@ function versionsOf(
92
92
  function checkpointKey(
93
93
  checkpoint: AdvisorCheckpoint,
94
94
  versions: AdvisorVersions,
95
+ signature?: string,
95
96
  ): string {
97
+ // pre-execution/pre-delivery 的意图是"本任务版本内只问一次"(见 beforeTool()
98
+ // 的 reason 文案"即将进行本任务版本的首次实质写入")。旧实现把 workVersion/
99
+ // evidenceVersion 也编进 key,而这两个计数器在几乎每次成功的工具调用(包括
100
+ // 纯 Read/Grep,见 observeToolResult)后都会变化——"首次"因此从未真正生效,
101
+ // 任务里的每一次不同写入都会重新触发一轮完整 Advisor+AdvisorDecision,同时
102
+ // 让 MAX_CONSECUTIVE_DEFERRALS 的连续暂缓降级网因为 key 很少连续重复而基本
103
+ // 失效。改成只用 taskVersion,让这两个检查点在一个任务版本内真正只问一次。
104
+ if (checkpoint === 'pre-execution' || checkpoint === 'pre-delivery') {
105
+ return `${versions.taskVersion}:${checkpoint}`;
106
+ }
107
+ // high-risk 的意图相反:每个不同的危险动作都该有自己的把关,但不能被"中间
108
+ // 插了一次无关的成功调用"误判成新动作——用动作自身的签名代替会漂移的全局
109
+ // 版本计数器,同一动作的重试才能被正确识别为"同一个"。
110
+ if (checkpoint === 'high-risk') {
111
+ return `${versions.taskVersion}:high-risk:${signature ?? 'unknown'}`;
112
+ }
96
113
  return `${versions.taskVersion}:${checkpoint}:${versions.workVersion}:${versions.evidenceVersion}`;
97
114
  }
98
115
 
@@ -245,6 +262,10 @@ export class AdvisorCoordinator implements AdvisorRuntimeBridge {
245
262
  return successful <= MAX_FREE_RECONCILIATIONS_PER_CHECKPOINT;
246
263
  }
247
264
 
265
+ get hasActiveGate(): boolean {
266
+ return this.#required !== undefined || this.#softCheckpoint !== undefined;
267
+ }
268
+
248
269
  setAvailable(available: boolean): void {
249
270
  this.#advisorAvailable = available;
250
271
  }
@@ -283,9 +304,18 @@ export class AdvisorCoordinator implements AdvisorRuntimeBridge {
283
304
  );
284
305
  }
285
306
  if (semantics.highRisk) {
307
+ // 用动作自身的签名(工具名+参数的短哈希)代替会被无关调用带动的全局版本
308
+ // 计数器:同一个危险动作的重试仍归到同一个 key,不同的危险动作各自独立
309
+ // 把关,两者都不受"中间插了一次成功的 Read"影响。
310
+ const signature = crypto
311
+ .createHash('sha1')
312
+ .update(`${call.function.name}:${call.function.arguments}`)
313
+ .digest('hex')
314
+ .slice(0, 16);
286
315
  return this.requestCheckpoint(
287
316
  'high-risk',
288
317
  semantics.riskReason ?? '即将执行不可逆操作',
318
+ { signature },
289
319
  );
290
320
  }
291
321
  if (semantics.mutatesWorkspace) {
@@ -300,11 +330,11 @@ export class AdvisorCoordinator implements AdvisorRuntimeBridge {
300
330
  requestCheckpoint(
301
331
  checkpoint: AdvisorCheckpoint,
302
332
  reason: string,
303
- options: { oneShot?: boolean } = {},
333
+ options: { oneShot?: boolean; signature?: string } = {},
304
334
  ): CheckpointDecision {
305
335
  if (!this.#advisorAvailable) return { allow: true };
306
336
  this.#commitTask();
307
- const key = checkpointKey(checkpoint, this.versions);
337
+ const key = checkpointKey(checkpoint, this.versions, options.signature);
308
338
  if (this.#completed.has(key)) return { allow: true };
309
339
  if (options.oneShot && this.#deferredOnce.has(key)) {
310
340
  this.#required = undefined;
@@ -402,7 +432,13 @@ export class AdvisorCoordinator implements AdvisorRuntimeBridge {
402
432
  : this.#required ??
403
433
  this.#softCheckpoint ?? {
404
434
  checkpoint: 'pre-execution' as const,
405
- key: checkpointKey('pre-execution', this.versions),
435
+ // 刻意不用 checkpointKey('pre-execution', ...):这个分支只在没有任何
436
+ // 活跃检查点时才会走到,即模型是主动/复核性质地咨询。pre-execution
437
+ // 的 key 现在只随 taskVersion 变化(见 checkpointKey 头注),如果这里
438
+ // 复用同一个 key,一次纯主动咨询 + AdvisorDecision 就会把 #completed
439
+ // 里真正的首次写入检查点提前标记掉,导致它对本任务版本永久失效。
440
+ // 'voluntary' 是任何真实检查点都不会产生的命名空间,两者永远不会撞车。
441
+ key: `${this.versions.taskVersion}:voluntary`,
406
442
  };
407
443
  return {
408
444
  kind: 'advisor_consultation',
@@ -9,10 +9,17 @@ export function QuickModelSetup({ onDone }) {
9
9
  const [adding, setAdding] = useState(false);
10
10
  const [readOnly, setReadOnly] = useState(false);
11
11
  const [message, setMessage] = useState(null);
12
+ // config.json 与 env 都不存在时 reload() 返回 config:null——这是唯一能安全
13
+ // 区分「本机从未配置过」与「配置存在但暂无可用模型」的信号。只在前者把
14
+ // 内置浏览器的 CLI 默认值显式落盘为关闭(见 DEFAULT_BROWSER_CONFIG 的注释:
15
+ // 这个共享常量本身仍是 true,Desktop 走同一常量不受影响;只有 CLI 首次建档
16
+ // 这一次性写入路径改默认)。
17
+ const [freshInstall, setFreshInstall] = useState(false);
12
18
  useEffect(() => {
13
19
  void configService.reload().then((snapshot) => {
14
20
  setReadOnly(snapshot.readOnly);
15
21
  setModels(snapshot.config?.models ?? []);
22
+ setFreshInstall(snapshot.config === null);
16
23
  });
17
24
  }, []);
18
25
  useInput((input, key) => {
@@ -50,7 +57,11 @@ export function QuickModelSetup({ onDone }) {
50
57
  return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: "yellow", bold: true, children: "\u5F53\u524D\u4E3A\u73AF\u5883\u53D8\u91CF\u53EA\u8BFB\u914D\u7F6E\uFF0C\u4F46\u6CA1\u6709\u53EF\u7528\u57FA\u5EA7\u6A21\u578B" }), _jsx(Text, { children: "\u8BF7\u68C0\u67E5 MINIMAL_AGENT_BASE_URL\u3001MINIMAL_AGENT_API_KEY \u548C MINIMAL_AGENT_MODEL\uFF0C\u7136\u540E\u91CD\u65B0\u542F\u52A8\u3002" })] }));
51
58
  if (adding)
52
59
  return (_jsx(ProviderWizard, { quick: true, setAsBase: true, onDone: () => {
53
- void configService.provider().then((provider) => {
60
+ void (freshInstall
61
+ ? configService.savePatch({ browser: { enabled: false } })
62
+ : Promise.resolve())
63
+ .then(() => configService.provider())
64
+ .then((provider) => {
54
65
  if (provider)
55
66
  onDone(provider);
56
67
  });
@@ -16,10 +16,17 @@ export function QuickModelSetup({ onDone }: Props): JSX.Element {
16
16
  const [adding, setAdding] = useState(false);
17
17
  const [readOnly, setReadOnly] = useState(false);
18
18
  const [message, setMessage] = useState<string | null>(null);
19
+ // config.json 与 env 都不存在时 reload() 返回 config:null——这是唯一能安全
20
+ // 区分「本机从未配置过」与「配置存在但暂无可用模型」的信号。只在前者把
21
+ // 内置浏览器的 CLI 默认值显式落盘为关闭(见 DEFAULT_BROWSER_CONFIG 的注释:
22
+ // 这个共享常量本身仍是 true,Desktop 走同一常量不受影响;只有 CLI 首次建档
23
+ // 这一次性写入路径改默认)。
24
+ const [freshInstall, setFreshInstall] = useState(false);
19
25
  useEffect(() => {
20
26
  void configService.reload().then((snapshot) => {
21
27
  setReadOnly(snapshot.readOnly);
22
28
  setModels(snapshot.config?.models ?? []);
29
+ setFreshInstall(snapshot.config === null);
23
30
  });
24
31
  }, []);
25
32
  useInput((input, key) => {
@@ -63,9 +70,14 @@ export function QuickModelSetup({ onDone }: Props): JSX.Element {
63
70
  quick
64
71
  setAsBase
65
72
  onDone={() => {
66
- void configService.provider().then((provider) => {
67
- if (provider) onDone(provider);
68
- });
73
+ void (freshInstall
74
+ ? configService.savePatch({ browser: { enabled: false } })
75
+ : Promise.resolve()
76
+ )
77
+ .then(() => configService.provider())
78
+ .then((provider) => {
79
+ if (provider) onDone(provider);
80
+ });
69
81
  }}
70
82
  onCancel={() => setAdding(false)}
71
83
  />
@@ -20,22 +20,32 @@ export const PRESETS = [
20
20
  },
21
21
  {
22
22
  // 火山方舟 ARK Coding Plan —— 一个 endpoint 后挂多个 model id,
23
- // model 字段大小写严格匹配('glm-5.1' 而非 'GLM-5.1')。
23
+ // model 字段大小写严格匹配('glm-5.3' 而非 'GLM-5.3')。
24
+ // ⚠️ 必须是 /api/coding/v3/(Coding Plan 折扣路由),不是 /api/v3/
25
+ // (官网原价路由)——控制台原文明确警告后者「会产生额外费用」。
26
+ // 2026-09 按控制台最新模型列表刷新(原列表含已下线的 doubao-seed-2.0-pro/
27
+ // doubao-seed-code/glm-5.1/minimax-latest/deepseek-v3.2/kimi-k2.6)。
24
28
  name: 'volces',
25
29
  label: '火山方舟 Coding Plan (ARK)',
26
30
  baseURL: 'https://ark.cn-beijing.volces.com/api/coding/v3/chat/completions',
27
31
  models: [
28
- 'doubao-seed-2.0-pro',
29
- 'doubao-seed-2.0-code',
30
- 'doubao-seed-code',
31
- 'glm-5.1',
32
- 'minimax-latest',
33
- 'deepseek-v3.2',
32
+ 'auto',
33
+ 'doubao-seed-evolving',
34
+ 'doubao-seed-2.1-turbo',
35
+ 'doubao-seed-2.0-lite',
36
+ 'glm-5.3-flash',
37
+ 'glm-5.3',
38
+ 'deepseek-v4-pro',
39
+ 'deepseek-v4-flash',
34
40
  'kimi-k2.7-code',
35
- 'kimi-k2.6',
41
+ 'minimax-m3',
36
42
  ],
37
43
  contextWindow: 204_800,
38
44
  modelContextWindows: {
45
+ // 控制台标注「支持 1M 超长上下文」的三个。
46
+ 'doubao-seed-evolving': 1_000_000,
47
+ 'glm-5.3': 1_000_000,
48
+ 'deepseek-v4-pro': 1_000_000,
39
49
  'kimi-k2.7-code': 512_000,
40
50
  },
41
51
  },
@@ -35,22 +35,32 @@ export const PRESETS: ProviderPreset[] = [
35
35
  },
36
36
  {
37
37
  // 火山方舟 ARK Coding Plan —— 一个 endpoint 后挂多个 model id,
38
- // model 字段大小写严格匹配('glm-5.1' 而非 'GLM-5.1')。
38
+ // model 字段大小写严格匹配('glm-5.3' 而非 'GLM-5.3')。
39
+ // ⚠️ 必须是 /api/coding/v3/(Coding Plan 折扣路由),不是 /api/v3/
40
+ // (官网原价路由)——控制台原文明确警告后者「会产生额外费用」。
41
+ // 2026-09 按控制台最新模型列表刷新(原列表含已下线的 doubao-seed-2.0-pro/
42
+ // doubao-seed-code/glm-5.1/minimax-latest/deepseek-v3.2/kimi-k2.6)。
39
43
  name: 'volces',
40
44
  label: '火山方舟 Coding Plan (ARK)',
41
45
  baseURL: 'https://ark.cn-beijing.volces.com/api/coding/v3/chat/completions',
42
46
  models: [
43
- 'doubao-seed-2.0-pro',
44
- 'doubao-seed-2.0-code',
45
- 'doubao-seed-code',
46
- 'glm-5.1',
47
- 'minimax-latest',
48
- 'deepseek-v3.2',
47
+ 'auto',
48
+ 'doubao-seed-evolving',
49
+ 'doubao-seed-2.1-turbo',
50
+ 'doubao-seed-2.0-lite',
51
+ 'glm-5.3-flash',
52
+ 'glm-5.3',
53
+ 'deepseek-v4-pro',
54
+ 'deepseek-v4-flash',
49
55
  'kimi-k2.7-code',
50
- 'kimi-k2.6',
56
+ 'minimax-m3',
51
57
  ],
52
58
  contextWindow: 204_800,
53
59
  modelContextWindows: {
60
+ // 控制台标注「支持 1M 超长上下文」的三个。
61
+ 'doubao-seed-evolving': 1_000_000,
62
+ 'glm-5.3': 1_000_000,
63
+ 'deepseek-v4-pro': 1_000_000,
54
64
  'kimi-k2.7-code': 512_000,
55
65
  },
56
66
  },
package/src/loop.js CHANGED
@@ -234,7 +234,15 @@ export async function* runQuery(userInput, options) {
234
234
  history,
235
235
  sessionState,
236
236
  // 配额随本回合工作量增长(见 advisorQuotaFor);下面每轮工具执行完后上调。
237
- advisorCalls: { used: 0, max: advisorQuotaFor(0) },
237
+ // mandatoryUsed/mandatoryMax 是独立的"强制检查点"配额池,不随工具调用数
238
+ // 增长——它是安全闸门的固定配额,不是"干得越多奖励越多"的自愿咨询配额;
239
+ // 复用 ADVISOR_QUOTA_MAX 而不新增字面量,避免和自愿池的封顶各写各的漂移。
240
+ advisorCalls: {
241
+ used: 0,
242
+ max: advisorQuotaFor(0),
243
+ mandatoryUsed: 0,
244
+ mandatoryMax: ADVISOR_QUOTA_MAX,
245
+ },
238
246
  advisorInvocation: 'agent',
239
247
  // Agent 等派生工具起嵌套子查询所需的运行态:与主循环同一 provider、
240
248
  // 同一审批闸门(子 agent 不绕过宿主策略)。agentCalls 与 advisorCalls 同构,
package/src/loop.ts CHANGED
@@ -436,7 +436,15 @@ export async function* runQuery(
436
436
  history,
437
437
  sessionState,
438
438
  // 配额随本回合工作量增长(见 advisorQuotaFor);下面每轮工具执行完后上调。
439
- advisorCalls: { used: 0, max: advisorQuotaFor(0) },
439
+ // mandatoryUsed/mandatoryMax 是独立的"强制检查点"配额池,不随工具调用数
440
+ // 增长——它是安全闸门的固定配额,不是"干得越多奖励越多"的自愿咨询配额;
441
+ // 复用 ADVISOR_QUOTA_MAX 而不新增字面量,避免和自愿池的封顶各写各的漂移。
442
+ advisorCalls: {
443
+ used: 0,
444
+ max: advisorQuotaFor(0),
445
+ mandatoryUsed: 0,
446
+ mandatoryMax: ADVISOR_QUOTA_MAX,
447
+ },
440
448
  advisorInvocation: 'agent',
441
449
  // Agent 等派生工具起嵌套子查询所需的运行态:与主循环同一 provider、
442
450
  // 同一审批闸门(子 agent 不绕过宿主策略)。agentCalls 与 advisorCalls 同构,
@@ -4,7 +4,7 @@
4
4
  * ------------------------------------------------------------
5
5
  * 独立配置的第二个 LLM,供主循环的模型在关键判断点(方案/诊断成型后动手写改之前、
6
6
  * 受阻或要改方向时、收尾汇报之前)主动调用,拿到一个不同模型的第二意见。
7
- * 时机规则的真身在 `prompts/promptAssembly.ts` 的 Advisor 工具规则 + 本文件的
7
+ * 时机规则的真身在 `prompts/policyRegistry.ts` 的 `capability.advisor` 能力块 + 本文件的
8
8
  * `description`;配额曲线在 `loop.ts::advisorQuotaFor`。
9
9
  *
10
10
  * 设计要点(吸收自 Claude Code 官方 /advisor 的架构,但按 minimal-agent
@@ -378,18 +378,39 @@ export async function runAdvisor(input, signal, ctx, chatFn = chat, runtime = {}
378
378
  const isReconcile = ctx?.advisorInvocation === 'agent'
379
379
  && Boolean(ctx.advisorRuntime?.pendingConsultationId)
380
380
  && ctx.advisorRuntime?.canReconcileConsultation === true;
381
- if (!isReconcile && ctx?.advisorCalls && ctx.advisorCalls.used >= ctx.advisorCalls.max) {
381
+ // 检查点强制咨询 vs 自愿咨询分池计费:宿主主动拦下模型、强制它咨询的这一次
382
+ // 调用(hasActiveGate === true)走独立的 mandatoryUsed/mandatoryMax 池,
383
+ // 永远不会被自愿/复核性质的咨询挤占配额。旧版只有一个共享池,配额起点为 1
384
+ // 时一个回合里第二个检查点几乎必然撞墙——这不是顾问后端配置问题,是结构性的。
385
+ const isMandated = ctx?.advisorInvocation === 'agent'
386
+ && ctx.advisorRuntime?.hasActiveGate === true;
387
+ const quota = ctx?.advisorCalls;
388
+ const usesMandatoryPool = isMandated && quota?.mandatoryMax !== undefined;
389
+ const quotaUsed = usesMandatoryPool ? (quota.mandatoryUsed ?? 0) : quota?.used;
390
+ const quotaMax = usesMandatoryPool ? quota.mandatoryMax : quota?.max;
391
+ if (!isReconcile && quota && quotaUsed !== undefined && quotaMax !== undefined && quotaUsed >= quotaMax) {
382
392
  // ⚠️ 必须说明「配额会增长」:配额随本回合工作量上调(loop.ts::advisorQuotaFor),
383
393
  // 若这里只说「已达上限」,模型会当成本回合的硬上限而不再尝试 —— 那样长回合里
384
394
  // 「批次做完再复核一次」的配额永远不会被用到,增长机制等于没做。
395
+ // ⚠️ 只有确实存在一个待结清的咨询(pendingConsultationId)时才能说"按已有
396
+ // 意见继续"——否则这一步可能压根没有产生过任何意见,那句话就是误导。
397
+ const hasPendingAdvice = ctx?.advisorRuntime?.pendingConsultationId !== undefined;
398
+ const poolLabel = usesMandatoryPool ? '检查点强制咨询配额' : '顾问咨询配额';
399
+ const guidance = hasPendingAdvice
400
+ ? '配额会随本回合的工作量增长,继续推进一段后可以再次咨询;现在请按已有意见继续。'
401
+ : '配额会随本回合的工作量增长,继续推进一段后可以再次咨询;这一步目前没有可参考的顾问意见,' +
402
+ '请基于代码、测试等一手证据自行判断,并在汇报时如实说明这一步未获得第二意见背书。';
385
403
  return finish({
386
404
  ok: false,
387
- error: `本回合顾问咨询配额暂时用完(当前 ${ctx.advisorCalls.max} 次)。` +
388
- '配额会随本回合的工作量增长,继续推进一段后可以再次咨询;现在请按已有意见继续。',
405
+ error: `本回合${poolLabel}暂时用完(当前 ${quotaMax} 次)。${guidance}`,
389
406
  });
390
407
  }
391
- if (!isReconcile && ctx?.advisorCalls)
392
- ctx.advisorCalls.used++;
408
+ if (!isReconcile && quota) {
409
+ if (usesMandatoryPool)
410
+ quota.mandatoryUsed = (quota.mandatoryUsed ?? 0) + 1;
411
+ else
412
+ quota.used++;
413
+ }
393
414
  const provider = advisor.provider;
394
415
  const snapshot = await configService.getSnapshot();
395
416
  const secrets = [
@@ -4,7 +4,7 @@
4
4
  * ------------------------------------------------------------
5
5
  * 独立配置的第二个 LLM,供主循环的模型在关键判断点(方案/诊断成型后动手写改之前、
6
6
  * 受阻或要改方向时、收尾汇报之前)主动调用,拿到一个不同模型的第二意见。
7
- * 时机规则的真身在 `prompts/promptAssembly.ts` 的 Advisor 工具规则 + 本文件的
7
+ * 时机规则的真身在 `prompts/policyRegistry.ts` 的 `capability.advisor` 能力块 + 本文件的
8
8
  * `description`;配额曲线在 `loop.ts::advisorQuotaFor`。
9
9
  *
10
10
  * 设计要点(吸收自 Claude Code 官方 /advisor 的架构,但按 minimal-agent
@@ -453,18 +453,38 @@ export async function runAdvisor(
453
453
  && Boolean(ctx.advisorRuntime?.pendingConsultationId)
454
454
  && ctx.advisorRuntime?.canReconcileConsultation === true;
455
455
 
456
- if (!isReconcile && ctx?.advisorCalls && ctx.advisorCalls.used >= ctx.advisorCalls.max) {
456
+ // 检查点强制咨询 vs 自愿咨询分池计费:宿主主动拦下模型、强制它咨询的这一次
457
+ // 调用(hasActiveGate === true)走独立的 mandatoryUsed/mandatoryMax 池,
458
+ // 永远不会被自愿/复核性质的咨询挤占配额。旧版只有一个共享池,配额起点为 1
459
+ // 时一个回合里第二个检查点几乎必然撞墙——这不是顾问后端配置问题,是结构性的。
460
+ const isMandated = ctx?.advisorInvocation === 'agent'
461
+ && ctx.advisorRuntime?.hasActiveGate === true;
462
+ const quota = ctx?.advisorCalls;
463
+ const usesMandatoryPool = isMandated && quota?.mandatoryMax !== undefined;
464
+ const quotaUsed = usesMandatoryPool ? (quota!.mandatoryUsed ?? 0) : quota?.used;
465
+ const quotaMax = usesMandatoryPool ? quota!.mandatoryMax! : quota?.max;
466
+
467
+ if (!isReconcile && quota && quotaUsed !== undefined && quotaMax !== undefined && quotaUsed >= quotaMax) {
457
468
  // ⚠️ 必须说明「配额会增长」:配额随本回合工作量上调(loop.ts::advisorQuotaFor),
458
469
  // 若这里只说「已达上限」,模型会当成本回合的硬上限而不再尝试 —— 那样长回合里
459
470
  // 「批次做完再复核一次」的配额永远不会被用到,增长机制等于没做。
471
+ // ⚠️ 只有确实存在一个待结清的咨询(pendingConsultationId)时才能说"按已有
472
+ // 意见继续"——否则这一步可能压根没有产生过任何意见,那句话就是误导。
473
+ const hasPendingAdvice = ctx?.advisorRuntime?.pendingConsultationId !== undefined;
474
+ const poolLabel = usesMandatoryPool ? '检查点强制咨询配额' : '顾问咨询配额';
475
+ const guidance = hasPendingAdvice
476
+ ? '配额会随本回合的工作量增长,继续推进一段后可以再次咨询;现在请按已有意见继续。'
477
+ : '配额会随本回合的工作量增长,继续推进一段后可以再次咨询;这一步目前没有可参考的顾问意见,' +
478
+ '请基于代码、测试等一手证据自行判断,并在汇报时如实说明这一步未获得第二意见背书。';
460
479
  return finish({
461
480
  ok: false,
462
- error:
463
- `本回合顾问咨询配额暂时用完(当前 ${ctx.advisorCalls.max} 次)。` +
464
- '配额会随本回合的工作量增长,继续推进一段后可以再次咨询;现在请按已有意见继续。',
481
+ error: `本回合${poolLabel}暂时用完(当前 ${quotaMax} 次)。${guidance}`,
465
482
  });
466
483
  }
467
- if (!isReconcile && ctx?.advisorCalls) ctx.advisorCalls.used++;
484
+ if (!isReconcile && quota) {
485
+ if (usesMandatoryPool) quota.mandatoryUsed = (quota.mandatoryUsed ?? 0) + 1;
486
+ else quota.used++;
487
+ }
468
488
 
469
489
  const provider = advisor.provider;
470
490
  const snapshot = await configService.getSnapshot();
@@ -89,6 +89,14 @@ export const CONTRACT_DESCRIPTION_OVERRIDES = {
89
89
  return notice ? `${base}\n\n${notice}` : base;
90
90
  },
91
91
  Advisor: '向无工具权限的顾问模型请求一次结构化第二意见。',
92
+ // 通用兜底截断逻辑会在 280 字符处硬切原始 description,此前把"partial/reject
93
+ // 必须列出被拒绝的意见点并给出一手证据"这条唯一的自然语言说明整句吞掉(schema
94
+ // 的 superRefine 仍会强制校验字段,但模型看不到为什么)。用覆写绕开截断。
95
+ AdvisorDecision: '记录主 Agent 在一次成功的 Advisor 咨询后对其意见作出的证据判断。' +
96
+ '应在恢复被暂缓的写入、不可逆操作或最终交付之前调用。' +
97
+ '顾问只是第二意见,不是权威结论——判断必须基于代码、测试或用户约束等一手证据,不能默认采纳顾问结论。' +
98
+ '选择 partial 或 reject 时必须明确列出被拒绝的意见点(rejectedPoints)并说明支持该判断的一手证据;' +
99
+ '未经过一次成功的 Advisor 咨询时不要调用本工具。',
92
100
  Agent: '启动一个具有明确任务书和隔离边界的子 Agent。',
93
101
  };
94
102
  function compactContractDescription(tool) {
@@ -107,6 +107,15 @@ export const CONTRACT_DESCRIPTION_OVERRIDES: Readonly<Record<string, string | ((
107
107
  return notice ? `${base}\n\n${notice}` : base;
108
108
  },
109
109
  Advisor: '向无工具权限的顾问模型请求一次结构化第二意见。',
110
+ // 通用兜底截断逻辑会在 280 字符处硬切原始 description,此前把"partial/reject
111
+ // 必须列出被拒绝的意见点并给出一手证据"这条唯一的自然语言说明整句吞掉(schema
112
+ // 的 superRefine 仍会强制校验字段,但模型看不到为什么)。用覆写绕开截断。
113
+ AdvisorDecision:
114
+ '记录主 Agent 在一次成功的 Advisor 咨询后对其意见作出的证据判断。' +
115
+ '应在恢复被暂缓的写入、不可逆操作或最终交付之前调用。' +
116
+ '顾问只是第二意见,不是权威结论——判断必须基于代码、测试或用户约束等一手证据,不能默认采纳顾问结论。' +
117
+ '选择 partial 或 reject 时必须明确列出被拒绝的意见点(rejectedPoints)并说明支持该判断的一手证据;' +
118
+ '未经过一次成功的 Advisor 咨询时不要调用本工具。',
110
119
  Agent: '启动一个具有明确任务书和隔离边界的子 Agent。',
111
120
  };
112
121
 
package/src/types.ts CHANGED
@@ -493,8 +493,19 @@ export interface ToolCallContext {
493
493
  history?: readonly Message[];
494
494
  /** Per-conversation mutable state; file freshness and background tasks must not leak across sessions. */
495
495
  sessionState?: import('./session/sessionState.js').SessionState;
496
- /** 同一用户回合内共享的 Advisor 调用计数,防止模型循环咨询。 */
497
- advisorCalls?: { used: number; max: number };
496
+ /**
497
+ * 同一用户回合内共享的 Advisor 调用计数,防止模型循环咨询。
498
+ * `mandatoryUsed`/`mandatoryMax` 是可选的独立"强制检查点"配额池
499
+ * (见 advisorRuntime.hasActiveGate)——宿主主动拦下模型、强制它咨询的
500
+ * 这一次调用走这个池,永远不会被自愿/复核性质的咨询挤占;缺失时
501
+ * (如手动 /advisor ask 场景)行为与改动前完全一致,只走 used/max。
502
+ */
503
+ advisorCalls?: {
504
+ used: number;
505
+ max: number;
506
+ mandatoryUsed?: number;
507
+ mandatoryMax?: number;
508
+ };
498
509
  /** 区分主模型自动咨询与用户显式 /advisor ask。 */
499
510
  advisorInvocation?: 'agent' | 'manual';
500
511
  /** Advisor 协调器运行时桥;只存在于主循环,不暴露给模型或持久化。 */
@@ -618,6 +629,12 @@ export interface AdvisorRuntimeBridge {
618
629
  readonly pendingConsultationId: string | undefined;
619
630
  /** 同一检查点最多允许一次不占配额的证据冲突复核。 */
620
631
  readonly canReconcileConsultation: boolean;
632
+ /**
633
+ * true = 当前有一个硬检查点(#required)或软提醒(#softCheckpoint)正在
634
+ * 生效——即接下来的 Advisor 调用是宿主强制模型做的,不是模型自愿发起的。
635
+ * `advisor.ts` 用它判断这次调用该走 advisorCalls 的哪个配额池。
636
+ */
637
+ readonly hasActiveGate: boolean;
621
638
  beginConsultation(): AdvisorConsultationMetadata;
622
639
  finishConsultation(
623
640
  metadata: AdvisorConsultationMetadata,