From 9f8aa87dbf2d31429fdf0940d3af33d3d440582a Mon Sep 17 00:00:00 2001 From: can1357 Date: Fri, 24 Jul 2026 14:51:48 +0200 Subject: [PATCH] feat(task): removed per-call model override from task tool - Removes `model` field from task item/schema, TaskParams, and TaskItem types. - Removes model selector validation, formatting, and approval display logic. - Updates task tool priority docs to reflect that model is no longer per-call overridable. - Updates eval agent() helper docs and prompt templates to remove model parameter. - Updates tests to reflect removal of model override capability. --- docs/task-agent-discovery.md | 9 ++--- docs/tools/eval.md | 7 ++-- docs/tools/task.md | 11 +++--- packages/agent/test/agent-loop.test.ts | 6 ++-- packages/coding-agent/CHANGELOG.md | 4 +++ .../src/prompts/system/workflow-notice.md | 2 +- .../coding-agent/src/prompts/tools/eval.md | 4 +-- .../coding-agent/src/prompts/tools/task.md | 2 -- packages/coding-agent/src/task/index.ts | 35 +------------------ packages/coding-agent/src/task/types.ts | 12 ------- .../coding-agent/test/task/task-batch.test.ts | 31 ++-------------- .../test/task/task-schema.test.ts | 19 +--------- .../coding-agent/test/task/task-spawn.test.ts | 3 +- .../test/task/wire-schema.test.ts | 6 +--- 14 files changed, 27 insertions(+), 124 deletions(-) diff --git a/docs/task-agent-discovery.md b/docs/task-agent-discovery.md index 7962f8b67..ea8392bbc 100644 --- a/docs/task-agent-discovery.md +++ b/docs/task-agent-discovery.md @@ -126,12 +126,9 @@ In spawn execution (`TaskTool.#executeSync` → `#runSpawn`): Runtime model precedence is resolved by `resolveEffectiveSubagentPolicy()`: -1. the task item's explicit `model` selector or fallback chain -2. `task.agentModelOverrides[agentName]` -3. agent frontmatter `model` -4. the parent session model fallback - -Reasoning suffixes such as `:high` are preserved. The task tool rejects blank, empty-array, and comma-only per-call selectors before policy resolution so they cannot bypass a configured agent override. +1. `task.agentModelOverrides[agentName]` +2. agent frontmatter `model` +3. the parent session model fallback Runtime output schema precedence is: diff --git a/docs/tools/eval.md b/docs/tools/eval.md index e41a97ee2..640132d63 100644 --- a/docs/tools/eval.md +++ b/docs/tools/eval.md @@ -137,7 +137,7 @@ Implemented in `packages/coding-agent/src/eval/js/worker-core.ts`, `packages/cod - JS host/runtime helpers (`read`, `write`, `output`) are async and `await`able; `env` returns synchronously. - JS helper options may be passed either positionally in the Python order or as a trailing options object. `null` and `undefined` skip positional slots: - `await read(path, offset?, limit?)` or `await read(path, { offset?, limit? })` - - `await agent(prompt, agent?, model?, label?, schema?)` or `await agent(prompt, { agent?, model?, label?, schema?, handle? })` + - `await agent(prompt, agent?, label?, schema?)` or `await agent(prompt, { agent?, label?, schema?, handle? })` - `await parallel([() => agent("a"), () => agent("b")])` - `await pipeline(items, stage1, stage2)` - `display(value)` behavior: @@ -191,10 +191,9 @@ Both runtimes expose `completion()` — a single stateless completion against a Both runtimes expose `agent()` — a single subagent invocation routed through `packages/coding-agent/src/eval/agent-bridge.ts` into the same `runSubprocess(...)` path used by the `task` tool. It uses the current eval session's spawn policy and inherits the parent eval executor id, so parent and subagent code share JS/Python runtime state. - Signatures: - - JS: `await agent(prompt, agent?, model?, label?, schema?)` or `await agent(prompt, { agent?, model?, label?, schema?, handle? })` - - Python: `agent(prompt, *, agent="task", model=None, label=None, schema=None, handle=False)` + - JS: `await agent(prompt, agent?, label?, schema?)` or `await agent(prompt, { agent?, label?, schema?, handle? })` + - Python: `agent(prompt, *, agent="task", label=None, schema=None, handle=False)` - `agent` defaults to the bundled `task` agent and resolves through normal agent discovery, so project and user agents work. -- `model` overrides the selected agent's model. Without it, normal per-agent settings and the agent frontmatter model apply. - Shared background is passed via files: write a `local://` file and reference it in the prompt. `label` controls the `agent://` output label prefix. - `schema` passes a JSON Schema to the subagent structured-output path. When present, the helper parses the final JSON text and returns an object. - `handle` (default off) returns a DAG node dict — `{ text, output, handle: "agent://", id, agent }`, plus a parsed `data` field when `schema` is set — instead of the bare output, so a downstream stage can reference the transcript by handle. diff --git a/docs/tools/task.md b/docs/tools/task.md index 3d7883489..48f4e1650 100644 --- a/docs/tools/task.md +++ b/docs/tools/task.md @@ -26,9 +26,9 @@ ## Inputs -The wire schema is shape-swapped by `task.batch` (default on). One unit of work is the task item `{ name?, agent?, task, model?, outputSchema?, schemaMode?, isolated? }` (`isolated` only when `task.isolation.mode` is not `none`): +The wire schema is shape-swapped by `task.batch` (default on). One unit of work is the task item `{ name?, agent?, task, outputSchema?, schemaMode?, isolated? }` (`isolated` only when `task.isolation.mode` is not `none`): -- **Batch shape** (`task.batch` on): `{ context, tasks: item[] }` — one subagent per item, all run under the same fan-out rules; there is no top-level agent field. `context` is **required** shared background rendered into every spawned subagent's system prompt (`CONTEXT` section); `agent`, `model`, `outputSchema`, `schemaMode`, and `isolated` are per item, so one call may mix agent types, models, and output contracts. +- **Batch shape** (`task.batch` on): `{ context, tasks: item[] }` — one subagent per item, all run under the same fan-out rules; there is no top-level agent field. `context` is **required** shared background rendered into every spawned subagent's system prompt (`CONTEXT` section); `agent`, `outputSchema`, `schemaMode`, and `isolated` are per item, so one call may mix agent types and output contracts. - **Flat shape** (`task.batch` off): `{ ...item }` — exactly one spawn per call. Shared background goes into a `local://` file (e.g. `local://ctx.md`) that each spawn's `task` references; subagents share the parent's `local://` root. | Field | Type | Required | Description | @@ -38,7 +38,6 @@ The wire schema is shape-swapped by `task.batch` (default on). One unit of work | `name` | `string` | No | Stable agent name — becomes the registry/IRC id. Defaults to a generated AdjectiveNoun name. Uniquified per session by `AgentOutputManager`. Item field in batch shape, top-level in flat shape. | | `agent` | `string` | No | Agent type to run this item (e.g. `scout`). Defaults to the spawn policy's default agent (usually `task`); items in one batch call may use different agent types. Item field in batch shape, top-level in flat shape. | | `task` | `string` | Yes | The work — complete, self-contained instructions. Empty-after-trim is rejected. Item field in batch shape, top-level in flat shape. | -| `model` | `string \| string[]` | No | Explicit non-empty model selector or non-empty fallback chain for this spawn. Optional `:reasoning` suffixes are preserved. Takes precedence over `task.agentModelOverrides` and agent frontmatter. Item field in batch shape, top-level in flat shape. | | `outputSchema` | JSON Schema object | No | Invocation-specific structured-output contract. Takes precedence over agent frontmatter `output` and the inherited parent session schema. Item field in batch shape, top-level in flat shape. | | `schemaMode` | `"permissive" \| "strict"` | No | Validation mode for the effective output schema. Overrides the parent session mode; defaults to `permissive`. Item field in batch shape, top-level in flat shape. | | `isolated` | `boolean` | No | Run in an isolated workspace and return patches. Exists only when `task.isolation.mode` is not `none`; per item in batch shape, top-level in flat shape. Isolated agents are torn down at completion — not revivable. | @@ -77,7 +76,7 @@ Artifacts and side channels: ## Flow 1. `TaskTool.create(...)` discovers agents once per cwd through a process-level memo (`discoverAgentsForCreate`) to render the dynamic prompt description. -2. `execute(...)` repairs raw params (`repairTaskParams`), then validates: `schema` is always rejected; model selectors that normalize to no patterns are rejected; `tasks`/`context` are rejected unless `task.batch` is on; batch calls need a non-empty `tasks` (a `task` per item, unique provided names), a non-empty shared `context`, and no top-level `task` alongside `tasks`; flat calls need `task`. The call is then normalized into its spawn list (`resolveSpawnItems`). +2. `execute(...)` repairs raw params (`repairTaskParams`), then validates: `schema` is always rejected; `tasks`/`context` are rejected unless `task.batch` is on; batch calls need a non-empty `tasks` (a `task` per item, unique provided names), a non-empty shared `context`, and no top-level `task` alongside `tasks`; flat calls need `task`. The call is then normalized into its spawn list (`resolveSpawnItems`). 3. Per-item execution split: items whose agent type declares `blocking: true` run inline; the rest become background jobs. The whole call runs sync when `async.enabled=false`, the session has no `AsyncJobManager` (orphaned host), or every item is blocking; inline spawns run through `#executeSync(...)` under the session-scoped semaphore. 4. Background execution (any non-blocking item with `async.enabled=true` and an `AsyncJobManager`): - agent ids are allocated up front via `AgentOutputManager.allocate(...)` — each item's `name`, or a generated AdjectiveNoun name — one per spawn; @@ -87,7 +86,7 @@ Artifacts and side channels: - a mixed call registers the async jobs first, then runs its blocking items inline and returns once they settle — the text combines the inline summaries with the spawned-job listing, and the block keeps rendering the still-running background rows beside the inline results. 5. `#executeSync(...)` runs the spawn path (`#runSpawn`), which rediscovers agents from disk, so runtime resolution can differ from the create-time description. 6. It resolves each spawn's requested `agent` type, rejects unknown or settings-disabled agents, and enforces parent spawn policy plus `PI_BLOCKED_AGENT` self-recursion prevention. -7. Model priority: per-call `model` → `task.agentModelOverrides` → agent frontmatter → configured task role/session fallback. Output schema priority: per-call `outputSchema` → agent frontmatter `output` → inherited parent session schema. +7. Model priority: `task.agentModelOverrides` → agent frontmatter → configured task role/session fallback. Output schema priority: per-call `outputSchema` → agent frontmatter `output` → inherited parent session schema. 8. Plan mode swaps in an `effectiveAgent` with a read-only tool subset and plan-mode prompt; `runSubprocess(...)` receives the effective agent. 9. If `isolated`, it requires a git repo (`getRepoRoot(...)` / `captureBaseline(...)`), maps `task.isolation.mode` to a backend-kind hint (`parseIsolationMode`), and materializes the workspace via the natives PAL (`ensureIsolation` → `isoResolve`/`isoStart`), walking the candidate list when a backend is unavailable. 10. Artifacts dir comes from the parent session file when available, otherwise a temp dir. When the session is executing an approved plan, the plan reference is handed to the subagent. @@ -106,7 +105,7 @@ Artifacts and side channels: - Background job — `async.enabled=true`; non-blocking spawns go through `AsyncJobManager`. - Sync inline — `async.enabled=false`, no job manager, or the item's agent declares `blocking: true` (per item: a mixed call runs both modes). - Batch mode (`task.batch`, default on) - - on — `{ context, tasks[] }`: one independent spawn per item, required `context` shared across the call's spawns, with `agent`, `model`, `outputSchema`, `schemaMode`, and `isolated` per item. Lifecycle, revival, and concurrency semantics match N parallel single calls. + - on — `{ context, tasks[] }`: one independent spawn per item, required `context` shared across the call's spawns, with `agent`, `outputSchema`, `schemaMode`, and `isolated` per item. Lifecycle, revival, and concurrency semantics match N parallel single calls. - off — single spawn per call; `tasks`/`context` are rejected and removed from the schema. - Isolation mode (`task.isolation.mode`): `none`, `auto`, `apfs`, `btrfs`, `zfs`, `reflink`, `overlayfs`, `projfs`, `block-clone`, `rcopy` (legacy `worktree`, `fuse-overlay`, `fuse-projfs` accepted for back-compat); the PAL resolves the actual backend with fallback. - Isolation merge strategy: patch mode (capture/apply root patches) or branch mode (commit to `omp/task/`, cherry-pick into parent). diff --git a/packages/agent/test/agent-loop.test.ts b/packages/agent/test/agent-loop.test.ts index e1b7a59db..94d662fac 100644 --- a/packages/agent/test/agent-loop.test.ts +++ b/packages/agent/test/agent-loop.test.ts @@ -1112,9 +1112,9 @@ describe("agentLoop with AgentMessage", () => { // Names the resolver does not know keep the "not found" failure. const missingResult = results.find(r => r.toolCallId === "tool-2"); expect(missingResult?.isError).toBe(true); - expect( - missingResult?.content.some(c => c.type === "text" && c.text.includes("Tool nonexistent not found")), - ).toBe(true); + expect(missingResult?.content.some(c => c.type === "text" && c.text.includes("Tool nonexistent not found"))).toBe( + true, + ); }); it("injects and strips intent when intent tracing is enabled", async () => { diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index cde118b9b..f37adfb1b 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -6,6 +6,10 @@ - Large pastes saved via the large-paste menu now insert `local://paste-N.md` references (previously `local://attachment-N`), so the saved paste carries a markdown extension and a clearer name. +### Removed + +- Removed the `model` parameter from `task` and `agent()`: explicit per-spawn model selectors and fallback chains are no longer supported; spawns always use the agent's configured model + ### Fixed - Fixed `todo` calls that omit `op` hard-failing validation ("op must be operation to apply (was missing)"): the tool now validates leniently and infers the op for unambiguous payloads (`list` → `init`, `phase`+`items` → `append`, bare `items` on an empty list → `init`); `op` stays required in the schema, and ambiguous op-less calls surface the schema error as a retryable tool error. diff --git a/packages/coding-agent/src/prompts/system/workflow-notice.md b/packages/coding-agent/src/prompts/system/workflow-notice.md index ab3e8df8d..cea71f1fe 100644 --- a/packages/coding-agent/src/prompts/system/workflow-notice.md +++ b/packages/coding-agent/src/prompts/system/workflow-notice.md @@ -13,7 +13,7 @@ Worth it when the task benefits from decomposition + parallel coverage, or from State persists across eval calls, so scout in one call and fan out in the next. Every eval call has: -- `agent(prompt, *, agent="task", model=None, label=None, schema=None, isolated=None, apply=None, merge=None, handle=False)` — run ONE subagent; returns its final text, or the validated object when `schema` (a JSON Schema dict) is given. With `schema` the subagent is forced to emit structured output that is validated for you — branch on the object, not on parsed prose. `agent` picks a discovered agent ("scout", "reviewer", …); `label` names the artifact. Shared background goes in a `local://` file referenced from each prompt, not a parameter. Subagents are told their final text IS the return value, so they hand back raw data. `agent()` blocks until the subagent finishes. Recursion follows `task.maxRecursionDepth` (default 2; a negative value disables the cap): main agent depth = 0, each `agent()` child increments depth by 1, and, when the cap is non-negative, a spawner may call `agent()` only while its current `taskDepth < cap`. Pass `isolated=True` to run the spawn in a copy-on-write worktree so parallel `agent()` calls can edit overlapping files safely — strict opt-in, mirrors the `task` tool, defaults off regardless of `task.isolation.mode`; `isolated=True` while the setting is `"none"` errors out instead of silently downgrading. With isolation, `apply=False` keeps changes in the worktree, and `merge=False` forces patch mode even when the setting is `"branch"`. Captured root patch path, branch name, nested repo patches, and apply summary reach the workflow through `handle=True` — combine it with `apply=False` (or `apply=False, schema=…`) and read `node["patch_path"]`, `node["branch_name"]`, `node["nested_patches"]`, `node["changes_applied"]`, `node["isolation_summary"]` (JS: same keys camelCased) to recover artifacts. +- `agent(prompt, *, agent="task", label=None, schema=None, isolated=None, apply=None, merge=None, handle=False)` — run ONE subagent; returns its final text, or the validated object when `schema` (a JSON Schema dict) is given. With `schema` the subagent is forced to emit structured output that is validated for you — branch on the object, not on parsed prose. `agent` picks a discovered agent ("scout", "reviewer", …); `label` names the artifact. Shared background goes in a `local://` file referenced from each prompt, not a parameter. Subagents are told their final text IS the return value, so they hand back raw data. `agent()` blocks until the subagent finishes. Recursion follows `task.maxRecursionDepth` (default 2; a negative value disables the cap); deeper calls raise an error. - `parallel(thunks)` — run zero-arg callables concurrently through a bounded pool, preserving input order; returns once all finish. The pool is bounded by the session's `task` concurrency — don't hand-tune it; fan out as wide as the work divides. A thunk that raises propagates — wrap risky work in `try/except` inside the thunk to keep partial results. In a loop, bind each closure's value with a default arg (`lambda d=d: …`) or every thunk captures the last one. - `pipeline(items, *stages)` — map items through `stages` left-to-right. There is a BARRIER between stages: ALL items clear stage N before stage N+1 begins. Each stage is a one-arg callable; stage 1 gets the original item, later stages get the previous result. Same pool width as `parallel()`. - `completion(prompt, *, model="default", system=None, schema=None)` — oneshot, stateless model call (no tools, no history). Tiers: "smol", "default", "slow". Cheap classification/scoring inside a fan-out. diff --git a/packages/coding-agent/src/prompts/tools/eval.md b/packages/coding-agent/src/prompts/tools/eval.md index 765fcef45..8afbdf5cc 100644 --- a/packages/coding-agent/src/prompts/tools/eval.md +++ b/packages/coding-agent/src/prompts/tools/eval.md @@ -20,9 +20,9 @@ tool.(args) → unknown Invoke any session tool; `args` = its parameter object. completion(prompt, model?="default"|"smol"|"slow", system?=None, schema?=None) → str | dict Oneshot, stateless (no history/tools). `model`: "smol" fast | "default" session | "slow" most capable. `schema` (JSON-Schema) → parsed object. -{{#if spawns}}agent(prompt, agent?="{{spawnDefaultAgent}}", model?=None, label?=None, schema?=None, schema{{#if js}}Mode{{else}}_mode{{/if}}?="permissive", isolated?=None, apply?=None, merge?=None, handle?=False) → str | dict +{{#if spawns}}agent(prompt, agent?="{{spawnDefaultAgent}}", label?=None, schema?=None, schema{{#if js}}Mode{{else}}_mode{{/if}}?="permissive", isolated?=None, apply?=None, merge?=None, handle?=False) → str | dict Run a subagent → final output. `agent` selects a discovered agent; omit it to use `{{spawnDefaultAgent}}`.{{#if spawnAllowedAgentsText}} Allowed agents: {{spawnAllowedAgentsText}}.{{/if}} `schema` overrides agent/session schemas; `schemaMode`/`schema_mode`: "permissive" | "strict". Effective schemas return parsed data. `isolated` requests a worktree; `apply`/`merge` control its changes. Background via `local://` files named in the prompt. `handle` → { text, output, handle: "agent://", id, agent }, parsed `data` when structured. -{{#if js}} JS: ONE trailing object — agent(prompt, { agent, model, label, schema, schemaMode, isolated, apply, merge, handle }).{{/if}} +{{#if js}} JS: ONE trailing object — agent(prompt, { agent, label, schema, schemaMode, isolated, apply, merge, handle }).{{/if}} {{/if}} parallel(thunks) → list pipeline(items, ...stages) → list log(message) → None phase(title) → None diff --git a/packages/coding-agent/src/prompts/tools/task.md b/packages/coding-agent/src/prompts/tools/task.md index 0e3be887f..ebf78e64a 100644 --- a/packages/coding-agent/src/prompts/tools/task.md +++ b/packages/coding-agent/src/prompts/tools/task.md @@ -22,7 +22,6 @@ Agents marked BLOCKING run inline — results return in this call; non-blocking - `name`: A stable CamelCase identifier (≤32 chars), used to address the agent (IRC, job ids). Generated automatically if omitted. - `agent`: The agent type running this item (e.g. `scout`, `reviewer`). Omitting it gives you the general-purpose worker (`{{defaultAgent}}`) — NEVER pass that name explicitly. Only omit it after checking the agent list below and finding no specialist that fits.{{#if allowedAgentsText}} Current spawn policy allows: {{allowedAgentsText}}.{{/if}} - `task`: Complete, self-contained instructions. One-liners or missing acceptance criteria are PROHIBITED. - - `model`: Explicit non-empty model selector or non-empty fallback chain for this spawn. A `:reasoning` suffix is preserved. Overrides agent-specific model settings. - `outputSchema`: Invocation-specific JSON Schema. Overrides the selected agent and parent-session schemas. - `schemaMode`: `"permissive"` (default) accepts a retry-exhausted invalid result with a warning; `"strict"` fails it. {{#if isolationEnabled}} @@ -36,7 +35,6 @@ Agents marked BLOCKING run inline — results return in this call; non-blocking - `name`: A stable CamelCase identifier (≤32 chars), used to address the agent (IRC, job ids). Generated automatically if omitted. - `agent`: The agent type to spawn (e.g. `scout`, `reviewer`). Omitting it gives you the general-purpose worker (`{{defaultAgent}}`) — NEVER pass that name explicitly. Only omit it after checking the agent list below and finding no specialist that fits.{{#if allowedAgentsText}} Current spawn policy allows: {{allowedAgentsText}}.{{/if}} - `task`: Complete, self-contained instructions. One-liners or missing acceptance criteria are PROHIBITED. -- `model`: Explicit non-empty model selector or non-empty fallback chain for this spawn. A `:reasoning` suffix is preserved. Overrides agent-specific model settings. - `outputSchema`: Invocation-specific JSON Schema. Overrides the selected agent and parent-session schemas. - `schemaMode`: `"permissive"` (default) accepts a retry-exhausted invalid result with a warning; `"strict"` fails it. {{#if isolationEnabled}} diff --git a/packages/coding-agent/src/task/index.ts b/packages/coding-agent/src/task/index.ts index c0d33da5e..9d03c611a 100644 --- a/packages/coding-agent/src/task/index.ts +++ b/packages/coding-agent/src/task/index.ts @@ -231,18 +231,6 @@ 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; - const materializedSelectors = selectors ? Array.from(selectors) : []; - return ( - !selectors || - materializedSelectors.length === 0 || - materializedSelectors.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() !== ""; @@ -259,9 +247,6 @@ 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(); for (const item of tasks) { @@ -284,9 +269,6 @@ 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; } @@ -300,7 +282,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, model: params.model }; + const item: TaskItem = { name: params.name, agent: params.agent, task: params.task }; if ("outputSchema" in params) item.outputSchema = params.outputSchema; if ("schemaMode" in params) item.schemaMode = params.schemaMode; if ("isolated" in params) item.isolated = params.isolated; @@ -320,7 +302,6 @@ 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; @@ -491,14 +472,6 @@ function discoverAgentsForCreate(cwd: string): Promise { 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 // ═══════════════════════════════════════════════════════════════════════════ @@ -522,8 +495,6 @@ export class TaskTool implements AgentTool { expect(offProperties.context).toBeUndefined(); expect(offProperties.task).toBeDefined(); expect(offProperties.name).toBeDefined(); - expect(offProperties.model).toBeDefined(); expect(offProperties.outputSchema).toBeDefined(); expect(typeof offProperties.outputSchema).toBe("object"); expect(offProperties.schemaMode).toBeDefined(); @@ -115,14 +114,12 @@ describe("task.batch schema gating", () => { expect(onProperties.task).toBeUndefined(); expect(onProperties.name).toBeUndefined(); expect(onProperties.agent).toBeUndefined(); - expect(onProperties.model).toBeUndefined(); expect(onProperties.outputSchema).toBeUndefined(); expect(onProperties.schemaMode).toBeUndefined(); const items = (onProperties.tasks as { items?: { properties?: Record } }).items; expect(items?.properties?.task).toBeDefined(); expect(items?.properties?.name).toBeDefined(); expect(items?.properties?.agent).toBeDefined(); - expect(items?.properties?.model).toBeDefined(); expect(items?.properties?.outputSchema).toBeDefined(); expect(typeof items?.properties?.outputSchema).toBe("object"); expect(items?.properties?.schemaMode).toBeDefined(); @@ -232,21 +229,6 @@ describe("task.batch validation", () => { expect(text).toContain("Missing `context`"); }); - it.each([{ model: [","] }, { model: Array(1) }])( - "rejects an empty per-item model selector", - async ({ model }) => { - const text = await executeText( - { - context: "Background.", - tasks: [{ name: "Alpha", task: "Work.", model }], - }, - { "task.batch": true }, - ); - expect(text).toContain("Task 1 (`Alpha`) has an invalid `model`"); - expect(text).toContain("non-empty array"); - }, - ); - it("rejects duplicate provided names case-insensitively", async () => { const text = await executeText( { @@ -347,14 +329,12 @@ describe("task.batch spawning", () => { { name: "Alpha", task: "Do A.", - model: "openai-codex/gpt-5.6-sol:high", outputSchema: alphaSchema, schemaMode: "strict", }, { name: "Beta", task: "Do B.", - model: ["anthropic/claude-sonnet-4-6:medium", "openai-codex/gpt-5.6-sol:low"], outputSchema: betaSchema, schemaMode: "permissive", }, @@ -382,15 +362,10 @@ describe("task.batch spawning", () => { expect(spawn.outputSchemaOverridesAgent).toBe(true); } const byId = new Map(seen.map(spawn => [spawn.id, spawn])); - expect(byId.get("Alpha")?.modelOverride).toEqual(["openai-codex/gpt-5.6-sol:high"]); expect(byId.get("Alpha")?.outputSchema).toEqual(alphaSchema); expect(byId.get("Alpha")?.outputSchemaMode).toBe("strict"); expect(byId.get("Beta")?.outputSchema).toEqual(betaSchema); expect(byId.get("Beta")?.outputSchemaMode).toBe("permissive"); - expect(byId.get("Beta")?.modelOverride).toEqual([ - "anthropic/claude-sonnet-4-6:medium", - "openai-codex/gpt-5.6-sol:low", - ]); expect(seen.map(spawn => spawn.assignment).sort()).toEqual(["Do A.", "Do B."]); for (const spawn of seen) expect(spawn.parentAgentId).toBe("ParentA"); }); @@ -451,7 +426,6 @@ describe("task.batch spawning", () => { name: "Review", agent: "reviewer", task: "Review.", - model: "openai-codex/gpt-5.6-sol:high", outputSchema: callerSchema, }, ], @@ -471,7 +445,7 @@ describe("task.batch spawning", () => { expect(scoutSpawn?.outputSchemaOverridesAgent).toBe(false); expect(reviewerSpawn?.agent).toBe(reviewerAgent); expect(reviewerSpawn?.agent.tools).toEqual(["read", "bash"]); - expect(reviewerSpawn?.modelOverride).toEqual(["openai-codex/gpt-5.6-sol:high"]); + expect(reviewerSpawn?.modelOverride).toEqual(["anthropic/claude-sonnet-4-6:medium"]); expect(reviewerSpawn?.outputSchema).toBe(callerSchema); expect(reviewerSpawn?.outputSchemaSource).toBe("caller"); expect(reviewerSpawn?.outputSchemaOverridesAgent).toBe(true); @@ -547,7 +521,6 @@ describe("task.batch spawning", () => { agent: "task", name: "Flat", task: "Do the thing.", - model: ["openai-codex/gpt-5.6-sol:high", "anthropic/claude-sonnet-4:low"], outputSchema: callerSchema, schemaMode: "strict", } as TaskParams); @@ -556,7 +529,7 @@ describe("task.batch spawning", () => { const job = manager.getJob(result.details!.async!.jobId)!; await job.promise; expect(job.status).toBe("completed"); - expect(captured?.modelOverride).toEqual(["openai-codex/gpt-5.6-sol:high", "anthropic/claude-sonnet-4:low"]); + expect(captured?.modelOverride).toEqual(["openai/gpt-4.1-mini"]); expect(captured?.outputSchema).toEqual(callerSchema); expect(captured?.outputSchemaMode).toBe("strict"); expect(captured?.outputSchemaSource).toBe("caller"); diff --git a/packages/coding-agent/test/task/task-schema.test.ts b/packages/coding-agent/test/task/task-schema.test.ts index 56c84dc8c..fed78adb4 100644 --- a/packages/coding-agent/test/task/task-schema.test.ts +++ b/packages/coding-agent/test/task/task-schema.test.ts @@ -30,12 +30,11 @@ describe("task schema (single-spawn)", () => { expect(parsed instanceof type.errors).toBe(true); }); - it("retains caller model, outputSchema, and schemaMode while stripping stale keys", () => { + it("retains caller outputSchema and schemaMode while stripping stale keys", () => { const outputSchema = { type: "object", properties: { answer: { type: "string" } } }; const parsed = taskSchema({ agent: "scout", task: "Map the auth module.", - model: "openai-codex/gpt-5.6-sol:high", outputSchema, schemaMode: "strict", context: "shared background", @@ -44,7 +43,6 @@ describe("task schema (single-spawn)", () => { }); expect(parsed instanceof type.errors).toBe(false); if (!(parsed instanceof type.errors)) { - expect(parsed.model).toBe("openai-codex/gpt-5.6-sol:high"); expect(parsed.outputSchema).toEqual(outputSchema); expect(parsed.schemaMode).toBe("strict"); expect("tasks" in parsed).toBe(false); @@ -87,19 +85,4 @@ describe("task spawn validation", () => { const text = await executeText({ agent: "scout" }); expect(text).toContain("Missing `task`"); }); - - it.each([ - { model: "" }, - { model: " " }, - { model: "," }, - { model: " , " }, - { model: [] }, - { model: Array(1) }, - { model: ["openai-codex/gpt-5.6-sol:high", " "] }, - { model: ["openai-codex/gpt-5.6-sol:high", ","] }, - ])("rejects an empty model selector", async ({ model }) => { - const text = await executeText({ agent: "scout", task: "Map the auth module.", model }); - expect(text).toContain("Invalid `model`"); - expect(text).toContain("non-empty selector"); - }); }); diff --git a/packages/coding-agent/test/task/task-spawn.test.ts b/packages/coding-agent/test/task/task-spawn.test.ts index 1a94bdc45..4b3b02c68 100644 --- a/packages/coding-agent/test/task/task-spawn.test.ts +++ b/packages/coding-agent/test/task/task-spawn.test.ts @@ -125,7 +125,6 @@ describe("task spawn routing", () => { agent: "task", name: "Spawnling", task: "Do the thing.", - model: "openai-codex/gpt-5.6-sol:high", } as TaskParams); // Tool returned while the job body is still gated on the deferred. @@ -146,7 +145,7 @@ describe("task spawn routing", () => { expect(job!.resultText).toContain("message it via `hub` to follow up"); expect(job!.resultText).toContain("history://Spawnling"); expect(runSpy).toHaveBeenCalledTimes(1); - expect(runSpy.mock.calls[0]?.[0].modelOverride).toEqual(["openai-codex/gpt-5.6-sol:high"]); + expect(runSpy.mock.calls[0]?.[0].modelOverride).toEqual(["openai/gpt-4.1-mini"]); }); it("bounds concurrent job bodies with the session spawn semaphore", async () => { diff --git a/packages/coding-agent/test/task/wire-schema.test.ts b/packages/coding-agent/test/task/wire-schema.test.ts index f5c2d668b..176abbd05 100644 --- a/packages/coding-agent/test/task/wire-schema.test.ts +++ b/packages/coding-agent/test/task/wire-schema.test.ts @@ -118,17 +118,15 @@ describe("task approval details surface the dispatch", () => { } as unknown as ToolSession); } - it("surfaces agent, name, model, and task for a flat spawn", async () => { + it("surfaces agent, name, and task for a flat spawn", async () => { const tool = await makeTool(); const lines = tool.formatApprovalDetails({ agent: "reviewer", name: "ReviewAuth", - model: "openai-codex/gpt-5.6-sol:high", task: "audit the auth module", }); expect(lines).toContain("Agent: reviewer"); expect(lines).toContain("Name: ReviewAuth"); - expect(lines).toContain("Model: openai-codex/gpt-5.6-sol:high"); expect(lines).toContain("Task:\naudit the auth module"); }); @@ -139,7 +137,6 @@ describe("task approval details surface the dispatch", () => { tasks: [ { name: "DbMigrator", - model: ["anthropic/claude-sonnet-4", "openai/gpt-5"], task: "migrate the schema", }, { task: "second item" }, @@ -149,7 +146,6 @@ describe("task approval details surface the dispatch", () => { expect(lines).toContain("Batch agents: scout ×2"); expect(lines).toContain("Name: DbMigrator"); expect(lines).toContain("Agent: scout"); - expect(lines).toContain("Model: anthropic/claude-sonnet-4 → openai/gpt-5"); expect(lines).toContain("Task:\nmigrate the schema"); expect(lines).toContain("+1 more task"); });