feat(KB-147): add planning and validator model settings

- Extend core Settings types with planningProvider, planningModelId, validatorProvider, validatorModelId fields
- Add model selectors in Settings UI for planning and validator models
- Use planning model for triage task specification generation
- Use validator model for specification review in triage
- Use validator model for code reviews in executor agent
- Update AGENTS.md with model hierarchy documentation
This commit is contained in:
gsxdsm
2026-03-31 00:02:16 -07:00
parent 9e8cf21058
commit c71dba7908
6 changed files with 147 additions and 28 deletions

View File

@@ -0,0 +1,13 @@
---
"@dustinbyrne/kb": minor
---
Add separate planning and verification model settings
Users can now configure separate AI models for:
- **Planning Model**: Used for triage/specification when writing PROMPT.md files
- **Validator Model**: Used for code and specification review
Both settings are available in the dashboard under Settings > Model, alongside the existing Default Model setting. When not configured, they fall back to the Default Model.
This allows using specialized models for different phases of task execution - for example, a fast/cheap model for planning and a powerful model for verification.

View File

@@ -286,9 +286,31 @@ To clear overrides, select "Use default" for both fields and save.
### Limitations ### Limitations
- Triage (task specification) always uses global defaults — per-task overrides apply only to execution and review - **Planning Model**: Task specification (triage) uses the global `planningProvider`/`planningModelId` settings — there is no per-task override for planning
- Both provider and modelId must be set together; partial configuration falls back to defaults - Both provider and modelId must be set together; partial configuration falls back to defaults
## Model Settings Hierarchy
The system uses the following precedence for model selection:
**For Task Specification (Triage):**
1. Global `planningProvider`/`planningModelId` (if both set)
2. Global `defaultProvider`/`defaultModelId` (if both set)
3. Automatic model resolution (fallback)
**For Task Execution (Executor):**
1. Per-task `modelProvider`/`modelId` (if both set)
2. Global `defaultProvider`/`defaultModelId` (if both set)
3. Automatic model resolution (fallback)
**For Code/Spec Review (Reviewer):**
1. Per-task `validatorModelProvider`/`validatorModelId` (if both set)
2. Global `validatorProvider`/`validatorModelId` (if both set)
3. Global `defaultProvider`/`defaultModelId` (if both set)
4. Automatic model resolution (fallback)
Configure global model settings in the dashboard under **Settings > Model**.
## Archive Cleanup ## Archive Cleanup
Archived tasks can be cleaned up from the filesystem to reduce storage overhead while preserving the ability to restore them later. Archived tasks can be cleaned up from the filesystem to reduce storage overhead while preserving the ability to restore them later.

View File

