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.
|
||||
|
||||
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
|
||||
|
||||
|
||||
+2
-2
@@ -149,9 +149,9 @@ A stateless, tool-free one-shot model call:
|
||||
|
||||
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).
|
||||
- `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.
|
||||
- `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.
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
## [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
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -23,7 +23,6 @@ export const EVAL_AGENT_BRIDGE_NAME = "__agent__";
|
||||
const agentArgsSchema = type({
|
||||
prompt: "string>0",
|
||||
"agent?": "string>0",
|
||||
"model?": "string>0|string>0[]",
|
||||
"label?": "string",
|
||||
"schema?": "unknown",
|
||||
"schemaMode?": "'permissive' | 'strict'",
|
||||
@@ -31,12 +30,12 @@ const agentArgsSchema = type({
|
||||
"apply?": "boolean",
|
||||
"merge?": "boolean",
|
||||
"handle?": "boolean",
|
||||
"+": "delete",
|
||||
});
|
||||
|
||||
interface EvalAgentArgs {
|
||||
prompt: string;
|
||||
agent?: string;
|
||||
model?: string | string[];
|
||||
label?: string;
|
||||
schema?: unknown;
|
||||
schemaMode?: StructuredSubagentSchemaMode;
|
||||
@@ -148,7 +147,6 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
|
||||
invocationKind: "eval",
|
||||
assignment: parsed.prompt,
|
||||
...(parsed.agent !== undefined ? { agent: parsed.agent } : {}),
|
||||
...(parsed.model !== undefined ? { model: parsed.model } : {}),
|
||||
...(Object.hasOwn(parsed, "schema") ? { outputSchema: parsed.schema } : {}),
|
||||
...(parsed.schemaMode !== undefined ? { schemaMode: parsed.schemaMode } : {}),
|
||||
...(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))
|
||||
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)
|
||||
if agent !== nothing
|
||||
args_dict["agent"] = agent
|
||||
end
|
||||
if model !== nothing
|
||||
args_dict["model"] = model
|
||||
end
|
||||
if label !== nothing
|
||||
args_dict["label"] = label
|
||||
end
|
||||
|
||||
@@ -104,8 +104,8 @@ if (!globalThis.__omp_js_prelude_loaded__) {
|
||||
"agent",
|
||||
opts,
|
||||
rest,
|
||||
["agent", "model", "label", "schema", "isolated", "apply", "merge", "schemaMode"],
|
||||
"{ agent, model, label, schema, isolated, apply, merge, schemaMode, handle }",
|
||||
["agent", "label", "schema", "isolated", "apply", "merge", "schemaMode"],
|
||||
"{ agent, label, schema, isolated, apply, merge, schemaMode, handle }",
|
||||
);
|
||||
const { handle, ...callArgs } = o;
|
||||
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,
|
||||
*,
|
||||
agent="task",
|
||||
model=None,
|
||||
label=None,
|
||||
schema=None,
|
||||
schema_mode=None,
|
||||
@@ -506,8 +505,6 @@ if "__omp_prelude_loaded__" not in globals():
|
||||
args = {"prompt": prompt}
|
||||
if agent is not None:
|
||||
args["agent"] = agent
|
||||
if model is not None:
|
||||
args["model"] = model
|
||||
if label is not None:
|
||||
args["label"] = label
|
||||
if schema is not None:
|
||||
|
||||
@@ -392,10 +392,9 @@ unless defined?($__omp_prelude_loaded) && $__omp_prelude_loaded
|
||||
schema.nil? ? text : JSON.parse(text)
|
||||
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["agent"] = agent unless agent.nil?
|
||||
args["model"] = model unless model.nil?
|
||||
args["label"] = label unless label.nil?
|
||||
args["schema"] = schema unless schema.nil?
|
||||
args["schemaMode"] = schema_mode unless schema_mode.nil?
|
||||
|
||||
@@ -290,10 +290,7 @@ describe("runEvalAgent", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
await runEvalAgent(
|
||||
{ prompt: " hello ", label: "My Agent", model: "p/override", schema },
|
||||
{ session, signal: abortController.signal },
|
||||
);
|
||||
await runEvalAgent({ prompt: " hello ", label: "My Agent", schema }, { session, signal: abortController.signal });
|
||||
await runEvalAgent({ prompt: "plain" }, { session });
|
||||
|
||||
const firstOptions = runSpy.mock.calls[0]?.[0];
|
||||
@@ -306,10 +303,26 @@ describe("runEvalAgent", () => {
|
||||
expect(firstOptions.outputSchemaOverridesAgent).toBe(true);
|
||||
expect(firstOptions.assignment).toBe("hello");
|
||||
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.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 () => {
|
||||
const agentSchema = { type: "object" };
|
||||
const sessionSchema = { type: "object" };
|
||||
|
||||
@@ -66,12 +66,11 @@ describe("eval js agent() handle", () => {
|
||||
) => Promise<unknown>;
|
||||
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({
|
||||
prompt: "scout",
|
||||
agent: "reviewer",
|
||||
model: "p/model",
|
||||
label: "Legacy",
|
||||
schema,
|
||||
isolated: true,
|
||||
|
||||
Reference in New Issue
Block a user