Repository navigation
dsh-team-models: per-teammate model selection for Agent Teams (Host plugin, MIT) #13303
QinLuza
started this conversation in
Show Your Plugins!
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Per-Teammate Model Selection for DeepSeek Harness Agent Teams
1. The problem: teammates pinned to a single model route
DeepSeek Harness (DSH) Agent Teams lets multiple agents collaborate: one Lead orchestrates, several teammates each own a job. It works well — until you hit two situations:
Intuitively, "switch a teammate's model" should be a config option. Reading the source shows it simply isn't possible today:
subagent/descriptorevent (versioned, first-event-wins). Every later wake-up — message, resume, cold recovery — rebuilds the route from that snapshot and never re-resolves the current default.spawn_teammatetool through the roster to runtime creation, there is no entry point for a model argument.session/agent-busyfor any session withorigin === "subagent"; the desktop model picker hides itself for teammate sessions.Stack those three layers and the teammate's model seat is empty. The only "fix" is to tear the teammate down and rebuild it — but roster names cannot be reused and failed members occupy slots permanently, so rebuilding is expensive.
2. The idea: rewrite the route at request time, without touching the base
DSH is an "everything is a plugin" architecture, and model selection itself has a public export:
installModelSelection(agentCtx, selection). It couples a mutable selection to three waterfalls:system-prompt/assemble: snapshot the current selection when prompt assembly begins;agent/request: afternext(), override the final request route with the snapshot;agent/pre-step: append a[model changed: ...]notice when the route changes.The key observation: this function works on any
agent.ctx. The official code only installs it at the entry points it manages; nobody installs it for teammate sessions — so we install our own selection reference on each teammate'sagent.ctx. Route resolution becomes a request-time concern:The descriptor snapshot is never modified, the spawn chain is never modified, the base stays untouched. With nothing configured, all three waterfalls pass through — behavior is identical to not having the plugin installed.
3. Design highlights
Four agent-callable tools, so the model itself can manage routing rather than only a human clicking UI:
team_model_listteam_model_setteam_model_resetteam_model_defaultIdentity comes from the execution context, not the arguments.
exec.agentin the tool handler is the exact caller injected by the loop; combined with team membership it determines the role. A Lead may target any teammate; a teammate may only name itself. Writing someone else's name in the arguments is rejected outright — authorization is enforced at the execution point, not by prompt wording.Persistence and consistency. Overrides and the team default live in
$DSH_HOME/storages/team-models/state.json: write a copy first, atomic rename, swap the in-memory state only after the write succeeds. Reads do field-level validation; bad entries are dropped with a warning, and a corrupt JSON file is quarantined before starting empty. State survives restarts.Three invariants, each asserted by unit tests:
model/selectionevent as a best-effort echo.4. A few representative pitfalls
listMembers(agent), notlist(membership)— the latter lives on the private roster. The first version assumed the intuitive name and threwTypeErroron the first call. Lesson: when an external plugin talks to host services, verify every signature against the source.nullthrough for non-required fields; a doc example writingdefaultModel: nullcrashed the plugin at startup. Fix: the seed accepts only non-empty strings, and the example became "omit to leave unset".team_model_resetwithout a target, the first version cleared the entire table — including other members. Review caught it; now a teammate must explicitly name itself, and clearing everything is Lead-only. "Omitted argument = global operation" defaults are a particularly easy trap in multi-agent tools.model/selectioncan miss under certain timing (target not in the live list), but routing itself is unaffected. Treat it as a nice-to-have, not a correctness dependency, and the design relaxes.5. How to verify it actually works
A model saying "I'm using model X" is not evidence. The hard evidence is in the session log:
request/headerroute equals the override and differs from the dispatch route insubagent/descriptor;Offline, vitest runs the waterfalls against the real
installModelSelection: with no configuration it asserts field-for-field passthrough; with configuration it asserts the override and effort clearing. Add the routing-precedence truth table, the identity/authorization matrix, and storage corruption recovery — 49 cases in total. On-device, the behavior gates above pass on both desktop and Android (arm64).A fun side effect: setting a teammate to a level the provider doesn't support produces an explicit "unsupported reasoning effort" error at request time — which turns out to be the hardest possible proof that the override really reached the request.
6. Usage
npm install && npm run buildAdd the package to a profile's
dependenciesand todsh.profile.bundles(dsh-team-models), then restart the host. All configuration is optional:Then let the Lead call the tools in conversation:
{ "tool": "team_model_set", "target": "grader", "model": "provider-b/model-b" }Changes take effect on the teammate's next step and never interrupt an in-flight request.
7. Limitations and retirement
Being honest about the boundaries: teammates only (the Lead's own model stays on the official path); no client UI in this release; state is keyed by member name without a team dimension, so two teams with identically named members would share overrides (single-team deployments are the intended scope); clearing an override falls back without a
[model changed]notice (the upstream notice waterfall early-returns when no selection is present).More importantly, the retirement condition: this plugin exists because upstream does not yet offer per-teammate model selection. There are already several community feature requests; once the official project supports it (or lets
model/selectionoverride the descriptor), this plugin should be retired rather than maintained in parallel. The destiny of an external patch is to be absorbed upstream — and that's a good thing.8. Links
>=0.2.1-rc.5 <0.3.0)If you use Agent Teams and have been stuck on "teammates can't switch models", try it and file issues.
All reactions