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.
This commit is contained in:
@@ -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:
|
||||
|
||||
|
||||
+3
-4
@@ -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://<id>` 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>", 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.
|
||||
|
||||
+5
-6
@@ -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/<id>`, cherry-pick into parent).
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -13,7 +13,7 @@ Worth it when the task benefits from decomposition + parallel coverage, or from
|
||||
<helpers>
|
||||
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.
|
||||
|
||||
@@ -20,9 +20,9 @@ tool.<name>(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>", 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
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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<string, string>();
|
||||
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<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
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -522,8 +495,6 @@ 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)}`);
|
||||
}
|
||||
@@ -554,8 +525,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
||||
lines.push(`Name: ${truncateForPrompt(firstTask.name)}`);
|
||||
}
|
||||
lines.push(`Agent: ${truncateForPrompt(effectiveAgent(firstTask))}`);
|
||||
const itemModel = formatModelForApproval("model" in firstTask ? firstTask.model : undefined);
|
||||
if (itemModel) lines.push(`Model: ${itemModel}`);
|
||||
if ("task" in firstTask && typeof firstTask.task === "string") {
|
||||
lines.push(`Task:\n${truncateForPrompt(firstTask.task)}`);
|
||||
}
|
||||
@@ -663,7 +632,6 @@ 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 } } : {}),
|
||||
@@ -1419,7 +1387,6 @@ 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 },
|
||||
|
||||
@@ -113,7 +113,6 @@ export const taskItemSchema = type({
|
||||
"name?": "string",
|
||||
agent: "string = 'task'",
|
||||
task: "string",
|
||||
"model?": "string | string[]",
|
||||
"outputSchema?": outputSchemaInputSchema,
|
||||
"schemaMode?": '"permissive" | "strict"',
|
||||
"+": "delete",
|
||||
@@ -122,7 +121,6 @@ const taskItemSchemaIsolated = type({
|
||||
"name?": "string",
|
||||
agent: "string = 'task'",
|
||||
task: "string",
|
||||
"model?": "string | string[]",
|
||||
"outputSchema?": outputSchemaInputSchema,
|
||||
"schemaMode?": '"permissive" | "strict"',
|
||||
"isolated?": "boolean",
|
||||
@@ -137,8 +135,6 @@ 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. */
|
||||
@@ -151,7 +147,6 @@ export const taskSchema = type({
|
||||
"name?": "string",
|
||||
agent: "string = 'task'",
|
||||
task: "string",
|
||||
"model?": "string | string[]",
|
||||
"outputSchema?": outputSchemaInputSchema,
|
||||
"schemaMode?": '"permissive" | "strict"',
|
||||
"isolated?": "boolean",
|
||||
@@ -161,7 +156,6 @@ const taskSchemaNoIsolation = type({
|
||||
"name?": "string",
|
||||
agent: "string = 'task'",
|
||||
task: "string",
|
||||
"model?": "string | string[]",
|
||||
"outputSchema?": outputSchemaInputSchema,
|
||||
"schemaMode?": '"permissive" | "strict"',
|
||||
"+": "delete",
|
||||
@@ -206,7 +200,6 @@ function createTaskSchema(options: {
|
||||
"name?": "string",
|
||||
agent,
|
||||
task: "string",
|
||||
"model?": "string | string[]",
|
||||
"outputSchema?": outputSchemaInputSchema,
|
||||
"schemaMode?": '"permissive" | "strict"',
|
||||
"isolated?": "boolean",
|
||||
@@ -222,7 +215,6 @@ function createTaskSchema(options: {
|
||||
"name?": "string",
|
||||
agent,
|
||||
task: "string",
|
||||
"model?": "string | string[]",
|
||||
"outputSchema?": outputSchemaInputSchema,
|
||||
"schemaMode?": '"permissive" | "strict"',
|
||||
"+": "delete",
|
||||
@@ -238,7 +230,6 @@ function createTaskSchema(options: {
|
||||
"name?": "string",
|
||||
agent,
|
||||
task: "string",
|
||||
"model?": "string | string[]",
|
||||
"outputSchema?": outputSchemaInputSchema,
|
||||
"schemaMode?": '"permissive" | "strict"',
|
||||
"isolated?": "boolean",
|
||||
@@ -249,7 +240,6 @@ function createTaskSchema(options: {
|
||||
"name?": "string",
|
||||
agent,
|
||||
task: "string",
|
||||
"model?": "string | string[]",
|
||||
"outputSchema?": outputSchemaInputSchema,
|
||||
"schemaMode?": '"permissive" | "strict"',
|
||||
"+": "delete",
|
||||
@@ -293,8 +283,6 @@ 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. */
|
||||
|
||||
@@ -101,7 +101,6 @@ describe("task.batch schema gating", () => {
|
||||
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<string, unknown> } }).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<string>(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");
|
||||
|
||||
@@ -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<string>(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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user