feat(coding-agent/task): added support for comma-separated model lists
- Model field now accepts string arrays for fallback model prioritization. - Extracted model list parsing into dedicated parseModelList helper function. - Extended default model alias detection to validate all entries in comma-separated lists.
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
- Added extension API to set working/loading messages during streaming
|
||||
- Added task worker propagation of context files, skills, and prompt templates
|
||||
- Added subagent option to skip Python preflight checks when Python tooling is unused
|
||||
- Model field now accepts string arrays for fallback model prioritization
|
||||
|
||||
### Changed
|
||||
- Merged patch application warnings into edit tool diagnostics output
|
||||
@@ -15,7 +16,7 @@
|
||||
### Fixed
|
||||
- Rewrote empty or corrupted session files to restore valid headers
|
||||
- Improved patch applicator ambiguity errors with match previews and overlap detection
|
||||
|
||||
- Fixed Task tool agent model resolution to honor comma-separated model lists
|
||||
## [8.3.0] - 2026-01-25
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -156,13 +156,23 @@ export function parseArrayOrCSV(value: unknown): string[] | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse model field into a prioritized list.
|
||||
*/
|
||||
export function parseModelList(value: unknown): string[] | undefined {
|
||||
const parsed = parseArrayOrCSV(value);
|
||||
if (!parsed) return undefined;
|
||||
const normalized = parsed.map(entry => entry.trim()).filter(Boolean);
|
||||
return normalized.length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
/** Parsed agent fields from frontmatter (excludes source/filePath/systemPrompt) */
|
||||
export interface ParsedAgentFields {
|
||||
name: string;
|
||||
description: string;
|
||||
tools?: string[];
|
||||
spawns?: string[] | "*";
|
||||
model?: string;
|
||||
model?: string[];
|
||||
output?: unknown;
|
||||
thinkingLevel?: ThinkingLevel;
|
||||
}
|
||||
@@ -202,7 +212,7 @@ export function parseAgentFields(frontmatter: Record<string, unknown>): ParsedAg
|
||||
}
|
||||
|
||||
const output = frontmatter.output !== undefined ? frontmatter.output : undefined;
|
||||
const model = typeof frontmatter.model === "string" ? frontmatter.model : undefined;
|
||||
const model = parseModelList(frontmatter.model);
|
||||
const thinkingLevel = parseThinkingLevel(frontmatter);
|
||||
|
||||
return { name, description, tools, spawns, model, output, thinkingLevel };
|
||||
|
||||
@@ -18,7 +18,7 @@ interface AgentFrontmatter {
|
||||
name: string;
|
||||
description: string;
|
||||
spawns?: string;
|
||||
model?: string;
|
||||
model?: string | string[];
|
||||
thinkingLevel?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,19 @@ import type {
|
||||
SubagentWorkerResponse,
|
||||
} from "./worker-protocol";
|
||||
|
||||
const DEFAULT_MODEL_ALIASES = new Set(["default", "pi/default", "omp/default"]);
|
||||
|
||||
function normalizeModelPatterns(value: string | string[] | undefined): string[] {
|
||||
if (!value) return [];
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(entry => entry.trim()).filter(Boolean);
|
||||
}
|
||||
return value
|
||||
.split(",")
|
||||
.map(entry => entry.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
/** Options for worker execution */
|
||||
export interface ExecutorOptions {
|
||||
cwd: string;
|
||||
@@ -50,7 +63,7 @@ export interface ExecutorOptions {
|
||||
index: number;
|
||||
id: string;
|
||||
context?: string;
|
||||
modelOverride?: string;
|
||||
modelOverride?: string | string[];
|
||||
thinkingLevel?: ThinkingLevel;
|
||||
outputSchema?: unknown;
|
||||
enableLsp?: boolean;
|
||||
@@ -283,23 +296,30 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
||||
const serializedSettings = options.settingsManager?.serialize();
|
||||
const availableModels = options.modelRegistry?.getAvailable() ?? [];
|
||||
|
||||
// Resolve model pattern to provider/modelId string
|
||||
const modelPattern = modelOverride ?? agent.model;
|
||||
// Resolve model pattern list to provider/modelId string
|
||||
const modelPatterns = normalizeModelPatterns(modelOverride ?? agent.model);
|
||||
let resolvedModel: string | undefined;
|
||||
if (modelPattern) {
|
||||
// Handle omp/<role> or pi/<role> aliases (e.g., "omp/slow", "pi/fast")
|
||||
let effectivePattern = modelPattern;
|
||||
const lower = modelPattern.toLowerCase();
|
||||
if (lower.startsWith("omp/") || lower.startsWith("pi/")) {
|
||||
const role = lower.startsWith("omp/") ? modelPattern.slice(4) : modelPattern.slice(3);
|
||||
const roles = serializedSettings?.modelRoles as Record<string, string> | undefined;
|
||||
const configured = roles?.[role] ?? roles?.[role.toLowerCase()];
|
||||
if (configured) {
|
||||
effectivePattern = configured;
|
||||
if (modelPatterns.length > 0) {
|
||||
const roles = serializedSettings?.modelRoles as Record<string, string> | undefined;
|
||||
for (const pattern of modelPatterns) {
|
||||
const normalized = pattern.trim().toLowerCase();
|
||||
if (!normalized || DEFAULT_MODEL_ALIASES.has(normalized)) {
|
||||
continue;
|
||||
}
|
||||
let effectivePattern = pattern;
|
||||
if (normalized.startsWith("omp/") || normalized.startsWith("pi/")) {
|
||||
const role = normalized.startsWith("omp/") ? pattern.slice(4) : pattern.slice(3);
|
||||
const configured = roles?.[role] ?? roles?.[role.toLowerCase()];
|
||||
if (configured) {
|
||||
effectivePattern = configured;
|
||||
}
|
||||
}
|
||||
const { model } = parseModelPattern(effectivePattern, availableModels);
|
||||
if (model) {
|
||||
resolvedModel = formatModelString(model);
|
||||
break;
|
||||
}
|
||||
}
|
||||
const { model } = parseModelPattern(effectivePattern, availableModels);
|
||||
resolvedModel = model ? formatModelString(model) : undefined;
|
||||
}
|
||||
const sessionFile = subtaskSessionFile ?? null;
|
||||
const spawnsEnv = agent.spawns === undefined ? "" : agent.spawns === "*" ? "*" : agent.spawns.join(",");
|
||||
|
||||
@@ -163,10 +163,14 @@ export class TaskTool implements AgentTool<typeof taskSchema, TaskToolDetails, T
|
||||
const { agent: agentName, context, output: outputSchema, isolated } = params;
|
||||
const isIsolated = isolated === true;
|
||||
|
||||
const isDefaultModelAlias = (value: string | undefined): boolean => {
|
||||
const isDefaultModelAlias = (value: string | string[] | undefined): boolean => {
|
||||
if (!value) return true;
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return normalized === "default" || normalized === "pi/default" || normalized === "omp/default";
|
||||
const values = Array.isArray(value) ? value : [value];
|
||||
if (values.length === 0) return true;
|
||||
return values.every(entry => {
|
||||
const normalized = entry.trim().toLowerCase();
|
||||
return normalized === "default" || normalized === "pi/default" || normalized === "omp/default";
|
||||
});
|
||||
};
|
||||
|
||||
// Validate agent exists
|
||||
|
||||
@@ -97,7 +97,7 @@ export interface AgentDefinition {
|
||||
systemPrompt: string;
|
||||
tools?: string[];
|
||||
spawns?: string[] | "*";
|
||||
model?: string;
|
||||
model?: string[];
|
||||
thinkingLevel?: ThinkingLevel;
|
||||
output?: unknown;
|
||||
source: AgentSource;
|
||||
@@ -122,7 +122,7 @@ export interface AgentProgress {
|
||||
toolCount: number;
|
||||
tokens: number;
|
||||
durationMs: number;
|
||||
modelOverride?: string;
|
||||
modelOverride?: string | string[];
|
||||
/** Data extracted by registered subprocess tool handlers (keyed by tool name) */
|
||||
extractedToolData?: Record<string, unknown[]>;
|
||||
}
|
||||
@@ -142,7 +142,7 @@ export interface SingleResult {
|
||||
truncated: boolean;
|
||||
durationMs: number;
|
||||
tokens: number;
|
||||
modelOverride?: string;
|
||||
modelOverride?: string | string[];
|
||||
error?: string;
|
||||
aborted?: boolean;
|
||||
/** Aggregated usage from the subprocess, accumulated incrementally from message_end events. */
|
||||
|
||||
Reference in New Issue
Block a user