这是「GSD 全景代码解析」专题的第 39 篇。在本系列中,我们将逐层拆解 Get Shit Done (GSD) 这一 56K+ stars 的 Meta-Prompting 框架,从命令系统到工作流编排,从 Agent 设计到上下文工程,再到 SDK 运行时内核,带你一览上下文驱动开发的工程全貌。
- 第 13 篇:工作流架构总览
- 第 14 篇:execute-phase 工作流深度解析
- 第 22 篇:Agent 架构总览
- 第 37 篇:参考文档编写最佳实践
- 第 39 篇:Phase Runner 核心引擎(本文)
在前 38 篇文章中,我们系统梳理了 GSD 的命令系统、工作流编排层、Agent 体系、上下文工程和参考文档体系。这些内容构成了 GSD 的「静态资产」——它们定义了规则、流程和规范,但真正让这些静态资产动起来的,是位于 SDK 层的一个核心模块:phase-runner.ts。
如果说工作流是「剧本」,Agent 是「演员」,那么 Phase Runner 就是「舞台导演」——它负责调度演员、控制灯光、管理幕布开关,并在演出出错时决定是否暂停或继续。作为 GSD SDK 中体积最大的单个模块(39KB),phase-runner.ts 承载着阶段执行、状态机管理和生命周期控制三大核心职责。
本文将深入拆解这个「舞台导演」的内部机制。
一、phase-runner.ts 概览:39KB 的引擎巨兽
phase-runner.ts 是 GSD SDK 中体积最大、逻辑最复杂的模块。在典型的 GSD 项目中,SDK 层的总代码量约为 80-120KB,而 Phase Runner 独占 39KB,占比超过 30%。这个比例并非偶然——它反映了 Phase Runner 在整个系统中的枢纽地位。
1.1 模块定位:编排与执行的桥梁
在 GSD 的架构分层中,Phase Runner 位于一个承上启下的关键位置:
flowchart TD
subgraph 上层抽象层
CLI["CLI 命令入口
gsd-cli.ts"]
Config["配置系统
config-loader.ts"]
end
subgraph 核心引擎层
PR["Phase Runner
phase-runner.ts (39KB)"]
end
subgraph 下层基础设施
WF["工作流解析器
workflow-parser.ts"]
AG["Agent 调度器
agent-dispatcher.ts"]
GH["Git 操作层
git-operations.ts"]
FS["文件系统层
fs-adapter.ts"]
end
CLI -->|"解析命令参数"| PR
Config -->|"加载配置上下文"| PR
PR -->|"解析工作流模板"| WF
PR -->|"派发 Agent 任务"| AG
PR -->|"提交状态变更"| GH
PR -->|"读写阶段文件"| FSPhase Runner 的上游是 CLI 和配置系统,下游是工作流解析器、Agent 调度器、Git 操作层和文件系统层。它的核心职责可以概括为一句话:
将工作流定义(workflow)转化为可执行的阶段序列,并在每个阶段中正确调度 Agent、管理状态、处理异常。
1.2 核心职责拆解
39KB 的代码量分布在以下五个核心职责域中:
| 职责域 | 代码量(估算) | 核心功能 |
|---|---|---|
| 阶段执行流程 | ~12KB | 阶段初始化、上下文组装、Agent 调用、结果处理、状态更新 |
| 状态机设计 | ~10KB | 状态定义、状态转换、事件驱动、持久化 |
| 生命周期管理 | ~8KB | 阶段钩子、错误处理、超时控制、重试机制 |
| 工作流衔接 | ~5KB | 解析 workflows/*.md、变量替换、条件分支 |
| 工具函数 | ~4KB | 日志、工具函数、类型守卫 |
这种代码分布反映了设计优先级:执行流程和状态机是核心,生命周期管理是保障,工作流衔接是边界。
1.3 为什么需要 Phase Runner
一个自然的问题是:为什么工作流不直接调用 Agent,而需要一个中间层?答案在于 GSD 的设计哲学——Separation of Orchestration and Execution(编排与执行分离)。
工作流文件(workflows/*.md)是声明式的,它们描述「做什么」,但不描述「怎么做」。Phase Runner 是命令式的,它负责:
- 何时进入下一个阶段(基于状态机判断)
- 如何组装 Agent 的上下文(基于阶段配置和运行时状态)
- 怎样处理 Agent 的返回结果(成功、失败、部分成功、需要人工介入)
- 是否需要重试或回滚(基于错误策略和超时配置)
没有 Phase Runner,工作流文件将不得不混入大量命令式逻辑,破坏其声明式的纯净性。
二、阶段执行流程:从计划到落地的五步法
Phase Runner 的核心工作循环是阶段执行流程。每个 GSD 项目的生命周期都由一系列 phase(阶段)组成,例如 plan-phase、execute-phase、verify-phase 等。Phase Runner 负责依次执行这些阶段,并确保每个阶段的输出正确传递给下一个阶段。
阶段执行流程可以抽象为五个步骤:
flowchart LR
subgraph "阶段执行五步法"
direction LR
S1["1. 阶段初始化
Phase Initialization"] --> S2["2. 上下文组装
Context Assembly"]
S2 --> S3["3. Agent 调用
Agent Invocation"]
S3 --> S4["4. 结果处理
Result Processing"]
S4 --> S5["5. 状态更新
State Update"]
end
S5 -.->|"下一阶段"| S1下面逐一解析每个步骤的实现细节。
2.1 阶段初始化:从 STATE.md 到运行时上下文
阶段初始化是 Phase Runner 执行一个 phase 的第一步。它的任务是将静态的阶段定义转化为可执行的运行时上下文。
初始化流程:
flowchart TD
Start(["开始阶段初始化"]) --> LoadState["加载 STATE.md"]
LoadState --> ParsePhase["解析阶段定义
从 workflows/*.md"]
ParsePhase --> CheckDeps["检查依赖阶段"]
CheckDeps -->|"依赖未完成"| Wait["等待/报错"]
CheckDeps -->|"依赖已完成"| LoadCtx["加载阶段上下文
phase context"]
LoadCtx --> Validate["验证输入条件
preconditions"]
Validate -->|"验证失败"| Abort["中止执行"]
Validate -->|"验证通过"| InitDone(["初始化完成"])
Wait --> Abort关键代码逻辑(概念伪代码):
async function initializePhase(
phaseId: string,
projectContext: ProjectContext
): Promise<PhaseRuntimeContext> {
// 1. 从 STATE.md 读取当前项目状态
const state = await loadState(projectContext.statePath);
// 2. 解析阶段定义(从 workflows/*.md 中读取该阶段的配置)
const phaseDef = await workflowParser.parsePhase(phaseId);
// 3. 检查依赖阶段是否已完成
for (const dep of phaseDef.dependsOn) {
if (!state.completedPhases.includes(dep)) {
throw new PhaseDependencyError(
`Phase ${phaseId} depends on ${dep}, which is not completed.`
);
}
}
// 4. 加载阶段上下文(模板变量、参考文档路径等)
const phaseContext = await loadPhaseContext(phaseDef, projectContext);
// 5. 验证前置条件
if (phaseDef.preconditions) {
const ok = await validatePreconditions(phaseDef.preconditions, phaseContext);
if (!ok) {
throw new PreconditionError(`Preconditions not met for phase ${phaseId}`);
}
}
// 6. 组装运行时上下文
return {
phaseId,
phaseDef,
state,
context: phaseContext,
startTime: Date.now(),
status: PhaseStatus.INITIALIZING,
};
}设计要点:
- STATE.md 是真相源:所有阶段的状态信息(已完成、进行中、失败)都来源于
STATE.md,而不是内存中的临时状态。这保证了崩溃恢复的可行性。 - 依赖检查是硬约束:如果一个阶段的依赖未完成,Phase Runner 会立即抛出
PhaseDependencyError,而不是静默等待。这种「fail-fast」策略避免了死锁和状态漂移。 - 前置条件(preconditions)是软约束:与依赖不同,preconditions 是阶段定义中可选的条件表达式(如"Git 工作树必须干净"),用于在阶段执行前进行环境检查。
2.2 上下文组装:将静态模板转化为动态提示
上下文组装是 Phase Runner 最具技术含量的环节之一。它的任务是将工作流文件中的静态模板转化为适合特定 Agent 的动态提示(prompt)。
组装流程:
flowchart TD
Start(["开始上下文组装"]) --> LoadTemplate["加载阶段模板
workflows/phase.md"]
LoadTemplate --> ResolveVars["解析模板变量
"]
ResolveVars --> LoadRefs["加载参考文档
references/*.md"]
LoadRefs --> InjectState["注入当前状态
STATE.md 片段"]
InjectState --> BuildPrompt["构建 Agent Prompt"]
BuildPrompt --> SetParams["设置模型参数
temperature, max_tokens"]
SetParams --> Done(["上下文组装完成"])变量解析机制:
GSD 工作流模板使用 Mustache 风格的变量插值:{{variable_name}}。Phase Runner 在组装上下文时,会按以下优先级解析这些变量:
| 优先级 | 变量来源 | 示例 | 说明 |
|---|---|---|---|
| 1 | 阶段参数 | {{phase_name}} | 从阶段定义中直接获取 |
| 2 | 项目配置 | {{project_name}} | 从 config.json 中读取 |
| 3 | 运行时状态 | {{current_phase}} | 从 STATE.md 中动态提取 |
| 4 | 环境变量 | {{env.API_KEY}} | 从进程环境变量中读取 |
| 5 | 系统内置 | {{timestamp}} | Phase Runner 自动注入 |
参考文档加载策略:
GSD 的一个核心设计是「参考文档(References)驱动」。Phase Runner 不会将所有参考文档一次性注入 Agent 的上下文,而是根据阶段定义中的 references 字段进行选择性加载:
async function loadReferences(
refConfig: ReferenceConfig,
projectContext: ProjectContext
): Promise<string[]> {
const loaded: string[] = [];
for (const ref of refConfig.includes) {
const content = await fs.readFile(
path.join(projectContext.refsDir, ref),
'utf-8'
);
// 根据阶段需求截取相关章节
if (refConfig.sections) {
loaded.push(extractSections(content, refConfig.sections));
} else {
loaded.push(content);
}
}
// 应用上下文预算控制
return applyContextBudget(loaded, refConfig.maxTokens);
}**上下文预算(Context Budget)**是 Phase Runner 的一个关键机制。它确保注入 Agent 的参考文档总量不超过模型的上下文窗口限制。如果参考文档总长度超出预算,Phase Runner 会按优先级截断或摘要处理。
2.3 Agent 调用:从 Prompt 到执行的桥梁
上下文组装完成后,Phase Runner 进入第三步:调用 Agent。这一步的本质是将组装好的 prompt 提交给 Agent 调度器(Agent Dispatcher),并等待执行结果。
调用流程:
sequenceDiagram
participant PR as Phase Runner
participant AD as Agent Dispatcher
participant LLM as LLM API
participant WS as 工作区
PR->>PR: 组装 Agent Prompt
PR->>AD: dispatch(agentId, prompt, config)
AD->>AD: 选择模型 & 工具
AD->>LLM: 发送请求
activate LLM
LLM-->>AD: 流式响应 (SSE)
AD-->>PR: 实时进度回调
LLM->>WS: 工具调用(文件读写等)
WS-->>LLM: 工具结果
LLM-->>AD: 最终响应
deactivate LLM
AD-->>PR: 返回 AgentResult关键设计决策:
流式响应(Streaming):Phase Runner 通过 SSE(Server-Sent Events)接收 Agent 的流式输出,并在控制台实时渲染进度。这不仅提升了用户体验,还允许 Phase Runner 在 Agent 执行过程中进行中断检查(例如用户按下 Ctrl+C)。
工具调用代理:Agent 在执行过程中可能会调用工具(如读写文件、执行 shell 命令)。这些工具调用不是由 Agent 直接执行的,而是通过 Agent Dispatcher 代理到工作区。Phase Runner 会记录所有工具调用的日志,用于后续的审计和回溯。
并发控制:如果一个阶段需要同时调度多个 Agent(例如 execute-phase 的 Wave 执行模型),Phase Runner 会维护一个并发池,确保同时运行的 Agent 数量不超过配置上限(默认为 3)。
Agent 调用接口:
interface AgentInvocationConfig {
agentId: string; // 如 "gsd-executor"
model?: string; // 覆盖默认模型
temperature?: number; // 采样温度
maxTokens?: number; // 最大输出长度
timeout?: number; // 超时时间(毫秒)
tools?: string[]; // 允许使用的工具列表
streaming?: boolean; // 是否启用流式输出
}
interface AgentResult {
status: 'success' | 'failure' | 'partial' | 'interrupted';
output: string; // Agent 的文本输出
artifacts?: Artifact[]; // 生成的文件、代码块等
toolCalls?: ToolCallLog[]; // 工具调用记录
metrics?: UsageMetrics; // Token 使用量等
}2.4 结果处理:从原始输出到结构化产物
Agent 返回的结果通常是原始文本(可能包含 Markdown、代码块、文件路径等)。Phase Runner 的第四步是结果处理——将原始输出解析为结构化产物,并验证其质量。
处理流程:
flowchart TD
Start(["Agent 返回结果"]) --> Parse["解析输出结构"]
Parse --> Extract["提取产物
代码块/文件路径/SUMMARY.md"]
Extract --> Validate["验证产物完整性"]
Validate -->|"验证失败"| Retry["触发重试/回滚"]
Validate -->|"验证通过"| Store["存储产物到工作区"]
Store --> Diff["生成变更 Diff"]
Diff --> Done(["结果处理完成"])产物提取策略:
不同的 Agent 类型会产出不同格式的结果。Phase Runner 维护了一套产物解析器注册表:
| Agent 类型 | 产物格式 | 解析器 |
|---|---|---|
| gsd-planner | PLAN.md | MarkdownPlanParser |
| gsd-executor | 代码块 + SUMMARY.md | CodeArtifactParser |
| gsd-verifier | 验证报告 | ReportParser |
| gsd-researcher | 研究笔记 | NoteParser |
以 gsd-executor 为例,其输出通常包含:
- 代码块(fenced code blocks):实际修改的代码
- 文件路径标注(如
// file: src/utils.ts):代码块对应的文件 - SUMMARY.md 片段:执行摘要,用于更新 STATE.md
Phase Runner 的 CodeArtifactParser 会提取这些信息,并生成一个 Artifact[] 数组:
interface Artifact {
type: 'code' | 'markdown' | 'config' | 'diff';
targetPath: string; // 目标文件路径
content: string; // 内容
language?: string; // 代码语言
description?: string; // 变更描述
}验证机制:
产物提取后,Phase Runner 会进行一系列验证:
- 路径合法性:目标路径是否在项目目录内(防止路径遍历攻击)
- 语法检查(可选):如果是代码文件,是否可通过编译/解析
- 冲突检测:目标文件是否已被其他并发 Agent 修改
- 格式合规:如果是 PLAN.md 或 STATE.md,是否符合 GSD 规范
2.5 状态更新:持久化阶段成果
阶段执行的最后一步是将成果持久化到项目状态文件中。这是保证 GSD 项目可恢复性的关键。
更新流程:
flowchart TD
Start(["状态更新"]) --> WriteArtifacts["写入产物到工作区"]
WriteArtifacts --> UpdateState["更新 STATE.md"]
UpdateState --> CheckCheckpoints["检查是否需要检查点"]
CheckCheckpoints -->|"需要人工介入"| CreateCheckpoint["创建检查点
checkpoint.md"]
CheckCheckpoints -->|"自动通过"| GitCommit["Git 原子提交"]
CreateCheckpoint --> Pause(["暂停等待用户"])
GitCommit --> LogAudit["写入审计日志"]
LogAudit --> Done(["阶段完成"])STATE.md 更新策略:
Phase Runner 使用原子更新策略修改 STATE.md:
- 读取当前
STATE.md到内存 - 根据阶段执行结果更新对应字段(如
current_phase、completed_phases、pending_tasks) - 将更新后的内容写入临时文件
.STATE.md.tmp - 重命名临时文件覆盖原文件(利用文件系统的原子性)
- 执行
git add STATE.md和git commit(如果配置了自动提交)
这种策略保证了即使在写入过程中进程崩溃,STATE.md 也不会处于损坏的中间状态。
检查点(Checkpoint)机制:
某些阶段在执行过程中可能需要人工介入(例如审查生成的计划、确认破坏性变更)。Phase Runner 支持在阶段定义中声明检查点:
# 阶段定义片段(来自 workflows/*.md)
checkpoints:
- name: "review_plan"
condition: "phase == 'plan-phase'"
prompt: "请审查生成的 PLAN.md,确认后输入 'yes' 继续"当检查点条件满足时,Phase Runner 会暂停阶段执行,生成 checkpoint.md 文件,并在控制台等待用户输入。只有在用户确认后,阶段才会继续。
三、状态机设计:阶段生命周期的精确控制
如果说阶段执行流程是 Phase Runner 的「骨架」,那么状态机就是它的「神经系统」。状态机负责精确控制每个阶段的生老病死,确保阶段在任何时刻都处于一个明确定义的状态。
3.1 状态定义:阶段的七种生命态
GSD Phase Runner 定义了七种阶段状态:
stateDiagram-v2
[*] --> PENDING: 项目初始化
PENDING --> INITIALIZING: 开始阶段
INITIALIZING --> RUNNING: 初始化完成
INITIALIZING --> FAILED: 前置条件失败
RUNNING --> PAUSED: 遇到检查点
RUNNING --> COMPLETED: 执行成功
RUNNING --> PARTIAL: 部分成功
RUNNING --> FAILED: 执行失败
RUNNING --> TIMEOUT: 超时
PAUSED --> RUNNING: 用户确认
PAUSED --> ABORTED: 用户取消
COMPLETED --> [*]
PARTIAL --> [*]
FAILED --> RETRYING: 自动重试
TIMEOUT --> RETRYING: 自动重试
RETRYING --> RUNNING: 重试开始
RETRYING --> FAILED: 重试次数耗尽
FAILED --> [*]
TIMEOUT --> [*]
ABORTED --> [*]状态详解:
| 状态 | 含义 | 可转换至 | 说明 |
|---|---|---|---|
| PENDING | 等待执行 | INITIALIZING | 阶段已被计划,但尚未开始 |
| INITIALIZING | 初始化中 | RUNNING, FAILED | 正在加载上下文、检查依赖 |
| RUNNING | 执行中 | COMPLETED, PARTIAL, FAILED, TIMEOUT, PAUSED | Agent 正在执行任务 |
| PAUSED | 暂停 | RUNNING, ABORTED | 等待用户输入(检查点) |
| COMPLETED | 已完成 | — | 阶段成功结束 |
| PARTIAL | 部分完成 | — | 主要任务完成,但存在警告 |
| FAILED | 失败 | RETRYING, — | 执行过程中发生错误 |
| TIMEOUT | 超时 | RETRYING, — | 超过配置的超时时间 |
| RETRYING | 重试中 | RUNNING, FAILED | 正在准备重新执行 |
| ABORTED | 已中止 | — | 用户手动取消 |
状态的数据结构:
enum PhaseStatus {
PENDING = 'pending',
INITIALIZING = 'initializing',
RUNNING = 'running',
PAUSED = 'paused',
COMPLETED = 'completed',
PARTIAL = 'partial',
FAILED = 'failed',
TIMEOUT = 'timeout',
RETRYING = 'retrying',
ABORTED = 'aborted',
}
interface PhaseState {
phaseId: string;
status: PhaseStatus;
startTime?: number; // 首次开始时间
endTime?: number; // 结束时间
retryCount: number; // 已重试次数
lastError?: PhaseError; // 最后一次错误信息
checkpoint?: Checkpoint; // 当前检查点(如果有)
artifacts?: Artifact[]; // 阶段产物
}3.2 状态转换:事件驱动的状态迁移
Phase Runner 的状态机是事件驱动的。任何状态转换都由一个明确的事件触发,而不是直接修改状态。
事件类型定义:
type PhaseEvent =
| { type: 'PHASE_START'; phaseId: string }
| { type: 'INIT_COMPLETE'; phaseId: string }
| { type: 'INIT_FAILED'; phaseId: string; error: PhaseError }
| { type: 'AGENT_COMPLETE'; phaseId: string; result: AgentResult }
| { type: 'AGENT_FAILED'; phaseId: string; error: PhaseError }
| { type: 'CHECKPOINT_REACHED'; phaseId: string; checkpoint: Checkpoint }
| { type: 'CHECKPOINT_RESOLVED'; phaseId: string; decision: 'continue' | 'abort' }
| { type: 'TIMEOUT'; phaseId: string }
| { type: 'RETRY'; phaseId: string }
| { type: 'USER_ABORT'; phaseId: string };状态转换函数:
function transition(
state: PhaseState,
event: PhaseEvent
): PhaseState {
// 状态转换表:当前状态 + 事件 => 下一状态
const transitions: Record<PhaseStatus, Partial<Record<PhaseEvent['type'], PhaseStatus>>> = {
[PhaseStatus.PENDING]: {
'PHASE_START': PhaseStatus.INITIALIZING,
},
[PhaseStatus.INITIALIZING]: {
'INIT_COMPLETE': PhaseStatus.RUNNING,
'INIT_FAILED': PhaseStatus.FAILED,
},
[PhaseStatus.RUNNING]: {
'AGENT_COMPLETE': PhaseStatus.COMPLETED,
'AGENT_FAILED': PhaseStatus.FAILED,
'TIMEOUT': PhaseStatus.TIMEOUT,
'CHECKPOINT_REACHED': PhaseStatus.PAUSED,
'USER_ABORT': PhaseStatus.ABORTED,
},
[PhaseStatus.PAUSED]: {
'CHECKPOINT_RESOLVED': PhaseStatus.RUNNING, // 继续
'USER_ABORT': PhaseStatus.ABORTED, // 取消
},
[PhaseStatus.FAILED]: {
'RETRY': PhaseStatus.RETRYING,
},
[PhaseStatus.TIMEOUT]: {
'RETRY': PhaseStatus.RETRYING,
},
[PhaseStatus.RETRYING]: {
'PHASE_START': PhaseStatus.RUNNING,
},
// 终态不接受任何事件
[PhaseStatus.COMPLETED]: {},
[PhaseStatus.PARTIAL]: {},
[PhaseStatus.ABORTED]: {},
};
const nextStatus = transitions[state.status]?.[event.type];
if (!nextStatus) {
throw new InvalidTransitionError(
`Cannot transition from ${state.status} via ${event.type}`
);
}
return {
...state,
status: nextStatus,
// 根据事件类型更新附加字段
...(event.type === 'AGENT_FAILED' && { lastError: event.error }),
...(event.type === 'TIMEOUT' && { lastError: { type: 'timeout', message: 'Phase timed out' } }),
...(event.type === 'RETRY' && { retryCount: state.retryCount + 1 }),
};
}设计要点:
- 不可变更新:状态转换函数是纯函数,接收旧状态返回新状态,不修改原状态对象。这保证了状态历史的可追溯性。
- 非法转换拦截:如果某个事件在当前状态下不被允许(例如在
COMPLETED状态下收到AGENT_FAILED),Phase Runner 会抛出InvalidTransitionError,而不是静默忽略。 - 终态不可变:
COMPLETED、PARTIAL、ABORTED是终态,一旦进入就不能再转换。这防止了「已完成的阶段被重新打开」这种逻辑错误。
3.3 事件驱动架构:观察者模式与异步队列
Phase Runner 的事件系统采用**发布-订阅(Pub/Sub)**模式实现。状态机是核心订阅者,但其他模块(如日志系统、UI 渲染器、外部监控系统)也可以订阅事件。
事件总线架构:
flowchart TD
subgraph 事件生产者
Init["阶段初始化器"]
Agent["Agent 调度器"]
Timer["超时定时器"]
User["用户输入"]
end
subgraph 事件总线
EB["EventBus
异步队列"]
end
subgraph 事件消费者
SM["状态机
State Machine"]
Logger["日志系统"]
UI["UI 渲染器"]
Audit["审计记录器"]
Ext["外部钩子"]
end
Init -->|"PHASE_START"| EB
Agent -->|"AGENT_COMPLETE"| EB
Timer -->|"TIMEOUT"| EB
User -->|"USER_ABORT"| EB
EB --> SM
EB --> Logger
EB --> UI
EB --> Audit
EB --> Ext事件队列的异步处理:
class PhaseEventBus {
private queue: PhaseEvent[] = [];
private handlers: Map<string, Set<(event: PhaseEvent) => void>> = new Map();
private processing = false;
emit(event: PhaseEvent): void {
this.queue.push(event);
if (!this.processing) {
this.processQueue();
}
}
private async processQueue(): Promise<void> {
this.processing = true;
while (this.queue.length > 0) {
const event = this.queue.shift()!;
const handlers = this.handlers.get(event.type) || new Set();
// 串行执行所有处理器,保证顺序一致性
for (const handler of handlers) {
try {
await handler(event);
} catch (error) {
// 处理器错误不应影响其他处理器
console.error(`Event handler failed for ${event.type}:`, error);
}
}
}
this.processing = false;
}
on(eventType: string, handler: (event: PhaseEvent) => void): void {
if (!this.handlers.has(eventType)) {
this.handlers.set(eventType, new Set());
}
this.handlers.get(eventType)!.add(handler);
}
}设计要点:
- 串行处理:虽然事件总线内部使用异步队列,但同一类型的事件是串行处理的。这避免了并发状态转换导致的竞态条件。
- 错误隔离:单个事件处理器的失败不会阻止其他处理器执行。这保证了日志记录、UI 更新等辅助功能不会因为主逻辑错误而中断。
- 背压控制:事件队列有最大长度限制(默认 1000)。如果队列溢出,新事件会被丢弃并记录警告。这防止了内存泄漏。
四、生命周期管理:钩子和异常控制
生命周期管理是 Phase Runner 的「安全带」机制。它通过阶段钩子、错误处理和超时控制,确保阶段执行过程的可观测性、可恢复性和安全性。
4.1 阶段生命周期钩子:在关键节点插入自定义逻辑
Phase Runner 提供了一套类似前端框架的生命周期钩子,允许用户和外部扩展在阶段执行的关键节点插入自定义逻辑。
钩子体系:
flowchart TD
subgraph "Phase Runner 生命周期钩子"
direction TB
A["beforePhaseStart
阶段开始前"] --> B["afterInit
初始化完成后"]
B --> C["beforeAgentCall
Agent 调用前"]
C --> D["onAgentStream
Agent 流式输出中"]
D --> E["afterAgentCall
Agent 调用后"]
E --> F["beforeStateUpdate
状态更新前"]
F --> G["afterPhaseComplete
阶段完成后"]
H["onError
错误发生时"] -.-> E
I["onTimeout
超时发生时"] -.-> E
J["onCheckpoint
检查点触发时"] -.-> C
end钩子接口定义:
interface PhaseHooks {
// 阶段开始前
beforePhaseStart?: (ctx: PhaseRuntimeContext) => Promise<void>;
// 初始化完成后(Agent 调用前)
afterInit?: (ctx: PhaseRuntimeContext) => Promise<void>;
// Agent 调用前(可用于修改 prompt)
beforeAgentCall?: (
ctx: PhaseRuntimeContext,
prompt: string
) => Promise<string>; // 返回修改后的 prompt
// Agent 流式输出中(实时回调)
onAgentStream?: (
ctx: PhaseRuntimeContext,
chunk: string
) => Promise<void>;
// Agent 调用后(结果处理前)
afterAgentCall?: (
ctx: PhaseRuntimeContext,
result: AgentResult
) => Promise<AgentResult>; // 可修改结果
// 状态更新前(可拦截或修改状态变更)
beforeStateUpdate?: (
ctx: PhaseRuntimeContext,
stateUpdate: Partial<ProjectState>
) => Promise<Partial<ProjectState>>;
// 阶段完成后(无论成功失败)
afterPhaseComplete?: (ctx: PhaseRuntimeContext) => Promise<void>;
// 错误发生时
onError?: (
ctx: PhaseRuntimeContext,
error: PhaseError
) => Promise<'retry' | 'abort' | 'continue'>;
// 超时发生时
onTimeout?: (
ctx: PhaseRuntimeContext
) => Promise<'retry' | 'abort'>;
// 检查点触发时
onCheckpoint?: (
ctx: PhaseRuntimeContext,
checkpoint: Checkpoint
) => Promise<string>; // 返回用户决策
}钩子的使用场景:
| 钩子 | 典型使用场景 |
|---|---|
beforePhaseStart | 清理临时文件、检查磁盘空间、加载自定义配置 |
afterInit | 注入额外的参考文档、修改阶段上下文 |
beforeAgentCall | Prompt 注入(如添加自定义指令)、A/B 测试不同 prompt |
onAgentStream | 实时渲染 Markdown、计算 Token 使用量、检测敏感信息 |
afterAgentCall | 结果过滤(去除 PII)、自动格式化代码、质量评分 |
beforeStateUpdate | 拦截危险操作(如删除关键文件)、添加自定义元数据 |
afterPhaseComplete | 发送通知(Slack/邮件)、生成阶段报告、触发 CI/CD |
onError | 自动创建 issue、通知运维、记录到错误追踪系统 |
onTimeout | 优雅降级(切换到低精度模型)、保存检查点 |
onCheckpoint | 自动化审查(用另一个 Agent 审查结果)、批量确认 |
钩子的注册方式:
// 方式一:在 config.json 中注册(全局钩子)
{
"hooks": {
"beforePhaseStart": "./hooks/before-start.js",
"onError": "./hooks/error-handler.js"
}
}
// 方式二:在阶段定义中注册(阶段级钩子)
// workflows/execute-phase.md
---
hooks:
beforeAgentCall: ./hooks/inject-custom-prompt.js
onAgentStream: ./hooks/real-time-renderer.js
---
// 方式三:SDK 编程式注册(运行时动态)
const runner = new PhaseRunner(config);
runner.hooks.beforePhaseStart = async (ctx) => {
console.log(`Starting phase: ${ctx.phaseId}`);
};4.2 错误处理:分层防御与优雅降级
Phase Runner 的错误处理采用分层防御策略,每一层负责处理特定类型的错误。
错误分层架构:
flowchart TD
subgraph 错误处理分层
direction TB
L1["L1: 预防层
Prevention"]
L2["L2: 捕获层
Catch"]
L3["L3: 恢复层
Recovery"]
L4["L4: 报告层
Reporting"]
end
L1 -->|"前置条件检查
参数验证
类型守卫"| L2
L2 -->|"try/catch
Promise.catch
Event error 隔离"| L3
L3 -->|"重试机制
回滚操作
检查点恢复"| L4
L4 -->|"日志记录
STATE.md 标记
用户通知"| End(["错误闭环"])错误分类与处理策略:
// Phase Runner 的错误体系
class PhaseError extends Error {
constructor(
message: string,
public code: string, // 错误代码
public severity: 'fatal' | 'recoverable' | 'warning',
public recoverable: boolean, // 是否可恢复
public context?: Record<string, unknown>
) {
super(message);
}
}
// 具体错误类型
class PhaseDependencyError extends PhaseError {
constructor(depPhase: string) {
super(
`Dependency phase ${depPhase} not completed`,
'DEPENDENCY_MISSING',
'fatal',
false
);
}
}
class PreconditionError extends PhaseError {
constructor(condition: string) {
super(
`Precondition not met: ${condition}`,
'PRECONDITION_FAILED',
'fatal',
false
);
}
}
class AgentInvocationError extends PhaseError {
constructor(cause: Error) {
super(
`Agent invocation failed: ${cause.message}`,
'AGENT_INVOCATION_FAILED',
'recoverable',
true
);
}
}
class TimeoutError extends PhaseError {
constructor(timeoutMs: number) {
super(
`Phase timed out after ${timeoutMs}ms`,
'PHASE_TIMEOUT',
'recoverable',
true
);
}
}错误处理流程:
async function executePhaseWithErrorHandling(
phaseId: string,
ctx: ProjectContext,
hooks: PhaseHooks
): Promise<PhaseResult> {
let phaseCtx: PhaseRuntimeContext | undefined;
try {
// L1: 预防层 - 参数验证
validatePhaseId(phaseId);
validateProjectContext(ctx);
// 阶段初始化
phaseCtx = await initializePhase(phaseId, ctx);
await hooks.beforePhaseStart?.(phaseCtx);
// 上下文组装
const prompt = await assembleContext(phaseCtx);
const modifiedPrompt = await hooks.beforeAgentCall?.(phaseCtx, prompt) ?? prompt;
// Agent 调用(带超时控制)
const result = await runWithTimeout(
() => invokeAgent(modifiedPrompt, phaseCtx!, hooks.onAgentStream),
phaseCtx.phaseDef.timeout ?? 300000 // 默认 5 分钟
);
const processedResult = await hooks.afterAgentCall?.(phaseCtx, result) ?? result;
// 结果处理
await processResult(phaseCtx, processedResult);
// 状态更新
const stateUpdate = buildStateUpdate(phaseCtx, processedResult);
const finalUpdate = await hooks.beforeStateUpdate?.(phaseCtx, stateUpdate) ?? stateUpdate;
await updateState(ctx.statePath, finalUpdate);
await hooks.afterPhaseComplete?.(phaseCtx);
return { success: true, phaseId };
} catch (error) {
// L2: 捕获层
const phaseError = normalizeError(error);
// L3: 恢复层 - 尝试恢复
if (phaseError.recoverable && phaseCtx) {
const decision = await hooks.onError?.(phaseCtx, phaseError) ?? 'abort';
if (decision === 'retry' && phaseCtx.retryCount < MAX_RETRIES) {
phaseCtx.retryCount++;
await emitEvent({ type: 'RETRY', phaseId });
return executePhaseWithErrorHandling(phaseId, ctx, hooks); // 递归重试
}
if (decision === 'continue') {
// 标记为 PARTIAL 并继续
await updateState(ctx.statePath, {
phaseId,
status: PhaseStatus.PARTIAL,
lastError: phaseError,
});
return { success: true, phaseId, partial: true };
}
}
// L4: 报告层 - 记录错误并终止
if (phaseCtx) {
await updateState(ctx.statePath, {
phaseId,
status: PhaseStatus.FAILED,
lastError: phaseError,
});
await hooks.afterPhaseComplete?.(phaseCtx);
}
logger.error(`Phase ${phaseId} failed:`, phaseError);
throw phaseError;
}
}4.3 超时控制:防止无限等待的保险丝
在 AI Agent 驱动的开发中,Agent 可能陷入「思考循环」或调用耗时过长的工具。超时控制是防止这种情况的保险丝。
超时机制架构:
sequenceDiagram
participant PR as Phase Runner
participant T as 超时定时器
participant AG as Agent 调度器
participant LLM as LLM API
PR->>AG: 启动 Agent 调用
PR->>T: 启动定时器 (timeout = 5min)
activate T
AG->>LLM: 发送请求
activate LLM
alt 正常完成
LLM-->>AG: 响应完成
AG-->>PR: 返回结果
PR->>T: 取消定时器
deactivate T
else 超时发生
T-->>PR: TIMEOUT 事件
PR->>AG: 取消调用
AG->>LLM: 中断请求
deactivate LLM
PR->>PR: 触发 onTimeout 钩子
alt 用户选择重试
PR->>PR: 进入 RETRYING 状态
else 用户选择中止
PR->>PR: 进入 FAILED 状态
end
end超时控制实现:
async function runWithTimeout<T>(
fn: () => Promise<T>,
timeoutMs: number,
onTimeout?: () => void
): Promise<T> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new TimeoutError(timeoutMs));
onTimeout?.();
}, timeoutMs);
fn()
.then((result) => {
clearTimeout(timer);
resolve(result);
})
.catch((error) => {
clearTimeout(timer);
reject(error);
});
});
}可配置的超时策略:
Phase Runner 支持三级超时配置:
| 级别 | 配置位置 | 说明 | 默认值 |
|---|---|---|---|
| 全局超时 | config.json 的 globalTimeout | 所有阶段的默认超时 | 300000ms (5min) |
| 阶段超时 | workflows/*.md 的 timeout | 特定阶段的超时 | 继承全局 |
| Agent 超时 | Agent 定义中的 timeout | 单次 Agent 调用的超时 | 继承阶段 |
超时时间的计算方式是累加的:阶段超时 = 初始化时间 + 上下文组装时间 + Agent 调用时间 + 结果处理时间。如果阶段定义中包含多个 Agent 调用(如 Wave 模型),则阶段超时需要覆盖所有 Agent 调用的总和。
五、与 workflows/*.md 的衔接:从声明到执行的桥梁
Phase Runner 与 GSD 工作流体系之间的衔接,是整个框架中最重要的边界之一。理解这个衔接机制,有助于我们在自定义工作流时遵循正确的约定。
5.1 工作流解析流水线
当 Phase Runner 需要执行一个阶段时,它首先通过工作流解析器将 workflows/*.md 文件转化为可执行的结构化配置。
解析流水线:
flowchart LR
MD["workflows/phase.md
Markdown 文件"] --> FM["Frontmatter
解析器"]
FM --> BD["Body
模板引擎"]
BD --> VL["变量
解析器"]
VL --> CD["条件
求值器"]
CD --> PD["PhaseDefinition
结构化配置"]Frontmatter 解析:
工作流文件的 YAML frontmatter 包含了阶段的元数据配置:
---
phase: execute-phase
name: "Execute Phase"
description: "Execute planned tasks for the current phase"
depends_on:
- plan-phase
timeout: 600000 # 10 minutes
max_retries: 2
agent: gsd-executor
context_profile: dev
checkpoints:
- name: pre_commit_review
condition: "has_uncommitted_changes"
references:
includes:
- coding-standards.md
- testing-guide.md
max_tokens: 8000
hooks:
beforeAgentCall: ./hooks/enforce-standards.js
---Phase Runner 的 frontmatter 解析器会提取这些字段,并将其映射到内部的 PhaseDefinition 接口:
interface PhaseDefinition {
phase: string;
name: string;
description: string;
dependsOn: string[];
timeout?: number;
maxRetries: number;
agent: string;
contextProfile?: string;
checkpoints: CheckpointDef[];
references: ReferenceConfig;
hooks: Partial<PhaseHooks>;
// ... 其他字段
}5.2 模板变量与运行时绑定
工作流文件的主体部分是一个 Mustache 模板。Phase Runner 在运行时通过**变量绑定(Variable Binding)**将模板转化为最终的 Agent prompt。
绑定过程:
interface VariableBinding {
// 静态绑定:值在阶段定义或配置中确定
staticVars: Record<string, string>;
// 动态绑定:值在运行时从 STATE.md 或环境计算
dynamicVars: Record<string, () => string>;
// 惰性绑定:值在首次访问时计算并缓存
lazyVars: Record<string, () => Promise<string>>;
}
async function bindVariables(
template: string,
binding: VariableBinding,
ctx: PhaseRuntimeContext
): Promise<string> {
let result = template;
// 1. 替换静态变量
for (const [key, value] of Object.entries(binding.staticVars)) {
result = result.replace(new RegExp(`{{\\s*${key}\\s*}}`, 'g'), value);
}
// 2. 替换动态变量
for (const [key, getter] of Object.entries(binding.dynamicVars)) {
if (result.includes(`{{${key}}}`)) {
result = result.replace(new RegExp(`{{\\s*${key}\\s*}}`, 'g'), getter());
}
}
// 3. 替换惰性变量(按需计算)
for (const [key, getter] of Object.entries(binding.lazyVars)) {
if (result.includes(`{{${key}}}`)) {
const value = await getter();
result = result.replace(new RegExp(`{{\\s*${key}\\s*}}`, 'g'), value);
}
}
// 4. 检查未解析的变量
const unresolved = result.match(/{{\s*[\w.]+\s*}}/g);
if (unresolved) {
logger.warn(`Unresolved variables in phase ${ctx.phaseId}:`, unresolved);
}
return result;
}内置变量清单:
Phase Runner 提供了一组内置变量,在所有阶段模板中均可使用:
| 变量名 | 来源 | 示例值 | 说明 |
|---|---|---|---|
{{project_name}} | config.json | my-web-app | 项目名称 |
{{current_phase}} | STATE.md | execute-phase | 当前阶段 ID |
{{phase_name}} | 阶段定义 | Execute Phase | 当前阶段名称 |
{{timestamp}} | 系统时间 | 2026-04-24T15:00:00Z | ISO 格式时间戳 |
{{state_summary}} | STATE.md | 3/5 tasks completed | 状态摘要 |
{{git_branch}} | Git 工作区 | feature/gsd-integration | 当前分支 |
{{git_commit}} | Git 工作区 | a1b2c3d | 当前提交 hash |
5.3 条件分支与动态编排
GSD 工作流支持简单的条件分支,Phase Runner 在运行时对条件表达式求值,决定是否包含某段模板内容。
条件语法:
{{#if condition}}
这部分内容仅在条件为真时包含
{{/if}}
{{#unless condition}}
这部分内容仅在条件为假时包含
{{/unless}}条件求值:
async function evaluateCondition(
condition: string,
ctx: PhaseRuntimeContext
): Promise<boolean> {
// 支持的条件表达式
// 1. 状态检查: "state.current_phase == 'plan-phase'"
// 2. 文件存在: "file_exists('PLAN.md')"
// 3. Git 检查: "git.has_uncommitted_changes"
// 4. 环境检查: "env.CI == 'true'"
// 5. 复合条件: "A && B || C"
const evaluators: Record<string, (args: string[]) => boolean> = {
'state': (args) => getNestedValue(ctx.state, args[0]) === args[1],
'file_exists': (args) => fs.existsSync(path.resolve(ctx.projectRoot, args[0])),
'git': (args) => evaluateGitCondition(args[0], ctx.projectRoot),
'env': (args) => process.env[args[0]] === args[1],
};
// 简单的表达式解析器
return parseAndEvaluate(condition, evaluators);
}5.4 工作流热重载与缓存
在开发模式下,Phase Runner 支持工作流文件的热重载——当 workflows/*.md 文件被修改时,无需重启进程即可生效。
热重载机制:
flowchart TD
Watch["文件监视器
fs.watch"] -->|"文件变化"| Invalidate["使缓存失效"]
Invalidate --> Reload["重新解析工作流"]
Reload --> Validate["验证工作流定义"]
Validate -->|"验证通过"| Update["更新运行时缓存"]
Validate -->|"验证失败"| Reject["拒绝热重载
保留旧版本"]Phase Runner 内部维护了一个 WorkflowCache,以 phaseId 为键缓存解析后的 PhaseDefinition。当文件变化时,对应的缓存条目被标记为失效,下次执行该阶段时会重新解析。
六、总结:Phase Runner 的设计哲学
通过本文的拆解,我们可以看到 phase-runner.ts 的 39KB 代码背后蕴含的设计哲学:
6.1 核心设计原则
| 原则 | 体现 | 收益 |
|---|---|---|
| Separation of Concerns | 工作流声明 phase,Phase Runner 执行 phase | 工作流保持简洁,引擎保持专注 |
| Event-Driven State Machine | 所有状态转换由事件触发 | 可预测、可追踪、可调试 |
| Fail-Fast | 依赖不满足立即报错,不静默等待 | 避免死锁和状态漂移 |
| Immutable State Updates | 状态转换函数返回新状态 | 历史可追溯,便于审计 |
| Graceful Degradation | 错误分层处理,支持重试和回滚 | 系统韧性高 |
| Context Budget | 参考文档按需加载,总量可控 | 防止上下文窗口溢出 |
6.2 Phase Runner 在 GSD 架构中的位置
flowchart TD
subgraph "GSD 完整架构"
direction TB
User["用户"] --> CLI["CLI 层"]
CLI --> Config["配置系统"]
Config --> PR["Phase Runner
核心引擎"]
PR --> WF["工作流解析器
workflows/*.md"]
PR --> AG["Agent 调度器
agents/*.md"]
PR --> Git["Git 操作层"]
PR --> FS["文件系统层"]
PR -.->|"状态更新"| State["STATE.md"]
PR -.->|"审计日志"| Audit["audit-trail/"]
endPhase Runner 是 GSD 架构的「心脏」——它接收来自 CLI 的指令,协调工作流、Agent、Git 和文件系统,最终将用户的意图转化为可验证的软件交付成果。理解 Phase Runner 的设计,是掌握 GSD SDK 运行时的关键一步。
6.3 阅读源码的建议
如果你希望进一步深入 phase-runner.ts 的实现细节,建议按以下顺序阅读:
- 从
runPhase()入口开始(约第 200 行):这是阶段执行的主循环,理解它就能把握整体流程。 - 关注状态机部分(约第 500-700 行):
transition()函数和PhaseEventBus是理解阶段生命周期管理的关键。 - 研究上下文组装逻辑(约第 800-1000 行):
assembleContext()和bindVariables()是 Phase Runner 最具技术含量的部分。 - 查看钩子系统(约第 1200-1400 行):
PhaseHooks接口和executeHook()函数展示了扩展机制的设计。
下一篇预告: 第 40 篇《Plan Parser 与 Prompt Builder》
我们将深入解析 GSD SDK 的另外两个核心模块:Plan Parser 如何将 Agent 生成的 PLAN.md 解析为结构化执行计划,以及 Prompt Builder 如何将阶段定义、参考文档和运行时状态组装成高质量的 LLM prompt。敬请期待。