@mickorz/opencode-agentic-workflow 0.5.1 → 0.6.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.
@@ -1,64 +1,61 @@
1
1
  /**
2
- * 声明式 workflow 装载器(P4)—— 用户自定义流程的 JSON 形态
2
+ * 代码 workflow 装载器 —— 用户自定义流程的 JS 模块形态(唯一形态)
3
3
  *
4
- * 装载契约(详见 dev-docs/planning/P4-custom-workflows.md):
5
- * - 入口:.json 文件路径 或 目录(扫一层 *.json);相对项目目录
6
- * - 步骤七类(互斥键):agent / checkpoint / verify / fileExists / subflow /
7
- * pipeline(条目并发 fan-out,{{item}} 引用条目)/ race(≥2 提示竞速取首胜)
8
- * - 模板:{{topic}}、{{args.x}}、{{steps.<name>}};pipeline 提示额外支持
9
- * {{item}}(条目字面替换,先于通用模板解析);未知变量 = 步骤失败
10
- * - 并发步骤的 resume 粒度:pipeline/race 各占一个 journal 步骤单元
11
- * (completed 即整体跳过;中断重跑整步——与 sequence 前缀语义一致)
12
- * - 错误语义:装载期文件级 skip+warn(不阻断其他文件与内置流程);
13
- * 运行期步骤级 failed(走既有 journal/resume 语义)
4
+ * v0.6.0 产品决策:声明式 JSON 流程面下线(设计未定型,暂只保留代码流程)。
5
+ * 历史 JSON 机器(validateWorkflow / toDefinition / 模板解析)在 git 历史与
6
+ * dev-docs/design 中留档,重设计时再取。
7
+ *
8
+ * 装载契约:
9
+ * - 入口:.js / .mjs / .cjs 文件路径或目录(扫一层,忽略点开头文件);相对 baseDir
10
+ * - 模块导出:default / definition / workflow 三形态之一,最小形状校验
11
+ * (id/version 非空字符串 + run 函数)
12
+ * - <pkg>/core 裸说明符重写(v0.6.0):用户 flows 目录通常解析不到本包
13
+ * (包在 opencode 全局缓存,不在用户 node_modules 链上)——装载时把
14
+ * "<pkg>/core" 重写为插件自身 dist/core 的绝对 file URL 再导入,
15
+ * 保证拿到与宿主同一模块实例(executor / ambient 状态已接线)。
16
+ * 实现方式:同目录隐藏临时 .mjs(保留用户目录的相对/npm 解析语义)
17
+ * → import → 清理;目录不可写时回退 data: URL 导入。
18
+ * - .json 入口不再装载:显式文件或目录内 .json 都给出迁移提示(fail-loud)
19
+ * - 错误语义:装载期文件级 skip+warn(不阻断其他文件与内置流程)
14
20
  *
15
21
  * 纪律:本文件属于 Core 侧资产装载,禁止 import OpenCode API
16
22
  * (架构不变量);路径解析基准由调用方(plugin)传入。
17
23
  */
24
+ import { createHash } from "node:crypto";
18
25
  import { existsSync, statSync } from "node:fs";
19
- import { readdir, readFile } from "node:fs/promises";
26
+ import { readdir, readFile, rm, writeFile } from "node:fs/promises";
20
27
  import path from "node:path";
