DHP v1.3 发布:让智能体任务在 kill -9 后仍可续跑
DHP – agent work that survives kill -9 (v1.3)
DHP(Durable Handoff Protocol)发布 v1.3,把工作所有权与 worker 身份分离,worker 被 kill -9、OOM 或抢占式实例回收后,备用 worker 可从最近检查点恢复,作者称返工不超过 1 个检查点。
在 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 死亡后仍存活 |
五项保证
- 不丢失结果——每个已完成交接的结果都是持久的且可检索。
- 不交叉输入——交接 ID 基于意图进行内容寻址;输入无法在运行中途被调换。
- 无未类型化结果——完成结果在接受前会依据 JSON Schema 进行验证。
- 无静默预算超支——对步数、花费、墙钟时间和尝试次数设置硬上限。耗尽的工作会停放在死信队列中:完好、可见、绝不静默丢弃。
- 无运行器锁定 — 可在任意主机、任意运行器上暂停 → 交付 → 恢复。传输格式为纯 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
会发生什么:
- 主机 A:工作进程获取 60 个真实维基百科页面,在获取每个页面时通过 TCP 向 主机 B 发送检查点。
- 在第 20 页时:在工作进程上执行
kill -9。真实的 SIGKILL,在urlopen中途。 - 监督者在 6.5 秒内将交接任务孤立。
- 主机 B 从其 TCP 日志中接收 20 个已发送的检查点。
- 主机 B 上的备用进程从第 20 页恢复 — 第 1–20 页永远不会重新获取。
- 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