homerailRail
§ 00 · Source-level Observatory

homerail 可审计的本地 DAG Agent 运行时

HomeRail 把语音入口、DAG 编排、容器化 Agent Worker 与本地审计串成一个可观察的运行时。它不把“多 Agent”停留在 prompt 层,而是把节点状态、交接端口、重试预算、会话与证据都写成可验证契约。

★ 366
GitHub Stars
85K
LOC
538
Files
today
Last commit
§ 00 · OVERVIEW

它解决什么,凭什么值得拆

HomeRail 的核心不是再包装一个聊天 UI,而是把 Agent 任务变成显式图运行。sources/manager/src/orchestration/dag-engine.ts:3、sources/manager/src/orchestration/dag-engine.ts:12、sources/manager/src/orchestration/dag-engine.ts:16、sources/manager/src/orchestration/dag-engine.ts:18、sources/manager/src/orchestration/dag-engine.ts:20、sources/manager/src/orchestration/dag-engine.ts:23 分别定义节点状态、整张图、交接记录、依赖满足集合、邮箱与失败端口;这些结构让系统可以回答“哪个节点能跑、为什么没跑、失败后流向哪里”,而不是依赖模型在自然语言里自行记忆。

执行面也没有和控制面揉成一团:sources/manager/src/runtime/active-runs.ts:61、sources/manager/src/runtime/active-runs.ts:69、sources/manager/src/runtime/active-runs.ts:73、sources/manager/src/runtime/active-runs.ts:74、sources/manager/src/runtime/active-runs.ts:75、sources/manager/src/runtime/active-runs.ts:76 把运行状态、预算计数和节点会话收进 ActiveRun,sources/worker/src/prompt-runner.ts:167、sources/worker/src/prompt-runner.ts:174、sources/worker/src/prompt-runner.ts:182、sources/worker/src/prompt-runner.ts:192、sources/worker/src/prompt-runner.ts:228、sources/worker/src/prompt-runner.ts:256 则在 Worker 内创建 DAG 工具、审计器、流式通道和具体模型客户端。Manager 决定何时调度,Worker 只执行一轮并通过 handoff 或 node_error 回报,职责切分清楚。

▸核心数据

架构模式
Manager 控制面 + Node 容器生命周期 + Worker 执行面 + Protocol 契约层;DAG 通过端口和 mailbox 传递数据。
核心抽象
DAGRun / ActiveRun / DispatchEnvelope / AgentSession;状态、预算、会话和审计互相独立但由 runId 串联。
性能/精度数据
仓库约 85K 行 TypeScript/JavaScript、538 个文件;本次关注状态推进、调度与审计,而非宣称模型精度。
下游依赖
Claude SDK、Codex App Server、Kimi Code、多种容器 Provider、WebSocket 控制通道和本地 Voice UI。
本文导航 — 6 个章节依次:概述 → 架构图 → 核心模块 → 关键流程 → 测试与验证 → 风险。子页面在每个 H3 末尾以 深度阅读 → 入口展开。
§ 01 · ARCHITECTURE

四层控制面与数据面

从源码关系看,UI/Voice 只负责把意图交给 Manager;Manager 在 sources/manager/src/runtime/active-runs.ts:336、sources/manager/src/runtime/active-runs.ts:341、sources/manager/src/runtime/active-runs.ts:357、sources/manager/src/runtime/active-runs.ts:360、sources/manager/src/runtime/active-runs.ts:365、sources/manager/src/runtime/active-runs.ts:377 创建运行、解析限制并发布 READY 事件,随后 Node 层按能力启动 Worker。Worker 的 sources/worker/src/prompt-runner.ts:193、sources/worker/src/prompt-runner.ts:207、sources/worker/src/prompt-runner.ts:231、sources/worker/src/prompt-runner.ts:238、sources/worker/src/prompt-runner.ts:239、sources/worker/src/prompt-runner.ts:240 把文本流、调试流和会话身份封装回 WebSocket,因此前端观察到的是结构化事件,而不是直接耦合某一家模型 SDK。

▸系统拓扑

