// Package role defines the per-role configuration shape (system prompt, tool // allowlist, sandbox kind, budget, and — the part actually driven this phase — // a multi-tier provider/model escalation ladder) used by the token-husbanding // harness. A RoleConfig is stored as a versioned row in storage's // role_configs table (see internal/storage/roleconfig.go); internal/executor // resolves tier 0 of a role's EscalationLadder for the initial dispatch of a // role-typed task, and internal/scheduler walks the ladder on failure // (retry-then-escalate). // // Not everything on RoleConfig is enforced yet. See the field doc comments // below and CLAUDE.md's Design Debt section for what's stored-but-not-wired, // matching how this codebase already documents task.Priority/RetryConfig. package role // RoleConfig is one version of a role's configuration. type RoleConfig struct { // Role names the role this config applies to (e.g. "coder", "reviewer"). Role string `json:"role"` // SystemPrompt is appended to Agent.SystemPromptAppend when a task // dispatches through this role (internal/executor.Pool.execute()). Wired. SystemPrompt string `json:"system_prompt,omitempty"` // Tools, SandboxKind, and DefaultBudgetUSD are stored and round-tripped // through the API/storage layer but are NOT YET enforced by the executor: // guardrails/tool availability are still applied uniformly by // NativeRunner (Phase 3) regardless of role, and SandboxKind here has no // effect on which sandbox.Sandbox implementation a runner picks (that's // still purely provider-driven, see NativeRunner.SandboxKind). A later // phase should thread these through. DefaultBudgetUSD is read by // internal/scheduler as the estimated cost passed to // budget.Accountant.Allow() when considering an escalation, which is a // narrow, specific use — not general per-role budget enforcement at // dispatch time. Tools []string `json:"tools,omitempty"` SandboxKind string `json:"sandbox_kind,omitempty"` DefaultBudgetUSD float64 `json:"default_budget_usd,omitempty"` // EscalationLadder is the ordered list of tiers a role-typed task climbs // on repeated failure. Tier 0 is used for the initial dispatch. EscalationLadder []Tier `json:"escalation_ladder,omitempty"` } // Tier is one rung-group in an escalation ladder: a set of candidate // (provider, model) rungs to choose among, plus how many attempts may be made // at this tier before the scheduler escalates to the next one. type Tier struct { Candidates []Rung `json:"candidates"` // SelectionMode is "round_robin" (default, when empty) or "single" (always // use Candidates[0]). SelectionMode string `json:"selection_mode,omitempty"` // MaxRetries is the number of attempts allowed at this tier before the // scheduler escalates to the next tier. 0 means "escalate immediately // after the first failure at this tier" (no retries held here). MaxRetries int `json:"max_retries"` } // Rung is a single (provider, model) pair. Provider must match a registered // executor runner key: "local", "anthropic", "google", "groq", "openrouter", // "openai". type Rung struct { Provider string `json:"provider"` Model string `json:"model"` } // EffectiveSelectionMode returns t.SelectionMode, defaulting to // "round_robin" when unset. func (t Tier) EffectiveSelectionMode() string { if t.SelectionMode == "" { return "round_robin" } return t.SelectionMode }