agentflowctl 0.10.0 → 0.12.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/README.md +116 -396
- package/dist/agentConfig.js +4 -3
- package/dist/agents/claude.js +12 -6
- package/dist/agents/codex.js +3 -1
- package/dist/agents/command.js +4 -2
- package/dist/agents/gemini.js +17 -6
- package/dist/cli.js +141 -7
- package/dist/engine.js +42 -18
- package/dist/handoff.js +14 -8
- package/dist/modelConfig.js +74 -0
- package/dist/modelProbe.js +74 -0
- package/dist/modelSelection.js +107 -0
- package/dist/proc.js +7 -2
- package/dist/roles.js +20 -17
- package/dist/runner.js +17 -4
- package/dist/schemas.js +17 -0
- package/dist/store.js +30 -10
- package/dist/tasks.js +7 -0
- package/package.json +1 -1
- package/prompts/fix.md +1 -1
- package/prompts/implement-code.md +1 -1
- package/prompts/implement-tests.md +1 -1
- package/prompts/plan-arbiter.md +1 -1
- package/prompts/plan-fix.md +2 -1
- package/prompts/plan-review.md +3 -1
- package/prompts/plan.md +8 -2
- package/prompts/review.md +1 -1
- package/prompts/spec.md +1 -1
- package/prompts/task-review.md +1 -1
package/dist/agents/claude.js
CHANGED
|
@@ -24,6 +24,11 @@ function settingsFile(o) {
|
|
|
24
24
|
}
|
|
25
25
|
export const claude = {
|
|
26
26
|
probe: () => ({ cmd: "claude", args: ["--version"] }),
|
|
27
|
+
invokeModelProbe: (model) => ({
|
|
28
|
+
cmd: "claude",
|
|
29
|
+
args: ["-p", "只回答 OK", "--model", model, "--output-format", "stream-json", "--verbose",
|
|
30
|
+
"--tools", "", "--strict-mcp-config", "--disable-slash-commands"],
|
|
31
|
+
}),
|
|
27
32
|
invoke: (o) => ({
|
|
28
33
|
cmd: "claude",
|
|
29
34
|
args: [
|
|
@@ -40,7 +45,10 @@ export const claude = {
|
|
|
40
45
|
if (!ev)
|
|
41
46
|
return [];
|
|
42
47
|
const out = [];
|
|
43
|
-
if (ev.type === "
|
|
48
|
+
if (ev.type === "system" && ev.subtype === "init" && str(ev.model)) {
|
|
49
|
+
out.push({ kind: "model", id: str(ev.model) });
|
|
50
|
+
}
|
|
51
|
+
else if (ev.type === "assistant") {
|
|
44
52
|
const content = ev.message?.content ?? [];
|
|
45
53
|
for (const b of content) {
|
|
46
54
|
if (b.type === "text" && str(b.text)?.trim())
|
|
@@ -51,11 +59,9 @@ export const claude = {
|
|
|
51
59
|
}
|
|
52
60
|
else if (ev.type === "result") {
|
|
53
61
|
const usage = (ev.usage ?? {});
|
|
54
|
-
|
|
55
|
-
kind: "usage",
|
|
56
|
-
|
|
57
|
-
outputTokens: num(usage.output_tokens),
|
|
58
|
-
});
|
|
62
|
+
if (num(usage.input_tokens) !== undefined || num(usage.output_tokens) !== undefined) {
|
|
63
|
+
out.push({ kind: "usage", inputTokens: num(usage.input_tokens), outputTokens: num(usage.output_tokens) });
|
|
64
|
+
}
|
|
59
65
|
out.push({ kind: "done", ok: ev.is_error !== true, summary: str(ev.result) });
|
|
60
66
|
}
|
|
61
67
|
return out;
|
package/dist/agents/codex.js
CHANGED
|
@@ -30,7 +30,9 @@ export const codex = {
|
|
|
30
30
|
}
|
|
31
31
|
else if (ev.type === "turn.completed") {
|
|
32
32
|
const usage = (ev.usage ?? {});
|
|
33
|
-
|
|
33
|
+
if (num(usage.input_tokens) !== undefined || num(usage.output_tokens) !== undefined) {
|
|
34
|
+
out.push({ kind: "usage", inputTokens: num(usage.input_tokens), outputTokens: num(usage.output_tokens) });
|
|
35
|
+
}
|
|
34
36
|
}
|
|
35
37
|
else if (ev.type === "turn.failed" || ev.type === "error") {
|
|
36
38
|
const err = (ev.error ?? {});
|
package/dist/agents/command.js
CHANGED
|
@@ -10,9 +10,11 @@ export const command = {
|
|
|
10
10
|
if (!cmd)
|
|
11
11
|
throw new Error("command adapter 需要設定 command 陣列");
|
|
12
12
|
const hasPlaceholder = rest.some((a) => a.includes("{prompt}"));
|
|
13
|
+
if (o.command?.some((a) => a.includes("{model}")) && !o.model)
|
|
14
|
+
throw new Error("command 含 {model},但沒有設定 model");
|
|
13
15
|
return {
|
|
14
|
-
cmd,
|
|
15
|
-
args: [...rest.map((a) => a.replaceAll("{prompt}", o.prompt)), ...o.extraArgs],
|
|
16
|
+
cmd: cmd.replaceAll("{model}", o.model ?? ""),
|
|
17
|
+
args: [...rest.map((a) => a.replaceAll("{prompt}", o.prompt).replaceAll("{model}", o.model ?? "")), ...o.extraArgs],
|
|
16
18
|
input: hasPlaceholder ? undefined : o.prompt,
|
|
17
19
|
};
|
|
18
20
|
},
|
package/dist/agents/gemini.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
import { num, str, toolDetail, tryJson } from "./types.js";
|
|
2
4
|
/**
|
|
3
5
|
* Google Gemini CLI:`gemini -p ... --output-format stream-json`。
|
|
@@ -7,6 +9,13 @@ import { num, str, toolDetail, tryJson } from "./types.js";
|
|
|
7
9
|
*/
|
|
8
10
|
export const gemini = {
|
|
9
11
|
probe: () => ({ cmd: "gemini", args: ["--version"] }),
|
|
12
|
+
invokeModelProbe: (model, cwd) => {
|
|
13
|
+
const policy = join(cwd, "deny-tools.toml");
|
|
14
|
+
writeFileSync(policy, '[[rule]]\ntoolName = "*"\ndecision = "deny"\npriority = 10000\n');
|
|
15
|
+
return { cmd: "gemini", args: ["-p", "只回答 OK", "--model", model, "--output-format", "stream-json",
|
|
16
|
+
"--approval-mode", "default", "--extensions", "none", "--policy", policy],
|
|
17
|
+
env: { GEMINI_CLI_TRUST_WORKSPACE: "true" } };
|
|
18
|
+
},
|
|
10
19
|
invoke: (o) => ({
|
|
11
20
|
cmd: "gemini",
|
|
12
21
|
args: ["-p", o.prompt, "--output-format", "stream-json", "--approval-mode", "yolo", ...(o.model ? ["-m", o.model] : []), ...o.extraArgs],
|
|
@@ -17,7 +26,10 @@ export const gemini = {
|
|
|
17
26
|
if (!ev)
|
|
18
27
|
return [];
|
|
19
28
|
const out = [];
|
|
20
|
-
if (ev.type === "
|
|
29
|
+
if (ev.type === "init" && str(ev.model)) {
|
|
30
|
+
out.push({ kind: "model", id: str(ev.model) });
|
|
31
|
+
}
|
|
32
|
+
else if (ev.type === "message" && ev.role === "assistant" && str(ev.content)?.trim()) {
|
|
21
33
|
out.push({ kind: "text", text: str(ev.content) });
|
|
22
34
|
}
|
|
23
35
|
else if (ev.type === "tool_use") {
|
|
@@ -25,11 +37,10 @@ export const gemini = {
|
|
|
25
37
|
}
|
|
26
38
|
else if (ev.type === "result") {
|
|
27
39
|
const stats = (ev.stats ?? {});
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
});
|
|
40
|
+
const inputTokens = num(stats.input_tokens) ?? num(stats.inputTokens);
|
|
41
|
+
const outputTokens = num(stats.output_tokens) ?? num(stats.outputTokens);
|
|
42
|
+
if (inputTokens !== undefined || outputTokens !== undefined)
|
|
43
|
+
out.push({ kind: "usage", inputTokens, outputTokens });
|
|
33
44
|
out.push({ kind: "done", ok: ev.status !== "error", summary: str(ev.response) });
|
|
34
45
|
}
|
|
35
46
|
else if (ev.type === "error") {
|
package/dist/cli.js
CHANGED
|
@@ -12,13 +12,25 @@ import { cleanableRuns, cleanRun } from "./cleanup.js";
|
|
|
12
12
|
import { describeDetected, detectProjectDefaults } from "./detect.js";
|
|
13
13
|
import { CMD_AGENT, listLogs, localTime, logMark, nextLogFile, renderLog } from "./logs.js";
|
|
14
14
|
import { flowDir, logDir, projectRoot, worktreeDir } from "./paths.js";
|
|
15
|
-
import { TaskList } from "./schemas.js";
|
|
16
|
-
import { agentRuns, getRun, listRuns, listSubstitutions, saveRun, usageByAgent } from "./store.js";
|
|
15
|
+
import { ModelStage, ModelStrength, TaskList } from "./schemas.js";
|
|
16
|
+
import { agentRuns, getRun, listRuns, listSubstitutions, saveRun, usageByAgent, usageByModelStage, usageByStage, usageByStrength, usageByTask } from "./store.js";
|
|
17
17
|
import { readJsonFile } from "./util.js";
|
|
18
18
|
import { openActions, readHandoff } from "./handoff.js";
|
|
19
19
|
import { stopReport } from "./stopReport.js";
|
|
20
20
|
import { runSetup, SETUP_ADAPTERS } from "./setup.js";
|
|
21
21
|
import { addAgent, readRawConfig, removeAgent, setAgent, setCycle, writeRawConfig } from "./agentConfig.js";
|
|
22
|
+
import { addModel, removeModel, setModelMode, setModelStrength, setStageStrength } from "./modelConfig.js";
|
|
23
|
+
import { DEFAULT_STAGE_STRENGTH, effectiveStageStrengths, validateAdaptiveConfig } from "./modelSelection.js";
|
|
24
|
+
import { probeModel } from "./modelProbe.js";
|
|
25
|
+
function printUsage(title, rows) {
|
|
26
|
+
if (!rows.length)
|
|
27
|
+
return;
|
|
28
|
+
console.log(`\n${title}`);
|
|
29
|
+
for (const [key, c] of rows) {
|
|
30
|
+
const value = c.reportedRuns ? `輸入 ${c.inputTokens}、輸出 ${c.outputTokens}、合計 ${c.tokens} tokens` : "未回報或回報狀態不明";
|
|
31
|
+
console.log(` ${key}: ${value};${c.runs} 次(未回報 ${c.unreportedRuns}、舊紀錄不明 ${c.legacyRuns})`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
22
34
|
function mustGetRun(id) {
|
|
23
35
|
const run = getRun(id);
|
|
24
36
|
if (!run)
|
|
@@ -107,6 +119,7 @@ program
|
|
|
107
119
|
.option("--max-agent-runs <n>", "單一 run 最多執行幾次 agent(預設取 flow.config.json 的 maxAgentRuns)")
|
|
108
120
|
.option("--manual-plan", "計畫通過 AI 審查後,仍停下來等你確認", false)
|
|
109
121
|
.option("--cycle <agents>", "參與的 agent,例如 claude,codex,gemini(順序不影響分工)")
|
|
122
|
+
.option("--model-mode <mode>", "這次 run 的模型模式:balanced 或 adaptive")
|
|
110
123
|
.action(async (opts) => {
|
|
111
124
|
const requirement = opts.reqFile ? readFileSync(opts.reqFile, "utf8") : opts.req;
|
|
112
125
|
if (!requirement?.trim())
|
|
@@ -116,12 +129,17 @@ program
|
|
|
116
129
|
if (!base)
|
|
117
130
|
throw new Error("目前不在任何分支上,請用 --base 指定基底分支");
|
|
118
131
|
const cycle = await resolveCycle(opts.cycle);
|
|
132
|
+
const cfg = loadRepoConfig();
|
|
133
|
+
const modelMode = opts.modelMode ?? cfg.modelSelection.mode;
|
|
134
|
+
if (modelMode !== "balanced" && modelMode !== "adaptive")
|
|
135
|
+
throw new Error(`未知的模型模式:${modelMode}`);
|
|
136
|
+
if (modelMode === "adaptive")
|
|
137
|
+
validateAdaptiveConfig(cfg, cycle);
|
|
119
138
|
const id = `f-${Date.now().toString(36)}`;
|
|
120
139
|
const branch = `flow/${id}`;
|
|
121
140
|
console.log(`[${id}] 🌿 從 ${base} 建立 worktree(分支 ${branch})`);
|
|
122
141
|
await addWorktree(root, worktreeDir(id), base, branch);
|
|
123
142
|
console.log(`[${id}] 🤝 參與的 agent:${cycle.join("、")}(角色隨機分配)`);
|
|
124
|
-
const cfg = loadRepoConfig();
|
|
125
143
|
const now = new Date().toISOString();
|
|
126
144
|
// worktree 一建好就寫入紀錄:之後在任何地方中斷,都能用 resume 接續或用 clean 清掉
|
|
127
145
|
const run = saveRun({
|
|
@@ -134,6 +152,7 @@ program
|
|
|
134
152
|
maxAgentRuns: opts.maxAgentRuns ? Number(opts.maxAgentRuns) : cfg.maxAgentRuns,
|
|
135
153
|
cycle,
|
|
136
154
|
attempts: {},
|
|
155
|
+
modelMode,
|
|
137
156
|
taskIndex: 0,
|
|
138
157
|
taskPhase: "tests",
|
|
139
158
|
createdAt: now,
|
|
@@ -162,13 +181,15 @@ program
|
|
|
162
181
|
.option("--max-agent-runs <n>", "調整 agent 執行次數上限")
|
|
163
182
|
.action(async (id, opts) => {
|
|
164
183
|
let run = mustGetRun(id);
|
|
184
|
+
if (run.modelMode === "adaptive")
|
|
185
|
+
validateAdaptiveConfig(loadRepoConfig(), run.cycle);
|
|
165
186
|
if (opts.maxAgentRuns)
|
|
166
187
|
run = { ...run, maxAgentRuns: Number(opts.maxAgentRuns) };
|
|
167
188
|
if (run.stage === "paused") {
|
|
168
189
|
run = { ...run, stage: run.pausedStage ?? "spec", pausedStage: undefined, pauseReason: undefined };
|
|
169
190
|
}
|
|
170
191
|
if (run.stage === "failed") {
|
|
171
|
-
run = { ...run, stage: run.failedStage ?? "spec", attempts: {}, failedStage: undefined, failureReason: undefined };
|
|
192
|
+
run = { ...run, stage: run.failedStage ?? "spec", attempts: {}, modelRetryAttempts: {}, failedStage: undefined, failureReason: undefined };
|
|
172
193
|
}
|
|
173
194
|
await drive(saveRun(run));
|
|
174
195
|
});
|
|
@@ -215,8 +236,27 @@ program
|
|
|
215
236
|
if (Object.keys(byAgent).length) {
|
|
216
237
|
console.log("\n各 agent 用量");
|
|
217
238
|
for (const [agent, c] of Object.entries(byAgent)) {
|
|
218
|
-
console.log(` ${agent.padEnd(10)} ${String(c.runs).padStart(3)} 次 ${String(c.tokens).padStart(9)} tokens`);
|
|
239
|
+
console.log(` ${agent.padEnd(10)} ${String(c.runs).padStart(3)} 次 ${String(c.tokens).padStart(9)} 已回報 tokens(未回報 ${c.unreportedRuns}、舊紀錄不明 ${c.legacyRuns}${c.legacyTokens ? `,原始數字 ${c.legacyTokens} tokens` : ""})`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const byStage = usageByStage(id);
|
|
243
|
+
const stageOrder = [...Object.keys(DEFAULT_STAGE_STRENGTH), "其他"];
|
|
244
|
+
printUsage("各階段用量(同一步驟的所有任務合計)", Object.entries(byStage).sort(([a], [b]) => stageOrder.indexOf(a) - stageOrder.indexOf(b)));
|
|
245
|
+
const taskNo = (key) => /^T-(\d+)$/.exec(key) ? Number(key.slice(2)) : Infinity;
|
|
246
|
+
printUsage("各任務用量(寫測試、實作、任務審查、任務修正)", Object.entries(usageByTask(id)).sort(([a], [b]) => taskNo(a) - taskNo(b)));
|
|
247
|
+
printUsage("各模型與步驟用量(只加總明確回報)", Object.entries(usageByModelStage(id)));
|
|
248
|
+
const byStrength = usageByStrength(id);
|
|
249
|
+
const reportedTotal = Object.values(byStrength).reduce((sum, entry) => sum + entry.tokens, 0);
|
|
250
|
+
if (Object.keys(byStrength).length) {
|
|
251
|
+
console.log("\n模型強度用量(占比只計入明確回報)");
|
|
252
|
+
for (const strength of ["low", "medium", "high", "未知"]) {
|
|
253
|
+
const entry = byStrength[strength];
|
|
254
|
+
if (!entry)
|
|
255
|
+
continue;
|
|
256
|
+
const share = reportedTotal ? `${(entry.tokens / reportedTotal * 100).toFixed(1)}%` : "無法計算";
|
|
257
|
+
console.log(` ${strength}: ${entry.tokens} tokens,占比 ${share},呼叫 ${entry.runs} 次(未回報 ${entry.unreportedRuns}、舊紀錄不明 ${entry.legacyRuns})`);
|
|
219
258
|
}
|
|
259
|
+
console.log(` 高強度呼叫:${byStrength.high?.runs ?? 0} 次`);
|
|
220
260
|
}
|
|
221
261
|
const subs = listSubstitutions(id);
|
|
222
262
|
if (subs.length) {
|
|
@@ -237,6 +277,18 @@ program
|
|
|
237
277
|
// ───────────── agent 管理:讀寫 flow.config.json 的 agents 與 cycle ─────────────
|
|
238
278
|
const configPath = () => join(projectRoot(), "flow.config.json");
|
|
239
279
|
const collect = (value, prev = []) => [...prev, value];
|
|
280
|
+
const parseStrength = (value) => {
|
|
281
|
+
const result = ModelStrength.safeParse(value);
|
|
282
|
+
if (!result.success)
|
|
283
|
+
throw new Error(`未知的模型強度:${value}(請使用 low、medium 或 high)`);
|
|
284
|
+
return result.data;
|
|
285
|
+
};
|
|
286
|
+
const parseModelStage = (value) => {
|
|
287
|
+
const result = ModelStage.safeParse(value);
|
|
288
|
+
if (!result.success)
|
|
289
|
+
throw new Error(`未知的 LLM 階段:${value}`);
|
|
290
|
+
return result.data;
|
|
291
|
+
};
|
|
240
292
|
function applyEdit(edit, done) {
|
|
241
293
|
const before = readRawConfig(configPath());
|
|
242
294
|
const { cfg, changes } = edit(before);
|
|
@@ -279,8 +331,9 @@ agent
|
|
|
279
331
|
.requiredOption("--adapter <adapter>", "claude、codex、gemini 或 command")
|
|
280
332
|
.option("--model <model>", "模型名稱")
|
|
281
333
|
.option("--extra-arg <arg>", "額外參數,可重複;以 - 開頭時寫成 --extra-arg=--sandbox", collect)
|
|
334
|
+
.option("--model-probe-arg <arg>", "自訂 command 的模型探測命令參數,可重複", collect)
|
|
282
335
|
.action((name, command, opts) => {
|
|
283
|
-
applyEdit((cfg) => addAgent(cfg, name, { adapter: opts.adapter, model: opts.model, extraArgs: opts.extraArg, command: command.length ? command : undefined }), `已新增 ${name};要讓它參與請用 agent cycle`);
|
|
336
|
+
applyEdit((cfg) => addAgent(cfg, name, { adapter: opts.adapter, model: opts.model, extraArgs: opts.extraArg, modelProbe: opts.modelProbeArg, command: command.length ? command : undefined }), `已新增 ${name};要讓它參與請用 agent cycle`);
|
|
284
337
|
});
|
|
285
338
|
agent
|
|
286
339
|
.command("set <name> [command...]")
|
|
@@ -288,8 +341,9 @@ agent
|
|
|
288
341
|
.option("--adapter <adapter>", "claude、codex、gemini 或 command")
|
|
289
342
|
.option("--model <model>", "模型名稱")
|
|
290
343
|
.option("--extra-arg <arg>", "額外參數,可重複,會整個取代原本的設定", collect)
|
|
344
|
+
.option("--model-probe-arg <arg>", "自訂 command 的模型探測命令參數,可重複,會整個取代", collect)
|
|
291
345
|
.action((name, command, opts) => {
|
|
292
|
-
applyEdit((cfg) => setAgent(cfg, name, { adapter: opts.adapter, model: opts.model, extraArgs: opts.extraArg, command: command.length ? command : undefined }), `已更新 ${name}`);
|
|
346
|
+
applyEdit((cfg) => setAgent(cfg, name, { adapter: opts.adapter, model: opts.model, extraArgs: opts.extraArg, modelProbe: opts.modelProbeArg, command: command.length ? command : undefined }), `已更新 ${name}`);
|
|
293
347
|
});
|
|
294
348
|
agent
|
|
295
349
|
.command("remove <name>")
|
|
@@ -346,6 +400,86 @@ agent
|
|
|
346
400
|
console.log("");
|
|
347
401
|
await doctor();
|
|
348
402
|
});
|
|
403
|
+
// ───────────── 模型清單與各階段強度 ─────────────
|
|
404
|
+
const model = program.command("model").description("設定模型強度並檢查目前帳號能否呼叫模型");
|
|
405
|
+
model.command("add <agent> <name>")
|
|
406
|
+
.requiredOption("--strength <strength>", "low、medium 或 high")
|
|
407
|
+
.action(async (agent, name, opts) => {
|
|
408
|
+
const strength = parseStrength(opts.strength);
|
|
409
|
+
const before = readRawConfig(configPath());
|
|
410
|
+
const cfg = loadRepoConfig();
|
|
411
|
+
const def = cfg.agents[agent];
|
|
412
|
+
if (!def)
|
|
413
|
+
throw new Error(`未定義的 agent:${agent}`);
|
|
414
|
+
const next = addModel(before, agent, name, strength);
|
|
415
|
+
console.log("模型檢查會送出最短請求,可能耗用少量 token。");
|
|
416
|
+
const checked = await probeModel(def, name);
|
|
417
|
+
if (checked.status !== "ok")
|
|
418
|
+
throw new Error(`${agent} 的模型 ${name} 未加入:${checked.reason}`);
|
|
419
|
+
writeRawConfig(configPath(), next);
|
|
420
|
+
console.log(`✅ 已新增 ${agent} 的模型 ${name}(${strength})${checked.resolvedModel ? ` → ${checked.resolvedModel}` : ""}${def.adapter === "command" ? "(由自訂探測命令回報)" : ""};探測可能耗用少量 token`);
|
|
421
|
+
});
|
|
422
|
+
model.command("set <agent> <name>")
|
|
423
|
+
.requiredOption("--strength <strength>", "low、medium 或 high")
|
|
424
|
+
.action((agent, name, opts) => {
|
|
425
|
+
writeRawConfig(configPath(), setModelStrength(readRawConfig(configPath()), agent, name, parseStrength(opts.strength)));
|
|
426
|
+
console.log(`✅ 已將 ${agent} 的模型 ${name} 強度設為 ${opts.strength};此指令不重驗可用性`);
|
|
427
|
+
});
|
|
428
|
+
model.command("remove <agent> <name>").action((agent, name) => {
|
|
429
|
+
writeRawConfig(configPath(), removeModel(readRawConfig(configPath()), agent, name));
|
|
430
|
+
console.log(`✅ 已移除 ${agent} 的模型 ${name}`);
|
|
431
|
+
});
|
|
432
|
+
model.command("list [agent]").action((agent) => {
|
|
433
|
+
const cfg = loadRepoConfig();
|
|
434
|
+
const names = agent ? [agent] : Object.keys(cfg.agents);
|
|
435
|
+
for (const name of names) {
|
|
436
|
+
const def = cfg.agents[name];
|
|
437
|
+
if (!def)
|
|
438
|
+
throw new Error(`未定義的 agent:${name}`);
|
|
439
|
+
console.log(`${name}(${def.adapter})`);
|
|
440
|
+
for (const item of def.models ?? [])
|
|
441
|
+
console.log(` ${item.name} ${item.strength}`);
|
|
442
|
+
if (!def.models?.length)
|
|
443
|
+
console.log(" 尚未設定模型");
|
|
444
|
+
}
|
|
445
|
+
console.log("清單只顯示設定內容;要確認當前帳號是否可用,請執行 model check。");
|
|
446
|
+
});
|
|
447
|
+
model.command("check [agent]").action(async (agent) => {
|
|
448
|
+
const cfg = loadRepoConfig();
|
|
449
|
+
const names = agent ? [agent] : Object.keys(cfg.agents);
|
|
450
|
+
console.log("模型重驗會逐一送出短請求,可能耗用少量 token。");
|
|
451
|
+
for (const name of names) {
|
|
452
|
+
const def = cfg.agents[name];
|
|
453
|
+
if (!def)
|
|
454
|
+
throw new Error(`未定義的 agent:${name}`);
|
|
455
|
+
for (const item of def.models ?? []) {
|
|
456
|
+
const checked = await probeModel(def, item.name);
|
|
457
|
+
const at = new Date().toISOString();
|
|
458
|
+
const mark = checked.status === "ok" ? "✅" : checked.status === "unverifiable" ? "⚪" : "❌";
|
|
459
|
+
const result = checked.status === "ok" ? `可呼叫${checked.resolvedModel ? ` → ${checked.resolvedModel}` : ""}${def.adapter === "command" ? "(由自訂探測命令回報)" : ""}` : checked.reason;
|
|
460
|
+
console.log(`${mark} ${at} ${name} ${item.name}: ${result}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
model.command("mode [mode]").action((mode) => {
|
|
465
|
+
if (!mode)
|
|
466
|
+
return console.log(`模型模式:${loadRepoConfig().modelSelection.mode}`);
|
|
467
|
+
if (mode !== "balanced" && mode !== "adaptive")
|
|
468
|
+
throw new Error(`未知的模型模式:${mode}`);
|
|
469
|
+
writeRawConfig(configPath(), setModelMode(readRawConfig(configPath()), mode));
|
|
470
|
+
console.log(`✅ 模型模式已設為 ${mode}`);
|
|
471
|
+
});
|
|
472
|
+
model.command("stage [stage] [strength]").action((stage, strength) => {
|
|
473
|
+
if (!stage || !strength) {
|
|
474
|
+
const all = effectiveStageStrengths(loadRepoConfig());
|
|
475
|
+
const stages = stage ? [parseModelStage(stage)] : Object.keys(all);
|
|
476
|
+
for (const s of stages)
|
|
477
|
+
console.log(`${s}: ${all[s].strength}${all[s].custom ? "(自訂)" : "(預設)"}`);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
writeRawConfig(configPath(), setStageStrength(readRawConfig(configPath()), parseModelStage(stage), parseStrength(strength)));
|
|
481
|
+
console.log(`✅ ${stage} 的最低強度已設為 ${strength}`);
|
|
482
|
+
});
|
|
349
483
|
/** 檢查設定的 agent 是否已安裝,印出參與的 agent 與主要設定 */
|
|
350
484
|
async function doctor() {
|
|
351
485
|
const cfg = loadRepoConfig();
|
package/dist/engine.js
CHANGED
|
@@ -14,7 +14,8 @@ import { arbiterPanel, availableAgent, fixAgent, planAgent, planFixAgent, review
|
|
|
14
14
|
import { resolveAgent, runAgent, runCommand } from "./runner.js";
|
|
15
15
|
import { AcceptanceList, ArbiterResult, ConsistentReviewResult, RepoConfig, TaskList, } from "./schemas.js";
|
|
16
16
|
import { addSubstitution, addUsage, agentRuns, saveRun } from "./store.js";
|
|
17
|
-
import {
|
|
17
|
+
import { clearModelReviewFailure, clearModelReviewStage, recordModelReviewFailure, selectModel } from "./modelSelection.js";
|
|
18
|
+
import { orderTasks, taskAcceptance, validateTaskComplexity } from "./tasks.js";
|
|
18
19
|
import { readJsonFile, renderPrompt, tail } from "./util.js";
|
|
19
20
|
// ───────────────────────── 共用工具 ─────────────────────────
|
|
20
21
|
const info = (run, msg) => console.log(`[${run.id}] ${msg}`);
|
|
@@ -60,10 +61,16 @@ async function agentStep(run, planned, step, prompt, mode) {
|
|
|
60
61
|
info(run, `🔁 ${agent} 額度已用完,${step} 由 ${sub} 代打${note ? `(注意:${note})` : ""}`);
|
|
61
62
|
agent = sub;
|
|
62
63
|
}
|
|
64
|
+
const selected = selectModel(run, cfg, agent, step, /^T-\d+-/.test(step) ? loadOrderedTasks(run)[run.taskIndex]?.complexity : undefined, mode.kind === "review" ? agent : undefined);
|
|
65
|
+
info(run, `🤖 ${step}:${agent} 使用 ${selected.name ?? "CLI 預設(名稱未知)"}${selected.insufficient ? `(低於目標 ${selected.targetStrength})` : ""}`);
|
|
63
66
|
const callKey = handoffKey(run, step, mode.slot ?? 0, agent);
|
|
64
67
|
prepareHandoff(run.id, callKey, handoffTarget(run), mode.blind ?? false);
|
|
65
|
-
const r = await runAgent(agent, resolveAgent(cfg, agent), target(run, step, agent), prompt);
|
|
66
|
-
|
|
68
|
+
const r = await runAgent(agent, { ...resolveAgent(cfg, agent), model: selected.name }, { ...target(run, step, agent), strength: selected.strength, targetStrength: selected.targetStrength }, prompt);
|
|
69
|
+
if (r.resolvedModel && r.resolvedModel !== selected.name)
|
|
70
|
+
info(run, ` ↳ CLI 回報實際模型:${r.resolvedModel}`);
|
|
71
|
+
addUsage(run.id, { stage: step, agent, model: selected.name, resolvedModel: r.resolvedModel,
|
|
72
|
+
strength: selected.strength, targetStrength: selected.targetStrength, usageReported: r.usageReported,
|
|
73
|
+
inputTokens: r.inputTokens, outputTokens: r.outputTokens });
|
|
67
74
|
if (!r.quotaExhausted) {
|
|
68
75
|
reportMeta(run, agent, r);
|
|
69
76
|
return { r, agent, step, callKey };
|
|
@@ -205,6 +212,9 @@ function validatePlan(run) {
|
|
|
205
212
|
const tasks = readJsonFile(flowFile(run, "tasks.json"), TaskList);
|
|
206
213
|
if (!tasks.ok)
|
|
207
214
|
return tasks.error;
|
|
215
|
+
const complexityError = validateTaskComplexity(tasks.data, run.modelMode ?? "balanced");
|
|
216
|
+
if (complexityError)
|
|
217
|
+
return complexityError;
|
|
208
218
|
return orderTasks(tasks.data, new Set(ids));
|
|
209
219
|
}
|
|
210
220
|
function acceptPlan(run, ordered) {
|
|
@@ -215,7 +225,7 @@ function announceTasks(run, ordered) {
|
|
|
215
225
|
info(run, `📋 共 ${ordered.length} 個任務:${ordered.map((t) => t.id).join(" → ")}`);
|
|
216
226
|
ordered.forEach((t, i) => {
|
|
217
227
|
const a = taskAgents(run.cycle, i, cfg.tddSplit, run.id);
|
|
218
|
-
info(run, ` ${t.id} 測試:${a.tests} 實作:${a.code}`);
|
|
228
|
+
info(run, ` ${t.id} 測試:${a.tests} 實作:${a.code} 審查:${a.review}`);
|
|
219
229
|
});
|
|
220
230
|
}
|
|
221
231
|
/** 計畫定案後:預設直接開始實作;--manual-plan 時才停下來等人 */
|
|
@@ -272,13 +282,14 @@ async function planReviewStage(run) {
|
|
|
272
282
|
if (tampered.length)
|
|
273
283
|
info(run, ` ↩️ 已還原審查者修改的檔案:${tampered.join(", ")}`);
|
|
274
284
|
if (!r.ok)
|
|
275
|
-
return retry(run, "plan-review-run", `Agent 執行失敗:${r.summary}`, "plan_review");
|
|
285
|
+
return retry(recordModelReviewFailure(run, "plan-review", reviewer), "plan-review-run", `Agent 執行失敗:${r.summary}`, "plan_review");
|
|
276
286
|
const review = readJsonFile(flowFile(run, "plan-review.json"), ConsistentReviewResult);
|
|
277
287
|
if (!review.ok)
|
|
278
|
-
return retry(run, "plan-review-run", review.error, "plan_review");
|
|
288
|
+
return retry(recordModelReviewFailure(run, "plan-review", reviewer), "plan-review-run", review.error, "plan_review");
|
|
279
289
|
const handoffError = finishHandoff(run, outcome, "reviewer", { target: "plan", verdict: review.data.verdict });
|
|
280
290
|
if (handoffError)
|
|
281
|
-
return retry(run, "plan-review-run", handoffError, "plan_review");
|
|
291
|
+
return retry(recordModelReviewFailure(run, "plan-review", reviewer), "plan-review-run", handoffError, "plan_review");
|
|
292
|
+
run = clearModelReviewFailure(run, "plan-review", reviewer);
|
|
282
293
|
// 審查紀錄移到 worktree 外面:之後的仲裁者看不到是哪一家提的意見
|
|
283
294
|
mkdirSync(join(runDir(run.id), "reviews"), { recursive: true });
|
|
284
295
|
renameSync(flowFile(run, "plan-review.json"), join(runDir(run.id), "reviews", `plan-review-${round}-${reviewer}.json`));
|
|
@@ -294,6 +305,10 @@ async function planReviewStage(run) {
|
|
|
294
305
|
issueLines.push(...lines);
|
|
295
306
|
issues.push(opinion(reviewer, lines));
|
|
296
307
|
}
|
|
308
|
+
run = clearModelReviewStage(run, "plan-review");
|
|
309
|
+
const attempts = { ...run.attempts };
|
|
310
|
+
delete attempts["plan-review-run"];
|
|
311
|
+
run = { ...run, attempts };
|
|
297
312
|
if (!firstObjector)
|
|
298
313
|
return planSettled(run, "plan-review");
|
|
299
314
|
const report = `計畫審查要求修改:\n\n${issues.join("\n\n")}`;
|
|
@@ -341,7 +356,8 @@ async function planFixStage(run) {
|
|
|
341
356
|
const handoffError = finishHandoff(run, outcome, "writer");
|
|
342
357
|
if (handoffError) {
|
|
343
358
|
restorePlan(run, snap);
|
|
344
|
-
|
|
359
|
+
// 計畫已還原,要保留原本的審查意見,否則下一次修正不知道要改什麼
|
|
360
|
+
return retry(run, "plan-fix", `${feedback}\n\n另外,交接回覆不合格,本次修改已還原:${handoffError}`, "plan_fix");
|
|
345
361
|
}
|
|
346
362
|
acceptPlan(run, ordered);
|
|
347
363
|
writeFileSync(flowFile(run, "feedback.md"), feedback); // 保留審查意見,讓下一輪審查者知道上次提了什麼
|
|
@@ -529,6 +545,8 @@ async function taskReviewStep(run, task, progress, taskJson, acceptanceJson) {
|
|
|
529
545
|
prompt: (reviewer, authors) => renderPrompt("task-review", { reviewer, authors, task: taskJson, acceptance: acceptanceJson }),
|
|
530
546
|
saveAs: (reviewer) => `review-${task.id}-${reviewer}.json`,
|
|
531
547
|
testAuthor: run.lastTestsAuthor,
|
|
548
|
+
// 輪流交換角色:優先由排定的審查者審查,讓各家用量平均
|
|
549
|
+
prefer: taskAgents(run.cycle, run.taskIndex, loadRepoConfig().tddSplit, run.id).review,
|
|
532
550
|
// 未結交接事項可能屬於後面的任務,由最後的整體審查把關
|
|
533
551
|
gate: false,
|
|
534
552
|
runKey: `${task.id}:review-run`,
|
|
@@ -536,7 +554,7 @@ async function taskReviewStep(run, task, progress, taskJson, acceptanceJson) {
|
|
|
536
554
|
});
|
|
537
555
|
if ("run" in result)
|
|
538
556
|
return result.run;
|
|
539
|
-
const reviewed = succeed(
|
|
557
|
+
const reviewed = succeed(result.state, `${task.id}:review-run`, "implement");
|
|
540
558
|
if (!result.objector)
|
|
541
559
|
return { ...succeed(reviewed, key, "implement"), taskPhase: "verify" };
|
|
542
560
|
return {
|
|
@@ -646,7 +664,7 @@ async function applyFix(run, opts) {
|
|
|
646
664
|
const handoffError = finishHandoff(run, outcome, "writer");
|
|
647
665
|
if (handoffError) {
|
|
648
666
|
await resetTo(repo, before);
|
|
649
|
-
return again(handoffError);
|
|
667
|
+
return again(`${feedback}\n\n另外,交接回覆不合格,本次修正已還原:${handoffError}`);
|
|
650
668
|
}
|
|
651
669
|
return { agent: actual };
|
|
652
670
|
}
|
|
@@ -662,7 +680,7 @@ async function fixStage(run) {
|
|
|
662
680
|
if ("run" in result)
|
|
663
681
|
return result.run;
|
|
664
682
|
// 修正者成為新的作者,下一輪審查會換成別人
|
|
665
|
-
return { ...
|
|
683
|
+
return { ...succeed(run, "fix", "verify"), lastWriter: result.agent };
|
|
666
684
|
}
|
|
667
685
|
/**
|
|
668
686
|
* 由作者以外的審查小組審查 base 之後的變更;回傳第一位要求修改的審查者與所有意見,
|
|
@@ -672,7 +690,7 @@ async function fixStage(run) {
|
|
|
672
690
|
async function codeReview(run, opts) {
|
|
673
691
|
const cfg = loadRepoConfig();
|
|
674
692
|
const repo = worktreeDir(run.id);
|
|
675
|
-
const panel = reviewers(run.cycle, run.lastWriter, cfg.reviewQuorum, opts.seed, opts.testAuthor);
|
|
693
|
+
const panel = reviewers(run.cycle, run.lastWriter, cfg.reviewQuorum, opts.seed, opts.testAuthor, opts.prefer);
|
|
676
694
|
writeFileSync(flowFile(run, "diff.patch"), await git(repo, "diff", `${opts.base}...HEAD`));
|
|
677
695
|
const authors = [...new Set((await git(repo, "log", "--format=%s", `${opts.base}..HEAD`)).match(/\[[^\]]+\]$/gm) ?? [])]
|
|
678
696
|
.map((s) => s.slice(1, -1));
|
|
@@ -691,14 +709,16 @@ async function codeReview(run, opts) {
|
|
|
691
709
|
if (tampered.length)
|
|
692
710
|
info(run, ` ↩️ 已還原審查者修改的檔案:${tampered.join(", ")}`);
|
|
693
711
|
if (!r.ok)
|
|
694
|
-
return { run: retry(run, opts.runKey, `Agent 執行失敗:${r.summary}`, opts.backTo) };
|
|
712
|
+
return { run: retry(opts.step === "review" ? recordModelReviewFailure(run, "review", reviewer) : run, opts.runKey, `Agent 執行失敗:${r.summary}`, opts.backTo) };
|
|
695
713
|
const review = readJsonFile(flowFile(run, "review.json"), ConsistentReviewResult);
|
|
696
714
|
if (!review.ok)
|
|
697
|
-
return { run: retry(run, opts.runKey, review.error, opts.backTo) };
|
|
715
|
+
return { run: retry(opts.step === "review" ? recordModelReviewFailure(run, "review", reviewer) : run, opts.runKey, review.error, opts.backTo) };
|
|
698
716
|
const gate = opts.gate ? { target: "code", verdict: review.data.verdict } : undefined;
|
|
699
717
|
const handoffError = finishHandoff(run, outcome, "reviewer", gate);
|
|
700
718
|
if (handoffError)
|
|
701
|
-
return { run: retry(run, opts.runKey, handoffError, opts.backTo) };
|
|
719
|
+
return { run: retry(opts.step === "review" ? recordModelReviewFailure(run, "review", reviewer) : run, opts.runKey, handoffError, opts.backTo) };
|
|
720
|
+
if (opts.step === "review")
|
|
721
|
+
run = clearModelReviewFailure(run, "review", reviewer);
|
|
702
722
|
renameSync(flowFile(run, "review.json"), flowFile(run, opts.saveAs(reviewer)));
|
|
703
723
|
if (review.data.verdict === "approve") {
|
|
704
724
|
info(run, ` ✓ ${reviewer} 核准`);
|
|
@@ -710,7 +730,11 @@ async function codeReview(run, opts) {
|
|
|
710
730
|
.filter((i) => i.status !== "met")
|
|
711
731
|
.map((i) => reviewIssue(i.criterion, i.status, i.note))));
|
|
712
732
|
}
|
|
713
|
-
|
|
733
|
+
if (opts.step === "review")
|
|
734
|
+
run = clearModelReviewStage(run, "review");
|
|
735
|
+
const attempts = { ...run.attempts };
|
|
736
|
+
delete attempts[opts.runKey];
|
|
737
|
+
return { state: { ...run, attempts }, objector, issues };
|
|
714
738
|
}
|
|
715
739
|
async function reviewStage(run) {
|
|
716
740
|
const result = await codeReview(run, {
|
|
@@ -727,9 +751,9 @@ async function reviewStage(run) {
|
|
|
727
751
|
if ("run" in result)
|
|
728
752
|
return result.run;
|
|
729
753
|
if (!result.objector)
|
|
730
|
-
return succeed(
|
|
754
|
+
return succeed(result.state, "review", "pr");
|
|
731
755
|
return {
|
|
732
|
-
...retry(
|
|
756
|
+
...retry(result.state, "review", `程式碼審查要求修改:\n\n${result.issues.join("\n\n")}`, "fix"),
|
|
733
757
|
fixSource: "review",
|
|
734
758
|
lastReviewer: result.objector,
|
|
735
759
|
};
|
package/dist/handoff.js
CHANGED
|
@@ -52,10 +52,9 @@ export function previewHandoff(ledger, callKey, source, response, role) {
|
|
|
52
52
|
const issue = issues.find((item) => item.id === disposition.id);
|
|
53
53
|
if (!issue)
|
|
54
54
|
throw new Error(`找不到交接事項:${disposition.id}`);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
throw new Error(`交接事項已結案:${disposition.id}`);
|
|
55
|
+
// 參考資訊沒有結案流程,已結案的事項也不需再處置:略過即可,不讓整個步驟因此重試
|
|
56
|
+
if (issue.kind !== "action" || issue.status === "resolved" || issue.status === "accepted")
|
|
57
|
+
continue;
|
|
59
58
|
if (role === "writer" && disposition.status !== "proposed_resolved")
|
|
60
59
|
throw new Error("作者只能提出已修正,不能自行結案");
|
|
61
60
|
if (role === "reviewer" && disposition.status === "proposed_resolved")
|
|
@@ -74,13 +73,20 @@ export function mergeHandoff(id, callKey, source, response, role) {
|
|
|
74
73
|
/** 只把目前步驟需要處理的事項投影給 agent。 */
|
|
75
74
|
export function prepareHandoff(id, _callKey, target, blind) {
|
|
76
75
|
const items = readHandoff(id).issues.filter((item) => item.targetStage === target && (item.kind === "info" || item.status === "open" || item.status === "proposed_resolved"));
|
|
77
|
-
const
|
|
76
|
+
const render = (item) => {
|
|
78
77
|
const source = blind ? "" : `\n來源:${item.source.stage}/${item.source.agent}`;
|
|
79
78
|
const resolution = item.resolution ? `\n處理理由:${item.resolution.reason}\n處理證據:${item.resolution.evidence}` : "";
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
const status = item.kind === "action" ? `\n狀態:${item.status}` : "";
|
|
80
|
+
return `### ${item.id}:${item.summary}\n類型:${item.kind}\n證據:${item.evidence}${status}${resolution}${source}`;
|
|
81
|
+
};
|
|
82
|
+
const actions = items.filter((item) => item.kind === "action").map(render);
|
|
83
|
+
const infos = items.filter((item) => item.kind !== "action").map(render);
|
|
84
|
+
const sections = [
|
|
85
|
+
actions.length ? `## 待處理事項(action,可在 dispositions 處置)\n\n${actions.join("\n\n")}` : "",
|
|
86
|
+
infos.length ? `## 參考資訊(info,只供參考,不要放進 dispositions)\n\n${infos.join("\n\n")}` : "",
|
|
87
|
+
].filter(Boolean);
|
|
82
88
|
mkdirSync(flowDir(id), { recursive: true });
|
|
83
|
-
writeFileSync(join(flowDir(id), "handoff-context.md"), `# 待處理交接事項\n\n${
|
|
89
|
+
writeFileSync(join(flowDir(id), "handoff-context.md"), `# 待處理交接事項\n\n${sections.length ? sections.join("\n\n") : "目前沒有待處理事項。"}\n`);
|
|
84
90
|
rmSync(responsePath(id), { force: true });
|
|
85
91
|
}
|
|
86
92
|
export function validateHandoffResponse(id) {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ModelStage, ModelStrength, RepoConfig } from "./schemas.js";
|
|
2
|
+
const agentsOf = (cfg) => ({ ...(cfg.agents ?? {}) });
|
|
3
|
+
const modelsOf = (agent) => [...(agent.models ?? [])];
|
|
4
|
+
function withAgent(cfg, agent, edit) {
|
|
5
|
+
const agents = agentsOf(cfg);
|
|
6
|
+
if (!agents[agent])
|
|
7
|
+
throw new Error(`未定義的 agent:${agent}`);
|
|
8
|
+
agents[agent] = edit({ ...agents[agent] });
|
|
9
|
+
const next = { ...cfg, agents };
|
|
10
|
+
RepoConfig.parse(next);
|
|
11
|
+
return next;
|
|
12
|
+
}
|
|
13
|
+
export function addModel(cfg, agent, name, strength) {
|
|
14
|
+
ModelStrength.parse(strength);
|
|
15
|
+
if (!name.trim())
|
|
16
|
+
throw new Error("模型名稱不可為空");
|
|
17
|
+
if (name !== name.trim())
|
|
18
|
+
throw new Error("模型名稱前後不可有空白");
|
|
19
|
+
return withAgent(cfg, agent, (def) => {
|
|
20
|
+
const models = modelsOf(def);
|
|
21
|
+
if (models.some((m) => m.name === name))
|
|
22
|
+
throw new Error(`agent ${agent} 的模型名稱 ${name} 重複`);
|
|
23
|
+
return { ...def, models: [...models, { name, strength }] };
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export function setModelStrength(cfg, agent, name, strength) {
|
|
27
|
+
ModelStrength.parse(strength);
|
|
28
|
+
return withAgent(cfg, agent, (def) => {
|
|
29
|
+
const models = modelsOf(def);
|
|
30
|
+
if (!models.some((m) => m.name === name))
|
|
31
|
+
throw new Error(`agent ${agent} 沒有模型 ${name}`);
|
|
32
|
+
return { ...def, models: models.map((m) => m.name === name ? { ...m, strength } : m) };
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/** 有 cycle 時看 cycle,否則看全部 agent;只做設定層的保守檢查。 */
|
|
36
|
+
function assertModelsPresent(cfg) {
|
|
37
|
+
const agents = agentsOf(cfg);
|
|
38
|
+
const affected = cfg.cycle ?? Object.keys(agents);
|
|
39
|
+
const missing = affected.filter((name) => !agents[name] || !modelsOf(agents[name]).length);
|
|
40
|
+
if (missing.length)
|
|
41
|
+
throw new Error(`以下 agent 缺少模型:${missing.join("、")}`);
|
|
42
|
+
}
|
|
43
|
+
export function removeModel(cfg, agent, name) {
|
|
44
|
+
const next = withAgent(cfg, agent, (def) => {
|
|
45
|
+
const models = modelsOf(def);
|
|
46
|
+
if (!models.some((m) => m.name === name))
|
|
47
|
+
throw new Error(`agent ${agent} 沒有模型 ${name}`);
|
|
48
|
+
return { ...def, models: models.filter((m) => m.name !== name) };
|
|
49
|
+
});
|
|
50
|
+
const lastModelRemoved = !modelsOf(agentsOf(next)[agent]).length;
|
|
51
|
+
if (lastModelRemoved || (cfg.modelSelection ?? {}).mode === "adaptive")
|
|
52
|
+
assertModelsPresent(next);
|
|
53
|
+
return next;
|
|
54
|
+
}
|
|
55
|
+
export function setModelMode(cfg, mode) {
|
|
56
|
+
if (mode !== "balanced" && mode !== "adaptive")
|
|
57
|
+
throw new Error(`未知的模型模式:${mode}`);
|
|
58
|
+
if (mode === "adaptive")
|
|
59
|
+
assertModelsPresent(cfg);
|
|
60
|
+
const next = { ...cfg, modelSelection: { ...(cfg.modelSelection ?? {}), mode } };
|
|
61
|
+
RepoConfig.parse(next);
|
|
62
|
+
return next;
|
|
63
|
+
}
|
|
64
|
+
export function setStageStrength(cfg, stage, strength) {
|
|
65
|
+
const parsed = ModelStage.safeParse(stage);
|
|
66
|
+
if (!parsed.success)
|
|
67
|
+
throw new Error(`未知的 LLM 階段:${stage}`);
|
|
68
|
+
ModelStrength.parse(strength);
|
|
69
|
+
const selection = (cfg.modelSelection ?? {});
|
|
70
|
+
const next = { ...cfg, modelSelection: { ...selection, stageStrength: { ...(selection.stageStrength ?? {}), [stage]: strength } } };
|
|
71
|
+
RepoConfig.parse(next);
|
|
72
|
+
return next;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=modelConfig.js.map
|