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:
+1
-1
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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 } } : {}),
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user