Skip to content

feat(agent): add a subagent model enforcement option - #3927

Open
czs914893385 wants to merge 1 commit into
openBitFun:mainfrom
czs914893385:feat/subagent-model-enforcement
Open

czs914893385 wants to merge 1 commit into
openBitFun:mainfrom
czs914893385:feat/subagent-model-enforcement

Conversation

@czs914893385

@czs914893385 czs914893385 commented Nov 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds ai.agent_model_defaults.subagents.enforce_default (runtime peer: ai.agent_model_selections.subagents.enforce_default, default false) 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: the fast slot) and that default works. In the sessions we inspected, every Task call that omitted model_id resolved to the configured fast model — 200 spawns across 26 Ultimate-mode roots, all on the cheap model.

The gap is the explicit override. Task accepts model_id, including the special value inherit ("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 model gpt-6.1-sol, primary/fast both on DeepSeek):

turn-0014.json:  "agent_type":"SwarmPlanner","model_id":"inherit"
                 -> "subagentModelId":"gpt-6.1-sol"
turn-0015.json:  "agent_type":"SwarmReviewer","model_id":"inherit"
                 -> "subagentModelId":"gpt-6.1-sol"

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. false and 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), and translate_legacy_model_selection_write accepts the new leaf plus the whole-.subagents object form, so old clients keep working.

One resolution rule, one helper. resolve_internal_subagent_model_target in the coordinator either calls the existing resolve_subagent_model_target unchanged (flag off) or short-circuits to subagents.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-builtin subagents.builtin overrides, and a custom agent definition's declared model.

What it deliberately does not touch:

  • Sessions on an approved external immutable binding keep their approved model and its existing fingerprint error. Verified for both the reuse path and the fork path (a fork child clones the parent's session config, so it can carry such a binding — enforcement must not re-pin it or the later binding check fails hard).
  • MemoryPhase2 consolidation (InternalAgentExecutionRequest, EphemeralChild, own memories.consolidation_model) is not delegation and is out of scope.
  • User-initiated session model edits are unaffected.

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:

Settings → Models & Agents → Execution & Collaboration
(设置 → 模型与智能体 → 执行与协作)
Scroll to the 「Subagents」/「子智能体」 section near the bottom of that page. Its first row is 「Default subagent model」/「子智能体默认模型」 (the existing model dropdown). The new switch is the row directly below it: 「强制子智能体固定使用该模型」 / "Always use this model for subagents".

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):

cargo test -p openbitfun-config-contracts
# ok. 153 passed; 0 failed

cargo test --locked -p openbitfun-core --no-default-features --features agent-runtime,git --lib subagent_model
# ok. 3 passed; 0 failed

cargo test --locked -p openbitfun-core --no-default-features --features agent-runtime,git --lib -- \
  enforced_subagent_default disabled_subagent_default approved_immutable enforced_inherit_default reused_subagent
# ok. 9 passed; 0 failed

cargo test -p openbitfun-core --no-default-features --lib service::config::
# ok. 80 passed; 0 failed

cargo check -p openbitfun-core --no-default-features
cargo fmt -p openbitfun-config-contracts -p openbitfun-core -- --check

Web UI:

cd src/web-ui
node scripts/run-vitest.mjs run src/app/scenes/settings src/infrastructure/config --maxWorkers=4
# Test Files 123 passed (123)   Tests 1110 passed (1110)

node scripts/audit-appearance-contracts.mjs   # passed
node scripts/i18n-audit.mjs                   # Passed with 0 warning(s)
npx eslint <changed files>                    # 0 problems

New tests: absent field → false; true round-trip; from_legacy carries 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 in loaded_*_same_path_remote_record, panicking in service/workspace/legacy_compat.rs:432 with Configuration("Remote workspace path must be an absolute POSIX path: C:\...") — a Windows-host fixture issue. 149 passed / 4 failed.
  • npx tsc --noEmit in src/web-ui → 3 Cannot find module '@/generated/api' errors; src/web-ui/src/generated/ holds only .gitkeep and that module is produced by the type-generation step.

Reviewer Notes

  • AI-assisted: yes. The implementation, tests and this description were produced with an AI agent working under review; every claim above is backed by a command that was actually run. Testing level: unit/integration tested, plus a visual check in a locally built debug app — no live end-to-end provider run. The Rust and Web UI suites listed above were executed and are green. The switch was then seen rendering in a debug desktop build (main @ ca7a55da + this change set, launched with the project's own desktop:preview:debug flow): 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 confirm ai.agent_model_selections.subagents.enforce_default in the config file.
  • Screenshots: not part of this description (the GitHub API cannot upload images). I have a full-window shot and a close-up of the row above; say the word and I will attach them, or drag them in from the local build.
  • The change set is 13 files: 5 Rust, 8 Web UI/locales. Cargo.lock and pnpm-lock.yaml untouched.
  • Naming: enforce_default describes 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

  • This PR is focused and does not include secrets, temporary prompts, generated scratch files, or unrelated artifacts.
  • Relevant verification is recorded above, or skipped checks are explained.
  • User-facing strings, docs, and locales are updated where applicable.

中文说明

  • 新增 ai.agent_model_defaults.subagents.enforce_default(运行时同名字段在 ai.agent_model_selections.subagents.enforce_default,默认 false)。
  • 开关位置:设置 → 模型与智能体 → 执行与协作,把页面往下滚到「子智能体」分区;该分区第一行是「子智能体默认模型」(原有下拉框),新开关就是它正下方那一行,文案为「强制子智能体固定使用该模型」。
  • 背景:子智能体本来就有默认模型(默认等于「快速模型」),而且这条默认是生效的——我们核查过的真实会话里,200 次没有显式指定模型的 Task 调用全部落在便宜模型上。问题出在显式覆盖:Task 的 model_id 支持特殊值 inherit("跟父会话同一个模型"),规划模型可以据此把每个子智能体都钉到父会话的贵模型上,而用户在界面上没有任何办法阻止。真实例子里(极致模式,会话用 gpt-6.1-sol、主力/快速都是 DeepSeek),SwarmPlanner 与 SwarmReviewer 的 spawn 都带了 model_id:"inherit",于是子智能体也跑到了贵模型上。
  • 开启开关后:所有内部创建的子智能体会话(新 spawn、fork 子会话、复用/send_input)一律解析到「子智能体默认模型」,忽略显式 model_id、inherit、subagents.fork、按内置角色的覆盖以及自定义 agent 定义的模型。已批准的外部不可变绑定保持其原模型与原报错行为,不受影响。
  • 不开启时行为与现状完全一致(默认 false,字段缺失也按 false),无迁移、无新依赖、不改协议。
  • 因为「子智能体默认模型」本来就已经可配(就是上面那个位置:设置 → 模型与智能体 → 执行与协作 → 「子智能体」分区第一行,那里还额外支持模型池),所以这里只加一个开关,没有在别处再加一个重复的模型选择项。

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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant