Agent Sessions
A session is a conversation with an agent seen from the runtime side: whether a turn is running, whether it paused for a person, what it has spent, which files it holds and what happened in it. client.agents.sessions starts one without sending a message, lists them, reads their state, changes their budget and deletes them.
New in 3.1.0
create, list and delete are new in @cognipeer/console-sdk 3.1.0, and responses.create({ session_id }) continues a session you started. They need a Console release that ships the sessions API; get, update, actions, events need the agent runtime phase 1 release. See the Agent Sessions guide for how the pieces fit.
When to use
- You need to upload a file before the first message — a session exists before any response does.
- You keep your own record (ticket, customer, case) and want to tag the session with it (
metadata) and find it again. - You want to cap spend per conversation.
- A client was not holding the original request (page reload, another device, a channel adapter) and needs to read the state of a conversation: running, idle, waiting for an answer.
- You need to list and clean up conversations.
If you only want a one-shot answer, use responses.create() without a session.
Quick start
import { ConsoleClient } from '@cognipeer/console-sdk';
const client = new ConsoleClient({ apiKey: process.env.COGNIPEER_API_KEY! });
// 1. Start an empty session: nothing runs and nothing is spent.
const session = await client.agents.sessions.create({
agent: 'support-bot',
title: 'Invoice question',
metadata: { ticket: 'T-1042' },
budget: { max_cost_usd: 2 },
});
// 2. Continue it by id.
const reply = await client.agents.responses.create({
model: 'support-bot',
session_id: session.id,
input: 'I have a billing question',
});
// 3. Read its state any time.
const state = await client.agents.sessions.get(session.id);
console.log(state.runtime_status, state.spent_usd);The session object
create() and get() return an AgentSession:
interface AgentSession {
id: string; // the conversation id — pass it as `session_id`
object: 'agent.session';
agent: string; // agent key
title: string | null; // the first message names a session that has none
created_at: string | null; // ISO timestamps
updated_at: string | null;
metadata: Record<string, string>; // the pairs given to create(); never read by the agent
runtime_status: 'idle' | 'running' | 'requires_action' | 'budget_paused' | (string & {});
stop_reason: 'end_turn' | 'requires_action' | 'limit' | 'cancelled' | 'error' | 'budget_reached' | (string & {}) | null;
pending_actions: PendingAction[]; // approvals / questions waiting for an answer
last_event_seq: number; // newest event in the log (0 = none); resume streams after it
budget: { max_cost_usd: number; on_reach: 'pause' | 'stop' } | null;
spent_usd: number; // priced spend so far
}| Field | Notes |
|---|---|
id | The conversation id. A response id is resp_<id>, so previous_response_id: 'resp_' + session.id names the same conversation. |
runtime_status | idle — no turn running (also a session that has not run yet). running — a turn is in progress. requires_action — paused on an approval or a question. budget_paused — stopped because the session reached its budget with on_reach: 'pause'. The type is an open string: a newer Console may add values. |
stop_reason | How the last turn ended; null before the first one. Open string. |
last_event_seq | Use it as after for events.stream() to start from "now". |
metadata | {} for a session that was not created with create({ metadata }). |
A list returns AgentSessionSummary rows instead: the same fields without pending_actions, plus message_count.
Create a session
const session = await client.agents.sessions.create({
agent: 'support-bot', // required: the key of an active agent
title: 'Invoice question', // optional, at most 200 characters
metadata: { ticket: 'T-1042' }, // optional: up to 16 string pairs, never read by the agent
budget: { max_cost_usd: 2 }, // optional spend cap; null = none
});POST /api/client/v1/agents/sessions → 201.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
agent | string | Yes | Key of an active agent of the token's project |
title | string | No | At most 200 characters. Default: the first message names the session |
metadata | Record<string, string> | No | Up to 16 pairs, keys 1–64 characters, values up to 512 characters, strings only. Returned as session.metadata; the agent never reads it |
budget | { max_cost_usd, on_reach? } | null | No | Spend cap. null = none (overriding the agent's default); omitted = the agent's default. See Budget |
runtime_context | RuntimeContext | No | Context entered once for the whole session, merged under every turn's own (downstream headers and metadata) |
options.signal | AbortSignal | No | Aborts the request |
The session belongs to the token's owner and is marked as coming from the API. The same agent and budget rules as the first turn of a Responses call apply.
Raw HTTP
curl -X POST https://console.cognipeer.com/api/client/v1/agents/sessions \
-H "Authorization: Bearer $COGNIPEER_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "agent": "support-bot", "title": "Invoice question", "metadata": { "ticket": "T-1042" }, "budget": { "max_cost_usd": 2 } }'{
"id": "66f2a1c8b0d4e5a9c3d71e04",
"object": "agent.session",
"agent": "support-bot",
"title": "Invoice question",
"created_at": "2026-10-12T09:15:00.000Z",
"updated_at": "2026-10-12T09:15:00.000Z",
"metadata": { "ticket": "T-1042" },
"runtime_status": "idle",
"stop_reason": null,
"pending_actions": [],
"last_event_seq": 0,
"budget": { "max_cost_usd": 2, "on_reach": "pause" },
"spent_usd": 0
}(Ids and timestamps in the examples are illustrative.)
Errors
| Status | errorCode | When |
|---|---|---|
| 400 | invalid_request | agent is missing, or title is not text of at most 200 characters |
| 400 | agent_not_active | The agent is not active |
| 400 | invalid_metadata | More than 16 pairs, a key outside 1–64 characters, a value over 512 characters or a non-string value |
| 400 | invalid_budget | The budget is malformed |
| 403 | budget_override_forbidden | The agent's own session budget binds API callers and this budget would raise or remove it |
| 404 | agent_not_found | No agent with that key in the token's project |
List sessions
// One page, newest created first.
const page = await client.agents.sessions.list({
agent: 'support-bot',
status: 'requires_action',
limit: 50,
});
for (const s of page.data) console.log(s.id, s.title, s.message_count);
page.has_more; // another page exists
page.next_cursor; // pass as `after` to continue by hand
const next = await page.getNextPage(); // null on the last page
// Every session, fetching pages as you go (break to stop).
for await (const s of client.agents.sessions.list({ agent: 'support-bot' })) {
console.log(s.id, s.runtime_status, s.spent_usd);
}GET /api/client/v1/agents/sessions
list() returns an AgentSessionListCall: it is both a promise for the first page and an async iterable over every session. await list() resolves to an AgentSessionPage; for await (const s of list()) walks every page lazily, one request per page.
Query
| Query | Type | Description |
|---|---|---|
agent | string | Only this agent's sessions (its key) |
status | 'idle' | 'running' | 'requires_action' | 'budget_paused' | Only sessions in this runtime state. idle includes a session that has not run yet |
limit | number | Page size, 1 to 100 (default 20). Anything else is 400 invalid_limit |
after | string | The next_cursor of the previous page. Not one the Console issued: 400 invalid_cursor |
The second argument, { signal }, applies to every page request.
AgentSessionPage
| Member | Description |
|---|---|
data | AgentSessionSummary[] — the sessions of this page |
has_more | Another page exists after this one |
next_cursor | Pass as after for the next page; null on the last page |
hasNextPage() | Whether getNextPage() has a page to fetch |
getNextPage() | The page after this one (same filters), or null on the last page |
[Symbol.asyncIterator]() | Every session of this page, then of the pages after it |
toJSON() | The Console's list body (object, data, has_more, next_cursor) |
A row is an AgentSessionSummary: an AgentSession without pending_actions, plus message_count. Open one session with get() to see its pending actions.
The cursor marks the last row you read, so sessions created while you page never shift a row into or out of a page you already have. A session list shows what the caller may see: sessions of other people only when the Console's permissions allow it.
spent_usd in a list row
Typed as a number. A list does not load transcripts, so for a session that last ran before the Console kept a running total the Console may report null (unknown, not zero). Open the session with get() for the figure, and guard with s.spent_usd ?? 'n/a' when you display it.
{
"object": "list",
"data": [
{
"id": "66f2a1c8b0d4e5a9c3d71e04",
"object": "agent.session",
"agent": "support-bot",
"title": "Invoice question",
"created_at": "2026-10-12T09:15:00.000Z",
"updated_at": "2026-10-12T09:17:42.000Z",
"metadata": { "ticket": "T-1042" },
"message_count": 2,
"runtime_status": "requires_action",
"stop_reason": "requires_action",
"last_event_seq": 9,
"budget": { "max_cost_usd": 2, "on_reach": "pause" },
"spent_usd": 0.0112
}
],
"has_more": false,
"next_cursor": null
}Get a session
const state = await client.agents.sessions.get(session.id);
state.runtime_status; // 'requires_action'
state.pending_actions; // [{ id: 'act_…', kind: 'tool_approval', tool_name: 'sandbox_exec', … }]
state.last_event_seq; // resume an event stream after thisGET /api/client/v1/agents/sessions/{id}. A session of another project answers 404 session_not_found, even when you know its id.
Budget
A session can carry a spend cap in USD. It is set with create({ budget }), with responses.create({ budget }) for the conversation that request creates (ignored on a continued one), or later with update().
interface AgentSessionBudget {
max_cost_usd: number;
on_reach?: 'pause' | 'stop'; // default 'pause'
}on_reach | What happens when the session reaches max_cost_usd |
|---|---|
'pause' (default) | The turn stops and the session goes budget_paused. Raise the budget with update() and it continues. |
'stop' | The turn stops and the session is left idle. |
In both cases a budget.reached event is logged, a webhook event of the same name fires, and a new message to the session is refused with 402 budget_reached (the response carries spent_usd and max_cost_usd) until the budget is raised or removed. A session.usage event with the per-turn cost (turn_cost_usd) and the running spent_usd is logged for a session that has a budget.
Update the budget
// Raise the cap of a budget_paused session; it continues as a background run.
const result = await client.agents.sessions.update(session.id, {
budget: { max_cost_usd: 10, on_reach: 'pause' },
});
if (result.object === 'agent.run') await client.agents.runs.wait(result.id);
// Remove the cap, without continuing:
await client.agents.sessions.update(session.id, { budget: null, resume: false });PATCH /api/client/v1/agents/sessions/{id}
| Name | Type | Description |
|---|---|---|
budget | AgentSessionBudget | null | Required (omitting it is 400 invalid_budget). The new budget, or null to remove it. A new budget must exceed what the session already spent |
resume | boolean | Default true. When the session is budget_paused, continue it without a new user message, as a background run. false changes the budget only |
runtime_context | RuntimeContext | Context for the continuation, when it resumes |
The return type is a union — tell the two answers apart with result.object:
| Answer | When |
|---|---|
200 AgentSession (object: 'agent.session') | The budget changed and nothing was resumed |
202 AgentRun (object: 'agent.run') | A budget_paused session continues as a queued background run — follow it with runs.wait() |
Errors: 400 invalid_budget (malformed, missing, or not above what the session already spent), 400 agent_background_disabled (a resume needs background execution; the Console refuses before changing anything), 403 budget_override_forbidden, 404 session_not_found, 409 agent_run_conflict (a run is already active).
An agent can bind API callers to its own budget
When the agent's administrator configured a session budget on the agent, API callers may lower it but cannot raise or remove it: 403 budget_override_forbidden. This applies to create, update and responses.create({ budget }).
Delete a session
await client.agents.sessions.delete(session.id);
// { id: '…', object: 'agent.session', deleted: true }DELETE /api/client/v1/agents/sessions/{id}
delete() removes the session with its files (object, converted text and record), its event log and any open approvals or questions. It is refused with 409 agent_run_conflict while a run is queued or running on the session (cancel it with runs.cancel() first), and answers 404 session_not_found for a session of another project. It cannot be undone.
Continue a session
responses.create({ session_id }) (alias conversation, a string or { id }) continues a session by id — the way previous_response_id does, for a session that has no response yet. Give one of session_id, conversation and previous_response_id, or several that agree:
| Situation | Result |
|---|---|
session_id / conversation names a different session than previous_response_id | 400 session_conflict |
session_id and conversation name two different sessions | 400 invalid_request |
| The session belongs to another agent or project | 404 session_not_found |
budget on a continued session | Ignored — use update() |
file_id input parts work with any of the three. See Agents → Multi-turn conversations.
Limits
| Limit | Value |
|---|---|
| Title | at most 200 characters |
metadata | at most 16 pairs; keys 1–64 characters; values up to 512 characters; strings only |
| Page size | 1–100, default 20 |
| Files per session, file size, retention | the agent's Configure → Files setting — see Session files |
Errors
Errors are CognipeerAPIErrors; branch on errorCode. The full table is on Agent error codes. The ones this page raises:
| Status | errorCode | Raised by |
|---|---|---|
| 400 | invalid_request, agent_not_active, invalid_metadata, invalid_budget, invalid_limit, invalid_status, invalid_cursor, agent_background_disabled | create, list, update |
| 402 | budget_reached | responses.create on a session at its budget |
| 403 | budget_override_forbidden | create, update |
| 404 | agent_not_found, session_not_found | create, get, update, delete, responses.create |
| 409 | agent_run_conflict | update, delete |
Troubleshooting
404 session_not_found for a session I just created. The token belongs to a different project than the one the session was created in, or the session was created for another agent than the one named in model. Sessions are resolved through the token's tenant and project.
create() says agent_not_active. Only agents with status active take sessions. Set the agent's status to active in the Console (client.agents.get(key).status tells you where it stands).
update() returned a run, not a session. The session was budget_paused and resume defaults to true. Pass resume: false to change the budget without continuing, or await the run with runs.wait().
update() says agent_background_disabled. A resume runs in the background, and the agent has background execution disabled. Use resume: false, then send a message yourself.
A session is stuck in requires_action. It is waiting for an approval or a question: read get(id).pending_actions and answer with actions.resolve().
list({ limit: 0 }) is rejected. limit must be a whole number from 1 to 100.
Related
- Agent Sessions guide — lifecycle and how the APIs fit together
- Session files · Approvals & questions · Session events · Webhooks
- Agents API — Responses, background runs
- Examples

