Agent Sessions API
Start, list and delete agent sessions, follow their event log, answer approvals and questions, attach files, change a session's budget, and poll background runs. The Responses calls that use a session are on the Agents API page; the concepts are in the Agent runtime guide.
All endpoints live under /api/client/v1 and are authenticated with a cpeer_ API token. A token sees only its own project.
Authorization: Bearer cpeer_…Conventions
- Field names are
snake_casethroughout this API (runtime_status,max_cost_usd,required_action). Toolargsand free-formanswersare never re-keyed. {id}/{conversationId}is the session id — the bare conversation id, i.e. the part ofresp_{id}after the prefix.- Errors use the envelope
{ "error": { "type", "code", "message", … } }with extra snake_case fields where they help (pending_action_ids,spent_usd,max_cost_usd,action_status). See the error index. - A session that belongs to another project, or does not exist, is
404 session_not_found— an id never confirms that something exists elsewhere.
Endpoint map
| Endpoint | |
|---|---|
POST /agents/sessions | Start a session without a message |
GET /agents/sessions | List sessions |
GET /agents/sessions/{id} | Get a session — runtime state, pending requests, budget |
PATCH /agents/sessions/{id} | Change the budget; continues a session paused on it |
DELETE /agents/sessions/{id} | Delete a session |
GET /agents/sessions/{id}/events | Read the event log |
GET /agents/sessions/{id}/events/stream | Follow the event log (SSE) |
POST /agents/sessions/{id}/actions/{actionId} | Answer an approval or a question |
POST /agents/sessions/{id}/files | Upload a file |
GET /agents/sessions/{id}/files | List a session's files |
GET /agents/sessions/{id}/files/{fileId}/content | Read a file |
DELETE /agents/sessions/{id}/files/{fileId} | Delete a file |
GET /agents/runs/{runId} | Poll a background run |
POST /agents/runs/{runId}/cancel | Cancel a background run |
Session objects
Session
{
"id": "6f1c0a2b9d3e4f5a6b7c8d9e",
"object": "agent.session",
"agent": "support-bot",
"title": "Refund #4821",
"created_at": "2026-10-11T09:00:00.000Z",
"updated_at": "2026-10-11T09:14:03.000Z",
"metadata": { "ticket": "4821" },
"runtime_status": "requires_action",
"stop_reason": "requires_action",
"pending_actions": [
{
"id": "act_0123456789abcdef01234567",
"kind": "tool_approval",
"tool_name": "sandbox_exec",
"args": { "command": "rm -rf build" },
"status": "pending",
"expires_at": "2026-10-12T09:14:03.000Z",
"requested_at": "2026-10-11T09:14:03.000Z"
}
],
"last_event_seq": 41,
"budget": { "max_cost_usd": 2, "on_reach": "pause" },
"spent_usd": 0.4213
}| Field | Type | |
|---|---|---|
id | string | The session id. Pass it as session_id. |
agent | string | The agent key. |
title | string | null | Up to 200 characters. |
metadata | object | The string pairs given when the session was created. {} for a session not created through this API. |
runtime_status | enum | idle, running, requires_action, budget_paused. |
stop_reason | enum | null | end_turn, requires_action, limit, cancelled, error, budget_reached. null before the first turn. |
pending_actions | array | Requests still waiting for an answer — see pending action. |
last_event_seq | integer | The newest event number; resume events/stream?after= from it. |
budget | object | null | { max_cost_usd, on_reach }. |
spent_usd | number | Priced spend so far, in USD. |
A list row is a session without pending_actions and with message_count instead of a transcript; its spent_usd is null for a session that last ran before the running total was kept.
Pending action
| Field | |
|---|---|
id | act_… — pass it to the resolve call. |
kind | tool_approval, user_question or client_tool. |
tool_name, args | tool_approval and client_tool only. Never on a question. |
questions, allow_free_text | user_question only. questions[]: { question, header?, multi_select?, placeholder?, required?, options?: [{ label, value?, description? }] }. |
timeout_seconds | client_tool only. |
status | pending, resolved, expired, cancelled. |
expires_at, requested_at | ISO timestamps. expires_at is absent when the agent waits forever. |
Start a session
POST /api/client/v1/agents/sessionsCreates an empty session. Nothing runs and nothing is spent. The session belongs to the user that owns the token and is marked as coming from the API.
| Body field | Type | |
|---|---|---|
agent | string, required | The key of an active agent in the token's project. |
title | string | At most 200 characters. Default New conversation. |
metadata | object | Up to 16 string pairs, keys 1–64 characters, values up to 512. |
budget | object | null | { max_cost_usd (> 0), on_reach?: "pause" | "stop" }. null = no budget, overriding the agent's default; absent = the agent's default. |
runtime_context | object | { headers?, metadata?, … } entered once for the whole session and merged under every turn's own. This request's HTTP headers are not kept. |
curl -X POST "$CONSOLE_URL/api/client/v1/agents/sessions" \
-H "Authorization: Bearer $COGNIPEER_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "agent": "support-bot", "title": "Refund #4821", "metadata": { "ticket": "4821" }, "budget": { "max_cost_usd": 2 } }'const session = await client.agents.sessions.create({
agent: 'support-bot',
title: 'Refund #4821',
metadata: { ticket: '4821' },
budget: { max_cost_usd: 2 },
});201 with the session. Errors: 400 invalid_request (agent missing, title too long), 400 agent_not_active, 400 invalid_metadata, 400 invalid_budget, 403 budget_override_forbidden, 404 agent_not_found.
List sessions
GET /api/client/v1/agents/sessions?agent=support-bot&status=requires_action&limit=20&after=<cursor>| Query | |
|---|---|
agent | Only this agent's sessions. |
status | idle, running, requires_action, budget_paused. idle includes a session that has not run yet. |
limit | 1–100, default 20. |
after | The previous page's next_cursor. |
200: { "object": "list", "data": [ …session rows… ], "has_more": true, "next_cursor": "…" }, newest created first. 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. Errors: 400 invalid_limit, 400 invalid_status, 400 invalid_cursor.
for await (const session of client.agents.sessions.list({ status: 'requires_action' })) {
console.log(session.id, session.agent);
}Get a session
GET /api/client/v1/agents/sessions/{id}200 with the session, including pending_actions and the newest last_event_seq read from the log. 404 session_not_found. SDK: client.agents.sessions.get(id).
Update a session budget
PATCH /api/client/v1/agents/sessions/{id}| Body field | Type | |
|---|---|---|
budget | object | null, required | The new { max_cost_usd, on_reach? }, or null to remove it. A new amount must be above what the session has already spent. |
resume | boolean | Default true. When the session is budget_paused, continue it as a background run. false = change the budget only. |
runtime_context | object | Context for the continuation. |
Responses
| Status | Body | When |
|---|---|---|
200 | The session | Budget changed; nothing to continue (the session was not paused, resume is false, the turn had already finished when it crossed the budget, or the budget stops instead of pausing). |
202 | An agent run | The session was budget_paused and its turn carries on as a background run. |
Errors: 400 invalid_budget (missing or malformed budget, or not above what was spent), 400 agent_background_disabled (a continuation needs background runs allowed; nothing is changed), 403 budget_override_forbidden, 404 session_not_found, 409 agent_run_conflict. SDK: client.agents.sessions.update(id, body) — tell the two result shapes apart with result.object (agent.session or agent.run). See Session budgets.
Delete a session
DELETE /api/client/v1/agents/sessions/{id}200: { "id": "…", "object": "agent.session", "deleted": true }. Removes the session with its files (object, markdown and record), its event log and any open approvals. 404 session_not_found; 409 agent_run_conflict while a run is queued or running on it. SDK: client.agents.sessions.delete(id).
Events
List events
GET /api/client/v1/agents/sessions/{id}/events?after=40&types=agent.message,session.status_idle&limit=100| Query | Default | |
|---|---|---|
after | 0 | Only events with a higher seq. |
types | all | Comma-separated event types. |
limit | 100 | At most 1000. |
200:
{
"object": "list",
"events": [
{
"seq": 41,
"type": "session.status_idle",
"data": { "stop_reason": "end_turn" },
"run_id": "run_3f9c1a2b4d5e6f708192a3b4",
"created_at": "2026-10-01T12:00:03.120Z"
}
],
"has_more": false,
"last_seq": 41
}Oldest first. run_id is present only for events written by a run. The full list of event types and their data is in Session events. SDK: client.agents.sessions.events.list(id, { after, types, limit }).
Stream events
GET /api/client/v1/agents/sessions/{id}/events/stream?after=41&types=…
Accept: text/event-stream
Last-Event-ID: 41Server-Sent Events. Replays the stored events after after (or after Last-Event-ID), then follows the session live. Each stored event is a frame with id: <seq>, event: <type>, and the event as JSON in data:. Live agent.message.delta frames have no id and are not stored. A : ping comment is sent every 15 seconds; the stream ends after an hour. SDK: client.agents.sessions.events.stream(id, { after, signal }) (reconnects for you).
Answer an action
POST /api/client/v1/agents/sessions/{id}/actions/{actionId}
Content-Type: application/jsonBody — one of:
| Kind | Body |
|---|---|
tool_approval | { "decision": "allow" | "deny", "comment"?, "approved_args"?: { … }, "remember_for_session"?: true } |
user_question | { "answers": { "<question text>": "value" | ["v1", "v2"] } } or { "cancelled": true } |
client_tool | { "output": <any JSON, ≤ 64 KB> } or { "error": "<text>" } |
(approvedArgs and rememberForSession — the camelCase spellings early drafts used — are still accepted, but write the snake_case ones.)
Optional in every body: runtime_context, and "background": true (or the X-Cognipeer-Background: true header) to continue as a run.
Responses
| Status | Body |
|---|---|
200 | The continuation, in the Responses shape: a finished response (completed or incomplete), or requires_action again. While other requests of the same pause are still open, status: "requires_action" with an empty output and only those in required_action.actions. |
202 | An agent run — with background: true. |
Errors: 400 invalid_resolution, 400 agent_background_disabled, 403 action_forbidden, 404 action_not_found, 404 session_not_found, 409 action_not_pending (with error.action_status), 409 action_cancelled, 409 agent_run_conflict, 410 action_expired.
const next = await client.agents.sessions.actions.resolve(sessionId, actionId, { decision: 'allow' });Files
Upload a file
POST /api/client/v1/agents/sessions/{id}/files?name=contract.pdf
Content-Type: application/pdf
<raw bytes>Either the raw bytes as the body (MIME type as Content-Type, name in ?name=), or JSON { "name": "notes.txt", "content_type": "text/plain", "data": "<base64 or data URL>" } for a small file. 201 with the file:
{
"id": "6710a3f2c1d4e5f60718293a",
"object": "agent.session.file",
"session_id": "6f1c0a2b9d3e4f5a6b7c8d9e",
"name": "contract.pdf",
"mime_type": "application/pdf",
"size": 183422,
"conversion_status": "succeeded",
"bucket_key": "uploads",
"path": "agent-sessions/support-bot/6f1c0a2b9d3e4f5a6b7c8d9e/contract-1727780400000-ab12cd34.pdf",
"created_at": "2026-10-01T12:00:00.000Z",
"expires_at": "2026-10-31T12:00:00.000Z",
"content_url": "/api/client/v1/agents/sessions/6f1c0a2b9d3e4f5a6b7c8d9e/files/6710a3f2c1d4e5f60718293a/content"
}conversion_status: succeeded, pending, failed, skipped (conversion_error says why). Errors: 400 empty_file, 400 invalid_request, 403 attachments_disabled, 404 session_not_found, 409 too_many_files, 409 storage_unavailable, 413 file_too_large, 415 unsupported_file_type, 429 quota_exceeded. SDK: client.agents.sessions.files.upload(id, bytes, { name, mimeType }).
List and delete
GET …/files → { "object": "list", "data": [ …files… ] }. DELETE …/files/{fileId} → { "id", "object": "agent.session.file", "deleted": true } (404 file_not_found). SDK: files.list(id), files.delete(id, fileId).
Read a file
GET /api/client/v1/agents/sessions/{id}/files/{fileId}/content?variant=original&disposition=inline
Range: bytes=0-1023| Query | |
|---|---|
variant | original (default) or markdown — the converted text (409 markdown_unavailable when there is none). |
disposition | inline or attachment, where it is safe. SVG, HTML and similar types are always a download. |
Returns the bytes with the file's own Content-Type, a sanitised Content-Disposition, Cache-Control: private, max-age=60, X-Content-Type-Options: nosniff and Accept-Ranges: bytes. One Range is answered 206 with Content-Range; an unsatisfiable range is 416. 404 file_not_found for a file of another session. SDK: files.content(id, fileId, { variant }), files.contentStream(…).
See Session files for what the agent does with them.
Background runs
GET /api/client/v1/agents/runs/{runId}
POST /api/client/v1/agents/runs/{runId}/cancelAgent run
{
"id": "run_65f1c0a2b9d3e4f5a6b7c8d9",
"object": "agent.run",
"status": "succeeded",
"stop_reason": "end_turn",
"agent": "support-bot",
"conversation_id": "6f1c0a2b9d3e4f5a6b7c8d9e",
"result": { "id": "resp_65f1c0a2b9d3e4f5a6b7c8d9", "object": "response", "status": "completed", "…": "…" },
"error": null,
"created_at": 1760000000,
"started_at": 1760000001,
"completed_at": 1760000042,
"cancel_requested_at": null,
"max_duration_ms": 1800000,
"callback": { "url": "https://example.com/hook", "status": "delivered", "attempts": 1, "signed": true }
}status: queued, running, requires_action, succeeded, failed, canceled. Timestamps are epoch seconds. result is set once succeeded, and while requires_action; error ({ type, message }) once failed or canceled. Cancel answers 200 with the run, 404 agent_run_not_found, or 409 agent_run_already_terminal. SDK: client.agents.runs.get / cancel / wait. See Background runs.
Responses fields that use a session
The calls themselves are documented on the Agents API. The fields that matter here:
| Field | |
|---|---|
session_id / conversation / previous_response_id | Continue a session. Give one, or several that agree. |
budget | A budget for the session this request starts; ignored when it continues one. |
background, callback_url, callback_secret | Run in the background. |
input[].content[] with input_file / input_image | Attach files to the session this turn runs on. |
runtime_context | Downstream headers and prompt variables for this turn. |
Error index
| Status | Code | Where |
|---|---|---|
400 | invalid_request | Malformed body or query; session_id and conversation name different sessions. |
400 | session_conflict | session_id/conversation and previous_response_id name different sessions. |
400 | invalid_metadata, invalid_budget, invalid_limit, invalid_status, invalid_cursor | Sessions. |
400 | agent_not_active, agent_background_disabled | Starting a session; continuing in the background. |
400 | invalid_resolution | An answer that is not valid for the action; the request stays open. |
400 | attachment_not_found, attachment_unreachable, empty_file | Files. |
400 | invalid_callback, idempotency_key_sync_not_supported, idempotency_key_too_long | Background calls. |
402 | budget_reached | A message to a session whose budget is used up (spent_usd, max_cost_usd). |
403 | attachments_disabled | Files off for the agent (or input_file / input_image sent to one). |
403 | budget_override_forbidden | The agent's session budget binds API clients. |
403 | action_forbidden | Not an allowed approver. |
404 | session_not_found, agent_not_found, action_not_found, file_not_found, agent_run_not_found | The thing does not exist in the token's project. |
409 | agent_run_conflict | A run is already active on the conversation. |
409 | pending_action | A new message while the session waits for an answer (pending_action_ids). |
409 | action_not_pending, action_cancelled | The request was already answered, or cancelled (action_status). |
409 | too_many_files, storage_unavailable, markdown_unavailable | Files. |
409 | idempotency_key_conflict, agent_run_already_terminal | Background calls. |
410 | action_expired | The request ran out of time. |
413 | file_too_large | A file over the agent's limit. |
415 | unsupported_file_type | A file type the agent does not accept. |
429 | spend_limit_exceeded | The tenant's daily or monthly spend limit is used up. |
429 | agent_run_concurrency_limit, quota_exceeded | Too many background runs; file quotas. |
504 | timeout | A synchronous call passed its ceiling (side_effects_possible: true). |
SDK mapping
| Endpoint | @cognipeer/console-sdk (3.1.0+) |
|---|---|
POST /agents/sessions | client.agents.sessions.create() |
GET /agents/sessions | client.agents.sessions.list() — awaitable page, or for await over every session |
GET /agents/sessions/{id} | client.agents.sessions.get() |
PATCH /agents/sessions/{id} | client.agents.sessions.update() |
DELETE /agents/sessions/{id} | client.agents.sessions.delete() |
…/events | client.agents.sessions.events.list() |
…/events/stream | client.agents.sessions.events.stream() |
…/actions/{actionId} | client.agents.sessions.actions.resolve(); actions.list() reads pending_actions |
…/files | client.agents.sessions.files.upload() / list() / content() / contentStream() / delete() |
/agents/runs/{runId} | client.agents.runs.get() / cancel() / wait() |

