Repository navigation
feat(agent): add a subagent model enforcement option - #3927
Open
czs914893385 wants to merge 1 commit into
Open
czs914893385 wants to merge 1 commit into
czs914893385 wants to merge 1 commit into
Conversation
Delegated subagents normally resolve to the configured subagent default (ai.agent_model_defaults.subagents.default, product default: the fast slot), and that default works. A Task call can override it per invocation, including model_id: "inherit", which pins the child to the parent session's model. That makes it impossible for a user to keep an expensive model on the planning session while guaranteeing every subagent runs on a cheaper one: the planning model decides, and observed sessions show it choosing inherit. Add ai.agent_model_defaults.subagents.enforce_default (runtime peer: ai.agent_model_selections.subagents.enforce_default), default false. When true, every internally created subagent session -- fresh spawns, forked children and reuse/send_input -- resolves to subagents.default, ignoring an explicit model_id, "inherit", the fork default, per-builtin overrides and custom-agent declared models. Approved external immutable bindings keep their approved model and their existing validation error. Resolution goes through one helper shared by the three creation paths, and the flag is exposed in Settings -> 运行设置 as a switch on the existing subagent default model row (en-US / zh-CN / zh-TW).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
ai.agent_model_defaults.subagents.enforce_default(runtime peer:ai.agent_model_selections.subagents.enforce_default, defaultfalse) plus a settings switch for it. When enabled, every internally created subagent session is resolved to the configured subagent default model, ignoring per-call model overrides — so a user can keep an expensive model on the session/planner and still guarantee subagents run on a cheaper one.Type: Feature (opt-in behavioral option)
Areas: Rust core (config contracts, config service, agent coordination), Web UI (settings), i18n
No tracking issue — happy to open one if you prefer the discussion to live there first.
Motivation / Impact
The subagent model already has a configured default (
ai.agent_model_defaults.subagents.default, product default: thefastslot) and that default works. In the sessions we inspected, everyTaskcall that omittedmodel_idresolved to the configured fast model — 200 spawns across 26 Ultimate-mode roots, all on the cheap model.The gap is the explicit override.
Taskacceptsmodel_id, including the special valueinherit("explicitly uses the same model as yours"). A planning model can therefore pin every child to the parent session's model, and the user has no way to prevent it. Real example, from a user's own session records (Ultimate mode, session modelgpt-6.1-sol, primary/fast both on DeepSeek):The user wanted "an expensive model plans, cheap models execute" and got an expensive model executing in every worker. Their model configuration was correct; only the per-call override leaked.
Impact for everyone who does not opt in: none.
falseand an absent field both keep today's resolution verbatim (pinned by tests). No new dependencies, no wire-format change, no migration; the new field is additive with a serde default and is carried through legacy→runtime projection.How
Config surfaces. The field is added to both the legacy typed defaults (
SubagentModelDefaultsConfig) and the runtime projection (SubagentModelSelections), andtranslate_legacy_model_selection_writeaccepts the new leaf plus the whole-.subagentsobject form, so old clients keep working.One resolution rule, one helper.
resolve_internal_subagent_model_targetin the coordinator either calls the existingresolve_subagent_model_targetunchanged (flag off) or short-circuits tosubagents.default(flag on). The three internal creation paths — fresh spawn, fork child, reuse/send_input— all route through it, so the rule is implemented once.What enforcement overrides, when on: an explicit
model_id,model_id: "inherit",subagents.fork, per-builtinsubagents.builtinoverrides, and a custom agent definition's declared model.What it deliberately does not touch:
MemoryPhase2consolidation (InternalAgentExecutionRequest,EphemeralChild, ownmemories.consolidation_model) is not delegation and is out of scope.UI. The subagent default model was already configurable, so this adds only a switch on that existing row rather than a second control elsewhere.
Exact location of the new switch:
It warns when enforcement is on but the row above is set to "inherit from the session", because that combination restricts nothing.
i18n: en-US / zh-CN / zh-TW.
Verification
Rust (on the pushed base):
Web UI:
New tests: absent field →
false;trueround-trip;from_legacycarries it; legacy dot-path and whole-object writes land on the runtime path and survive a restart; provider validation keeps the flag; and rule-level tests for explicit id /inherit/ fork / per-builtin override / reuse, both with the flag on and off. Each enforcement test was checked to fail without its rule, and each of the two hard-failure guards to fail without its guard.Pre-existing failures, unrelated to this change (same on the base commit; the files involved are untouched):
cargo test --lib agentic::coordination::coordinator::tests→ 4 failures inloaded_*_same_path_remote_record, panicking inservice/workspace/legacy_compat.rs:432withConfiguration("Remote workspace path must be an absolute POSIX path: C:\...")— a Windows-host fixture issue. 149 passed / 4 failed.npx tsc --noEmitinsrc/web-ui→ 3Cannot find module '@/generated/api'errors;src/web-ui/src/generated/holds only.gitkeepand that module is produced by the type-generation step.Reviewer Notes
ca7a55da+ this change set, launched with the project's owndesktop:preview:debugflow): Settings → Models & Agents → Execution & Collaboration → the 「Subagents」 section → the row directly below 「Default subagent model」. What is not covered: a real subagent run against a live provider proving the enforced model is the one actually used — the resolution rule is covered by unit/integration tests instead. Manual check of the stored value: toggle the switch, then confirmai.agent_model_selections.subagents.enforce_defaultin the config file.Cargo.lockandpnpm-lock.yamluntouched.enforce_defaultdescribes the rule ("the subagent default is enforced"), not a model class. If you prefer a different name or want it scoped per builtin id rather than all subagents, that is a small change.Checklist
中文说明
ai.agent_model_defaults.subagents.enforce_default(运行时同名字段在ai.agent_model_selections.subagents.enforce_default,默认false)。Task的model_id支持特殊值inherit("跟父会话同一个模型"),规划模型可以据此把每个子智能体都钉到父会话的贵模型上,而用户在界面上没有任何办法阻止。真实例子里(极致模式,会话用 gpt-6.1-sol、主力/快速都是 DeepSeek),SwarmPlanner与SwarmReviewer的 spawn 都带了model_id:"inherit",于是子智能体也跑到了贵模型上。send_input)一律解析到「子智能体默认模型」,忽略显式model_id、inherit、subagents.fork、按内置角色的覆盖以及自定义 agent 定义的模型。已批准的外部不可变绑定保持其原模型与原报错行为,不受影响。false,字段缺失也按false),无迁移、无新依赖、不改协议。