跳到正文
Hacker News· siddarthareddy8·· 8 天前AI 评分62

DHP v1.3 发布:让智能体任务在 kill -9 后仍可续跑

DHP – agent work that survives kill -9 (v1.3)

AI 导读

DHP(Durable Handoff Protocol)发布 v1.3,把工作所有权与 worker 身份分离,worker 被 kill -9、OOM 或抢占式实例回收后,备用 worker 可从最近检查点恢复,作者称返工不超过 1 个检查点。

正文 · AI 翻译

PyPI version Python versions License: MIT Tests Conformance

在 worker 死亡后仍能存活的 agent 工作。 当某个 worker 被 kill -9、被 OOM 杀死,或它的 spot 实例在运行中途被回收时,DHP 会把工作交给一个备用 worker,从最后一个检查点恢复——零返工,零结果丢失。

MCP 标准化了 agent↔工具。A2A 标准化了 agent↔agent。DHP 是缺失的那一层:agent↔时间——比任何单个 worker 都更长寿的执行状态。

pip install dhp-protocol

一行安装(pip + MCP 注册 + 数据目录):

curl -fsSL https://raw.githubusercontent.com/SIDDARTHAREDDY8/dhp/main/install.sh | bash

直接接入你的 agent:

工具 命令
Claude Code claude mcp add dhp -- dhp-mcp --root ~/.dhp
Cursor 一键安装
Windsurf / Zed 从 MCP_SETUP.md 复制 JSON
任意 MCP 客户端 dhp-mcp --root ~/.dhp(stdio)

其他运行方式: Docker(docker compose up -d 用于 supervisor + transport server)· 从源码构建(git clone + pip install -e ".[dev]")


目录


问题所在

你把一个 20 分钟的任务委派给一个 AI agent。在第 14 分钟时,进程被杀——OOM、spot 回收、部署、笔记本合盖。全部 14 分钟的进度都没了。 agent 从零重启,重做一切,消耗两倍的 token。

如今每个 agent 框架都有同一个漏洞:工作归属于 worker,而不是交接。 当 worker 死亡时,工作也随之死亡。重试从零开始。没有任何原语能表达“这个工作单元独立于当前正在执行它的任何人而存在”。

DHP 就是那个原语。

DHP 是什么

DHP(Durable Handoff Protocol,持久化交接协议)将工作所有权与worker 身份分离:

  • 工作以内容寻址的交接形式被派发——一份关于意图的持久记录(任务、输入、输出 schema、预算)。
  • Worker 认领交接,持有有时间限制的租约,并在进行过程中设置检查点。
  • 一个 supervisor 监视租约。一个沉默的 worker 会在约 6 秒内被宣告死亡;它的交接被孤立,并递增一个单调的栅栏令牌——已死 worker 的迟到写入会被拒绝,因此两个 worker 永远不可能都认为自己拥有该工作。
  • 任何备用 worker 都会从最后一个检查点恢复被孤立的交接。零返工。
  • 检查点在生成时通过 TCP 发送,因此即使整个主机死亡,对端也已经拥有全部内容。

它与现有技术栈组合,而不是取代它:

层 协议 关注点
工具 MCP agent↔工具调用
Agent A2A agent↔agent 消息传递
持久性 DHP 工作在 worker 死亡后仍存活

五项保证

  1. 不丢失结果——每个已完成交接的结果都是持久的且可检索。
  2. 不交叉输入——交接 ID 基于意图进行内容寻址;输入无法在运行中途被调换。
  3. 无未类型化结果——完成结果在接受前会依据 JSON Schema 进行验证。
  4. 无静默预算超支——对步数、花费、墙钟时间和尝试次数设置硬上限。耗尽的工作会停放在死信队列中:完好、可见、绝不静默丢弃。
  5. 无运行器锁定 — 可在任意主机、任意运行器上暂停 → 交付 → 恢复。传输格式为纯 JSON-lines。

快速开始

import dhp

store = dhp.Store("./demo-dhp")

# 1. Dispatch durable work
hid = dhp.dispatch(
    store,
    task_kind="demo.count",
    inputs={"target": 50},
    output_schema={"type": "object",
                   "properties": {"counted": {"type": "integer"}},
                   "required": ["counted"]},
)

# 2. Work it — the context manager heartbeats automatically in the background
with dhp.claim(store, hid, worker_id="w1") as ctx:
    state = dhp.last_checkpoint(store, hid) or {"n": 0}
    while state["n"] < 50:
        state["n"] += 1
        ctx.checkpoint(state)          # every step is a resume point
    ctx.complete({"counted": state["n"]})

现在在运行中途终止工作进程,观察恢复过程:

# terminal 1: supervisor watches leases, orphans the silent in ~7s
dhp-supervisor ./demo-dhp sup1 3600
# terminal 2: any standby picks up exactly where the dead worker stopped
with dhp.recover(store, hid, worker_id="w2") as ctx:
    state = dhp.last_checkpoint(store, hid)   # {"n": 20} — resumes here
    while state["n"] < 50:
        state["n"] += 1
        ctx.checkpoint(state)
    ctx.complete({"counted": state["n"]})