21
- import { pathToFileURL } from "node:url";
22
- import { agent } from "../workflow/agent.js";
23
- import { pipeline } from "../workflow/pipeline.js";
24
- import { race } from "../workflow/race.js";
25
- import { assert } from "../quality/check.js";
26
- import { assertVerify } from "../quality/verify.js";
27
- import { checkpoint } from "../quality/checkpoint.js";
28
- import { fileExists as fileExistsPredicate } from "../quality/predicates.js";
29
- const ID_PATTERN = /^[a-z][a-z0-9-]*$/;
30
- const STEP_KEYS = ["agent", "checkpoint", "verify", "fileExists", "subflow", "pipeline", "race"];
31
- /** 各步骤键开放的可选键(未知键拒绝;无则仅 name + 步骤键本身) */
32
- const OPTION_KEYS_BY_KIND = {
33
- agent: ["model", "timeoutMs", "retries"],
34
- subflow: ["args"],
35
- pipeline: ["items", "outputAs", "onFailure", "model", "timeoutMs", "retries"],
36
- race: ["outputAs"],
37
- };
38
- /** agent 调用级选项校验(agent 步与 pipeline 步共用同一规则) */
39
- function checkAgentOptions(step, index) {
40
- if (step.model !== undefined && (typeof step.model !== "string" || !step.model.includes("/"))) {
41
- return `steps[${index}].model must be "providerID/modelId"`;
42
- }
43
- if (step.timeoutMs !== undefined &&
44
- (typeof step.timeoutMs !== "number" || !Number.isFinite(step.timeoutMs) || step.timeoutMs <= 0)) {
45
- return `steps[${index}].timeoutMs must be a positive number (ms)`;
46
- }
47
- if (step.retries !== undefined &&
48
- (typeof step.retries !== "number" || !Number.isInteger(step.retries) || step.retries < 0)) {
49
- return `steps[${index}].retries must be a non-negative integer`;
50
- }
51
- return undefined;
52
- }
53
- /** 代码流程模块扩展名(P2-14:flows 目录可放 JS 模块,恢复 v1 自定义逻辑能力) */
28
+ import { fileURLToPath, pathToFileURL } from "node:url";
29
+ /** 代码流程模块扩展名 */
54
30
  const CODE_EXTENSIONS = [".js", ".mjs", ".cjs"];
