Merge PR #7621: fix(coding-agent/eval): remove per-call model override from agent() (@szavadsky)

This commit is contained in:
can1357
2026-08-05 01:12:01 +02:00
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.
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
View File
@@ -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.
+1
View File
@@ -16,6 +16,7 @@
- Fixed translated MCP importers (Claude Code, Cursor, Gemini CLI, Windsurf, VS Code) silently dropping a server's `enabled: false` flag, so a server disabled at the source config stayed mounted; the flag is now propagated and honored like Codex, OpenCode, and native `mcp.json`. These importers now also load project entries before same-named user entries (matching native/Codex) so a project `enabled: false` suppresses a same-named user server ([#7652](https://github.com/can1357/oh-my-pi/issues/7652)).
- 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.8] - 2026-08-04
### 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 } } : {}),
+4 -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))
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
@@ -545,6 +542,9 @@ function agent(prompt::String; agent="task", model=nothing, label=nothing, schem
if merge !== nothing
args_dict["merge"] = Bool(merge)
end
if haskey(kwargs, :model)
error("agent() no longer accepts a per-call model override; the selected agent's frontmatter model is used")
end
handle_result = handle
for (k, v) in kwargs
args_dict[string(k)] = v
@@ -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:
+1 -2
View File
@@ -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,