mirror of
https://github.com/BaaRai/ARTEX.git
synced 2026-11-13 11:40:53 +08:00
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
107 lines
4.8 KiB
Go
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
|
|
}
|
|
}
|