Agent: Session streaming
Section titled “Agent: Session streaming”Four endpoints cover a session’s event surface. They differ on whether they attach (consume the exclusive attach slot), what they return, and which transport they use. Choose based on what your client needs:
streamattaches to a live session over WebSocket or SSE and receives events as they happen. The connection consumes the exclusive attach slot.replayreturns the gateway’s buffered event tail without attaching. Use it to catch up after a reload, then resumestreamwith?since=...&incarnation=....statereturns a consistent snapshot plus the stream watermark, without attaching. Use it after a reload that kept no cursor, or to recover a parked gate’s identity.transcriptreads the persisted conversation without attaching. Use it for a non-live session, or for history older than the replay ring.
All endpoints are container-scoped. The base URL for each is https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com.
Live event stream
Section titled “Live event stream”GET /api/v1/agent/sessions/{id}/stream
Section titled “GET /api/v1/agent/sessions/{id}/stream”Attach to a session’s event stream over WebSocket (primary) or SSE. Each frame carries a gateway-stamped seq and the incarnation that stamped it. Resume from the replay ring with ?since=<seq>&incarnation=<id>. A gap past eviction emits an SSE event: lagged frame (code replay_gap) then streams from min_seq. WS clients can post inline confirm, answer, cancel, and workflow_message frames to drive a parked gate.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The session id. |
since | query | integer | No | Resume from this gateway int64 seq (also accepted as the Last-Event-ID header). |
incarnation | query | string | No | The incarnation the since cursor belongs to. When it differs from the live session’s, the cursor is treated as invalid and the full retained ring is sent. |
Last-Event-ID | header | string | No | SSE resume cursor, the gateway int64 seq to resume from. Sent automatically by an SSE client on reconnect. |
X-Hoody-Cwd | header | string | No | Per-request working-directory scope. |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves against. |
X-Hoody-Container | header | string | No | Per-request bound remote container (omitted = local). Rejected on routes with no container dimension. |
X-Hoody-Realm | header | string | No | Per-request realm selector: global or a 24-hex id. Rejected on active-only / no-realm routes. |
realm | query | string | No | In-query alias of X-Hoody-Realm, read only when the header is absent. |
curl -X GET "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/67e89abc123def456789abc1/stream?since=42&incarnation=inc-20240115103000123456" \ -H "Authorization: Bearer <token>" \ -Nimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.agent.sessions.connect('67e89abc123def456789abc1', { since: 42, incarnation: 'inc-20240115103000123456' });Response
Section titled “Response”WebSocket upgrade accepted. The response has no body. The connection switches to the WebSocket frame protocol: each frame is a JSON object of the form {"seq": <int64>, "incarnation": "<id>", "turn_id"?: "<id>", "gate"?: {"id": "<id>", "generation": <int>, "type": "confirm" | "question"}, "event": {...}}.
Event stream (SSE transport). Each SSE frame has the event: line stripped of the leading event. prefix; the data: line carries the full envelope JSON.
event: text_deltadata: {"seq":43,"incarnation":"inc-20240115103000123456","event":{"type":"event.text_delta","text":"Let me check the build output."}}
event: agent_donedata: {"seq":44,"incarnation":"inc-20240115103000123456","turn_id":"67e89abc123def456789abc2","event":{"type":"event.agent_done","turn_id":"67e89abc123def456789abc2"}}{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
realm_scope_unsupported | Realm scope unsupported | A per-request realm header was supplied to an active-only or global-no-realm RPC, which has no realm dimension to scope. | Omit the realm header on this route, or open a session to scope by realm. |
bad_request | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |
invalid_realm | Invalid realm selector | The realm selector is malformed (not ""/“global”/a 24-hex id). | Pass a valid realm selector. |
{ "code": "forbidden", "message": "request must arrive through the Hoody proxy"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
forbidden | Forbidden (Source IP Guard) | The request did not come through the program’s URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with hoody agent …. |
{ "code": "not_found", "message": "resource not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | The requested resource does not exist. | Verify the path and identifier. |
{ "code": "rate_limited", "message": "request rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Too many requests | The per-client request rate limit was exceeded; the gateway throttled the request before dispatch. | Honor the Retry-After header and retry; reduce the request rate. |
{ "code": "service_unavailable", "message": "service unavailable"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
service_unavailable | Service unavailable | The daemon could not service the request (too busy, or a per-client stream concurrency cap was hit). | Honor Retry-After and retry. |
Replay buffered events
Section titled “Replay buffered events”GET /api/v1/agent/sessions/{id}/replay
Section titled “GET /api/v1/agent/sessions/{id}/replay”Replay a live session’s buffered event tail. Returns the gateway’s own ring snapshot, not a daemon reply, with min_seq and max_seq plus the incarnation they belong to for resuming the stream. While a gate is parked, the response includes a pending_gate whose id is the addressable identity to echo on POST /sessions/{id}/confirm or /answer. For a non-live session, use GET /sessions/{id}/transcript instead.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The session id. |
X-Hoody-Cwd | header | string | No | Per-request working-directory scope. |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override. |
X-Hoody-Container | header | string | No | Per-request bound remote container (omitted = local). Rejected on routes with no container dimension. |
X-Hoody-Realm | header | string | No | Per-request realm selector: global or a 24-hex id. Rejected on active-only / no-realm routes. |
realm | query | string | No | In-query alias of X-Hoody-Realm, read only when the header is absent. |
curl -X GET "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/67e89abc123def456789abc1/replay" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const replay = await client.agent.sessions.replay('67e89abc123def456789abc1');Response
Section titled “Response”{ "session_id": "67e89abc123def456789abc1", "incarnation": "inc-20240115103000123456", "min_seq": 1, "max_seq": 42, "events": [ {"seq": 1, "incarnation": "inc-20240115103000123456", "event": {"type": "event.turn_started"}}, {"seq": 2, "incarnation": "inc-20240115103000123456", "event": {"type": "event.text_delta", "text": "I'll look at the build output."}} ], "pending_gate": { "id": "01HMW7D3Q4K5J6V7Y8Z9X0C1BV", "generation": 1, "type": "confirm", "tool_name": "bash", "gate_cause": "approval_policy", "risk": "destructive", "human_only": true }}{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |
realm_scope_unsupported | Realm scope unsupported | A per-request realm header was supplied to an active-only or global-no-realm RPC, which has no realm dimension to scope. | Omit the realm header on this route, or open a session to scope by realm. |
invalid_realm | Invalid realm selector | The realm selector is malformed (not ""/“global”/a 24-hex id). | Pass a valid realm selector. |
{ "code": "forbidden", "message": "request must arrive through the Hoody proxy"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
forbidden | Forbidden (Source IP Guard) | The request did not come through the program’s URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with hoody agent …. |
{ "code": "not_found", "message": "resource not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | The requested resource does not exist. | Verify the path and identifier. |
{ "code": "rate_limited", "message": "request rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Too many requests | The per-client request rate limit was exceeded; the gateway throttled the request before dispatch. | Honor the Retry-After header and retry; reduce the request rate. |
{ "code": "internal_error", "message": "internal server error"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
internal_error | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. |
{ "code": "service_unavailable", "message": "service unavailable"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
service_unavailable | Service unavailable | The daemon could not service the request (too busy, or a per-client stream concurrency cap was hit). | Honor Retry-After and retry. |
Recoverable state
Section titled “Recoverable state”GET /api/v1/agent/sessions/{id}/state
Section titled “GET /api/v1/agent/sessions/{id}/state”Read a session’s recoverable state without attaching. Returns one consistent snapshot: the daemon’s state object paired with this gateway’s stream watermark (incarnation and seq). The watermark is read before the daemon snapshot, so any event that lands during the read is replayed, never lost. While a gate is parked, the response includes pending_gate_ref whose id is the addressable identity to echo on POST /sessions/{id}/confirm or /answer (the daemon’s own numeric gate_id is diagnostic only and is never what those routes take). Works for a live or dormant session; a dormant one carries no incarnation or seq. Response is Cache-Control: no-store and is active-realm-scoped.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The session id. |
X-Hoody-Cwd | header | string | No | Per-request working-directory scope. |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override. |
X-Hoody-Container | header | string | No | Per-request bound remote container (omitted = local). Rejected on routes with no container dimension. |
X-Hoody-Realm | header | string | No | Per-request realm selector: global or a 24-hex id. Rejected on active-only / no-realm routes. |
realm | query | string | No | In-query alias of X-Hoody-Realm, read only when the header is absent. |
curl -X GET "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/67e89abc123def456789abc1/state" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const snapshot = await client.agent.sessions.getSnapshot('67e89abc123def456789abc1');Response
Section titled “Response”{ "session_id": "67e89abc123def456789abc1", "live": true, "epoch": "epoch-20240115103000123456", "state": "parked", "current_turn_id": "67e89abc123def456789abc2", "queued_turn_ids": [], "approval": {"policy": "ask", "session_allow": []}, "pending_gate": { "type": "confirm", "gate_id": 3, "generation": 1, "tool_name": "bash", "gate_cause": "approval_policy", "human_only": true, "lease_required": true, "params": {"command": "rm -rf build"} }, "last_detach": null, "incarnation": "inc-20240115103000123456", "seq": 42, "pending_gate_ref": { "id": "01HMW7D3Q4K5J6V7Y8Z9X0C1BV", "generation": 1, "type": "confirm", "tool_name": "bash", "gate_cause": "approval_policy", "human_only": true }}{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |
realm_scope_unsupported | Realm scope unsupported | A per-request realm header was supplied to an active-only or global-no-realm RPC, which has no realm dimension to scope. | Omit the realm header on this route, or open a session to scope by realm. |
invalid_realm | Invalid realm selector | The realm selector is malformed (not ""/“global”/a 24-hex id). | Pass a valid realm selector. |
{ "code": "forbidden", "message": "request must arrive through the Hoody proxy"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
forbidden | Forbidden (Source IP Guard) | The request did not come through the program’s URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with hoody agent …. |
{ "code": "not_found", "message": "resource not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | The requested resource does not exist. | Verify the path and identifier. |
{ "code": "rate_limited", "message": "request rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Too many requests | The per-client request rate limit was exceeded; the gateway throttled the request before dispatch. | Honor the Retry-After header and retry; reduce the request rate. |
{ "code": "internal_error", "message": "internal server error"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
internal_error | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. |
{ "code": "restriction_unknown", "message": "the login's realm restriction could not be read — retry"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve, so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
Persisted transcript
Section titled “Persisted transcript”GET /api/v1/agent/sessions/{id}/transcript
Section titled “GET /api/v1/agent/sessions/{id}/transcript”Read a session’s persisted conversation without attaching. Returns the message projection from the session record: text, thinking, tool_use, and tool_result blocks (and on an outcome_claims session, an outcome_claims block following each report_outcome tool result), plus turn boundaries. The endpoint never attaches, never resumes, and never consumes the exclusive attach slot. For a live session the content is fresh to the last persist point; the mid-turn streaming tail is served by GET /sessions/{id}/replay (live ring) and GET /sessions/{id}/stream instead. X-Hoody-Config-Dir selects the record store; X-Hoody-Cwd is not a match filter here.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The session id. |
after_turn | query | integer | No | Exclusive completed-turn skip cursor: return content strictly after completed turn N (0 = full transcript; values past the end clamp; negative or non-integer = 400). |
X-Hoody-Cwd | header | string | No | Per-request working-directory scope. |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override selecting the record store. |
X-Hoody-Realm | header | string | No | Per-request realm selector: “global” (not tied to a realm) or a 24-hex realm id (also accepted as ?realm=). On a session route it names the realm the session is looked up in: a session in another realm, or in a realm this login does not serve, is 404 not_found. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes, and for any other realm on an agent pinned to one realm. |
realm | query | string | No | Per-request realm selector — the in:query alias of the X-Hoody-Realm header (read only when the header is absent): “global” (not tied to a realm) or a 24-hex realm id. On a session route it names the realm the session is looked up in: a session in another realm, or in a realm this login does not serve, is 404 not_found. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes, and for any other realm on an agent pinned to one realm. |
curl -X GET "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/67e89abc123def456789abc1/transcript?after_turn=0" \ -H "Authorization: Bearer <token>"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const transcript = await client.agent.sessions.getTranscript('67e89abc123def456789abc1', { after_turn: 0 });Response
Section titled “Response”{ "session_id": "67e89abc123def456789abc1", "live": false, "realm": "_global", "name": "Fix the build", "model": "claude-opus-4", "cwd": "/home/user/project", "turns_total": 2, "turns_returned": 2, "after_turn": 0, "turn_boundaries": [1, 4], "messages": [ { "role": "user", "blocks": [ {"kind": "text", "text": "Please fix the build."} ] }, { "role": "assistant", "blocks": [ {"kind": "thinking", "text": "Let me check the build output first."}, {"kind": "text", "text": "I'll look at the error."}, {"kind": "tool_use", "tool_id": "toolu_01abc", "tool_name": "bash", "input": {"command": "npm run build"}}, {"kind": "tool_result", "tool_id": "toolu_01abc", "tool_name": "bash", "output": "Error: ENOENT: no such file or directory", "is_error": true}, {"kind": "text", "text": "The issue is a missing config file."} ] } ], "started_at": "2024-01-15T10:00:00Z", "last_request_at": "2024-01-15T10:05:00Z"}{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |
realm_scope_unsupported | Realm scope unsupported | A per-request realm header was supplied to an active-only or global-no-realm RPC, which has no realm dimension to scope. | Omit the realm header on this route, or open a session to scope by realm. |
invalid_realm | Invalid realm selector | The realm selector is malformed (not ""/“global”/a 24-hex id). | Pass a valid realm selector. |
{ "code": "forbidden", "message": "request must arrive through the Hoody proxy"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
forbidden | Forbidden (Source IP Guard) | The request did not come through the program’s URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with hoody agent …. |
{ "code": "not_found", "message": "resource not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | The requested resource does not exist. | Verify the path and identifier. |
{ "code": "rate_limited", "message": "request rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Too many requests | The per-client request rate limit was exceeded; the gateway throttled the request before dispatch. | Honor the Retry-After header and retry; reduce the request rate. |
{ "code": "internal_error", "message": "internal server error"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
internal_error | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. |
{ "code": "restriction_unknown", "message": "the login's realm restriction could not be read — retry"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve, so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |