DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

boom-10 /

boom-10/dsh-goal-budget

Verified

Resource budget gate for DeepSeek Harness goals: blocks a goal when its token or cost budget is exhausted, instead of only reporting spend.

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

dsh-goal-budget

A resource budget gate for DeepSeek Harness goals.

When a goal's token or cost budget runs out, this plugin blocks the goal — it does not just show you a number.

ctx.goals.block(agent, { id, revision }, {
  code: 'budget-exceeded',
  message: 'Goal exceeded its configured tokens budget: 5200 / 100',
})

Not another cost dashboard

There are already 40+ DSH plugins that report spend. Every one of them is read-only:

Plugin ⭐ What it does
@kenz1117/dsh-ui-usage-billing 95 usage billing dashboard, sidebar cost metrics
dsh-mimo-quota 0 usage and estimated-spend views from session event logs
dsh-credits 3 track provider balances, plan quotas, session costs
dsh-composer-balance 0 display account balance and estimated session cost
dsh-web-api-balance 1 shows API balance and this conversation's spend

They answer "how much did I spend?". This plugin answers "should this keep running?" — and acts on the answer.

That difference is not cosmetic. A display can be wrong and you lose nothing. An enforcement gate can be wrong and kill work that was about to succeed. Different problem, different failure modes (see Failure modes).


Why this gap, specifically

Not a guess. The harness ships an automatic continuation driver, and its own README names three things it does not do:

Round cap, not resource budget — token, currency, time, and provider quota policies remain independent. Their session events are not attributed to the goal message or mapped into goal blocker codes.

— packages/goal/goal-round-driver/README.md, Known Limitations and Deferred Work

That sentence names three missing pieces. This plugin fills two of them:

Named gap Status
session events not attributed to the goal message ✅ done here, keyed by goalId + round
not mapped into goal blocker codes ✅ done here, code: 'budget-exceeded'
wall-clock budget ⬜ not implemented yet

GoalBlockReason.code is an open string documented as "chosen by the blocking policy" — it exists so a policy plugin can name its own condition. Only two codes ship in-tree today (model-reported, round-limit). This adds budget-exceeded.


Install

dsh plugin --profile desktop add dsh-goal-budget

dsh plugin forwards to pnpm inside the profile directory, so a git reference works too — useful before an npm release:

dsh plugin --profile desktop add github:boom-10/dsh-goal-budget

When installing from a local path, pnpm links the package and warns that peerDependencies will not resolve from the target node_modules. That warning is expected here: @deepseek-ai/cordis and @deepseek-ai/schemastery are provided by the host at runtime. Use the file: protocol if it bothers you.

Then set a budget in your profile's cordis.patch.yml:

- id: dsh-goal-budget
  config:
    budget:
      tokens: 2000000      # cumulative input + output + reasoning tokens
      costUsd: 5.00        # or a spend ceiling, in USD
    onExceed: block        # or 'warn' to only log
    warnAtRatio: 0.8       # log once when 80% is used

With no budget configured the plugin loads and idles.

Pricing

Cost estimation needs unit prices. Defaults are DeepSeek's; override them if yours differ or you use a gateway:

    pricing:
      inputPerMillion: 0.15
      outputPerMillion: 0.60
      cacheReadPerMillion: 0.003   # see "Cache pricing" below

How it works

Attribution

Two facts make attribution possible, and only one event carries both.

// packages/core/session/src/types.ts:355 — spend lands here, keyed by turn
'assistant/message': { turn: number; step: number; usage?: TokenUsage; … }

// packages/core/agent/src/runtime-types.ts:328 — the ONLY event linking a message to a turn
'agent/inbox/claimed'(payload: { agent; message: UserMessage; turn: number }): void

user/message carries no turn — its payload is just a UserMessage. Sequencing alone would misread every agent.inject() context block as a new round. agent/inbox/claimed is the only event that pairs a message with the turn it opens, and message.source identifies automatic goal rounds:

source: { kind: 'goal', goalId, revision, round }   // round > 0 means an automatic round

So the chain is:

agent/inbox/claimed   source.kind === 'goal'  →  remember turn → { goalId, round }
        ↓
assistant/message     { turn, usage }         →  add to that goal's that round
        ↓
checkBudget()                                 →  over? → ctx.goals.block()

