u1s1-cli 0.17.0 → 0.19.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/dist/agent-setup.js +9 -2
- package/dist/subagent.d.ts +10 -0
- package/dist/subagent.js +49 -3
- package/dist/tools.js +1 -1
- package/dist/web.js +3 -1
- package/dist/workflow/runner.d.ts +15 -0
- package/dist/workflow/runner.js +75 -4
- package/dist/workflow/templates.d.ts +11 -0
- package/dist/workflow/templates.js +167 -0
- package/dist/workflow/tool.d.ts +4 -0
- package/dist/workflow/tool.js +66 -6
- package/package.json +1 -1
package/dist/agent-setup.js
CHANGED
|
@@ -328,14 +328,21 @@ First decide whether this task really needs a workflow. Good fit: many independe
|
|
|
328
328
|
|
|
329
329
|
If it is a fit:
|
|
330
330
|
|
|
331
|
-
|
|
331
|
+
0. **Prefer a built-in template** when one matches — call \`run_workflow\` with \`template\` + \`input\` instead of writing a script:
|
|
332
|
+
- \`i18n\` — translate locale file(s) to N languages: { "langs": ["en", "ja"], "source": "src/locales/zh.json" }
|
|
333
|
+
- \`review\` — per-file review + adversarial verification: { "files": [...] } or omit for git-diff files
|
|
334
|
+
- \`research\` — parallel investigation + synthesis: { "questions": ["..."], "context": "..." }
|
|
335
|
+
- \`refactor\` — rule-first batch refactor in isolated worktrees: { "instruction": "...", "files": [...] }
|
|
336
|
+
1. Otherwise decompose the task and write a JavaScript orchestration script. Top-level await and return are allowed. The script runs in a sandbox where ONLY these are available:
|
|
332
337
|
- \`subagent(task)\` or \`subagent({ task, model })\` — runs one sub-agent, resolves to its final output text
|
|
333
338
|
- \`parallel([() => ..., ...])\` — concurrent fan-out; each thunk's error becomes \`null\`; barrier semantics
|
|
334
339
|
- \`pipeline(items, [stage1, stage2, ...])\` — each item flows through all stages independently (prefer over parallel)
|
|
340
|
+
- \`judge(question)\` — cheap tool-free model call returning true/false; use for loop-until-done checks
|
|
341
|
+
- \`subagent({ task, worktree: true })\` — run in an isolated temp git worktree (auto-cleaned) when tasks would write conflicting files
|
|
335
342
|
- \`log(...)\`, \`setTimeout/clearTimeout\`, standard JS builtins (Promise/JSON/Math/...)
|
|
336
343
|
- NO require/import/process/fetch/fs access — the static validator rejects scripts that try
|
|
337
344
|
2. Every task string must be fully self-contained: which files to read/write, what "done" means, how to self-verify.
|
|
338
|
-
3. Call the \`run_workflow\` tool with the full source as \`script\`. Read its report carefully; on failures fix the script, or rerun with \`script_path\` + \`resume: true\` to continue from saved progress.
|
|
345
|
+
3. Call the \`run_workflow\` tool with the full source as \`script\`. Read its report carefully; on failures fix the script, or rerun with \`script_path\` + \`resume: true\` to continue from saved progress. For workflows worth rerunning (e.g. recurring i18n syncs), pass a memorable \`save_as\` name — rerun later with just \`script_path\`.
|
|
339
346
|
|
|
340
347
|
Keep scripts deterministic (same order of subagent calls every run) so resume works. Summarize the outcome to the user afterwards in their language.
|
|
341
348
|
`;
|
package/dist/subagent.d.ts
CHANGED
|
@@ -15,10 +15,20 @@ export interface SubagentOptions {
|
|
|
15
15
|
model?: string;
|
|
16
16
|
timeoutMs?: number;
|
|
17
17
|
signal?: AbortSignal;
|
|
18
|
+
/** 禁用全部工具(纯文本推理,判分等廉价场景用)。 */
|
|
19
|
+
noTools?: boolean;
|
|
20
|
+
/** 子 agent 的工作目录(git worktree 隔离等场景);缺省继承主进程 cwd。 */
|
|
21
|
+
cwd?: string;
|
|
22
|
+
}
|
|
23
|
+
/** 单个子 agent 的 token/费用用量(汇总自会话内全部 assistant 消息)。 */
|
|
24
|
+
export interface SubagentUsage {
|
|
25
|
+
totalTokens: number;
|
|
26
|
+
costUsd: number;
|
|
18
27
|
}
|
|
19
28
|
export interface SubagentOutcome {
|
|
20
29
|
ok: boolean;
|
|
21
30
|
text: string;
|
|
31
|
+
usage: SubagentUsage;
|
|
22
32
|
}
|
|
23
33
|
/**
|
|
24
34
|
* Spawn 一个独立上下文的子 agent 执行任务,返回其最终文本输出。
|
package/dist/subagent.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createAgentSession, ModelRuntime, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { createAgentSession, DefaultResourceLoader, getAgentDir, ModelRuntime, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
// ---- 子 agent spawn 基础设施:spawn_subagent 工具与 workflow runner 共用 ----
|
|
3
3
|
/** 同时在跑的子 agent 上限;超额排队,防止打爆网关。 */
|
|
4
4
|
export const SUBAGENT_CONCURRENCY = 4;
|
|
@@ -22,6 +22,36 @@ function extractFinalText(messages) {
|
|
|
22
22
|
}
|
|
23
23
|
return "(子 agent 结束但没有文本输出)";
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* 汇总会话内全部 assistant 消息的 token 与费用。
|
|
27
|
+
* pi-ai 对自定义 provider(models.json)不算钱(usage.cost.total 恒 0),
|
|
28
|
+
* 所以优先用自带费用,否则按模型价率(美元/百万 token)自算。
|
|
29
|
+
*/
|
|
30
|
+
function extractUsage(messages, model) {
|
|
31
|
+
let totalTokens = 0;
|
|
32
|
+
let costUsd = 0;
|
|
33
|
+
const rates = model?.cost;
|
|
34
|
+
for (const m of messages) {
|
|
35
|
+
const msg = m;
|
|
36
|
+
const usage = msg?.usage;
|
|
37
|
+
if (!usage || msg.role !== "assistant")
|
|
38
|
+
continue;
|
|
39
|
+
totalTokens += usage.totalTokens ?? 0;
|
|
40
|
+
const reported = usage.cost?.total ?? 0;
|
|
41
|
+
if (reported > 0) {
|
|
42
|
+
costUsd += reported;
|
|
43
|
+
}
|
|
44
|
+
else if (rates) {
|
|
45
|
+
costUsd +=
|
|
46
|
+
((usage.input ?? 0) * rates.input +
|
|
47
|
+
(usage.output ?? 0) * rates.output +
|
|
48
|
+
(usage.cacheRead ?? 0) * rates.cacheRead +
|
|
49
|
+
(usage.cacheWrite ?? 0) * rates.cacheWrite) /
|
|
50
|
+
1_000_000;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { totalTokens, costUsd };
|
|
54
|
+
}
|
|
25
55
|
/**
|
|
26
56
|
* Spawn 一个独立上下文的子 agent 执行任务,返回其最终文本输出。
|
|
27
57
|
* 模型解析:"provider/id" 精确匹配;裸 id(如 deepseek-v4-flash)先按原样找,
|
|
@@ -48,10 +78,25 @@ export async function runSubagent(opts) {
|
|
|
48
78
|
else {
|
|
49
79
|
model = undefined;
|
|
50
80
|
}
|
|
81
|
+
// 子会话不加载精简 UI 扩展:它用 process.cwd() 重建内置工具,会破坏自定义
|
|
82
|
+
// cwd(worktree 隔离)下的路径解析;且 UI 美化对无界面的子 agent 毫无意义
|
|
83
|
+
const loader = new DefaultResourceLoader({
|
|
84
|
+
agentDir: getAgentDir(),
|
|
85
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
86
|
+
extensionsOverride: (base) => ({
|
|
87
|
+
...base,
|
|
88
|
+
extensions: base.extensions.filter((e) => !JSON.stringify(e).includes("u1s1-compact-ui")),
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
await loader.reload();
|
|
51
92
|
const { session } = await createAgentSession({
|
|
52
|
-
|
|
93
|
+
// cwd 必须同时传给顶层(工具的路径解析基于它)和 sessionManager
|
|
94
|
+
...(opts.cwd ? { cwd: opts.cwd } : {}),
|
|
95
|
+
resourceLoader: loader,
|
|
96
|
+
sessionManager: SessionManager.inMemory(opts.cwd ?? process.cwd()),
|
|
53
97
|
modelRuntime: sharedModelRuntime,
|
|
54
98
|
...(model ? { model } : {}),
|
|
99
|
+
...(opts.noTools ? { noTools: "all" } : {}),
|
|
55
100
|
});
|
|
56
101
|
const timeoutMs = opts.timeoutMs ?? SUBAGENT_TIMEOUT_MS;
|
|
57
102
|
const timer = setTimeout(() => void session.abort(), timeoutMs);
|
|
@@ -67,7 +112,8 @@ export async function runSubagent(opts) {
|
|
|
67
112
|
}
|
|
68
113
|
if (opts.signal?.aborted)
|
|
69
114
|
throw new Error("已取消");
|
|
70
|
-
|
|
115
|
+
const messages = session.messages;
|
|
116
|
+
return { ok: true, text: extractFinalText(messages), usage: extractUsage(messages, model) };
|
|
71
117
|
}
|
|
72
118
|
/**
|
|
73
119
|
* 固定并发的工作池:limit 个工人依次领任务跑完一个补位一个。
|
package/dist/tools.js
CHANGED
|
@@ -213,7 +213,7 @@ export function createSubagentTool(getParentModel) {
|
|
|
213
213
|
const outcomes = new Array(list.length);
|
|
214
214
|
try {
|
|
215
215
|
await runPool(list, SUBAGENT_CONCURRENCY, async (task, i) => {
|
|
216
|
-
outcomes[i] = await runSubagent({ task, parentModel, model: params.model, timeoutMs, signal }).catch((e) => ({ ok: false, text: e.message }));
|
|
216
|
+
outcomes[i] = await runSubagent({ task, parentModel, model: params.model, timeoutMs, signal }).catch((e) => ({ ok: false, text: e.message, usage: { totalTokens: 0, costUsd: 0 } }));
|
|
217
217
|
}, signal);
|
|
218
218
|
}
|
|
219
219
|
finally {
|
package/dist/web.js
CHANGED
|
@@ -2,7 +2,7 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
2
2
|
import { mkdirSync } from "node:fs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
|
-
import { cleanupBrandThemes, endpointKeyEnv, ensureAuthCredential, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, scrubForeignProviderEnv, writeWebToolsExtension, } from "./agent-setup.js";
|
|
5
|
+
import { cleanupBrandThemes, endpointKeyEnv, ensureAuthCredential, ensureBrandPrompt, ensureWorkflowPromptTemplate, ensureDefaultSettings, ensureProviderModels, scrubForeignProviderEnv, writeWebToolsExtension, } from "./agent-setup.js";
|
|
6
6
|
import { agentDir, apiModelToDef, resolvePreferredModel, setModelsFromApi, u1s1Dir, writeAgentDefaultModel, } from "./config.js";
|
|
7
7
|
import { ensureSearchTools } from "./search-tools.js";
|
|
8
8
|
import { ensureUsableShell } from "./shell-doctor.js";
|
|
@@ -47,6 +47,8 @@ export async function prepareWebEnv(cfg) {
|
|
|
47
47
|
}
|
|
48
48
|
await endpointsReady;
|
|
49
49
|
ensureBrandPrompt(await shellReady);
|
|
50
|
+
// /workflow 提示词模板与 TUI 同源,web/桌面版也要有
|
|
51
|
+
ensureWorkflowPromptTemplate();
|
|
50
52
|
ensureProviderModels(cfg);
|
|
51
53
|
// pi-web-ui 靠 auth.json 判断「已配置」,否则网页会弹 pi 安装引导
|
|
52
54
|
ensureAuthCredential();
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type ParentModelRef } from "../subagent.js";
|
|
2
2
|
/** 整个 run 的默认墙钟上限。 */
|
|
3
3
|
export declare const WORKFLOW_TIMEOUT_MS: number;
|
|
4
|
+
/** 默认整个 run 的 token 预算;0 或负数表示不设限。 */
|
|
5
|
+
export declare const WORKFLOW_DEFAULT_BUDGET_TOKENS = 20000000;
|
|
4
6
|
/** 静态白名单校验:语法能编译 + 不碰沙箱之外的任何能力。 */
|
|
5
7
|
export declare function validateScript(code: string): string[];
|
|
6
8
|
export interface WorkflowRunResult {
|
|
@@ -15,6 +17,15 @@ export interface WorkflowRunResult {
|
|
|
15
17
|
aborted: boolean;
|
|
16
18
|
};
|
|
17
19
|
}
|
|
20
|
+
export interface WorkflowProgress {
|
|
21
|
+
spawns: number;
|
|
22
|
+
ok: number;
|
|
23
|
+
failed: number;
|
|
24
|
+
cached: number;
|
|
25
|
+
running: number;
|
|
26
|
+
spentTokens: number;
|
|
27
|
+
budgetTokens: number;
|
|
28
|
+
}
|
|
18
29
|
export interface WorkflowRunOptions {
|
|
19
30
|
/** 脚本源码(调用方负责落盘)。 */
|
|
20
31
|
code: string;
|
|
@@ -24,6 +35,10 @@ export interface WorkflowRunOptions {
|
|
|
24
35
|
parentModel: ParentModelRef;
|
|
25
36
|
signal?: AbortSignal;
|
|
26
37
|
timeoutMs?: number;
|
|
38
|
+
/** 整个 run 的 token 上限;缺省 WORKFLOW_DEFAULT_BUDGET_TOKENS,<=0 不限。 */
|
|
39
|
+
budgetTokens?: number;
|
|
40
|
+
/** 每次子任务状态变化时回调(工具层节流后转成流式进度)。 */
|
|
41
|
+
onProgress?: (p: WorkflowProgress) => void;
|
|
27
42
|
}
|
|
28
43
|
/** 执行一个 workflow 脚本,返回给主对话的报告。不抛错——失败也进报告让模型自行修复。 */
|
|
29
44
|
export declare function runWorkflow(opts: WorkflowRunOptions): Promise<WorkflowRunResult>;
|
package/dist/workflow/runner.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
4
|
import vm from "node:vm";
|
|
4
5
|
import { dirname, resolve } from "node:path";
|
|
5
6
|
import { runPool, runSubagent, SUBAGENT_CONCURRENCY } from "../subagent.js";
|
|
@@ -11,6 +12,8 @@ export const WORKFLOW_TIMEOUT_MS = 30 * 60_000;
|
|
|
11
12
|
const PER_TASK_TIMEOUT_MS = 15 * 60_000;
|
|
12
13
|
/** 报告里最多保留多少行脚本 log 输出。 */
|
|
13
14
|
const MAX_LOG_LINES = 60;
|
|
15
|
+
/** 默认整个 run 的 token 预算;0 或负数表示不设限。 */
|
|
16
|
+
export const WORKFLOW_DEFAULT_BUDGET_TOKENS = 20_000_000;
|
|
14
17
|
/** JSONL 进度存档:一行一个已完成的子任务,断点续跑时按 key 跳过。 */
|
|
15
18
|
class ProgressStore {
|
|
16
19
|
path;
|
|
@@ -108,7 +111,36 @@ export async function runWorkflow(opts) {
|
|
|
108
111
|
internal.abort(new Error(`整个工作流超时(${Math.round((opts.timeoutMs ?? WORKFLOW_TIMEOUT_MS) / 60_000)} 分钟)`));
|
|
109
112
|
}, opts.timeoutMs ?? WORKFLOW_TIMEOUT_MS);
|
|
110
113
|
const store = new ProgressStore(opts.progressPath);
|
|
111
|
-
|
|
114
|
+
const budgetTokens = opts.budgetTokens ?? WORKFLOW_DEFAULT_BUDGET_TOKENS;
|
|
115
|
+
let spentTokens = 0;
|
|
116
|
+
let running = 0;
|
|
117
|
+
const progress = { spawns: 0, ok: 0, failed: 0, cached: 0, running: 0, spentTokens: 0, budgetTokens };
|
|
118
|
+
const reportProgress = () => {
|
|
119
|
+
progress.spentTokens = spentTokens;
|
|
120
|
+
opts.onProgress?.({ ...progress });
|
|
121
|
+
};
|
|
122
|
+
/** git worktree 隔离:在 .u1s1/worktrees/ 下建临时工作树,返回其路径。 */
|
|
123
|
+
let worktreeSeq = 0;
|
|
124
|
+
function createTempWorktree() {
|
|
125
|
+
const dir = resolve(workflowsDir(), "../../.u1s1-worktrees", `wf-${Date.now()}-${++worktreeSeq}`);
|
|
126
|
+
execFileSync("git", ["worktree", "add", "--detach", dir], { stdio: "pipe" });
|
|
127
|
+
return dir;
|
|
128
|
+
}
|
|
129
|
+
function removeWorktree(dir) {
|
|
130
|
+
try {
|
|
131
|
+
execFileSync("git", ["worktree", "remove", "--force", dir], { stdio: "pipe" });
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
rmSync(dir, { recursive: true, force: true });
|
|
135
|
+
try {
|
|
136
|
+
execFileSync("git", ["worktree", "prune"], { stdio: "pipe" });
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// 尽力清理即可
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** 注入沙箱的 subagent():字符串或 {task, model, worktree} 都行,返回最终输出文本。 */
|
|
112
144
|
async function subagentImpl(input) {
|
|
113
145
|
const o = typeof input === "string" ? { task: input } : (input ?? {});
|
|
114
146
|
const task = typeof o.task === "string" ? o.task.trim() : "";
|
|
@@ -117,18 +149,30 @@ export async function runWorkflow(opts) {
|
|
|
117
149
|
const model = typeof o.model === "string" && o.model ? o.model : undefined;
|
|
118
150
|
if (internal.signal.aborted)
|
|
119
151
|
throw new Error("工作流已中止,不再派发新子任务");
|
|
152
|
+
if (budgetTokens > 0 && spentTokens >= budgetTokens) {
|
|
153
|
+
throw new Error(`token 预算已用尽(已用 ${spentTokens.toLocaleString()} / 上限 ${budgetTokens.toLocaleString()}),不再派发新子任务`);
|
|
154
|
+
}
|
|
120
155
|
const key = taskKey(task, model);
|
|
121
156
|
if (opts.resume) {
|
|
122
157
|
const prev = store.get(key);
|
|
123
158
|
if (prev) {
|
|
124
159
|
stats.cached++;
|
|
160
|
+
progress.cached = stats.cached;
|
|
161
|
+
reportProgress();
|
|
125
162
|
if (prev.ok)
|
|
126
163
|
return prev.text;
|
|
127
164
|
throw new Error(prev.text);
|
|
128
165
|
}
|
|
129
166
|
}
|
|
130
167
|
stats.spawns++;
|
|
168
|
+
progress.spawns = stats.spawns;
|
|
169
|
+
running++;
|
|
170
|
+
progress.running = running;
|
|
171
|
+
reportProgress();
|
|
131
172
|
const t0 = Date.now();
|
|
173
|
+
let worktreeDir;
|
|
174
|
+
if (o.worktree === true)
|
|
175
|
+
worktreeDir = createTempWorktree();
|
|
132
176
|
try {
|
|
133
177
|
const out = await runSubagent({
|
|
134
178
|
task,
|
|
@@ -136,17 +180,42 @@ export async function runWorkflow(opts) {
|
|
|
136
180
|
parentModel: opts.parentModel,
|
|
137
181
|
timeoutMs: PER_TASK_TIMEOUT_MS,
|
|
138
182
|
signal: internal.signal,
|
|
183
|
+
cwd: worktreeDir,
|
|
184
|
+
noTools: o.noTools === true,
|
|
139
185
|
});
|
|
140
186
|
stats.ok++;
|
|
187
|
+
progress.ok = stats.ok;
|
|
188
|
+
spentTokens += out.usage.totalTokens;
|
|
141
189
|
store.append({ key, task: task.slice(0, 120), ok: true, text: out.text, ms: Date.now() - t0 });
|
|
142
190
|
return out.text;
|
|
143
191
|
}
|
|
144
192
|
catch (e) {
|
|
145
193
|
stats.failed++;
|
|
194
|
+
progress.failed = stats.failed;
|
|
146
195
|
const msg = e.message || "未知错误";
|
|
147
196
|
store.append({ key, task: task.slice(0, 120), ok: false, text: msg, ms: Date.now() - t0 });
|
|
148
197
|
throw new Error(msg);
|
|
149
198
|
}
|
|
199
|
+
finally {
|
|
200
|
+
if (worktreeDir)
|
|
201
|
+
removeWorktree(worktreeDir);
|
|
202
|
+
running--;
|
|
203
|
+
progress.running = running;
|
|
204
|
+
reportProgress();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/** 注入沙箱的 judge():廉价无工具模型调用,严格回答 PASS/FAIL,转成布尔。 */
|
|
208
|
+
async function judgeImpl(question) {
|
|
209
|
+
if (typeof question !== "string" || !question.trim())
|
|
210
|
+
throw new Error("judge() 需要问题字符串");
|
|
211
|
+
const answer = await subagentImpl({
|
|
212
|
+
task: `回答以下判定问题。只允许回答一个词:PASS 或 FAIL,不要任何其他内容。\n\n判定问题:${question.trim()}`,
|
|
213
|
+
noTools: true,
|
|
214
|
+
});
|
|
215
|
+
const pass = /\bPASS\b/i.test(answer) && !/\bFAIL\b/i.test(answer);
|
|
216
|
+
if (!pass && !/\bFAIL\b/i.test(answer))
|
|
217
|
+
logs.push(`⚠ judge 回答无法解析,按 FAIL 处理:${answer.slice(0, 120)}`);
|
|
218
|
+
return pass;
|
|
150
219
|
}
|
|
151
220
|
/** barrier 扇出:全部并发(封顶排队),单个失败该项为 null,不拖垮整批。 */
|
|
152
221
|
async function parallel(thunks) {
|
|
@@ -201,6 +270,7 @@ export async function runWorkflow(opts) {
|
|
|
201
270
|
subagent: subagentImpl,
|
|
202
271
|
parallel,
|
|
203
272
|
pipeline,
|
|
273
|
+
judge: judgeImpl,
|
|
204
274
|
log: pushLog,
|
|
205
275
|
console: { log: pushLog, info: pushLog, warn: pushLog, error: pushLog },
|
|
206
276
|
setTimeout: (fn, ms) => setTimeout(fn, Math.max(0, Math.min(Number(ms) || 0, 120_000))),
|
|
@@ -223,8 +293,9 @@ export async function runWorkflow(opts) {
|
|
|
223
293
|
}
|
|
224
294
|
const seconds = Math.round((Date.now() - started) / 1000);
|
|
225
295
|
const aborted = internal.signal.aborted;
|
|
226
|
-
const
|
|
227
|
-
const
|
|
296
|
+
const budgetExhausted = budgetTokens > 0 && spentTokens >= budgetTokens;
|
|
297
|
+
const details = { ...stats, seconds, aborted, spentTokens, budgetExhausted };
|
|
298
|
+
const lines = [`工作流结束,用时 ${seconds}s · 子任务 ✓${stats.ok} ✗${stats.failed}${stats.cached ? ` · 续跑命中 ${stats.cached}` : ""} · 用 ${spentTokens.toLocaleString()} tokens${budgetTokens > 0 ? ` / 预算 ${budgetTokens.toLocaleString()}` : " (不限)"}${timedOut ? " · ⏱ 整体超时" : ""}${aborted && !timedOut ? " · 已中止" : ""}`, ""];
|
|
228
299
|
if (completionValue !== undefined) {
|
|
229
300
|
let shown;
|
|
230
301
|
try {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface WorkflowTemplate {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
/** 生成脚本源码;input 已由调用方做过形状校验。 */
|
|
5
|
+
build: (input: Record<string, unknown>) => string;
|
|
6
|
+
/** 给模型看的 input 说明。 */
|
|
7
|
+
inputHint: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const TEMPLATES: Record<string, WorkflowTemplate>;
|
|
10
|
+
/** 按模板名生成脚本;生成后自检必须过静态校验(防模板本身写出不合规代码)。 */
|
|
11
|
+
export declare function buildFromTemplate(template: string, input: Record<string, unknown>): string;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { validateScript } from "./runner.js";
|
|
3
|
+
function str(v, fallback = "") {
|
|
4
|
+
return typeof v === "string" && v.trim() ? v.trim() : fallback;
|
|
5
|
+
}
|
|
6
|
+
function strArray(v) {
|
|
7
|
+
return Array.isArray(v)
|
|
8
|
+
? v.filter((x) => typeof x === "string" && !!x.trim()).map((x) => x.trim())
|
|
9
|
+
: [];
|
|
10
|
+
}
|
|
11
|
+
/** JSON.stringify 后内插进脚本;保证引号安全。 */
|
|
12
|
+
function lit(v) {
|
|
13
|
+
return JSON.stringify(v ?? null);
|
|
14
|
+
}
|
|
15
|
+
/** git 改动文件列表(模板构建期在主进程执行,不进沙箱)。 */
|
|
16
|
+
function gitDiffFiles() {
|
|
17
|
+
try {
|
|
18
|
+
const out = execFileSync("git", ["diff", "--name-only", "HEAD"], { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
19
|
+
return out.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export const TEMPLATES = {
|
|
26
|
+
i18n: {
|
|
27
|
+
name: "i18n",
|
|
28
|
+
description: "批量多语言翻译:每个语言一个子 agent 并行翻译,完成后统一校验 key 对齐并自动修复",
|
|
29
|
+
inputHint: '{ "langs": ["en", "ja", "de"], "source": "src/locales/zh.json", "dir": "src/locales" } — source/dir 可省略(默认 src/locales/zh.json 与同目录)',
|
|
30
|
+
build(input) {
|
|
31
|
+
const langs = strArray(input.langs);
|
|
32
|
+
if (langs.length === 0)
|
|
33
|
+
throw new Error('i18n 模板需要 langs 数组,例如 { "langs": ["en", "ja"] }');
|
|
34
|
+
const source = str(input.source, "src/locales/zh.json");
|
|
35
|
+
const dir = str(input.dir, source.includes("/") ? source.slice(0, source.lastIndexOf("/")) : "src/locales");
|
|
36
|
+
return `
|
|
37
|
+
const langs = ${lit(langs)};
|
|
38
|
+
log("i18n:翻译 " + langs.length + " 个语言,源文件 ${source}");
|
|
39
|
+
const results = await parallel(langs.map(lang => () =>
|
|
40
|
+
subagent({
|
|
41
|
+
task: "读取 ${source},把所有文案翻译成对应语言(保持所有 key 不变、输出合法 JSON、译文自然地道)," +
|
|
42
|
+
"写入 ${dir}/" + lang + ".json。写完后重新读一遍文件,确认是合法 JSON 且 key 与源文件完全一致。" +
|
|
43
|
+
"目标语言代码:" + lang
|
|
44
|
+
})
|
|
45
|
+
));
|
|
46
|
+
const okLangs = langs.filter((_, i) => results[i] !== null);
|
|
47
|
+
if (okLangs.length > 0) {
|
|
48
|
+
await subagent({
|
|
49
|
+
task: "对照 ${source} 检查 ${dir}/ 目录下这些语言的翻译文件:" + okLangs.join(",") + "." +
|
|
50
|
+
"逐个核对 key 是否与源文件完全一致、JSON 是否合法、译文是否有明显机翻痕迹,发现问题直接修复。最后汇报每个语言的状态"
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return { translated: okLangs, failed: langs.filter(l => !okLangs.includes(l)) };
|
|
54
|
+
`;
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
review: {
|
|
58
|
+
name: "review",
|
|
59
|
+
description: "全面代码审查:每个文件一个评审 agent 并行审查,发现项再交独立 agent 对抗性验证,只保留核实为真的问题",
|
|
60
|
+
inputHint: '{ "files": ["src/a.ts", "src/b.ts"] } — files 可省略,默认取 git diff --name-only HEAD 的改动文件',
|
|
61
|
+
build(input) {
|
|
62
|
+
let files = strArray(input.files);
|
|
63
|
+
if (files.length === 0)
|
|
64
|
+
files = gitDiffFiles();
|
|
65
|
+
if (files.length === 0)
|
|
66
|
+
throw new Error("review 模板需要 files 数组,或当前仓库有未提交改动");
|
|
67
|
+
return `
|
|
68
|
+
const files = ${lit(files)};
|
|
69
|
+
log("review:评审 " + files.length + " 个文件,发现项将做对抗性验证");
|
|
70
|
+
const reports = await parallel(files.map(f => () =>
|
|
71
|
+
subagent({
|
|
72
|
+
task: "通读 " + f + " 及其直接依赖,做代码审查:正确性 bug、安全漏洞、明显的性能问题。" +
|
|
73
|
+
"只报告有把握的问题,每条给出文件、行号、严重程度(高/中/低)、问题描述和建议修法。没有问题就明确说无发现。"
|
|
74
|
+
})
|
|
75
|
+
));
|
|
76
|
+
const findings = reports.filter(Boolean).join("\\n\\n---\\n\\n");
|
|
77
|
+
if (!findings.trim()) return { verified: "(所有文件均无发现)" };
|
|
78
|
+
const verified = await subagent({
|
|
79
|
+
task: "下面是一份代码审查发现列表。逐条对抗性验证:打开涉及的文件核对,剔除不成立、夸大或已过时的条目," +
|
|
80
|
+
"只保留核实为真的问题,按严重程度排序输出完整清单。\\n\\n" + findings
|
|
81
|
+
});
|
|
82
|
+
return { verified };
|
|
83
|
+
`;
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
research: {
|
|
87
|
+
name: "research",
|
|
88
|
+
description: "并行研究:每个方向一个子 agent 独立调查(可联网),最后汇总成一份综合报告",
|
|
89
|
+
inputHint: '{ "questions": ["方向一", "方向二"], "context": "可选背景" }',
|
|
90
|
+
build(input) {
|
|
91
|
+
const questions = strArray(input.questions);
|
|
92
|
+
if (questions.length === 0)
|
|
93
|
+
throw new Error('research 模板需要 questions 数组,例如 { "questions": ["竞品A的定价", "竞品B的功能"] }');
|
|
94
|
+
const context = str(input.context);
|
|
95
|
+
const contextLine = context ? `调查背景:${context}。` : "";
|
|
96
|
+
return `
|
|
97
|
+
const questions = ${lit(questions)};
|
|
98
|
+
log("research:并行调查 " + questions.length + " 个方向");
|
|
99
|
+
const reports = await parallel(questions.map(q => () =>
|
|
100
|
+
subagent({
|
|
101
|
+
task: "调查以下问题,可以用 web_search/web_fetch 联网,也可以读本地代码和文档。" +
|
|
102
|
+
"输出:关键事实(带来源)、不同观点、你的结论。${contextLine}问题:" + q
|
|
103
|
+
})
|
|
104
|
+
));
|
|
105
|
+
const merged = reports
|
|
106
|
+
.map((r, i) => (r ? "## 方向 " + (i + 1) + ":" + questions[i] + "\\n\\n" + r : null))
|
|
107
|
+
.filter(Boolean)
|
|
108
|
+
.join("\\n\\n");
|
|
109
|
+
if (!merged.trim()) return { synthesis: "(所有方向都失败了)", failed: questions.length };
|
|
110
|
+
const synthesis = await subagent({
|
|
111
|
+
task: "把以下几份调查报告综合成一份结论性报告:先给核心结论(不超过 5 条),再给分方向要点," +
|
|
112
|
+
"最后列分歧点和待确认项。不要编造报告里没有的信息。\\n\\n" + merged
|
|
113
|
+
});
|
|
114
|
+
return { synthesis };
|
|
115
|
+
`;
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
refactor: {
|
|
119
|
+
name: "refactor",
|
|
120
|
+
description: "批量重构/迁移:先由一个 agent 读代表文件制定统一规则,然后每个文件一个子 agent 在独立 worktree 里套用规则,互不冲突",
|
|
121
|
+
inputHint: '{ "instruction": "把所有 var 改为 const", "files": ["src/a.ts", ...] } — files 省略时用 git diff 改动文件',
|
|
122
|
+
build(input) {
|
|
123
|
+
const instruction = str(input.instruction);
|
|
124
|
+
if (!instruction)
|
|
125
|
+
throw new Error('refactor 模板需要 instruction,例如 { "instruction": "把 var 全部改为 const" }');
|
|
126
|
+
const files = strArray(input.files);
|
|
127
|
+
return `
|
|
128
|
+
const instruction = ${lit(instruction)};
|
|
129
|
+
let targetFiles = ${lit(files)};
|
|
130
|
+
if (targetFiles.length === 0) {
|
|
131
|
+
const listed = await subagent({
|
|
132
|
+
task: "运行 git diff --name-only HEAD,把输出的文件路径一行一个原样返回,不要加任何其他文字。"
|
|
133
|
+
});
|
|
134
|
+
targetFiles = listed.split("\\n").map(s => s.trim()).filter(Boolean);
|
|
135
|
+
}
|
|
136
|
+
if (targetFiles.length === 0) throw new Error("没有可处理的文件:files 为空且 git 无改动");
|
|
137
|
+
log("refactor:共 " + targetFiles.length + " 个文件,先制定统一规则");
|
|
138
|
+
const rule = await subagent({
|
|
139
|
+
task: "我们要对一批文件做统一重构:" + instruction + "。" +
|
|
140
|
+
"先读其中 1-2 个代表性文件了解现状,然后输出一份精确的重构规则清单(每条:什么模式改成什么、边界情况怎么处理)。" +
|
|
141
|
+
"规则要具体到可以直接照做,不要空话。涉及文件:" + targetFiles.slice(0, 10).join(",")
|
|
142
|
+
});
|
|
143
|
+
const results = await parallel(targetFiles.map(f => () =>
|
|
144
|
+
subagent({
|
|
145
|
+
task: "按以下重构规则处理文件 " + f + ",改完检查该文件语法正确(必要时运行 tsc 或构建验证)。\\n\\n重构规则:\\n" + rule,
|
|
146
|
+
worktree: true
|
|
147
|
+
})
|
|
148
|
+
));
|
|
149
|
+
const done = targetFiles.filter((_, i) => results[i] !== null);
|
|
150
|
+
return { applied: done, failed: targetFiles.filter(f => !done.includes(f)), rule };
|
|
151
|
+
`;
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
/** 按模板名生成脚本;生成后自检必须过静态校验(防模板本身写出不合规代码)。 */
|
|
156
|
+
export function buildFromTemplate(template, input) {
|
|
157
|
+
const t = TEMPLATES[template];
|
|
158
|
+
if (!t) {
|
|
159
|
+
throw new Error(`未知模板 "${template}",可用:${Object.keys(TEMPLATES).join(", ")}`);
|
|
160
|
+
}
|
|
161
|
+
const code = t.build(input ?? {});
|
|
162
|
+
const errors = validateScript(code);
|
|
163
|
+
if (errors.length > 0) {
|
|
164
|
+
throw new Error(`模板 ${template} 生成的脚本未通过静态校验(模板 bug,请反馈):${errors.join(";")}`);
|
|
165
|
+
}
|
|
166
|
+
return code;
|
|
167
|
+
}
|
package/dist/workflow/tool.d.ts
CHANGED
|
@@ -6,7 +6,11 @@ import type { ParentModelRef } from "../subagent.js";
|
|
|
6
6
|
*/
|
|
7
7
|
export declare function createRunWorkflowTool(getParentModel: () => ParentModelRef): import("@earendil-works/pi-coding-agent").ToolDefinition<Type.TObject<{
|
|
8
8
|
script: Type.TOptional<Type.TString>;
|
|
9
|
+
template: Type.TOptional<Type.TString>;
|
|
10
|
+
input: Type.TOptional<Type.TRecord<"^.*$", Type.TUnknown>>;
|
|
9
11
|
script_path: Type.TOptional<Type.TString>;
|
|
12
|
+
save_as: Type.TOptional<Type.TString>;
|
|
10
13
|
resume: Type.TOptional<Type.TBoolean>;
|
|
11
14
|
timeout_minutes: Type.TOptional<Type.TNumber>;
|
|
15
|
+
budget_tokens: Type.TOptional<Type.TNumber>;
|
|
12
16
|
}>, unknown, any> & import("@earendil-works/pi-coding-agent").ToolDefinition<any, any, any>;
|
package/dist/workflow/tool.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { resolve } from "node:path";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
3
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { Text } from "@earendil-works/pi-tui";
|
|
5
5
|
import { Type } from "typebox";
|
|
6
6
|
import { compactResultRender, truncate } from "../tools.js";
|
|
7
|
-
import { runWorkflow, saveWorkflowScript, validateScript, workflowsDir, WORKFLOW_TIMEOUT_MS, } from "./runner.js";
|
|
7
|
+
import { runWorkflow, saveWorkflowScript, validateScript, workflowsDir, WORKFLOW_DEFAULT_BUDGET_TOKENS, WORKFLOW_TIMEOUT_MS, } from "./runner.js";
|
|
8
|
+
import { buildFromTemplate, TEMPLATES } from "./templates.js";
|
|
8
9
|
/**
|
|
9
10
|
* run_workflow 工具:主 agent 把模型生成的编排脚本交给 WorkflowRunner 执行。
|
|
10
11
|
* 脚本在 vm 沙箱里跑,只能用注入的 subagent/parallel/pipeline 原语。
|
|
@@ -27,9 +28,20 @@ export function createRunWorkflowTool(getParentModel) {
|
|
|
27
28
|
],
|
|
28
29
|
parameters: Type.Object({
|
|
29
30
|
script: Type.Optional(Type.String({ description: "Full JavaScript source of the orchestration script (top-level await allowed)." })),
|
|
31
|
+
template: Type.Optional(Type.String({
|
|
32
|
+
description: `Use a built-in template instead of writing a script: ${Object.keys(TEMPLATES).join(" / ")}. ` +
|
|
33
|
+
"Provide `input` to parametrize it. Prefer this over hand-writing a script when a template fits.",
|
|
34
|
+
})),
|
|
35
|
+
input: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
|
|
36
|
+
description: 'Parameters for `template`, e.g. i18n: { "langs": ["en", "ja"] }; review/refactor: { "files": [...] } or omit for git-diff files; research: { "questions": [...] }.',
|
|
37
|
+
})),
|
|
30
38
|
script_path: Type.Optional(Type.String({
|
|
31
39
|
description: "Path to an existing workflow script under .u1s1/workflows/ — rerun it instead of passing `script`.",
|
|
32
40
|
})),
|
|
41
|
+
save_as: Type.Optional(Type.String({
|
|
42
|
+
description: 'Save this run\'s script as .u1s1/workflows/<name>.mjs (letters/digits/-/_) for easy reuse; rerun later via script_path. Fails if the name already exists.',
|
|
43
|
+
pattern: "^[a-zA-Z0-9_-]+$",
|
|
44
|
+
})),
|
|
33
45
|
resume: Type.Optional(Type.Boolean({
|
|
34
46
|
description: "Skip sub-tasks already recorded as succeeded in the progress file (for crash/interrupt recovery).",
|
|
35
47
|
})),
|
|
@@ -38,6 +50,11 @@ export function createRunWorkflowTool(getParentModel) {
|
|
|
38
50
|
minimum: 1,
|
|
39
51
|
maximum: 180,
|
|
40
52
|
})),
|
|
53
|
+
budget_tokens: Type.Optional(Type.Number({
|
|
54
|
+
description: `Total token ceiling for the whole run across all sub-agents (default ${WORKFLOW_DEFAULT_BUDGET_TOKENS}). Once exceeded, no new sub-agents are spawned; running ones finish. Use 0 for unlimited.`,
|
|
55
|
+
minimum: 0,
|
|
56
|
+
maximum: 1_000_000_000,
|
|
57
|
+
})),
|
|
41
58
|
}),
|
|
42
59
|
// 精简展示与 spawn_subagent 同款:收起一行摘要,ctrl+o 展开报告
|
|
43
60
|
renderShell: "self",
|
|
@@ -53,15 +70,27 @@ export function createRunWorkflowTool(getParentModel) {
|
|
|
53
70
|
: "🧭 工作流";
|
|
54
71
|
return compactResultRender(result, options, theme, context, summary);
|
|
55
72
|
},
|
|
56
|
-
async execute(_toolCallId, params, signal) {
|
|
73
|
+
async execute(_toolCallId, params, signal, onUpdate) {
|
|
57
74
|
let code = params.script?.trim();
|
|
58
75
|
let scriptPath;
|
|
76
|
+
const named = params.save_as?.trim();
|
|
77
|
+
if (named && !/^[a-zA-Z0-9_-]+$/.test(named)) {
|
|
78
|
+
throw new Error("save_as 只允许字母、数字、横线和下划线");
|
|
79
|
+
}
|
|
59
80
|
if (code) {
|
|
60
81
|
const errors = validateScript(code);
|
|
61
82
|
if (errors.length > 0) {
|
|
62
83
|
throw new Error(`脚本未通过静态校验,请修复后重试:\n- ${errors.join("\n- ")}`);
|
|
63
84
|
}
|
|
64
|
-
|
|
85
|
+
}
|
|
86
|
+
else if (params.template?.trim()) {
|
|
87
|
+
// 内置模板:模型只填参数,脚本由模板生成(生成后自检过静态校验)
|
|
88
|
+
try {
|
|
89
|
+
code = buildFromTemplate(params.template.trim(), (params.input ?? {}));
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
throw new Error(`${e.message}\n模板 input 说明:${TEMPLATES[params.template.trim()]?.inputHint ?? "—"}`);
|
|
93
|
+
}
|
|
65
94
|
}
|
|
66
95
|
else if (params.script_path?.trim()) {
|
|
67
96
|
scriptPath = resolve(params.script_path.trim());
|
|
@@ -73,9 +102,38 @@ export function createRunWorkflowTool(getParentModel) {
|
|
|
73
102
|
}
|
|
74
103
|
}
|
|
75
104
|
else {
|
|
76
|
-
throw new Error("script
|
|
105
|
+
throw new Error("script、template、script_path 至少填一个");
|
|
106
|
+
}
|
|
107
|
+
// 落盘:save_as 用语义化名字(同名报错防覆盖进度配对),否则时间戳名;
|
|
108
|
+
// script_path 分支上面已读入,不重复落盘
|
|
109
|
+
if (!scriptPath) {
|
|
110
|
+
scriptPath = named ? resolve(workflowsDir(), `${named}.mjs`) : saveWorkflowScript(code ?? "");
|
|
111
|
+
if (named && existsSync(scriptPath)) {
|
|
112
|
+
throw new Error(`工作流 "${named}" 已存在(${scriptPath})。要重跑它请用 script_path 指向该文件(可加 resume:true 续跑);要新建请换个名字`);
|
|
113
|
+
}
|
|
114
|
+
mkdirSync(dirname(scriptPath), { recursive: true });
|
|
115
|
+
writeFileSync(scriptPath, code ?? "");
|
|
77
116
|
}
|
|
78
117
|
const progressPath = scriptPath.replace(/\.mjs$/, ".progress.jsonl");
|
|
118
|
+
// 流式进度:onProgress 高频触发,节流到 ≥2s 一次才推给 TUI
|
|
119
|
+
let lastPush = 0;
|
|
120
|
+
const onProgress = onUpdate
|
|
121
|
+
? (p) => {
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
if (now - lastPush < 2_000)
|
|
124
|
+
return;
|
|
125
|
+
lastPush = now;
|
|
126
|
+
onUpdate({
|
|
127
|
+
content: [
|
|
128
|
+
{
|
|
129
|
+
type: "text",
|
|
130
|
+
text: `🧭 子任务 ✓${p.ok} ✗${p.failed}${p.cached ? ` 缓存${p.cached}` : ""} · 在跑 ${p.running} · ${(p.spentTokens / 1000).toFixed(0)}k${p.budgetTokens > 0 ? ` / ${(p.budgetTokens / 1000).toFixed(0)}k tokens` : ""}`,
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
details: p,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
: undefined;
|
|
79
137
|
const result = await runWorkflow({
|
|
80
138
|
code: code ?? "",
|
|
81
139
|
progressPath,
|
|
@@ -83,6 +141,8 @@ export function createRunWorkflowTool(getParentModel) {
|
|
|
83
141
|
parentModel: getParentModel(),
|
|
84
142
|
signal,
|
|
85
143
|
timeoutMs: Math.min(180, params.timeout_minutes ?? WORKFLOW_TIMEOUT_MS / 60_000) * 60_000,
|
|
144
|
+
budgetTokens: params.budget_tokens,
|
|
145
|
+
onProgress,
|
|
86
146
|
});
|
|
87
147
|
return {
|
|
88
148
|
content: [{ type: "text", text: truncate(result.report) }],
|