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
- 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
## 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
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,
* the engine uses pi's automatic model resolution. */
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.
* Controls how much reasoning effort the model uses — higher levels
* produce better results but cost more. When undefined, the engine
@@ -342,6 +358,10 @@ export const DEFAULT_SETTINGS: Settings = {
includeTaskIdInCommit: true,
defaultProvider: undefined,
defaultModelId: undefined,
planningProvider: undefined,
planningModelId: undefined,
validatorProvider: undefined,
validatorModelId: undefined,
defaultThinkingLevel: undefined,
autoResolveConflicts: true,
smartConflictResolution: true,

View File

@@ -269,6 +269,12 @@ export function SettingsModal({
const selectedValue = 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 (
<>
<h4 className="settings-section-heading">Model</h4>
@@ -279,6 +285,7 @@ export function SettingsModal({
No models available. Configure authentication first.
</div>
) : (
<>
<div className="form-group">
<label htmlFor="defaultModel">Default Model</label>
<CustomModelDropdown
@@ -300,8 +307,55 @@ export function SettingsModal({
}}
placeholder="Use default"
/>
<small>Select the AI model used for agent sessions. &quot;Use default&quot; lets the engine choose automatically.</small>
<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(

View File

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

View File

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