agentflowctl 0.12.0 → 0.13.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 +4 -1
- package/dist/agents/claude.js +12 -2
- package/dist/agents/codex.js +3 -1
- package/dist/cli.js +25 -2
- package/dist/engine.js +1 -1
- package/dist/logs.js +1 -1
- package/dist/runner.js +7 -1
- package/dist/stats.js +53 -0
- package/dist/store.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,11 +46,14 @@ agentflowctl list # 列出 run
|
|
|
46
46
|
agentflowctl status f-xxxx # 看進度、結果與下一步
|
|
47
47
|
agentflowctl logs f-xxxx # 列出各步驟的 log
|
|
48
48
|
agentflowctl logs f-xxxx --latest # 看最新一份 log
|
|
49
|
+
agentflowctl stats f-xxxx # 各步驟耗時、執行與失敗次數
|
|
49
50
|
agentflowctl resume f-xxxx # 從暫停、中斷或失敗處接續
|
|
50
51
|
```
|
|
51
52
|
|
|
52
53
|
`status` 會列出目前階段、未結的交接事項與下一步指令;失敗或暫停時也會顯示原因。要看某一步的詳細輸出,可用 `logs <id> <編號>`;加 `--full` 看完整工具內容,或加 `--raw` 看原始輸出。
|
|
53
54
|
|
|
55
|
+
`stats` 依 log 的開始與結束時間統計每個步驟的執行次數、失敗次數、總耗時與最長一次,並分開列出 agent 與專案指令(install、測試、checks)各占多少時間,最耗時的步驟排在最前面。沒有結束紀錄的 log 列為未完成,不計入耗時;總經過時間包含暫停與等待核准。
|
|
56
|
+
|
|
54
57
|
執行紀錄在 `.agentflowctl/runs/<id>/`,工作分支在 `.agentflowctl/worktrees/<id>/`。不再需要某次 run 時,可用 `agentflowctl clean <id>` 清除 worktree 與紀錄;`flow/<id>` 分支會保留。
|
|
55
58
|
|
|
56
59
|
## 執行停下來時怎麼做
|
|
@@ -129,7 +132,7 @@ agentflowctl run --req-file ./requirement.md
|
|
|
129
132
|
|
|
130
133
|
把 `MODEL_NAME` 換成該 CLI 目前可呼叫的別名或完整 ID。`model add` 會用目前登入的帳號送出短請求,可能耗用少量 token;成功才寫入設定。需要重驗時執行 `model check`。用 `model set claude MODEL_NAME --strength medium` 改強度、`model remove claude MODEL_NAME` 移除模型,或用 `model stage taskReview high` 調整階段最低強度;`model stage` 不帶強度時列出各階段實際生效的強度。終端機每次呼叫會顯示送給 CLI 的模型名稱,Claude Code 與 Gemini CLI 回報的實際模型不同時也會顯示;`status <id>` 會按階段、任務、模型與步驟顯示用量。`run --model-mode balanced` 可暫時回到原設定。
|
|
131
134
|
|
|
132
|
-
`status <id>` 的用量以每次 LLM 呼叫為一筆,失敗、額度用完及代打也會計入呼叫次數。只有 CLI 同時回報輸入與輸出 token,才把兩者納入合計與模型強度占比;明確回報的 0 仍算已回報。缺少任一數字列為「未回報」;舊紀錄無法分辨真實 0 與預設補值,列為「舊紀錄不明」,原始數字只供查閱。各 agent
|
|
135
|
+
`status <id>` 的用量以每次 LLM 呼叫為一筆,失敗、額度用完及代打也會計入呼叫次數。只有 CLI 同時回報輸入與輸出 token,才把兩者納入合計與模型強度占比;明確回報的 0 仍算已回報。缺少任一數字列為「未回報」;舊紀錄無法分辨真實 0 與預設補值,列為「舊紀錄不明」,原始數字只供查閱。各 agent、階段、任務、模型與步驟、模型強度是同一批呼叫的不同分組,不應跨組相加。輸入 token 一律包含 cache 讀取與寫入:Claude Code 回報的 `input_tokens` 不含 cache,agentflowctl 會把 cache 讀寫加回去;Codex 的 `input_tokens` 本來就包含 cache。有回報 cache 時,`status` 與 `logs` 會另外標出其中讀取與寫入 cache 各多少。`model add/check` 的探測請求可能耗用 token,但不屬於 run,因此不在 `status` 內。
|
|
133
136
|
|
|
134
137
|
計畫 agent 會查閱相關程式碼,依影響範圍、技術不確定性與失敗後果為每個任務標註 `low`/`medium`/`high` 難度,取三者中最高等級,並在計畫中寫出依據;計畫審查會逐項核對。自動選模先遵守角色分配,再取階段強度與任務難度中較高者;失敗重試會提高強度。若分配到的 agent 沒有足夠強度的模型,會選它最強的模型並提示。這些強度是你對模型能力的設定,不由 CLI 自動評分。
|
|
135
138
|
|
package/dist/agents/claude.js
CHANGED
|
@@ -59,8 +59,18 @@ export const claude = {
|
|
|
59
59
|
}
|
|
60
60
|
else if (ev.type === "result") {
|
|
61
61
|
const usage = (ev.usage ?? {});
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
// Claude 的 input_tokens 不含 cache,要加回來才是實際送入量
|
|
63
|
+
const input = num(usage.input_tokens);
|
|
64
|
+
const cacheRead = num(usage.cache_read_input_tokens);
|
|
65
|
+
const cacheWrite = num(usage.cache_creation_input_tokens);
|
|
66
|
+
if (input !== undefined || num(usage.output_tokens) !== undefined) {
|
|
67
|
+
out.push({
|
|
68
|
+
kind: "usage",
|
|
69
|
+
inputTokens: input === undefined ? undefined : input + (cacheRead ?? 0) + (cacheWrite ?? 0),
|
|
70
|
+
outputTokens: num(usage.output_tokens),
|
|
71
|
+
...(cacheRead !== undefined && { cacheReadTokens: cacheRead }),
|
|
72
|
+
...(cacheWrite !== undefined && { cacheWriteTokens: cacheWrite }),
|
|
73
|
+
});
|
|
64
74
|
}
|
|
65
75
|
out.push({ kind: "done", ok: ev.is_error !== true, summary: str(ev.result) });
|
|
66
76
|
}
|
package/dist/agents/codex.js
CHANGED
|
@@ -31,7 +31,9 @@ export const codex = {
|
|
|
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
|
-
|
|
34
|
+
const cacheRead = num(usage.cached_input_tokens);
|
|
35
|
+
out.push({ kind: "usage", inputTokens: num(usage.input_tokens), outputTokens: num(usage.output_tokens),
|
|
36
|
+
...(cacheRead !== undefined && { cacheReadTokens: cacheRead }) });
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
39
|
else if (ev.type === "turn.failed" || ev.type === "error") {
|
package/dist/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ 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
15
|
import { ModelStage, ModelStrength, TaskList } from "./schemas.js";
|
|
16
|
+
import { computeStats, formatDuration } from "./stats.js";
|
|
16
17
|
import { agentRuns, getRun, listRuns, listSubstitutions, saveRun, usageByAgent, usageByModelStage, usageByStage, usageByStrength, usageByTask } from "./store.js";
|
|
17
18
|
import { readJsonFile } from "./util.js";
|
|
18
19
|
import { openActions, readHandoff } from "./handoff.js";
|
|
@@ -22,12 +23,13 @@ import { addAgent, readRawConfig, removeAgent, setAgent, setCycle, writeRawConfi
|
|
|
22
23
|
import { addModel, removeModel, setModelMode, setModelStrength, setStageStrength } from "./modelConfig.js";
|
|
23
24
|
import { DEFAULT_STAGE_STRENGTH, effectiveStageStrengths, validateAdaptiveConfig } from "./modelSelection.js";
|
|
24
25
|
import { probeModel } from "./modelProbe.js";
|
|
26
|
+
const cacheNote = (c) => c.cacheReadTokens || c.cacheWriteTokens ? `(含 cache 讀 ${c.cacheReadTokens}、寫 ${c.cacheWriteTokens})` : "";
|
|
25
27
|
function printUsage(title, rows) {
|
|
26
28
|
if (!rows.length)
|
|
27
29
|
return;
|
|
28
30
|
console.log(`\n${title}`);
|
|
29
31
|
for (const [key, c] of rows) {
|
|
30
|
-
const value = c.reportedRuns ? `輸入 ${c.inputTokens}、輸出 ${c.outputTokens}、合計 ${c.tokens} tokens` : "未回報或回報狀態不明";
|
|
32
|
+
const value = c.reportedRuns ? `輸入 ${c.inputTokens}${cacheNote(c)}、輸出 ${c.outputTokens}、合計 ${c.tokens} tokens` : "未回報或回報狀態不明";
|
|
31
33
|
console.log(` ${key}: ${value};${c.runs} 次(未回報 ${c.unreportedRuns}、舊紀錄不明 ${c.legacyRuns})`);
|
|
32
34
|
}
|
|
33
35
|
}
|
|
@@ -236,7 +238,7 @@ program
|
|
|
236
238
|
if (Object.keys(byAgent).length) {
|
|
237
239
|
console.log("\n各 agent 用量");
|
|
238
240
|
for (const [agent, c] of Object.entries(byAgent)) {
|
|
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` : ""})`);
|
|
241
|
+
console.log(` ${agent.padEnd(10)} ${String(c.runs).padStart(3)} 次 ${String(c.tokens).padStart(9)} 已回報 tokens${cacheNote(c)}(未回報 ${c.unreportedRuns}、舊紀錄不明 ${c.legacyRuns}${c.legacyTokens ? `,原始數字 ${c.legacyTokens} tokens` : ""})`);
|
|
240
242
|
}
|
|
241
243
|
}
|
|
242
244
|
const byStage = usageByStage(id);
|
|
@@ -537,6 +539,27 @@ program
|
|
|
537
539
|
const text = readFileSync(entry.file, "utf8");
|
|
538
540
|
console.log(opts.raw ? text : renderLog(text, entry.file, { full: opts.full }));
|
|
539
541
|
});
|
|
542
|
+
program
|
|
543
|
+
.command("stats <id>")
|
|
544
|
+
.description("依步驟統計耗時、執行次數與失敗次數,找出最花時間與最常重試的地方")
|
|
545
|
+
.action((id) => {
|
|
546
|
+
mustGetRun(id);
|
|
547
|
+
const stats = computeStats(listLogs(logDir(id)));
|
|
548
|
+
if (!stats.steps.length)
|
|
549
|
+
return console.log("還沒有 log");
|
|
550
|
+
const total = stats.agentMs + stats.cmdMs;
|
|
551
|
+
const share = (ms) => (total ? `${(ms / total * 100).toFixed(0)}%` : "-");
|
|
552
|
+
console.log(`總經過時間 ${formatDuration(stats.wallMs)}(含暫停與等待核准)`);
|
|
553
|
+
console.log(` agent ${formatDuration(stats.agentMs).padStart(7)} ${share(stats.agentMs)}`);
|
|
554
|
+
console.log(` 專案指令 ${formatDuration(stats.cmdMs).padStart(7)} ${share(stats.cmdMs)}`);
|
|
555
|
+
if (stats.unfinished)
|
|
556
|
+
console.log(` 未完成 ${stats.unfinished} 份(沒有結束紀錄,不計入耗時)`);
|
|
557
|
+
console.log("\n 步驟 類型 次數 失敗 總耗時 最長 占比");
|
|
558
|
+
for (const s of stats.steps) {
|
|
559
|
+
console.log(` ${s.step.padEnd(19)} ${s.kind === "cmd" ? "指令 " : "agent"} ${String(s.runs).padStart(4)} ${String(s.failed).padStart(4)} ${formatDuration(s.totalMs).padStart(7)} ${formatDuration(s.maxMs).padStart(7)} ${share(s.totalMs).padStart(5)}${s.unfinished ? ` (未完成 ${s.unfinished})` : ""}`);
|
|
560
|
+
}
|
|
561
|
+
console.log(`\n次數多或失敗多的步驟可用 agentflowctl logs ${id} 找出編號查看原因;token 用量見 agentflowctl status ${id}`);
|
|
562
|
+
});
|
|
540
563
|
program.parseAsync().catch((err) => {
|
|
541
564
|
console.error(`錯誤:${err.message}`);
|
|
542
565
|
process.exit(1);
|
package/dist/engine.js
CHANGED
|
@@ -70,7 +70,7 @@ async function agentStep(run, planned, step, prompt, mode) {
|
|
|
70
70
|
info(run, ` ↳ CLI 回報實際模型:${r.resolvedModel}`);
|
|
71
71
|
addUsage(run.id, { stage: step, agent, model: selected.name, resolvedModel: r.resolvedModel,
|
|
72
72
|
strength: selected.strength, targetStrength: selected.targetStrength, usageReported: r.usageReported,
|
|
73
|
-
inputTokens: r.inputTokens, outputTokens: r.outputTokens });
|
|
73
|
+
inputTokens: r.inputTokens, outputTokens: r.outputTokens, cacheReadTokens: r.cacheReadTokens, cacheWriteTokens: r.cacheWriteTokens });
|
|
74
74
|
if (!r.quotaExhausted) {
|
|
75
75
|
reportMeta(run, agent, r);
|
|
76
76
|
return { r, agent, step, callKey };
|
package/dist/logs.js
CHANGED
|
@@ -119,7 +119,7 @@ function renderEvent(ev, full, lastText) {
|
|
|
119
119
|
return `🔧 ${ev.name}: ${full ? indent(detail) : compactDetail(detail)}`;
|
|
120
120
|
}
|
|
121
121
|
case "usage":
|
|
122
|
-
return `📊 用量 input ${ev.inputTokens ?? "?"} / output ${ev.outputTokens ?? "?"} tokens`;
|
|
122
|
+
return `📊 用量 input ${ev.inputTokens ?? "?"} / output ${ev.outputTokens ?? "?"} tokens${ev.cacheReadTokens !== undefined || ev.cacheWriteTokens !== undefined ? `(input 含 cache 讀 ${ev.cacheReadTokens ?? 0}、寫 ${ev.cacheWriteTokens ?? 0})` : ""}`;
|
|
123
123
|
case "done": {
|
|
124
124
|
const summary = ev.summary?.trim();
|
|
125
125
|
// 最後一則回覆通常就是 summary,精簡模式不再重印一次
|
package/dist/runner.js
CHANGED
|
@@ -70,6 +70,8 @@ export async function runAgent(name, def, t, prompt) {
|
|
|
70
70
|
let outputTokens = 0;
|
|
71
71
|
let inputReported = false;
|
|
72
72
|
let outputReported = false;
|
|
73
|
+
let cacheReadTokens;
|
|
74
|
+
let cacheWriteTokens;
|
|
73
75
|
let resolvedModel;
|
|
74
76
|
const r = await exec(inv.cmd, inv.args, {
|
|
75
77
|
cwd: t.cwd,
|
|
@@ -98,6 +100,10 @@ export async function runAgent(name, def, t, prompt) {
|
|
|
98
100
|
outputReported = true;
|
|
99
101
|
outputTokens += ev.outputTokens;
|
|
100
102
|
}
|
|
103
|
+
if (ev.cacheReadTokens !== undefined)
|
|
104
|
+
cacheReadTokens = (cacheReadTokens ?? 0) + ev.cacheReadTokens;
|
|
105
|
+
if (ev.cacheWriteTokens !== undefined)
|
|
106
|
+
cacheWriteTokens = (cacheWriteTokens ?? 0) + ev.cacheWriteTokens;
|
|
101
107
|
}
|
|
102
108
|
else if (ev.kind === "model") {
|
|
103
109
|
resolvedModel = ev.id;
|
|
@@ -118,7 +124,7 @@ export async function runAgent(name, def, t, prompt) {
|
|
|
118
124
|
const quotaExhausted = !ok && isQuotaError(`${summary}\n${done?.summary ?? ""}\n${r.stderr}\n${tail(r.stdout, 4000)}`);
|
|
119
125
|
const meta = parseResultMeta(summary) ?? parseResultMeta(lastText);
|
|
120
126
|
return { ok, quotaExhausted, summary, meta, usageReported: inputReported && outputReported,
|
|
121
|
-
inputTokens: inputReported ? inputTokens : undefined, outputTokens: outputReported ? outputTokens : undefined, resolvedModel };
|
|
127
|
+
inputTokens: inputReported ? inputTokens : undefined, outputTokens: outputReported ? outputTokens : undefined, cacheReadTokens, cacheWriteTokens, resolvedModel };
|
|
122
128
|
}
|
|
123
129
|
/**
|
|
124
130
|
* 在 worktree 內執行專案指令(安裝、測試、建置)。
|
package/dist/stats.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { CMD_AGENT } from "./logs.js";
|
|
2
|
+
const time = (iso) => (iso ? new Date(iso).getTime() : NaN);
|
|
3
|
+
/** 從 log 的檔頭與檔尾算出每個步驟的次數、失敗與耗時,不讀 log 內容 */
|
|
4
|
+
export function computeStats(entries) {
|
|
5
|
+
const byKey = new Map();
|
|
6
|
+
let agentMs = 0;
|
|
7
|
+
let cmdMs = 0;
|
|
8
|
+
let unfinished = 0;
|
|
9
|
+
let first = Infinity;
|
|
10
|
+
let last = -Infinity;
|
|
11
|
+
for (const { header, footer } of entries) {
|
|
12
|
+
if (!header)
|
|
13
|
+
continue;
|
|
14
|
+
const kind = header.agent === CMD_AGENT ? "cmd" : "agent";
|
|
15
|
+
const key = `${kind}:${header.step}`;
|
|
16
|
+
const s = byKey.get(key) ?? { step: header.step, kind, runs: 0, failed: 0, unfinished: 0, totalMs: 0, maxMs: 0 };
|
|
17
|
+
byKey.set(key, s);
|
|
18
|
+
s.runs += 1;
|
|
19
|
+
const start = time(header.startedAt);
|
|
20
|
+
const end = time(footer?.endedAt);
|
|
21
|
+
if (!footer || Number.isNaN(start) || Number.isNaN(end)) {
|
|
22
|
+
s.unfinished += 1;
|
|
23
|
+
unfinished += 1;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (!footer.ok)
|
|
27
|
+
s.failed += 1;
|
|
28
|
+
const ms = Math.max(0, end - start);
|
|
29
|
+
s.totalMs += ms;
|
|
30
|
+
s.maxMs = Math.max(s.maxMs, ms);
|
|
31
|
+
if (kind === "cmd")
|
|
32
|
+
cmdMs += ms;
|
|
33
|
+
else
|
|
34
|
+
agentMs += ms;
|
|
35
|
+
first = Math.min(first, start);
|
|
36
|
+
last = Math.max(last, end);
|
|
37
|
+
}
|
|
38
|
+
const steps = [...byKey.values()].sort((a, b) => b.totalMs - a.totalMs);
|
|
39
|
+
return { steps, agentMs, cmdMs, wallMs: last > first ? last - first : 0, unfinished };
|
|
40
|
+
}
|
|
41
|
+
/** 毫秒轉成 1h02m、3m05s、12s */
|
|
42
|
+
export function formatDuration(ms) {
|
|
43
|
+
const sec = Math.round(ms / 1000);
|
|
44
|
+
const h = Math.floor(sec / 3600);
|
|
45
|
+
const m = Math.floor((sec % 3600) / 60);
|
|
46
|
+
const s = sec % 60;
|
|
47
|
+
if (h)
|
|
48
|
+
return `${h}h${String(m).padStart(2, "0")}m`;
|
|
49
|
+
if (m)
|
|
50
|
+
return `${m}m${String(s).padStart(2, "0")}s`;
|
|
51
|
+
return `${s}s`;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=stats.js.map
|
package/dist/store.js
CHANGED
|
@@ -35,7 +35,7 @@ export function addUsage(id, entry) {
|
|
|
35
35
|
mkdirSync(runDir(id), { recursive: true });
|
|
36
36
|
appendFileSync(usagePath(id), `${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`);
|
|
37
37
|
}
|
|
38
|
-
const emptySummary = () => ({ tokens: 0, inputTokens: 0, outputTokens: 0, runs: 0, reportedRuns: 0, unreportedRuns: 0, legacyRuns: 0, legacyTokens: 0 });
|
|
38
|
+
const emptySummary = () => ({ tokens: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, runs: 0, reportedRuns: 0, unreportedRuns: 0, legacyRuns: 0, legacyTokens: 0 });
|
|
39
39
|
export function listUsage(id) {
|
|
40
40
|
const p = usagePath(id);
|
|
41
41
|
return existsSync(p) ? readFileSync(p, "utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line)) : [];
|
|
@@ -47,6 +47,8 @@ function addSummary(acc, e) {
|
|
|
47
47
|
acc.inputTokens += e.inputTokens ?? 0;
|
|
48
48
|
acc.outputTokens += e.outputTokens ?? 0;
|
|
49
49
|
acc.tokens += (e.inputTokens ?? 0) + (e.outputTokens ?? 0);
|
|
50
|
+
acc.cacheReadTokens += e.cacheReadTokens ?? 0;
|
|
51
|
+
acc.cacheWriteTokens += e.cacheWriteTokens ?? 0;
|
|
50
52
|
}
|
|
51
53
|
else if (e.usageReported === false || e.usageReported === true)
|
|
52
54
|
acc.unreportedRuns += 1;
|