agentflowctl 0.3.0 → 0.5.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 +166 -42
- package/dist/agentConfig.js +8 -25
- package/dist/agents/index.js +0 -4
- package/dist/arbitration.js +10 -0
- package/dist/cleanup.js +54 -0
- package/dist/cli.js +130 -60
- package/dist/config.js +2 -2
- package/dist/detect.js +82 -0
- package/dist/engine.js +62 -28
- package/dist/logs.js +173 -0
- package/dist/paths.js +4 -2
- package/dist/proc.js +1 -4
- package/dist/roles.js +70 -39
- package/dist/runner.js +25 -29
- package/dist/schemas.js +10 -6
- package/dist/setup.js +70 -0
- package/examples/flow.config.json +1 -0
- package/package.json +1 -1
package/dist/runner.js
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { ADAPTERS
|
|
5
|
-
import { AgentDef } from "./schemas.js";
|
|
4
|
+
import { ADAPTERS } from "./agents/index.js";
|
|
6
5
|
import { projectRoot, runDir } from "./paths.js";
|
|
6
|
+
import { config } from "./config.js";
|
|
7
|
+
import { CMD_AGENT, footerLine, headerLine, logSeq, STDERR_MARK } from "./logs.js";
|
|
7
8
|
import { exec, execShell } from "./proc.js";
|
|
8
9
|
import { tail } from "./util.js";
|
|
9
|
-
/** 會讓各家 CLI 改走 API 計費的環境變數;只用訂閱登入,執行 agent 時一律移除 */
|
|
10
|
-
export const API_KEY_VARS = [
|
|
11
|
-
"ANTHROPIC_API_KEY",
|
|
12
|
-
"ANTHROPIC_AUTH_TOKEN",
|
|
13
|
-
"CODEX_API_KEY",
|
|
14
|
-
"OPENAI_API_KEY",
|
|
15
|
-
"GEMINI_API_KEY",
|
|
16
|
-
"GOOGLE_API_KEY",
|
|
17
|
-
];
|
|
18
10
|
/**
|
|
19
11
|
* 判斷 agent 失敗是否因為方案額度或速率限制。
|
|
20
12
|
* 各家的錯誤訊息會隨版本變動,這裡用寬鬆的樣式比對,且只在執行失敗時才檢查,避免誤判正常輸出。
|
|
@@ -62,17 +54,13 @@ export function formatToolLine(agent, tool) {
|
|
|
62
54
|
const detail = tool.detail?.trim();
|
|
63
55
|
return detail ? `${head}: ${detail.split("\n").join("\n ")}` : head;
|
|
64
56
|
}
|
|
65
|
-
/**
|
|
57
|
+
/** 取出 flow.config.json 裡定義的 agent;沒有內建 agent */
|
|
66
58
|
export function resolveAgent(cfg, name) {
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
if (cfg.removedAgents.includes(name))
|
|
74
|
-
throw new Error(`${name} 已從設定移除(removedAgents),要使用請先 agent add ${name} --adapter ${name}`);
|
|
75
|
-
throw new Error(`未定義的 agent:${name}(請在 flow.config.json 的 agents 裡設定)`);
|
|
59
|
+
const def = cfg.agents[name];
|
|
60
|
+
if (!def)
|
|
61
|
+
throw new Error(`未定義的 agent:${name}(請先用 agent add ${name} --adapter <adapter> 新增)`);
|
|
62
|
+
const defaultModel = def.adapter === "command" ? undefined : cfg.defaultModels[def.adapter];
|
|
63
|
+
return { ...def, model: def.model ?? defaultModel };
|
|
76
64
|
}
|
|
77
65
|
/** 確認某個 agent 的 CLI 是否可以執行 */
|
|
78
66
|
export async function probeAgent(def) {
|
|
@@ -96,7 +84,7 @@ export async function runAgent(name, def, t, prompt) {
|
|
|
96
84
|
projectRoot: projectRoot(),
|
|
97
85
|
command: def.command,
|
|
98
86
|
});
|
|
99
|
-
appendLog(t.logFile,
|
|
87
|
+
appendLog(t.logFile, headerLine({ stage: t.stage, step: t.step, agent: name, adapter: def.adapter, startedAt: new Date().toISOString() }));
|
|
100
88
|
let done;
|
|
101
89
|
let lastText = "";
|
|
102
90
|
let inputTokens = 0;
|
|
@@ -104,7 +92,6 @@ export async function runAgent(name, def, t, prompt) {
|
|
|
104
92
|
const r = await exec(inv.cmd, inv.args, {
|
|
105
93
|
cwd: t.cwd,
|
|
106
94
|
env: inv.env,
|
|
107
|
-
unsetEnv: API_KEY_VARS,
|
|
108
95
|
input: inv.input,
|
|
109
96
|
onStdoutLine: (line) => {
|
|
110
97
|
if (!line.trim())
|
|
@@ -113,10 +100,12 @@ export async function runAgent(name, def, t, prompt) {
|
|
|
113
100
|
for (const ev of adapter.parse(line)) {
|
|
114
101
|
if (ev.kind === "text") {
|
|
115
102
|
lastText = ev.text;
|
|
116
|
-
|
|
103
|
+
if (config.verbose)
|
|
104
|
+
console.log(` 💬 [${name}] ${ev.text.trim().split("\n")[0]?.slice(0, 110)}`);
|
|
117
105
|
}
|
|
118
106
|
else if (ev.kind === "tool") {
|
|
119
|
-
|
|
107
|
+
if (config.verbose)
|
|
108
|
+
console.log(formatToolLine(name, ev));
|
|
120
109
|
}
|
|
121
110
|
else if (ev.kind === "usage") {
|
|
122
111
|
inputTokens += ev.inputTokens ?? 0;
|
|
@@ -129,8 +118,11 @@ export async function runAgent(name, def, t, prompt) {
|
|
|
129
118
|
},
|
|
130
119
|
});
|
|
131
120
|
if (r.stderr.trim())
|
|
132
|
-
appendLog(t.logFile,
|
|
121
|
+
appendLog(t.logFile, `${STDERR_MARK}\n${r.stderr}`);
|
|
133
122
|
const ok = r.code === 0 && (done?.ok ?? true);
|
|
123
|
+
appendLog(t.logFile, footerLine({ code: r.code, ok, endedAt: new Date().toISOString() }));
|
|
124
|
+
if (!ok)
|
|
125
|
+
console.log(` ✗ ${name} 執行失敗(結束碼 ${r.code}),可用 agentflowctl logs ${t.runId} ${logSeq(t.logFile)} 查看`);
|
|
134
126
|
const summary = done?.summary || lastText || tail(r.stdout, 2000) || tail(r.stderr, 2000);
|
|
135
127
|
const quotaExhausted = !ok && isQuotaError(`${summary}\n${done?.summary ?? ""}\n${r.stderr}\n${tail(r.stdout, 4000)}`);
|
|
136
128
|
const meta = parseResultMeta(summary) ?? parseResultMeta(lastText);
|
|
@@ -141,11 +133,15 @@ export async function runAgent(name, def, t, prompt) {
|
|
|
141
133
|
* 注意:這些指令會執行 agent 寫出來的程式碼,而且不在任何沙箱內。
|
|
142
134
|
*/
|
|
143
135
|
export async function runCommand(t, cmd) {
|
|
136
|
+
appendLog(t.logFile, headerLine({ stage: t.stage, step: t.step, agent: CMD_AGENT, startedAt: new Date().toISOString() }));
|
|
144
137
|
appendLog(t.logFile, `$ ${cmd}`);
|
|
145
|
-
|
|
138
|
+
if (config.verbose)
|
|
139
|
+
console.log(` $ ${cmd.trim().split("\n").join("\n ")}`);
|
|
146
140
|
const r = await execShell(cmd, { cwd: t.cwd });
|
|
147
141
|
const output = `${r.stdout}\n${r.stderr}`.trim();
|
|
148
|
-
|
|
149
|
-
|
|
142
|
+
if (output)
|
|
143
|
+
appendLog(t.logFile, output);
|
|
144
|
+
appendLog(t.logFile, footerLine({ code: r.code, ok: r.code === 0, endedAt: new Date().toISOString() }));
|
|
145
|
+
return { ok: r.code === 0, output, seq: logSeq(t.logFile) };
|
|
150
146
|
}
|
|
151
147
|
//# sourceMappingURL=runner.js.map
|
package/dist/schemas.js
CHANGED
|
@@ -39,7 +39,7 @@ export const ReviewResult = z.object({
|
|
|
39
39
|
note: z.string().default(""),
|
|
40
40
|
})),
|
|
41
41
|
});
|
|
42
|
-
/** 一個 agent 的定義;名稱(agents 的 key
|
|
42
|
+
/** 一個 agent 的定義;名稱(agents 的 key)用在 cycle 裡 */
|
|
43
43
|
export const AgentDef = z.object({
|
|
44
44
|
adapter: z.enum(["claude", "codex", "gemini", "command"]),
|
|
45
45
|
model: z.string().optional(),
|
|
@@ -49,11 +49,15 @@ export const AgentDef = z.object({
|
|
|
49
49
|
});
|
|
50
50
|
/** 目標專案可選的 flow.config.json,預設值對應 Vite + TypeScript + Vitest 專案 */
|
|
51
51
|
export const RepoConfig = z.object({
|
|
52
|
-
/**
|
|
52
|
+
/** 可用的 agent;沒有內建,全部都要在這裡定義(通常用 agent add) */
|
|
53
53
|
agents: z.record(z.string(), AgentDef).default({}),
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
/** 各 CLI adapter 的預設模型;agent 自己指定 model 時優先使用個別設定 */
|
|
55
|
+
defaultModels: z.strictObject({
|
|
56
|
+
claude: z.string().trim().min(1).optional(),
|
|
57
|
+
codex: z.string().trim().min(1).optional(),
|
|
58
|
+
gemini: z.string().trim().min(1).optional(),
|
|
59
|
+
}).default({}),
|
|
60
|
+
/** 參與的 agent(順序不影響分工);未設定時取 agents 裡已安裝的 CLI */
|
|
57
61
|
cycle: z.array(z.string()).min(1).optional(),
|
|
58
62
|
/** review 後的修正由誰做:ring=輪到下一位;author=最後寫程式的 agent */
|
|
59
63
|
fixStrategy: z.enum(["ring", "author"]).default("ring"),
|
|
@@ -96,7 +100,7 @@ export const FlowRun = z.object({
|
|
|
96
100
|
/** 暫停前所在的階段與原因(額度用完時) */
|
|
97
101
|
pausedStage: Stage.optional(),
|
|
98
102
|
pauseReason: z.string().optional(),
|
|
99
|
-
/** 這個 run
|
|
103
|
+
/** 這個 run 參與的 agent(建立時決定,resume 時沿用) */
|
|
100
104
|
cycle: z.array(z.string()).min(1),
|
|
101
105
|
/** 最後一個寫程式的 agent,reviewer 不可以是它 */
|
|
102
106
|
lastWriter: z.string().optional(),
|
package/dist/setup.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { addAgent, setAgent, setCycle } from "./agentConfig.js";
|
|
2
|
+
/**
|
|
3
|
+
* `agentflowctl agent setup` 的互動精靈。
|
|
4
|
+
* 問答與偵測結果由外部注入,這裡只負責流程;所有修改都在記憶體裡完成,確認後才交給呼叫端寫入。
|
|
5
|
+
*/
|
|
6
|
+
export const SETUP_ADAPTERS = ["claude", "codex", "gemini"];
|
|
7
|
+
/** 回傳要寫入的設定;使用者沒選任何 agent 或最後沒確認時回傳 null */
|
|
8
|
+
export async function runSetup(initial, deps) {
|
|
9
|
+
const { detected, log } = deps;
|
|
10
|
+
const ask = async (q, def = "") => (await deps.ask(def ? `${q}(${def}):` : `${q}:`)).trim() || def;
|
|
11
|
+
const confirm = async (q, def) => {
|
|
12
|
+
const a = (await deps.ask(`${q}(${def ? "Y/n" : "y/N"}):`)).trim().toLowerCase();
|
|
13
|
+
return a ? a.startsWith("y") : def;
|
|
14
|
+
};
|
|
15
|
+
let cfg = initial;
|
|
16
|
+
const changes = [];
|
|
17
|
+
const chosen = [];
|
|
18
|
+
const existing = () => Object.keys((cfg.agents ?? {}));
|
|
19
|
+
for (const adapter of SETUP_ADAPTERS) {
|
|
20
|
+
log(`${detected[adapter] ? "✅" : "❌"} ${adapter}${detected[adapter] ? "" : "(沒有偵測到 CLI)"}`);
|
|
21
|
+
if (!(await confirm(` 加入 ${adapter}?`, detected[adapter])))
|
|
22
|
+
continue;
|
|
23
|
+
let name;
|
|
24
|
+
for (;;) {
|
|
25
|
+
name = await ask(" 名稱", adapter);
|
|
26
|
+
if (!/^[\w-]+$/.test(name))
|
|
27
|
+
log(" 名稱只能用英數字、底線與連字號");
|
|
28
|
+
else if (chosen.includes(name))
|
|
29
|
+
log(` ${name} 這次已經用過了`);
|
|
30
|
+
else
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
const isNew = !existing().includes(name);
|
|
34
|
+
if (!isNew && !(await confirm(` ${name} 已存在,要覆寫嗎?`, false))) {
|
|
35
|
+
chosen.push(name);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const model = (await ask(" model(Enter 不指定)")) || undefined;
|
|
39
|
+
const edit = isNew ? addAgent(cfg, name, { adapter, model }) : setAgent(cfg, name, { adapter, model });
|
|
40
|
+
cfg = edit.cfg;
|
|
41
|
+
changes.push(...edit.changes);
|
|
42
|
+
chosen.push(name);
|
|
43
|
+
}
|
|
44
|
+
if (!chosen.length) {
|
|
45
|
+
log("沒有選擇任何 agent,設定沒有變更");
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
for (;;) {
|
|
49
|
+
const list = (await ask("參與的 agent,用逗號分隔", chosen.join(","))).split(",").map((s) => s.trim()).filter(Boolean);
|
|
50
|
+
try {
|
|
51
|
+
cfg = setCycle(cfg, list).cfg;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
log(` ${e.message}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const agents = cfg.agents;
|
|
59
|
+
log("\n即將寫入:");
|
|
60
|
+
for (const name of chosen)
|
|
61
|
+
log(` ${name.padEnd(14)} adapter=${agents[name].adapter}${agents[name].model ? ` model=${agents[name].model}` : ""}`);
|
|
62
|
+
log(` 參與的 agent:${cfg.cycle.join("、")}`);
|
|
63
|
+
log(" 需要自訂指令的 CLI 請改用 agent add <name> --adapter command -- <指令>");
|
|
64
|
+
if (!(await confirm("寫入 flow.config.json?", true))) {
|
|
65
|
+
log("已取消,設定沒有變更");
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return { cfg, changes };
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=setup.js.map
|