16 KiB
Dynamic Model Switching Feature Analysis
Status: Design Document Created: 2026-01-21 Scope: Mid-session model switching within the Kandev application
Executive Summary
This document analyzes the implementation of dynamic model switching, allowing users to change the AI model mid-session without starting a new session. The feature requires UI changes to expose model selection, backend logic to detect model changes and restart agents, and careful handling of session continuity.
Current Architecture Overview
Session Lifecycle (Current Flow)
User clicks "Start Session"
→ Frontend sends orchestrator.start with agentProfileId
→ Orchestrator.StartTask → Executor.ExecuteWithProfile
→ Lifecycle Manager resolves profile → gets model from AgentProfileInfo
→ CommandBuilder.BuildCommand adds --model flag
→ Agent process started with fixed model
→ ACP session initialized
→ User sends prompts via session/prompt
Key Insight: Model is determined at session start via AgentProfileInfo.Model and baked into the agent command. There's no current mechanism to change the model mid-session.
Relevant Components
| Component | Path | Role |
|---|---|---|
| TaskChatPanel | apps/web/components/task/task-chat-panel.tsx |
Chat UI, displays current model |
| SessionsDropdown | apps/web/components/task/sessions-dropdown.tsx |
Session switcher |
| Orchestrator Service | apps/backend/internal/orchestrator/service.go |
PromptTask, StartTask, ResumeTaskSession |
| Executor | apps/backend/internal/orchestrator/executor/executor.go |
Agent launch, prompt forwarding |
| Lifecycle Manager | apps/backend/internal/agent/lifecycle/manager.go |
Agent process management |
| CommandBuilder | apps/backend/internal/agent/lifecycle/command.go |
Builds CLI args including --model |
| ProfileResolver | apps/backend/internal/agent/lifecycle/profile_resolver.go |
Resolves profile → model |
| SessionManager | apps/backend/internal/agent/lifecycle/session.go |
ACP session init/prompt |
| TaskSession Model | apps/backend/internal/task/models/models.go |
Session persistence |
| Message Service | apps/backend/internal/task/service/service.go |
CreateMessage |
Requirements Breakdown
UI Requirements
- Model Selector in Session View: Replace existing agent selector with model dropdown
- Deferred Application: Selection does NOT take effect immediately
- Apply on Prompt: Selected model applied when user sends next prompt
- Persistent State: All subsequent prompts use new model until changed again
Backend Requirements
- Model Change Detection: Compare current model vs requested model on prompt
- System Message Insertion: "Model switching to [model name]" message
- Agent Restart: Stop current agent, restart with new
--modelflag - Session Resumption: Attempt to resume ACP session after restart (if supported)
Technical Approach
Phase 1: Frontend Model Selector
1.1 Store State for Pending Model
Add to the Zustand store a per-session "pending model" that tracks what model the user wants to switch to.
Files to modify:
apps/web/lib/state/store.ts- AddpendingModelBySessionId: Record<string, string>apps/web/lib/types/http.ts- Add model-related types if needed
1.2 Model Selector Component
Create a model dropdown in the chat panel that:
- Shows available models for the current agent type
- Shows current model (from session's agent profile)
- Allows selection of a different model
- Stores selection in
pendingModelBySessionId[sessionId]
Files to modify:
apps/web/components/task/task-chat-panel.tsx- Add ModelSelector component- May create new component:
apps/web/components/task/model-selector.tsx
Data source for available models:
- From
AvailableAgent.model_config.available_modelsin the agent discovery store - Need to resolve which agent the session is using via
session.agent_profile_id
1.3 Prompt Submission with Model Override
When user submits a prompt, check if pendingModelBySessionId[sessionId] differs from current model:
- If different: include
modelfield in WebSocket prompt payload - Clear pending model after submission (backend will handle the switch)
WebSocket payload change:
// Current
{ action: "orchestrator.prompt", payload: { taskId, sessionId, content } }
// Proposed
{ action: "orchestrator.prompt", payload: { taskId, sessionId, content, model?: string } }
Files to modify:
apps/web/lib/ws/actions.ts- Addmodelto prompt action payloadapps/web/components/task/task-chat-panel.tsx- Include model in prompt submission
Phase 2: Backend Model Change Detection
2.1 Extend Prompt Handling
Modify the orchestrator's PromptTask to accept an optional model parameter.
Files to modify:
apps/backend/internal/orchestrator/service.go-PromptTaskmethodapps/backend/internal/ws/handlers/orchestrator.go- Parse model from WS payload
Logic flow:
PromptTask(taskId, sessionId, content, model)
→ If model != currentExecution.Model:
→ Insert system message "Switching to {model}..."
→ Stop current agent (graceful shutdown)
→ Update session's effective model
→ Restart agent with new --model flag
→ Resume or reinitialize ACP session
→ Forward prompt to agent
2.2 Executor Model Switch
The executor needs a new method to handle model switching:
Files to modify:
apps/backend/internal/orchestrator/executor/executor.go- Add
SwitchModel(sessionId, newModel)method - Coordinates stop → rebuild → restart → resume
- Add
Implementation sketch:
func (e *Executor) SwitchModel(ctx context.Context, sessionId, newModel string) error {
exec, ok := e.running[sessionId]
if !ok {
return ErrSessionNotFound
}
// 1. Stop current agent
e.lifecycleManager.StopAgent(ctx, exec.AgentID)
// 2. Update profile with new model
newProfileInfo := exec.ProfileInfo
newProfileInfo.Model = newModel
// 3. Restart agent with new command
newAgentID, err := e.lifecycleManager.Launch(ctx, LaunchOpts{
Model: newModel,
// ... other opts from exec
})
// 4. Attempt session resume
e.lifecycleManager.InitializeSession(ctx, newAgentID, InitOpts{
IsResume: true,
ResumeToken: exec.ResumeToken,
})
return nil
}
2.3 System Message for Model Switch
Insert a visible message in the chat indicating the model change.
Files to modify:
apps/backend/internal/task/service/service.go- UseCreateMessagewith typestatus
Message format:
{
"type": "status",
"content": "Switching model to Claude Opus 4.5...",
"metadata": {
"status_type": "model_switch",
"from_model": "sonnet4.5",
"to_model": "opus4.5"
}
}
Phase 3: Session Resumption After Restart
3.1 Agent-Specific Resumption
Different agents handle resumption differently (from agents.json):
| Agent | resume_via_acp |
resume_flag |
can_recover |
|---|---|---|---|
| auggie-agent | false | --resume |
false |
| codex-agent | true | (none) | true |
For Codex-style agents (resume_via_acp: true):
- Use ACP
session/loadwith the storedResumeToken - Session history is preserved by the agent
For Auggie-style agents (resume_via_acp: false, can_recover: false):
- Cannot truly resume; must reinitialize with fresh ACP session
- Decision: Accept context loss, display system message: "Model switched. A new session was started."
3.2 Handling Resume Failures
Files to modify:
apps/backend/internal/agent/lifecycle/session.go-InitializeSessionerror handling
Fallback strategy:
- Attempt resume with stored token
- If resume fails → start new session
- Emit warning message to user: "Session could not be fully resumed after model switch"
Integration Points
WebSocket Integration
| Layer | Change Required |
|---|---|
Client (apps/web/lib/ws/client.ts) |
No changes |
Actions (apps/web/lib/ws/actions.ts) |
Add model field to prompt action |
Handler (apps/backend/internal/ws/handlers/orchestrator.go) |
Parse model from payload |
Wire format example:
{
"id": "uuid",
"type": "request",
"action": "orchestrator.prompt",
"payload": {
"task_id": "task-uuid",
"session_id": "session-uuid",
"content": "User message",
"model": "opus4.5"
}
}
Store Integration
New store slice additions:
// In store.ts
pendingModel: {
bySessionId: Record<string, string>, // sessionId → pending model id
setPendingModel: (sessionId: string, modelId: string) => void,
clearPendingModel: (sessionId: string) => void,
getPendingModel: (sessionId: string) => string | null,
}
Store → Component data flow:
ModelSelector reads: settingsAgents (for available models), session.agent_profile_id
ModelSelector writes: pendingModel.bySessionId[sessionId]
TaskChatPanel reads: pendingModel.bySessionId[sessionId] on prompt submit
Database Integration
Decision: No schema change (runtime-only)
- Model switch is runtime-only; session's
agent_profile_idremains unchanged - On backend restart, session resumes with original profile model
- No migration required
Challenges and Edge Cases
1. Agent Busy During Model Switch Request
Scenario: User selects new model while agent is processing a previous prompt.
Solution options:
- Queue the switch: Store pending model, apply after current response completes
- Block selection: Disable model selector while agent is busy
- Immediate abort: Cancel current response, switch model (destructive)
Recommendation: Queue the switch; apply on next user-initiated prompt.
2. Rapid Model Changes
Scenario: User changes model multiple times before sending a prompt.
Solution: Only the final selection matters; each change overwrites pendingModelBySessionId[sessionId].
3. Resume Failure After Switch
Scenario: Agent restarts with new model but ACP session/load fails.
Handling:
- Log error for debugging
- Fall back to new session initialization
- Show user message: "Model switched successfully but session context could not be fully restored"
4. Different Agent Types
Scenario: User tries to switch to a model not supported by current agent type.
Prevention: Model selector only shows models from agent.model_config.available_models for the session's agent type.
5. Session State Persistence
Scenario: Backend restarts mid-session; what model should resume use?
Behavior: Uses agent_profile_snapshot.model from session creation (original profile model). Runtime model switches are not persisted.
6. Model Availability Changes
Scenario: A model is removed from agent configuration while sessions are active.
Handling: Validate model against current available_models before sending to backend. If invalid, fall back to session's original model.
Alternative Approaches
Alternative A: Runtime Model Switching via ACP
Concept: Send a hypothetical session/switch_model ACP call without restarting agent.
Pros:
- No process restart; faster switch
- Seamless context preservation
Cons:
- Not supported by current agent implementations
- Would require agent CLI changes upstream
- Different agents would need different implementations
Verdict: Not viable without significant upstream work.
Alternative B: Model as Separate Session Concept
Concept: Treat each model switch as a "sub-session" within the same UI session.
Pros:
- Clean separation of concerns
- Easy to track model history
Cons:
- Complex UI implications
- Breaks current session model significantly
- Overhead for simple model changes
Verdict: Over-engineering for the requirement.
Alternative C: Pre-select Model Before Session Start Only
Concept: Allow model selection only at session creation, not mid-session.
Pros:
- Simplest implementation
- No restart complexity
Cons:
- Doesn't meet the stated requirement
- Poor UX for iterative work
Verdict: Does not satisfy user requirements.
Implementation Phases
Phase 1: Frontend Model Selector (Est. 2-3 days)
Goal: Model dropdown appears in UI; selection stored locally.
Files to modify:
apps/web/lib/state/store.ts- Add pendingModel sliceapps/web/components/task/task-chat-panel.tsx- Add ModelSelector- Create
apps/web/components/task/model-selector.tsx- New component
Deliverable: User can select a model, but it has no effect yet.
Phase 2: Backend Prompt Extension (Est. 2-3 days)
Goal: Prompts can include model; backend detects change.
Files to modify:
apps/web/lib/ws/actions.ts- Add model to prompt payloadapps/backend/internal/ws/handlers/orchestrator.go- Parse modelapps/backend/internal/orchestrator/service.go- PromptTask accepts model
Deliverable: Model parameter flows from UI to orchestrator.
Phase 3: Agent Restart Logic (Est. 3-4 days)
Goal: Backend restarts agent with new model when detected.
Files to modify:
apps/backend/internal/orchestrator/executor/executor.go- SwitchModel methodapps/backend/internal/agent/lifecycle/manager.go- Ensure clean restartapps/backend/internal/task/service/service.go- Insert status message
Deliverable: Model switch triggers agent restart; status message appears.
Phase 4: Session Resumption (Est. 2-3 days)
Goal: Resume ACP session after agent restart (where supported).
Files to modify:
apps/backend/internal/agent/lifecycle/session.go- Resume logicapps/backend/internal/orchestrator/executor/executor.go- Handle resume failures
Deliverable: Codex-style agents resume context; Auggie-style gracefully degrade.
Phase 5: Testing & Edge Cases (Est. 2-3 days)
Goal: Verify all scenarios work correctly.
Test scenarios:
- Model switch while agent idle
- Model switch while agent busy (queued)
- Rapid model changes
- Resume failure fallback
- Invalid model selection
- Backend restart mid-session
Summary of Files to Modify
| File | Changes |
|---|---|
apps/web/lib/state/store.ts |
Add pendingModel slice |
apps/web/components/task/task-chat-panel.tsx |
Integrate ModelSelector, pass model on prompt |
apps/web/components/task/model-selector.tsx |
New component |
apps/web/lib/ws/actions.ts |
Add model field to prompt action |
apps/backend/internal/ws/handlers/orchestrator.go |
Parse model from WS payload |
apps/backend/internal/orchestrator/service.go |
Extend PromptTask signature |
apps/backend/internal/orchestrator/executor/executor.go |
Add SwitchModel method |
apps/backend/internal/agent/lifecycle/manager.go |
Ensure clean restart support |
apps/backend/internal/agent/lifecycle/session.go |
Handle resume after restart |
apps/backend/internal/task/service/service.go |
Create model switch status message |
Estimated Total Effort
| Phase | Estimate |
|---|---|
| Phase 1: Frontend Model Selector | 2-3 days |
| Phase 2: Backend Prompt Extension | 2-3 days |
| Phase 3: Agent Restart Logic | 3-4 days |
| Phase 4: Session Resumption | 2-3 days |
| Phase 5: Testing & Edge Cases | 2-3 days |
| Total | 11-16 days |
Resolved Decisions
| Decision | Resolution |
|---|---|
| Database schema | Runtime-only, no schema changes |
| Non-recoverable agents (Auggie) | Accept context loss, show "new session started" message |
| Agent busy during switch | Not an issue - model applied on next user prompt |
| UI placement | Replace existing agent selector with model selector |
| Confirmation dialog | None required - seamless switch |