DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

wings1848 /

dsh-economizer

Verified

dsh-economizer — 给 DeepSeek Harness (dsh) 省 token 的 agent preset:tools/MCP/skills 按需加载、缓存稳定注入式加载、v3 检索匹配器、缓存命中率实测。On-demand tool/MCP/skill loading for DeepSeek Harness — cache-stable injection, CJK-aware matcher, measured cache-hit & token savings.

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@0b0634b9

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.

License: MIT CI tests dependencies

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_load inject 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_mode that 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 as skill_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 runs skill_search for details, then skill_load to activate; skill changes trigger a rebuild.
  • When tool_search / tool_load fire, the matcher scores and picks tools, and the schemas are appended to the conversation tail via agent.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 equivalent skill_search / skill_load); don't mount it alongside liangshen's tool-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.

Star History Chart

License

MIT © 2026 wings

—/ 5

No ratings yet

Verified DSH bundle

Commit 0b0634b96c32

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout