feat(coding-agent): introduced fine grained per agent advisor configuration

- Replaced blanket subagent advisor global settings with fine-grained per-agent configuration and frontmatter support.
- Added dashboard keybindings and inline override editors for managing agent advisor patterns.
- Implemented settings migration logic to convert legacy global options into per-agent settings.
- Updated session persistence and execution layers to restore and enforce per-agent advisor behaviors.
This commit is contained in:
can1357
2026-08-13 04:59:50 +02:00
parent 1132c3e31c
commit e8eed95130
27 changed files with 586 additions and 91 deletions
@@ -40,6 +40,7 @@ import type { ModelRegistry } from "../../config/model-registry";
import {
formatModelString,
normalizeModelPatternList,
resolveAgentAdvisorSelection,
resolveAgentModelPatterns,
resolveAgentPrewalkPattern,
resolveConfiguredModelPatterns,
@@ -78,6 +79,8 @@ interface DashboardAgent extends AgentDefinition {
overrideModel?: string;
/** `task.agentPrewalk` value for this agent: "on", "off", or a model pattern. */
prewalkOverride?: string;
/** `task.agentAdvisor` value for this agent: "on", "off", or a model pattern. */
advisorOverride?: string;
}
interface ModelResolution {
@@ -85,6 +88,20 @@ interface ModelResolution {
thinkingLevel?: string;
explicitThinkingLevel: boolean;
}
/** Which per-agent settings override the inline editor is editing. */
type OverrideEditKind = "model" | "prewalk" | "advisor";
const OVERRIDE_EDIT_META: Record<OverrideEditKind, { title: string; hint: string }> = {
model: { title: "Model override", hint: "Enter model pattern (empty clears override)" },
prewalk: {
title: "Prewalk override",
hint: 'Enter "on", "off", or a prewalk target model pattern (empty = agent default)',
},
advisor: {
title: "Advisor override",
hint: 'Enter "on", "off", or an advisor model pattern (empty = agent default)',
},
};
interface GeneratedAgentSpec {
identifier: string;
@@ -111,7 +128,7 @@ const SOURCE_LABEL: Record<AgentSource, string> = {
};
const LIST_FOOTER =
" ↑/↓: navigate Space: toggle Enter: model override P: prewalk N: new agent ←/→: source Ctrl+R: reload Esc: close";
" ↑/↓: navigate Space: toggle Enter: model override P: prewalk A: advisor N: new agent ←/→: source Ctrl+R: reload Esc: close";
const IDENTIFIER_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+){1,5}$/;
function joinPatterns(patterns: string[]): string {
@@ -266,6 +283,8 @@ class AgentInspectorPane implements Component {
private readonly effectiveResolution: ModelResolution | undefined,
private readonly prewalkPattern: string | undefined,
private readonly prewalkResolution: ModelResolution | undefined,
private readonly advisorPattern: string | undefined,
private readonly advisorResolution: ModelResolution | undefined,
) {}
render(width: number): readonly string[] {
@@ -296,6 +315,7 @@ class AgentInspectorPane implements Component {
`${theme.fg("muted", "Effective:")} ${this.effectiveResolution ? this.#formatResolution(this.effectiveResolution) : theme.fg("dim", "(unresolved)")}`,
);
lines.push(`${theme.fg("muted", "Prewalk:")} ${this.#prewalkLabel()}`);
lines.push(`${theme.fg("muted", "Advisor:")} ${this.#advisorLabel()}`);
if (this.agent.filePath) {
lines.push("");
@@ -330,6 +350,23 @@ class AgentInspectorPane implements Component {
: theme.fg("dim", "(unresolved)");
return `${theme.fg("success", "on")} ${theme.fg("dim", `${replaceTabs(this.prewalkPattern)} →`)} ${target}${sourceTag}`;
}
/** "off", "on → advisor model" (with source: agent default vs override), or the unresolved pattern. */
#advisorLabel(): string {
if (!this.agent) return theme.fg("dim", "off");
const override = this.agent.advisorOverride?.trim();
const sourceTag = override
? theme.fg("warning", " (override)")
: this.agent.advisor !== undefined && this.agent.advisor !== false
? theme.fg("dim", " (agent default)")
: "";
if (!this.advisorPattern) {
return `${theme.fg("dim", "off")}${override ? sourceTag : ""}`;
}
const target = this.advisorResolution
? this.#formatResolution(this.advisorResolution)
: theme.fg("dim", "(unresolved)");
return `${theme.fg("success", "on")} ${theme.fg("dim", `${replaceTabs(this.advisorPattern)} →`)} ${target}${sourceTag}`;
}
#formatResolution(resolution: ModelResolution): string {
return formatResolution(resolution);
@@ -386,6 +423,7 @@ export class AgentDashboard extends Container {
#builtCols = -1;
#editInput: Input | null = null;
#editKind: OverrideEditKind = "model";
#editingAgentName: string | null = null;
#createInput: Editor | null = null;
@@ -437,6 +475,7 @@ export class AgentDashboard extends Container {
const disabled = new Set((this.#settingsManager?.get("task.disabledAgents") as string[] | undefined) ?? []);
const overrides = this.#settingsManager?.get("task.agentModelOverrides") ?? {};
const prewalkOverrides = this.#settingsManager?.get("task.agentPrewalk") ?? {};
const advisorOverrides = this.#settingsManager?.get("task.agentAdvisor") ?? {};
this.#allAgents = agents
.slice()
@@ -450,6 +489,7 @@ export class AgentDashboard extends Container {
disabled: disabled.has(agent.name),
overrideModel: normalizeModelPatternList(overrides[agent.name]).join(",") || undefined,
prewalkOverride: prewalkOverrides[agent.name]?.trim() || undefined,
advisorOverride: advisorOverrides[agent.name]?.trim() || undefined,
}));
this.#tabs = this.#buildTabs(this.#allAgents);
@@ -596,20 +636,16 @@ export class AgentDashboard extends Container {
this.#settingsManager.set("task.agentPrewalk", overrides);
}
/** Cycle the prewalk override for the selected agent: agent default → on → off → agent default. */
#cyclePrewalkOverride(): void {
const selected = this.#selectedAgent();
if (!selected) return;
const current = selected.prewalkOverride?.trim().toLowerCase();
selected.prewalkOverride = current === undefined || current === "" ? "on" : current === "on" ? "off" : undefined;
this.#persistPrewalkOverrides();
const pattern = resolveAgentPrewalkPattern({
settingsOverride: selected.prewalkOverride,
agentPrewalk: resolveAgentPrewalkDefault(selected, this.#settingsManager?.get("task.prewalk") ?? false),
});
const state = selected.prewalkOverride ?? "agent default";
this.#notice = `Prewalk for ${selected.name}: ${state}${pattern ? ` (into ${pattern})` : ""}`;
this.#buildLayout();
#persistAdvisorOverrides(): void {
if (!this.#settingsManager) return;
const overrides: Record<string, string> = {};
for (const agent of this.#allAgents) {
const value = agent.advisorOverride?.trim();
if (value) {
overrides[agent.name] = value;
}
}
this.#settingsManager.set("task.agentAdvisor", overrides);
}
#toggleSelectedAgent(): void {
@@ -620,41 +656,74 @@ export class AgentDashboard extends Container {
this.#buildLayout();
}
#beginModelEdit(): void {
#overrideValueFor(agent: DashboardAgent, kind: OverrideEditKind): string | undefined {
switch (kind) {
case "model":
return agent.overrideModel;
case "prewalk":
return agent.prewalkOverride;
case "advisor":
return agent.advisorOverride;
}
}
#beginOverrideEdit(kind: OverrideEditKind): void {
const selected = this.#selectedAgent();
if (!selected) return;
this.#createError = null;
this.#editKind = kind;
this.#editingAgentName = selected.name;
this.#editInput = new Input();
if (selected.overrideModel) {
this.#editInput.setValue(selected.overrideModel);
const current = this.#overrideValueFor(selected, kind);
if (current) {
this.#editInput.setValue(current);
}
this.#editInput.onSubmit = value => {
this.#saveModelOverride(value);
this.#saveOverrideEdit(value);
};
this.#buildLayout();
}
#saveModelOverride(rawValue: string): void {
#saveOverrideEdit(rawValue: string): void {
if (!this.#editingAgentName) return;
const selected = this.#allAgents.find(agent => agent.name === this.#editingAgentName);
if (!selected) return;
const value = rawValue.trim();
selected.overrideModel = value || undefined;
this.#persistModelOverrides();
const kind = this.#editKind;
const value = rawValue.trim() || undefined;
switch (kind) {
case "model":
selected.overrideModel = value;
this.#persistModelOverrides();
this.#notice = `Updated model override for ${selected.name}`;
break;
case "prewalk": {
selected.prewalkOverride = value;
this.#persistPrewalkOverrides();
const pattern = resolveAgentPrewalkPattern({
settingsOverride: value,
agentPrewalk: resolveAgentPrewalkDefault(selected, this.#settingsManager?.get("task.prewalk") ?? false),
});
this.#notice = `Prewalk for ${selected.name}: ${value ?? "agent default"}${pattern ? ` (into ${pattern})` : " (off)"}`;
break;
}
case "advisor": {
selected.advisorOverride = value;
this.#persistAdvisorOverrides();
const selection = resolveAgentAdvisorSelection({ settingsOverride: value, agentAdvisor: selected.advisor });
this.#notice = `Advisor for ${selected.name}: ${value ?? "agent default"}${selection ? ` (advised by ${selection.model ?? "@advisor"})` : " (off)"}`;
break;
}
}
this.#editingAgentName = null;
this.#editInput = null;
this.#applyFilters();
this.#notice = `Updated model override for ${selected.name}`;
this.#buildLayout();
}
#cancelModelEdit(): void {
#cancelOverrideEdit(): void {
this.#editingAgentName = null;
this.#editInput = null;
this.#buildLayout();
}
#beginCreateFlow(): void {
if (this.#createGenerating) return;
this.#createError = null;
@@ -1032,39 +1101,87 @@ export class AgentDashboard extends Container {
this.#renderCreateInput();
} else if (this.#editInput && this.#editingAgentName) {
const editingAgent = this.#allAgents.find(agent => agent.name === this.#editingAgentName) ?? null;
const kind = this.#editKind;
const meta = OVERRIDE_EDIT_META[kind];
const draft = this.#editInput.getValue();
const defaultPatterns = editingAgent ? this.#defaultPatternsFor(editingAgent) : [];
const defaultResolution = editingAgent ? this.#resolvePatterns(defaultPatterns) : undefined;
const previewPatterns = editingAgent ? this.#effectivePatternsFor(editingAgent, draft) : [];
const previewResolution = editingAgent ? this.#resolvePatterns(previewPatterns) : undefined;
const suggestions = this.#getModelSuggestions(draft);
this.addChild(
new Text(theme.bold(theme.fg("accent", `Model override: ${replaceTabs(this.#editingAgentName)}`)), 0, 0),
new Text(theme.bold(theme.fg("accent", `${meta.title}: ${replaceTabs(this.#editingAgentName)}`)), 0, 0),
);
this.addChild(new Spacer(1));
this.addChild(new Text(theme.fg("muted", "Enter model pattern (empty clears override)"), 0, 0));
this.addChild(new Text(theme.fg("muted", meta.hint), 0, 0));
this.addChild(new Spacer(1));
this.addChild(this.#editInput);
this.addChild(new Spacer(1));
this.addChild(
new Text(theme.fg("muted", `Default pattern: ${replaceTabs(joinPatterns(defaultPatterns))}`), 0, 0),
);
this.addChild(
new Text(
`${theme.fg("muted", "Default resolves:")} ${defaultResolution ? formatResolution(defaultResolution) : theme.fg("dim", "(unresolved)")}`,
0,
0,
),
);
this.addChild(
new Text(
`${theme.fg("muted", "Preview effective:")} ${previewResolution ? formatResolution(previewResolution) : theme.fg("dim", "(unresolved)")}`,
0,
0,
),
);
if (kind === "model") {
const defaultPatterns = editingAgent ? this.#defaultPatternsFor(editingAgent) : [];
const defaultResolution = editingAgent ? this.#resolvePatterns(defaultPatterns) : undefined;
const previewPatterns = editingAgent ? this.#effectivePatternsFor(editingAgent, draft) : [];
const previewResolution = editingAgent ? this.#resolvePatterns(previewPatterns) : undefined;
this.addChild(
new Text(theme.fg("muted", `Default pattern: ${replaceTabs(joinPatterns(defaultPatterns))}`), 0, 0),
);
this.addChild(
new Text(
`${theme.fg("muted", "Default resolves:")} ${defaultResolution ? formatResolution(defaultResolution) : theme.fg("dim", "(unresolved)")}`,
0,
0,
),
);
this.addChild(
new Text(
`${theme.fg("muted", "Preview effective:")} ${previewResolution ? formatResolution(previewResolution) : theme.fg("dim", "(unresolved)")}`,
0,
0,
),
);
} else if (editingAgent) {
// Prewalk/advisor: preview the effective state the draft override
// would produce, resolving "on"/"off"/pattern against the agent
// definition's own default.
const previewPattern =
kind === "prewalk"
? resolveAgentPrewalkPattern({
settingsOverride: draft,
agentPrewalk: resolveAgentPrewalkDefault(
editingAgent,
this.#settingsManager?.get("task.prewalk") ?? false,
),
})
: (() => {
const selection = resolveAgentAdvisorSelection({
settingsOverride: draft,
agentAdvisor: editingAgent.advisor,
});
return selection ? (selection.model ?? "@advisor") : undefined;
})();
const previewResolution = previewPattern ? this.#resolvePatterns([previewPattern]) : undefined;
const agentDefault = kind === "prewalk" ? editingAgent.prewalk : editingAgent.advisor;
this.addChild(
new Text(
`${theme.fg("muted", "Agent default:")} ${
agentDefault === undefined || agentDefault === false
? theme.fg("dim", "off")
: replaceTabs(String(agentDefault))
}`,
0,
0,
),
);
this.addChild(
new Text(
`${theme.fg("muted", "Preview effective:")} ${
previewPattern
? `${theme.fg("success", "on")} ${theme.fg("dim", `${replaceTabs(previewPattern)} →`)} ${previewResolution ? formatResolution(previewResolution) : theme.fg("dim", "(unresolved)")}`
: theme.fg("dim", "off")
}`,
0,
0,
),
);
}
if (suggestions.length > 0) {
this.addChild(new Spacer(1));
@@ -1092,6 +1209,14 @@ export class AgentDashboard extends Container {
})
: undefined;
const prewalkResolution = prewalkPattern ? this.#resolvePatterns([prewalkPattern]) : undefined;
const advisorSelection = selected
? resolveAgentAdvisorSelection({
settingsOverride: selected.advisorOverride,
agentAdvisor: selected.advisor,
})
: undefined;
const advisorPattern = advisorSelection ? (advisorSelection.model ?? "@advisor") : undefined;
const advisorResolution = advisorPattern ? this.#resolvePatterns([advisorPattern]) : undefined;
const listPane = new AgentListPane(
this.#filteredAgents,
@@ -1108,6 +1233,8 @@ export class AgentDashboard extends Container {
effectiveResolution,
prewalkPattern,
prewalkResolution,
advisorPattern,
advisorResolution,
);
const bodyHeight = this.#computeBodyHeight();
this.addChild(new TwoColumnBody(listPane, inspector, bodyHeight));
@@ -1180,7 +1307,7 @@ export class AgentDashboard extends Container {
if (this.#editInput) {
if (matchesAppInterrupt(data)) {
this.#cancelModelEdit();
this.#cancelOverrideEdit();
return;
}
this.#editInput.handleInput(data);
@@ -1224,7 +1351,7 @@ export class AgentDashboard extends Container {
return;
}
if (matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n") {
this.#beginModelEdit();
this.#beginOverrideEdit("model");
return;
}
if (data.toLowerCase() === "n") {
@@ -1232,7 +1359,13 @@ export class AgentDashboard extends Container {
return;
}
if (data.toLowerCase() === "p") {
this.#cyclePrewalkOverride();
this.#beginOverrideEdit("prewalk");
return;
}
// Uppercase-only: lowercase `a` stays available for search-typing agent
// names ("task", "librarian", ...), unlike the rarely-typed n/p letters.
if (data === "A") {
this.#beginOverrideEdit("advisor");
return;
}