Skip to content

feat(agent-core): per-workspace subagent model bindings#1928

Open
Yorha9e wants to merge 2 commits into
MoonshotAI:mainfrom
Yorha9e:feat/subagent-model-binding
Open

feat(agent-core): per-workspace subagent model bindings#1928
Yorha9e wants to merge 2 commits into
MoonshotAI:mainfrom
Yorha9e:feat/subagent-model-binding

Conversation

@Yorha9e

@Yorha9e Yorha9e commented Jul 19, 2026

Copy link
Copy Markdown

Related Issue

Resolve #1927

Problem

See linked issue.

What changed

Bindings belong to the user, not the model. Model selection for delegated work is treated as user configuration, applied mechanically — the calling agent never sees or overrides it. This deliberately avoids LLM-facing model directories/parameters: in testing, agents would self-fill a model argument in ways that silently overrode user intent.

  • Bindings persist in <projectRoot>/.kimi-code/local.toml under [subagent.<type>], following the existing workspace.additional_dir pattern
  • First unbound spawn of a type asks the user once interactively and persists the answer (an explicit "keep inheriting" choice is recorded too); non-interactive environments simply inherit
  • New /subagent-model command (list / set <type> / clear <type>) via a new session RPC chain (agent-core → node-sdk → TUI)
  • Precedence: workspace binding > profile binding (new modelAlias/thinkingEffort on profile subagents entries) > parent inheritance
  • Sticky resume/retry: a subagent always keeps its configured model/effort (restored via config.update wire records); no mid-conversation switches. Unresolvable stored aliases fail fast with a configuration error instead of silently realigning to the parent — a deliberate behavior change from today's realign-on-resume
  • Effective model/effort exposed in the subagent.spawned event, AgentBackgroundTaskInfo, approval label, and tool result text
  • Gated by the subagent-model-selection experiment (off by default, KIMI_CODE_EXPERIMENTAL_SUBAGENT_MODEL_SELECTION=1), so default behavior is unchanged

Validation

  • New coverage: binding store round-trips, first-use ask flow (model/effort/inherit/dismiss), AgentTool binding application & gating & approval label, sticky resume, spawn override + event payload, profile bindings, SDK RPC round-trip, /subagent-model flows — all passing
  • Full agent-core suite: no regressions vs base (remaining failures are pre-existing Windows environment issues: missing /bin/bash, symlink/UNC semantics)
  • tsc --noEmit clean (agent-core, protocol, node-sdk, apps/kimi-code); oxlint 0 errors in touched files
  • Live TUI smoke: first-use ask → persisted binding → mechanical re-application → sticky resume → /subagent-model list/clear, all verified

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

@changeset-bot

changeset-bot Bot commented Jul 19, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f8293ac

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ccf0f39e9d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const { child, profileName } = await this.ensureIdleSubagent(agentId, parent);
// Sticky semantics: a resumed child always keeps its configured
// model/effort; mid-conversation model switches are not supported.
const modelAlias = this.resolveChildModel(parent, child.config.modelAlias);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate sticky resume behind the experiment

When the experiment is off, this still changes the default resume path: the only flag check is in AgentTool's binding lookup, but Agent(resume=...) now resolves the child's persisted alias instead of the current parent alias. A user who changes or removes the main model and then resumes an old subagent will keep the stale child model or get CONFIG_INVALID, whereas the pre-change code realigned to the parent; gate sticky resume/retry with subagent-model-selection or keep the old realignment for unbound children.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — the sticky resume was applying unconditionally, which contradicted the "default behavior unchanged" claim. Fixed: resume()/retry() now check parent.experimentalFlags.enabled('subagent-model-selection'). With the flag on, a resumed child keeps its configured model/effort (sticky, no mid-conversation switches; unresolvable stored aliases fail fast with CONFIG_INVALID). With the flag off, resume realigns the child to the parent's current model exactly as before, byte-for-byte the old behavior — effort is untouched on resume in both modes. Added test coverage for both the flag-off realign path and the flag-on sticky/error paths.