已完成的步骤绝不会重新计算。完整 5 分钟操作指南见 QUICKSTART.md。

工作原理

租约与隔离

工作进程在有时限的租约下持有交接任务(默认:2 秒心跳间隔,3 次未响应 ≈ 6 秒 TTL)。心跳在后台线程中运行 — 检查点之间缓慢的 LLM 推理绝不会错误地将健康的工作进程判定为孤儿。

当监督者发现租约过期时,它会将交接任务孤立:所有权转移给墓碑持有者,并且单调递增的隔离令牌递增。后续每次写入都携带工作进程的隔离令牌;过期工作进程的延迟检查点会被拒绝,并返回 LeaseLost。这与 Chubby、ZooKeeper 和 etcd 使用的隔离模式相同 — 无需分布式共识即可实现精确一次所有权。

十个同时恢复者 → 恰好一个胜出(原子比较并交换)。

内容寻址信封

交接 ID 是对意图(任务类型、输入、输出模式、预算)的 SHA-256 哈希,而非可变状态。恢复的交接任务可验证为与派发时相同的工作。被篡改的信封和检查点在接收时会被拒绝。

仅追加传输

检查点被追加到 JSON-lines 日志中 — 一个信封头,然后每个检查点一行。两种传输方式:

  • 文件(dhp.transport):共享目录或卷。
  • TCP(dhp.net):对等端使用 TransportServer,工作进程中使用 NetShipper。检查点在生成时即到达;服务器验证信封身份和每个检查点的哈希。客户端也会在本地缓冲,因此网络分区不会丢失任何内容 — 重新连接时会重放。

任何失败后的重做上限:≤ 1 个检查点。

可插拔存储

所有持久化都通过 StoreBackend 接口进行。默认是 SQLite/WAL — 零依赖、崩溃安全、单写入者。Postgres(咨询锁 + FOR UPDATE SKIP LOCKED 提供相同的 CAS 语义)是干净的扩展,而非重写。

终止演示

证明这一点的演示 — 不是模拟:

python3 demo/run_mad_demo.py

会发生什么:

  1. 主机 A:工作进程获取 60 个真实维基百科页面,在获取每个页面时通过 TCP 向 主机 B 发送检查点。
  2. 在第 20 页时:在工作进程上执行 kill -9。真实的 SIGKILL,在 urlopen 中途。
  3. 监督者在 6.5 秒内将交接任务孤立。
  4. 主机 B 从其 TCP 日志中接收 20 个已发送的检查点。
  5. 主机 B 上的备用进程从第 20 页恢复 — 第 1–20 页永远不会重新获取。
  6. 60/60 完成。磁盘上有真实的 CSV 数据集。
=== MAD DEMO: 60 real pages, kill -9 at 20 ===
worker-A started (pid 4414)
*** kill -9 worker-A at 20 pages ***
supervisor orphaned after 6.5s
host B ingested: 20 checkpoints, 0 skipped
host B resumes from page 20 (worker-A died at 20)
worker-B started (pid 4444)
=== DONE: 60/60 pages, 60 fetched OK, zero refetched ===
MAD DEMO: GREEN

一个网络混沌测试工具(demo/net_chaos.py)在多轮中于发送中途对工作进程执行 SIGKILL:24/24 个检查点到达对等端,零重做。

架构

flowchart TB
    subgraph Interfaces
        MCP[dhp-mcp<br/>8 MCP tools]
        SDK[Python SDK<br/>dhp.dispatch/claim/recover]
        A2A[A2A bridge<br/>stable Task ID]
    end
    subgraph Core
        R[Runner<br/>lifecycle: dispatch → claim →<br/>checkpoint → complete]
        S[Supervisor<br/>lease watchdog → orphan<br/>leader election]
    end
    subgraph Durability
        SB[StoreBackend<br/>interface]
        SQ[(SQLite/WAL<br/>default)]
        PG[(Postgres<br/>extension)]
    end
    subgraph Portability
        NET[TCP transport<br/>live checkpoint shipping]
        LOG[JSON-lines log<br/>verified ingest]
    end
    MCP --> R
    SDK --> R
    A2A --> R
    R --> SB
    S --> SB
    SB --> SQ
    SB --> PG
    R --> NET
    NET --> LOG

加载中

模块图(dhp/):

模块 职责
envelope.py 内容寻址的交接信封、身份验证
store.py SQLite/WAL StoreBackend — 线程安全、模式版本化
backend.py StoreBackend 接口(CAS 所有权、隔离、检查点、领导权)
runner3.py 生命周期:派发/认领/恢复/检查点/完成;TaskContext 带自动心跳
supervisor3.py 租约看门狗、孤立、DLQ、领导者选举、指标
transport.py 仅追加 JSON-lines 日志:发送/接收带哈希验证
net.py TCP 传输:TransportServer + NetShipper、令牌认证、重连
mcp_server.py MCP 服务器(8 个工具 + 健康检查)
a2a_bridge.py A2A 持久化基础
config.py / errors.py / validate.py / log.py 调优、类型化错误、输入验证、结构化日志

