Skip to content

Commit e9ed08e

Browse files
furiosaclaude
andcommitted
design(gt-yy9v): architecture for model selection by role
Adds comprehensive design document for GitHub issue gastownhall#335: "Ability to choose models for different roles" Contents: - requirements.md - Problem statement and success criteria - codebase-context.md - Relevant code patterns and integration points - 6 dimension analyses: api.md, data.md, ux.md, scale.md, security.md, integration.md - design-doc.md - Unified design document with Rule-of-5 refinement Key finding: This feature works today with no code changes. Users can define agent variants with --model flags and map them via role_agents in ~/gt/settings/config.json. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 41e3ef0 commit e9ed08e

9 files changed

Lines changed: 1853 additions & 0 deletions

File tree

.designs/gt-yy9v/api.md

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
# API & Interface Design: Model Selection by Role
2+
3+
## Command-Line Interface
4+
5+
### Option A: Using Existing RoleAgents (Recommended)
6+
7+
No new CLI flags needed. Users define agent presets with models baked in:
8+
9+
```bash
10+
# Town-level config: ~/gt/settings/agents.json
11+
{
12+
"version": 1,
13+
"agents": {
14+
"claude-opus": {
15+
"command": "claude",
16+
"args": ["--dangerously-skip-permissions", "--model", "opus-4.5"]
17+
},
18+
"claude-sonnet": {
19+
"command": "claude",
20+
"args": ["--dangerously-skip-permissions", "--model", "sonnet-4"]
21+
},
22+
"claude-haiku": {
23+
"command": "claude",
24+
"args": ["--dangerously-skip-permissions", "--model", "haiku-3.5"]
25+
}
26+
}
27+
}
28+
29+
# Town-level settings: ~/gt/settings/config.json
30+
{
31+
"type": "town-settings",
32+
"version": 1,
33+
"default_agent": "claude-opus",
34+
"role_agents": {
35+
"polecat": "claude-opus",
36+
"witness": "claude-sonnet",
37+
"refinery": "claude-haiku",
38+
"mayor": "claude-sonnet",
39+
"deacon": "claude-sonnet",
40+
"crew": "claude-sonnet"
41+
}
42+
}
43+
```
44+
45+
### Option B: New --model Flag (Alternative)
46+
47+
Add explicit model flag to spawn commands:
48+
49+
```bash
50+
# Override at spawn time
51+
gt sling <bead> <rig> --model sonnet
52+
53+
# Show current model config
54+
gt config models
55+
56+
# Set role model
57+
gt config set role-model polecat opus
58+
gt config set role-model witness sonnet
59+
```
60+
61+
### Subcommand Ergonomics
62+
63+
If implementing Option B, new commands would be:
64+
65+
```bash
66+
# View all model assignments
67+
gt config models
68+
# Output:
69+
# Role Model Source
70+
# ---- ----- ------
71+
# polecat opus-4.5 rig (gastown)
72+
# witness sonnet-4 town
73+
# refinery haiku-3.5 default
74+
# crew sonnet-4 town
75+
76+
# Set model for a role
77+
gt config set-model <role> <model>
78+
79+
# List available models
80+
gt config list-models
81+
# Output: opus-4.5, sonnet-4, haiku-3.5
82+
```
83+
84+
## Programmatic API
85+
86+
### Configuration Interface (Go)
87+
88+
For Option A (agent presets with models):
89+
```go
90+
// No API changes needed - use existing RoleAgents
91+
92+
// Get agent for role (existing function)
93+
func ResolveRoleAgentConfig(role string, rigSettings *RigSettings, townSettings *TownSettings) (*AgentPresetInfo, error)
94+
```
95+
96+
For Option B (explicit model field):
97+
```go
98+
// New function to resolve model
99+
func ResolveRoleModel(role string, rigSettings *RigSettings, townSettings *TownSettings) string
100+
101+
// Extended TownSettings
102+
type TownSettings struct {
103+
// ...existing fields...
104+
RoleModels map[string]string `json:"role_models,omitempty"`
105+
}
106+
107+
// Extended RigSettings
108+
type RigSettings struct {
109+
// ...existing fields...
110+
RoleModels map[string]string `json:"role_models,omitempty"`
111+
}
112+
```
113+
114+
### Return Types
115+
116+
Model resolution returns a simple string:
117+
- `"opus-4.5"` | `"sonnet-4"` | `"haiku-3.5"` | `""`
118+
119+
Empty string means "use Claude's default" (currently Opus 4.5).
120+
121+
## Configuration Interface
122+
123+
### File Locations
124+
125+
| Level | File | Purpose |
126+
|-------|------|---------|
127+
| Town | `~/gt/settings/agents.json` | Custom agent definitions with model args |
128+
| Town | `~/gt/settings/config.json` | Default agent and role_agents mapping |
129+
| Rig | `<rig>/settings/agents.json` | Rig-specific agent definitions |
130+
| Rig | `<rig>/settings/config.json` | Rig-specific overrides |
131+
132+
### Environment Variables
133+
134+
For runtime override (useful for debugging/testing):
135+
```bash
136+
GT_MODEL_OVERRIDE=haiku # Forces all roles to use haiku
137+
```
138+
139+
Not recommended for production use - config files are better for persistence.
140+
141+
## Error Messages
142+
143+
### Invalid Model Name
144+
```
145+
Error: unknown model "gpt4"
146+
Available models: opus-4.5, sonnet-4, haiku-3.5
147+
```
148+
149+
### Invalid Role Name
150+
```
151+
Error: unknown role "worker"
152+
Valid roles: polecat, witness, refinery, mayor, deacon, crew
153+
```
154+
155+
### Config Validation
156+
```
157+
Warning: role_agents references undefined agent "claude-fast"
158+
Available agents: claude, claude-opus, claude-sonnet, claude-haiku, gemini, codex
159+
```
160+
161+
## Help Text
162+
163+
```
164+
MODEL SELECTION
165+
166+
Gas Town supports per-role model selection for cost optimization.
167+
168+
Configuration:
169+
Define agent presets with model args in ~/gt/settings/agents.json:
170+
171+
{
172+
"agents": {
173+
"claude-opus": {"command": "claude", "args": ["--model", "opus-4.5"]},
174+
"claude-sonnet": {"command": "claude", "args": ["--model", "sonnet-4"]}
175+
}
176+
}
177+
178+
Then assign to roles in ~/gt/settings/config.json:
179+
180+
{
181+
"role_agents": {
182+
"polecat": "claude-opus",
183+
"witness": "claude-sonnet"
184+
}
185+
}
186+
187+
Resolution order:
188+
1. Rig role_agents (if set)
189+
2. Town role_agents (if set)
190+
3. Rig default_agent (if set)
191+
4. Town default_agent (if set)
192+
5. Built-in default: "claude"
193+
```
194+
195+
## Naming Conventions
196+
197+
### Model Names
198+
Use Claude Code's short names for ergonomics:
199+
- `opus``opus-4.5` (aliases work)
200+
- `sonnet``sonnet-4`
201+
- `haiku``haiku-3.5`
202+
203+
### Agent Preset Names
204+
Convention: `<cli>-<model>` for model variants:
205+
- `claude-opus` (Claude with Opus)
206+
- `claude-sonnet` (Claude with Sonnet)
207+
- `claude-haiku` (Claude with Haiku)
208+
209+
## Discoverability
210+
211+
### How Users Learn This Feature
212+
213+
1. **Documentation**: Add section to Gas Town README and `gt help config`
214+
2. **Example configs**: Ship example `agents.json` with model presets
215+
3. **Status command**: `gt status` shows current model for each active agent
216+
4. **Cost reporting**: `gt costs` shows model breakdown (existing feature)
217+
218+
### Happy Path
219+
220+
1. User wants to reduce costs
221+
2. Runs `gt help config` or reads docs
222+
3. Copies example agent definitions to settings
223+
4. Maps roles to agents via `role_agents`
224+
5. Starts Gas Town - polecats run on Opus, others on Sonnet/Haiku
225+
226+
### Edge Cases
227+
228+
1. **Undefined agent**: Config references agent not in registry → warning, fall back to default
229+
2. **Invalid model**: Claude rejects model name → agent fails to start, error in logs
230+
3. **Mixed models in rig**: Different polecats in same rig can't use different models (by design)
231+
232+
## Consistency with Existing Interfaces
233+
234+
### Follows Existing Patterns
235+
236+
- Uses same config file locations (`settings/config.json`, `settings/agents.json`)
237+
- Uses same resolution hierarchy (rig → town → default)
238+
- Uses same JSON schema patterns (`map[string]string` for mappings)
239+
- Uses existing `RoleAgents` field (Option A)
240+
241+
### Changes from Existing Patterns
242+
243+
- Option B would add a new `RoleModels` field (parallel to `RoleAgents`)
244+
- New CLI flags if implementing `gt config set-model`
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# Codebase Context: Model Selection by Role
2+
3+
## Relevant Files
4+
5+
| File | Purpose | Relevance |
6+
|------|---------|-----------|
7+
| `internal/config/types.go` | Config type definitions | Contains `TownSettings`, `RigSettings` with `RoleAgents` map - infrastructure exists but for agents, not models |
8+
| `internal/config/agents.go` | Agent registry and presets | Defines agent presets (claude, gemini, etc.) and `AgentPresetInfo` - could be extended for model selection |
9+
| `internal/config/loader.go` | Config loading and resolution | `ResolveRoleAgentConfig()` resolves role → agent mapping; command building would need `--model` injection |
10+
| `internal/cmd/polecat_spawn.go` | Polecat spawning | `SpawnPolecatForSling()` creates polecats; uses `BuildPolecatStartupCommandWithAgentOverride()` |
11+
| `internal/cmd/start.go` | Agent startup orchestration | Manager `Start()` methods for each role type |
12+
| `internal/polecat/session_manager.go` | Session management | `SessionStartOptions` - could pass model selection here |
13+
| `internal/cmd/sling.go` | Work dispatch | `gt sling` supports `--agent` flag, could add `--model` |
14+
15+
## Existing Patterns
16+
17+
### Role-Based Agent Selection (Already Exists)
18+
19+
The codebase already supports per-role agent selection via `RoleAgents`:
20+
21+
```go
22+
// types.go lines 53-58 (TownSettings)
23+
RoleAgents map[string]string `json:"role_agents,omitempty"`
24+
// Example: {"mayor": "claude", "witness": "gemini", "polecat": "claude"}
25+
26+
// types.go lines 221-226 (RigSettings)
27+
RoleAgents map[string]string `json:"role_agents,omitempty"`
28+
// Example: {"witness": "claude-haiku", "polecat": "claude-sonnet"}
29+
```
30+
31+
### Resolution Hierarchy
32+
33+
Config resolution follows this precedence (loader.go):
34+
1. `agentOverride` flag (e.g., `--agent gemini`)
35+
2. Rig-level `RigSettings.Agent` or `RigSettings.RoleAgents[role]`
36+
3. Town-level `TownSettings.DefaultAgent` or `TownSettings.RoleAgents[role]`
37+
4. Ultimate fallback: `"claude"`
38+
39+
### Command Building
40+
41+
Startup commands are built with:
42+
```go
43+
// loader.go line 1294
44+
BuildPolecatStartupCommand(rigName, polecatName, rigPath, prompt string) string
45+
// Results in: "claude --dangerously-skip-permissions ..."
46+
```
47+
48+
The `Args` array from `AgentPresetInfo` or `RuntimeConfig` gets appended to the command.
49+
50+
## Key Data Structures
51+
52+
### AgentPresetInfo (agents.go)
53+
```go
54+
type AgentPresetInfo struct {
55+
Name AgentPreset // "claude"
56+
Command string // "claude"
57+
Args []string // ["--dangerously-skip-permissions"]
58+
// ... other fields
59+
}
60+
```
61+
62+
### RuntimeConfig (types.go)
63+
```go
64+
type RuntimeConfig struct {
65+
Command string // "claude"
66+
Args []string // ["--dangerously-skip-permissions"]
67+
// ... other fields
68+
}
69+
```
70+
71+
**Key insight**: `Args` is where `--model opus` would be injected.
72+
73+
### TownSettings / RigSettings
74+
Both have:
75+
- `DefaultAgent string` - which agent preset to use
76+
- `Agents map[string]*RuntimeConfig` - custom agent definitions
77+
- `RoleAgents map[string]string` - role → agent mapping
78+
79+
## Integration Points
80+
81+
### Where Model Selection Could Be Injected
82+
83+
1. **At Config Level** - New field in `TownSettings`/`RigSettings`:
84+
```go
85+
RoleModels map[string]string `json:"role_models,omitempty"`
86+
// {"polecat": "opus", "witness": "sonnet", "refinery": "haiku"}
87+
```
88+
89+
2. **At Agent Definition Level** - Add `Model` to `AgentPresetInfo`:
90+
```go
91+
type AgentPresetInfo struct {
92+
// ...existing fields...
93+
Model string `json:"model,omitempty"` // "opus", "sonnet", "haiku"
94+
}
95+
```
96+
97+
3. **At Command Building** - Inject `--model` in `BuildStartupCommand`:
98+
```go
99+
// In BuildStartupCommand or normalizeRuntimeConfig
100+
if model != "" {
101+
args = append(args, "--model", model)
102+
}
103+
```
104+
105+
4. **At Spawn Time** - Add `--model` flag to `gt sling`:
106+
```bash
107+
gt sling <bead> <rig> --model sonnet
108+
```
109+
110+
### Claude Code --model Flag
111+
112+
Claude Code accepts `--model` flag:
113+
```bash
114+
claude --model opus-4.5 # Use Opus 4.5
115+
claude --model sonnet-4 # Use Sonnet 4
116+
claude --model haiku-3.5 # Use Haiku 3.5
117+
```
118+
119+
This is the mechanism to leverage.
120+
121+
## Constraints from Code
122+
123+
1. **Backwards Compatibility**: `RoleAgents` already exists - new model config must coexist
124+
2. **Command Building**: All paths must flow through `BuildStartupCommand*` functions
125+
3. **Agent Presets**: Model could be per-preset (define "claude-opus", "claude-sonnet") OR per-role
126+
4. **Session Persistence**: Claude remembers model per session - must set on every init
127+
5. **Multiple Resolution Layers**: Town → Rig → Role → Override hierarchy must be maintained
128+
129+
## Recommended Approach
130+
131+
Based on the existing patterns, the cleanest approach is:
132+
133+
**Option A: Model as Agent Variant** (Minimal Changes)
134+
- Define agent presets: `"claude-opus"`, `"claude-sonnet"`, `"claude-haiku"`
135+
- Use existing `RoleAgents` map: `{"polecat": "claude-opus", "witness": "claude-sonnet"}`
136+
- Add `--model` to preset Args
137+
138+
**Option B: Separate RoleModels Config** (More Flexible)
139+
- Add `RoleModels map[string]string` to `TownSettings`/`RigSettings`
140+
- Resolve model separately from agent
141+
- Inject `--model` in command building
142+
143+
**Option C: Model in RuntimeConfig** (Most Granular)
144+
- Add `Model string` field to `RuntimeConfig`
145+
- Allow per-agent model specification
146+
- Most flexible but more complex
147+
148+
Given the issue request ("role-based configuration"), **Option A** is simplest and works with existing infrastructure.

0 commit comments

Comments
 (0)