package agent import ( "fmt" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" ) // Option bounds and defaults. These are compile-time constants so no magic // number appears in the runtime path. const ( defaultMaxRounds = 8 minTemperature = 0.0 maxTemperature = 2.0 ) // Options carries the config knobs for an Agent. Zero values fall back to a // provider default: temperature uses 0 to mean "provider default", and // MaxRounds/MaxTokens use 0 to mean "default"/"provider default" respectively. type Options struct { // Model is the requested model name. Empty lets the injected Router or the // adapter's own default decide. Model string // Temperature is the sampling temperature. A nonzero value is forwarded to // the model; zero means the provider default. Temperature float64 // MaxRounds is the tool-loop upper bound. Zero is defaulted to 8. MaxRounds int // Purpose labels every CallRecord emitted for accounting ("chat", "agent"). Purpose string // MaxTokens caps generated tokens. Zero means the provider default. MaxTokens int } // Option configures an Agent at construction time. Options are applied in the // order they are passed to New. type Option func(*Agent) // WithModel injects the LLM backend used by RunStep: a model.Adapter plus an // optional per-role Router. When router is non-nil it overrides the Request's // Model from the request contents — which is how vision requests are routed to // the vision model — otherwise the agent's configured Model or the adapter // default applies. func WithModel(adapter model.Adapter, router *model.Router) Option { return func(a *Agent) { a.adapter = adapter a.router = router } } // WithOptions applies opts to the agent's config, merging over the defaults. A // zero Model, MaxRounds, Purpose or MaxTokens leaves the existing value // unchanged; Temperature is copied verbatim. func WithOptions(opts Options) Option { return func(a *Agent) { if opts.Model != "" { a.opts.Model = opts.Model } a.opts.Temperature = opts.Temperature if opts.MaxRounds != 0 { a.opts.MaxRounds = opts.MaxRounds } if opts.Purpose != "" { a.opts.Purpose = opts.Purpose } if opts.MaxTokens != 0 { a.opts.MaxTokens = opts.MaxTokens } } } // WithRecorder wires a CallRecord sink. After each successful model call the // agent emits one CallRecord to the recorder. func WithRecorder(r model.Recorder) Option { return func(a *Agent) { a.recorder = r } } // WithHandoff sets the agent's handoff-target placeholder (M3 orchestration). func WithHandoff(h *Handoff) Option { return func(a *Agent) { a.handoff = h } } // validateOptions rejects out-of-range option values so bad config fails at // construction rather than at request time. func validateOptions(o Options) error { if o.Temperature < minTemperature || o.Temperature > maxTemperature { return fmt.Errorf("%w: temperature %v out of range [%v, %v]", ErrBadOptions, o.Temperature, minTemperature, maxTemperature) } if o.MaxRounds < 0 { return fmt.Errorf("%w: max rounds %d", ErrBadOptions, o.MaxRounds) } if o.MaxTokens < 0 { return fmt.Errorf("%w: max tokens %d", ErrBadOptions, o.MaxTokens) } return nil }