This commit is contained in:
2026-06-22 08:55:57 +08:00
parent dbb87823e8
commit 6ade6e8fa9
325 changed files with 41131 additions and 855 deletions
+131
View File
@@ -0,0 +1,131 @@
// 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))
}
}
}