const operation = resumeAgentId !== undefined && resumeAgentId.length > 0 ? 'resume' : 'spawn';
// Workspace model binding (experiment-gated): applied mechanically at
// spawn; the first unbound spawn may ask the user once interactively.
const binding = await this.resolveSpawnBinding(requestedProfileName ?? 'coder', operation, true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply workspace bindings to AgentSwarm launches

With workspace binding enabled, this is the only path that reads or asks for .kimi-code/local.toml bindings. AgentSwarm launches item-based subagents through AgentSwarmToolSessionSubagentHost.runQueued()spawn() without going through this code or passing modelAlias/thinkingEffort, so /subagent-model set coder is ignored for swarm-spawned coder agents even though the feature is described as applying to every new subagent of that type. Move binding resolution into the shared spawn path or wire it through AgentSwarm too.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct — AgentSwarm launches go through runQueued() and never see the bindings. For this PR we've deliberately scoped bindings to Agent-tool spawns only and narrowed the docs accordingly (bindings are applied to subagents spawned via the Agent tool; AgentSwarm support is future work). Rationale: swarm model routing deserves its own design (per-item vs per-batch bindings, ramp behavior), and we'd rather not ship a half-considered version of it here. If maintainers prefer, a clean follow-up is to move binding resolution into the shared SessionSubagentHost.spawn path so swarm benefits mechanically with no swarm-specific code — happy to do that in a separate PR.

Comment on lines +183 to +184
if (target.modelAlias === undefined && subagent.modelAlias !== undefined) {
target.modelAlias = subagent.modelAlias;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Store profile overrides on the subagent edge

Profile bindings are properties of an owner's subagents entry, but this mutates the shared resolved target profile. If two owner profiles bind the same subagent type differently, whichever owner is processed first permanently sets target.modelAlias, so later owners silently run that subagent on the wrong model. Keep the override on the owner-to-subagent link instead of the global target profile.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reply: profile overrides are stored on the shared resolved target

Thanks for the careful read — the mechanism is described accurately. Sharing our analysis of why we kept it, and what the actual blast radius is.

What the mechanism is

A model binding declared in an owner's subagents: entry is conceptually a property of the owner → subagent edge ("when this owner delegates to coder, use model M"). applySubagentOverrides instead writes it onto the shared resolved target profile (coder.modelAlias = ...), because every subagent type resolves to a single global ResolvedAgentProfile object.

When it would break

The failure mode requires two owner profiles binding the same subagent type differently:

# agent.yaml
subagents:
  coder:
    modelAlias: cheap-model

# researcher.yaml
subagents:
  coder:
    modelAlias: strong-model

Both entries point at the same coder object; the first owner processed wins, and the second owner's binding is silently skipped. Codex's point is exactly right about this.

Why we assess it as low-risk in practice

  1. Unreachable with shipped configuration. The bundled profiles have exactly one owner (agent.yaml) declaring subagent entries. No second owner exists, so the conflict cannot occur out of the box.
  2. The pattern is pre-existing, not introduced here. The description field has used the identical shared-target mechanism since applySubagentDescriptions. This PR extends an established pattern rather than inventing a new one — if the shared-target hazard ever becomes real, it needs solving for description and modelAlias/thinkingEffort together.
  3. Bounded impact. The worst case is a subagent running on an unintended model — a configuration surprise, not data loss or a correctness failure, and it is corrected by editing one binding.
  4. Discovery cost is low by design of this PR. The effective model is now visible in the tool result (model: line), the subagent.spawned event payload, and AgentBackgroundTaskInfo. A wrong-model outcome is observable immediately rather than silent.

What we did about it

  • Added a code comment on applySubagentOverrides documenting the shared-target limitation and the direction of the proper fix (moving overrides onto the owner→subagent edge).
  • We explicitly chose not to do the edge-storage refactor in this PR: it would change the shape of ResolvedAgentProfile.subagents and every consumer for a scenario that is currently unreachable.

If maintainers disagree

Two clean fallbacks, and we're happy to take either:

  • Edge storage: keep { profile, modelAlias, thinkingEffort } per owner→subagent link and resolve per-owner at spawn time.
  • Drop profile bindings from this PR: the workspace local.toml bindings are the core feature and cover the user need; profile-level bindings were included for completeness and can be split into a follow-up.

Yorha9e pushed a commit to Yorha9e/kimi-code that referenced this pull request Jul 19, 2026
…xperiment

Address review feedback on MoonshotAI#1928:

- resume/retry only keep the child's configured model when
  subagent-model-selection is enabled; with the experiment off, resume
  realigns to the parent's current model exactly as before
- spawn-time profile modelAlias/thinkingEffort bindings are likewise
  ignored unless the experiment is enabled
- docs: workspace bindings apply to Agent tool spawns only; AgentSwarm
  does not read bindings yet (swarm-wide routing is future work)
- note the shared-target limitation of profile override resolution
@Yorha9e
Yorha9e force-pushed the feat/subagent-model-binding branch from d5dca1f to 9bd3cb2 Compare July 19, 2026 18:07
@wraghadismail-creator

Copy link
Copy Markdown

Thanks — the experiment-gating part looks fixed, but I think two original review points are still unresolved.

  • AgentSwarm still doesn’t apply workspace bindings. Updating the docs helps, but it doesn’t fix the behavior. I think binding resolution should live in the shared spawn path so Agent tool spawns and AgentSwarm launches behave the same way.

  • The shared-target override issue also looks unchanged. The new comment documents the limitation, but owner-specific model/thinking-effort overrides still seem like they should stay scoped to the owner->subagent edge rather than mutating shared resolved target state.

  • Could we follow up on those remaining pieces here, or at least track them in a separate issues?

@wraghadismail-creator

Copy link
Copy Markdown

Thanks for the work here — the overall direction looks good, but I think there’s still a remaining freshness gap around session.list_changed.

From the PR notes, this path can stall for >45s on macOS under multi-instance fs load because it still depends on fs event delivery. I agree that GET /sessions with ownership is the authoritative path, but I think the current watch-based hint mechanism still needs a reconciliation fallback so clients don’t sit on stale session/ownership state for too long.

Suggested fix:

  • keep fs/watch events as the fast hint
  • add a periodic readdir/stat reconciliation loop in SessionListWatchService
  • rebuild the authoritative session/ownership view from

@Yorha9e

Yorha9e commented Jul 20, 2026

Copy link
Copy Markdown
Author

Thanks — the experiment-gating part looks fixed, but I think two original review points are still unresolved.
AgentSwarm still doesn’t apply workspace bindings. Updating the docs helps, but it doesn’t fix the behavior. I think binding resolution should live in the shared spawn path so Agent tool spawns and AgentSwarm launches behave the same way.
The shared-target override issue also looks unchanged. The new comment documents the limitation, but owner-specific model/thinking-effort overrides still seem like they should stay scoped to the owner->subagent edge rather than mutating shared resolved target state.
Could we follow up on those remaining pieces here, or at least track them in a separate issues?

Thanks for pushing on this — you're right that narrowing the docs doesn't change behavior, and I appreciate the concrete suggestion.
On AgentSwarm specifically, we've spent some time mapping the current swarm machinery (SubagentBatch scheduling, a single uniform subagent_type per batch, one prompt_template, and a star topology where members can't see each other). The current swarm is honestly still fairly minimal here: applying workspace bindings as-is would only bind one model for the entire batch, which isn't actually useful. What swarm really needs is per-member model routing — a swarmIndex → model mapping so different members of one batch can run different models — and that deserves its own design rather than bolting a type-level binding onto a uniform batch.
We're actively exploring what a better swarm should look like, including MoA-style orchestration and intra-swarm communication, so I'd rather not rush a half-fitting change into this PR. Once that exploration lands, I'll file a proper issue/proposal for swarm-level model routing and link it back here.
On the profile edge point: agreed the override belongs on the owner→subagent edge in principle — I'll track that refactor in the same follow-up so it doesn't get lost.
Thanks again for the careful review — this is exactly the push the design needed.

@Yorha9e

Yorha9e commented Jul 20, 2026

Copy link
Copy Markdown
Author

Thanks for the work here — the overall direction looks good, but I think there’s still a remaining freshness gap around session.list_changed.

From the PR notes, this path can stall for >45s on macOS under multi-instance fs load because it still depends on fs event delivery. I agree that GET /sessions with ownership is the authoritative path, but I think the current watch-based hint mechanism still needs a reconciliation fallback so clients don’t sit on stale session/ownership state for too long.

Suggested fix:

  • keep fs/watch events as the fast hint
  • add a periodic readdir/stat reconciliation loop in SessionListWatchService
  • rebuild the authoritative session/ownership view from

This looks unrelated to this PR's scope (per-workspace subagent model bindings) — the session-list freshness/watch mechanism isn't touched here and deserves its own issue or PR. Flagging so it doesn't get lost

yorha9e added 2 commits July 20, 2026 18:15
Add an experimental subagent-model-selection feature (off by default,
KIMI_CODE_EXPERIMENTAL_SUBAGENT_MODEL_SELECTION=1) that binds configured
model aliases and thinking efforts to subagent types per workspace:

- Bindings live in <projectRoot>/.kimi-code/local.toml [subagent.<type>]
  and are applied mechanically at spawn (workspace binding > profile
  binding > parent inheritance); the calling agent never sees or
  overrides them, removing LLM discretion from model routing
- First unbound spawn of a type asks the user once interactively and
  persists the answer, including an explicit keep-inheriting choice
- /subagent-model command (list/set/clear) manages bindings via a new
  session RPC chain (agent-core -> node-sdk -> TUI)
- Resume/retry are sticky: a subagent always keeps the model/effort it
  was configured with (persisted via config.update wire records); no
  mid-conversation model switches. Unresolvable stored aliases fail fast
  with a configuration error instead of silently realigning
- Agent profiles can bind modelAlias/thinkingEffort per subagent type
- Effective model/effort are exposed in the subagent.spawned event,
  AgentBackgroundTaskInfo, approval label, and tool result text
…xperiment

Address review feedback on MoonshotAI#1928:

- resume/retry only keep the child's configured model when
  subagent-model-selection is enabled; with the experiment off, resume
  realigns to the parent's current model exactly as before
- spawn-time profile modelAlias/thinkingEffort bindings are likewise
  ignored unless the experiment is enabled
- docs: workspace bindings apply to Agent tool spawns only; AgentSwarm
  does not read bindings yet (swarm-wide routing is future work)
- note the shared-target limitation of profile override resolution
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.

per-workspace model bindings for subagent types/子agent指定模型与思考强度功能

2 participants