fix(coding-agent/eval): remove per-call model override from agent() (#6438)

Completes the maintainer's removal of per-call model selection from
subagent spawns (9f8aa87dbf removed it from the task tool and the
model-facing agent() docs/prompt, but the eval agent() runtime and all
four preludes still accepted and forwarded a per-call model).

Subagents now always resolve through the selected agent's frontmatter
model and settings, so an explicit model: "default" can no longer
silently route children onto the parent session model.

- agent-bridge: drops "model?" from agentArgsSchema and the request
  forward; adds "+": "delete" so a legacy model argument is stripped
  (same contract as the task wire schemas).
- JS/Python/Ruby/Julia preludes: remove the model parameter from
  agent(); completion()'s tier selector is unchanged.
- docs (tools/eval.md, python-repl.md) updated to the removed surface.

Refs #6438
This commit is contained in:
Slava Zavadsky
2026-08-04 08:35:01 -04:00
parent 003bb5548c
commit b8779dae63
10 changed files with 31 additions and 24 deletions
+1 -1
View File
@@ -146,7 +146,7 @@ The backend settings `eval.py` / `eval.js` default to `true`; `eval.rb` / `eval.
The tool's session-scoped schema lists only enabled runtimes. If Python preflight fails while another runtime is enabled, `eval` remains available for that runtime and a `py` call reports a Python-backend availability error with enabled alternatives. The tool's session-scoped schema lists only enabled runtimes. If Python preflight fails while another runtime is enabled, `eval` remains available for that runtime and a `py` call reports a Python-backend availability error with enabled alternatives.
Python prelude helpers include `agent(prompt, *, agent="task", model=None, label=None, schema=None, schema_mode=None, isolated=None, apply=None, merge=None, handle=False)`. It synchronously calls the host bridge and returns final text, or parsed data when `schema` is supplied. `schema_mode` selects permissive or strict structured-output handling; the isolation/apply/merge flags control task worktree behavior. With `handle=True`, it returns a DAG node dict (`{"text", "output", "handle", "id", "agent"}`) whose handle is the recoverable `agent://<id>` URI; parsed output is also stored under `"data"` when available. Python prelude helpers include `agent(prompt, *, agent="task", label=None, schema=None, schema_mode=None, isolated=None, apply=None, merge=None, handle=False)`. It synchronously calls the host bridge and returns final text, or parsed data when `schema` is supplied. `schema_mode` selects permissive or strict structured-output handling; the isolation/apply/merge flags control task worktree behavior. With `handle=True`, it returns a DAG node dict (`{"text", "output", "handle", "id", "agent"}`) whose handle is the recoverable `agent://<id>` URI; parsed output is also stored under `"data"` when available.
## Execution flow and cancellation/timeout ## Execution flow and cancellation/timeout
+2 -2
View File
@@ -149,9 +149,9 @@ A stateless, tool-free one-shot model call:
Runs one subagent through `runStructuredSubagent(...)`: Runs one subagent through `runStructuredSubagent(...)`:
- JS supports the preferred `await agent(prompt, { agent?, model?, label?, schema?, schemaMode?, isolated?, apply?, merge?, handle? })`; legacy positional slots are still implemented. - JS supports the preferred `await agent(prompt, { agent?, label?, schema?, schemaMode?, isolated?, apply?, merge?, handle? })`; legacy positional slots are still implemented.
- Python/Ruby/Julia use keyword arguments (`schema_mode` outside JS). - Python/Ruby/Julia use keyword arguments (`schema_mode` outside JS).
- `agent` defaults from the current spawn policy. `model` may pin a selector/fallback chain. `schema` overrides agent/session schemas; `schemaMode`/`schema_mode` chooses `permissive` or `strict`. - `agent` defaults from the current spawn policy; the selected agent's frontmatter model and settings always apply (there is no per-call model override — `model` is not accepted). `schema` overrides agent/session schemas; `schemaMode`/`schema_mode` chooses `permissive` or `strict`.
- `isolated` requests isolation. `apply` controls whether captured changes are integrated; `merge=false` selects patch mode while the normal setting controls branch mode. - `isolated` requests isolation. `apply` controls whether captured changes are integrated; `merge=false` selects patch mode while the normal setting controls branch mode.
- `handle=true` returns `{ text, output, handle, id, agent }`, optional parsed `data`, and isolation metadata instead of only output/data. - `handle=true` returns `{ text, output, handle, id, agent }`, optional parsed `data`, and isolation metadata instead of only output/data.
- Eval subagents are one-shot (`keepAlive=false`), are unregistered/disposed after completion, and **do not share the caller's eval executor** (`shareEvalSession=false`). Their code mutations therefore do not appear in the caller's retained VM/kernel. - Eval subagents are one-shot (`keepAlive=false`), are unregistered/disposed after completion, and **do not share the caller's eval executor** (`shareEvalSession=false`). Their code mutations therefore do not appear in the caller's retained VM/kernel.
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased] ## [Unreleased]
### Changed
- Removed the per-call `model` override from the eval `agent()` helper (all runtimes), completing the earlier task-tool removal (`9f8aa87dbf`). Subagents always use their selected agent's frontmatter model and settings; a legacy `model` argument is silently ignored, so an explicit `model: "default"` can no longer route children onto the parent session model ([#6438](https://github.com/can1357/oh-my-pi/issues/6438)).
## [17.2.7] - 2026-08-03 ## [17.2.7] - 2026-08-03
### Changed ### Changed
@@ -23,7 +23,6 @@ export const EVAL_AGENT_BRIDGE_NAME = "__agent__";
const agentArgsSchema = type({ const agentArgsSchema = type({
prompt: "string>0", prompt: "string>0",
"agent?": "string>0", "agent?": "string>0",
"model?": "string>0|string>0[]",
"label?": "string", "label?": "string",
"schema?": "unknown", "schema?": "unknown",
"schemaMode?": "'permissive' | 'strict'", "schemaMode?": "'permissive' | 'strict'",
@@ -31,12 +30,12 @@ const agentArgsSchema = type({
"apply?": "boolean", "apply?": "boolean",
"merge?": "boolean", "merge?": "boolean",
"handle?": "boolean", "handle?": "boolean",
"+": "delete",
}); });
interface EvalAgentArgs { interface EvalAgentArgs {
prompt: string; prompt: string;
agent?: string; agent?: string;
model?: string | string[];
label?: string; label?: string;
schema?: unknown; schema?: unknown;
schemaMode?: StructuredSubagentSchemaMode; schemaMode?: StructuredSubagentSchemaMode;
@@ -148,7 +147,6 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
invocationKind: "eval", invocationKind: "eval",
assignment: parsed.prompt, assignment: parsed.prompt,
...(parsed.agent !== undefined ? { agent: parsed.agent } : {}), ...(parsed.agent !== undefined ? { agent: parsed.agent } : {}),
...(parsed.model !== undefined ? { model: parsed.model } : {}),
...(Object.hasOwn(parsed, "schema") ? { outputSchema: parsed.schema } : {}), ...(Object.hasOwn(parsed, "schema") ? { outputSchema: parsed.schema } : {}),
...(parsed.schemaMode !== undefined ? { schemaMode: parsed.schemaMode } : {}), ...(parsed.schemaMode !== undefined ? { schemaMode: parsed.schemaMode } : {}),
...(parsed.label !== undefined ? { identity: { label: parsed.label } } : {}), ...(parsed.label !== undefined ? { identity: { label: parsed.label } } : {}),
+1 -4
View File
@@ -519,14 +519,11 @@ function completion(prompt::String; model="default", system=nothing, schema=noth
return schema === nothing ? text : Main.json_parse(string(text)) return schema === nothing ? text : Main.json_parse(string(text))
end end
function agent(prompt::String; agent="task", model=nothing, label=nothing, schema=nothing, schema_mode=nothing, isolated=nothing, apply=nothing, merge=nothing, handle=false, kwargs...) function agent(prompt::String; agent="task", label=nothing, schema=nothing, schema_mode=nothing, isolated=nothing, apply=nothing, merge=nothing, handle=false, kwargs...)
args_dict = Dict{String, Any}("prompt" => prompt) args_dict = Dict{String, Any}("prompt" => prompt)
if agent !== nothing if agent !== nothing
args_dict["agent"] = agent args_dict["agent"] = agent
end end
if model !== nothing
args_dict["model"] = model
end
if label !== nothing if label !== nothing
args_dict["label"] = label args_dict["label"] = label
end end
@@ -104,8 +104,8 @@ if (!globalThis.__omp_js_prelude_loaded__) {
"agent", "agent",
opts, opts,
rest, rest,
["agent", "model", "label", "schema", "isolated", "apply", "merge", "schemaMode"], ["agent", "label", "schema", "isolated", "apply", "merge", "schemaMode"],
"{ agent, model, label, schema, isolated, apply, merge, schemaMode, handle }", "{ agent, label, schema, isolated, apply, merge, schemaMode, handle }",
); );
const { handle, ...callArgs } = o; const { handle, ...callArgs } = o;
const res = await globalThis.__omp_call_tool__("__agent__", { prompt, ...callArgs, handle: Boolean(handle) }); const res = await globalThis.__omp_call_tool__("__agent__", { prompt, ...callArgs, handle: Boolean(handle) });
@@ -488,7 +488,6 @@ if "__omp_prelude_loaded__" not in globals():
prompt, prompt,
*, *,
agent="task", agent="task",
model=None,
label=None, label=None,
schema=None, schema=None,
schema_mode=None, schema_mode=None,
@@ -506,8 +505,6 @@ if "__omp_prelude_loaded__" not in globals():
args = {"prompt": prompt} args = {"prompt": prompt}
if agent is not None: if agent is not None:
args["agent"] = agent args["agent"] = agent
if model is not None:
args["model"] = model
if label is not None: if label is not None:
args["label"] = label args["label"] = label
if schema is not None: if schema is not None:
+1 -2
View File
@@ -392,10 +392,9 @@ unless defined?($__omp_prelude_loaded) && $__omp_prelude_loaded
schema.nil? ? text : JSON.parse(text) schema.nil? ? text : JSON.parse(text)
end end
def agent(prompt, agent: "task", model: nil, label: nil, schema: nil, schema_mode: nil, isolated: nil, apply: nil, merge: nil, handle: false) def agent(prompt, agent: "task", label: nil, schema: nil, schema_mode: nil, isolated: nil, apply: nil, merge: nil, handle: false)
args = { "prompt" => prompt } args = { "prompt" => prompt }
args["agent"] = agent unless agent.nil? args["agent"] = agent unless agent.nil?
args["model"] = model unless model.nil?
args["label"] = label unless label.nil? args["label"] = label unless label.nil?
args["schema"] = schema unless schema.nil? args["schema"] = schema unless schema.nil?
args["schemaMode"] = schema_mode unless schema_mode.nil? args["schemaMode"] = schema_mode unless schema_mode.nil?
@@ -290,10 +290,7 @@ describe("runEvalAgent", () => {
}), }),
}); });
await runEvalAgent( await runEvalAgent({ prompt: " hello ", label: "My Agent", schema }, { session, signal: abortController.signal });
{ prompt: " hello ", label: "My Agent", model: "p/override", schema },
{ session, signal: abortController.signal },
);
await runEvalAgent({ prompt: "plain" }, { session }); await runEvalAgent({ prompt: "plain" }, { session });
const firstOptions = runSpy.mock.calls[0]?.[0]; const firstOptions = runSpy.mock.calls[0]?.[0];
@@ -306,10 +303,26 @@ describe("runEvalAgent", () => {
expect(firstOptions.outputSchemaOverridesAgent).toBe(true); expect(firstOptions.outputSchemaOverridesAgent).toBe(true);
expect(firstOptions.assignment).toBe("hello"); expect(firstOptions.assignment).toBe("hello");
expect(firstOptions.description).toBe("My Agent"); expect(firstOptions.description).toBe("My Agent");
expect(firstOptions.modelOverride).toEqual(["p/override"]); // No per-call override: the agent's own frontmatter model applies.
expect(firstOptions.modelOverride).toEqual(["p/current"]);
expect(secondOptions.outputSchema).toBeUndefined(); expect(secondOptions.outputSchema).toBeUndefined();
expect(secondOptions.outputSchemaOverridesAgent).toBeUndefined(); expect(secondOptions.outputSchemaOverridesAgent).toBeUndefined();
}); });
it("drops a per-call model argument on agent() (removed, issue #6438)", async () => {
mockAgents();
const runSpy = vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => singleResult(options));
// The schema strips unknown keys; a legacy `model` argument is silently
// discarded so resolution is identical to omitting it — the agent's own
// frontmatter model applies (issue #6438).
await runEvalAgent({ prompt: "work", model: "default" }, { session: makeSession() });
await runEvalAgent({ prompt: "work" }, { session: makeSession() });
const withModel = runSpy.mock.calls[0]?.[0];
const withoutModel = runSpy.mock.calls[1]?.[0];
expect(withModel?.modelOverride).toEqual(withoutModel?.modelOverride);
});
it("returns host-parsed data for caller, agent, and inherited schemas", async () => { it("returns host-parsed data for caller, agent, and inherited schemas", async () => {
const agentSchema = { type: "object" }; const agentSchema = { type: "object" };
const sessionSchema = { type: "object" }; const sessionSchema = { type: "object" };
@@ -66,12 +66,11 @@ describe("eval js agent() handle", () => {
) => Promise<unknown>; ) => Promise<unknown>;
const schema = { type: "object", properties: { ok: { type: "boolean" } } }; const schema = { type: "object", properties: { ok: { type: "boolean" } } };
await positionalAgent("scout", "reviewer", "p/model", "Legacy", schema, true, false, true, "strict"); await positionalAgent("scout", "reviewer", "Legacy", schema, true, false, true, "strict");
expect(seenArgs).toEqual({ expect(seenArgs).toEqual({
prompt: "scout", prompt: "scout",
agent: "reviewer", agent: "reviewer",
model: "p/model",
label: "Legacy", label: "Legacy",
schema, schema,
isolated: true, isolated: true,