Attribution is rebuilt from the session log, never stored separately — restarting cannot double-count or reset.

Why ctx.goals.block() and not something else

It is the documented policy entry point, and it disarms continuation as part of the same call:

/**
 * Mark an active goal blocked and disarm it.
 * @param reason - policy-owned stable code and human-readable explanation.
 */
block(agent: Agent, ref: GoalRef, reason: GoalBlockReason): GoalView

Mechanisms used

Purpose Mechanism Why this one
Read spend session/event → assistant/message Durable fact; replays across restarts. agent/assistant-stream is presentation data, not a replay source.
Attribute to a goal agent/inbox/claimed The only message↔turn link that exists.
Enforce ctx.goals.block() Documented policy entry point; disarms in the same call.
Configuration @deepseek-ai/schemastery Config Deployment-varying choices must be validated config, not constants.
Service access inject = ['goals', 'sessions', 'agents'] Same set as the in-tree continuation driver.

Rejected alternatives

Using agent/turn-stopping to stop runaway turns. Wrong granularity: a budget is a goal-level fact spanning many rounds, while turn-stopping is a turn-level hook with no cross-round view. Worse, blocking there leaves the goal active, so the continuation driver immediately opens another round — a deadlock, not a stop.

Letting the model police itself via a tool. The harness already notes the weakness of self-assessment ("No independent evaluator — the model-facing goal policy decides when evidence is sufficient"). Asking the constrained party to enforce its own constraint fails exactly when the budget is exhausted and judgment is worst — and it costs another model call after the budget is already gone.

Building the 44th cost dashboard. The ecosystem has that covered, and a dashboard cannot stop anything.


Failure modes

Known, with current mitigation and what is still unsolved.

# Fails how Looks like Mitigation Still open
1 A turn mixes human and goal messages human spend billed to the goal; premature block Strict source.kind === 'goal' filter Mixed-turn spend cannot be split cleanly; a warning records when it happens
2 Unit prices differ from defaults cost estimate off; wrong block point All prices are config; docs state these are estimates Provider price changes, peak/off-peak, gateway markups are not tracked
3 State rebuilt after restart budget resets or doubles Spend is recomputed from the session log only; nothing is stored separately Session-format migrations may change field availability
4 Adapter reports no usage spend silently uncounted Marked unattributable and counted, never guessed; the block message reports the count A provider that never reports usage cannot be constrained at all
5 Several goals in one session spend billed to the wrong goal Filtered by goalId The boundary turn when goals are switched
6 Race with the continuation driver a round starts after the block block() disarms in the same call; ledgers cleared on dispose A round already accepted but not yet run (the unload race the driver's README documents)
7 A ledger is registered in one structure but not the other attribution silently stops working — no error Both structures are maintained together and asserted by tests None known
8 Startup log invisible at INFO level plugin looks unloaded Startup visibility uses console.log; alerts use logger.warn Verbosity is not yet configurable

Number 7 is the one to watch: it fails silently. So does number 8. Both were found by testing behaviour rather than checking for absence of errors — the same shape as the harness's own post-mortem 0001, where 178 green tests and 100% line coverage coexisted with a completely non-functional feature.

Cache pricing

At a 90% cache-hit ratio, ignoring cacheReadTokens overstates cost by 2.7× on a measured scenario (1000 input / 900 cached / 100 output). The error is not a fixed multiple — it grows with the hit ratio, and the unit price gap is ~50×. So cacheReadTokens is priced separately and subtracted from inputTokens to avoid double-billing.


Development

npm run typecheck   # tsc, zero external @deepseek-ai dependencies
npm test            # loader-path test; no API key needed
npm run build       # esbuild bundle → lib/index.js

Types for the harness APIs this plugin uses live in types/dsh-ambient.d.ts, each annotated with its source file and line. That file doubles as the dependency manifest — it is the complete list of harness surface this plugin touches, and it costs no install.

test/loader.spec.ts drives the real unwrapExports semantics rather than mounting the plugin by hand, because a hand-mounted test cannot validate how a plugin loads. It asserts that no export default is present: a default export makes the loader resolve to a bare apply function, discarding the sibling inject/name/Config exports, so the fiber is built with an empty inject and the plugin crashes on first service access.

License

MIT

—/ 5

No ratings yet

Verified DSH bundle

Commit 78ee84650e79

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