Agent Profiles UI
Overview
The Agent Profiles UI enables project maintainers to customize agent behavior on a per-project basis. Prompts can be read-only (from version control) or editable (custom instructions), and individual agents can be enabled/disabled. This feature bridges the gap between stable agent identities and project-specific configuration.
How It Works
The system has three layers:
UI Layer
SettingsAgentPrompts.vue displays:
- Agent type header with enable/disable toggle (checkbox)
- Model selector dropdown (Opus/Sonnet/Haiku/default)
- Expandable section (click to reveal) containing:
- Agent description (read-only)
- Research interval slider (researcher only)
- Default system prompt (read-only, syntax-highlighted)
- Custom instructions textarea (auto-saves on blur)
Key interactions:
- Toggle an agent → calls
handleToggleAgent()→ updates local state viatoggleAgent()composable → saves to/api/projects/[id](ProjectConfig) - Change model → calls
saveAgentModel()→ POST/PUT to/agent-profiles/endpoint → refreshes prompt list - Edit custom instructions → blur event → calls
saveCustomInstructions()→ POST (if new) or PUT (if existing) → shows "Saved" flash - Change research interval → updates local state → saved on blur via
handleResearchIntervalSave()
Composable Layer (useAgentConfig)
useAgentConfig provides shared reactive state to prevent prop drilling between sibling components (SettingsOrchestrator and SettingsAgentPrompts):
// Shared state per project ID
const stateMap: Map<projectId, { state, initialized }>
interface AgentConfigState {
plannerEnabled: boolean
workerEnabled: boolean
judgeEnabled: boolean
researcherEnabled: boolean
architectEnabled: boolean
researchIntervalMs: number
}Key methods:
isAgentEnabled(type: string)— check if agent is enabledtoggleAgent(type: string)— flip enable/disable, return new state objectsetResearchIntervalMs(value: number)— update research intervaltoConfigPatch()— build partial ProjectConfig for API POST/PUT
State lifecycle:
- Component calls
useAgentConfig(projectId, projectConfig) - First initialization: read project.config → hydrate state (if not already initialized)
- User toggles/changes interval → local state updates
- Component calls
toConfigPatch()→ merge with existing config → POST/PUT to/api/projects/[id] - Refresh emitted → parent re-fetches project → composable syncs new config (if not initialized)
API Layer
1. GET /api/projects/[projectId]/agent-prompts
Fetches prompt info for all agent types (planner, worker, judge, researcher, architect):
// Returns AgentPromptInfo[] with:
{
type: string // 'worker', 'judge', 'researcher', 'architect', 'planner'
name: string // Profile name or capitalized type
defaultPrompt: string // Read from phase contract (research/architect/planner) or agent profile (worker/judge)
customInstructions: string // From agent_profiles table systemPrompt
profileId: string | null // Primary key of project-specific profile (null if using global default)
model: string | null // Model override (null = use project default)
}Logic:
- Phase agents (researcher/architect/planner): read defaultPrompt from
prompts/phases/{phase}.md(contract body) - Non-phase agents (worker/judge): read from
getDefaultPrompt()(profile files) - Custom instructions: lookup agentProfiles table by projectId + type, fallback to global (projectId=null)
2. POST /api/projects/[projectId]/agent-profiles
Create a new agent profile (when user saves custom instructions for an agent type that doesn't have a project-specific profile yet):
// POST body:
{
name: string // e.g., "Worker"
type: string // 'planner' | 'worker' | 'judge' | 'researcher' | 'architect' | 'intake' | 'custom'
systemPrompt?: string // Custom instructions (null = empty)
model?: string // Model override (null = use default)
isDefault?: boolean // Mark this profile as default (atomically unsets previous)
}3. PUT /api/projects/[projectId]/agent-profiles/[profileId]
Update an existing agent profile:
// PUT body: partial updates
{
name?: string
type?: string
systemPrompt?: string // null clears custom instructions
model?: string // null = use default
isDefault?: boolean
}Atomic default management: If setting isDefault=true, atomically sets isDefault=false on all other profiles for this project (SQLite transaction).
4. PUT /api/projects/[projectId]
Update project configuration (agent enable/disable flags):
// PUT body:
{
config: {
plannerEnabled: boolean
workerEnabled: boolean
judgeEnabled: boolean
researcherEnabled: boolean
architectEnabled: boolean
researchIntervalMs: number // Only if researcherEnabled=true
}
}Merge behavior: Existing config is preserved, new values merged in (does NOT replace entire config).
Key Components
| File | Responsibility |
|---|---|
app/components/SettingsAgentPrompts.vue | UI: agent toggles, model selector, prompt editor, auto-save |
app/composables/useAgentConfig.ts | Shared reactive state, composable for agent enable/disable + research interval |
app/utils/settingsTypes.ts | TypeScript interfaces (SettingsProject, AgentPromptInfo, agentEnabledKeys map) |
app/utils/colors.ts | Agent type colors (planner=amber, worker=blue, judge=purple, researcher=emerald) |
app/utils/constants.ts | Model options (Opus/Sonnet/Haiku) |
server/api/projects/[projectId]/agent-prompts.get.ts | Fetch default + custom prompts for all types |
server/api/projects/[projectId]/agent-profiles/index.get.ts | List all agent profiles for project |
server/api/projects/[projectId]/agent-profiles/index.post.ts | Create new agent profile |
server/api/projects/[projectId]/agent-profiles/[profileId].put.ts | Update agent profile (custom instructions, model) |
server/api/projects/[projectId]/index.put.ts | Update project config (agent toggles, research interval) |
server/database/schema.ts | agentProfiles table, ProjectConfig interface |
Design Decisions
1. Shared Composable State Over Props
Decision: useAgentConfig provides centralized reactive state shared by all sibling components that need agent config.
Rationale:
- Eliminates prop drilling (SettingsProject → parent → SettingsAgentPrompts)
- Allows multiple independent components to read/write same state without coupling
- Prevents stale state when components don't have direct communication
- State is keyed by projectId, so switching projects works seamlessly
Trade-off: Single source of truth means component must trust composable isn't modified elsewhere (but it is only used by settings).
2. Separate Agent Enable/Disable Toggles
Decision: Agent on/off flags stored in ProjectConfig (not in agent_profiles table).
Rationale:
- Profiles are for storing custom instructions + model overrides (static content)
- Toggles are for runtime dispatch control (dynamic behavior)
- Atomicity: can toggle agent without creating/modifying profile
- Profile deletion doesn't break dispatch (toggle remains independent)
Trade-off: Two tables to query instead of one, but clearer separation of concerns.
3. Phase Agents Read From Contracts
Decision: researcher/architect/planner agents read default prompt from prompts/phases/{phase}.md (phase contracts), not from profiles table.
Rationale:
- Phase contracts define the complete prompt agents receive (not just a profile reference)
- Single source of truth per phase (contract = spec + prompt)
- Separation of concerns: execution profiles vs design-phase contracts
- Prevents accidental cross-cutting changes (editing worker profile shouldn't affect architect)
Trade-off: Must query filesystem + parse YAML frontmatter on each fetch (mitigated by response caching).
4. Custom Instructions Are Append-Only
Decision: systemPrompt field is appended to agent context, never replaces default.
Rationale:
- Safety: can't accidentally break agent by editing custom instructions
- Composition: default prompt structure + behavior is preserved, only constraints added
- Reversibility: clearing custom instructions restores exact original behavior
- Clarity: UI explicitly shows default + custom separately
Trade-off: More verbose prompts, but safer and more transparent.
5. Auto-Save on Blur for Custom Instructions
Decision: Custom instructions textarea saves when focus is lost.
Rationale:
- Eliminates need for explicit "Save" button (lower cognitive load)
- Flash message ("Saving..." → "Saved" → disappears) provides feedback
- Matches modern UI expectations (Gmail, Slack, etc.)
- Grouped with research interval changes (both save on interaction, not immediate)
Trade-off: No "Revert" capability if user changes mind mid-edit (must refresh to undo).
6. Atomic Default Profile Management
Decision: When setting isDefault=true on a profile, atomically set isDefault=false on all others for that project.
Rationale:
- Database-level constraint prevents multiple defaults (no unique index needed)
- Atomic transaction ensures consistency (can't have zero or multiple defaults)
- Client doesn't need to manage this logic
Trade-off: Extra DB write per profile change, but negligible cost (rare operation).
Known Gaps
- Model resolution conflicts — If both project and agent profile specify a model, which wins? Currently: profile overrides project (but not documented in code).
- Limited artifact path placeholders — Phase contracts use hardcoded artifact paths; could support variables like
{issueId}or{phase}. - Profile versioning — No way to version/migrate profiles when code changes agent expectations.
- Test coverage — saveCustomInstructions() and saveAgentModel() flows are not well-tested.
- Concurrent edits — No optimistic locking; if two tabs edit same profile, last write wins (silent conflict).
- Performance at scale — agent-prompts endpoint queries profile table + filesystem for every request; could cache phase contract bodies.