// Context-scoped retry progress sink. // // Problem this solves: transport-level retries (pre-stream HTTP retry in // both the anthropic and openai-compat clients) happen BEFORE the event // channel is handed to the caller, so a consumer ranging on the channel // sees nothing while exponential backoff burns up to 32s per wait. The // engine-level retry loop already emits WarningEvent before each backoff; // this sink extends the same visibility down into the transport layer. // // Design: mirror the WithQuerySource ctx plumbing (policy.go). The engine // injects a callback via WithProgressSink before calling the provider; // Retryer.Do invokes it right before each backoff wait. Both transports // share Retryer.Do, so one read site covers every provider. The sink is // optional -- uninstrumented paths behave exactly as before. // // Alternative (rejected): wire Retryer.OnRetry at client construction. // The Retryer is per-client and long-lived while the event channel is // per-run, so a construction-time callback cannot route progress to the // right consumer without a registry; ctx already scopes per-call state. // // Context 携带的重试进度 sink. // // 解决的问题: transport 层重试 (anthropic 与 openai-compat 两条通道的 // pre-stream HTTP 重试) 发生在 event channel 交给调用方之前, 消费者在 // 指数退避 (单次最长 32s) 期间完全看不到任何事件. 引擎层重试循环已在 // 每次退避前发 WarningEvent; 本 sink 把同样的可见性下沉到 transport 层. // // 设计: 镜像 WithQuerySource 的 ctx 注入范式 (policy.go). 引擎在调 // provider 前经 WithProgressSink 注入回调; Retryer.Do 在每次退避等待前 // 调用它. 两条 transport 共用 Retryer.Do, 一个读位置覆盖所有 provider. // sink 可选 -- 未插桩路径行为完全不变. // // 替代方案 (否决): 在 client 构造时接 Retryer.OnRetry. Retryer 是 // per-client 长生命周期而 event channel 是 per-run, 构造期回调无法把 // 进度路由到正确的消费者; ctx 本来就承载 per-call 状态. package retry import ( "context" "time" ) // Progress describes one transport-level retry that is about to happen. // Fields are read-only observations; the sink must not mutate retry state. // // Progress 描述一次即将发生的 transport 层重试. 字段是只读观测值; sink // 不得改变重试状态. type Progress struct { // Attempt is the attempt number that just failed (1-based). // Attempt 是刚失败的尝试序号 (从 1 起). Attempt int // Delay is the backoff wait before the next attempt. // Delay 是下一次尝试前的退避等待时长. Delay time.Duration // Reason is the policy decision reason (e.g. "server_error backoff"). // Reason 是策略决策原因 (如 "server_error backoff"). Reason string // Category is the failed error's category (e.g. "rate_limit"). // Category 是失败错误的类别 (如 "rate_limit"). Category string // Message is the failed error's sanitized message. // Message 是失败错误的 sanitize 后消息. Message string } // ProgressSink receives retry progress notifications. Implementations must // be fast and non-blocking relative to the retry loop; they run on the // request goroutine before the backoff wait. // // ProgressSink 接收重试进度通知. 实现必须相对重试循环快速且不长阻塞; // 它在请求 goroutine 上, 退避等待之前运行. type ProgressSink func(p Progress) // progressSinkCtxKey is the unexported context key for ProgressSink. // progressSinkCtxKey 是 ProgressSink 的不导出 context key. type progressSinkCtxKey struct{} // WithProgressSink injects a retry progress callback into ctx so // Retryer.Do can surface transport-level retries to the caller (the // engine converts them into consumer-visible WarningEvent). // // WithProgressSink 将重试进度回调注入 ctx, Retryer.Do 借此把 transport // 层重试上浮给调用方 (引擎将其转成消费者可见的 WarningEvent). func WithProgressSink(ctx context.Context, sink ProgressSink) context.Context { return context.WithValue(ctx, progressSinkCtxKey{}, sink) } // ProgressSinkFromCtx reads the retry progress callback from ctx. Returns // nil when not injected -- callers treat that as "no observer" and skip // the notification, so uninstrumented paths still work. // // ProgressSinkFromCtx 从 ctx 读重试进度回调. 未注入时返回 nil -- 调用方 // 按"无观察者"处理并跳过通知, 未插桩路径仍能工作. func ProgressSinkFromCtx(ctx context.Context) ProgressSink { v, _ := ctx.Value(progressSinkCtxKey{}).(ProgressSink) return v }