Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/agent/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export interface AgentOptions {
export class Agent {
private _state: AgentState = {
systemPrompt: "",
model: getModel("google", "gemini-2.5-flash-lite-preview-06-17"),
model: getModel("google", "gemini-2.5-flash-lite"),
thinkingLevel: "off",
tools: [],
messages: [],
Expand Down
61 changes: 61 additions & 0 deletions packages/coding-agent/docs/mach6-models.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Mach6 Model Settings

Configure per-agent model overrides for mach6 skill agents via settings.

## What it does

The `agentModels.models` setting lets you override the default model used by each subagent type (e.g., Explore, Sandbox) without modifying the agent definition files. You can specify an ordered fallback list — the first available model is used.

## Configuration

Add to your `~/.dreb/settings.json`:

```json
{
"agentModels": {
"models": {
"Explore": ["openai/gpt-4o", "anthropic/claude-sonnet-4-20250514"],
"Sandbox": ["anthropic/claude-haiku-3-20250422"]
}
}
}
```

Each key is an agent type name, and the value is an ordered list of model IDs (in `provider/model` format).

## Resolution Order

When a subagent is launched, models are resolved in this priority:

1. **Per-invocation `model` override** — highest priority, set explicitly in the subagent tool call
2. **`agentModels.models` setting** — from your settings.json, per agent type
3. **Agent definition `model` field** — from the `.md` agent file's frontmatter

If the mach6 models list is empty or undefined for a given agent, it falls through to the agent definition's model.

## TUI Usage

Open `/settings` and scroll to the ⚡ items. Each discovered agent type gets its own entry where you can:

- **Reorder** models (move up/down to set priority)
- **Add** new models from the available model list
- **Remove** models from the fallback list

Changes are saved to your global settings immediately.

## Example

```json
{
"agentModels": {
"models": {
"Explore": [
"openai/gpt-4o-mini",
"anthropic/claude-haiku-3-20250422"
]
}
}
}
```

This configures the Explore agent to prefer `gpt-4o-mini`, falling back to `claude-haiku` if the first isn't available. All other agents use their default models.
1 change: 1 addition & 0 deletions packages/coding-agent/src/core/agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2761,6 +2761,7 @@ export class AgentSession {
parentProvider: () => this.model?.provider,
parentModel: () => this.model?.id,
modelRegistry: this._modelRegistry,
getAgentModelsForAgent: (name: string) => this.settingsManager?.getAgentModelsForAgent(name),
onBackgroundStart: (agentId, agentType, taskSummary) => {
this._emit({ type: "background_agent_start", agentId, agentType, taskSummary });
},
Expand Down
34 changes: 34 additions & 0 deletions packages/coding-agent/src/core/settings-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export interface MarkdownSettings {
codeBlockIndent?: string; // default: " "
}

export interface AgentModelsSettings {
models?: Record<string, string[]>;
}

export type TransportSetting = Transport;

/**
Expand Down Expand Up @@ -99,6 +103,7 @@ export interface Settings {
forbiddenCommands?: string[]; // Regex patterns for commands blocked by the forbidden-commands guard
sensitiveFilePaths?: string[]; // Additional glob patterns for sensitive file paths blocked by the read/bash guard
secretOutputPatterns?: { name: string; pattern: string }[]; // Additional regex patterns for secret scrubbing in tool output
agentModels?: AgentModelsSettings;
dream?: {
archivePath?: string; // Custom archive location for dream backups (default: ~/.dreb/memory-archive/)
};
Expand Down Expand Up @@ -972,4 +977,33 @@ export class SettingsManager {
this.markModified("dream", "archivePath");
this.save();
}

getAgentModels(): Record<string, string[]> {
return this.settings.agentModels?.models ? { ...this.settings.agentModels.models } : {};
}

getAgentModelsForAgent(agentName: string): string[] | undefined {
const models = this.settings.agentModels?.models?.[agentName];
return models && models.length > 0 ? [...models] : undefined;
}

setAgentModelsForAgent(agentName: string, models: string[]): void {
if (!this.globalSettings.agentModels) {
this.globalSettings.agentModels = {};
}
if (!this.globalSettings.agentModels.models) {
this.globalSettings.agentModels.models = {};
}
this.globalSettings.agentModels.models[agentName] = [...models];
this.markModified("agentModels", "models");
this.save();
}

removeAgentModelsForAgent(agentName: string): void {
if (this.globalSettings.agentModels?.models) {
delete this.globalSettings.agentModels.models[agentName];
this.markModified("agentModels", "models");
this.save();
}
}
}
28 changes: 18 additions & 10 deletions packages/coding-agent/src/core/tools/subagent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export function parseAgentFrontmatter(
};
}

function discoverAgentTypes(cwd: string): Map<string, AgentTypeConfig> {
export function discoverAgentTypes(cwd: string): Map<string, AgentTypeConfig> {
const agents = new Map<string, AgentTypeConfig>();

// Package-bundled agents (shipped with dreb — the canonical source of truth for built-in agents)
Expand Down Expand Up @@ -819,6 +819,7 @@ export async function executeSingle(
registry?: ModelRegistry,
sessionDir?: string,
parentModel?: string,
agentModels?: string[],
): Promise<SubagentResult> {
const name = agentName || DEFAULT_AGENT;
const config = agents.get(name);
Expand All @@ -843,9 +844,9 @@ export async function executeSingle(
errorMessage: `Task prompt too long (${task.length} chars, max ${MAX_TASK_LENGTH}). Shorten the prompt.`,
};
}
// Per-invocation model override takes precedence over agent definition model.
// Override is always a single string; agent config may be a string or fallback list.
const modelSpec = modelOverride || config.model;
// Per-invocation model override takes precedence over agent settings, which take precedence over agent definition model.
// Override is always a single string; agentModels and agent config may be arrays.
const modelSpec = modelOverride || (agentModels && agentModels.length > 0 ? agentModels : undefined) || config.model;
let effectiveConfig: AgentTypeConfig = modelOverride ? { ...config, model: modelOverride } : config;
let resolvedProvider = parentProvider;
let warning: string | undefined;
Expand Down Expand Up @@ -878,13 +879,10 @@ export async function executeSingle(
warning = resolved.warning;
}

onProgress?.(`Running ${name} agent...`);
const usedModel = effectiveConfig.model?.toString();
onProgress?.(`Running ${name} agent${usedModel ? ` (${usedModel})` : ""}...`);
const result = await spawnSubagent(effectiveConfig, task, cwd, signal, onProgress, resolvedProvider, sessionDir);
result.output = prependModelFallbackSummary(
result.output,
skippedModels,
result.model ?? effectiveConfig.model?.toString(),
);
result.output = prependModelFallbackSummary(result.output, skippedModels, result.model ?? usedModel);
if (warning) {
result.output = `[WARNING: ${warning}]\n\n${result.output}`;
}
Expand All @@ -903,6 +901,7 @@ async function executeChain(
defaultAgent?: string,
defaultModel?: string,
parentModel?: string,
getAgentModelsForAgentFn?: (name: string) => string[] | undefined,
): Promise<SubagentResult[]> {
const results: SubagentResult[] = [];
let previousOutput = "";
Expand Down Expand Up @@ -941,6 +940,8 @@ async function executeChain(

// Each chain step gets its own session subdirectory
const stepSessionDir = sessionBaseDir ? join(sessionBaseDir, `step-${i + 1}`) : undefined;
const stepAgentName = step.agent || defaultAgent || DEFAULT_AGENT;
const stepMach6Models = getAgentModelsForAgentFn?.(stepAgentName);
const result = await executeSingle(
agents,
step.agent || defaultAgent,
Expand All @@ -953,6 +954,7 @@ async function executeChain(
registry,
stepSessionDir,
parentModel,
stepMach6Models,
);
results.push(result);

Expand Down Expand Up @@ -1032,6 +1034,8 @@ export interface SubagentToolOptions {
parentModel?: () => string | undefined;
/** Model registry for validating model names before spawning child processes. */
modelRegistry?: ModelRegistry;
/** Settings-based model override getter for mach6.models. */
getAgentModelsForAgent?: (agentName: string) => string[] | undefined;
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1191,6 +1195,7 @@ export function createSubagentToolDefinition(
const getParentProvider = options?.parentProvider ?? (() => undefined);
const getParentModel = options?.parentModel ?? (() => undefined);
const modelRegistry = options?.modelRegistry;
const getAgentModelsForAgent = options?.getAgentModelsForAgent;

// Discover agents at definition time to build the prompt guidelines.
// This is cheap (reads .md files) and the same call happens on every execute().
Expand Down Expand Up @@ -1368,6 +1373,7 @@ export function createSubagentToolDefinition(
// Each background agent gets its own session subdirectory
const sessionId = generateAgentId();
const sessionDir = join(subagentSessionsBase, sessionId);
const agentModels = getAgentModelsForAgent?.(agentName || DEFAULT_AGENT);
return launchBackgroundLifecycle(agentName, taskLabel, (signal) =>
executeSingle(
agents,
Expand All @@ -1381,6 +1387,7 @@ export function createSubagentToolDefinition(
modelRegistry,
sessionDir,
getParentModel(),
agentModels,
),
);
};
Expand Down Expand Up @@ -1471,6 +1478,7 @@ export function createSubagentToolDefinition(
params.agent,
params.model,
getParentModel(),
getAgentModelsForAgent,
);
const resultText = results
.map((r, i) => `### Step ${i + 1}\n${formatSingleResult(r)}`)
Expand Down
Loading
Loading