Skip to content

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

Open
Yorha9e wants to merge 7 commits into
MoonshotAI:mainfrom
Yorha9e:feat/v2-subagent-model-bindings
Open

feat(agent-core-v2): per-workspace subagent model bindings#2034
Yorha9e wants to merge 7 commits into
MoonshotAI:mainfrom
Yorha9e:feat/v2-subagent-model-bindings

Conversation

@Yorha9e

@Yorha9e Yorha9e commented Jul 22, 2026

Copy link
Copy Markdown

Related Issue

Related to #1928 — that PR brings per-workspace subagent model bindings to the v1 engine (packages/agent-core). This PR ports the same capability to packages/agent-core-v2, where no equivalent exists today.

Problem

On the v2 engine, every subagent spawn copies the caller's model and thinking effort verbatim (Agent tool, swarm, and resume paths alike). Users who organize work around specialist subagents cannot route a subagent type (or a specific spawn) to a different model — something the v1 engine can do via #1928's [subagent.<type>] bindings in .kimi-code/local.toml. As kap-server sessions run on v2, the v1 mechanism covers a shrinking share of real usage.

What changed

All behavior is gated behind the subagent-model-selection experiment flag (env KIMI_CODE_EXPERIMENTAL_SUBAGENT_MODEL_SELECTION, default off). With the flag off, behavior is byte-for-byte identical to today, including wire payloads.

  • Storage: IProjectLocalConfigService (the workspaceLocalConfig domain, renamed by feat: agent-core-v2 permission/workspace refactors and transcript durability #2021 — this branch is rebased past it and follows the new naming) learns to read and write [subagent.<type>] and [subagent-slot.<name>] sections in .kimi-code/local.toml, preserving unrelated TOML content (same file format as the v1 side, so one workspace config serves both engines).
  • Resolution: a shared pure-function resolver (session/subagent/bindingResolution.ts) resolves the effective model/thinking for each spawn with precedence slot > type binding > inherit caller. Stale aliases (not configured or no longer resolvable) warn and fall through; inherit: true records an explicit keep-inheriting choice.
  • Spawn paths: both creation sites — the Agent tool and the swarm scheduler — resolve through it. The Agent tool gains an optional binding_slot parameter (the model is named by user configuration, never by the LLM); AgentSwarm accepts a batch-wide binding_slot for item-spawned members.
  • Resume: with the flag on, resume is sticky (no realign to the caller's current model) and validates the child's bound alias up front, failing fast with a clear error instead of mid-turn.
  • Ask-once: in interactive sessions, spawning an unbound type (or a requested-but-missing slot, or a stale binding) asks the user once via ISessionQuestionService and records the answer — including an explicit "keep inheriting" choice. One spawn asks at most once; non-interactive clients and dismissals fall back to silent inheritance; the swarm path never asks.
  • Observability: subagent.spawned carries the effective modelAlias/thinkingEffort when the flag is on (the protocol schema already reserved these fields; the kap-server zod schema gains them additively).

Seven commits: the binding MVP, sticky resume + swarm slots, ask-once, three follow-up fixes (fail-fast resume validation, at-most-one-ask per spawn, review feedback), and a mechanical rename-follow for #2021 (workspaceLocalConfigprojectLocalConfig).

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 (64 new assertions across the storage round-trip, resolver precedence, both spawn paths, sticky resume, and ask-once flows).
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update (experimental, off by default; docs can land when the flag graduates).

Verification: typecheck and lint:domain green for @moonshot-ai/agent-core-v2 (921 files) and typecheck green for @moonshot-ai/kap-server; the agent-core-v2 vitest run on this branch matches the clean-main baseline failure-for-failure on this Windows host (only the pre-existing platform failures remain — the #2021-era ones included — and the branch adds 64 passing tests on top).

@changeset-bot

changeset-bot Bot commented Jul 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ba15016

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

ℹ️ 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".

try {
result = await deps.question.request(
{ questions: [{ question: item.question, header: 'Subagent', options: item.options }] },
{ agentId: deps.agentId, signal: deps.signal },

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 Abort instead of dismissing cancelled binding prompts

When the parent Agent call is cancelled while this prompt is pending, SessionQuestionService handles the supplied signal by dismissing the question and resolving null, not by throwing. askOne then returns undefined, so launch falls through to create and emit a subagent before runAgentTurn observes the aborted signal, leaving a spawned child with no run/terminal event. Check the signal after request returns and rethrow the abort instead of treating it as a user dismissal.

Useful? React with 👍 / 👎.

let result: QuestionResult;
try {
result = await deps.question.request(
{ questions: [{ question: item.question, header: 'Subagent', options: item.options }] },

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 Provide unique IDs for binding questions

This new direct question request does not set id or toolCallId, so SessionQuestionService.requestId falls back to question:${Date.now()}. When multiple unbound Agent calls ask in the same millisecond, the second pending interaction overwrites the first in the pending map and one tool call can hang forever. Include a unique request id for each model/effort prompt.

Useful? React with 👍 / 👎.

Comment on lines +123 to +125
binding_slot: z
.string()
.optional()

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 Hide binding_slot while the experiment is disabled

When subagent-model-selection is off, this parameter is still advertised and accepted in the tool schema (and AgentSwarm adds the same unguarded parameter), changing the LLM/API surface even though resolution ignores it. Keep the new argument out of the schema/description or reject it unless the flag is enabled.

AGENTS.md reference: AGENTS.md:L62-L64

Useful? React with 👍 / 👎.

Comment on lines +45 to +48
/**
* Context for one interactive ask-once point: `slot` is set when the ask
* concerns a named binding slot rather than a subagent type, and
* `missingModel` is set when a stored binding references a model alias that

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Move non-header comments into the module header

This package allows comments only in the top-of-file block; this new doc block, along with the following helper/interface doc blocks in the file, sits beside declarations and violates the package comment convention. Fold any externally useful responsibility into the file header or remove the local narration.

AGENTS.md reference: packages/agent-core-v2/AGENTS.md:L13-L13

Useful? React with 👍 / 👎.

yorha9e added 6 commits July 22, 2026 21:09
Resolve [subagent.<type>] / [subagent-slot.<name>] bindings from
.kimi-code/local.toml at subagent spawn (slot > type > inherit), add the
Agent tool binding_slot parameter, and carry the effective model and
thinking effort on subagent.spawned. Gated by the subagent-model-selection
experiment; behavior is unchanged while the flag is off.
…lots

While the subagent-model-selection experiment is on, resuming a subagent
no longer realigns it to the caller's model, and AgentSwarm accepts a
batch-wide binding_slot applied to every item-spawned member. Flag-off
behavior is unchanged.
While the subagent-model-selection experiment is on, the Agent tool asks
the user once when an unbound subagent type or requested slot is spawned,
or when a stored binding references a vanished model alias, and records
the answer (including an explicit keep-inheriting choice) in
.kimi-code/local.toml. Non-interactive clients and dismissals fall back
to silent inheritance; the swarm path never asks.
…y resume

Validate the resumed child's bound model alias before a sticky resume so
a stale binding surfaces a clear error instead of failing mid-turn (v1
parity with resolveChildModel). Also cover aliases that exist but cannot
be resolved in binding warnings, and warn when inherit=true is set
alongside an ignored model or thinking_effort.
When a binding slot was explicitly requested and its interactive ask was
dismissed, the resolver fell through to the type section and fired a
second, type-level question in the same spawn. A dismissal is a 'stop
asking' signal: the type binding still applies when configured, but the
type-level ask is now suppressed whenever a slot was explicitly
requested.
Fold PR-added narration into file headers per the package comment
convention, rethrow the abort when a cancelled binding prompt resolves
as a dismissal, give binding questions unique request ids, and stop
advertising binding_slot in the Agent/AgentSwarm tool schemas while the
subagent-model-selection experiment is disabled. Pin the tools-snapshot
tests to a disabled flag stub so they pass in either environment.
@Yorha9e
Yorha9e force-pushed the feat/v2-subagent-model-bindings branch from 8cb835e to ba15016 Compare July 22, 2026 13:16
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.

2 participants