Voice / Web UI→Manager API→DAG ActiveRun→Node Provider→Worker + Agent Backend
JSONL Audit← events / tools / usage ←WebSocket Event Bus

▸模块清单

协议层规定消息与工具契约;Manager 维护 DAG 和运行预算;Node 管容器及挂载;Worker 适配具体模型并产出审计证据。四层都能独立测试,故障不会被一个巨型服务吞掉。

homerail_protocol
跨进程消息、Agent 配置、DAG 端口与工具 schema。
homerail_manager
图验证、ActiveRun、调度、恢复和事件总线。
homerail_worker / node
模型后端、工具执行、审计以及容器生命周期。

▸关键数据结构

src/homerail_manager/src/orchestration/dag-engine.ts · L3-21TypeScript
3export type NodeState =
4  | "PENDING"
5  | "READY"
6  | "RUNNING"
7  | "COMPLETED"
8  | "FAILED"
9  | "CANCELLED"
10  | "SKIPPED";
11
12export interface DAGRun {
13  runId: string;
14  graph: DAGGraphData;
15  loopSources: Set<string>;
16  nodeStates: Map<string, NodeState>;
17  handoffedNodes: Set<string>;
18  afterSatisfied: Map<string, Set<string>>;
19  inputSatisfied: Map<string, Set<string>>;
20  mailboxes: Map<string, Map<string, unknown[]>>;
21}
深度阅读 · DAG 状态机与调度预算
§ 02 · CORE MODULES

DAG 引擎与 Worker 执行环

▸状态晋升不是“上游结束就跑”,还要确认路由真的命中

节点初始状态由 after_dep 决定,sources/manager/src/orchestration/dag-engine.ts:65、sources/manager/src/orchestration/dag-engine.ts:67、sources/manager/src/orchestration/dag-engine.ts:70、sources/manager/src/orchestration/dag-engine.ts:76、sources/manager/src/orchestration/dag-engine.ts:77、sources/manager/src/orchestration/dag-engine.ts:83 初始化每个节点的状态与邮箱;真正晋升时,sources/manager/src/orchestration/dag-engine.ts:106、sources/manager/src/orchestration/dag-engine.ts:109、sources/manager/src/orchestration/dag-engine.ts:110、sources/manager/src/orchestration/dag-engine.ts:113、sources/manager/src/orchestration/dag-engine.ts:120、sources/manager/src/orchestration/dag-engine.ts:125 同时检查依赖是否完成、显式输入边是否被选中。这个双重条件避免条件分支没有走到某节点时,节点仍因“所有前驱都结束”而误启动。

