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_status tool, 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', 'agents'] (web server is optional)
apply(ctx, config) — the function-plugin entry point
Config — 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-limit with { scopeKey, limitTokens } → overwrites the scope's
limit
POST /reset with { 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 durable parentSession
chain, 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/stream call with a sessionId is 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
sessionId are 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
storageDir is intended for one DSH process at a time. If headless
and web run concurrently, give each profile a distinct storageDir or 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 into ledger.jsonl and scope-index.json;
- removes those events from each session log;
- creates a
.bak backup 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: tree may 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: the
llm/stream hook signature, the agent/request-error payload shape, and the
ctx.agents runtime 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