dsh-plugin-agent-budget
No description
- Stars
- 1
- Language
- TypeScript
- Created
- Aug 29, 2026
- Updated
- Aug 29, 2026
Introduction
dsh-plugin-agent-budget
Shared token budgets for DeepSeek Harness agent trees. A root agent, its one-shot and continuable subagents, and workflow descendants can spend from one durable budget.
Status: experimental, verified against DSH
0.1.0-rc.5. The package is not published to npm yet; it is developed as an open-source contribution to the DSH plugin ecosystem.
Highlights
- treats a whole agent tree as one budget account (
scope: tree), or each session independently (scope: session); - stores the ledger in a plugin-owned sidecar under
~/.dsh/agent-budget/, never in session logs, so uninstalling the plugin cannot break sessions; - keeps the ledger append-only, replayable, and recoverable;
- gives the model itself a read-only
budget_statustool, not just a human command; - ships a settings page panel to list scopes, adjust limits, and reset usage without touching ledger files by hand;
- has no external service dependency and can be installed as a plain bundle;
- keeps a narrow scope: durable, replayable, fail-closed token accounting for agent trees.
Install
The package is not published to npm yet. The commands below assume the package is available in your profile workspace (for example via a local checkout with
dsh plugin --profile <name> add -w ., or from a GitHub bundle once this repository is public).
Install as a bundle into a profile (recommended after publishing):
dsh plugin --profile <name> add dsh-plugin-agent-budget
Or install directly with npm/pnpm:
pnpm add dsh-plugin-agent-budget
For git installs, the package builds during install via its prepare script.
pnpm ≥10 blocks build scripts for git dependencies by default, so the first
add fails; add the exact package key pnpm printed to the profile's
pnpm-workspace.yaml and re-run:
allowBuilds:
dsh-plugin-agent-budget: true
The package declares dsh.bundle, so dsh plugin adds it to the profile's
dsh.profile.bundles automatically and --dump-config shows the
# == dsh-plugin-agent-budget layer. The layer is provided by the root
cordis.patch.yml:
- insert:
- id: agent-budget
name: dsh-plugin-agent-budget
config:
maxTokens: 200000
missingUsage: exhaust
scope: tree
maxTokens is required and must be a positive safe integer. missingUsage
defaults to exhaust; set it to ignore only when a provider intentionally
omits usage and you accept incomplete enforcement. scope defaults to tree;
set it to session for one independent budget per session. storageDir is
optional and defaults to ~/.dsh/agent-budget/.
Export shape
The plugin exports four named members and no default export:
name: 'agent-budget'inject: ['llm', 'sessions', 'tools']apply(ctx, config)— the function-plugin entry pointConfig— the loader-facing config schema
Model Experience
The model can call the read-only budget_status tool. It returns the limit,
used and remaining tokens, exhaustion state, all four usage buckets,
meteringComplete, and unmeteredCalls. It cannot change the limit.
Settings panel and HTTP API
The web client registers a Token 预算 section in Settings. It lists every
open budget scope, shows limit/used/remaining/exhausted state with a progress
bar, and polls every 30 seconds.
Host HTTP API under /agent-budget/api:
GET /scopes→{ ok, scopes: [{ scopeKey, limitTokens, usedTokens, ... }] }POST /adjust-limitwith{ scopeKey, limitTokens }→ overwrites the scope's limitPOST /resetwith{ scopeKey }→ clears usage but keeps the current limit
adjust and reset are appended to the sidecar ledger as new event lines.
Old open/sample lines are never modified, so replaying the ledger after a
reload produces the same result as incremental updates.
Semantics
- With
scope: tree(default), the plugin resolves the tree root using DSH's runtime agent ownership first, falls back to the durableparentSessionchain, and finally falls back to an independent budget with a warning when neither is available. It never merges unrelated sessions into one account. - With
scope: session, every session has an independent budget, including subagents. - Every
llm/streamcall with asessionIdis included: conversation, subagent, workflow, compaction, and title-generation calls. - Uncached input, cache reads, cache writes, and output are disjoint buckets. Reasoning tokens are already part of output and are not added again.
- The limit is captured by a scope's first open ledger record. A later plugin reload does not mutate an existing budget.
- Calls admitted concurrently may finish above the limit. Once settled usage
reaches the limit, later calls fail before provider dispatch with
TOKEN_BUDGET_EXHAUSTED. - Calls without a
sessionIdare outside any agent tree and are not metered.
Storage and uninstall
The plugin stores its ledger in:
~/.dsh/agent-budget/
ledger.jsonl append-only ledger (open/sample/unmetered)
scope-index.json sessionId -> scopeKey index
- New versions never write
budget/*events into session logs. - After uninstalling the plugin, DSH can open every session normally.
- To fully remove budget data, delete
~/.dsh/agent-budget/. - One
storageDiris intended for one DSH process at a time. Ifheadlessandwebrun concurrently, give each profile a distinctstorageDiror do not run them against the same ledger simultaneously.
Migrating legacy session logs
Versions before this sidecar design wrote budget/* events into session logs.
Run the migration once before upgrading a profile that used those versions.
Migrate them with:
node scripts/migrate-session-log.mjs
The migration tool:
- scans
~/.dsh/sessions/**/session.jsonl(.zstd); - converts
budget/*events intoledger.jsonlandscope-index.json; - removes those events from each session log;
- creates a
.bakbackup before writing.
Stop DSH processes that use the affected profile before running it.
Known Limitations
- The
agent/request-errorhandler matchesfailure.code === 'TOKEN_BUDGET_EXHAUSTED'globally and swallows any error carrying that code. Today only this plugin produces it; if another plugin ever reuses the code, this boundary needs to be tightened. scope: treemay fall back to an independent budget in extreme cold-start cases when no parent can be resolved. This prefers under-sharing over incorrectly locking unrelated chats together.- Verified against DSH
0.1.0-rc.5. When upgrading DSH, re-check: thellm/streamhook signature, theagent/request-errorpayload shape, and thectx.agentsruntime ownership API. - Concurrent admission can overshoot the limit by design (see Semantics).
- Metering is fail-closed by default: providers that intentionally omit usage
need
missingUsage: 'ignore'.
Repository layout
src/index.ts plugin implementation
tests/ unit + integration tests (deterministic mock streams)
cordis.patch.yml default bundle layer for dsh plugin profiles
scripts/ lint, test, pack verification, legacy migration
docs/design.md design decisions and compatibility notes
.github/workflows/ci.yml CI on Node.js 22 and 24
See CHANGELOG.md for release history.
Development
pnpm install
pnpm check
The test suite uses a deterministic mock stream. pnpm test:smoke additionally
runs a small real DeepSeek request when DEEPSEEK_API_KEY is present and skips
otherwise. CI covers Node.js 22 and 24.
See CONTRIBUTING.md for contribution guidelines.
License
MIT