dsh-plugin-agent-budget
Durable shared token budgets for DeepSeek Harness agent trees — a fail-closed admission gate plus a replayable usage ledger.
- Stars
- 2
- Language
- TypeScript
- Created
- Aug 29, 2026
- Updated
- Sep 22, 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.6. 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/.
Control layer (optional)
Beyond accounting, the plugin can actively slow consumption as a scope nears its limit. Every knob is opt-in except the pressure prompt:
degradeRatio(0–1): when a scope's remaining ratio drops below this threshold, each furtheragent/requestis degraded —maxTokensis clamped to the remaining budget (floor 1) and, whendegradeModelis set, the model is swapped for a cheaper one.maxOutputTokensadds a hard output cap while degraded.maxConcurrentCalls: caps simultaneous provider calls per scope; excess calls fail before dispatch withTOKEN_BUDGET_CONCURRENT_LIMIT. This also bounds the concurrent-admission overshoot described in Semantics.pressurePrompt(defaulttrue): injects a budget-pressure notice into the system prompt once usage passes 50%, escalating at 80%, so the model self-throttles before hitting the hard limit. It rides the optional@deepseek-ai/dsh-system-promptpeer; compositions without it load unchanged.
Degradation uses the agent/request waterfall because llm/stream requests
are deep-frozen by the loop and cannot be rewritten there. Controls never
write to the ledger: accounting semantics are unchanged.
Export shape
The plugin exports four named members and no default export:
name: 'agent-budget'inject: ['llm', 'sessions', 'tools', 'agents'](web server is optional)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:
The API is local-only. POST requests require Content-Type: application/json
and X-Agent-Budget-Request: 1; bodies are limited to 16 KiB. Cross-origin
requests are rejected. An active scope cannot be reset (HTTP 409).
See SECURITY.md for access and recovery instructions.
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 (including start/end call boundaries)
scope-index.json sessionId -> scopeKey index
writer.lock exclusive writer lease
- 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. - Concurrent writers are now rejected by a lock. Corrupt ledgers or indexes stop startup. Back up the sidecar before upgrading; do not downgrade data written by 0.3.0. See SECURITY.md for stale-lock recovery.
- Calls interrupted by cancellation, stream failure, or process crashes are marked unmetered if final usage is unknown, blocking dispatch by default.
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 error handler only suppresses budget errors for sessions actually denied by this plugin. Error codes should still remain unique to producers.
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.6. 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); set
maxConcurrentCallsto bound it. - 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