Files
BaaRai e233c02314 Translate all Chinese text to English
Comments, doc comments, UI labels, log and error messages, LLM prompt
strings, markdown docs, test data and assertions. Identifiers, enum
keys, routes, placeholders and code structure are unchanged.

Notes:
- docs/漏洞流量证据.md renamed to docs/vulnerability-traffic-evidence.md
- tests that exercised CJK specific handling (GBK zip names, Chinese
  full text search) now use ASCII equivalents; go build passes and the
  test suite matches the pre translation baseline (remaining failures
  need a PostgreSQL DSN and fail identically on the original)
- frontend mention syntax now uses English kinds (finding, asset, ...)
  matching the backend, chat mentions tests pass
2026-10-08 23:14:34 +02:00

107 lines
4.8 KiB
Go

package server
import (
"time"
"github.com/Autumn-27/artex/agent"
"github.com/Autumn-27/artex/db"
)
// Server-side resolution of the retry policy; see docs/llm-retry-design.md. Of the five layers:
// - connect / empty response / same-provider safe window "follow the endpoint": every LLM
// profile can override the global default (an empty profile field inherits the global; if the global is unset too, the built-in default applies);
// - circuit breaker / intent re-run are process-wide — there is only one global copy.
//
// The global policy reads one settings row from the DB; its call sites are all on low-frequency
// paths (building providers, work wrap-up, saving profiles), so another cache layer is not worth
// it. Circuit-breaker parameters are the exception — they are read on every failure path, so applyRetryPolicy pushes them into the 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{
// The counts keep their raw "0=default / negative=off" semantics here: the
// SDK's MaxRetries / EmptyResponseRetries are fully isomorphic to this — let it parse them itself.
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())
}
// Defaults for circuit breaking (pool cool-down), identical to the llmpool built-ins — overridden here only when the user has set a value.
// Intent re-run defaults are engine.go's 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
}
// emptyTurnNudgeLimit resolves how many empty-turn continuations one work may
// inject (see steerHooks.Stop). It deliberately reuses layer ②'s knob — "empty-response
// retry count": the two are two means to the same end. The SDK layer handles "not a single
// content block" by resending the very same request; this one handles "only thinking, neither
// body nor tools" by appending an instruction so the model continues from its existing thinking
// (a verbatim resend is pointless for an idle turn dictated by the shape of the context). The
// emptiness criteria differ because the SDK goes by "has any event been yielded", and thinking
// increments are themselves events — but a user configuring "how many empty-response retries" means "retry once more when the model produced nothing substantive", so sharing one count across both layers matches that mental model.
//
// Reads the global policy rather than a profile's override: a run may switch profiles
// mid-way due to failover, while this is the total gate for the whole intent and must not change with the endpoint. Semantics mirror the SDK's emptyRetries():
// 0 = the default defaultEmptyTurnNudges; -1 (negative) = disable idle-turn continuation; >0 = use that value.
func (e *Engine) emptyTurnNudgeLimit() int {
if e == nil || e.m == nil || e.m.pg == nil {
return defaultEmptyTurnNudges
}
switch n := e.m.pg.LLMRetryPolicy().Empty.Attempts; {
case n == 0:
return defaultEmptyTurnNudges
case n < 0:
return 0
default:
return n
}
}