176 lines
7.5 KiB
Go
176 lines
7.5 KiB
Go
// Package effort defines enkly's manual reasoning-depth control: five
|
|
// named levels (Low, Medium, High, Extra, Max) a caller can pick to
|
|
// arbitrate between speed and precision for one agent turn, modeled on
|
|
// Claude's effort parameter. It is deliberately a small, dependency-free
|
|
// package — just a Level type plus the static Profile each one maps to
|
|
// — so both the agent service (which actually runs a turn at a given
|
|
// level) and any future transport (gRPC field, HTTP query param, a
|
|
// gateway settings dropdown) can depend on it without pulling in
|
|
// agentrun or vllmkit.
|
|
//
|
|
// vLLM/Qwen has no first-class notion of "effort" the way the
|
|
// Anthropic API does (a token budget plus a reasoning-depth knob) — the
|
|
// only dial it exposes per request is vllmkit.Client's think string
|
|
// ("" / "true" / "false" / "low" / "medium" / "high", all of which
|
|
// collapse to a single on/off enable_thinking toggle on the wire — see
|
|
// vllmkit.Client.Think's doc comment) plus the ordinary generation
|
|
// options (max_tokens, temperature, top_p). Profile is this package's
|
|
// honest mapping of Claude's five-level vocabulary onto that smaller
|
|
// vocabulary: Think picks the per-round reasoning-effort floor (see
|
|
// agentrun.EffortThinkPolicy), MaxRounds caps how many tool-call
|
|
// round-trips a turn is allowed before Low has to converge fast and Max
|
|
// can keep investigating, and NumPredictCap bounds a single reply's
|
|
// length so Low genuinely costs less and Max isn't truncated mid-answer.
|
|
package effort
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// Level is one of five named reasoning-depth settings, ordered from
|
|
// fastest/cheapest to deepest/most thorough.
|
|
type Level string
|
|
|
|
const (
|
|
Low Level = "low"
|
|
Medium Level = "medium"
|
|
High Level = "high"
|
|
Extra Level = "extra"
|
|
Max Level = "max"
|
|
)
|
|
|
|
// Default is used whenever no level was explicitly selected — the same
|
|
// balance the agent struck before effort control existed.
|
|
const Default = Medium
|
|
|
|
// All lists every level in ascending order of depth, for validation
|
|
// messages and a future settings-UI dropdown.
|
|
var All = []Level{Low, Medium, High, Extra, Max}
|
|
|
|
// Parse normalizes free-form text (a slash-directive argument, a query
|
|
// param, a gRPC field once wired) into a Level. Case/space-insensitive;
|
|
// a handful of obvious synonyms are accepted so "quick"/"fast" from the
|
|
// fast-mode vocabulary and "deep"/"thorough" from everyday language
|
|
// both resolve sensibly instead of erroring on a near-miss.
|
|
func Parse(s string) (Level, error) {
|
|
switch strings.ToLower(strings.TrimSpace(s)) {
|
|
case "":
|
|
return Default, nil
|
|
case "low", "fast", "quick", "speed":
|
|
return Low, nil
|
|
case "medium", "default", "normal", "balanced":
|
|
return Medium, nil
|
|
case "high", "deep", "careful":
|
|
return High, nil
|
|
case "extra", "extra-high", "extra_high", "thorough":
|
|
return Extra, nil
|
|
case "max", "maximum", "deepest", "ultra":
|
|
return Max, nil
|
|
default:
|
|
return "", fmt.Errorf("effort: unknown level %q (want one of low, medium, high, extra, max)", s)
|
|
}
|
|
}
|
|
|
|
// Valid reports whether l is one of the five defined levels — distinct
|
|
// from Parse accepting synonyms: Valid is for checking an already
|
|
// canonical value (e.g. one stored verbatim), Parse is for user input.
|
|
func (l Level) Valid() bool {
|
|
switch l {
|
|
case Low, Medium, High, Extra, Max:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Profile is the concrete tuning one Level resolves to. Every field has
|
|
// a conservative, locally-reasonable value — there's no remote
|
|
// "effort=max" service to defer to the way the Anthropic API has, so
|
|
// these numbers are enkly's own calibration for enkly's vLLM
|
|
// deployment, meant to be adjusted (see cmd/server/main.go's
|
|
// ENKLY_EFFORT_* overrides) rather than treated as gospel.
|
|
type Profile struct {
|
|
// Level this profile was resolved from, for logging.
|
|
Level Level
|
|
// Think is the thinking-effort floor passed to the chat model on
|
|
// every round of the turn (see vllmkit.Client's Think field). A
|
|
// per-round policy (agentrun.EffortThinkPolicy) may still ask for
|
|
// more on round 0 or less on a retry — this is the floor beneath
|
|
// which it won't go, not an override of every round alike.
|
|
Think string
|
|
// MaxRounds caps tool-call round-trips for a turn run at this
|
|
// level. Low converges fast on purpose; Max gets the same ceiling
|
|
// dispatch_workflow's own sub-runs use, so a Max-effort turn can
|
|
// still delegate broad work instead of exhausting its own budget
|
|
// investigating serially.
|
|
MaxRounds int
|
|
// NumPredictCap bounds a single model reply's length in tokens (0
|
|
// = no cap beyond the client's own configured default). Only Low
|
|
// sets one — the other levels trust the model to stop when it's
|
|
// done rather than truncating a legitimately long answer.
|
|
NumPredictCap int
|
|
// ContextBudgetChars overrides agentrun's default context-budget
|
|
// threshold (see agentrun.Agent.ContextBudgetChars) for a turn run
|
|
// at this level — Max keeps more tool-result history verbatim
|
|
// before compacting, trading context budget for fewer "re-call the
|
|
// tool if you need it again" gaps on a long investigation.
|
|
ContextBudgetChars int
|
|
// AdaptiveThinking, when true, lets the per-round policy raise
|
|
// effort above Think on a round that looks like it needs it (an
|
|
// error result, a round deep into a long turn) instead of holding
|
|
// every round at exactly Think — see agentrun.AdaptiveThinkPolicy.
|
|
// Low and Medium leave it off (predictable cost); High and above
|
|
// turn it on (predictable quality).
|
|
AdaptiveThinking bool
|
|
// MaxParallelSubagents is the ceiling dispatch_workflow enforces
|
|
// for a single dispatch_workflow call made during a turn run at
|
|
// this level — see toolexec's workflow orchestrator. Low effort
|
|
// still allows some parallel fan-out (a migration-shaped request
|
|
// at Low effort is still a migration), just a smaller one.
|
|
MaxParallelSubagents int
|
|
}
|
|
|
|
// defaultContextBudgetChars mirrors agentrun's own unexported default
|
|
// (contextBudgetChars) so Resolve's Medium profile reproduces the
|
|
// agent's pre-effort-control behavior exactly. Keeping one literal
|
|
// value here rather than importing agentrun (which would create an
|
|
// import cycle: agentrun -> effort -> agentrun) is deliberate — see
|
|
// agentrun.Agent.ContextBudgetChars's doc comment for the other half of
|
|
// this contract.
|
|
const defaultContextBudgetChars = 60_000
|
|
|
|
// Resolve returns the Profile for l. An invalid l (never produced by
|
|
// Parse, but possible from a stored/decoded value) resolves as Medium
|
|
// rather than panicking or erroring — effort control degrading to "the
|
|
// behavior enkly always had" is always a safe failure mode.
|
|
func (l Level) Resolve() Profile {
|
|
switch l {
|
|
case Low:
|
|
return Profile{
|
|
Level: Low, Think: "low", MaxRounds: 6, NumPredictCap: 700,
|
|
ContextBudgetChars: defaultContextBudgetChars / 2, AdaptiveThinking: false, MaxParallelSubagents: 16,
|
|
}
|
|
case High:
|
|
return Profile{
|
|
Level: High, Think: "high", MaxRounds: 16,
|
|
ContextBudgetChars: defaultContextBudgetChars * 2, AdaptiveThinking: true, MaxParallelSubagents: 96,
|
|
}
|
|
case Extra:
|
|
return Profile{
|
|
Level: Extra, Think: "high", MaxRounds: 24,
|
|
ContextBudgetChars: defaultContextBudgetChars * 4, AdaptiveThinking: true, MaxParallelSubagents: 192,
|
|
}
|
|
case Max:
|
|
return Profile{
|
|
Level: Max, Think: "high", MaxRounds: 32,
|
|
ContextBudgetChars: defaultContextBudgetChars * 8, AdaptiveThinking: true, MaxParallelSubagents: 256,
|
|
}
|
|
default: // Medium and anything unrecognized
|
|
return Profile{
|
|
Level: Medium, Think: "", MaxRounds: 12,
|
|
ContextBudgetChars: defaultContextBudgetChars, AdaptiveThinking: false, MaxParallelSubagents: 48,
|
|
}
|
|
}
|
|
}
|