dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
Named subagent templates for DeepSeek Harness: a template fixes a starting route (or the route pool it resolves one from), an agent preset, and an optional persona, so a delegating agent picks by name.
Named subagent templates for DeepSeek Harness. Each template fixes a starting route — one provider/model pair, or a route pool a route is resolved from per child — an agent preset, and an optional persona, so a delegating agent picks a template by name and description instead of naming a provider, a model, and a reasoning effort on every call.
This is an out-of-tree bundle. It imports one package — the schema library
@deepseek-ai/schemastery, from which the row's Config schema is built; that
schema is what the Loader validates the row with and what the Settings surface
edits — and no Harness runtime package: otherwise it is plain JavaScript over the
ctx services, and lib/harness.js holds the small amount of Harness behavior
it replicates. Nothing in the Harness checkout changes.
A delegation is not a Harness subagent. It creates a root Session, exactly the way a person opening a session by hand does:
workspaceRegistry.create(cwd) → a workspace in the parent's directory
agents.create({ sessionId }) → a top-level Agent on the template's route
workspace.attachSession(id) → it appears in the normal session list
agent.followup(prompt) → it starts working
This is the shipped webhook Session-creation path
(packages/webhook/webhook/src/session.ts), not an invention. The consequence
is the point of the design:
@deepseek-ai/dsh-client-ui-model-selection
refuses any session with a subagentAddress, because those RPCs would
activate persisted history outside the direct-parent continuation path. A
delegated task and a session the user opens are the same kind of thing, so
they get the same surface: the picker, the preset selector, permission
presets, and a composer the user can type into.subagent tool and can open
its own template sessions. Nesting needs no recursion budget, because none of
this travels through ctx.subagents.Because the child's header deliberately carries no parentSession and no
origin, the Client never classifies it as a subagent. The parent/child
relationship therefore cannot live in the header — those two fields are exactly
what would re-hide the picker — so it lives in this plugin's own storage domain
(subagent_templates, table children) instead.
Every delegation carries a name, and that name is the child's only handle:
| Where the name appears | What it does |
|---|---|
| the child's Session title | the sidebar tab, the session list, and the panel row all read the same word |
the subagent tool result | a background call returns { kind, name }, plus siblings when the caller had other children; no Session id ever reaches the model |
| the settlement notice | subagent "explorer" finished. — the parent refers to the child by this word |
delete_subagent | takes names — one or several — not a Session id |
| the child's own prompt | tells the child what it is called, so ask_parent can speak for itself |
The name is stored verbatim: no character set is imposed, no case folding, no normalization. It must be non-blank, because it becomes a title and a handle. It is unique among one parent's live children — two children of the same parent cannot share a word, or every later reference to either would be ambiguous. A duplicate is refused before anything is created, so the failed call costs nothing; an in-flight claim covers two calls in one turn, which run concurrently. Deleting a child frees its name again.
Two different parents may each have a child called reviewer. The uniqueness is
per parent, because that is the scope any reference is resolved in.
A template child is an ordinary root Session. That is what keeps its model
picker and its full Session surface, and it is also exactly why it is
deliberately absent from ctx.subagents. The native delegation tools all read
that registry, so none of them can serve a template child:
| Native tool | Why it cannot serve a template child |
|---|---|
list_agents | reads ctx.subagents, which holds no template child — it would report an empty list |
send_message | reaches only a resident continuable child, and the Harness refuses that route for a session it does not classify as a subagent |
interrupt_agent | authorizes against a target's recorded lineage in the subagent service |
subagent_fork | creates a native child: a second kind of object with a second lifecycle |
This is not a configuration oversight. Registering a child with ctx.subagents is
precisely the thing that makes the Client treat it as a subagent and hide its
model picker, so the two are mutually exclusive for the same child.
This plugin therefore supplies the three capabilities that mattered, over its own mapping and naming:
| This plugin | Replaces | What it does |
|---|---|---|
list_subagents | list_agents | this session's children, by name, with the template each runs under and whether it is working, idle, or ended |
message_subagent | send_message | sends to a child by name: a working one takes it at its next step, an idle one starts a new turn with it — and the output that turn produces is reported back like the first one was |
interrupt_subagent | interrupt_agent | stops a child's current work and keeps it — session, transcript, and name all survive, so a message afterwards redirects it |
interrupt_subagent and delete_subagent are the two ends of one decision:
interrupt stops the work and leaves the child usable, delete ends it. An
interrupted child reports nothing for the turn that was stopped, because it did
not finish and there is no result to report; the delegation itself is still open,
and whatever the child's next turn produces is what reports (see "Results").
Two things are deliberately not reproduced:
subagent_fork. An inherited-context child is a different kind of object
with a different lifecycle, and re-adding it would reopen exactly the split this
design removes. Delegation with a self-contained prompt is what templates are
for.list_subagents reports direct children only, because that
is the scope the other tools can act on: a child that delegates again has its own
children, reachable from that child. A grandchild is a real object with no name
this session could use.To use the native tools instead, re-enable tool-subagent-control,
tool-subagent-control/list-agents, and tool-subagent-fork. A session then has
both surfaces, which is the state this plugin exists to avoid.
One caveat about the preset registry: a deployment decides which presets it
exposes and which one is its default. Disabling the three rows in that default
preset covers new sessions, but a session that explicitly selects a preset which
still mounts them gets the native surface back. Overriding such a preset would
mean restating its whole config, which is a worse trade than a preset choice
the user can see.
A delegated task usually has something the parent knows and the child does not:
a decision it already made, a file it meant, an answer it holds. A template
child is an ordinary Session, so it inherits the deployment's send_message —
which reaches only a resident continuable child, and the Harness refuses that
route for a session it does not classify as a subagent. This is that missing
route.
In the child's creation window the plugin registers, on the child's own scope so no other Session ever sees it:
ask_parent — send the delegating session a question or a finding. The
parent receives it at its next step, and an idle parent is woken by it. It
carries the same agent-message attribution, so the transcript shows who
asked — the header opens subagent "<name>" sent a message:, naming the child
rather than its Session id, and the attribution's own senderSessionId keeps
the sending Session identifiable. There is no
reply inside the child's turn, so the tool's own description asks for a
question the parent can answer in one go.ask_parent is for what the final
message cannot carry, not a second copy of it.The child also keeps the deployment's send_message, and the context says
plainly that this route cannot reach the delegating Session. It is deliberately
not withdrawn: a template child holds subagent_fork (in the personal
preset), which creates a continuable child, and send_message is the working
route to that one. Denying it for an unrelated reason would remove a capability
that works.
Both registrations live in the child's creation scope, so they belong to the
Agent that creation made. A Session the Host resumed later — after a restart, or
under a reloaded plugin row — arrives as a different Agent with neither, and the
mapping record is all that still says which session delegated it. The plugin
teaches such a child again when it sees the Agent appear; a scope that already
carries ask_parent refuses the second registration, and that refusal is the
"already taught" answer rather than a failure.
ask_parent, message_subagent and the settlement notice all travel through one
channel (lib/delivery.js), and it chooses between two routes on what the
receiving Session is doing at that moment. What it chooses is not a preference:
one route is readable in the Client and the other is not.
An idle recipient is queued the message as a turn of its own. The message
keeps its own source — agent-message for a question, subagent-settled for a
notice — because a delivery that opens a turn is already drawn as a visible turn
trigger under that source, with the icon and title that name who sent it.
A recipient that is already working takes the message at its next step, and
that delivery is spliced into a turn that is already running. The Web Chat draws
a message whose source is not user as injected context, and a context row is
not one of its visible rows — so the message would be read by the model and seen
by nobody. A mid-turn delivery therefore claims user, which is what makes it a
visible in-turn row. This is the same rule the office plugin follows for the
same reason.
The disguise is only the source's kind. Everything else about the message is
unchanged: the model-visible content, the durable MessageId, and every other
field on the source, including the senderSessionId that records who sent it. No
rpcId is written — the browser's prompt admission is the only writer of that
field, and a message without one is not read as a human instruction by anything
downstream.
Before every proposed step the Harness claims the recipient's whole inbox batch
as a durable deletion, and the batch is written back into the log as
user/message only once the request for that step is being prepared. Anything
that fails in between — a pre-step listener, a route that will not resolve, a
full disk — ends the turn with the batch claimed and never logged. The message is
then absent from the log and absent from the inbox, and the next turn the
recipient runs has no idea it was ever sent: continuing it would go on without
the finding.
The channel remembers what it handed over and watches the recipient's own log for
the message the model was actually shown. When a turn ends in error or in
blocked — a refusal that discarded the batch it had claimed — anything it is
still holding that is neither logged nor queued is put back and delivered again.
It is put back after the recipient goes idle, so the replacement is the first
thing the next turn claims rather than one the driver has already decided not to
start a turn for. A message the failed turn never claimed is still queued and is
left alone.
A cancellation is deliberately not repaired. A stop is somebody deciding this work should not continue; putting the message back into it would override that decision rather than repair a failure. A message is put back at most three times, after which it is dropped with a warning rather than retried forever.
| Registration | Scope | Purpose |
|---|---|---|
subagent (name is toolName) | the Host plane | delegates to one template under a name the parent chose, stating run_in_background; returns the child's output in the foreground, or that name in the background |
list_subagent_templates | the Host plane | reports each template's model and agent preset |
list_subagents | a delegating session's own scope | this session's children, by name, with template and work state |
message_subagent | a delegating session's own scope | sends a message to one of the caller's own children |
interrupt_subagent | a delegating session's own scope | stops one child's current work and keeps the child |
delete_subagent | a delegating session's own scope | the delegating agent deletes its own children by name — several names in one call, or "*" for all of them |
ask_parent + a parent-identity context | each child session's own scope | lets a delegated child ask its parent a question |
the subagentTemplates Session projection | the host | publishes a parent's children to the Client panel |
the Subagents right-Sidebar tab | the browser | lists this session's children, and opens or deletes one |
Only subagent and list_subagent_templates are registered at the Host plane.
The four tools that act on children — list_subagents, message_subagent,
interrupt_subagent, delete_subagent — are installed into a session's own
agent scope, at the moment that session's first delegation succeeds.
Before its first delegation every one of them could only answer "you have no subagent named that", so a session that never delegates never carries them: four tools of prompt for nothing, and four tempting dead ends. An agent-scoped registration also means the tools belong to one session rather than to the deployment — a child that delegates gets its own set for its own children, and no unrelated session ever sees them.
Two consequences worth stating plainly:
(no subagents), unknown) rather than the session having to earn
them a second time. That is also the only way list_subagents can report
nothing.A hot reload replaces this plugin while the children it recorded are still in the store, so activation adopts the live sessions that already have children — found through the Agent registry and the mapping store. Without that, a reloaded session would keep working and silently have no way to manage what it delegated. What that adoption cannot cover on its own is a restart, which brings no Agent back at all until the user opens one; see "What a restart changes" below.
Those four tools live in the Agent's scope rather than this plugin's, so they outlive the instance that registered them and the teardown that ends an instance has to withdraw them itself. A scope refuses two registrations of one name, so an instance that left a stale registration behind would make its replacement adopt nothing — and leave the session calling tools bound to the closed mapping domain. Adoption therefore keeps the registration's own disposer and runs it on unload, and a registration the scope refuses leaves the session unmarked, so a later delegation may adopt instead of the session losing the tools for good.
Both an agent-scoped registration and a settlement watch belong to one Agent, so both die with it. A hot reload keeps the Agents, which is why a reload was always survivable; a restart does not, and the Sessions come back as different Agents, one at a time, whenever the user opens them. A Host resumes no Session before it loads plugins, so anything this plugin does only while loading sees nothing at all on a cold start.
So the work is re-derived per Agent instead, from agent/created, in whichever
role that Session plays:
ask_parent back and a settlement
watch armed over its turns.Each watch resumes from a count, not from zero. A re-armed watch has two halves
to its memory of what the parent already has: the child's own log says how many
turns have ended, and the mapping record's reportedTurns says how many of them
reached the parent. "The child's log is ahead of the record" is exactly "there is
a finished turn the parent has not been told about", which is what makes a turn
that ended while the Host was down delivered once, when the parent comes
back, instead of lost or repeated. A record written before that counter existed
carries none, and is read as "everything the log holds" rather than as zero, so
an older delegation is never replayed.
The same wait decides what happens while the parent is closed: a notice with no Agent to land in is held rather than dropped, and delivered when that Session is resumed. Only an ended delegation gives up on it.
A restart therefore costs a session nothing it can still act on. What it does cost is in-flight work: a turn running in a child dies with the process, exactly as it would anywhere else, and a foreground delegation that was waiting on it loses its tool result and reports through the notice route instead.
The browser half (client.js) adds one tab type to the right Sidebar through the
two public stages any external tab type uses: sidebarRightTabs.register for
what the type is, and a keyed sidebar.right.pane.tab registration under that
definition's own id for the body. A guide entry is what makes the tab
discoverable.
The body is mounted per session and reads only framework data: the Host's
subagentTemplates projection for the template facts, the Session catalog for
whether a child still exists, and the per-session running state for the status
dot. It holds no subscription of its own and never touches the storage domain.
Each row is the child's name — the same word the parent uses — its template, its age, and its state. Clicking the name opens the child through the shipped workspace navigation, so it arrives as an ordinary session with its own picker. Deleting asks inside the row and then performs the shipped archive-with-stop; the plugin invents no session lifecycle. A child leaves the list when the Session catalog drops it, which is the archive's own consequence, so a refused archive keeps the row and puts the reason on it.
A delegated child is an ordinary Session, so it lives until something explicitly ends it:
delete_subagent with the child's name.Nothing else does. In particular a delegated child is not stopped when its
parent session closes, and not stopped when this plugin unloads — a code reload
must never destroy work in progress. delete_subagent stops the child, releases
its handle, and archives its session (removed from the lists, log kept), which is
the same operation the panel performs. It is scoped to the caller's own children:
a name the caller never gave is refused, with a result that says so rather than
one that names somebody else's child. To stop a child's work without ending
it, use interrupt_subagent.
"*" deletes alldelete_subagent takes names, an array, rather than one name per call. A
parent is most often finished with several children at once — a fan-out that
answered, plus a scope abandoned — and the cost of a rule is one call, not one
call per child. It answers { deleted, notFound }: the names it ended and the
names it never had, so a partly-failing batch reports each name honestly instead
of stopping at the first miss.
"*" is the one name that means every child this session delegated. It is
bounded by the same authority as any other name — the wildcard resolves against
the caller's own mapping — so it can only ever reach that session's own children,
never another's. It is worth stating what it costs: * is a destructive
operation over whatever this session still has running, including a child whose
answer has not arrived. Pass explicit names when the batch is known. Because the
wildcard is resolved by name, a child actually named * is deleted by any
wildcard call.
A delegation whose caller already has other children returns, in its tool result,
the rows list_subagents would have returned — name, work state, template — plus
the sentence that a subagent is never stopped for its parent:
started background subagent "reviewer"
You already delegated: explorer [idle]: medium, scout [working]: high. A subagent is never stopped for you, so when one is no longer needed — its answer is in, or its work is off track — delete it with
delete_subagent, which takes several names in one call and"*"for every subagent you delegated.
Both facts are needed at that moment and nowhere else: the parent's tool list says nothing about which children it holds, and nothing in the conversation says that deletion is its job alone. A first delegation has no other children, so its result carries neither, and both routes report the same way — the note follows a foreground call's output as well as a background call's name.
That lifetime needs two owners, and neither is the delegating call.
agent-loop keeps a listener on it from createAgent until disposal),
so a child created with the tool call's own exec.signal is torn down the moment that
call returns — which is exactly what a background delegation does. The child is created
with a controller this plugin owns instead.ctx.get() returns a traced service, and AgentRegistry.create() binds
the new Agent's lifecycle to the context the call was read through: agent-loop
registers the Agent's whole teardown as an effect of that context's fiber, cancelling
its machine with disposed when the fiber unloads. Created through this plugin's own
context, a child would die with this plugin's loader entry — on an HMR edit of a watched
file, or on any profile recomposition whose composed row for this entry chan