55
- function isWorkflowFileName(name) {
56
- return name.endsWith(".json") || CODE_EXTENSIONS.some((ext) => name.endsWith(ext));
31
+ const JSON_REMOVED_HINT = 'JSON workflows were removed in v0.6.0 - convert to a .js/.mjs module (see README "代码流程")';
32
+ /** 用户代码里引用核心 API 的裸说明符(重写目标) */
33
+ const PKG_CORE_SPEC = "@mickorz/opencode-agentic-workflow/core";
34
+ /** 本插件自身 core barrel 的绝对 file URL(惰性求值;dist 与 tsx src 两种布局兼容) */
35
+ let cachedCoreUrl;
36
+ function coreBarrelUrl() {
37
+ if (cachedCoreUrl !== undefined)
38
+ return cachedCoreUrl;
39
+ const dir = path.dirname(fileURLToPath(import.meta.url));
40
+ for (const rel of ["../core/index.js", "../core/index.ts"]) {
41
+ const candidate = path.resolve(dir, rel);
42
+ if (existsSync(candidate)) {
43
+ cachedCoreUrl = pathToFileURL(candidate).href;
44
+ return cachedCoreUrl;
45
+ }
46
+ }
47
+ // 布局异常时按包结构兜底(装载报错会自行暴露路径问题)
48
+ cachedCoreUrl = pathToFileURL(path.resolve(dir, "../core/index.js")).href;
49
+ return cachedCoreUrl;
57
50
  }
58
- /** 解析入口路径列表为流程文件列表(目录 = 一层扫描;不存在的入口报错) */
51
+ /**
52
+ * 解析入口路径列表为流程文件列表(目录 = 一层扫描,忽略点开头文件;
53
+ * 不存在的入口报错;.json 给迁移提示不装载)。
54
+ */
59
55
  async function expandEntries(entries, baseDir) {
60
56
  const files = [];
61
57
  const errors = [];
58
+ const isCodeFile = (name) => !name.startsWith(".") && CODE_EXTENSIONS.some((ext) => name.endsWith(ext));
62
59
  for (const entry of entries) {
63
60
  const resolved = path.isAbsolute(entry) ? entry : path.join(baseDir, entry);
64
61
  if (!existsSync(resolved)) {
@@ -67,12 +64,21 @@ async function expandEntries(entries, baseDir) {
67
64
  }
68
65
  const stat = statSync(resolved);
69
66
  if (stat.isDirectory()) {
70
- const names = (await readdir(resolved)).filter(isWorkflowFileName).sort();
71
- if (names.length === 0) {
72
- errors.push(`${entry}: directory has no workflow files (.json/.js/.mjs/.cjs)`);
67
+ const names = (await readdir(resolved)).sort();
68
+ const codeNames = names.filter(isCodeFile);
69
+ for (const name of names) {
70
+ if (!name.startsWith(".") && name.endsWith(".json")) {
71
+ errors.push(`${path.join(entry, name)}: ${JSON_REMOVED_HINT}`);
72
+ }
73
+ }
74
+ if (codeNames.length === 0) {
75
+ errors.push(`${entry}: directory has no workflow files (.js/.mjs/.cjs)`);
73
76
  continue;
74
77
  }
75
- files.push(...names.map((n) => path.join(resolved, n)));
78
+ files.push(...codeNames.map((n) => path.join(resolved, n)));
79
+ }
80
+ else if (entry.endsWith(".json")) {
81
+ errors.push(`${entry}: ${JSON_REMOVED_HINT}`);
76
82
  }
77
83
  else {
78
84
  files.push(resolved);
@@ -80,337 +86,56 @@ async function expandEntries(entries, baseDir) {
80
86
  }
81
87
  return { files, errors };
82
88
  }
83
- /** 单文件校验:返回声明对象或错误文案(含文件名前缀) */
84
- export function validateWorkflow(raw, file) {
89
+ /**
90
+ * 装载代码流程模块:.js/.mjs/.cjs 动态 import,取
91
+ * default / definition / workflow 三种导出形态之一,校验
92
+ * WorkflowDefinition 最小形状(id/version 字符串 + run 函数)。
93
+ * 含 <pkg>/core 裸说明符重写(见文件头)。返回定义或错误文案(含文件名前缀);不抛出。
94
+ */
95
+ async function loadCodeWorkflowModule(file) {
85
96
  const at = (msg) => `${path.basename(file)}: ${msg}`;
86
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
87
- return { ok: false, error: at("workflow file must be a JSON object") };
88
- }
89
- const dw = raw;
90
- for (const key of Object.keys(dw)) {
91
- if (!["id", "version", "description", "args", "steps", "output"].includes(key)) {
92
- return { ok: false, error: at(`unknown top-level key "${key}" (allowed: id, version, description, args, steps, output)`) };
93
- }
94
- }
95
- if (typeof dw.id !== "string" || !ID_PATTERN.test(dw.id)) {
96
- return { ok: false, error: at(`"id" must match ${ID_PATTERN} (kebab-case, tool flow 参数直接用它)`) };
97
- }
98
- const version = typeof dw.version === "string" && dw.version.length > 0 ? dw.version : "1.0.0";
99
- if (dw.version !== undefined && version !== dw.version) {
100
- return { ok: false, error: at(`"version" must be a non-empty string`) };
101
- }
102
- if (dw.description !== undefined && typeof dw.description !== "string") {
103
- return { ok: false, error: at(`"description" must be a string`) };
104
- }
105
- if (dw.args !== undefined && (typeof dw.args !== "object" || dw.args === null)) {
106
- return { ok: false, error: at(`"args" must be a JSON Schema object`) };
107
- }
108
- if (!Array.isArray(dw.steps) || dw.steps.length === 0) {
109
- return { ok: false, error: at(`"steps" must be a non-empty array`) };
97
+ let source;
98
+ try {
99
+ source = await readFile(file, "utf8");
110
100
  }
111
- const names = new Set();
112
- for (const [index, stepRaw] of dw.steps.entries()) {
113
- if (typeof stepRaw !== "object" || stepRaw === null) {
114
- return { ok: false, error: at(`steps[${index}] must be an object`) };
115
- }
116
- const step = stepRaw;
117
- if (typeof step.name !== "string" || !ID_PATTERN.test(step.name)) {
118
- return { ok: false, error: at(`steps[${index}].name must match ${ID_PATTERN}`) };
119
- }
120
- if (names.has(step.name)) {
121
- return { ok: false, error: at(`duplicate step name "${step.name}"`) };
101
+ catch (error) {
102
+ return { ok: false, error: at(`failed to read (${error instanceof Error ? error.message : String(error)})`) };
103
+ }
104
+ const coreUrl = coreBarrelUrl();
105
+ // 三种引用形态:from "<pkg>/core" / 副作用 import "<pkg>/core" / 动态 import("<pkg>/core")
106
+ const rewritten = source
107
+ .replace(/(\bfrom\s*)(["'])@mickorz\/opencode-agentic-workflow\/core\2/g, `$1"${coreUrl}"`)
108
+ .replace(/(\bimport\s*)(["'])@mickorz\/opencode-agentic-workflow\/core\2/g, `$1"${coreUrl}"`)
109
+ .replace(/(\bimport\s*\(\s*)(["'])@mickorz\/opencode-agentic-workflow\/core\2/g, `$1"${coreUrl}"`);
110
+ let mod;
111
+ try {
112
+ if (!source.includes(PKG_CORE_SPEC)) {
113
+ // 未引用核心 API:原样导入(用户目录自身的相对/npm 解析语义保持不变)
114
+ mod = await import(pathToFileURL(file).href);
122
115
  }
123
- names.add(step.name);
124
- const present = STEP_KEYS.filter((k) => step[k] !== undefined);
125
- if (present.length !== 1) {
116
+ else if (file.endsWith(".cjs")) {
126
117
  return {
127
118
  ok: false,
128
- error: at(`steps[${index}] ("${step.name}") must have exactly one of ${STEP_KEYS.join("/")}, got [${present.join("/") || "none"}]`),
119
+ error: at(`imports "${PKG_CORE_SPEC}" but .cjs cannot be specifier-rewritten - rename to .mjs (ESM)`),
129
120
  };
130
121
  }
131
- const kind = present[0];
132
- // 各步骤键开放的可选键(P1-4 agent 调用级选项;P2-9 subflow args;
133
- // pipeline items/outputAs/onFailure + 调用级选项;race outputAs)
134
- const optionKeys = OPTION_KEYS_BY_KIND[kind] ?? [];
135
- if (kind === "agent" || kind === "pipeline") {
136
- const optionError = checkAgentOptions(step, index);
137
- if (optionError) {
138
- return { ok: false, error: at(optionError) };
139
- }
140
- }
141
- if (kind === "verify") {
142
- const v = step.verify;
143
- if (typeof v !== "object" || v === null || typeof v.artifact !== "string") {
144
- return { ok: false, error: at(`steps[${index}].verify must be { artifact: string, criteria?, reviewers?, label?, threshold?, lenses? }`) };
145
- }
146
- const vo = v;
147
- for (const k of Object.keys(vo)) {
148
- if (!["artifact", "criteria", "reviewers", "label", "threshold", "lenses"].includes(k)) {
149
- return { ok: false, error: at(`steps[${index}].verify has unknown key "${k}"`) };
150
- }
151
- }
152
- if (vo.reviewers !== undefined && typeof vo.reviewers !== "number") {
153
- return { ok: false, error: at(`steps[${index}].verify.reviewers must be a number`) };
154
- }
155
- // P2-11 投票阈值:数值且在 (0, 1]
156
- if (vo.threshold !== undefined &&
157
- (typeof vo.threshold !== "number" || !Number.isFinite(vo.threshold) || vo.threshold <= 0 || vo.threshold > 1)) {
158
- return { ok: false, error: at(`steps[${index}].verify.threshold must be a number in (0, 1]`) };
159
- }
160
- // P2-11 多视角:非空数组,每项 { name, criteria } 均为非空字符串
161
- if (vo.lenses !== undefined) {
162
- if (!Array.isArray(vo.lenses) || vo.lenses.length === 0) {
163
- return { ok: false, error: at(`steps[${index}].verify.lenses must be a non-empty array of { name, criteria }`) };
164
- }
165
- for (const [li, lens] of vo.lenses.entries()) {
166
- const lo = lens;
167
- if (typeof lo !== "object" || lo === null || typeof lo.name !== "string" || lo.name.length === 0 || typeof lo.criteria !== "string" || lo.criteria.length === 0) {
168
- return { ok: false, error: at(`steps[${index}].verify.lenses[${li}] must be { name: non-empty string, criteria: non-empty string }`) };
169
- }
170
- for (const k of Object.keys(lo)) {
171
- if (!["name", "criteria"].includes(k)) {
172
- return { ok: false, error: at(`steps[${index}].verify.lenses[${li}] has unknown key "${k}"`) };
173
- }
174
- }
175
- }
176
- }
177
- }
178
- else if (kind === "subflow") {
179
- // P2-9:args = 原始值(number/boolean)或字符串模板的对象
180
- if (step.args !== undefined) {
181
- if (typeof step.args !== "object" || step.args === null || Array.isArray(step.args)) {
182
- return { ok: false, error: at(`steps[${index}].args must be an object of primitives or templates`) };
122
+ else {
123
+ // 同目录隐藏临时 .mjs:保留该文件其余相对/npm 导入的解析语义
124
+ const temp = path.join(path.dirname(file), `.${path.basename(file)}.${createHash("sha256").update(file).digest("hex").slice(0, 8)}.aw.mjs`);
125
+ try {
126
+ await writeFile(temp, rewritten, "utf8");
127
+ try {
128
+ mod = await import(pathToFileURL(temp).href);
183
129
  }
184
- for (const [argKey, argValue] of Object.entries(step.args)) {
185
- if (typeof argValue !== "string" && typeof argValue !== "number" && typeof argValue !== "boolean") {
186
- return { ok: false, error: at(`steps[${index}].args.${argKey} must be a string (template), number, or boolean`) };
187
- }
130
+ finally {
131
+ await rm(temp, { force: true }).catch(() => undefined);
188
132
  }
189
133
  }
190
- }
191
- else if (kind === "pipeline") {
192
- // 声明式 pipeline:payload 非空提示模板 + items 非空模板数组 + 可选键类型
193
- if (typeof step.pipeline !== "string" || step.pipeline.length === 0) {
194
- return { ok: false, error: at(`steps[${index}].pipeline must be a non-empty string (prompt template, {{item}} references the entry)`) };
195
- }
196
- if (!Array.isArray(step.items) ||
197
- step.items.length === 0 ||
198
- !step.items.every((it) => typeof it === "string" && it.length > 0)) {
199
- return { ok: false, error: at(`steps[${index}].items must be a non-empty array of non-empty string templates`) };
200
- }
201
- if (step.outputAs !== undefined && (typeof step.outputAs !== "string" || !ID_PATTERN.test(step.outputAs))) {
202
- return { ok: false, error: at(`steps[${index}].outputAs must match ${ID_PATTERN}`) };
203
- }
204
- if (step.onFailure !== undefined && step.onFailure !== "fail-fast" && step.onFailure !== "continue") {
205
- return { ok: false, error: at(`steps[${index}].onFailure must be "fail-fast" | "continue"`) };
206
- }
207
- }
208
- else if (kind === "race") {
209
- // 声明式 race:≥2 个非空提示模板(单分支无竞速意义,fail-loud)+ outputAs
210
- if (!Array.isArray(step.race) ||
211
- step.race.length < 2 ||
212
- !step.race.every((p) => typeof p === "string" && p.length > 0)) {
213
- return { ok: false, error: at(`steps[${index}].race must be an array of at least 2 non-empty prompt templates`) };
214
- }
215
- if (step.outputAs !== undefined && (typeof step.outputAs !== "string" || !ID_PATTERN.test(step.outputAs))) {
216
- return { ok: false, error: at(`steps[${index}].outputAs must match ${ID_PATTERN}`) };
134
+ catch {
135
+ // 目录不可写等:回退 data: URL(此形态下文件内其余相对导入不可用,核心 API 可用)
136
+ mod = await import("data:text/javascript;base64," + Buffer.from(rewritten, "utf8").toString("base64"));
217
137
  }
218
138
  }
219
- else if (typeof step[kind] !== "string") {
220
- return { ok: false, error: at(`steps[${index}].${kind} must be a string (template)`) };
221
- }
222
- const unknownStepKeys = Object.keys(step).filter((k) => k !== "name" &&
223
- !STEP_KEYS.includes(k) &&
224
- !optionKeys.includes(k));
225
- if (unknownStepKeys.length > 0) {
226
- return {
227
- ok: false,
228
- error: at(`steps[${index}] has unknown key(s) [${unknownStepKeys.join(", ")}] ` +
229
- `(allowed: name + exactly one step key (${STEP_KEYS.join("/")})` +
230
- `${optionKeys.length > 0 ? ` + optional ${optionKeys.join("/")}` : ""})`),
231
- };
232
- }
233
- }
234
- if (dw.output !== undefined && typeof dw.output !== "string") {
235
- return { ok: false, error: at(`"output" must be a string (template)`) };
236
- }
237
- return {
238
- ok: true,
239
- value: {
240
- id: dw.id,
241
- version,
242
- description: dw.description,
243
- args: dw.args,
244
- steps: dw.steps,
245
- output: dw.output,
246
- },
247
- };
248
- }
249
- /**
250
- * 模板解析:{{topic}} / {{args.x}} / {{steps.<name>}}(允许空白)。
251
- * 未知变量抛错(步骤级失败、journal 可见,绝不静默空串)。
252
- */
253
- export function resolveTemplate(template, args, stepOutputs) {
254
- return template.replace(/\{\{\s*([\w.-]+)\s*\}\}/g, (_match, ref) => {
255
- if (ref === "topic") {
256
- const topic = args.topic;
257
- if (topic === undefined)
258
- throw new Error(`[agentic-workflow] template variable {{topic}} is not provided (flow args lack topic)`);
259
- return String(topic);
260
- }
261
- if (ref.startsWith("args.")) {
262
- const key = ref.slice("args.".length);
263
- const value = args[key];
264
- if (value === undefined)
265
- throw new Error(`[agentic-workflow] template variable {{args.${key}}} is not provided`);
266
- return String(value);
267
- }
268
- if (ref.startsWith("steps.")) {
269
- const name = ref.slice("steps.".length);
270
- const value = stepOutputs.get(name);
271
- if (value === undefined) {
272
- throw new Error(`[agentic-workflow] template variable {{steps.${name}}} not found ` +
273
- `(available step outputs: [${[...stepOutputs.keys()].join(", ") || "none"}])`);
274
- }
275
- return value;
276
- }
277
- throw new Error(`[agentic-workflow] unknown template variable {{${ref}}} (supported: {{topic}}, {{args.x}}, {{steps.<name>}})`);
278
- });
279
- }
280
- /** P0-2:声明对象 -> 可注册 definition(workflow_define 内联定义复用同一转换) */
281
- export function toDefinition(dw) {
282
- // 默认输出来源:最后一个「产出型」步骤(agent 原文 / race 胜者原文;
283
- // pipeline 为结果数组的 JSON 串——需要可读输出就显式写 output 模板)
284
- const lastProducerName = [...dw.steps]
285
- .reverse()
286
- .find((s) => "agent" in s || "pipeline" in s || "race" in s)?.name;
287
- return {
288
- id: dw.id,
289
- version: dw.version ?? "1.0.0",
290
- description: dw.description ?? `custom workflow (${dw.steps.length} steps)`,
291
- argsSchema: dw.args ?? {
292
- type: "object",
293
- properties: { topic: { type: "string", description: "主题(工具恒传参数)" } },
294
- required: ["topic"],
295
- },
296
- stepNames: dw.steps.map((s) => s.name),
297
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
298
- async run(args, ctx) {
299
- const flowArgs = (args ?? {});
300
- const stepOutputs = new Map();
301
- const resolve = (template) => resolveTemplate(template, flowArgs, stepOutputs);
302
- const root = ctx.workspaceRoot ?? process.cwd();
303
- const built = dw.steps.map((step) => {
304
- if ("agent" in step) {
305
- return async (prev) => {
306
- // P1-4:声明级调用选项透传(model/timeoutMs/retries)
307
- const result = await agent(resolve(step.agent), {
308
- ...(step.model !== undefined ? { model: step.model } : {}),
309
- ...(step.timeoutMs !== undefined ? { timeoutMs: step.timeoutMs } : {}),
310
- ...(step.retries !== undefined ? { retries: step.retries } : {}),
311
- });
312
- stepOutputs.set(step.name, result.output);
313
- return { ...prev, [step.name]: result.output };
314
- };
315
- }
316
- if ("checkpoint" in step) {
317
- return async (prev) => {
318
- await checkpoint(resolve(step.checkpoint), { label: step.name });
319
- return { ...prev, [step.name]: "approved" };
320
- };
321
- }
322
- if ("verify" in step) {
323
- return async (prev) => {
324
- const result = await assertVerify(resolve(step.verify.artifact), {
325
- criteria: step.verify.criteria,
326
- reviewers: step.verify.reviewers,
327
- label: step.verify.label ?? step.name,
328
- // P2-11:投票阈值 + 多视角透传
329
- ...(step.verify.threshold !== undefined
330
- ? { passThreshold: step.verify.threshold }
331
- : {}),
332
- ...(step.verify.lenses !== undefined ? { lenses: step.verify.lenses } : {}),
333
- });
334
- return { ...prev, [step.name]: result.passed ? "passed" : "failed" };
335
- };
336
- }
337
- if ("subflow" in step) {
338
- return async (prev) => {
339
- if (typeof ctx.subflow !== "function") {
340
- throw new Error(`[agentic-workflow] subflow step "${step.name}" requires the journalDir ` +
341
- "plugin option (subflow runs are journaled with lineage)");
342
- }
343
- // args 值:字符串走模板解析,原始值直传
344
- const subArgs = {};
345
- for (const [key, value] of Object.entries(step.args ?? {})) {
346
- subArgs[key] = typeof value === "string" ? resolve(value) : value;
347
- }
348
- const result = await ctx.subflow(resolve(step.subflow), subArgs);
349
- stepOutputs.set(step.name, result.output);
350
- return { ...prev, [step.name]: result.output };
351
- };
352
- }
353
- if ("pipeline" in step) {
354
- return async (prev) => {
355
- // 条目 = 模板数组逐项解析({{topic}}/{{args.x}}/{{steps.x}})
356
- const items = step.items.map((tpl) => resolve(tpl));
357
- // 每条目一次 agent 调用;{{item}} 先字面替换(replace 用函数形参,
358
- // 条目含 $ 等特殊字符不受 replacement pattern 影响)再走通用模板
359
- const results = await pipeline(items, [
360
- async (item) => {
361
- const prompt = resolveTemplate(step.pipeline.replace(/\{\{\s*item\s*\}\}/g, () => item), flowArgs, stepOutputs);
362
- const result = await agent(prompt, {
363
- ...(step.model !== undefined ? { model: step.model } : {}),
364
- ...(step.timeoutMs !== undefined ? { timeoutMs: step.timeoutMs } : {}),
365
- ...(step.retries !== undefined ? { retries: step.retries } : {}),
366
- });
367
- return result.output;
368
- },
369
- ], { ...(step.onFailure !== undefined ? { onFailure: step.onFailure } : {}) });
370
- stepOutputs.set(step.name, JSON.stringify(results));
371
- return { ...prev, [step.outputAs ?? step.name]: results };
372
- };
373
- }
374
- if ("race" in step) {
375
- return async (prev) => {
376
- // 并发起跑,首个成功者的输出胜出(全败抛 WorkflowRaceError)
377
- const winner = await race(step.race.map((tpl) => async () => {
378
- const result = await agent(resolve(tpl));
379
- return result.output;
380
- }));
381
- stepOutputs.set(step.name, winner);
382
- return { ...prev, [step.outputAs ?? step.name]: winner };
383
- };
384
- }
385
- return async (prev) => {
386
- const target = path.resolve(root, resolve(step.fileExists));
387
- await assert(() => fileExistsPredicate(target), `${step.name}: ${target} exists in workspace (${root})`);
388
- return { ...prev, [step.name]: target };
389
- };
390
- });
391
- await ctx.runSteps(built, { stepNames: dw.steps.map((s) => s.name) });
392
- const output = dw.output !== undefined
393
- ? resolve(dw.output)
394
- : stepOutputs.get(lastProducerName ?? "") ?? `workflow ${dw.id} completed`;
395
- return { output };
396
- },
397
- };
398
- }
399
- /**
400
- * 装载入口:读取 + 校验 + 转换。文件级错误收集返回(调用方 warn+跳过),
401
- * 绝不因单个坏文件中断装载。reservedIds 命中即拒(避免遮蔽内置流程)。
402
- */
403
- /**
404
- * 装载代码流程模块(P2-14):.js/.mjs/.cjs 动态 import,取
405
- * default / definition / workflow 三种导出形态之一,校验
406
- * WorkflowDefinition 最小形状(id/version 字符串 + run 函数)。
407
- * 返回定义或错误文案(含文件名前缀);不抛出。
408
- */
409
- async function loadCodeWorkflowModule(file) {
410
- const at = (msg) => `${path.basename(file)}: ${msg}`;
411
- let mod;
412
- try {
413
- mod = await import(pathToFileURL(file).href);
414
139
  }
415
140
  catch (error) {
416
141
  return { ok: false, error: at(`failed to import (${error instanceof Error ? error.message : String(error)})`) };
@@ -438,7 +163,7 @@ async function loadCodeWorkflowModule(file) {
438
163
  return { ok: true, definition: candidate };
439
164
  }
440
165
  /**
441
- * 装载自定义流程(声明式 JSON + 代码式 JS 模块,P2-14 起统一入口)。
166
+ * 装载自定义流程(代码式 JS 模块;v0.6.0 起唯一形态)。
442
167
  * 文件级错误 warn+跳过(与装载纪律一致:观测/装载不能成为主链路故障源)。
443
168
  */
444
169
  export async function loadCustomWorkflows(entries, baseDir, reservedIds = []) {
@@ -447,54 +172,24 @@ export async function loadCustomWorkflows(entries, baseDir, reservedIds = []) {
447
172
  const reserved = new Set(reservedIds);
448
173
  const seenIds = new Map();
449
174
  for (const file of files) {
450
- if (CODE_EXTENSIONS.some((ext) => file.endsWith(ext))) {
451
- const loaded = await loadCodeWorkflowModule(file);
452
- if (!loaded.ok) {
453
- errors.push(loaded.error);
454
- continue;
455
- }
456
- if (reserved.has(loaded.definition.id)) {
457
- errors.push(`${path.basename(file)}: id "${loaded.definition.id}" is reserved by a built-in workflow`);
458
- continue;
459
- }
460
- const key = `${loaded.definition.id}@${loaded.definition.version}`;
461
- const seenIn = seenIds.get(key);
462
- if (seenIn) {
463
- errors.push(`${path.basename(file)}: duplicate ${key} (already loaded from ${seenIn})`);
464
- continue;
465
- }
466
- seenIds.set(key, path.basename(file));
467
- definitions.push(loaded.definition);
468
- continue;
469
- }
470
- let raw;
471
- try {
472
- raw = JSON.parse(await readFile(file, "utf8"));
473
- }
474
- catch (error) {
475
- errors.push(`${path.basename(file)}: invalid JSON (${error instanceof Error ? error.message : String(error)})`);
476
- continue;
477
- }
478
- const validated = validateWorkflow(raw, file);
479
- if (!validated.ok) {
480
- errors.push(validated.error);
175
+ const loaded = await loadCodeWorkflowModule(file);
176
+ if (!loaded.ok) {
177
+ errors.push(loaded.error);
481
178
  continue;
482
179
  }
483
- const dw = validated.value;
484
- if (reserved.has(dw.id)) {
485
- errors.push(`${path.basename(file)}: id "${dw.id}" is reserved by a built-in workflow`);
180
+ if (reserved.has(loaded.definition.id)) {
181
+ errors.push(`${path.basename(file)}: id "${loaded.definition.id}" is reserved by a built-in workflow`);
486
182
  continue;
487
183
  }
488
- const seenIn = seenIds.get(`${dw.id}@${dw.version}`);
184
+ const key = `${loaded.definition.id}@${loaded.definition.version}`;
185
+ const seenIn = seenIds.get(key);
489
186
  if (seenIn) {
490
- errors.push(`${path.basename(file)}: duplicate ${dw.id}@${dw.version} (already loaded from ${seenIn})`);
187
+ errors.push(`${path.basename(file)}: duplicate ${key} (already loaded from ${seenIn})`);
491
188
  continue;
492
189
  }
493
- seenIds.set(`${dw.id}@${dw.version}`, path.basename(file));
494
- definitions.push(toDefinition(dw));
190
+ seenIds.set(key, path.basename(file));
191
+ definitions.push(loaded.definition);
495
192
  }
496
193
  return { definitions, errors };
497
194
  }
498
- /** @deprecated 旧名(P2-14 起装载范围扩展到代码流程),等价 loadCustomWorkflows */
499
- export const loadDeclarativeWorkflows = loadCustomWorkflows;
500
195
  //# sourceMappingURL=loader.js.map