@@ -290,6 +290,22 @@ export interface Settings {
* Must be set together with `defaultProvider`. When both are undefined, * Must be set together with `defaultProvider`. When both are undefined,
* the engine uses pi's automatic model resolution. */ * the engine uses pi's automatic model resolution. */
defaultModelId?: string; defaultModelId?: string;
/** AI model provider for planning/triage (specification) agent.
* Must be set together with `planningModelId`. When both are undefined,
* falls back to `defaultProvider`/`defaultModelId`. */
planningProvider?: string;
/** AI model ID for planning/triage (specification) agent.
* Must be set together with `planningProvider`. When both are undefined,
* falls back to `defaultProvider`/`defaultModelId`. */
planningModelId?: string;
/** AI model provider for validator/reviewer agent.
* Must be set together with `validatorModelId`. When both are undefined,
* falls back to `defaultProvider`/`defaultModelId`. */
validatorProvider?: string;
/** AI model ID for validator/reviewer agent.
* Must be set together with `validatorProvider`. When both are undefined,
* falls back to `defaultProvider`/`defaultModelId`. */
validatorModelId?: string;
/** Default thinking effort level for AI agent sessions. /** Default thinking effort level for AI agent sessions.
* Controls how much reasoning effort the model uses — higher levels * Controls how much reasoning effort the model uses — higher levels
* produce better results but cost more. When undefined, the engine * produce better results but cost more. When undefined, the engine
@@ -342,6 +358,10 @@ export const DEFAULT_SETTINGS: Settings = {
includeTaskIdInCommit: true, includeTaskIdInCommit: true,
defaultProvider: undefined, defaultProvider: undefined,
defaultModelId: undefined, defaultModelId: undefined,
planningProvider: undefined,
planningModelId: undefined,
validatorProvider: undefined,
validatorModelId: undefined,
defaultThinkingLevel: undefined, defaultThinkingLevel: undefined,
autoResolveConflicts: true, autoResolveConflicts: true,
smartConflictResolution: true, smartConflictResolution: true,

View File

@@ -269,6 +269,12 @@ export function SettingsModal({
const selectedValue = form.defaultProvider && form.defaultModelId const selectedValue = form.defaultProvider && form.defaultModelId
? `${form.defaultProvider}/${form.defaultModelId}` ? `${form.defaultProvider}/${form.defaultModelId}`
: ""; : "";
const planningValue = form.planningProvider && form.planningModelId
? `${form.planningProvider}/${form.planningModelId}`
: "";
const validatorValue = form.validatorProvider && form.validatorModelId
? `${form.validatorProvider}/${form.validatorModelId}`
: "";
return ( return (
<> <>
<h4 className="settings-section-heading">Model</h4> <h4 className="settings-section-heading">Model</h4>
@@ -279,29 +285,77 @@ export function SettingsModal({
No models available. Configure authentication first. No models available. Configure authentication first.
</div> </div>
) : ( ) : (
<div className="form-group"> <>
<label htmlFor="defaultModel">Default Model</label> <div className="form-group">
<CustomModelDropdown <label htmlFor="defaultModel">Default Model</label>
id="defaultModel" <CustomModelDropdown
label="Default Model" id="defaultModel"
models={availableModels} label="Default Model"
value={selectedValue} models={availableModels}
onChange={(val) => { value={selectedValue}
if (!val) { onChange={(val) => {
setForm((f) => ({ ...f, defaultProvider: undefined, defaultModelId: undefined })); if (!val) {
} else { setForm((f) => ({ ...f, defaultProvider: undefined, defaultModelId: undefined }));
const slashIdx = val.indexOf("/"); } else {
setForm((f) => ({ const slashIdx = val.indexOf("/");
...f, setForm((f) => ({
defaultProvider: val.slice(0, slashIdx), ...f,
defaultModelId: val.slice(slashIdx + 1), defaultProvider: val.slice(0, slashIdx),
})); defaultModelId: val.slice(slashIdx + 1),
} }));
}} }
placeholder="Use default" }}
/> placeholder="Use default"
<small>Select the AI model used for agent sessions. &quot;Use default&quot; lets the engine choose automatically.</small> />
</div> <small>Default AI model used for task execution when no per-task override is set. &quot;Use default&quot; lets the engine choose automatically.</small>
</div>
<div className="form-group">
<label htmlFor="planningModel">Planning Model</label>
<CustomModelDropdown
id="planningModel"
label="Planning Model"
models={availableModels}
value={planningValue}
onChange={(val) => {
if (!val) {
setForm((f) => ({ ...f, planningProvider: undefined, planningModelId: undefined }));
} else {
const slashIdx = val.indexOf("/");
setForm((f) => ({
...f,
planningProvider: val.slice(0, slashIdx),
planningModelId: val.slice(slashIdx + 1),
}));
}
}}
placeholder="Use default"
/>
<small>AI model used for task planning and specification (triage). Falls back to Default Model when not set.</small>
</div>
<div className="form-group">
<label htmlFor="validatorModel">Validator Model</label>
<CustomModelDropdown
id="validatorModel"
label="Validator Model"
models={availableModels}
value={validatorValue}
onChange={(val) => {
if (!val) {
setForm((f) => ({ ...f, validatorProvider: undefined, validatorModelId: undefined }));
} else {
const slashIdx = val.indexOf("/");
setForm((f) => ({
...f,
validatorProvider: val.slice(0, slashIdx),
validatorModelId: val.slice(slashIdx + 1),
}));
}
}}
placeholder="Use default"
/>
<small>AI model used for code and specification review. Falls back to Default Model when not set.</small>
</div>
</>
)} )}
{(() => { {(() => {
const selectedModel = availableModels.find( const selectedModel = availableModels.find(

View File

@@ -851,8 +851,9 @@ export class TaskExecutor {
defaultProvider: settings.defaultProvider, defaultProvider: settings.defaultProvider,
defaultModelId: settings.defaultModelId, defaultModelId: settings.defaultModelId,
defaultThinkingLevel: settings.defaultThinkingLevel, defaultThinkingLevel: settings.defaultThinkingLevel,
validatorModelProvider: detail.validatorModelProvider, // Per-task validator overrides take precedence over global validator settings
validatorModelId: detail.validatorModelId, validatorModelProvider: detail.validatorModelProvider ?? settings.validatorProvider,
validatorModelId: detail.validatorModelId ?? settings.validatorModelId,
store, store,
taskId, taskId,
}, },

View File

@@ -465,8 +465,13 @@ export class TriageProcessor {
onThinking: agentLogger.onThinking, onThinking: agentLogger.onThinking,
onToolStart: agentLogger.onToolStart, onToolStart: agentLogger.onToolStart,
onToolEnd: agentLogger.onToolEnd, onToolEnd: agentLogger.onToolEnd,
defaultProvider: settings.defaultProvider, // Use planning model settings if both provider and modelId are set, otherwise fall back to defaults
defaultModelId: settings.defaultModelId, defaultProvider: settings.planningProvider && settings.planningModelId
? settings.planningProvider
: settings.defaultProvider,
defaultModelId: settings.planningProvider && settings.planningModelId
? settings.planningModelId
: settings.defaultModelId,
defaultThinkingLevel: settings.defaultThinkingLevel, defaultThinkingLevel: settings.defaultThinkingLevel,
}); });
@@ -846,6 +851,8 @@ export class TriageProcessor {
defaultProvider?: string; defaultProvider?: string;
defaultModelId?: string; defaultModelId?: string;
defaultThinkingLevel?: string; defaultThinkingLevel?: string;
validatorProvider?: string;
validatorModelId?: string;
}, },
): ToolDefinition { ): ToolDefinition {
const store = this.store; const store = this.store;
@@ -904,6 +911,8 @@ export class TriageProcessor {
onText: (delta) => options.onAgentText?.(taskId, delta), onText: (delta) => options.onAgentText?.(taskId, delta),
defaultProvider: settings.defaultProvider, defaultProvider: settings.defaultProvider,
defaultModelId: settings.defaultModelId, defaultModelId: settings.defaultModelId,
validatorModelProvider: settings.validatorProvider,
validatorModelId: settings.validatorModelId,
defaultThinkingLevel: settings.defaultThinkingLevel, defaultThinkingLevel: settings.defaultThinkingLevel,
store, store,
taskId, taskId,