Skip to content

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:

  1. Toggle an agent → calls handleToggleAgent() → updates local state via toggleAgent() composable → saves to /api/projects/[id] (ProjectConfig)
  2. Change model → calls saveAgentModel() → POST/PUT to /agent-profiles/ endpoint → refreshes prompt list
  3. Edit custom instructions → blur event → calls saveCustomInstructions() → POST (if new) or PUT (if existing) → shows "Saved" flash
  4. 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):

typescript
// 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 enabled
  • toggleAgent(type: string) — flip enable/disable, return new state object
  • setResearchIntervalMs(value: number) — update research interval
  • toConfigPatch() — build partial ProjectConfig for API POST/PUT

State lifecycle:

  1. Component calls useAgentConfig(projectId, projectConfig)
  2. First initialization: read project.config → hydrate state (if not already initialized)
  3. User toggles/changes interval → local state updates
  4. Component calls toConfigPatch() → merge with existing config → POST/PUT to /api/projects/[id]
  5. 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):

typescript
// 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):

typescript
// 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:

typescript
// 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):

typescript
// 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

FileResponsibility
app/components/SettingsAgentPrompts.vueUI: agent toggles, model selector, prompt editor, auto-save
app/composables/useAgentConfig.tsShared reactive state, composable for agent enable/disable + research interval
app/utils/settingsTypes.tsTypeScript interfaces (SettingsProject, AgentPromptInfo, agentEnabledKeys map)
app/utils/colors.tsAgent type colors (planner=amber, worker=blue, judge=purple, researcher=emerald)
app/utils/constants.tsModel options (Opus/Sonnet/Haiku)
server/api/projects/[projectId]/agent-prompts.get.tsFetch default + custom prompts for all types
server/api/projects/[projectId]/agent-profiles/index.get.tsList all agent profiles for project
server/api/projects/[projectId]/agent-profiles/index.post.tsCreate new agent profile
server/api/projects/[projectId]/agent-profiles/[profileId].put.tsUpdate agent profile (custom instructions, model)
server/api/projects/[projectId]/index.put.tsUpdate project config (agent toggles, research interval)
server/database/schema.tsagentProfiles 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

  1. Model resolution conflicts — If both project and agent profile specify a model, which wins? Currently: profile overrides project (but not documented in code).
  2. Limited artifact path placeholders — Phase contracts use hardcoded artifact paths; could support variables like {issueId} or {phase}.
  3. Profile versioning — No way to version/migrate profiles when code changes agent expectations.
  4. Test coverage — saveCustomInstructions() and saveAgentModel() flows are not well-tested.
  5. Concurrent edits — No optimistic locking; if two tabs edit same profile, last write wins (silent conflict).
  6. Performance at scale — agent-prompts endpoint queries profile table + filesystem for every request; could cache phase contract bodies.