# Session Budgets

URL: https://cogitator.app/docs/core/budgets

> Limits of money, tokens and tool calls for a session and for each agent in it, with a warning before the limit and a pause instead of a failure when it is reached.

## Overview

A session (a thread, across all its runs) can have a budget: how much money, how many tokens and how many tool calls its runs may spend together, and how much each agent of it may spend. A run that reaches a limit does not fail. It pauses with an interrupt of kind `budget`, and a person raises the limit, lets the run go on past it, or stops it with what it has.

```typescript
const cog = new Cogitator({
  memory: { adapter: 'postgres', postgres: { connectionString } },
  budget: {
    session: { maxCostUsd: 2, maxTokens: 400_000, maxToolCalls: 200 },
    agent: { maxCostUsd: 1 },
    warnAt: 0.8,
    onWarning: (warning) =>
      notify(`${warning.scope} ${warning.metric} at ${warning.used}/${warning.limit}`),
  },
});

const researcher = new Agent({
  name: 'researcher',
  instructions: 'You research.',
  budget: { maxToolCalls: 40 }, // over budget.agent for this agent
});
```

A session is a thread: a new `threadId` starts with nothing spent, so tie the thread ids your clients may use to your users if a budget must hold per user. An agent with a `budget` of its own is held to it within the thread, its spending kept under its `id`, else its `name` (so a new instance of the same agent, on a resume or in another process, counts on), which makes unique agent names a must for agent budgets, and what an agent run through [`agentAsTool()`](/docs/tools/agent-as-tool) spends counts toward the caller's session and agent too. A paused run keeps its own budgets (`RunOptions.budget`) in its checkpoint for the resume.

Cost is the run's `usage.cost`: what the provider reported, else the tokens priced from the model registry or `llm.pricing` (see [Cogitator](/docs/core/cogitator)). Tokens are input plus output tokens. Tool calls are the calls that ran. `RunOptions.budget` sets budgets for one run over the runtime's, field by field, and `RunOptions.onBudgetWarning` hears the warnings of one run.

## Where the Spending Is Kept

What a session spent lives in the thread's metadata (`budget`, see `SESSION_BUDGET_KEY`): in all, by agent, and the limits answers raised. Without memory, or for a run with `useMemory: false`, it is kept in the process. Each run adds what it spent when it pauses, ends or fails, and `RunResult.sessionSpend` holds the total after it. Runs of one thread that go on at the same time in one process share one live count of the session, read again from storage when a run starts, so each sees what the others spend before every model call and tool call. Runs of one thread in different processes see each other's spending only once it is added, so give a session one run at a time (`multitaskStrategy: 'enqueue'`) there when the limit must hold to the cent.

## When a Limit Is Reached

Before each model call the run checks money and tokens, before each tool turn the tool calls the turn would add. A call counts once its tool starts, so a call refused or paused for an approval first does not use up the allowance, and right before it starts the allowance is checked again, with what delegates of the run spent meanwhile: a call past it answers the model that the budget is spent. What the memory upkeep after a run (extraction, compaction) spends on the model counts in the run's usage and budgets and in the session's spending. The first limit reached pauses the run:

```typescript
const result = await cog.run(researcher, { input, threadId });

if (result.status === 'paused') {
  const budget = result.interrupts?.find((entry) => entry.kind === 'budget');
  console.log(budget?.message);
  // budget.details: { scope: 'session', metric: 'cost', used: 2.04, limit: 2 }
  await cog.resume(researcher, threadId, {
    budget: { session: { maxCostUsd: 5 } }, // the host raises the limit
  });
  // or end it: responses: { [budget!.id]: { action: 'stop' } }
}
```

Without `budget.allowRaise: true` a client can only answer `{ action: 'stop' }` (the interrupt's `responseSchema` allows nothing else): raising a limit is the host's decision, made by resuming with a higher `budget` of its own (`cog.resume(agent, threadId, { budget: { session: { maxCostUsd: 5 } } })`). A budget the host passes to `resume()` wins over a limit an earlier answer raised or lifted, and changes only the limits it names: `{ session: { maxCostUsd: 5 } }` keeps the run's `maxToolCalls`. With `allowRaise: true`:

| Answer                             | What happens                                                                                                             |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `{ action: 'continue', limit: 5 }` | the limit is 5 from now on: for the session (kept in the thread) for a `session` or `agent` limit, for the run otherwise |
| `{ action: 'continue' }`           | the run goes on past this limit, the next run of the session stops at it again                                           |
| `{ action: 'stop' }`               | the run ends with `status: 'completed'`, `budgetStop` names the limit, calls of a paused tool turn get "not run" results |

An answer that does not match the interrupt's `responseSchema` fails the resume with `INTERRUPT_ANSWER_INVALID`. A resume without an answer pauses again at the same limit.

`warnAt` (default 0.8) is the share of a limit at which `onWarning` hears of it, once per run per limit.

## Run Limits

`limits.maxTokensPerRun` and the [cost routing](/docs/advanced/cost-routing) budgets (`maxCostPerRun`, `maxCostPerHour`, `maxCostPerDay`) fail a run as before (`RUN_TOKEN_LIMIT_EXCEEDED`, `BUDGET_EXCEEDED`). With `limits.onExceeded: 'interrupt'` they pause it the same way instead, with scopes `run`, `hour` and `day`: a raised limit or a `continue` holds for the rest of that run.

```typescript
const cog = new Cogitator({
  limits: { maxTokensPerRun: 200_000, onExceeded: 'interrupt' },
});
```
