feat(delegation): optional per-task model selection in delegate_task - #55286
feat(delegation): optional per-task model selection in delegate_task#55286teknium1 wants to merge 1 commit into
Conversation
Port from Kilo-Org/kilocode#11786. When delegation.allow_model_selection is enabled, delegate_task accepts an optional per-task `model` field so the agent can fan work out across different models (e.g. review the same code with Opus, GPT-5, and GLM in parallel). The agent names a model the way a human would ("opus", "gpt-5", "glm", or a full vendor/model slug); resolution reuses the existing aggregator-aware model_switch pipeline (the same chain /model uses), so the provider is resolved — not dictated — preferring the parent's current provider. Gated off by default to preserve the documented "subagents inherit the parent model" contract and avoid silently routing work to a more expensive model. The schema field only appears when the flag is on (built in _build_dynamic_schema_overrides, so prompt caching stays valid for the life of a session). Unresolvable names return a clear per-task tool error rather than silently falling back to the default model.
🔎 Lint report:
|
| Rule | Count |
|---|---|
invalid-argument-type |
7 |
invalid-assignment |
3 |
unresolved-attribute |
1 |
First entries
tools/delegate_tool.py:3206: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(i: SupportsIndex, /) -> str, (s: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> list[str]]` cannot be called with key of type `Literal["properties"]` on object of type `list[str]`
tools/delegate_tool.py:3205: [invalid-assignment] invalid-assignment: Cannot assign to a subscript on an object of type `str`
tests/tools/test_delegate_model_selection.py:76: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(i: SupportsIndex, /) -> str, (s: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> list[str]]` cannot be called with key of type `Literal["properties"]` on object of type `list[str]`
tools/delegate_tool.py:2383: [unresolved-attribute] unresolved-attribute: Attribute `strip` is not defined on `list[str] & ~AlwaysFalsy` in union `(Any & ~AlwaysFalsy) | (str & ~AlwaysFalsy) | (list[str] & ~AlwaysFalsy) | Literal[""]`
tools/delegate_tool.py:3206: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `bound method str.__getitem__(key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> str` cannot be called with key of type `Literal["model"]` on object of type `str`
tools/delegate_tool.py:3206: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(i: SupportsIndex, /) -> str, (s: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> list[str]]` cannot be called with key of type `Literal["model"]` on object of type `list[str]`
tests/tools/test_delegate_model_selection.py:76: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(i: SupportsIndex, /) -> Unknown, (s: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> list[Unknown]]` cannot be called with key of type `Literal["tasks"]` on object of type `list[Unknown]`
tools/delegate_tool.py:3205: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `Literal["model"]` and value of type `dict[str, str]` on object of type `list[str]`
tools/delegate_tool.py:3200: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `Literal["model"]` and value of type `dict[str, str]` on object of type `dict[Unknown | str, dict[str, str | dict[str, str] | dict[str, str | dict[str, dict[str, str] | dict[str, str | dict[str, str]] | dict[str, str | list[str]]] | list[str]] | list[str]]]`
tests/tools/test_delegate_model_selection.py:76: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `bound method str.__getitem__(key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> str` cannot be called with key of type `Literal["properties"]` on object of type `str`
tests/tools/test_delegate_model_selection.py:76: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `bound method str.__getitem__(key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> str` cannot be called with key of type `Literal["tasks"]` on object of type `str`
✅ Fixed issues: none
Unchanged: 6174 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
Cluster note: per-task model selection in |
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: LGTM
Per-task model selection for delegate_task, gated behind config flag.
Looks Good
- Off by default: preserves subagent-inherits-parent-model contract
- Reuses existing model_switch resolution pipeline (no new model tool)
- Cache-safe: model field only in dynamic schema overrides when flag is on
- 128 lines of tests covering schema gating, resolution, and error cases
- Ported from Kilo-Org/kilocode with footprint ladder compliance
Summary
delegate_taskcan now fan a single task out across different models — review the same code with several models in parallel — when the user opts in viadelegation.allow_model_selection.Ported (adapted) from Kilo-Org/kilocode#11786, which added per-task model + reasoning-variant selection to that project's
agent_managerchat tool (the direct analog of ourdelegate_task).What it does
With
delegation.allow_model_selection: true, the agent can name a model per task —"opus","gpt-5","glm", or a fullvendor/modelslug. The provider is resolved, not dictated: names are matched leniently against the parent's current provider (aggregator-aware), exactly the way the/modelcommand resolves them. Off by default, so the documented "subagents inherit the parent model" contract is unchanged unless you opt in.How it was adapted
agent_manager_modelsdiscovery tool. We skip it to respect the footprint ladder — the per-taskmodelfield rides on the existingdelegate_taskschema and is the only addition.hermes_cli.model_switch.switch_model()pipeline (/model's resolution chain), so vendor/model slug conversion, fuzzy aliasing, and cross-provider fallback all come for free.modelfield is injected in_build_dynamic_schema_overrides()only when the flag is on. The flag is a stable per-session config value, so the system prompt stays byte-stable for the life of a conversation — prompt caching is preserved.Changes
tools/delegate_tool.py:_get_allow_model_selection(),_resolve_task_model_creds(), per-task creds resolution in the child-build loop, top-levelmodelparam, handler wiring, conditional schema injection.hermes_cli/config.py:delegation.allow_model_selection(defaultFalse).website/docs/user-guide/features/delegation.md: documents the flag + behavior.tests/tools/test_delegate_model_selection.py: 10 new tests.Validation
modelin tool schemamodelhonoredmodel_switch)"sonnet"→anthropic/claude-sonnet-4.6on the parent aggregator; full slug passthrough; bad name →ValueError).tests/hermes_cli/tests green (config validation, config drift, model_switch all unaffected).Infographic