u1s1-cli 1.13.2 → 1.14.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.d.ts +2 -5
- package/dist/agent-setup.js +6 -43
- package/dist/index.js +2 -2
- package/dist/subagent.js +1 -1
- package/dist/tools.js +1 -1
- package/dist/web.js +3 -3
- package/package.json +1 -1
- package/dist/workflow/runner.d.ts +0 -67
- package/dist/workflow/runner.js +0 -349
- package/dist/workflow/templates.d.ts +0 -11
- package/dist/workflow/templates.js +0 -200
- package/dist/workflow/tool.d.ts +0 -16
- package/dist/workflow/tool.js +0 -179
package/dist/agent-setup.d.ts
CHANGED
|
@@ -141,11 +141,8 @@ export declare function toProviderModels(models: ModelDef[], options?: {
|
|
|
141
141
|
export declare function endpointProviderEntry(ep: CustomEndpoint, options?: {
|
|
142
142
|
literalKey?: boolean;
|
|
143
143
|
}): Record<string, unknown>;
|
|
144
|
-
/**
|
|
145
|
-
|
|
146
|
-
* 拆解任务 → 生成沙箱脚本 → 调 run_workflow 执行。每次启动重写,便于随版迭代文案。
|
|
147
|
-
*/
|
|
148
|
-
export declare function ensureWorkflowPromptTemplate(): void;
|
|
144
|
+
/** run_workflow 已下线:清掉旧版本每次启动写入的 /workflow 提示词模板,免得它引导模型调用不存在的工具。 */
|
|
145
|
+
export declare function removeLegacyWorkflowPromptTemplate(): void;
|
|
149
146
|
export declare function scrubForeignProviderEnv(): void;
|
|
150
147
|
/** 把各端点密钥放进环境(TUI 直接 process.env;web 传给子进程)。 */
|
|
151
148
|
export declare function endpointKeyEnv(): Record<string, string>;
|
package/dist/agent-setup.js
CHANGED
|
@@ -314,7 +314,7 @@ export function writeWebToolsExtension(cfg, features) {
|
|
|
314
314
|
const imageLine = features.imageGen
|
|
315
315
|
? ` pi.registerTool(tools.createImageTool({ baseUrl, apiKey: process.env.U1S1_API_KEY }));\n`
|
|
316
316
|
: "";
|
|
317
|
-
// spawn_subagent
|
|
317
|
+
// spawn_subagent 只给主会话注册;子 agent 环境里跳过,杜绝孙 agent 递归 spawn
|
|
318
318
|
const subagentBlock = ` if (process.env.U1S1_IN_SUBAGENT !== "1") {\n` +
|
|
319
319
|
` let getParentModel = () => undefined;\n` +
|
|
320
320
|
` pi.on("session_start", (_event, ctx) => {\n` +
|
|
@@ -324,10 +324,6 @@ export function writeWebToolsExtension(cfg, features) {
|
|
|
324
324
|
` }\n` +
|
|
325
325
|
` });\n` +
|
|
326
326
|
` pi.registerTool(tools.createSubagentTool(getParentModel));\n` +
|
|
327
|
-
` try {\n` +
|
|
328
|
-
` const workflow = await import(${JSON.stringify(new URL("./workflow/tool.js", import.meta.url).href)});\n` +
|
|
329
|
-
` pi.registerTool(workflow.createRunWorkflowTool(getParentModel));\n` +
|
|
330
|
-
` } catch {}\n` +
|
|
331
327
|
` }\n`;
|
|
332
328
|
// 密钥剥离守卫也从这里装:Desktop App 的 pi 跑在 pi-web-ui 子进程里,没有
|
|
333
329
|
// 启动器代码,只有扩展能替它把 U1S1_API_KEY / U1S1_EP_KEY_* 挡在子进程之外
|
|
@@ -699,45 +695,12 @@ export function endpointProviderEntry(ep, options = {}) {
|
|
|
699
695
|
models: toProviderModels(ep.models),
|
|
700
696
|
};
|
|
701
697
|
}
|
|
702
|
-
/**
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
export function ensureWorkflowPromptTemplate() {
|
|
707
|
-
const dir = join(agentDir, "prompts");
|
|
708
|
-
mkdirSync(dir, { recursive: true });
|
|
709
|
-
const content = `---
|
|
710
|
-
description: Orchestrate many sub-agents for a large parallelizable task via run_workflow
|
|
711
|
-
argument-hint: "<task description>"
|
|
712
|
-
---
|
|
713
|
-
# Workflow: $ARGUMENTS
|
|
714
|
-
|
|
715
|
-
First decide whether this task really needs a workflow. Good fit: many independent pieces (e.g. translating each locale file, renaming across hundreds of files, reviewing many modules). Bad fit: a few small edits — just do them directly with your own tools.
|
|
716
|
-
|
|
717
|
-
If it is a fit:
|
|
718
|
-
|
|
719
|
-
0. **Prefer a built-in template** when one matches — call \`run_workflow\` with \`template\` + \`input\` instead of writing a script:
|
|
720
|
-
- \`i18n\` — translate locale file(s) to N languages: { "langs": ["en", "ja"], "source": "src/locales/zh.json" }
|
|
721
|
-
- \`review\` — per-file review + adversarial verification: { "files": [...] } or omit for git-diff files
|
|
722
|
-
- \`research\` — parallel investigation + synthesis: { "questions": ["..."], "context": "..." }
|
|
723
|
-
- \`refactor\` — rule-first batch refactor in isolated worktrees: { "instruction": "...", "files": [...] }
|
|
724
|
-
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:
|
|
725
|
-
- \`subagent(task)\` or \`subagent({ task, model })\` — runs one sub-agent, resolves to its final output text
|
|
726
|
-
- \`parallel([() => ..., ...])\` — concurrent fan-out; each thunk's error becomes \`null\`; barrier semantics
|
|
727
|
-
- \`pipeline(items, [stage1, stage2, ...])\` — each item flows through all stages independently (prefer over parallel)
|
|
728
|
-
- \`judge(question)\` — cheap tool-free model call returning true/false; use for loop-until-done checks
|
|
729
|
-
- \`subagent({ task, worktree: true })\` — run in an isolated temp git worktree (auto-cleaned) when tasks would write conflicting files
|
|
730
|
-
- \`log(...)\`, \`setTimeout/clearTimeout\`, standard JS builtins (Promise/JSON/Math/...)
|
|
731
|
-
- NO require/import/process/fetch/fs access — the static validator rejects scripts that try
|
|
732
|
-
2. Every task string must be fully self-contained: which files to read/write, what "done" means, how to self-verify.
|
|
733
|
-
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\`.
|
|
734
|
-
|
|
735
|
-
Keep scripts deterministic (same order of subagent calls every run) so resume works. Summarize the outcome to the user afterwards in their language.
|
|
736
|
-
`;
|
|
737
|
-
const p = join(dir, "workflow.md");
|
|
738
|
-
if (!existsSync(p) || readFileSync(p, "utf8") !== content) {
|
|
739
|
-
writeFileSync(p, content);
|
|
698
|
+
/** run_workflow 已下线:清掉旧版本每次启动写入的 /workflow 提示词模板,免得它引导模型调用不存在的工具。 */
|
|
699
|
+
export function removeLegacyWorkflowPromptTemplate() {
|
|
700
|
+
try {
|
|
701
|
+
rmSync(join(agentDir, "prompts", "workflow.md"), { force: true });
|
|
740
702
|
}
|
|
703
|
+
catch { }
|
|
741
704
|
}
|
|
742
705
|
/**
|
|
743
706
|
* pi 会把环境里的厂商密钥(ANTHROPIC_API_KEY 等)当作「已配置的 provider」,
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execSync, spawnSync } from "node:child_process";
|
|
3
3
|
import { writeFileSync } from "node:fs";
|
|
4
|
-
import { cleanupBrandThemes, endpointKeyEnv, endpointProviderEntry, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels,
|
|
4
|
+
import { cleanupBrandThemes, endpointKeyEnv, endpointProviderEntry, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, removeLegacyWorkflowPromptTemplate, scrubForeignProviderEnv, toProviderModels, writeAnnouncementsExtension, writeAttributionExtension, writeMcpExtension, writeCompactUiExtension, writeErrorHumanizeExtension, writeWebToolsExtension, } from "./agent-setup.js";
|
|
5
5
|
import { Text } from "@earendil-works/pi-tui";
|
|
6
6
|
import { printConsoleBanner } from "./brand.js";
|
|
7
7
|
import { activeSite, agentDir, apiModelToDef, CUSTOM_ENDPOINTS, isPortableInstall, loadConfig, MODELS, persistPreferredModel, PROVIDER_ID, readCompactionBudgetSetting, readCompactionReserveTokens, readSettings, refValid, resolvePreferredModel, setModelsFromApi, VERSION, } from "./config.js";
|
|
@@ -245,7 +245,7 @@ async function runAgent(cfg, args) {
|
|
|
245
245
|
const officialCfg = { ...cfg, baseUrl: signing.baseUrl, apiKey: signing.localKey };
|
|
246
246
|
ensureBrandPrompt(await shellReady);
|
|
247
247
|
ensureProviderModels(officialCfg);
|
|
248
|
-
|
|
248
|
+
removeLegacyWorkflowPromptTemplate();
|
|
249
249
|
// 联网工具经 agentDir/extensions 投影,TUI 和 Desktop App 共用一份注册
|
|
250
250
|
writeWebToolsExtension(officialCfg, {
|
|
251
251
|
webSearch: webSearchEnabled,
|
package/dist/subagent.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createAgentSession, DefaultResourceLoader, getAgentDir, hasTrustRequiringProjectResources, ModelRuntime, ProjectTrustStore, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { tr } from "./i18n.js";
|
|
3
|
-
// ---- 子 agent spawn 基础设施:spawn_subagent
|
|
3
|
+
// ---- 子 agent spawn 基础设施:spawn_subagent 工具使用 ----
|
|
4
4
|
/** 同时在跑的子 agent 上限;超额排队,防止打爆网关。 */
|
|
5
5
|
export const SUBAGENT_CONCURRENCY = 4;
|
|
6
6
|
/** 单个子任务默认超时;到点 abort,不让一个卡死的任务拖住整批。 */
|
package/dist/tools.js
CHANGED
|
@@ -209,7 +209,7 @@ export function createSearchTool(cfg) {
|
|
|
209
209
|
},
|
|
210
210
|
});
|
|
211
211
|
}
|
|
212
|
-
// ---- spawn_subagent 工具:spawn 设施在 ./subagent.ts
|
|
212
|
+
// ---- spawn_subagent 工具:spawn 设施在 ./subagent.ts ----
|
|
213
213
|
/** 单次调用最多并行任务数;再多模型也看不过来,让主 agent 分批调。 */
|
|
214
214
|
const SUBAGENT_MAX_TASKS = 10;
|
|
215
215
|
/** 子 agent 工具:独立上下文窗口干活,只把结果摘要带回主对话,不污染主 token 预算。 */
|
package/dist/web.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { cleanupBrandThemes, endpointKeyEnv, ensureAuthCredential, ensureBrandPrompt,
|
|
4
|
+
import { cleanupBrandThemes, endpointKeyEnv, ensureAuthCredential, ensureBrandPrompt, removeLegacyWorkflowPromptTemplate, ensureDefaultSettings, ensureProviderModels, scrubForeignProviderEnv, writeAttributionExtension, writeMcpExtension, writeWebToolsExtension, } from "./agent-setup.js";
|
|
5
5
|
import { agentDir, apiModelToDef, MODELS, resolvePreferredModel, setModelsFromApi, u1s1Dir, writeAgentDefaultModel, } from "./config.js";
|
|
6
6
|
import { ensureSearchTools } from "./search-tools.js";
|
|
7
7
|
import { ensureUsableShell } from "./shell-doctor.js";
|
|
@@ -87,8 +87,8 @@ export async function prepareWebEnv(cfg) {
|
|
|
87
87
|
webOfficialCfg = officialCfg;
|
|
88
88
|
const modelsPath = refreshWebModels();
|
|
89
89
|
ensureBrandPrompt(await shellReady);
|
|
90
|
-
// /workflow
|
|
91
|
-
|
|
90
|
+
// 清理旧版 /workflow 提示词模板
|
|
91
|
+
removeLegacyWorkflowPromptTemplate();
|
|
92
92
|
// pi-web-ui 靠 auth.json 判断「已配置」,否则网页会弹 pi 安装引导
|
|
93
93
|
ensureAuthCredential();
|
|
94
94
|
// 联网工具经 agentDir/extensions 投影,和 TUI 共用一份注册
|
package/package.json
CHANGED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { type ParentModelRef } from "../subagent.js";
|
|
2
|
-
/** 整个 run 的默认墙钟上限。 */
|
|
3
|
-
export declare const WORKFLOW_TIMEOUT_MS: number;
|
|
4
|
-
/** 默认整个 run 的 token 预算;0 或负数表示不设限。 */
|
|
5
|
-
export declare const WORKFLOW_DEFAULT_BUDGET_TOKENS = 20000000;
|
|
6
|
-
interface ProgressEntry {
|
|
7
|
-
key: string;
|
|
8
|
-
/** 任务摘要(前 120 字),调试/排查用,key 才是身份。 */
|
|
9
|
-
task: string;
|
|
10
|
-
ok: boolean;
|
|
11
|
-
text: string;
|
|
12
|
-
ms: number;
|
|
13
|
-
}
|
|
14
|
-
/** JSONL 进度存档:一行一个已完成的子任务,断点续跑时按 key 跳过。 */
|
|
15
|
-
export declare class ProgressStore {
|
|
16
|
-
#private;
|
|
17
|
-
readonly path: string;
|
|
18
|
-
constructor(path: string);
|
|
19
|
-
getSuccessful(key: string): ProgressEntry | undefined;
|
|
20
|
-
append(entry: ProgressEntry): void;
|
|
21
|
-
}
|
|
22
|
-
/** 静态白名单校验:语法能编译 + 不碰沙箱之外的任何能力。 */
|
|
23
|
-
export declare function validateScript(code: string): string[];
|
|
24
|
-
export interface WorkflowRunResult {
|
|
25
|
-
/** 给主 agent 的完整报告(markdown)。 */
|
|
26
|
-
report: string;
|
|
27
|
-
details: {
|
|
28
|
-
spawns: number;
|
|
29
|
-
ok: number;
|
|
30
|
-
failed: number;
|
|
31
|
-
cached: number;
|
|
32
|
-
seconds: number;
|
|
33
|
-
aborted: boolean;
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
export interface WorkflowProgress {
|
|
37
|
-
spawns: number;
|
|
38
|
-
ok: number;
|
|
39
|
-
failed: number;
|
|
40
|
-
cached: number;
|
|
41
|
-
running: number;
|
|
42
|
-
spentTokens: number;
|
|
43
|
-
budgetTokens: number;
|
|
44
|
-
}
|
|
45
|
-
export interface WorkflowRunOptions {
|
|
46
|
-
/** 脚本源码(调用方负责落盘)。 */
|
|
47
|
-
code: string;
|
|
48
|
-
/** 进度存档路径;同名文件存在且 resume=true 时跳过已完成任务。 */
|
|
49
|
-
progressPath: string;
|
|
50
|
-
resume?: boolean;
|
|
51
|
-
parentModel: ParentModelRef;
|
|
52
|
-
/** 主会话的项目信任裁决;子 agent 与临时 worktree 都沿用它,缺省按 trust.json 非交互裁决。 */
|
|
53
|
-
projectTrusted?: boolean;
|
|
54
|
-
signal?: AbortSignal;
|
|
55
|
-
timeoutMs?: number;
|
|
56
|
-
/** 整个 run 的 token 上限;缺省 WORKFLOW_DEFAULT_BUDGET_TOKENS,<=0 不限。 */
|
|
57
|
-
budgetTokens?: number;
|
|
58
|
-
/** 每次子任务状态变化时回调(工具层节流后转成流式进度)。 */
|
|
59
|
-
onProgress?: (p: WorkflowProgress) => void;
|
|
60
|
-
}
|
|
61
|
-
/** 执行一个 workflow 脚本,返回给主对话的报告。不抛错——失败也进报告让模型自行修复。 */
|
|
62
|
-
export declare function runWorkflow(opts: WorkflowRunOptions): Promise<WorkflowRunResult>;
|
|
63
|
-
/** 工作区目录:项目根 .u1s1/workflows/(脚本 + 进度存档都在这里)。 */
|
|
64
|
-
export declare function workflowsDir(): string;
|
|
65
|
-
/** 内联脚本落盘,返回脚本路径(进度存档按同名约定派生)。 */
|
|
66
|
-
export declare function saveWorkflowScript(code: string): string;
|
|
67
|
-
export {};
|
package/dist/workflow/runner.js
DELETED
|
@@ -1,349 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
-
import vm from "node:vm";
|
|
5
|
-
import { dirname, resolve } from "node:path";
|
|
6
|
-
import { tr } from "../i18n.js";
|
|
7
|
-
import { runPool, runSubagent, SUBAGENT_CONCURRENCY } from "../subagent.js";
|
|
8
|
-
// ---- WorkflowRunner:执行模型生成的编排脚本(路线 A:主进程 vm 上下文) ----
|
|
9
|
-
// 定位「防误不防恶」:vm 非硬安全边界,靠白名单注入 + 静态校验 + 脚本落盘可审查兜底。
|
|
10
|
-
/** 整个 run 的默认墙钟上限。 */
|
|
11
|
-
export const WORKFLOW_TIMEOUT_MS = 30 * 60_000;
|
|
12
|
-
/** 单个子任务的超时(与 spawn_subagent 工具一致)。 */
|
|
13
|
-
const PER_TASK_TIMEOUT_MS = 15 * 60_000;
|
|
14
|
-
/** 报告里最多保留多少行脚本 log 输出。 */
|
|
15
|
-
const MAX_LOG_LINES = 60;
|
|
16
|
-
/** 默认整个 run 的 token 预算;0 或负数表示不设限。 */
|
|
17
|
-
export const WORKFLOW_DEFAULT_BUDGET_TOKENS = 20_000_000;
|
|
18
|
-
/** JSONL 进度存档:一行一个已完成的子任务,断点续跑时按 key 跳过。 */
|
|
19
|
-
export class ProgressStore {
|
|
20
|
-
path;
|
|
21
|
-
#cache = new Map();
|
|
22
|
-
constructor(path) {
|
|
23
|
-
this.path = path;
|
|
24
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
25
|
-
if (!existsSync(path))
|
|
26
|
-
return;
|
|
27
|
-
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
28
|
-
if (!line.trim())
|
|
29
|
-
continue;
|
|
30
|
-
try {
|
|
31
|
-
const e = JSON.parse(line);
|
|
32
|
-
if (typeof e.key === "string")
|
|
33
|
-
this.#cache.set(e.key, e);
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
// 尾部半行(进程被杀)直接忽略
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
getSuccessful(key) {
|
|
41
|
-
const entry = this.#cache.get(key);
|
|
42
|
-
return entry?.ok === true && typeof entry.text === "string" ? entry : undefined;
|
|
43
|
-
}
|
|
44
|
-
append(entry) {
|
|
45
|
-
this.#cache.set(entry.key, entry);
|
|
46
|
-
appendFileSync(this.path, `${JSON.stringify(entry)}\n`);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* 断点续跑的任务标识:任务内容 + 模型的哈希。
|
|
51
|
-
* 前提是脚本确定性好 —— 相同任务串在续跑时视为同一任务(循环里重复相同任务串
|
|
52
|
-
* 的场景续跑粒度不精确,W1 接受此限制;i18n 这类每项任务内容唯一的场景完全够用)。
|
|
53
|
-
*/
|
|
54
|
-
function taskKey(task, model) {
|
|
55
|
-
return createHash("sha1").update(`${task}|${model ?? ""}`).digest("hex").slice(0, 16);
|
|
56
|
-
}
|
|
57
|
-
/** 去掉注释和字符串字面量后再扫禁用模式,避免文档式注释误报。 */
|
|
58
|
-
function stripCommentsAndStrings(code) {
|
|
59
|
-
return code
|
|
60
|
-
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
|
61
|
-
.replace(/(^|[^:])\/\/[^\n]*/g, "$1 ")
|
|
62
|
-
.replace(/"(?:[^"\\\n]|\\.)*"/g, '""')
|
|
63
|
-
.replace(/'(?:[^'\\\n]|\\.)*'/g, "''")
|
|
64
|
-
.replace(/`(?:[^`\\]|\\.)*`/g, "``");
|
|
65
|
-
}
|
|
66
|
-
/** 静态白名单校验:语法能编译 + 不碰沙箱之外的任何能力。 */
|
|
67
|
-
export function validateScript(code) {
|
|
68
|
-
const errors = [];
|
|
69
|
-
try {
|
|
70
|
-
// 与执行时同款包装,保证报错行号一致
|
|
71
|
-
new vm.Script(`(async () => {\n${code}\n})()`, { filename: "workflow.mjs" });
|
|
72
|
-
}
|
|
73
|
-
catch (e) {
|
|
74
|
-
errors.push(tr(`语法错误:${e.message}`, `Syntax error: ${e.message}`));
|
|
75
|
-
return errors;
|
|
76
|
-
}
|
|
77
|
-
const bare = stripCommentsAndStrings(code);
|
|
78
|
-
const forbidden = [
|
|
79
|
-
[/\brequire\s*\(/, "require()"],
|
|
80
|
-
[/\bimport\s*\(/, tr("动态 import()", "dynamic import()")],
|
|
81
|
-
[/^\s*import\s/m, tr("静态 import", "static import")],
|
|
82
|
-
[/\bprocess\b/, tr("process 对象", "the process object")],
|
|
83
|
-
[/\bglobalThis\b/, "globalThis"],
|
|
84
|
-
[/\bfetch\s*\(/, "fetch()"],
|
|
85
|
-
[/\beval\s*\(/, "eval()"],
|
|
86
|
-
[/\bFunction\s*\(/, "new Function()"],
|
|
87
|
-
[/\.constructor\b/, tr(".constructor(原型链逃逸)", ".constructor (prototype-chain escape)")],
|
|
88
|
-
[/\bprototype\b/, tr("prototype(原型链逃逸)", "prototype (prototype-chain escape)")],
|
|
89
|
-
[/\b__proto__\b/, "__proto__"],
|
|
90
|
-
[/\bWebAssembly\b/, "WebAssembly"],
|
|
91
|
-
[/node:/, tr("node: 内置模块", "node: built-in modules")],
|
|
92
|
-
[/child_process/, "child_process"],
|
|
93
|
-
[/\bfs\b\s*\./, tr("fs 模块", "the fs module")],
|
|
94
|
-
];
|
|
95
|
-
for (const [re, label] of forbidden) {
|
|
96
|
-
if (re.test(bare))
|
|
97
|
-
errors.push(tr(`脚本使用了沙箱之外的能力:${label}`, `The script uses something outside the sandbox: ${label}`));
|
|
98
|
-
}
|
|
99
|
-
return errors;
|
|
100
|
-
}
|
|
101
|
-
/** 执行一个 workflow 脚本,返回给主对话的报告。不抛错——失败也进报告让模型自行修复。 */
|
|
102
|
-
export async function runWorkflow(opts) {
|
|
103
|
-
const started = Date.now();
|
|
104
|
-
const logs = [];
|
|
105
|
-
const stats = { spawns: 0, ok: 0, failed: 0, cached: 0 };
|
|
106
|
-
// 内部中止控制器:父会话 ESC 或整体超时都会触发,并传导到每个子 session
|
|
107
|
-
const internal = new AbortController();
|
|
108
|
-
const onParentAbort = () => internal.abort(opts.signal?.reason);
|
|
109
|
-
opts.signal?.addEventListener("abort", onParentAbort, { once: true });
|
|
110
|
-
let timedOut = false;
|
|
111
|
-
const overallTimer = setTimeout(() => {
|
|
112
|
-
timedOut = true;
|
|
113
|
-
const minutes = Math.round((opts.timeoutMs ?? WORKFLOW_TIMEOUT_MS) / 60_000);
|
|
114
|
-
internal.abort(new Error(tr(`整个工作流超时(${minutes} 分钟)`, `The workflow timed out (${minutes} minutes)`)));
|
|
115
|
-
}, opts.timeoutMs ?? WORKFLOW_TIMEOUT_MS);
|
|
116
|
-
const store = new ProgressStore(opts.progressPath);
|
|
117
|
-
const budgetTokens = opts.budgetTokens ?? WORKFLOW_DEFAULT_BUDGET_TOKENS;
|
|
118
|
-
let spentTokens = 0;
|
|
119
|
-
let running = 0;
|
|
120
|
-
const progress = { spawns: 0, ok: 0, failed: 0, cached: 0, running: 0, spentTokens: 0, budgetTokens };
|
|
121
|
-
const reportProgress = () => {
|
|
122
|
-
progress.spentTokens = spentTokens;
|
|
123
|
-
opts.onProgress?.({ ...progress });
|
|
124
|
-
};
|
|
125
|
-
/** git worktree 隔离:在 .u1s1/worktrees/ 下建临时工作树,返回其路径。 */
|
|
126
|
-
let worktreeSeq = 0;
|
|
127
|
-
function createTempWorktree() {
|
|
128
|
-
const dir = resolve(workflowsDir(), "../../.u1s1-worktrees", `wf-${Date.now()}-${++worktreeSeq}`);
|
|
129
|
-
execFileSync("git", ["worktree", "add", "--detach", dir], { stdio: "pipe" });
|
|
130
|
-
return dir;
|
|
131
|
-
}
|
|
132
|
-
function removeWorktree(dir) {
|
|
133
|
-
try {
|
|
134
|
-
execFileSync("git", ["worktree", "remove", "--force", dir], { stdio: "pipe" });
|
|
135
|
-
}
|
|
136
|
-
catch {
|
|
137
|
-
rmSync(dir, { recursive: true, force: true });
|
|
138
|
-
try {
|
|
139
|
-
execFileSync("git", ["worktree", "prune"], { stdio: "pipe" });
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
// 尽力清理即可
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
/** 注入沙箱的 subagent():字符串或 {task, model, worktree} 都行,返回最终输出文本。 */
|
|
147
|
-
async function subagentImpl(input) {
|
|
148
|
-
const o = typeof input === "string" ? { task: input } : (input ?? {});
|
|
149
|
-
const task = typeof o.task === "string" ? o.task.trim() : "";
|
|
150
|
-
if (!task)
|
|
151
|
-
throw new Error(tr("subagent() 需要 task 字符串", "subagent() needs a task string"));
|
|
152
|
-
const model = typeof o.model === "string" && o.model ? o.model : undefined;
|
|
153
|
-
if (internal.signal.aborted)
|
|
154
|
-
throw new Error(tr("工作流已中止,不再派发新子任务", "The workflow was aborted; no new subtasks will be started"));
|
|
155
|
-
if (budgetTokens > 0 && spentTokens >= budgetTokens) {
|
|
156
|
-
throw new Error(tr(`token 预算已用尽(已用 ${spentTokens.toLocaleString()} / 上限 ${budgetTokens.toLocaleString()}),不再派发新子任务`, `Token budget exhausted (used ${spentTokens.toLocaleString()} / limit ${budgetTokens.toLocaleString()}); no new subtasks will be started`));
|
|
157
|
-
}
|
|
158
|
-
const key = taskKey(task, model);
|
|
159
|
-
if (opts.resume) {
|
|
160
|
-
const prev = store.getSuccessful(key);
|
|
161
|
-
if (prev) {
|
|
162
|
-
stats.cached++;
|
|
163
|
-
progress.cached = stats.cached;
|
|
164
|
-
reportProgress();
|
|
165
|
-
return prev.text;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
stats.spawns++;
|
|
169
|
-
progress.spawns = stats.spawns;
|
|
170
|
-
running++;
|
|
171
|
-
progress.running = running;
|
|
172
|
-
reportProgress();
|
|
173
|
-
const t0 = Date.now();
|
|
174
|
-
let worktreeDir;
|
|
175
|
-
if (o.worktree === true)
|
|
176
|
-
worktreeDir = createTempWorktree();
|
|
177
|
-
try {
|
|
178
|
-
const out = await runSubagent({
|
|
179
|
-
task,
|
|
180
|
-
model,
|
|
181
|
-
parentModel: opts.parentModel,
|
|
182
|
-
timeoutMs: PER_TASK_TIMEOUT_MS,
|
|
183
|
-
signal: internal.signal,
|
|
184
|
-
cwd: worktreeDir,
|
|
185
|
-
// 临时 worktree 永远不在 trust.json 里:信任裁决按源仓库(主进程 cwd)算
|
|
186
|
-
projectTrusted: opts.projectTrusted,
|
|
187
|
-
trustCwd: process.cwd(),
|
|
188
|
-
noTools: o.noTools === true,
|
|
189
|
-
});
|
|
190
|
-
stats.ok++;
|
|
191
|
-
progress.ok = stats.ok;
|
|
192
|
-
spentTokens += out.usage.totalTokens;
|
|
193
|
-
store.append({ key, task: task.slice(0, 120), ok: true, text: out.text, ms: Date.now() - t0 });
|
|
194
|
-
return out.text;
|
|
195
|
-
}
|
|
196
|
-
catch (error) {
|
|
197
|
-
stats.failed++;
|
|
198
|
-
progress.failed = stats.failed;
|
|
199
|
-
const msg = error.message || tr("未知错误", "Unknown error");
|
|
200
|
-
store.append({ key, task: task.slice(0, 120), ok: false, text: msg, ms: Date.now() - t0 });
|
|
201
|
-
throw new Error(msg, { cause: error });
|
|
202
|
-
}
|
|
203
|
-
finally {
|
|
204
|
-
if (worktreeDir)
|
|
205
|
-
removeWorktree(worktreeDir);
|
|
206
|
-
running--;
|
|
207
|
-
progress.running = running;
|
|
208
|
-
reportProgress();
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
/** 注入沙箱的 judge():廉价无工具模型调用,严格回答 PASS/FAIL,转成布尔。 */
|
|
212
|
-
async function judgeImpl(question) {
|
|
213
|
-
if (typeof question !== "string" || !question.trim())
|
|
214
|
-
throw new Error(tr("judge() 需要问题字符串", "judge() needs a question string"));
|
|
215
|
-
const answer = await subagentImpl({
|
|
216
|
-
task: tr(`回答以下判定问题。只允许回答一个词:PASS 或 FAIL,不要任何其他内容。\n\n判定问题:${question.trim()}`, `Answer the following yes/no check. Reply with exactly one word, PASS or FAIL, and nothing else.\n\nCheck: ${question.trim()}`),
|
|
217
|
-
noTools: true,
|
|
218
|
-
});
|
|
219
|
-
const pass = /\bPASS\b/i.test(answer) && !/\bFAIL\b/i.test(answer);
|
|
220
|
-
if (!pass && !/\bFAIL\b/i.test(answer))
|
|
221
|
-
logs.push(tr(`⚠ judge 回答无法解析,按 FAIL 处理:${answer.slice(0, 120)}`, `⚠ Couldn't parse the judge answer; treating it as FAIL: ${answer.slice(0, 120)}`));
|
|
222
|
-
return pass;
|
|
223
|
-
}
|
|
224
|
-
/** barrier 扇出:全部并发(封顶排队),单个失败该项为 null,不拖垮整批。 */
|
|
225
|
-
async function parallel(thunks) {
|
|
226
|
-
if (!Array.isArray(thunks))
|
|
227
|
-
throw new Error(tr("parallel() 需要一个 thunk 数组", "parallel() needs an array of thunks"));
|
|
228
|
-
const results = [];
|
|
229
|
-
await runPool({
|
|
230
|
-
items: thunks,
|
|
231
|
-
limit: SUBAGENT_CONCURRENCY,
|
|
232
|
-
run: async (thunk, i) => {
|
|
233
|
-
results[i] = await Promise.resolve()
|
|
234
|
-
.then(thunk)
|
|
235
|
-
.catch((e) => {
|
|
236
|
-
logs.push(tr(`✗ 任务 ${i + 1} 失败:${e.message}`, `✗ Task ${i + 1} failed: ${e.message}`));
|
|
237
|
-
return null;
|
|
238
|
-
});
|
|
239
|
-
},
|
|
240
|
-
signal: internal.signal,
|
|
241
|
-
});
|
|
242
|
-
return results;
|
|
243
|
-
}
|
|
244
|
-
/** 无屏障流水线:每个 item 独立穿过所有 stage,item 失败只废自己。 */
|
|
245
|
-
async function pipeline(items, stages) {
|
|
246
|
-
if (!Array.isArray(items))
|
|
247
|
-
throw new Error(tr("pipeline() 第一个参数需要数组", "pipeline() needs an array as its first argument"));
|
|
248
|
-
if (!Array.isArray(stages) || stages.length === 0)
|
|
249
|
-
throw new Error(tr("pipeline() 第二个参数需要非空 stage 数组", "pipeline() needs a non-empty array of stages as its second argument"));
|
|
250
|
-
const results = [];
|
|
251
|
-
await runPool({
|
|
252
|
-
items: items,
|
|
253
|
-
limit: SUBAGENT_CONCURRENCY,
|
|
254
|
-
run: async (item, i) => {
|
|
255
|
-
let current = item;
|
|
256
|
-
for (let s = 0; s < stages.length; s++) {
|
|
257
|
-
try {
|
|
258
|
-
current = await stages[s](current);
|
|
259
|
-
}
|
|
260
|
-
catch (e) {
|
|
261
|
-
logs.push(tr(`✗ item ${i + 1} 在第 ${s + 1} 阶段失败:${e.message}`, `✗ Item ${i + 1} failed at stage ${s + 1}: ${e.message}`));
|
|
262
|
-
current = null;
|
|
263
|
-
break;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
results[i] = current;
|
|
267
|
-
},
|
|
268
|
-
signal: internal.signal,
|
|
269
|
-
});
|
|
270
|
-
return results;
|
|
271
|
-
}
|
|
272
|
-
// 沙箱:只有编排原语 + 受限 log/timer,标准 JS 内建(Promise/JSON/Math…)天然可用
|
|
273
|
-
const fmtLogArg = (a) => typeof a === "string" ? a : (() => { try {
|
|
274
|
-
return JSON.stringify(a);
|
|
275
|
-
}
|
|
276
|
-
catch {
|
|
277
|
-
return String(a);
|
|
278
|
-
} })();
|
|
279
|
-
const pushLog = (...args) => {
|
|
280
|
-
if (logs.length < 500)
|
|
281
|
-
logs.push(args.map(fmtLogArg).join(" "));
|
|
282
|
-
};
|
|
283
|
-
const sandbox = {
|
|
284
|
-
subagent: subagentImpl,
|
|
285
|
-
parallel,
|
|
286
|
-
pipeline,
|
|
287
|
-
judge: judgeImpl,
|
|
288
|
-
log: pushLog,
|
|
289
|
-
console: { log: pushLog, info: pushLog, warn: pushLog, error: pushLog },
|
|
290
|
-
setTimeout: (fn, ms) => setTimeout(fn, Math.max(0, Math.min(Number(ms) || 0, 120_000))),
|
|
291
|
-
clearTimeout,
|
|
292
|
-
};
|
|
293
|
-
let scriptError;
|
|
294
|
-
let completionValue;
|
|
295
|
-
try {
|
|
296
|
-
// 包成 async IIFE:支持顶层 await 和 return;line 数与校验时一致
|
|
297
|
-
const script = new vm.Script(`(async () => {\n${opts.code}\n})()`, { filename: "workflow.mjs" });
|
|
298
|
-
const ctx = vm.createContext(sandbox);
|
|
299
|
-
completionValue = await Promise.resolve().then(() => script.runInContext(ctx));
|
|
300
|
-
}
|
|
301
|
-
catch (e) {
|
|
302
|
-
scriptError = e.stack?.split("\n").slice(0, 3).join("\n") || e.message;
|
|
303
|
-
}
|
|
304
|
-
finally {
|
|
305
|
-
clearTimeout(overallTimer);
|
|
306
|
-
opts.signal?.removeEventListener("abort", onParentAbort);
|
|
307
|
-
}
|
|
308
|
-
const seconds = Math.round((Date.now() - started) / 1000);
|
|
309
|
-
const aborted = internal.signal.aborted;
|
|
310
|
-
const budgetExhausted = budgetTokens > 0 && spentTokens >= budgetTokens;
|
|
311
|
-
const details = { ...stats, seconds, aborted, spentTokens, budgetExhausted };
|
|
312
|
-
const header = tr(`工作流结束,用时 ${seconds}s · 子任务 ✓${stats.ok} ✗${stats.failed}${stats.cached ? ` · 续跑命中 ${stats.cached}` : ""} · 用 ${spentTokens.toLocaleString()} tokens${budgetTokens > 0 ? ` / 预算 ${budgetTokens.toLocaleString()}` : " (不限)"}${timedOut ? " · ⏱ 整体超时" : ""}${aborted && !timedOut ? " · 已中止" : ""}`, `Workflow finished in ${seconds}s · subtasks ✓${stats.ok} ✗${stats.failed}${stats.cached ? ` · resumed from cache ${stats.cached}` : ""} · used ${spentTokens.toLocaleString()} tokens${budgetTokens > 0 ? ` / budget ${budgetTokens.toLocaleString()}` : " (no limit)"}${timedOut ? " · ⏱ timed out" : ""}${aborted && !timedOut ? " · aborted" : ""}`);
|
|
313
|
-
const lines = [header, ""];
|
|
314
|
-
if (completionValue !== undefined) {
|
|
315
|
-
let shown;
|
|
316
|
-
try {
|
|
317
|
-
shown = JSON.stringify(completionValue, null, 2) ?? String(completionValue);
|
|
318
|
-
}
|
|
319
|
-
catch {
|
|
320
|
-
shown = String(completionValue);
|
|
321
|
-
}
|
|
322
|
-
lines.push(tr("**返回值**", "**Return value**"), "", shown.slice(0, 3000), "");
|
|
323
|
-
}
|
|
324
|
-
if (scriptError)
|
|
325
|
-
lines.push(tr("**脚本报错**", "**Script error**"), "", "```", scriptError, "```", "");
|
|
326
|
-
if (logs.length > 0) {
|
|
327
|
-
lines.push(tr("**运行日志**", "**Run log**"), "", ...logs.slice(0, MAX_LOG_LINES).map((l) => `- ${l.slice(0, 300)}`));
|
|
328
|
-
if (logs.length > MAX_LOG_LINES) {
|
|
329
|
-
lines.push(tr(`- …(另有 ${logs.length - MAX_LOG_LINES} 条省略)`, `- … (${logs.length - MAX_LOG_LINES} more omitted)`));
|
|
330
|
-
}
|
|
331
|
-
lines.push("");
|
|
332
|
-
}
|
|
333
|
-
lines.push(tr(`进度存档:${opts.progressPath}(失败重跑可加 resume=true 续跑)`, `Progress saved to: ${opts.progressPath} (to rerun after failures, add resume=true to continue)`));
|
|
334
|
-
return { report: lines.join("\n").trim(), details };
|
|
335
|
-
}
|
|
336
|
-
/** 工作区目录:项目根 .u1s1/workflows/(脚本 + 进度存档都在这里)。 */
|
|
337
|
-
export function workflowsDir() {
|
|
338
|
-
const dir = resolve(process.cwd(), ".u1s1/workflows");
|
|
339
|
-
mkdirSync(dir, { recursive: true });
|
|
340
|
-
return dir;
|
|
341
|
-
}
|
|
342
|
-
/** 内联脚本落盘,返回脚本路径(进度存档按同名约定派生)。 */
|
|
343
|
-
export function saveWorkflowScript(code) {
|
|
344
|
-
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/T(\d{6}).*/, "-$1");
|
|
345
|
-
const path = resolve(workflowsDir(), `wf-${stamp}.mjs`);
|
|
346
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
347
|
-
writeFileSync(path, code);
|
|
348
|
-
return path;
|
|
349
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
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;
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { tr } from "../i18n.js";
|
|
3
|
-
import { validateScript } from "./runner.js";
|
|
4
|
-
function str(v, fallback = "") {
|
|
5
|
-
return typeof v === "string" && v.trim() ? v.trim() : fallback;
|
|
6
|
-
}
|
|
7
|
-
function strArray(v) {
|
|
8
|
-
return Array.isArray(v)
|
|
9
|
-
? v.filter((x) => typeof x === "string" && !!x.trim()).map((x) => x.trim())
|
|
10
|
-
: [];
|
|
11
|
-
}
|
|
12
|
-
/** JSON.stringify 后内插进脚本;保证引号安全。 */
|
|
13
|
-
function lit(v) {
|
|
14
|
-
return JSON.stringify(v ?? null);
|
|
15
|
-
}
|
|
16
|
-
/** 脚本里给子 agent 的任务文案:按界面语言取中/英,再经 lit() 成为脚本里的字符串字面量。 */
|
|
17
|
-
function q(zh, en) {
|
|
18
|
-
return lit(tr(zh, en));
|
|
19
|
-
}
|
|
20
|
-
/** git 改动文件列表(模板构建期在主进程执行,不进沙箱)。
|
|
21
|
-
* 剔除已删除文件(评审/重构 agent 没法打开),并入未跟踪的新文件。 */
|
|
22
|
-
function gitDiffFiles() {
|
|
23
|
-
const run = (args) => {
|
|
24
|
-
const out = execFileSync("git", args, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
25
|
-
return out.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
26
|
-
};
|
|
27
|
-
try {
|
|
28
|
-
const changed = run(["diff", "--name-only", "--diff-filter=d", "HEAD"]);
|
|
29
|
-
const untracked = run(["ls-files", "--others", "--exclude-standard"]);
|
|
30
|
-
return [...new Set([...changed, ...untracked])];
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
return [];
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
export const TEMPLATES = {
|
|
37
|
-
i18n: {
|
|
38
|
-
name: "i18n",
|
|
39
|
-
get description() {
|
|
40
|
-
return tr("批量多语言翻译:每个语言一个子 agent 并行翻译,完成后统一校验 key 对齐并自动修复", "Batch translation: one sub-agent per language translates in parallel, then a final pass checks key alignment and fixes issues");
|
|
41
|
-
},
|
|
42
|
-
get inputHint() {
|
|
43
|
-
return tr('{ "langs": ["en", "ja", "de"], "source": "src/locales/zh.json", "dir": "src/locales" } — source/dir 可省略(默认 src/locales/zh.json 与同目录)', '{ "langs": ["en", "ja", "de"], "source": "src/locales/zh.json", "dir": "src/locales" } — source/dir are optional (default: src/locales/zh.json and its directory)');
|
|
44
|
-
},
|
|
45
|
-
build(input) {
|
|
46
|
-
const langs = strArray(input.langs);
|
|
47
|
-
if (langs.length === 0) {
|
|
48
|
-
throw new Error(tr('i18n 模板需要 langs 数组,例如 { "langs": ["en", "ja"] }', 'The i18n template needs a langs array, e.g. { "langs": ["en", "ja"] }'));
|
|
49
|
-
}
|
|
50
|
-
const source = str(input.source, "src/locales/zh.json");
|
|
51
|
-
const dir = str(input.dir, source.includes("/") ? source.slice(0, source.lastIndexOf("/")) : "src/locales");
|
|
52
|
-
// 用户输入一律经 lit() 进脚本,再在运行期拼接——裸内插遇到引号/反斜杠/换行会把生成的脚本弄成语法错误
|
|
53
|
-
return `
|
|
54
|
-
const langs = ${lit(langs)};
|
|
55
|
-
const source = ${lit(source)};
|
|
56
|
-
const dir = ${lit(dir)};
|
|
57
|
-
log(${q("i18n:翻译 ", "i18n: translating ")} + langs.length + ${q(" 个语言,源文件 ", " languages, source file ")} + source);
|
|
58
|
-
const results = await parallel(langs.map(lang => () =>
|
|
59
|
-
subagent({
|
|
60
|
-
task: ${q("读取 ", "Read ")} + source + ${q(",把所有文案翻译成对应语言(保持所有 key 不变、输出合法 JSON、译文自然地道),", " and translate all strings into the target language (keep every key unchanged, output valid JSON, make the translation natural and idiomatic), ")} +
|
|
61
|
-
${q("写入 ", "and write it to ")} + dir + "/" + lang + ${q(".json。写完后重新读一遍文件,确认是合法 JSON 且 key 与源文件完全一致。", ".json. After writing, re-read the file to confirm it is valid JSON and its keys exactly match the source file. ")} +
|
|
62
|
-
${q("目标语言代码:", "Target language code: ")} + lang
|
|
63
|
-
})
|
|
64
|
-
));
|
|
65
|
-
const okLangs = langs.filter((_, i) => results[i] !== null);
|
|
66
|
-
if (okLangs.length > 0) {
|
|
67
|
-
await subagent({
|
|
68
|
-
task: ${q("对照 ", "Compare against ")} + source + ${q(" 检查 ", " and check the translation files under ")} + dir + ${q("/ 目录下这些语言的翻译文件:", "/ for these languages: ")} + okLangs.join(",") + "." +
|
|
69
|
-
${q("逐个核对 key 是否与源文件完全一致、JSON 是否合法、译文是否有明显机翻痕迹,发现问题直接修复。最后汇报每个语言的状态", " For each one, verify the keys exactly match the source, the JSON is valid, and the translation doesn't read like obvious machine translation; fix any problems directly. Finally, report the status of each language")}
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
return { translated: okLangs, failed: langs.filter(l => !okLangs.includes(l)) };
|
|
73
|
-
`;
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
review: {
|
|
77
|
-
name: "review",
|
|
78
|
-
get description() {
|
|
79
|
-
return tr("全面代码审查:每个文件一个评审 agent 并行审查,发现项再交独立 agent 对抗性验证,只保留核实为真的问题", "Thorough code review: one reviewer agent per file in parallel, then an independent agent adversarially verifies the findings and keeps only confirmed issues");
|
|
80
|
-
},
|
|
81
|
-
get inputHint() {
|
|
82
|
-
return tr('{ "files": ["src/a.ts", "src/b.ts"] } — files 可省略,默认取 git diff --name-only HEAD 的改动文件', '{ "files": ["src/a.ts", "src/b.ts"] } — files is optional; defaults to the changed files from git diff --name-only HEAD');
|
|
83
|
-
},
|
|
84
|
-
build(input) {
|
|
85
|
-
let files = strArray(input.files);
|
|
86
|
-
if (files.length === 0)
|
|
87
|
-
files = gitDiffFiles();
|
|
88
|
-
if (files.length === 0) {
|
|
89
|
-
throw new Error(tr("review 模板需要 files 数组,或当前仓库有未提交改动", "The review template needs a files array, or uncommitted changes in the current repository"));
|
|
90
|
-
}
|
|
91
|
-
return `
|
|
92
|
-
const files = ${lit(files)};
|
|
93
|
-
log(${q("review:评审 ", "review: reviewing ")} + files.length + ${q(" 个文件,发现项将做对抗性验证", " files; findings will be adversarially verified")});
|
|
94
|
-
const reports = await parallel(files.map(f => () =>
|
|
95
|
-
subagent({
|
|
96
|
-
task: ${q("通读 ", "Read ")} + f + ${q(" 及其直接依赖,做代码审查:正确性 bug、安全漏洞、明显的性能问题。", " and its direct dependencies, and review the code for correctness bugs, security vulnerabilities, and obvious performance problems. ")} +
|
|
97
|
-
${q("只报告有把握的问题,每条给出文件、行号、严重程度(高/中/低)、问题描述和建议修法。没有问题就明确说无发现。", "Only report issues you are confident about; for each, give the file, line number, severity (high/medium/low), a description, and a suggested fix. If there are none, say explicitly that there are no findings.")}
|
|
98
|
-
})
|
|
99
|
-
));
|
|
100
|
-
const findings = reports.filter(Boolean).join("\\n\\n---\\n\\n");
|
|
101
|
-
if (!findings.trim()) return { verified: ${q("(所有文件均无发现)", "(no findings in any file)")} };
|
|
102
|
-
const verified = await subagent({
|
|
103
|
-
task: ${q("下面是一份代码审查发现列表。逐条对抗性验证:打开涉及的文件核对,剔除不成立、夸大或已过时的条目,", "Below is a list of code review findings. Adversarially verify each one: open the files involved to check, drop entries that are wrong, exaggerated, or outdated, ")} +
|
|
104
|
-
${q("只保留核实为真的问题,按严重程度排序输出完整清单。\n\n", "keep only issues confirmed to be real, and output the complete list sorted by severity.\n\n")} + findings
|
|
105
|
-
});
|
|
106
|
-
return { verified };
|
|
107
|
-
`;
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
research: {
|
|
111
|
-
name: "research",
|
|
112
|
-
get description() {
|
|
113
|
-
return tr("并行研究:每个方向一个子 agent 独立调查(可联网),最后汇总成一份综合报告", "Parallel research: one sub-agent investigates each question independently (with web access), then the results are merged into one report");
|
|
114
|
-
},
|
|
115
|
-
get inputHint() {
|
|
116
|
-
return tr('{ "questions": ["方向一", "方向二"], "context": "可选背景" }', '{ "questions": ["question 1", "question 2"], "context": "optional background" }');
|
|
117
|
-
},
|
|
118
|
-
build(input) {
|
|
119
|
-
const questions = strArray(input.questions);
|
|
120
|
-
if (questions.length === 0)
|
|
121
|
-
throw new Error(tr('research 模板需要 questions 数组,例如 { "questions": ["竞品A的定价", "竞品B的功能"] }', 'The research template needs a questions array, e.g. { "questions": ["Competitor A pricing", "Competitor B features"] }'));
|
|
122
|
-
const context = str(input.context);
|
|
123
|
-
const contextLine = context ? tr(`调查背景:${context}。`, `Background: ${context}. `) : "";
|
|
124
|
-
return `
|
|
125
|
-
const questions = ${lit(questions)};
|
|
126
|
-
const contextLine = ${lit(contextLine)};
|
|
127
|
-
log(${q("research:并行调查 ", "research: investigating ")} + questions.length + ${q(" 个方向", " questions in parallel")});
|
|
128
|
-
const reports = await parallel(questions.map(q => () =>
|
|
129
|
-
subagent({
|
|
130
|
-
task: ${q("调查以下问题,可以用 web_search/web_fetch 联网,也可以读本地代码和文档。", "Investigate the question below. You can use web_search/web_fetch to search the web, and you can also read local code and docs. ")} +
|
|
131
|
-
${q("输出:关键事实(带来源)、不同观点、你的结论。", "Output: key facts (with sources), differing views, and your conclusion. ")} + contextLine + ${q("问题:", "Question: ")} + q
|
|
132
|
-
})
|
|
133
|
-
));
|
|
134
|
-
const merged = reports
|
|
135
|
-
.map((r, i) => (r ? ${q("## 方向 ", "## Question ")} + (i + 1) + ${q(":", ": ")} + questions[i] + "\\n\\n" + r : null))
|
|
136
|
-
.filter(Boolean)
|
|
137
|
-
.join("\\n\\n");
|
|
138
|
-
if (!merged.trim()) return { synthesis: ${q("(所有方向都失败了)", "(all questions failed)")}, failed: questions.length };
|
|
139
|
-
const synthesis = await subagent({
|
|
140
|
-
task: ${q("把以下几份调查报告综合成一份结论性报告:先给核心结论(不超过 5 条),再给分方向要点,", "Synthesize the following research reports into one conclusive report: start with the core conclusions (at most 5), then key points per question, ")} +
|
|
141
|
-
${q("最后列分歧点和待确认项。不要编造报告里没有的信息。\n\n", "and finally list disagreements and open items. Do not invent information that isn't in the reports.\n\n")} + merged
|
|
142
|
-
});
|
|
143
|
-
return { synthesis };
|
|
144
|
-
`;
|
|
145
|
-
},
|
|
146
|
-
},
|
|
147
|
-
refactor: {
|
|
148
|
-
name: "refactor",
|
|
149
|
-
get description() {
|
|
150
|
-
return tr("批量重构/迁移:先由一个 agent 读代表文件制定统一规则,然后每个文件一个子 agent 在当前工作区套用规则", "Batch refactor/migration: one agent reads representative files and writes a single set of rules, then one sub-agent per file applies them in the current working tree");
|
|
151
|
-
},
|
|
152
|
-
get inputHint() {
|
|
153
|
-
return tr('{ "instruction": "把所有 var 改为 const", "files": ["src/a.ts", ...] } — files 省略时用 git diff 改动文件', '{ "instruction": "change every var to const", "files": ["src/a.ts", ...] } — if files is omitted, the git diff changed files are used');
|
|
154
|
-
},
|
|
155
|
-
build(input) {
|
|
156
|
-
const instruction = str(input.instruction);
|
|
157
|
-
if (!instruction) {
|
|
158
|
-
throw new Error(tr('refactor 模板需要 instruction,例如 { "instruction": "把 var 全部改为 const" }', 'The refactor template needs an instruction, e.g. { "instruction": "change every var to const" }'));
|
|
159
|
-
}
|
|
160
|
-
// 与 review 模板一致:构建期取 git 改动文件,不花一个 subagent 在沙箱里跑 git
|
|
161
|
-
let files = [...new Set(strArray(input.files))];
|
|
162
|
-
if (files.length === 0)
|
|
163
|
-
files = gitDiffFiles();
|
|
164
|
-
if (files.length === 0) {
|
|
165
|
-
throw new Error(tr("refactor 模板需要 files 数组,或当前仓库有未提交改动", "The refactor template needs a files array, or uncommitted changes in the current repository"));
|
|
166
|
-
}
|
|
167
|
-
return `
|
|
168
|
-
const instruction = ${lit(instruction)};
|
|
169
|
-
const targetFiles = ${lit(files)};
|
|
170
|
-
log(${q("refactor:共 ", "refactor: ")} + targetFiles.length + ${q(" 个文件,先制定统一规则", " files; drafting a common set of rules first")});
|
|
171
|
-
const rule = await subagent({
|
|
172
|
-
task: ${q("我们要对一批文件做统一重构:", "We are applying the same refactor to a batch of files: ")} + instruction + ${q("。", ". ")} +
|
|
173
|
-
${q("先读其中 1-2 个代表性文件了解现状,然后输出一份精确的重构规则清单(每条:什么模式改成什么、边界情况怎么处理)。", "First read 1-2 representative files to understand the current state, then output a precise list of refactoring rules (each: which pattern becomes what, and how to handle edge cases). ")} +
|
|
174
|
-
${q("规则要具体到可以直接照做,不要空话。涉及文件:", "Make the rules concrete enough to follow directly; no vague advice. Files involved: ")} + targetFiles.slice(0, 10).join(",")
|
|
175
|
-
});
|
|
176
|
-
const results = await parallel(targetFiles.map(f => () =>
|
|
177
|
-
subagent({
|
|
178
|
-
task: ${q("按以下重构规则处理文件 ", "Apply the refactoring rules below to the file ")} + f + ${q("。只修改这个目标文件,不要改动其他文件,避免与并行任务冲突。", ". Only modify this target file; do not change any other files, to avoid conflicts with parallel tasks. ")} +
|
|
179
|
-
${q("改完检查该文件语法正确(必要时运行 tsc 或构建验证)。\n\n重构规则:\n", "When done, check that the file's syntax is correct (run tsc or a build to verify if needed).\n\nRefactoring rules:\n")} + rule
|
|
180
|
-
})
|
|
181
|
-
));
|
|
182
|
-
const done = targetFiles.filter((_, i) => results[i] !== null);
|
|
183
|
-
return { applied: done, failed: targetFiles.filter(f => !done.includes(f)), rule };
|
|
184
|
-
`;
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
};
|
|
188
|
-
/** 按模板名生成脚本;生成后自检必须过静态校验(防模板本身写出不合规代码)。 */
|
|
189
|
-
export function buildFromTemplate(template, input) {
|
|
190
|
-
const t = TEMPLATES[template];
|
|
191
|
-
if (!t) {
|
|
192
|
-
throw new Error(tr(`未知模板 "${template}",可用:${Object.keys(TEMPLATES).join(", ")}`, `Unknown template "${template}"; available: ${Object.keys(TEMPLATES).join(", ")}`));
|
|
193
|
-
}
|
|
194
|
-
const code = t.build(input ?? {});
|
|
195
|
-
const errors = validateScript(code);
|
|
196
|
-
if (errors.length > 0) {
|
|
197
|
-
throw new Error(tr(`模板 ${template} 生成的脚本未通过静态校验(模板 bug,请反馈):${errors.join(";")}`, `The script generated by template ${template} failed static validation (template bug, please report it): ${errors.join("; ")}`));
|
|
198
|
-
}
|
|
199
|
-
return code;
|
|
200
|
-
}
|
package/dist/workflow/tool.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { Type } from "typebox";
|
|
2
|
-
import { type ParentModelRef } from "../subagent.js";
|
|
3
|
-
/**
|
|
4
|
-
* run_workflow 工具:主 agent 把模型生成的编排脚本交给 WorkflowRunner 执行。
|
|
5
|
-
* 脚本在 vm 沙箱里跑,只能用注入的 subagent/parallel/pipeline 原语。
|
|
6
|
-
*/
|
|
7
|
-
export declare function createRunWorkflowTool(getParentModel: () => ParentModelRef): import("@earendil-works/pi-coding-agent").ToolDefinition<Type.TObject<{
|
|
8
|
-
script: Type.TOptional<Type.TString>;
|
|
9
|
-
template: Type.TOptional<Type.TString>;
|
|
10
|
-
input: Type.TOptional<Type.TRecord<"^.*$", Type.TUnknown>>;
|
|
11
|
-
script_path: Type.TOptional<Type.TString>;
|
|
12
|
-
save_as: Type.TOptional<Type.TString>;
|
|
13
|
-
resume: Type.TOptional<Type.TBoolean>;
|
|
14
|
-
timeout_minutes: Type.TOptional<Type.TNumber>;
|
|
15
|
-
budget_tokens: Type.TOptional<Type.TNumber>;
|
|
16
|
-
}>, unknown, any> & import("@earendil-works/pi-coding-agent").ToolDefinition<any, any, any>;
|
package/dist/workflow/tool.js
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { dirname, resolve } from "node:path";
|
|
3
|
-
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
5
|
-
import { Type } from "typebox";
|
|
6
|
-
import { tr } from "../i18n.js";
|
|
7
|
-
import { contextProjectTrust } from "../subagent.js";
|
|
8
|
-
import { compactResultRender, truncate } from "../tools.js";
|
|
9
|
-
import { runWorkflow, saveWorkflowScript, validateScript, workflowsDir, WORKFLOW_DEFAULT_BUDGET_TOKENS, WORKFLOW_TIMEOUT_MS, } from "./runner.js";
|
|
10
|
-
import { buildFromTemplate, TEMPLATES } from "./templates.js";
|
|
11
|
-
function assertValidWorkflowCode(code) {
|
|
12
|
-
const errors = validateScript(code);
|
|
13
|
-
if (errors.length > 0) {
|
|
14
|
-
throw new Error(tr(`脚本未通过静态校验,请修复后重试:\n- ${errors.join("\n- ")}`, `The script failed static validation; fix it and try again:\n- ${errors.join("\n- ")}`));
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
function buildTemplateSource(template, input) {
|
|
18
|
-
try {
|
|
19
|
-
return buildFromTemplate(template, input);
|
|
20
|
-
}
|
|
21
|
-
catch (error) {
|
|
22
|
-
throw new Error(tr(`${error.message}\n模板 input 说明:${TEMPLATES[template]?.inputHint ?? "—"}`, `${error.message}\nTemplate input: ${TEMPLATES[template]?.inputHint ?? "—"}`), { cause: error });
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
function readWorkflowSource(scriptPath) {
|
|
26
|
-
try {
|
|
27
|
-
return readFileSync(scriptPath, "utf8");
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
throw new Error(tr(`读不到脚本文件:${scriptPath}`, `Can't read the script file: ${scriptPath}`));
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
function resolveWorkflowSource(params) {
|
|
34
|
-
const saveAs = params.save_as?.trim();
|
|
35
|
-
if (saveAs && !/^[a-zA-Z0-9_-]+$/.test(saveAs)) {
|
|
36
|
-
throw new Error(tr("save_as 只允许字母、数字、横线和下划线", "save_as may only contain letters, digits, hyphens, and underscores"));
|
|
37
|
-
}
|
|
38
|
-
if (saveAs && params.script_path?.trim()) {
|
|
39
|
-
throw new Error(tr("script_path 指向的脚本已经落盘,不能再配 save_as;要另存新名字请用 script 传入源码", "The script at script_path is already saved, so save_as can't be used with it; to save under a new name, pass the source via script"));
|
|
40
|
-
}
|
|
41
|
-
const inlineCode = params.script?.trim();
|
|
42
|
-
if (inlineCode) {
|
|
43
|
-
assertValidWorkflowCode(inlineCode);
|
|
44
|
-
return { code: inlineCode, saveAs };
|
|
45
|
-
}
|
|
46
|
-
const template = params.template?.trim();
|
|
47
|
-
if (template) {
|
|
48
|
-
return { code: buildTemplateSource(template, params.input ?? {}), saveAs };
|
|
49
|
-
}
|
|
50
|
-
const requestedPath = params.script_path?.trim();
|
|
51
|
-
if (requestedPath) {
|
|
52
|
-
const scriptPath = resolve(requestedPath);
|
|
53
|
-
return { code: readWorkflowSource(scriptPath), scriptPath };
|
|
54
|
-
}
|
|
55
|
-
throw new Error(tr("script、template、script_path 至少填一个", "Provide at least one of script, template, or script_path"));
|
|
56
|
-
}
|
|
57
|
-
function persistWorkflowSource(source) {
|
|
58
|
-
if (source.scriptPath)
|
|
59
|
-
return source.scriptPath;
|
|
60
|
-
if (!source.saveAs)
|
|
61
|
-
return saveWorkflowScript(source.code);
|
|
62
|
-
const scriptPath = resolve(workflowsDir(), `${source.saveAs}.mjs`);
|
|
63
|
-
if (existsSync(scriptPath)) {
|
|
64
|
-
throw new Error(tr(`工作流 "${source.saveAs}" 已存在(${scriptPath})。要重跑它请用 script_path 指向该文件(可加 resume:true 续跑);要新建请换个名字`, `Workflow "${source.saveAs}" already exists (${scriptPath}). To rerun it, point script_path at that file (add resume:true to continue); to create a new one, pick another name`));
|
|
65
|
-
}
|
|
66
|
-
mkdirSync(dirname(scriptPath), { recursive: true });
|
|
67
|
-
writeFileSync(scriptPath, source.code);
|
|
68
|
-
return scriptPath;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* run_workflow 工具:主 agent 把模型生成的编排脚本交给 WorkflowRunner 执行。
|
|
72
|
-
* 脚本在 vm 沙箱里跑,只能用注入的 subagent/parallel/pipeline 原语。
|
|
73
|
-
*/
|
|
74
|
-
export function createRunWorkflowTool(getParentModel) {
|
|
75
|
-
return defineTool({
|
|
76
|
-
name: "run_workflow",
|
|
77
|
-
label: tr("工作流", "Workflow"),
|
|
78
|
-
description: "Execute a JavaScript orchestration script that runs many sub-agents in structured patterns (fan-out/pipeline/loop). " +
|
|
79
|
-
"The script runs in a sandbox with ONLY these APIs: `subagent(task | {task, model})` returns the sub-agent's final output; " +
|
|
80
|
-
"`parallel([thunks])` fans out concurrently, a failed item becomes null (barrier); " +
|
|
81
|
-
"`pipeline(items, [stageFns])` streams each item through stages independently; plus `log(...)` and standard JS builtins. " +
|
|
82
|
-
"No require/import/process/fetch/fs access. Use for large parallelizable work (e.g. translate N locale files); " +
|
|
83
|
-
"don't use it for small tasks — write the script, pass it as `script`, then verify the report.",
|
|
84
|
-
promptSnippet: "Run a generated orchestration script that drives many sub-agents in parallel/pipeline patterns",
|
|
85
|
-
promptGuidelines: [
|
|
86
|
-
"Use run_workflow only when a task decomposes into many independent self-contained pieces; small jobs are cheaper done directly.",
|
|
87
|
-
"Every task string inside the script must be fully self-contained — sub-agents start with zero conversation context.",
|
|
88
|
-
"If the report shows failures, fix the script or call again with script_path + resume:true to continue from saved progress.",
|
|
89
|
-
],
|
|
90
|
-
parameters: Type.Object({
|
|
91
|
-
script: Type.Optional(Type.String({ description: "Full JavaScript source of the orchestration script (top-level await allowed)." })),
|
|
92
|
-
template: Type.Optional(Type.String({
|
|
93
|
-
description: `Use a built-in template instead of writing a script: ${Object.keys(TEMPLATES).join(" / ")}. ` +
|
|
94
|
-
"Provide `input` to parametrize it. Prefer this over hand-writing a script when a template fits.",
|
|
95
|
-
})),
|
|
96
|
-
input: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
|
|
97
|
-
description: 'Parameters for `template`, e.g. i18n: { "langs": ["en", "ja"] }; review/refactor: { "files": [...] } or omit for git-diff files; research: { "questions": [...] }.',
|
|
98
|
-
})),
|
|
99
|
-
script_path: Type.Optional(Type.String({
|
|
100
|
-
description: "Path to an existing workflow script under .u1s1/workflows/ — rerun it instead of passing `script`.",
|
|
101
|
-
})),
|
|
102
|
-
save_as: Type.Optional(Type.String({
|
|
103
|
-
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. Only valid with `script` or `template`, not `script_path`.',
|
|
104
|
-
pattern: "^[a-zA-Z0-9_-]+$",
|
|
105
|
-
})),
|
|
106
|
-
resume: Type.Optional(Type.Boolean({
|
|
107
|
-
description: "Skip sub-tasks already recorded as succeeded in the progress file (for crash/interrupt recovery).",
|
|
108
|
-
})),
|
|
109
|
-
timeout_minutes: Type.Optional(Type.Number({
|
|
110
|
-
description: `Wall-clock limit for the whole run in minutes (default ${WORKFLOW_TIMEOUT_MS / 60_000}, max 180).`,
|
|
111
|
-
minimum: 1,
|
|
112
|
-
maximum: 180,
|
|
113
|
-
})),
|
|
114
|
-
budget_tokens: Type.Optional(Type.Number({
|
|
115
|
-
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.`,
|
|
116
|
-
minimum: 0,
|
|
117
|
-
maximum: 1_000_000_000,
|
|
118
|
-
})),
|
|
119
|
-
}),
|
|
120
|
-
// 精简展示与 spawn_subagent 同款:收起一行摘要,ctrl+o 展开报告
|
|
121
|
-
renderShell: "self",
|
|
122
|
-
renderCall() {
|
|
123
|
-
return new Text("", 0, 0);
|
|
124
|
-
},
|
|
125
|
-
renderResult(...args) {
|
|
126
|
-
const [result, options, theme, context] = args;
|
|
127
|
-
const d = result.details;
|
|
128
|
-
const summary = d && typeof d.ok === "number"
|
|
129
|
-
? tr("🧭 工作流", "🧭 Workflow") +
|
|
130
|
-
` · ✓${d.ok}${d.failed ? ` ✗${d.failed}` : ""}` +
|
|
131
|
-
(typeof d.seconds === "number" ? ` · ${d.seconds}s` : "") +
|
|
132
|
-
(d.aborted ? tr(" · 已中止", " · aborted") : "")
|
|
133
|
-
: tr("🧭 工作流", "🧭 Workflow");
|
|
134
|
-
return compactResultRender({ result, options, theme, context, summaryLine: summary });
|
|
135
|
-
},
|
|
136
|
-
async execute(...args) {
|
|
137
|
-
const [, params, signal, onUpdate, ctx] = args;
|
|
138
|
-
const source = resolveWorkflowSource(params);
|
|
139
|
-
const scriptPath = persistWorkflowSource(source);
|
|
140
|
-
const progressPath = scriptPath.replace(/\.mjs$/, ".progress.jsonl");
|
|
141
|
-
// 流式进度:onProgress 高频触发,节流到 ≥2s 一次才推给 TUI
|
|
142
|
-
let lastPush = 0;
|
|
143
|
-
const onProgress = onUpdate
|
|
144
|
-
? (p) => {
|
|
145
|
-
const now = Date.now();
|
|
146
|
-
if (now - lastPush < 2_000)
|
|
147
|
-
return;
|
|
148
|
-
lastPush = now;
|
|
149
|
-
onUpdate({
|
|
150
|
-
content: [
|
|
151
|
-
{
|
|
152
|
-
type: "text",
|
|
153
|
-
text: tr(`🧭 子任务 ✓${p.ok} ✗${p.failed}${p.cached ? ` 缓存${p.cached}` : ""} · 在跑 ${p.running}`, `🧭 Subtasks ✓${p.ok} ✗${p.failed}${p.cached ? ` cached ${p.cached}` : ""} · running ${p.running}`) +
|
|
154
|
-
` · ${(p.spentTokens / 1000).toFixed(0)}k${p.budgetTokens > 0 ? ` / ${(p.budgetTokens / 1000).toFixed(0)}k tokens` : ""}`,
|
|
155
|
-
},
|
|
156
|
-
],
|
|
157
|
-
details: p,
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
: undefined;
|
|
161
|
-
const result = await runWorkflow({
|
|
162
|
-
code: source.code,
|
|
163
|
-
progressPath,
|
|
164
|
-
resume: params.resume === true,
|
|
165
|
-
parentModel: getParentModel(),
|
|
166
|
-
// 子 agent(含临时 worktree)沿用主会话的项目信任裁决
|
|
167
|
-
projectTrusted: contextProjectTrust(ctx),
|
|
168
|
-
signal,
|
|
169
|
-
timeoutMs: Math.min(180, params.timeout_minutes ?? WORKFLOW_TIMEOUT_MS / 60_000) * 60_000,
|
|
170
|
-
budgetTokens: params.budget_tokens,
|
|
171
|
-
onProgress,
|
|
172
|
-
});
|
|
173
|
-
return {
|
|
174
|
-
content: [{ type: "text", text: truncate(result.report) }],
|
|
175
|
-
details: { ...result.details, scriptPath, workflowsDir: workflowsDir() },
|
|
176
|
-
};
|
|
177
|
-
},
|
|
178
|
-
});
|
|
179
|
-
}
|