Problem
The starter kit records token usage for every run and then ignores it. An agent can burn through as many tokens as it likes, nothing tells you why a run was allowed, and if an agent goes runaway the only lever is deleting it. The challenge brief lists this as "runaway execution or cost".
It is not a hypothetical on this platform. One Node hello-world-with-tests task cost about 94k tokens with Seed-2.0-Code.
What I added
A token budget per agent, plus a log of what the budget did.
Before a run is created, AgentService.sendMessage checks the agent's meter against its limit. If the limit is reached the API returns 429 and nothing else happens: no run, no message, no container. When a run finishes, the usage the runtime reports gets added to the meter. Every one of these steps writes an event (budget.allowed, budget.charged, budget.denied, budget.unmetered, budget.reset) with the meter and limit as they were at that moment. An operator can reset the meter through POST /api/agents/:id/budget/reset, which is refused while a run is active.
The UI got a budget field on the create/edit forms and a small panel showing the meter, the events, and a reset button. The decision itself is in budget-policy.ts, which does no I/O.
Things worth knowing about the design
The check, the event, and the run creation happen in one store transaction, the same one that marks the agent busy. Since an agent can only have one run at a time, you cannot get two runs past the check before the first one is charged.
Denials are written to the store before the 429 is thrown. My first version threw from inside the transaction, so the store rolled back and the denial vanished along with the run it rejected. Took a failing test to notice.
Codex CLI reports usage only when a turn completes. On a failed or cancelled run there is nothing to charge, so I write a budget.unmetered event instead of leaving a gap.
Old launchpad.json files get the new fields filled in on startup.
Tests
22 tests, 9 of them new: the pure policy, the full allow → charge → deny → reset sequence through the service (including checking that the denied run never reached the runner), the HTTP layer, reset-while-busy, the failure path, and the legacy database upgrade. npm run check passes.
Limitations
- The budget is checked between runs, not during one. A run admitted with 1 token left can still use 90k. Fixing that means metering at the model boundary, probably a proxy in front of the Ark endpoint. Not done.
- Failed runs are not charged because the runtime reports no usage on failure. The
budget.unmeteredevent shows the gap but does not close it. - Reset has no notion of who did it. The platform has no user identity.
- Cached input tokens are not counted. Deliberate, but a choice.
- The event list grows forever and the UI shows the last 8.
Notes
Most of the time went into deciding where the check should live, and then into the transaction boundary. Once the decision was in the control plane, the "no container started" evidence and the persisted denial both came for free.
Built With
- byteplus-modelark
- codex-cli
- docker
- fastify
- node.js
- react
- typescript
- vite
- vitest
Log in or sign up for Devpost to join the conversation.