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 +1 -1
- package/src/advisor/advisorCoordinator.js +37 -4
- package/src/advisor/advisorCoordinator.ts +39 -3
- package/src/cli/quickModelSetup.js +12 -1
- package/src/cli/quickModelSetup.tsx +15 -3
- package/src/cli/wizards/providerPresets.js +18 -8
- package/src/cli/wizards/providerPresets.ts +18 -8
- package/src/loop.js +9 -1
- package/src/loop.ts +9 -1
- package/src/tools/advisor/advisor.js +27 -6
- package/src/tools/advisor/advisor.ts +26 -6
- package/src/tools/toolPromptGuides.js +8 -0
- package/src/tools/toolPromptGuides.ts +9 -0
- package/src/types.ts +19 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minimal-agent",
|
|
3
|
-
"version": "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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
67
|
-
|
|
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.
|
|
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
|
-
'
|
|
29
|
-
'doubao-seed-
|
|
30
|
-
'doubao-seed-
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'
|
|
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
|
-
'
|
|
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.
|
|
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
|
-
'
|
|
44
|
-
'doubao-seed-
|
|
45
|
-
'doubao-seed-
|
|
46
|
-
'
|
|
47
|
-
'
|
|
48
|
-
'
|
|
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
|
-
'
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
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:
|
|
388
|
-
'配额会随本回合的工作量增长,继续推进一段后可以再次咨询;现在请按已有意见继续。',
|
|
405
|
+
error: `本回合${poolLabel}暂时用完(当前 ${quotaMax} 次)。${guidance}`,
|
|
389
406
|
});
|
|
390
407
|
}
|
|
391
|
-
if (!isReconcile &&
|
|
392
|
-
|
|
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/
|
|
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
|
-
|
|
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 &&
|
|
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
|
-
/**
|
|
497
|
-
|
|
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,
|