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:
@@ -1195,6 +1195,42 @@ export function resolveAgentPrewalkPattern(options: AgentPrewalkResolutionOption
|
||||
return agentPattern;
|
||||
}
|
||||
|
||||
export interface AgentAdvisorResolutionOptions {
|
||||
/** `task.agentAdvisor` settings value for this agent: `"on"`, `"off"`, or a model pattern. */
|
||||
settingsOverride?: string;
|
||||
/** Agent definition `advisor` frontmatter: `true` = default advisor-role model, string = custom model pattern. */
|
||||
agentAdvisor?: boolean | string;
|
||||
}
|
||||
|
||||
/** Effective advisor for one spawned agent: absent `model` resolves through the `advisor` role. */
|
||||
export interface AgentAdvisorSelection {
|
||||
model?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Effective advisor selection for a subagent, or `undefined` when the agent
|
||||
* runs unadvised. The settings override decides enablement first ("off" wins,
|
||||
* "on" enables with the agent's own model pattern or the `advisor` role, any
|
||||
* other value is a custom model pattern); otherwise the agent definition's
|
||||
* `advisor` field applies. A returned pattern lands on the spawned session's
|
||||
* `modelRoles.advisor`, so role aliases and `:level` suffixes resolve there.
|
||||
*/
|
||||
export function resolveAgentAdvisorSelection(
|
||||
options: AgentAdvisorResolutionOptions,
|
||||
): AgentAdvisorSelection | undefined {
|
||||
const agentPattern =
|
||||
typeof options.agentAdvisor === "string" && options.agentAdvisor.trim() ? options.agentAdvisor.trim() : undefined;
|
||||
const override = options.settingsOverride?.trim();
|
||||
if (override) {
|
||||
const lowered = override.toLowerCase();
|
||||
if (lowered === "off" || lowered === "false") return undefined;
|
||||
if (lowered === "on" || lowered === "true") return { model: agentPattern };
|
||||
return { model: override };
|
||||
}
|
||||
if (options.agentAdvisor === true) return {};
|
||||
return agentPattern ? { model: agentPattern } : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a model role value into a concrete model and thinking metadata.
|
||||
*/
|
||||
|
||||
@@ -468,17 +468,6 @@ export const SETTINGS_SCHEMA = {
|
||||
"Start on the active model, then switch to a fast/cheap model (default the 'smol' role) at the first edit/write after the plan nudge's todo list exists — the strong model plans, commits the todos, and starts the implementation before handing off. Overridable per session with --prewalk / --no-prewalk.",
|
||||
},
|
||||
},
|
||||
"advisor.subagents": {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
ui: {
|
||||
tab: "model",
|
||||
group: "Advisor",
|
||||
label: "Advisor for Subagents",
|
||||
description: "Also enable the advisor on spawned task/eval subagents.",
|
||||
condition: "advisorEnabled",
|
||||
},
|
||||
},
|
||||
"advisor.syncBacklog": {
|
||||
type: "enum",
|
||||
values: ["off", "1", "3", "5"] as const,
|
||||
@@ -4743,6 +4732,10 @@ export const SETTINGS_SCHEMA = {
|
||||
type: "record",
|
||||
default: {} as Record<string, string>,
|
||||
},
|
||||
"task.agentAdvisor": {
|
||||
type: "record",
|
||||
default: {} as Record<string, string>,
|
||||
},
|
||||
"task.prewalk": {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
@@ -4751,7 +4744,7 @@ export const SETTINGS_SCHEMA = {
|
||||
group: "Subagents",
|
||||
label: "Generic Task Prewalk",
|
||||
description:
|
||||
"Arm prewalk for the bundled generic `task` subagent: it starts on its resolved model, plans and begins the implementation, then hands off to the 'smol' role at its first edit/write. Per-agent overrides (task.agentPrewalk, toggled with P in /agents) and user agent `prewalk` frontmatter apply regardless of this toggle.",
|
||||
"Arm prewalk for the bundled generic `task` subagent: it starts on its resolved model, plans and begins the implementation, then hands off to the 'smol' role at its first edit/write. Per-agent overrides (task.agentPrewalk, edited with P in /agents) and user agent `prewalk` frontmatter apply regardless of this toggle.",
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -1786,6 +1786,26 @@ export class Settings {
|
||||
if (tierTouched) raw.tier = tierObj;
|
||||
delete raw.fastModeScope;
|
||||
|
||||
// advisor.subagents (blanket advisor on every spawned subagent) → per-agent
|
||||
// task.agentAdvisor, migrated to the bundled generic `task` agent. An
|
||||
// explicit boolean maps to "on"/"off" IN THE SAME LAYER — migration runs
|
||||
// per file, so a project-level `false` must keep overriding a global
|
||||
// `true` after both layers migrate.
|
||||
{
|
||||
const advisorObj = isRecord(raw.advisor) ? raw.advisor : undefined;
|
||||
const legacySubagents =
|
||||
advisorObj && "subagents" in advisorObj ? advisorObj.subagents : raw["advisor.subagents"];
|
||||
if (typeof legacySubagents === "boolean") {
|
||||
const taskObj = isRecord(raw.task) ? raw.task : {};
|
||||
const agentAdvisor = isRecord(taskObj.agentAdvisor) ? taskObj.agentAdvisor : {};
|
||||
if (!("task" in agentAdvisor)) agentAdvisor.task = legacySubagents ? "on" : "off";
|
||||
taskObj.agentAdvisor = agentAdvisor;
|
||||
raw.task = taskObj;
|
||||
}
|
||||
if (advisorObj) delete advisorObj.subagents;
|
||||
delete raw["advisor.subagents"];
|
||||
}
|
||||
|
||||
// v17 renames that used to nest under a boolean parent path:
|
||||
// dev.autoqa.consent -> dev.autoqaConsent
|
||||
// todo.reminders.max -> todo.remindersMax
|
||||
|
||||
@@ -245,6 +245,8 @@ export interface ParsedAgentFields {
|
||||
blocking?: boolean;
|
||||
/** `true` = prewalk into the default target; string = prewalk into that model pattern. */
|
||||
prewalk?: boolean | string;
|
||||
/** `true` = advise with the default advisor-role model; string = advise with that model pattern. */
|
||||
advisor?: boolean | string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -305,6 +307,12 @@ export function parseAgentFields(frontmatter: Record<string, unknown>): ParsedAg
|
||||
const trimmed = frontmatter.prewalk.trim();
|
||||
if (trimmed) prewalk = trimmed;
|
||||
}
|
||||
// advisor: true → advise with the default advisor-role model; "<pattern>" → custom advisor model.
|
||||
let advisor: boolean | string | undefined = parseBoolean(frontmatter.advisor);
|
||||
if (advisor === undefined && typeof frontmatter.advisor === "string") {
|
||||
const trimmed = frontmatter.advisor.trim();
|
||||
if (trimmed) advisor = trimmed;
|
||||
}
|
||||
const autoloadSkills = parseArrayOrCSV(frontmatter.autoloadSkills)
|
||||
?.map(s => s.trim())
|
||||
.filter(Boolean);
|
||||
@@ -320,6 +328,7 @@ export function parseAgentFields(frontmatter: Record<string, unknown>): ParsedAg
|
||||
autoloadSkills,
|
||||
readSummarize,
|
||||
prewalk,
|
||||
advisor,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -136,6 +136,7 @@ const HOST_DEFAULTED_SETTING_PATHS: SettingPath[] = [
|
||||
"task.disabledAgents",
|
||||
"task.agentModelOverrides",
|
||||
"task.agentPrewalk",
|
||||
"task.agentAdvisor",
|
||||
// Memory subsystems are off-by-default for RPC/ACP hosts; embedders that want
|
||||
// memory should opt in explicitly through their own settings layer.
|
||||
"memory.backend",
|
||||
@@ -144,7 +145,6 @@ const HOST_DEFAULTED_SETTING_PATHS: SettingPath[] = [
|
||||
// instead of inheriting a user's globally-enabled local preference, and when
|
||||
// they do opt in they get the default tuning rather than the user's local tuning.
|
||||
"advisor.enabled",
|
||||
"advisor.subagents",
|
||||
"advisor.syncBacklog",
|
||||
"advisor.immuneTurns",
|
||||
"tier.advisor",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ description: {{jsonStringify description}}
|
||||
{{/if}}{{#if thinkingLevel}}thinking-level: {{jsonStringify thinkingLevel}}
|
||||
{{/if}}{{#if blocking}}blocking: true
|
||||
{{/if}}{{#if prewalk}}prewalk: {{jsonStringify prewalk}}
|
||||
{{/if}}{{#if advisor}}advisor: {{jsonStringify advisor}}
|
||||
{{/if}}{{#if autoloadSkills}}autoloadSkills: {{jsonStringify autoloadSkills}}
|
||||
{{/if}}---
|
||||
{{body}}
|
||||
|
||||
@@ -1436,7 +1436,6 @@ export class AgentSession {
|
||||
onPayload: this.#onPayload,
|
||||
onResponse: this.#onResponse,
|
||||
onSseEvent: this.#onSseEvent,
|
||||
agentKind: () => this.#agentKind,
|
||||
isDisposed: () => this.#isDisposed,
|
||||
abortInProgress: () => this.#abortInProgress,
|
||||
allowAgentInitiatedTurns: () => this.#allowAcpAgentInitiatedTurns,
|
||||
@@ -9188,9 +9187,10 @@ export class AgentSession {
|
||||
|
||||
/**
|
||||
* Whether a live advisor agent is attached to this session. True only when
|
||||
* `advisor.enabled` is set AND a model resolved for the `advisor` role AND
|
||||
* the advisor applies to this agent kind — i.e. the actual runtime exists,
|
||||
* not merely the setting. Drives the status-line badge and `/dump advisor`.
|
||||
* `advisor.enabled` is set for this session (subagents opt in per agent via
|
||||
* frontmatter `advisor` / `task.agentAdvisor`) AND a model resolved for the
|
||||
* `advisor` role — i.e. the actual runtime exists, not merely the setting.
|
||||
* Drives the status-line badge and `/dump advisor`.
|
||||
*/
|
||||
isAdvisorActive(): boolean {
|
||||
return this.#advisors.isAdvisorActive();
|
||||
|
||||
@@ -242,7 +242,6 @@ export interface SessionAdvisorsHost {
|
||||
onPayload: SimpleStreamOptions["onPayload"] | undefined;
|
||||
onResponse: SimpleStreamOptions["onResponse"] | undefined;
|
||||
onSseEvent: SimpleStreamOptions["onSseEvent"] | undefined;
|
||||
agentKind(): "main" | "sub";
|
||||
isDisposed(): boolean;
|
||||
abortInProgress(): boolean;
|
||||
allowAgentInitiatedTurns(): boolean;
|
||||
@@ -657,7 +656,6 @@ export class SessionAdvisors {
|
||||
if (this.#host.isDisposed()) return false;
|
||||
if (this.#advisors.length > 0) return true;
|
||||
if (!this.#advisorEnabled) return false;
|
||||
if (this.#host.agentKind() !== "main" && !this.#host.settings.get("advisor.subagents")) return false;
|
||||
|
||||
// Rebuild the status map from scratch so removed/renamed advisors don't
|
||||
// leave stale entries. #resolveAdvisorRuntimeDescriptors populates every
|
||||
@@ -1626,9 +1624,10 @@ export class SessionAdvisors {
|
||||
|
||||
/**
|
||||
* Whether a live advisor agent is attached to this session. True only when
|
||||
* `advisor.enabled` is set AND a model resolved for the `advisor` role AND
|
||||
* the advisor applies to this agent kind — i.e. the actual runtime exists,
|
||||
* not merely the setting. Drives the status-line badge and `/dump advisor`.
|
||||
* `advisor.enabled` is set for this session (subagents opt in per agent via
|
||||
* frontmatter `advisor` / `task.agentAdvisor`) AND a model resolved for the
|
||||
* `advisor` role — i.e. the actual runtime exists, not merely the setting.
|
||||
* Drives the status-line badge and `/dump advisor`.
|
||||
*/
|
||||
isAdvisorActive(): boolean {
|
||||
return this.#advisors.length > 0;
|
||||
|
||||
@@ -227,6 +227,8 @@ export interface SessionInitEntry extends SessionEntryBase {
|
||||
spawns?: string;
|
||||
/** The agent's `readSummarize` setting (`false` = read summarization disabled); absent uses the session default. */
|
||||
readSummarize?: boolean;
|
||||
/** Effective advisor for this subagent: `"on"` = advisor-role model, else an explicit model pattern; absent = unadvised. */
|
||||
advisor?: string;
|
||||
}
|
||||
|
||||
/** Mode change entry - tracks agent mode transitions (e.g. plan mode). */
|
||||
|
||||
@@ -2142,6 +2142,7 @@ export class SessionManager {
|
||||
restrictToolNames?: boolean;
|
||||
spawns?: string;
|
||||
readSummarize?: boolean;
|
||||
advisor?: string;
|
||||
}): string {
|
||||
const entry: SessionInitEntry = { type: "session_init", ...this.#freshEntryFields(), ...init };
|
||||
this.#recordEntry(entry);
|
||||
@@ -2634,6 +2635,7 @@ export class SessionManager {
|
||||
restrictToolNames?: boolean;
|
||||
spawns?: string;
|
||||
readSummarize?: boolean;
|
||||
advisor?: string;
|
||||
} | null;
|
||||
} | null> {
|
||||
let loaded: FileEntry[];
|
||||
@@ -2657,6 +2659,7 @@ export class SessionManager {
|
||||
restrictToolNames?: boolean;
|
||||
spawns?: string;
|
||||
readSummarize?: boolean;
|
||||
advisor?: string;
|
||||
} | null = null;
|
||||
for (let index = loaded.length - 1; index >= 0; index--) {
|
||||
const entry = loaded[index];
|
||||
@@ -2673,6 +2676,7 @@ export class SessionManager {
|
||||
restrictToolNames: entry.restrictToolNames,
|
||||
readSummarize: entry.readSummarize,
|
||||
spawns: entry.spawns,
|
||||
advisor: entry.advisor,
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ interface AgentFrontmatter {
|
||||
thinkingLevel?: string;
|
||||
blocking?: boolean;
|
||||
prewalk?: boolean | string;
|
||||
advisor?: boolean | string;
|
||||
}
|
||||
|
||||
interface EmbeddedAgentDef {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { ModelRegistry } from "../config/model-registry";
|
||||
import {
|
||||
formatModelSelectorValue,
|
||||
formatModelStringWithRouting,
|
||||
resolveAgentAdvisorSelection,
|
||||
resolveAgentPrewalkPattern,
|
||||
resolveConfiguredModelPatterns,
|
||||
resolveExplicitModelRole,
|
||||
@@ -894,6 +895,9 @@ export function createSubagentSettings(
|
||||
// the parent task approval is the authorization boundary. Use yolo mode
|
||||
// to preserve unattended subagent execution. User `tools.approval` policies still apply.
|
||||
"tools.approvalMode": "yolo",
|
||||
// Subagents run unadvised by default; runSubprocess opts a spawn back in
|
||||
// per agent (frontmatter `advisor` / `task.agentAdvisor`) via overrides.
|
||||
"advisor.enabled": false,
|
||||
...overrides,
|
||||
},
|
||||
{ storage: baseSettings.getStorage() },
|
||||
@@ -2690,12 +2694,26 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
||||
}
|
||||
|
||||
const settings = options.settings ?? Settings.isolated();
|
||||
// Per-agent advisor: the agent definition's `advisor` frontmatter or the
|
||||
// `task.agentAdvisor` settings override (agent name → "on"/"off"/model
|
||||
// pattern) pairs the spawned session with an advisor. Subagents default to
|
||||
// no advisor (createSubagentSettings forces `advisor.enabled` off); an
|
||||
// explicit model pattern lands on the child's `modelRoles.advisor` so role
|
||||
// aliases and `:level` suffixes resolve inside the spawned session.
|
||||
const advisorSelection = resolveAgentAdvisorSelection({
|
||||
settingsOverride: settings.get("task.agentAdvisor")[agent.name],
|
||||
agentAdvisor: agent.advisor,
|
||||
});
|
||||
const subagentSettings = createSubagentSettings(
|
||||
settings,
|
||||
{
|
||||
...(agent.readSummarize === false ? { "read.summarize.enabled": false } : undefined),
|
||||
// Isolated runs must not expose roots outside the worktree.
|
||||
...(worktree !== undefined ? { "workspace.additionalDirectories": [] } : undefined),
|
||||
...(advisorSelection ? { "advisor.enabled": true } : undefined),
|
||||
...(advisorSelection?.model
|
||||
? { modelRoles: { ...settings.getModelRoles(), advisor: advisorSelection.model } }
|
||||
: undefined),
|
||||
},
|
||||
options.parentServiceTier,
|
||||
);
|
||||
@@ -3197,6 +3215,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
||||
readOnly: isReadOnlyAgent(agent),
|
||||
spawns: spawnsEnv,
|
||||
readSummarize: agent.readSummarize,
|
||||
advisor: advisorSelection ? (advisorSelection.model ?? "on") : undefined,
|
||||
outputSchema,
|
||||
outputSchemaMode: options.outputSchemaMode,
|
||||
restrictToolNames: restrictToolNames || undefined,
|
||||
|
||||
@@ -79,10 +79,21 @@ export function createPersistedSubagentReviverFactory(
|
||||
taskDepth++;
|
||||
parentId = registry.get(parentId)?.parentId;
|
||||
}
|
||||
const subagentSettings = createSubagentSettings(
|
||||
ctx.settings,
|
||||
init.readSummarize === false ? { "read.summarize.enabled": false } : undefined,
|
||||
);
|
||||
// Rebuild the same advisor opt-in the original spawn resolved: `"on"` =
|
||||
// advisor-role model, anything else = the explicit pattern stamped onto
|
||||
// this session's `modelRoles.advisor`. Absent = unadvised (the
|
||||
// createSubagentSettings default).
|
||||
const subagentSettings = createSubagentSettings(ctx.settings, {
|
||||
...(init.readSummarize === false ? { "read.summarize.enabled": false } : undefined),
|
||||
...(init.advisor
|
||||
? {
|
||||
"advisor.enabled": true,
|
||||
...(init.advisor !== "on"
|
||||
? { modelRoles: { ...ctx.settings.getModelRoles(), advisor: init.advisor } }
|
||||
: undefined),
|
||||
}
|
||||
: undefined),
|
||||
});
|
||||
const persistedModelPattern =
|
||||
init.modelRole && init.modelRole !== "default"
|
||||
? [formatModelRoleAlias(init.modelRole), ...(init.resolvedModel ? [init.resolvedModel] : [])]
|
||||
|
||||
@@ -371,6 +371,8 @@ export interface AgentDefinition {
|
||||
readSummarize?: boolean;
|
||||
/** Prewalk hand-off for the spawned session: `true` = switch to the default prewalk target at the first edit/write, string = custom target model pattern. */
|
||||
prewalk?: boolean | string;
|
||||
/** Advisor for spawned sessions of this agent: `true` = advise with the default advisor-role model, string = advise with that model pattern (optional `:level` suffix). Absent/`false` = no advisor. */
|
||||
advisor?: boolean | string;
|
||||
source: AgentSource;
|
||||
filePath?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user