deerworkflow
§ · Nested detail page

workflow loader 与 phase 生命周期 深度解析

从 deer-workflow 源码逐段确认该模块的契约、执行路径、事件语义和失败边界。

§ 00 · IMPORT CHAIN

导入链与契约

workflow loader 与 phase 生命周期 的第一层证据来自 src/flow/workflow.ts:90、src/flow/workflow.ts:94、src/flow/workflow.ts:97、src/flow/workflow.ts:101 与 src/flow/context.ts:30:这些行把入口、上下文、错误或配置集中在一个小范围内,说明 deer-workflow 倾向把复杂度留在明确契约里,而不是散到调用者脚本。这里的设计价值是让 Agent 自动化可以被测试、被日志系统消费,也能被 CLI 和库调用共享。

src/src/flow/workflow.ts · L90-147TypeScript
90export async function workflow<TOutput = unknown, TArgs = unknown>(
91  target: WorkflowTarget,
92  args?: TArgs,
93): Promise<TOutput> {
94  const parentContext = getWorkflowContext();
95  const depth = parentContext ? parentContext.depth + 1 : 0;
96
97  if (depth > MAX_NESTED_WORKFLOW_DEPTH) {
98    throw new WorkflowNestingError(depth);
99  }
100
101  const scriptPath = resolveWorkflowPath(target, parentContext);
102  const context: WorkflowExecutionContext<TArgs | undefined> = {
103    id: crypto.randomUUID(),
104    parentId: parentContext?.id,
105    depth,
106    scriptPath,
107    args,
108  };
109
110  return runInWorkflowContext(context, async () => {
111    const startedAt = performance.now();
112    emitWorkflowEvent({
113      type: "workflow:start",
114      ...toWorkflowEventContext(context),
115    });
116
117    try {
118      const definition = await loadWorkflowDefinition<
119        TArgs | undefined,
120        TOutput
121      >(scriptPath);
122      if (definition.meta !== undefined) {
123        emitWorkflowEvent({
124          type: "workflow:meta",
125          ...toWorkflowEventContext(context),
126          meta: definition.meta,
127        });
128      }
129      const result = await definition.handler(args, context);
130      endCurrentPhase(context);
131      emitWorkflowEvent({
132        type: "workflow:end",
133        ...toWorkflowEventContext(context),
134        durationMs: elapsedMilliseconds(startedAt),
135      });
136      return result;
137    } catch (error) {
138      endCurrentPhase(context);
139      emitWorkflowEvent({
140        type: "workflow:error",
141        ...toWorkflowEventContext(context),
142        durationMs: elapsedMilliseconds(startedAt),
143        error: serializeWorkflowError(error),
144      });
145      throw error;
146    }
147  });
§ 01 · CORE IMPL

核心实现,逐段拆

▸入口与状态

第二层证据是 src/flow/workflow.ts:118、src/flow/workflow.ts:129、src/flow/workflow.ts:137、src/flow/phase.ts:34 与 src/flow/phase.ts:49。这些引用展示了失败路径、事件路径或参数路径如何闭环:不是只在 happy path 发一个结果,而是把输入解析、阶段切换、stdout/stderr、schema 或 duration 都纳入可解释的运行轨迹。

src/src/flow/phase.ts · L34-91TypeScript
34export function phase(title: string): void {
35  const normalizedTitle = title.trim();
36  if (!normalizedTitle) {
37    throw new TypeError("phase() requires a non-empty title.");
38  }
39
40  const context = getWorkflowContext();
41  if (!context) {
42    throw new PhaseContextError();
43  }
44
45  if (context.phase === normalizedTitle) {
46    return;
47  }
48
49  endCurrentPhase(context);
50  context.phase = normalizedTitle;
51  phaseStartTimes.set(context, performance.now());
52  emitWorkflowEvent({
53    type: "workflow:phase:start",
54    ...toWorkflowEventContext(context),
55    phase: normalizedTitle,
56  });
57}
58
59/**
60 * Reads the active Workflow's current phase.
61 *
62 * @returns The phase title, or `undefined` outside a Workflow or before the
63 * first call to {@link phase}.
64 */
65export function getCurrentPhase(): string | undefined {
66  return getWorkflowContext()?.phase;
67}
68
69/**
70 * Ends the active phase and emits its lifecycle event.
71 *
72 * @internal
73 */
74export function endCurrentPhase(
75  context: WorkflowExecutionContext = requireWorkflowContext(),
76): void {
77  const currentPhase = context.phase;
78  if (!currentPhase) {
79    return;
80  }
81
82  const startedAt = phaseStartTimes.get(context);
83  context.phase = undefined;
84  phaseStartTimes.delete(context);
85
86  emitWorkflowEvent({
87    type: "workflow:phase:end",
88    ...toWorkflowEventContext(context),
89    phase: currentPhase,
90    durationMs: elapsedMilliseconds(startedAt),
91  });

▸失败与边界

第三层证据来自 src/flow/workflow.ts:232、src/flow/workflow.ts:246、src/flow/workflow.ts:271、tests/flow/workflow.test.ts:175 与 tests/flow/workflow.test.ts:187。这些行说明这个模块的边界主要靠类型和测试守住:当 workflow 嵌套、metadata、JSON input、CLI 命令或 Agent 子进程出现异常时,源码会抛出具名错误或保留原始诊断,而不是吞掉上下文。

src/src/flow/workflow.ts · L220-350TypeScript
220  value: unknown,
221  scriptPath: string,
222): WorkflowMeta | undefined {
223  if (value === undefined) {
224    return undefined;
225  }
226
227  if (!isModuleRecord(value)) {
228    throw invalidWorkflowMeta(scriptPath, "meta must be an object");
229  }
230
231  const { name, description, phases } = value;
232  if (typeof name !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) {
233    throw invalidWorkflowMeta(scriptPath, "meta.name must be kebab-case");
234  }
235  if (
236    typeof description !== "string" ||
237    !description.trim() ||
238    /[\r\n]/.test(description) ||
239    hasControlCharacters(description)
240  ) {
241    throw invalidWorkflowMeta(
242      scriptPath,
243      "meta.description must be a non-empty, one-line string",
244    );
245  }
246  if (!Array.isArray(phases) || phases.length === 0) {
247    throw invalidWorkflowMeta(
248      scriptPath,
249      "meta.phases must be a non-empty array",
250    );
251  }
252
253  const titles = phases.map((phase, index) => {
254    if (
255      !isModuleRecord(phase) ||
256      typeof phase.title !== "string" ||
257      !phase.title.trim() ||
258      hasControlCharacters(phase.title)
259    ) {
260      throw invalidWorkflowMeta(
261        scriptPath,
262        `meta.phases[${index}].title must be a non-empty string`,
263      );
264    }
265    return phase.title.trim();
266  });
267  if (new Set(titles).size !== titles.length) {
268    throw invalidWorkflowMeta(scriptPath, "meta.phases titles must be unique");
269  }
270
271  const exampleArgs = parseExampleArgs(value.exampleArgs, scriptPath);
272  return Object.freeze({
273    name,
274    description: description.trim(),
275    phases: Object.freeze(titles.map((title) => Object.freeze({ title }))),
276    ...(exampleArgs === undefined ? {} : { exampleArgs }),
277  });
278}
279
280function parseExampleArgs(
281  value: unknown,
282  scriptPath: string,
283): Readonly<Record<string, WorkflowExampleValue>> | undefined {
284  if (value === undefined) {
285    return undefined;
286  }
287  if (!isModuleRecord(value) || Array.isArray(value)) {
288    throw invalidWorkflowMeta(
289      scriptPath,
290      "meta.exampleArgs must be a JSON-safe object",
291    );
292  }
293
294  return Object.freeze(
295    Object.fromEntries(
296      Object.entries(value).map(([key, item]) => [
297        key,
298        cloneExampleValue(item, scriptPath),
299      ]),
300    ),
301  );
302}
303
304function cloneExampleValue(
305  value: unknown,
306  scriptPath: string,
307): WorkflowExampleValue {
308  if (
309    value === null ||
310    typeof value === "boolean" ||
311    typeof value === "number" ||
312    typeof value === "string"
313  ) {
314    if (typeof value === "number" && !Number.isFinite(value)) {
315      throw invalidWorkflowMeta(
316        scriptPath,
317        "meta.exampleArgs must contain only finite JSON numbers",
318      );
319    }
320    return value;
321  }
322  if (Array.isArray(value)) {
323    return Object.freeze(
324      value.map((item) => cloneExampleValue(item, scriptPath)),
325    );
326  }
327  if (isModuleRecord(value)) {
328    return Object.freeze(
329      Object.fromEntries(
330        Object.entries(value).map(([key, item]) => [
331          key,
332          cloneExampleValue(item, scriptPath),
333        ]),
334      ),
335    );
336  }
337  throw invalidWorkflowMeta(
338    scriptPath,
339    "meta.exampleArgs must contain only JSON-safe values",
340  );
341}
342
343function invalidWorkflowMeta(
344  scriptPath: string,
345  detail: string,
346): WorkflowLoadError {
347  return new WorkflowLoadError(
348    scriptPath,
349    `Invalid Workflow metadata at ${scriptPath}: ${detail}.`,
350  );
§ 02 · KEY FLOW

关键流程

▸端到端调用路径

input → contract check → context/event binding → user handler → end/error event → typed result

最后看 src/flow/phase.ts:52、src/flow/phase.ts:74、src/flow/context.ts:42、tests/flow/workflow.test.ts:199 与 tests/flow/workflow.test.ts:205。这组证据把实现和测试连起来:源码定义行为,测试断言事件顺序、错误闭合和返回值结构。对真实 coding-agent 平台来说,这比 README 示例更有价值,因为它回答了失败时系统还剩下哪些可观察信号。

§ 03 · EDGE CASES

边界 / 失败模式

  • 源码把错误路径显式化,但调用方仍要决定 null、throw 和 retry 的业务语义。
  • CLI adapter 依赖宿主机安装对应命令,测试中通过 stub 覆盖,生产中还需要版本治理。
  • 事件流是同步 emitter,监听器自身不能做阻塞重活。
§ 04 · RELATED

关联模块

Generated · 2026-07-28Source · deerwork-ai/deer-workflow↑ 回到索引