homerail_manager/src/orchestration/dag-engine.ts · L234-260TypeScript
234export function handoff(
235  run: DAGRun,
236  fromNode: string,
237  port: string,
238  content: unknown,
239): DAGTransitionResult {
240  if (!run.nodeStates.has(fromNode)) {
241    throw new Error(`Unknown node: ${fromNode}`);
242  }
243
244  const matchingDownstream = run.graph.edges.filter(
245    (edge) =>
246      edge.from_node === fromNode &&
247      edge.label !== "after_dep" &&
248      edge.to_node !== "" &&
249      edgeMatchesHandoff(edge, port),
250  );
251  const terminalFailure = isFailurePort(port) && matchingDownstream.length === 0;
252  run.handoffedNodes.add(fromNode);
253  run.nodeStates.set(
254    fromNode,
255    terminalFailure
256      ? "FAILED"
257      : run.loopSources.has(fromNode)
258        ? "RUNNING"
259        : "COMPLETED",
260  );

src/homerail_manager/src/orchestration/dag-engine.ts · L106-126TypeScript
106function _tryPromote(run: DAGRun, nodeId: string): void {
107  if (run.nodeStates.get(nodeId) !== "PENDING") return;
108  const deps = _incomingAfterDeps(run.graph.edges, nodeId);
109  const satisfiedDeps = run.afterSatisfied.get(nodeId);
110  const satisfied = deps.every((e) => satisfiedDeps?.has(e.from_node));
111  if (!satisfied) return;
112
113  const routedInputs = run.inputSatisfied.get(nodeId);
114  for (const dep of deps) {
115    const explicitFromDep = _incomingExplicitEdgesFrom(
116      run.graph.edges,
117      nodeId,
118      dep.from_node,
119    );
120    if (explicitFromDep.length > 0 && !routedInputs?.has(dep.from_node)) {
121      return;
122    }
123  }
124
125  run.nodeStates.set(nodeId, "READY");
126}

▸Worker 把模型事件折叠为统一流,并把“未 handoff”视为错误

Worker 在 sources/worker/src/prompt-runner.ts:228、sources/worker/src/prompt-runner.ts:229、sources/worker/src/prompt-runner.ts:230、sources/worker/src/prompt-runner.ts:231、sources/worker/src/prompt-runner.ts:243、sources/worker/src/prompt-runner.ts:249 校验模型协议、创建适配器并先保存去密后的会话配置;事件循环从 sources/worker/src/prompt-runner.ts:256、sources/worker/src/prompt-runner.ts:258、sources/worker/src/prompt-runner.ts:263、sources/worker/src/prompt-runner.ts:291、sources/worker/src/prompt-runner.ts:303、sources/worker/src/prompt-runner.ts:312 将文本、调试、工具调用和工具结果分别发往 UI、transcript 与 tool-events。若循环正常结束但 DAG 工具没有 yielded,sources/worker/src/prompt-runner.ts:352、sources/worker/src/prompt-runner.ts:355、sources/worker/src/prompt-runner.ts:360、sources/worker/src/prompt-runner.ts:364 会主动发送 node_error,阻止“模型说完了但图没有前进”的假成功。

homerail_worker/src/prompt-runner.ts · L228-260TypeScript
228  try {
229    assertAgentRuntimeProtocol(agentBackend, job.llmProtocol);
230    const agent = createAgentClient(agentBackend);
231    const context: AgentRunContext = {
232      systemPrompt: job.systemPrompt,
233      provider: job.llmProvider,
234      protocol: job.llmProtocol,
235      model: job.dagConfig.model,
236      apiKey: job.llmApiKey ?? process.env.LLM_API_KEY ?? "",
237      baseUrl: resolveAgentBaseUrl(job, agentBackend),
238      workspace: process.env.WORKSPACE ?? process.cwd(),
239      sessionId: job.dagConfig.session_id ?? job.runId,
240      abortSignal: deps.abortSignal,
241    };
242    try {
243      saveSession({
244        sessionId,
245        runId: job.runId,
246        nodeId: job.dagConfig.node_id,
247        messages: [{ role: "user", content: job.task }],
248        toolCallState: { inFlight: false },
249        agentConfig: redactAgentContext(context),
250        timestamp: Date.now(),
251      });
252    } catch {
253      // Best-effort.
254    }
255    let errorMessage: string | null = null;
256    for await (const event of agent.run(job.task, dagTools, context)) {
257      switch (event.type) {
258        case "text":
259          sendContent(event.text);
260          audit?.transcript.write({ event: "text", text: event.text });

src/homerail_worker/src/prompt-runner.ts · L167-190TypeScript
167  // Create DAG tools state
168  const dagState = createDagToolsState(job.dagConfig, job.runId, wsSend);
169  const dagTools = createDagTools(dagState);
170  const unregisterInboxHandler = deps.registerInboxHandler?.((content) => {
171    deliverInbox(dagState, content);
172  });
173
174  // Create audit writers
175  let audit: AuditWriters | null = null;
176  try {
177    audit = createAuditWriters(job.runId, auditDir);
178  } catch {
179    // Non-fatal — audit is best-effort
180  }
181
182  // Write initial transcript entry
183  audit?.transcript.write({
184    event: "prompt_start",
185    node_id: job.dagConfig.node_id,
186    run_id: job.runId,
187    sender: job.sender,
188    task_preview: job.task.slice(0, 500),
189  });
190  appendSessionTranscript("prompt_start", { task_preview: job.task.slice(0, 500) });
深度阅读 · 审计证据、校验和与会话恢复
§ 03 · KEY FLOW

从 READY 到可审计完成

一轮执行的关键链条是 READY → 构造 DispatchEnvelope → Provider 发送 → RUNNING → Worker 事件流 → handoff → 下游 mailbox。sources/manager/src/runtime/active-runs.ts:1343、sources/manager/src/runtime/active-runs.ts:1344、sources/manager/src/runtime/active-runs.ts:1355、sources/manager/src/runtime/active-runs.ts:1357、sources/manager/src/runtime/active-runs.ts:1360、sources/manager/src/runtime/active-runs.ts:1375 把输入、稳定 sessionId、Agent 配置和能力要求装进 envelope;sources/manager/src/runtime/active-runs.ts:1389、sources/manager/src/runtime/active-runs.ts:1398、sources/manager/src/runtime/active-runs.ts:1407、sources/manager/src/runtime/active-runs.ts:1412、sources/manager/src/runtime/active-runs.ts:1420、sources/manager/src/runtime/active-runs.ts:1445 再逐个调度 READY 节点、执行预算门禁、处理一次瞬时重试并最终切到 RUNNING。

▸成功路径:显式端口驱动下游

READY→ build envelope →dispatcher→RUNNING→ handoff(port)mailbox→next READY

▸失败路径:纠正预算优先,耗尽后再自动交接或终止

node_error→correction mailbox→ retryREADY→ exhaustedauto handoff / FAILED
深度阅读 · 交接、条件分支与瞬时调度重试
§ 04 · TESTS & VALIDATION

纠正、重试与恢复测试

仓库不是只测 happy path。纠正测试从 sources/manager/tests/dag-correction-retry.test.ts:76、sources/manager/tests/dag-correction-retry.test.ts:77、sources/manager/tests/dag-correction-retry.test.ts:81、sources/manager/tests/dag-correction-retry.test.ts:84、sources/manager/tests/dag-correction-retry.test.ts:88、sources/manager/tests/dag-correction-retry.test.ts:90 验证缺少 handoff 时节点回到 READY 并收到 correction 输入;sources/manager/tests/dag-correction-retry.test.ts:93、sources/manager/tests/dag-correction-retry.test.ts:99、sources/manager/tests/dag-correction-retry.test.ts:101、sources/manager/tests/dag-correction-retry.test.ts:103、sources/manager/tests/dag-correction-retry.test.ts:105 验证纠正次数耗尽后的自动成功端口;sources/manager/tests/dag-correction-retry.test.ts:108、sources/manager/tests/dag-correction-retry.test.ts:111、sources/manager/tests/dag-correction-retry.test.ts:113、sources/manager/tests/dag-correction-retry.test.ts:117、sources/manager/tests/dag-correction-retry.test.ts:118、sources/manager/tests/dag-correction-retry.test.ts:119 则确认可重试派发失败只重试一次并记录计数。

▸测试覆盖

测试还明确区分瞬时故障和能力不匹配:sources/manager/tests/dag-correction-retry.test.ts:122、sources/manager/tests/dag-correction-retry.test.ts:125、sources/manager/tests/dag-correction-retry.test.ts:127、sources/manager/tests/dag-correction-retry.test.ts:128、sources/manager/tests/dag-correction-retry.test.ts:131、sources/manager/tests/dag-correction-retry.test.ts:135、sources/manager/tests/dag-correction-retry.test.ts:136 用 retryable:false 模拟找不到满足能力的 Worker,要求不重试、运行失败、节点进入 FAILED。这个断言与 sources/manager/src/runtime/active-runs.ts:1421、sources/manager/src/runtime/active-runs.ts:1422、sources/manager/src/runtime/active-runs.ts:1423、sources/manager/src/runtime/active-runs.ts:1439、sources/manager/src/runtime/active-runs.ts:1440 的生产逻辑对应,避免把确定性配置错误伪装成网络抖动反复消耗预算。

tests/dag-correction-retry.test.ts · L76-105TypeScript
76  it("reschedules a node with a correction prompt before failing the run", () => {
77    const parsed = parseDAGYaml(correctionYaml(1));
78    createActiveRun("run-correction", parsed);
79    const dispatcher = new FlakyDispatcher({ status: "dispatched", targetType: "fake", targetId: "first" });
80
81    expect(dispatchReadyNodes("run-correction", dispatcher)).toBe(1);
82    expect(getActiveRun("run-correction")?.dagRun.nodeStates.get("start")).toBe("RUNNING");
83
84    const correction = requestNodeCorrection("run-correction", "start", "agent ended without DAG handoff");
85    expect(correction.status).toBe("scheduled");
86    expect(getActiveRun("run-correction")?.dagRun.nodeStates.get("start")).toBe("READY");
87
88    expect(dispatchReadyNodes("run-correction", dispatcher)).toBe(1);
89    expect(dispatcher.calls).toHaveLength(2);
90    expect(dispatcher.calls[1].inputs.correction?.[0]).toContain("agent ended without DAG handoff");
91  });
92
93  it("auto-handoffs on a success port after correction attempts are exhausted", () => {
94    const parsed = parseDAGYaml(correctionYaml(1));
95    createActiveRun("run-auto-handoff", parsed);
96    const dispatcher = new FlakyDispatcher({ status: "dispatched", targetType: "fake", targetId: "first" });
97    dispatchReadyNodes("run-auto-handoff", dispatcher);
98
99    expect(requestNodeCorrection("run-auto-handoff", "start", "missing handoff").status).toBe("scheduled");
100    dispatchReadyNodes("run-auto-handoff", dispatcher);
101    expect(requestNodeCorrection("run-auto-handoff", "start", "missing handoff again").status).toBe("exhausted");
102
103    const run = autoHandoffAfterCorrectionExhausted("run-auto-handoff", "start", "missing handoff again");
104    expect(run?.status).toBe("completed");
105    expect(run?.dagRun.nodeStates.get("start")).toBe("COMPLETED");

▸已知边界 / 失败模式

  • 条件边未命中时节点必须 SKIPPED,而不是因 after_dep 满足被误唤醒。
  • Agent 正常结束但没有 handoff 会触发 correction;次数受 max_corrections_per_node 限制。
  • 瞬时派发错误最多重试一次;能力不匹配等确定性失败不重试。
  • Manager 冷重启会重放 handoff 历史,但孤儿 RUNNING 节点会被降级为失败。
§ 05 · HIGHLIGHTS

值得借鉴的工程亮点

显式状态机
节点可达性、条件分支、循环唤醒和终止条件均由代码推进,不把控制权交给模型措辞。
预算化容错
dispatch、handoff、edge traversal、correction 都有上限,避免自治循环无限扩张。
双轨证据
transcript 和 tool-events 分离写入,结束时为 transcript 写校验和,可检查证据是否完整。
可替换模型后端
Worker 在统一事件协议下适配 Claude、Codex App Server、Kimi 等运行时。
§ 06 · RISKS & GAPS

技术亮点之外,别忽略这些

HomeRail 仍是 2026 年 7 月刚创建的早期项目。sources/worker/src/audit/index.ts:57、sources/worker/src/audit/index.ts:63、sources/worker/src/audit/index.ts:70、sources/worker/src/audit/index.ts:77、sources/worker/src/audit/index.ts:79、sources/worker/src/audit/index.ts:85 能检查审计文件、校验和 sidecar 与工具事件,但 PromptRunner 的审计创建在 sources/worker/src/prompt-runner.ts:174、sources/worker/src/prompt-runner.ts:177、sources/worker/src/prompt-runner.ts:178、sources/worker/src/prompt-runner.ts:179 被明确设计为 best-effort:审计器初始化失败不会阻断任务。这有利于可用性,却意味着“任务完成”不自动等于“证据完整”,生产环境必须额外把 completeness 结果纳入发布门禁。

早期 API 变化
协议标记 v0.1.x,模块数量大且变化快,集成方应锁定提交与 schema。
本地密钥与挂载
多 Provider、容器和外部模型意味着密钥、workspace mount 与网络策略需要独立审计。
Best-effort 审计
审计写入失败不阻止 Agent 继续;强合规场景要把证据缺失升级为硬失败。
恢复语义
冷恢复会把孤儿 RUNNING 节点判失败;长任务需明确是否允许自动补偿或人工接管。
诚实记录 — 这部分列的是我在源码里看到、但作者没有明确文档化的限制。读者请按需取舍。