package server import ( "time" "github.com/Autumn-27/artex/agent" "github.com/Autumn-27/artex/db" ) // 重试策略的服务端解析,见 docs/LLM重试设计.md。五层里: // - 建连 / 空响应 / 同 provider 安全窗口 是「跟着端点走」的,每个 LLM 配置可以覆盖 // 全局默认(profile 的某项留空就继承全局,全局也没配就用内置默认); // - 熔断 / 意图重跑 是进程级的,只有全局一份。 // // 全局策略读一次 DB 一行 settings,调用点都在低频路径(构建 provider、work 收尾、 // 保存配置),不值得再加一层缓存;熔断参数是例外——它在失败路径上每次都要读,所以 // 由 applyRetryPolicy 推给 Registry 保存。 // retryPolicy reads the global policy; a nil DB yields the zero policy (all // layers on their built-in defaults). func (s *Server) retryPolicy() db.LLMRetryPolicy { if s.m == nil || s.m.pg == nil { return db.LLMRetryPolicy{} } return s.m.pg.LLMRetryPolicy() } // resolveRetry layers one profile's override on top of the global policy and // converts the result into the form agent.Config carries. Rules combine field by // field, so a profile that only pins an interval still inherits the global count. func resolveRetry(o db.RetryOverride, pol db.LLMRetryPolicy) agent.RetryConfig { connect := o.Connect.Or(pol.Connect) empty := o.Empty.Or(pol.Empty) stream := o.Stream.Or(pol.Stream) return agent.RetryConfig{ // 次数在这里保持「0=默认 / 负=关闭」的原始语义:SDK 的 MaxRetries / // EmptyResponseRetries 与之完全同构,交给它自己解析即可。 ConnectAttempts: connect.Attempts, ConnectInterval: connect.Interval(), EmptyAttempts: empty.Attempts, EmptyInterval: empty.Interval(), StreamAttempts: stream.Attempts, StreamInterval: stream.Interval(), } } // applyProfileRetry fills cfg.Retry for a profile read from the DB. func (s *Server) applyProfileRetry(cfg *agent.Config, p *db.LLMProfile) { if p == nil { return } cfg.Retry = resolveRetry(p.Retry, s.retryPolicy()) } // 熔断(轮询冷却)的默认值,与 llmpool 内置的一致 —— 这里只在「用户配了值」时才覆盖。 // 意图重跑的默认值见 engine.go 的 modelErrorRetries / modelErrorRetryBackoff。 // applyRetryPolicy pushes the process-wide layers of the policy into the objects // that consume them on a hot path: the circuit-breaker registry. Called at // startup and whenever the policy is saved. func (s *Server) applyRetryPolicy() { pol := s.retryPolicy() if s.llmHealth != nil { s.llmHealth.SetPolicy(pol.Breaker.Attempts, pol.Breaker.Interval()) } } // modelErrorRetryPolicy resolves the intent-level replay knobs (layer ⑤): how // many times a model_error work is re-run and how long to back off between runs. func (e *Engine) modelErrorRetryPolicy() (retries int, backoff time.Duration) { retries, backoff = modelErrorRetries, modelErrorRetryBackoff if e == nil || e.m == nil || e.m.pg == nil { return retries, backoff } rule := e.m.pg.LLMRetryPolicy().Intent if rule.Attempts != 0 { retries = max(rule.Attempts, 0) } if d := rule.Interval(); d > 0 { backoff = d } return retries, backoff }