API 参考

Python SDK

import dhp

store = dhp.Store("./data")                        # or any StoreBackend

hid = dhp.dispatch(store, task_kind="...",          # create durable work
                   inputs={...},
                   output_schema={...},
                   budget={"max_attempts": 5})      # optional caps

with dhp.claim(store, hid, worker_id="w1") as ctx: # claim + auto-heartbeat
    state = dhp.last_checkpoint(store, hid) or {}   # resume point
    ctx.checkpoint(state)                           # per chunk
    ctx.heartbeat()                                 # manual (optional w/ ctx mgr)
    ctx.complete(result)                             # schema-validated
    ctx.release("reason")                            # cooperative handoff

with dhp.recover(store, hid, worker_id="w2") as ctx:# recover orphaned work
    ...

dhp.status(store, hid)                              # status/owner/checkpoints

跨主机传输

from dhp import TransportServer, NetShipper

# peer host:
TransportServer("./peer-logs", port=8471).serve_forever()

# worker host:
shipper = NetShipper("peer.example.com", 8471,
                     local_logpath="./ship-w1.log")  # partition buffer
shipper.ship_envelope(hid, envelope)
shipper.ship_checkpoint(hid, seq, state)             # verified server-side

CLI

命令 用途
dhp-supervisor <root> <id> [seconds] 租约看门狗(将沉默者孤立)
dhp-mcp --root <dir> MCP 服务器(stdio)
dhp-conform 10 项协议断言
dhp-chaos 随机化 SIGKILL 故障注入

MCP 工具

dhp_dispatch · dhp_claim · dhp_checkpoint · dhp_heartbeat · dhp_complete · dhp_recover · dhp_status · dhp_last_checkpoint · dhp_health

实测性能

不是宣称——是经过混沌测试的数字:

事件 实测
kill → 孤立 → 备用接管恢复 ~7s
双重 kill(worker + supervisor leader)→ 恢复 6.6s
主机被摧毁 → 对等节点摄取 TCP 日志 → 恢复 ~6s
任意 kill 后的返工 0 个检查点
20 个 agent 的 MCP 浸泡测试,75% worker 死亡率 20/20 完成
随机化 SIGKILL 混沌(14 次 worker + 5 次 supervisor kill) 所有不变量均保持

验证:45 个 pytest 测试(生命周期、并发 CAS 竞争、模糊测试、kill -9 完整性、网络传输)· dhp-conform 10/10 · 网络混沌 通过。

对比

DHP Temporal LangGraph CrewAI / AutoGen
Worker 死亡检测 ✅ 租约 + supervisor(~7s) ✅(~12s+) ❌ ❌
从检查点恢复,零返工 ✅ ✅ 部分(手动) ❌
跨主机,无共享数据库 ✅(TCP 日志传输) ❌(需要集群) ❌ ❌
隔离(拒绝过期 worker) ✅ 单调令牌 ✅ ❌ ❌
内容寻址的工作标识 ✅ ❌ ❌ ❌
MCP 原生 ✅ ❌ ❌ ❌
A2A 组合 ✅ ❌ ❌ ❌
零依赖单节点 ✅(SQLite) ❌(JVM 集群) ✅ ✅

DHP 不是工作流引擎——它是工作流引擎(以及 agent 框架)可以构建于其上的持久性原语。如果你在用 Temporal,继续用;DHP 面向的是 Temporal 触及不到的 agent 层。

MCP 与 A2A 集成

MCP —— 运行 dhp-mcp --root <dir> 并将任意 MCP 客户端指向它。skills/dhp-durable-work/SKILL.md 处的技能教会 agent 调度 → 检查点 → 恢复的模式。

A2A —— dhp.a2a_bridge 使 DHP 成为 A2A 任务之下的持久性基底:A2A 任务 ID 保持稳定,而 DHP 在其下方替换掉已死亡的 worker(填补 A2A 文档中记载的“无共识或全局事务语义”缺口)。参见 BRIDGE_DEMO.md:Agent B 在任务中途被 kill → Agent C 恢复 → 客户端看到 WORKING → COMPLETED,替换过程不可见。

路线图

  • Postgres StoreBackend 实现
  • TCP 传输的 TLS
  • 跨交接尝试的 OpenTelemetry 追踪
  • dhp dashboard —— 实时交接/租约/DLQ 可观测性
  • 多区域 supervisor 法定人数

贡献

欢迎 PR。标准是:每一个行为变更都需要一个混沌测试或一致性断言,在 kill -9 下证明它,而不仅仅是在顺利路径下。

git clone https://github.com/SIDDARTHAREDDY8/dhp
cd dhp
pip install -e ".[dev]"
python -m pytest tests/ -q   # 45 passed
dhp-conform                   # 10/10
python3 demo/run_mad_demo.py  # the kill demo

许可证

MIT —— 参见 LICENSE。

来源:Hacker News · github.com