Appearance
Run 真正怎样从 API 跑到 Worker:Dispatch、Lease 与 Lifecycle Hooks
前置:运行入口与调试面:API、RunManifest 与 Developer Console
核心问题:API 创建 Run 后,任务怎样真正 Dispatch 到 Worker 并获得 ownership,Queue/Lease/Hook 各自在哪一层生效?
架构图里最容易缺的那一段
很多 Harness 架构图会画到:
text
API → Runtime → Tool单进程 demo 可以这样跑;真正服务化以后却还差一段关键路径:谁把可运行工作交给 Worker,谁判断 Worker 仍拥有它,Worker 崩溃后谁接手。
最终的生产路径更接近:
text
API command
↓
RunService / StateStore
↓ state says work is eligible
Scheduler / Dispatcher
↓ queue notification
Worker
↓ claim lease
Runtime + Executor
↓ durable result/event
StateStoreQueue 负责“提醒有工作”,StateStore/lease 负责“谁有权做这份工作”。两者不要混成一个概念。
Command Path 与 Work Dispatch 分开
用户的 create/cancel/resume/approve 是 command,它们先修改 durable Runtime facts;Worker dispatch 是这些 facts 变化后的执行结果。
例如取消 Run:
text
POST /runs/123/cancel
→ persist CANCEL_REQUESTED
→ scheduler stops issuing new work
→ active worker observes cancellation
→ child Tool cancellation / cleanup
→ persist CANCELLED不要让 API handler 直接找到某个 worker coroutine 并调用 .cancel()。那会把系统正确性绑在单进程内存对象上。
Queue 是 Notification,不是 Canonical State
可以把 Node identity 放进 Queue:
json
{"run_id":"r1","node_id":"n7"}Worker 收到后仍然先:
text
load NodeState
→ verify READY/retry eligibility/deadline
→ try_claim lease
→ execute only if claim succeeds因此消息重复、延迟、乱序时,Runtime 仍能依赖 durable state 做正确判断。Queue 丢一条通知则可以由 periodic scanner/outbox 重新发现 eligible work。
Worker Topology 不应改变 Domain Contract
本地开发可以:
text
API process = Scheduler = Worker生产环境可以拆成:
text
API replicas
Scheduler/dispatcher
Tool workers
Sandbox workers
background reconciliation workers无论怎么拆,RunState / NodeState / ToolExecutionResult / operation_id 等 domain contract 不应改变。拓扑是 deployment concern,不应渗透成两套 Runtime 语义。
Lifecycle Hooks 是 Observer / Extension Point
Harness 还需要在模型、Tool、handoff、Run lifecycle 周围挂 tracing、usage、audit、prefetch 等扩展。可以定义:
python
class RunHooks(Protocol):
async def on_model_start(self, ctx, request): ...
async def on_model_end(self, ctx, response): ...
async def on_tool_start(self, ctx, call): ...
async def on_tool_end(self, ctx, result): ...
async def on_transition(self, ctx, event): ...Hooks 的重要边界是:默认观察,不拥有 Runtime state transition。
Tracing exporter 失败不应把一个已经成功的 Tool 改成 FAILED;一个可选 analytics hook 超时也不应阻塞 checkpoint。真正会改变权限、输入或执行资格的逻辑应进入显式 Policy/Guardrail/Runtime step,而不是藏在“观察 hook”里。
Hook 也要有失败策略
不同 Hook 不能一概而论:
- telemetry/analytics:通常 fail-open + bounded timeout;
- audit/security evidence:可能要求 fail-closed 或写 durable outbox;
- prefetch/cache warmup:失败可忽略;
- policy mutation:不应伪装成 observer hook,应使用明确 contract。
这样调用方看到接口就能知道失败是否会改变 Run。
Failure Modes
Queue 消息就是执行权
重复 delivery 会直接产生重复 Worker。
处理: queue → load state → claim lease;有效 owner 才能执行和提交。
Hook 可以随意改 RunState
Observability 或插件故障会变成 Runtime correctness 故障,而且很难从代码路径看出谁改了状态。
处理: observer hook 与 policy hook 类型/接口分开;state transition 只经过 Runtime command/reducer。
如何验证
| 场景 | 必须成立 |
|---|---|
| 同一 queue message 投递两次 | 只有一个 Worker 获得有效 lease |
| Worker claim 后 crash | lease 失效后其他 Worker 能继续 |
| telemetry hook 抛异常 | 按配置降级,已提交 Tool result 不被篡改 |
| cancel API 与 worker 不在同一进程 | cancellation 仍通过 durable state 传播 |
自测
- API 接收 Run 后进程立刻重启,怎样通过 dispatch/queue 让任务仍能被 Worker 接管,而不是绑定在 HTTP 进程里?
- Worker lease 过期后新 Worker 接管,旧 Worker 的 lifecycle hook 晚到时应怎样被 fencing 拒绝?