dsh-economizer
A token-saving preset for DeepSeek Harness (dsh): tools / MCP / skills load on demand, schemas are injected at the tail of the conversation, and the tools array never changes - so the prefix cache stays valid from start to finish.
Read this in 简体中文.
Introduction
By default, dsh stuffs every tool schema (core + MCP + delegation + web) into every single request. DeepSeek's API has no server-side tool search, so those schemas are tokens you pay for on every turn even when unused; the skill catalog adds another ~9KB per turn.
What this preset does:
- keep a small set of core tools resident, plus a name-only catalog of everything else (the skill catalog lists names only, no descriptions);
- when the model needs a tool,
tool_search/tool_loadinject the full schema at the tail of the conversation, and the model calls it by name; - the tools array is fixed from boot, so the prefix cache never invalidates no matter how much you load - that's the main difference from "naive on-demand loading".
Measured on the author's machine (67 tools):
| Metric | Result |
|---|---|
| Tool schemas per request | 67 down to 26 (after loading 7), 16,443 down to 6,342 tokens |
| New-session startup | ~67% saved (the ~9KB/turn full skill catalog is gone, replaced by a tens-of-tokens name-only list in system) |
| Loading tools | 0 cache invalidations |
The more tools you have, the more you save; the mechanism is environment-agnostic. Design and experiment data are in the docs.
Installation
The repo itself is a complete preset directory - no install scripts. Two options, pick one (don't use both).
Option A: per-session opt-in (recommended)
git clone https://github.com/wings1848/dsh-economizer ~/.dsh/.agent-presets/dsh-economizer
Launch dsh (dsh web or dsh tui), and in a new session pick 省钱模式 in the Agent preset picker. The choice is remembered, so future new sessions default to it. The directory name dsh-economizer is the preset's fixed id - don't rename it.
Option B: global, one command
dsh plugin --profile web add https://github.com/wings1848/dsh-economizer
The repo declares dsh.bundle, so dsh automatically adds it to the web profile's bundle layer stack and rewrites the config - no manual editing. All web sessions get the savings.
To verify the install (optional): run await ctx.agentPresets.standingKeyFor('dsh-economizer') inside dsh; a clean return means the preset is usable.
To uninstall Option A, just remove the ~/.dsh/.agent-presets/dsh-economizer directory. See the installation doc for details.
Usage
After installing, just chat normally - nothing special to do. The model sees the tool catalog and will search, load, and call tools on its own when needed. You can also steer it manually:
tool_search("browser automation") # search deferred tools by capability; schemas injected
tool_load(["mcp__chrome__click"]) # batch-load by exact name
tool_load(["mcp__tavily-*"]) # wildcards work
tool_load(scope="mcp:chrome-devtools") # load a whole MCP server at once
skill_search("obsidian") # search skills
skill_load("obsidian") # inject skill instructions
Search supports scope prefixes: mcp:chrome, subagent:delegate, dsh:filesystem. Each search returns at most 6 tools and at most 30 accumulate in total, so loading doesn't eat back the tokens you saved.
Architecture
The whole project is two source files: src/deferred-load.mjs (the plugin) and src/matcher.mjs (the search matcher, standalone module).
dsh tool registry (all tools, e.g. 67)
| index rebuilt at boot and on tools/change
v
┌──────────────────────────────────────────────────┐
│ carried by EVERY request (the cache-stable core) │
│ tools array: CORE(11) + GUARANTEED(8) full │
│ schemas, plus name-only stubs for deferred │
│ non-MCP tools │
│ system prompt: deferred-tool catalog; MCP │
│ folded to one line per server (not in array), │
│ plus a name-only skill catalog (tens of tokens)│
└──────────────────────────────────────────────────┘
| model needs a deferred tool
v
tool_search("browser") / tool_load(scope="mcp:chrome")
| matcher scores and picks tools; schemas are
| appended to the conversation tail (agent.inject)
v
┌──────────────────────────────────────────────────┐
│ conversation tail gains one user message with the│
│ full schemas; the tools array never changes │
│ -> prefix cache stays valid; each tool injected │
│ exactly once, re-injected after compaction │
└──────────────────────────────────────────────────┘
| next turn
v
model calls the tool by name; execution as usual
In short:
- At boot the plugin reads the tool registry once and computes the resident set: CORE (11) + GUARANTEED (8, things like
exit_plan_modethat must never be deferred), plus name-only stubs for deferred non-MCP tools; MCP tools stay out of the array, folded into one catalog line per server. Registry changes trigger a rebuild - the tool list is never hardcoded. The skill catalog (names only, deterministically sorted, same source asskill_search) is computed lazily at the first assembly, per agent and with the agent scope (the skill registry is layered - an unscoped call only sees the global layer and would always come back empty), into a system constant section, so the model knows which skills exist - on a name match it runsskill_searchfor details, thenskill_loadto activate; skill changes trigger a rebuild. - When
tool_search/tool_loadfire, the matcher scores and picks tools, and the schemas are appended to the conversation tail viaagent.inject; the tools array doesn't move, the cache stays valid, and each tool is injected exactly once. - After compaction, loaded tools and skills are re-injected automatically.
- Matcher v3: CJK-aware tokenization, substring/word-form matching, field weighting, cross-namespace de-duplication. Offline recall eval 32/32 (v2 was 19/32); blind test on 21 never-tuned tools: 18/18.
Notes
- Pick one install option; using both double-registers
tool_search/skill_search. - This preset replaces the base preset's
dsh-tool-skill(it provides equivalentskill_search/skill_load); don't mount it alongside liangshen'stool-bootstrap.mjs/skill-search.mjs- that would double-filter and double-register. - Any error inside the plugin degrades to exposing all tools; a session can't be bricked. Deferred tools can always be called by name - a deliberate escape hatch.
- Host-side logic only, no platform APIs: identical behavior on Linux / macOS / Windows and across
dsh web/dsh tui; multiple sessions sharing a process don't interfere.
Development
Zero dependencies, pure ESM, node >= 18. All evals run offline; exit code 0 means pass:
npm run check # syntax check
npm test # all three eval suites (the three below)
node tests/eval-search.mjs # recall@K: tools 32/32, skills 9/9
node tests/eval-generalization.mjs # generalization blind test 18/18 + compat regression 32/32
node tests/eval-cache-stable.mjs # cache-stability gate: determinism + keep-set + tools fingerprint + skill name-only catalog
npm run test:compaction # compaction re-injection gate
Read the ground rules in AGENTS.md before changing code. The three big ones: cache stability is the bottom line (never stuff schemas into the tools array); the deferred-tool list is never hardcoded; the matcher must not overfit the eval set - after changing matching rules, run the generalization blind test, and roll back if generalization and compatibility both drop. Eval fixtures come from real registry dumps; when adding matching rules, add cases in tests/*.json too.
Layout:
| Path | What it is |
|---|---|
src/deferred-load.mjs |
the plugin |
src/matcher.mjs |
matcher v3 |
agent.cordis.yml / preset.yml |
preset composition and display name |
cordis.patch.yml |
patch for the bundle form |
tests/ |
offline evals and fixtures |
scripts/extract.py |
per-step tool fingerprint + usage from real session logs |
Links
- Repo: https://github.com/wings1848/dsh-economizer
- Docs (
docs/):- Design and mechanics: full design, matcher evolution, measurements
- Search matching v3: CJK / word-form / semantic layering + recall eval
- Cache-stable loading: why injection-style loading keeps the cache
- Cache-hit experiment: old vs new mechanism, design + results
- Caching and skills: internals in plain language
- Installation / Configuration / Architecture / Troubleshooting / Compatibility
- tests/README.md, scripts/README.md, CHANGELOG.md
- Contributing: CONTRIBUTING.md; security: SECURITY.md; code of conduct: CODE_OF_CONDUCT.md
Trends
Currently v0.1.0 (initial release). See CHANGELOG.md for history.
License
MIT © 2026 wings
还没有评论,来写第一条。