Recipes
Cost-guarded chat
A chat that aborts the run when token usage exceeds your budget — using only an observer.
A chat that tracks per-run token spend and aborts cleanly when the budget is exceeded. No special primitives — just an observer.
Install
npm install @agentskit/runtime @agentskit/adaptersThe guard
import { createRuntime } from '@agentskit/runtime'
import { openai } from '@agentskit/adapters'
import type { Observer } from '@agentskit/core'
// Pricing as of late 2025 — keep in sync with provider docs
const PRICE_PER_1K = { input: 0.0025, output: 0.01 } // gpt-4o
const BUDGET_USD = 0.10
function createCostGuard(budgetUsd: number, abort: () => void): Observer {
let inputTokens = 0
let outputTokens = 0
return {
name: 'cost-guard',
on(event) {
if (event.type === 'llm:end') {
// AgentEvent carries usage totals at the end of each LLM call
if (event.usage) {
inputTokens += event.usage.promptTokens
outputTokens += event.usage.completionTokens
}
const costUsd =
(inputTokens / 1000) * PRICE_PER_1K.input +
(outputTokens / 1000) * PRICE_PER_1K.output
if (costUsd > budgetUsd) {
console.warn(`Cost budget exceeded: $${costUsd.toFixed(4)} > $${budgetUsd}`)
abort()
}
}
if (event.type === 'agent:step') {
const total =
(inputTokens / 1000) * PRICE_PER_1K.input +
(outputTokens / 1000) * PRICE_PER_1K.output
console.log(`Step ${event.step} running cost: $${total.toFixed(4)} (${inputTokens}+${outputTokens} tokens)`)
}
},
}
}
const controller = new AbortController()
const runtime = createRuntime({
adapter: openai({ apiKey: KEY, model: 'gpt-4o' }),
observers: [createCostGuard(BUDGET_USD, () => controller.abort())],
})
try {
const result = await runtime.run('Write a 5000-word essay on quantum computing', {
signal: controller.signal,
})
console.log(result.content)
} catch (err) {
if ((err as Error).name === 'AbortError') {
console.log('Aborted due to cost budget.')
} else {
throw err
}
}Why this works
- The
llm:endevent carries ausagefield (promptTokens,completionTokens) when the adapter reports it — well-behaved adapters always do - Observers are read-only (RT9) — they can't mutate state, but they can call external APIs (like
controller.abort()) - Aborting from an observer triggers the Runtime's clean abort path: stream stops, memory does not save (RT7+RT13), promise rejects with
AbortError
Tighten the recipe
- Per-tool budget — handle
tool:start/tool:endevents in the observer and assign costs (e.g. web search at $0.005/call) - Per-user quota — store cumulative spend in Redis keyed by user id; reject before
run()if over - Pre-check budget with
tiktokencount of the system prompt before sending - Cost-aware adapter — wrap the adapter to upgrade/downgrade model based on remaining budget
Related
- Concepts: Runtime — observers, abort semantics
- Recipe: Discord bot — apply this guard per-channel