Session events
Every agent session keeps an append-only log of what happened in it: the messages, each tool call and its result, the requests it paused on, the cost. You can read it afterwards, or follow a turn while it runs from a client that did not start it — after a page reload, from another device, from a channel adapter.
Events are kept for 30 days (AGENT_SESSION_EVENT_RETENTION_DAYS) and go with the session when it is deleted. Writing the log is best effort: a failed write is logged and dropped, and never fails the turn it describes.
In Studio
Open a session (Sessions tab → a row) and choose Activity in the side panel. It lists, oldest first, what the session recorded:
| Entry | Written for |
|---|---|
| User message | The message that started a turn |
| Run started | session.status_running |
| Called {tool} / {tool} returned / {tool} failed | Tool calls and their results |
| Context summarized | A context compaction |
| Approval requested for {tool}, The agent asked a question, Asked the page to run {tool} | A turn paused for a person |
| Approved, Denied, Question answered, Request cancelled, Request expired, The page answered {tool} | The answer |
| Agent replied | The answer text |
| Session spend $0.42 of $2.00, Session budget reached | Spend, for a session with a budget |
| Run ended: finished / waiting for a response / stopped by a limit / cancelled / failed / budget reached | session.status_idle |
| Messages removed from the session | A truncate, regenerate or edit |
The list loads a page at a time (Load more), and a session that logs something new while the tab is open appends it instead of reloading. Refresh forces a read.
Live follow. When you open a session whose turn is still running — one a client started over the API, a background run, or a turn you began in another tab — Studio follows it: a notice reads Following a turn in progress… with Stop following. The page renders the followed turn exactly like one it ran itself: tool calls appear as they start and finish, and answer text streams in when the turn's starter streams.
Session state
GET /api/client/v1/agents/sessions/{id}{
"id": "6f1c0a2b9d3e4f5a6b7c8d9e",
"object": "agent.session",
"agent": "support-bot",
"runtime_status": "requires_action",
"stop_reason": "requires_action",
"pending_actions": [
{ "id": "act_0123456789abcdef01234567", "kind": "tool_approval", "tool_name": "sandbox_exec", "status": "pending" }
],
"last_event_seq": 41,
"budget": { "max_cost_usd": 2, "on_reach": "pause" },
"spent_usd": 0.4213
}runtime_status is idle, running, requires_action (waiting for an approval or an answer) or budget_paused (see budgets). stop_reason says why the last turn stopped: end_turn, requires_action, limit, cancelled, error or budget_reached. last_event_seq is the newest event number — the point to resume a stream from.
Reading events
curl "$CONSOLE_URL/api/client/v1/agents/sessions/$SESSION_ID/events?after=40&types=agent.message,session.status_idle&limit=100" \
-H "Authorization: Bearer $COGNIPEER_API_TOKEN"let after = 0;
for (;;) {
const page = await client.agents.sessions.events.list(sessionId, {
after,
types: ['agent.message', 'session.status_idle'],
limit: 100,
});
for (const event of page.events) console.log(event.seq, event.type, event.data);
after = page.last_seq;
if (!page.has_more) break;
}Returns { "object": "list", "events": [...], "has_more": false, "last_seq": 41 }. Events come oldest first; pass the last_seq of one page as after for the next.
| Query | Default | |
|---|---|---|
after | 0 | Only events with a higher seq. |
types | all | Comma-separated filter. |
limit | 100 | At most 1000. |
Each event:
{
"seq": 38,
"type": "agent.tool_result",
"data": { "tool_call_id": "call_1", "name": "sandbox_exec", "ok": true, "duration_ms": 812 },
"run_id": "run_3f9c1a2b4d5e6f708192a3b4",
"created_at": "2026-10-01T12:00:03.120Z"
}run_idis the idGET /agents/runs/{runId}and the202of a background call return, so an event joins to its run directly. Events written outside a run carry none.seqis unique and ascending within the session.datalarger than 16 KB is cut: long strings are shortened, big nested values become a JSON preview, and_truncated: trueis set.- Field names are
snake_case, like the rest of the agent endpoints. The toolargsinside an event are the tool's own and are left as they are.
Event types
| Type | data |
|---|---|
user.message | content |
session.status_running | trigger: message, resume or timeout |
agent.tool_use | tool_call_id, name, args, subagent? |
agent.tool_result | tool_call_id, name, ok, duration_ms?, error?, subagent?, source? (client: the page embedding the chat ran it) |
agent.compaction | What a context compaction did: at, messages_compressed, tokens_before, tokens_after, … |
action.required | action_id, kind (tool_approval, user_question or client_tool), tool_name? (tool_approval and client_tool), args?, questions?, allow_free_text?, timeout_seconds? (client_tool), expires_at? |
action.resolved | action_id, kind, tool_name? (client_tool), decision?, cancelled?, expired?, ok?, duration_ms?, timed_out? (client_tool), resolved_by, channel (dashboard, api, portal, timeout or system) |
agent.message | content, message_index?, stop_reason?, usage? |
session.usage | turn_cost_usd, spent_usd, budget — written each turn of a session with a budget |
budget.reached | spent_usd, max_cost_usd |
session.status_idle | stop_reason, pending_action_ids?, detail? |
session.error | message, code? |
session.work_done | none — right after an idle with stop_reason: end_turn |
session.truncated | reason (truncate, regenerate or edit), kept_messages, removed_messages, removed_from, cancelled_actions, by, channel: messages were cut from the transcript (details) |
A turn is bracketed by session.status_running and session.status_idle. A turn that fails writes session.error and then session.status_idle with stop_reason: error. A new event type can appear in a future version: ignore the ones you do not know.
Following a turn live
GET /api/client/v1/agents/sessions/{id}/events/stream?after=41
Accept: text/event-streamServer-Sent Events. The stream replays the stored events after after (or after the Last-Event-ID header a browser EventSource sends when it reconnects), then follows the session live. Each stored event is a frame:
id: 42
event: agent.tool_use
data: {"seq":42,"type":"agent.tool_use","data":{"tool_call_id":"call_2","name":"web_search","args":{"query":"…"}},"run_id":"run_…","created_at":"…"}data has the same shape as an item of the event list. types=a,b filters the stream too.
Besides the stored events, the stream carries agent.message.delta frames with the model's text as it is written:
event: agent.message.delta
data: {"type":"agent.message.delta","data":{"text":"Your refund was "},"created_at":"…"}They have no id and no seq, and they are not stored, so a client that reconnects gets the finished agent.message instead.
Deltas are a bonus, not a contract
agent.message.delta frames exist only for a turn whose starter streams. A turn started with POST /agents/responses (synchronous or background) does not stream the model, so a follower sees the finished agent.message arrive at once, without deltas. Deltas appear when the caller that starts the turn asks for a streamed answer — for example the dashboard's chat stream in Studio. Build a follower around the stored events and treat deltas as extra when they come.
A : ping comment is sent every 15 seconds. The stream ends after an hour or when you close it; reconnect with the last id to pick up where you left off. With Redis configured, the stream follows a turn that runs on another node.
curl -N "$CONSOLE_URL/api/client/v1/agents/sessions/$SESSION_ID/events/stream?after=41" \
-H "Authorization: Bearer $COGNIPEER_API_TOKEN" \
-H "Accept: text/event-stream"import { isSessionEvent } from '@cognipeer/console-sdk';
const session = await client.agents.sessions.get(sessionId);
for await (const event of client.agents.sessions.events.stream(sessionId, { after: session.last_event_seq })) {
// Live text chunks are not persisted events, so they are matched on `type` directly.
if (event.type === 'agent.message.delta') process.stdout.write(String((event.data as { text?: string }).text ?? ''));
if (isSessionEvent(event, 'action.required')) askSomeone(event.data);
if (isSessionEvent(event, 'session.status_idle')) break; // the stream never ends by itself when a session goes idle
}The SDK's stream resumes automatically after a dropped connection, an idle stall or the server's one-hour end, with Last-Event-ID set to the last seq it saw — no persisted event is lost or repeated. It gives up after maxReconnects (default 8) consecutive attempts that receive nothing, fails at once on a non-retryable status (401, 403, 404…), and throws the abort reason when you pass a signal that aborts.
Need to know when, not what?
A client that only needs to know that something happened does not have to hold a stream open. Subscribe a webhook to session.idled, action.required or budget.reached and fetch the details when it fires.
The dashboard routes
Studio reads the same log from /api/agents/{agentId}/sessions/{sessionId}/events and …/events/stream, plus GET …/actions for the session's approvals and questions. The dashboard stream's frames keep the dashboard's own field names and values (runId — the bare id without the run_ prefix — and createdAt). These routes authenticate with a browser session; scripts use the client routes above.
Troubleshooting
The stream connects but nothing arrives. Check after. A value above the session's last_event_seq replays nothing and waits for new events. Start from 0 to see the whole log, or from last_event_seq to see only what comes next.
I see no agent.message.delta frames. The turn was not started by a streaming caller; see the warning above. The finished agent.message always arrives.
An event I expected is missing from an old session. Events are kept for 30 days. A session's own transcript is not affected — only the log is trimmed.
data._truncated is true. The event's payload went over 16 KB. Fetch the full tool result from your own system or from the transcript; the log keeps the shape and a preview.
What's new
The event log, its poll and SSE endpoints, the live Activity tab and the follow mode in Studio are new in the October 2026 agent runtime release.

