插件目录 / Developer / dsh-subagent-registry
dsh-subagent-registry
已验证 · 实测可装 fan56
功能简介
Developer 类别的 DeepSeek Harness 插件。
可用 — 实测通过,早期项目
Developer 类别的 DeepSeek Harness 插件。 实测能干净安装、正常启动。早期项目,但功能可用。
「已验证」表示我们的自动化 CI 在干净 profile 里实际执行了 dsh plugin add 并启动成功——仅此而已。功能描述与版本兼容性均为作者声明。这不是安全审计,也不代表对第三方代码的背书。
README
dsh-subagent-registry
Register locally-defined custom agents (~/.dsh/agents/*.md) as callable
subagents in dsh: the main conversation
can invoke any of them by name through the use_agent tool, and each runs as
a real dsh subagent with its own persona (system prompt). When a run is
interrupted (error, cancellation, crash, token limit), the next use_agent
call for the same agent resumes it from its saved partial work instead of
restarting from scratch.
中文简介:把 ~/.dsh/agents/*.md 定义的自定义 agent(frontmatter 元数据 +
markdown 正文作为 persona)注册成 dsh 可按名调用的 subagent。主对话通过use_agent 工具点名调用;每个自定义 agent 以独立 subagent 运行,拥有自己
的 system prompt,跑在 dsh 自带的 spawn provider 上,不需要 patch dsh 本体。
How it works
- One file per agent:
<agents-dir>/<name>.md— a loosekey: value
frontmatter block (name,description,model,deep,thinking,display_name, …) followed by a markdown body that is used verbatim as
the child's persona (system prompt). - One tool: at session startup the plugin registers
use_agent
(configurabletoolName). The tool's static description carries the roster
— every agent name plus its sanitized description — so the main model can
pick an agent by name. - At call time the target file is re-read and parsed; the body becomes the
child'spersona, the frontmattermodel(provider/modelroute) is split
intoagentOptions, and the child is started through the already-assembledspawnsubagent provider (the same single-instance realm dsh uses for its
native subagent tool). The result is returned to the parent conversation.
Installation
Option A — add this checkout as a dsh plugin (tui profile):
dsh plugin --profile tui add ~/github/dsh-subagent-registry
Option B — npm dependency: npm i @aiwayds/dsh-subagent-registry, then load
the plugin under the stable id dsh-subagent-registry in your profile's config, or mount
it through a bundle patch (see cordis.patch.yml in this repo for the pattern).
Configuration
| Config field | Default | Description |
|---|---|---|
agentsDir |
~/.dsh/agents |
Directory holding <name>.md agent definitions. |
provider |
spawn |
Subagent provider the child runs through (reuses dsh-base's spawn). |
toolName |
use_agent |
Name of the registered tool. |
leafDenyTools |
[] (computed default) |
Explicit tool-deny list installed on deep: 0 (leaf) children. Empty = computed default (every agent-spawning tool in the dsh base distribution plus toolName). |
resume |
auto |
When use_agent continues a prior interrupted run: auto resumes whenever one exists, opt-in only when the caller passes resume: true, off never (an explicit resume: true still overrides). |
Resuming interrupted runs
Long subagent runs (>10 min) die for many reasons — API errors, cancellation,
a crashed dsh process, a token limit — and re-dispatching the same agent used
to mean redoing everything from zero. It doesn't anymore.
Nothing extra is logged: dsh already persists every in-process subagent
child as a full session under the deployment's session store
(~/.dsh/sessions/...), interrupted runs included. What was missing is the
recall layer, which this plugin now provides on top of the stock mechanisms:
- On each
use_agentcall, the plugin enumerates the calling conversation's
prior one-shot children (ctx.subagents.listChildren, which merges the
live store with session persistence) and picks the newest inactive child
whose creation label matches the requested agent. - The child's persisted event log is classified by its last
turn/end:
anything other thancompleted(error, aborted, max-tokens, crash with no
recorded turn result) marks the run as interrupted and resumable. - The child session is resumed (
ctx.agents.resume) with the same
composition a fresh dispatch applies — the current agent-file body as the
persona, the frontmattermodelroute, and the leaf tool scoping fordeep: 0agents — and driven for exactly one continuation turn with a
"continue where you left off, don't redo finished work" instruction. The
original task and all partial work are already in the child's replayed
context. - The result flows back to the parent like any
use_agentresult, prefixed
with a provenance line naming the resumed session. If the continuation
fails again, the next call simply resumes it again — each retry keeps
accumulating progress.
Tool-call parameters:
| Parameter | Effect |
|---|---|
fresh: true |
Force a clean start, ignoring any interrupted prior run. |
resume: true |
Require resuming; the call fails loudly if no interrupted run exists. |
Lookups (and the resume itself) fail open: with no session persistence
mounted, an unreadable log, any enumeration error, or a resume that cannot
even start (e.g. a concurrent duplicate resume raced to the session id), the
tool silently falls back to a fresh dispatch — resume is an optimization,
never a blocker. A continuation turn that runs and fails again is different:
its partial output is kept and surfaced as an error, never silently thrown
away. Note that only runs dispatched with a label are discoverable; this
plugin has always stamped display_name ?? agent name as the label, so
pre-existing failed runs are resumable too.
deep semantics
deep is the agent's spawn-depth budget, declared in the frontmatter:
deep |
Meaning |
|---|---|
0 |
Leaf: the agent runs normally but can never start a subagent. |
>= 1 |
May start subagents. Default when the key is absent: 1. |
Implementation, at use_agent execute time:
deep: 0— the start request carriestoolFilter: { deny: [...] }and
nomaxDepth. The in-processspawndriver applies the filter as a
scopedtools.restrict()in the child's creation window, so the named tools
vanish from the child's tool prompt and refuse to execute — the child keeps
its full non-spawn tool set but has zero spawn capability. PassingmaxDepth: 0(the old behavior) would have rejected the child's own start,
since the child's absolute depth is always ≥ 1. Default deny list:subagent,subagent_fork,workflow,ralph, plus this plugin's own
tool name (use_agentby default; a customizedtoolNameis denied
automatically).send_message/interrupt_agent/list_agentsonly
address already-running children and cannot spawn, so they stay visible.
Override withleafDenyToolswhen your deployment's tool set differs.deep >= 1— notoolFilter;maxDepthis set to the child's absolute
depth plusdeep— a relative budget. Start can never be blocked by
the depth check (childDepth ≤ childDepth + deepalways holds), while the
"deep = how many generations of subagents I may open" reading is preserved.
Each subsequent delegation level enforces its own per-request caps (the
native subagent tool defaults tomaxDepth: 3), which acts as the outer
recursion backstop.
thinking semantics
The optional frontmatter thinking key sets the reasoning effort used for
the dispatched child's model calls:
| Value | Meaning |
|---|---|
off / low / medium / high / max |
Reasoning effort stamped onto every model call of the child. |
| (key absent) | Nothing is injected — the child runs at the model's default effort. |
Values outside this whitelist are not clamped: the agent file is marked
broken at parse time (invalid \thinking`: expected one of
off/low/medium/high/max, got "…") and excluded from the roster until fixed. **Upgrading note:** this whitelist check is fail-loud as of this version — an invalid value (including a case mismatch such as High) drops the whole agent file from the roster, so check your existing .md` files before
upgrading.
Mechanically the effort travels out of band: while a run is live the child
session id is mapped to the declared level, and an agent/request waterfall
listener stamps that reasoningEffort onto every model-call config proposed
for the child — identically across the fresh-dispatch, resume, and fallback
branches. Only registry-dispatched children are injected; all other agents
pass through untouched.
Scope: the injection rides this plugin's in-process agent/request waterfall,
so it only applies to children that actually run through an in-process
spawn/fork provider. Children of remote providers (codex, claude-code,acp, …) never enter this process's agent loop; for them the declaredthinking level has no effect and no error is raised.
Known residual risk: on a fresh dispatch there is a theoretical race
window — the child session id is registered only after start() returns,
while the child's first turn may begin running synchronously — so with very
low probability the first model request goes out at the model's default
effort and later requests self-heal onto the declared level. A root fix
requires upstream first-class reasoning-effort support in the start request.
Adapter support for medium depends on the route: llm-deepseek routing
advertises only off / low / high / max, so a declared medium fails
loudly with an unsupported reasoning-effort error before any network I/O;
pi-ai-style routes accept whatever efforts the model catalog advertises for
the selected model.
Example:
---
name: workhorse
display_name: 牛马狗
description: "牛马狗:干活的主力……"
model: opencode-go/deepseek-v4-flash
thinking: medium
deep: 0
---
You are 牛马狗,干活的主力。……(这里写完整的 system prompt)
Default agent roster
The author's personal ~/.dsh/agents/ ships with three agents: workhorse
(牛马狗, the general workhorse), oldfox (老法师, the review/audit oracle),
and rubber-duck (小黄鸭). rubber-duck is the multimodal visual
agent: it reads screenshots / charts / OCR text and draws plotext / mermaid /
matplotlib figures, running on an image-capable model. It now occupies the role
formerly filled by the removed ArtyDuck (艺术鸭) in this setup.
Usage example
A multimodal example — ~/.dsh/agents/rubber-duck.md:
---
name: rubber-duck
display_name: 小黄鸭
description: "小黄鸭:多模态视觉 agent,看图识别、OCR、画 plotext/mermaid 图……"
model: digitalvolvo/kimi-k2.7-code
thinking: max
extensions: ["*"]
---
你是「小黄鸭」——多模态视觉 agent。……(这里写完整的 system prompt)
Then ask in a conversation:
用 rubber-duck 看一下这个浏览器截图,描述页面状态并提取文字
The main model calls use_agent(agent: "rubber-duck", prompt: "…"), the child
runs with the file body as its persona and an image-capable model, reads the
screenshot, and its result comes back into the conversation.
A text-only example — ~/.dsh/agents/workhorse.md:
---
name: workhorse
display_name: 牛马狗
description: "牛马狗:干活的主力……"
model: opencode-go/deepseek-v4-flash
deep: 0
---
You are 牛马狗,干活的主力。……(这里写完整的 system prompt)
Then ask in a conversation:
用 workhorse 把今天的发布清单整理成表格
The main model calls use_agent(agent: "workhorse", prompt: "…"), the child
runs with the file body as its persona, and its result comes back into the
conversation.
Known limitations
deepis a per-agent relative budget enforced at eachuse_agentcall;
it does not re-arm deeper descendants. Real recursion is additionally bounded
by every spawning tool's ownmaxDepth(native subagent tools default tomaxDepth: 3) as the outer backstop.- Continuable / background follow-up conversations with a custom agent
(send_message-style resumption) are v2; today everyuse_agentrun is
one-shot (a resumed run is still one continuation turn on the old session). - Resume matches by agent label within the same parent conversation: if you
re-dispatch the same agent for a different task after an interruption,
passfresh: true(or the resumed agent will continue the old task). - Resume requires the deployment's session persistence (the JSONL/SQLite
session store every standard dsh profile mounts); without it the tool
silently dispatches fresh. - Resume candidates are matched by creation label within the parent
conversation. This plugin labels its childrendisplay_name ?? agent name;
a one-shot child started by another tool (e.g. the nativesubagenttool)
with the same label in the same conversation is indistinguishable and would
be resumed under this plugin's persona. tools.restrict()validates the deny list against globally registered tool
names and throws on unknown names — the default list only names tools the
stock dsh base distribution always registers; non-stock deployments should
tuneleafDenyTools.
Publishing note (read before npm publish)
This plugin is a dsh plugin: its @deepseek-ai/* imports must resolve to
the single dsh closure instance the host provides. Never declare@deepseek-ai/* in dependencies — pnpm would install a second copy of
the cordis/dsh-session closure, breaking module identity and surfacing as
bizarre runtime errors likeCannot read properties of undefined (reading 'prepare') in
session-persistence. Keep them in peerDependencies (and devDependencies
for local typecheck/build), matching @aiwayds/dsh-tui-pi's convention.
Development
npm run check # tsc --noEmit -p tsconfig.json
npm run build # tsc -p tsconfig.json -> lib/
npm test # deep-semantics + display-name + resume (no LLM)
License
MIT
安装
装一次目录插件,之后本站所有插件都能让 DeepSeek Harness 自动找、自动装:
dsh plugin add dshbase-catalog 然后对 agent 说「帮我装 dsh-subagent-registry」,它会在目录里找到并自动安装。文档:dshbase-catalog · 已验证场景包。
该插件是 GitHub 源码(未发 npm)——直接从仓库装:
Web profile:
dsh plugin --profile web add github:fan56/dsh-subagent-registry Headless(CLI)profile:
dsh plugin --profile headless add github:fan56/dsh-subagent-registry 实测报告
验证通过:从 GitHub 源码完成 L1 安装 + L2 加载 + L3 运行(dsh 0.1.0-rc.6)。