wings1848 / dsh-economizer

Listed

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.

mainToolSkill View source

Installation

npx -y @deepseek-ai/dsh plugin --profile web add github:wings1848/dsh-economizer

This installation command is an unverified starting point generated from the GitHub repository address.

README

Maintainer-authored documentation snapshot.

View on GitHub ↗
Commit 0b0634bSynced Aug 18, 2026

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):

MetricResult
Tool schemas per request67 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 tools0 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:

PathWhat it is
src/deferred-load.mjsthe plugin
src/matcher.mjsmatcher v3
agent.cordis.yml / preset.ymlpreset composition and display name
cordis.patch.ymlpatch for the bundle form
tests/offline evals and fixtures
scripts/extract.pyper-step tool fingerprint + usage from real session logs

Currently v0.1.0 (initial release). See CHANGELOG.md for history.

Star History Chart

License

MIT © 2026 wings

Project files and signals

Shown items are public repository signals detected in the directory snapshot.

TestsDetected
Security policyDetected
Contributing guideDetected
DocumentationDetected

Repository information

Language
JavaScript
License
MIT
Last updated
Aug 18, 2026, 2:08 PM

Install deliberately

Review source code, permissions, lifecycle hooks, dependencies and network access. Test untrusted plugins in an isolated environment.