// turnbus.go 实现 TurnBus:进程内的极简 observer / pub-sub,作为"回合完成 / // 会话空闲"的内部事件原语。与 notify.Dispatcher(面向用户的外发通知)解耦—— // TurnBus 是给未来的编排层(auto-continue / gate / hand-off)订阅用的内部信号, // 不落库、不外发。 // // 关键不变量:Publish 必须永不阻塞 relay reader 热路径。订阅者以 fire-and-forget // 方式在独立 goroutine 中调用,并用 recover() 隔离 panic——一个慢/有 bug 的 // 订阅者不能拖垮协议 I/O。语义对齐 notify.Dispatcher 的非阻塞 fan-out。 package acp import ( "context" "fmt" "log/slog" "sync" "time" "github.com/google/uuid" ) // TurnEventKind 标识 TurnEvent 的类别。 type TurnEventKind string const ( // TurnCompletedEvent 表示一次回合正常完成(收到 session/prompt 响应的 stopReason)。 TurnCompletedEvent TurnEventKind = "turn_completed" // SessionIdleEvent 表示会话当前空闲(回合完成或被中止后),编排层可据此决策。 SessionIdleEvent TurnEventKind = "session_idle" ) // TurnEvent 是 TurnBus 上发布的事件载荷。RawStopReason 是 agent 上报的原始字符串 // (可能为空,例如 abort 路径),StopReason 是其归一化枚举。 type TurnEvent struct { SessionID uuid.UUID UserID uuid.UUID WorkspaceID uuid.UUID TurnIndex int Kind TurnEventKind StopReason StopReason RawStopReason string CompletedAt time.Time } // TurnBus 是回合事件的进程内 pub-sub 抽象。 type TurnBus interface { // Subscribe 注册一个具名订阅者。name 仅用于日志/排障。重复 name 会追加, // 不去重(与 notify 一致,调用方自行保证唯一)。 Subscribe(name string, fn func(context.Context, TurnEvent)) // Publish 把事件 fire-and-forget 派发给所有订阅者,永不阻塞调用方。 Publish(ctx context.Context, ev TurnEvent) } type deliver struct { ctx context.Context ev TurnEvent } type subscriber struct { name string fn func(context.Context, TurnEvent) ch chan deliver } // subBuffer 是每个订阅者的投递缓冲容量。 const subBuffer = 256 // defaultTurnBus 是 TurnBus 的默认实现:每个订阅者一个常驻 worker goroutine,从带缓冲 // channel 顺序消费事件——因此对同一订阅者保证 per-subscriber FIFO(turn_completed 必先于 // 同一次 Publish 序列里随后的 session_idle 到达)。panic 用 recover() 隔离。Publish 以非阻塞 // 方式投递(缓冲满则丢弃并告警),永不阻塞 relay reader 热路径。 type defaultTurnBus struct { mu sync.RWMutex subs []*subscriber log *slog.Logger } // NewTurnBus 构造默认 TurnBus。 func NewTurnBus(log *slog.Logger) TurnBus { if log == nil { log = slog.Default() } return &defaultTurnBus{log: log} } func (b *defaultTurnBus) Subscribe(name string, fn func(context.Context, TurnEvent)) { if fn == nil { return } s := &subscriber{name: name, fn: fn, ch: make(chan deliver, subBuffer)} b.mu.Lock() b.subs = append(b.subs, s) b.mu.Unlock() // 每个订阅者一个常驻 worker,顺序消费保证 FIFO。worker 与 app 同生命周期。 go b.worker(s) } func (b *defaultTurnBus) worker(s *subscriber) { for d := range s.ch { b.dispatch(s, d) } } func (b *defaultTurnBus) dispatch(s *subscriber, d deliver) { defer func() { if rec := recover(); rec != nil { b.log.Error("acp.turnbus.subscriber_panic", "subscriber", s.name, "session_id", d.ev.SessionID, "panic", fmt.Sprintf("%v", rec)) } }() s.fn(d.ctx, d.ev) } func (b *defaultTurnBus) Publish(ctx context.Context, ev TurnEvent) { b.mu.RLock() subs := b.subs b.mu.RUnlock() // 解绑调用方 ctx 的取消(relay teardown 会 cancel ctx,但订阅者应能完成它的短暂处理)。 d := deliver{ctx: context.WithoutCancel(ctx), ev: ev} for _, s := range subs { select { case s.ch <- d: default: // 缓冲满:丢弃以保证 Publish 永不阻塞 relay reader(正常负载不应发生)。 b.log.Warn("acp.turnbus.drop_full", "subscriber", s.name, "session_id", ev.SessionID, "kind", string(ev.Kind)) } } }