feat(task): allow per-call model selection

This commit is contained in:
panosAthDBX
2026-07-23 09:42:39 +01:00
parent 7504d4c24d
commit 1c8d9db6dd
8 changed files with 123 additions and 12 deletions
+32 -1
View File
@@ -229,6 +229,16 @@ function validateShapeParams(batchEnabled: boolean, params: TaskParams): string
* policy later, in `spawnParamsFor`. Returns a problem description, or
* undefined when valid.
*/
function hasInvalidModelSelector(model: unknown): boolean {
if (model === undefined) return false;
const selectors = typeof model === "string" ? [model] : Array.isArray(model) ? model : undefined;
return (
!selectors ||
selectors.length === 0 ||
selectors.some(selector => typeof selector !== "string" || !selector.split(",").some(pattern => pattern.trim()))
);
}
function validateSpawnParams(params: TaskParams, batchEnabled: boolean): string | undefined {
const hasTask = typeof params.task === "string" && params.task.trim() !== "";
const tasks = params.tasks;
@@ -244,6 +254,9 @@ function validateSpawnParams(params: TaskParams, batchEnabled: boolean): string
if (!item || typeof item.task !== "string" || item.task.trim() === "") {
return `Task ${i + 1}${item?.name ? ` (\`${item.name}\`)` : ""} is missing \`task\`. Every task needs complete, self-contained instructions.`;
}
if (hasInvalidModelSelector(item.model)) {
return `Task ${i + 1}${item.name ? ` (\`${item.name}\`)` : ""} has an invalid \`model\`. Provide a non-empty selector or a non-empty array of non-empty selectors.`;
}
}
const seen = new Map<string, string>();
for (const item of tasks) {
@@ -266,6 +279,9 @@ function validateSpawnParams(params: TaskParams, batchEnabled: boolean): string
? "Missing `tasks`. Provide a `tasks` array (one subagent per item) with a shared `context`."
: "Missing `task`. Provide complete, self-contained instructions for the agent.";
}
if (hasInvalidModelSelector(params.model)) {
return "Invalid `model`. Provide a non-empty selector or a non-empty array of non-empty selectors.";
}
return undefined;
}
@@ -279,7 +295,7 @@ function resolveSpawnItems(params: TaskParams): TaskItem[] {
if (Array.isArray(params.tasks) && params.tasks.length > 0) {
return params.tasks;
}
const item: TaskItem = { name: params.name, agent: params.agent, task: params.task };
const item: TaskItem = { name: params.name, agent: params.agent, task: params.task, model: params.model };
if ("outputSchema" in params) item.outputSchema = params.outputSchema;
if ("schemaMode" in params) item.schemaMode = params.schemaMode;
if ("isolated" in params) item.isolated = params.isolated;
@@ -299,6 +315,7 @@ function spawnParamsFor(params: TaskParams, item: TaskItem, defaultAgent: string
const spawn: TaskParams = { agent: item.agent?.trim() || defaultAgent };
if (item.name !== undefined) spawn.name = item.name;
if (item.task !== undefined) spawn.task = item.task;
if (item.model !== undefined) spawn.model = item.model;
if (params.context !== undefined) spawn.context = params.context;
if ("outputSchema" in item) spawn.outputSchema = item.outputSchema;
if ("schemaMode" in item) spawn.schemaMode = item.schemaMode;
@@ -469,6 +486,14 @@ function discoverAgentsForCreate(cwd: string): Promise<DiscoveryResult> {
return pending;
}
function formatModelForApproval(model: unknown): string | undefined {
const selectors = typeof model === "string" ? [model] : Array.isArray(model) ? model : [];
const normalized = selectors.filter(
(selector): selector is string => typeof selector === "string" && !!selector.trim(),
);
return normalized.length > 0 ? truncateForPrompt(normalized.join(" → ")) : undefined;
}
// ═══════════════════════════════════════════════════════════════════════════
// Tool Class
// ═══════════════════════════════════════════════════════════════════════════
@@ -492,6 +517,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
if (typeof params.name === "string" && params.name.trim()) {
lines.push(`Name: ${truncateForPrompt(params.name)}`);
}
const model = formatModelForApproval(params.model);
if (model) lines.push(`Model: ${model}`);
if (typeof params.task === "string") {
lines.push(`Task:\n${truncateForPrompt(params.task)}`);
}
@@ -507,6 +534,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
if (typeof firstTask.agent === "string" && firstTask.agent.trim()) {
lines.push(`Agent: ${truncateForPrompt(firstTask.agent)}`);
}
const itemModel = formatModelForApproval(firstTask.model);
if (itemModel) lines.push(`Model: ${itemModel}`);
if (typeof firstTask.task === "string") {
lines.push(`Task:\n${truncateForPrompt(firstTask.task)}`);
}
@@ -612,6 +641,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
assignment: (params.task ?? "").trim(),
context: this.#isBatchEnabled() ? params.context?.trim() || undefined : undefined,
agent: params.agent,
model: params.model,
...(Object.hasOwn(params, "outputSchema") ? { outputSchema: params.outputSchema } : {}),
...(Object.hasOwn(params, "schemaMode") ? { schemaMode: params.schemaMode } : {}),
...("isolated" in params ? { isolation: { requested: params.isolated } } : {}),
@@ -1383,6 +1413,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
assignment,
context,
agent: params.agent,
model: params.model,
...(Object.hasOwn(params, "outputSchema") ? { outputSchema: params.outputSchema } : {}),
...(Object.hasOwn(params, "schemaMode") ? { schemaMode: params.schemaMode } : {}),
identity: { id: preAllocatedId, label: params.name },
+12
View File
@@ -113,6 +113,7 @@ export const taskItemSchema = type({
"name?": "string",
agent: "string = 'task'",
task: "string",
"model?": "string | string[]",
"outputSchema?": outputSchemaInputSchema,
"schemaMode?": '"permissive" | "strict"',
"+": "delete",
@@ -121,6 +122,7 @@ const taskItemSchemaIsolated = type({
"name?": "string",
agent: "string = 'task'",
task: "string",
"model?": "string | string[]",
"outputSchema?": outputSchemaInputSchema,
"schemaMode?": '"permissive" | "strict"',
"isolated?": "boolean",
@@ -135,6 +137,8 @@ export interface TaskItem {
agent?: string;
/** The work; required by the schema. */
task?: string;
/** Explicit model selector or fallback chain for this spawn, including optional reasoning suffixes. */
model?: string | string[];
/** Caller-provided output schema; its presence overrides the selected agent's schema. */
outputSchema?: unknown;
/** Validation behavior for a caller-provided or inherited output schema. */
@@ -147,6 +151,7 @@ export const taskSchema = type({
"name?": "string",
agent: "string = 'task'",
task: "string",
"model?": "string | string[]",
"outputSchema?": outputSchemaInputSchema,
"schemaMode?": '"permissive" | "strict"',
"isolated?": "boolean",
@@ -156,6 +161,7 @@ const taskSchemaNoIsolation = type({
"name?": "string",
agent: "string = 'task'",
task: "string",
"model?": "string | string[]",
"outputSchema?": outputSchemaInputSchema,
"schemaMode?": '"permissive" | "strict"',
"+": "delete",
@@ -200,6 +206,7 @@ function createTaskSchema(options: {
"name?": "string",
agent,
task: "string",
"model?": "string | string[]",
"outputSchema?": outputSchemaInputSchema,
"schemaMode?": '"permissive" | "strict"',
"isolated?": "boolean",
@@ -215,6 +222,7 @@ function createTaskSchema(options: {
"name?": "string",
agent,
task: "string",
"model?": "string | string[]",
"outputSchema?": outputSchemaInputSchema,
"schemaMode?": '"permissive" | "strict"',
"+": "delete",
@@ -230,6 +238,7 @@ function createTaskSchema(options: {
"name?": "string",
agent,
task: "string",
"model?": "string | string[]",
"outputSchema?": outputSchemaInputSchema,
"schemaMode?": '"permissive" | "strict"',
"isolated?": "boolean",
@@ -240,6 +249,7 @@ function createTaskSchema(options: {
"name?": "string",
agent,
task: "string",
"model?": "string | string[]",
"outputSchema?": outputSchemaInputSchema,
"schemaMode?": '"permissive" | "strict"',
"+": "delete",
@@ -283,6 +293,8 @@ export interface TaskParams {
agent?: string;
/** The work (flat form). */
task?: string;
/** Explicit model selector or fallback chain for the spawn, including optional reasoning suffixes. */
model?: string | string[];
/** Caller-provided output schema; its presence overrides the selected agent's schema. */
outputSchema?: unknown;
/** Validation behavior for a caller-provided or inherited output schema. */