agentskit.js
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/adapters

The guard

cost-guard.ts
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:end event carries a usage field (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:end events 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 tiktoken count of the system prompt before sending
  • Cost-aware adapter — wrap the adapter to upgrade/downgrade model based on remaining budget
✎ Edit this page on GitHub·Found a problem? Open an issue →·How to contribute →

On this page