feat(hitl): 增量3b —— 持久决定投递 + 生产激活(HITL 中断/恢复上线)

把中断/恢复模型在 dispatcher 接线启用,并让审批决定持久化抗离线。至此 HITL
从「阻塞 goroutine 等 5min、core NATS 非持久、不抗重启」升级为「持久化中断 +
决定持久投递 + 从 checkpoint 恢复续跑」。

- contract: 新增审批决定流 StreamApprovals(SUNDYNIX_APPROVALS)/通配
  SubjectApprovalAll/消费者 ConsumerApprovals + checkpoint 桶 BucketCheckpoints。
- bus: EnsureApprovalStream(JetStream 流持久捕获 sundynix.approval.>,MaxAge 24h;
  gateway 现有 nc.Publish 的决定被本流自动捕获,无需改 gateway)+ ConsumeApprovals
  (持久消费者,队列组多副本安全)。
- orchestrator: HandleApprovalDecision(据 task_id 取 resume 记录续跑;无记录则忽略,
  兼容阻塞态任务的决定 + 决定重投幂等)+ finishResumed(收尾对齐 Handle 尾段:
  中断/拒绝/预算/失败/成功+评测落历史)。
- main: 开 checkpoint 存储 + 审批流 + 起决定消费者 → SetCheckpoints 启用中断模型;
  任一步失败优雅降级回阻塞模型;停机 drain 在途 resume。

决定经 JetStream 持久:dispatcher 在决定发出时离线,重连后仍消费到并续跑;任一
dispatcher 副本都能据共享 KV 的 checkpoint+记录恢复(HA)。
测试:决定驱动恢复(续跑下游+判 done+清记录)、无记录忽略(幂等/兼容)。
全模块 go build + go test 绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-29 13:08:41 +08:00
parent 368938ce89
commit 515cf7f87a
6 changed files with 217 additions and 2 deletions
@@ -102,6 +102,22 @@ func (s *Subscriber) WaitApproval(ctx context.Context, taskID string, timeout ti
return s.inner.WaitApproval(ctx, taskID, timeout)
}
// Checkpoints 打开 HITL 持久化中断的 KV 桶(compose checkpoint + resume 记录)。
// 返回的 *KVHandle 结构化满足 eino.CheckpointKVGet/Put/Delete),可直接 SetCheckpoints。
func (s *Subscriber) Checkpoints(ctx context.Context, bucket string, ttl time.Duration) (*sharedbus.KVHandle, error) {
return s.inner.Checkpoints(ctx, bucket, ttl)
}
// EnsureApprovalStream 确保审批决定流存在(中断/恢复模型用,决定持久抗 dispatcher 离线)。
func (s *Subscriber) EnsureApprovalStream(ctx context.Context) error {
return s.inner.EnsureApprovalStream(ctx)
}
// ConsumeApprovals 持久消费审批决定,每条交给 h 驱动 resume(队列组多副本安全)。
func (s *Subscriber) ConsumeApprovals(ctx context.Context, h func(context.Context, *contract.ApprovalDecision)) (func(context.Context), error) {
return s.inner.ConsumeApprovals(ctx, h)
}
// RequestModelConfig 向控制面(Gateway)取当前激活的对话模型配置。
func (s *Subscriber) RequestModelConfig(ctx context.Context) (*contract.ModelConfig, error) {
return s.inner.RequestConfig(ctx, contract.ConfigKindChat)