Agent: Session turns
Section titled “Agent: Session turns”A turn is one user message dispatched into an agent session. This page covers the four ways to dispatch a turn, how to cancel one (the active turn on the session, or a named turn), how to send commands to a session that may already be working (a message, an interrupt, or a stop) and how to read their receipts, how to stop every running item in the realm, how to send input into a running workflow, and how to read the durable turn receipts that survive a disconnect or a daemon restart. All routes below honor per-request realm, container, cwd, and config-dir scoping through the standard X-Hoody-* headers (also accepted as query parameters for realm).
Dispatching a turn
Section titled “Dispatching a turn”Four endpoints dispatch a turn, each with a different posture. Pick by what your caller can do, not by what feels familiar.
| Endpoint | Blocks until done | Streams events | Retry-safe with Idempotency-Key |
|---|---|---|---|
POST /api/v1/agent/sessions/{id}/messages | No (returns 202 immediately) | No (observe the session stream separately) | No |
POST /api/v1/agent/sessions/{id}/prompt:sync | Until the turn ends, a gate needs a person, or 290 s (then 503 with details.turn_running) | No | No, but repeated calls against a parked session return the same pending_gate |
POST /api/v1/agent/sessions/{id}/prompt:stream | No (opens SSE at once) | Yes (SSE starting just before dispatch) | No (replay returns 409 replay_unavailable) |
POST /api/v1/agent/sessions/{id}/turns | No (returns 202 receipt once admitted) | No (observe the session stream separately) | Yes (returns the same receipt for the same key) |
POST /api/v1/agent/sessions/{id}/messages
Section titled “POST /api/v1/agent/sessions/{id}/messages”Fire-and-observe. Dispatches a user turn and returns immediately with {job_id, session_id, turn_id}. Observe agent_done on the session stream (its turn_id matches the response) to learn the outcome. The session is single-writer: a parked gate (409 gate_parked) or a running turn (409 turn_in_flight) refuses a new dispatch. Not retry-safe; for retries, dispatch through POST /turns with an Idempotency-Key.
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: the .hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
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 (400) on routes with no container dimension. |
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. |
Idempotency-Key | header | string | No | Opaque retry key (1–255 printable ASCII, no whitespace). A retry with the same key returns the same receipt and never re-runs the turn; the same key with a different request body is 422. |
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
text | string | Yes | Required (may be empty only with attachments). The user message text for this turn: empty or whitespace-only text with no attachments is refused (400 empty_prompt) and no turn runs. |
tool_mode | string | No | Optional per-turn tool mode override: standard or orchestrator. Any other value is refused 400 invalid_tool_mode. |
dir_scope | string | No | Optional per-turn directory-access scope override: home or full. Any other value is refused 400 invalid_dir_scope. |
attachments | array | No | Optional images for this turn, as INLINE base64 bytes. At most 4 per turn, each at most 4 MiB DECODED (~5.33 MiB of base64); the request body limit applies on top and is authoritative, so several individually-legal images can still exceed it. A violation is refused with 400 BEFORE the turn is accepted, nothing is silently dropped. path is not supported here: it would name a file on the CALLER’s machine, and a path-bearing attachment is discarded outright on a container-bound session. If the active model cannot accept images the daemon reports that on the session stream. |
Responses
Section titled “Responses”{ "job_id": "job-9f2c4e1ab07d3355", "session_id": "sess-9f2c4e1ab07d3355", "turn_id": "turn-9f2c4e1ab07d3355"}{ "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. |
empty_prompt | Empty prompt | The turn has no text (empty or only whitespace) and no attachments, so there is nothing to send to the model. No turn was started. details.field is “text”. | Send the message text, or at least one image attachment. |
invalid_tool_mode | Invalid tool mode | tool_mode is not standard or orchestrator. Nothing was started. details.field is “tool_mode”. | Send standard or orchestrator, or omit tool_mode. |
invalid_dir_scope | Invalid directory scope | dir_scope is not home or full. Nothing was started. details.field is “dir_scope”. | Send home or full, or omit dir_scope. |
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 …. |
realm_not_allowed | Realm not allowed for this login | The agent is logged in with a token limited to some realms (GET /hoody/auth/status lists them in realm_ids), and this session’s realm is not one of them, or is the global scope (a session restored from before the login, or a realm the login was narrowed away from). The session can still be read; nothing was run. | Continue in a session of a realm the login serves, or log the agent in with a token that covers this session’s realm. |
{ "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": "gate_parked", "message": "a gate is parked on this session", "details": { "pending_gate": { "generation": 3, "id": "gate-9f2c4e1ab07d3355-3", "type": "confirm" } }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
gate_parked | Gate parked | A confirm or question gate is parked on this session, so a new turn is refused until it is answered. The parked gate is surfaced under details.pending_gate. | Answer the parked gate and retry; read it from details.pending_gate, from pending_gate on GET /sessions/{id}, or from the gate field of the stream frame that parked it. |
turn_in_flight | Turn in flight | A turn is already running on this session; the single serial turn slot is occupied. | Wait for the running turn to finish (observe agent_done on the session stream), then retry. |
{ "code": "payload_too_large", "message": "request body exceeds the configured size limit"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
payload_too_large | Payload too large | The request body exceeds the size limit (default 8 MiB). | Reduce the request body below the configured limit; split a large payload into smaller requests. |
{ "code": "idempotency_key_reused", "message": "this idempotency key was used for a different request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
idempotency_key_reused | Idempotency key reused | The Idempotency-Key was already used for a DIFFERENT request on this session. A key binds to one request. | Use a fresh Idempotency-Key for a different request, or resend the identical request to get the original receipt. |
{ "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": "admission_unconfirmed", "message": "the daemon did not acknowledge the turn's admission within the wait", "details": { "result_url": "/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/turns/turn-9f2c4e1ab07d3355", "turn_id": "turn-9f2c4e1ab07d3355" }}| 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. |
admission_unconfirmed | Admission unconfirmed | The turn was sent but the daemon’s admission receipt did not arrive within the wait. It may still have been accepted: details.turn_id and details.result_url name the turn to poll. | GET details.result_url; retry with the SAME Idempotency-Key only after it reports the turn was not recorded (404). |
admission_write_failed | Admission write failed | The daemon could not durably record the turn’s admission, so the turn was NOT run (fail-closed). | Retry; if it persists, check the daemon’s session store (disk and permissions). |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction “unknown”), so it runs no work in any session until it can. Nothing was run. | Retry after the restriction is readable. |
SDK usage
Section titled “SDK usage”import { 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.startTurn('sess-9f2c4e1ab07d3355', { text: 'Summarize the last 10 commits in the repo.'});curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/messages" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "text": "Summarize the last 10 commits in the repo." }'POST /api/v1/agent/sessions/{id}/prompt:sync
Section titled “POST /api/v1/agent/sessions/{id}/prompt:sync”Dispatches a turn and waits at most 290 seconds. Returns when the turn ends, or as soon as a turn parks on a confirm or question gate with 200 pending_gate (no status; the turn is still running). At the 290 s mark it returns 200 pending_gate if a gate needs a person, and otherwise 503 service_unavailable with details.turn_id and details.turn_running: true; the turn is NOT cancelled, follow it on the stream or with GET /sessions/{id}/turns/{turn_id}. Repeated prompt:sync against a parked session returns the same pending_gate (no duplicate parked turns). Pass X-Hoody-Gate-Policy: auto_approve (or ?policy=auto_approve) to adopt the headless posture and auto-answer confirm gates with approved=true (a confirm gate a tool-call rule raised is answered approved=false; question and plan gates still wait for /answer or /plan).
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The session id. |
policy | query | string | No | auto_approve adopts the headless auto-answer posture (alias of the X-Hoody-Gate-Policy header); off by default; the gateway asks for no separate credentials to use it; access is decided by the container’s proxy permission policy. |
X-Hoody-Gate-Policy | header | string | No | Confirm-gate posture: “auto_approve” adopts the headless auto-answer posture (off by default; the in:query alias is ?policy=). Any other value is rejected 400. |
X-Hoody-Cwd | header | string | No | Per-request working-directory scope: the .hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
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 (400) on routes with no container dimension. |
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. |
Idempotency-Key | header | string | No | Opaque retry key (1–255 printable ASCII, no whitespace). The turn runs at most once per key: a retry answers 200 duplicate:true with the turn’s outcome (or pending_turn while it still runs) and never re-runs it; the same key with a different request body is 422. |
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
text | string | Yes | Required (may be empty only with attachments). The user message text for this turn: empty or whitespace-only text with no attachments is refused (400 empty_prompt) and no turn runs. |
tool_mode | string | No | Optional per-turn tool mode override: standard or orchestrator. Any other value is refused 400 invalid_tool_mode. |
dir_scope | string | No | Optional per-turn directory-access scope override: home or full. Any other value is refused 400 invalid_dir_scope. |
attachments | array | No | Optional images for this turn, as INLINE base64 bytes. At most 4 per turn, each at most 4 MiB DECODED (~5.33 MiB of base64); the request body limit applies on top and is authoritative, so several individually-legal images can still exceed it. A violation is refused with 400 BEFORE the turn is accepted, nothing is silently dropped. path is not supported here: it would name a file on the CALLER’s machine, and a path-bearing attachment is discarded outright on a container-bound session. If the active model cannot accept images the daemon reports that on the session stream. |
Responses
Section titled “Responses”{ "status": "done", "session_id": "sess-9f2c4e1ab07d3355", "turn_id": "turn-9f2c4e1ab07d3355"}A parked turn returns {pending_gate} with no status. Other outcomes carry status: done, error, canceled, or quit. A 290 s timeout returns pending_gate if a gate needs a person, otherwise 503 service_unavailable with details.turn_id and details.turn_running: 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. |
empty_prompt | Empty prompt | The turn has no text (empty or only whitespace) and no attachments, so there is nothing to send to the model. No turn was started. details.field is “text”. | Send the message text, or at least one image attachment. |
invalid_tool_mode | Invalid tool mode | tool_mode is not standard or orchestrator. Nothing was started. details.field is “tool_mode”. | Send standard or orchestrator, or omit tool_mode. |
invalid_dir_scope | Invalid directory scope | dir_scope is not home or full. Nothing was started. details.field is “dir_scope”. | Send home or full, or omit dir_scope. |
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 …. |
realm_not_allowed | Realm not allowed for this login | The agent is logged in with a token limited to some realms (GET /hoody/auth/status lists them in realm_ids), and this session’s realm is not one of them, or is the global scope (a session restored from before the login, or a realm the login was narrowed away from). The session can still be read; nothing was run. | Continue in a session of a realm the login serves, or log the agent in with a token that covers this session’s realm. |
{ "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": "turn_in_flight", "message": "a turn is already running on this session"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
turn_in_flight | Turn in flight | A turn is already running on this session; the single serial turn slot is occupied, so a new turn/workflow run is refused. | Wait for the running turn to finish (observe agent_done on the session stream), then retry. |
approval_policy_active | Approval policy active | This session requires an explicit decision on every action, so an automatic-answer alias (the auto_approve gate posture, arming YOLO, an allow rule) is refused. | Answer each gate through POST /sessions/{id}/confirm, or change the policy first (PUT /sessions/{id}/approval) if it is not locked. |
{ "code": "payload_too_large", "message": "request body exceeds the configured size limit"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
payload_too_large | Payload too large | The request body exceeds the size limit (default 8 MiB). | Reduce the request body below the configured limit; split a large payload into smaller requests. |
{ "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. | 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", "details": { "turn_id": "turn-9f2c4e1ab07d3355", "turn_running": true }}A 290 s timeout: the turn is still running, not cancelled. Follow it on the session stream or with GET /sessions/{id}/turns/{turn_id}.
| 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), or the 290 s prompt-sync wait elapsed with the turn still running. | Honor Retry-After and retry; for a 290 s timeout, follow the named turn rather than retrying. |
admission_unconfirmed | Admission unconfirmed | The turn was sent but the daemon’s admission receipt did not arrive within the wait. It may still have been accepted: details.turn_id / details.result_url name the turn to poll. The gateway keeps the turn slot and job rather than inventing or discarding acceptance. | GET details.result_url; retry with the SAME Idempotency-Key only after it reports the turn was not recorded (404). |
admission_write_failed | Admission write failed | The daemon could not durably record the turn’s admission, so the turn was NOT run (fail-closed: nothing runs unrecorded). | Retry; if it persists, check the daemon’s session store (disk / permissions). |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction “unknown”), so it runs no work in any session until it can. Nothing was run. | Retry after the restriction is readable. |
{ "code": "idempotency_key_reused", "message": "this idempotency key was used for a different request", "details": { "turn_id": "turn-3f2a9c1e-12" }}| Error Code | Title | Description | Resolution |
|---|---|---|---|
idempotency_key_reused | Idempotency key reused | The Idempotency-Key was already used for a DIFFERENT request on this session. A key binds to one request; reusing it for another is a client bug. details.turn_id names the turn the key admitted (GET /sessions/{id}/turns/{turn_id} reads it). | Use a fresh Idempotency-Key for a different request, or resend the identical request to get the original receipt. |
SDK usage
Section titled “SDK usage”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 outcome = await client.agent.sessions.turns.run('sess-9f2c4e1ab07d3355', { text: 'Summarize the last 10 commits in the repo.'});curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/prompt:sync" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "text": "Summarize the last 10 commits in the repo." }'POST /api/v1/agent/sessions/{id}/prompt:stream
Section titled “POST /api/v1/agent/sessions/{id}/prompt:stream”Stream the response. Dispatches a turn and opens an SSE stream positioned at the seq captured just before dispatch, so the first frames are this turn’s rather than the retained ring’s backlog.
Pass X-Hoody-Gate-Policy: auto_approve (or ?policy=auto_approve) to auto-answer confirm gates with approved=true for the life of the stream. Off by default.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The session id. |
policy | query | string | No | auto_approve auto-answers confirm gates for the life of the stream (alias of the X-Hoody-Gate-Policy header); off by default. |
X-Hoody-Gate-Policy | header | string | No | Confirm-gate posture: auto_approve adopts the headless auto-answer posture. Any other value is rejected with 400. |
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). |
X-Hoody-Realm | header | string | No | Per-request realm selector (global or a 24-hex id). |
realm | query | string | No | Per-request realm selector (query alias of X-Hoody-Realm). |
Idempotency-Key | header | string | No | Opaque retry key (1–255 printable ASCII, no whitespace). The turn runs at most once per key: a retry is 409 replay_unavailable with the turn’s receipt (details.turn), never a re-run and never a second stream; the same key with a different request body is 422. |
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
text | string | Yes | The user message text for this turn. |
tool_mode | string | No | Optional per-turn tool mode override. |
dir_scope | string | No | Optional per-turn directory-access scope override (home or full). |
attachments | array | No | Optional inline base64 images (at most 4 per turn, each at most 4 MiB decoded). |
Responses
Section titled “Responses”Response is text/event-stream. Stop reading at the agent_done whose turn_id matches the X-Hoody-Turn-Id response header, then close the connection yourself.
event: agent_text_deltadata: {"text": "I'll look that up.\n"}
event: agent_tool_usedata: {"name": "shell", "input": {"cmd": "git log --oneline -10"}}
event: agent_text_deltadata: {"text": "Last 10 commits:\n"}
event: agent_donedata: {"turn_id": "turn-9f2c4e1ab07d3355", "state": "completed", "outcome": "completed"}{ "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. | Omit the realm header on this route, or open a session to scope by realm. |
empty_prompt | Empty prompt | The turn has no text (empty or only whitespace) and no attachments, so there is nothing to send to the model. No turn was started. details.field is “text”. | Send the message text, or at least one image attachment. |
invalid_tool_mode | Invalid tool mode | tool_mode is not standard or orchestrator. Nothing was started. details.field is “tool_mode”. | Send standard or orchestrator, or omit tool_mode. |
invalid_dir_scope | Invalid directory scope | dir_scope is not home or full. Nothing was started. details.field is “dir_scope”. | Send home or full, or omit dir_scope. |
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 …. |
realm_not_allowed | Realm not allowed for this login | The agent is logged in with a token limited to some realms (GET /hoody/auth/status lists them in realm_ids), and this session’s realm is not one of them, or is the global scope (a session restored from before the login, or a realm the login was narrowed away from). The session can still be read; nothing was run. | Continue in a session of a realm the login serves, or log the agent in with a token that covers this session’s realm. |
{ "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": "turn_in_flight", "message": "a turn is already running on this session"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
gate_parked | Gate parked | A confirm or question gate is parked on this session, so a new turn is refused until it is answered. | Answer the parked gate and retry. |
turn_in_flight | Turn in flight | A turn is already running on this session; the single serial turn slot is occupied. | Wait for the running turn to finish, then retry. |
approval_policy_active | Approval policy active | This session requires an explicit decision on every action, so auto_approve is refused. | Answer each gate, or change the policy first if it is not locked. |
replay_unavailable | Replay unavailable | The Idempotency-Key names a turn that already ran (details.turn is its receipt); prompt:stream cannot re-stream the original events and never re-runs the turn. | Read details.turn or GET details.turn.result_url for the outcome, or attach GET /sessions/{id}/stream with a cursor you hold. |
{ "code": "payload_too_large", "message": "request body exceeds the configured size limit"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
payload_too_large | Payload too large | The request body exceeds the size limit (default 8 MiB). | Reduce the request body below the configured limit; split a large payload into smaller requests. |
{ "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. | 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 | A transport error occurred before SSE opened. | Retry; if persistent, inspect the daemon logs. |
{ "code": "admission_unconfirmed", "message": "the daemon did not acknowledge the turn's admission within the wait", "details": { "result_url": "/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/turns/turn-9f2c4e1ab07d3355", "turn_id": "turn-9f2c4e1ab07d3355" }}| 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. |
admission_unconfirmed | Admission unconfirmed | The turn was sent but the daemon’s admission receipt did not arrive within the wait. It may still have been accepted. | GET details.result_url; retry with the SAME Idempotency-Key only after it reports the turn was not recorded (404). |
admission_write_failed | Admission write failed | The daemon could not durably record the turn’s admission, so the turn was NOT run (fail-closed). | Retry; if it persists, check the daemon’s session store (disk and permissions). |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction “unknown”), so it runs no work in any session until it can. Nothing was run. | Retry after the restriction is readable. |
| Error Code | Title | Description | Resolution |
|---|---|---|---|
idempotency_key_reused | Idempotency key reused | The Idempotency-Key was already used for a DIFFERENT request on this session. A key binds to one request; reusing it for another is a client bug. details.turn_id names the turn the key admitted (GET /sessions/{id}/turns/{turn_id} reads it). | Use a fresh Idempotency-Key for a different request, or resend the identical request to get the original receipt. |
SDK usage
Section titled “SDK usage”import { 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.startTurnAndStream('sess-9f2c4e1ab07d3355', { text: 'What is in this image?', attachments: [ { type: 'image', media_type: 'image/png', data: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=' } ]}, { policy: 'auto_approve'});curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/prompt:stream?policy=auto_approve" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: text/event-stream" \ -d '{ "text": "What is in this image?", "attachments": [{ "type": "image", "media_type": "image/png", "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=" }] }'POST /api/v1/agent/sessions/{id}/turns
Section titled “POST /api/v1/agent/sessions/{id}/turns”Retry-safe dispatch. Dispatches a user turn and returns 202 with a durable receipt once the daemon has admitted it.
The session is single-writer: a parked gate (409 gate_parked) or a running turn (409 turn_in_flight) refuses a new dispatch and records nothing. If the daemon does not acknowledge within the wait the answer is 503 admission_unconfirmed carrying turn_id and result_url; the turn may still have been accepted, so poll result_url before retrying (acceptance is never invented and never rolled back). A ledger write failure is 503 admission_write_failed (fail-closed). A turn interrupted by a daemon restart replays as state interrupted with effects_may_have_occurred and is never re-run.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The session id. |
Idempotency-Key | header | string | No | Opaque retry key (1 to 255 printable ASCII, no whitespace). A retry with the same key returns the same receipt and never re-runs the turn; the same key with a different request body returns 422. |
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). |
X-Hoody-Realm | header | string | No | Per-request realm selector (global or a 24-hex id). |
realm | query | string | No | Per-request realm selector (query alias of X-Hoody-Realm). |
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
text | string | Yes | The user message text for this turn. |
tool_mode | string | No | Optional per-turn tool mode override. |
dir_scope | string | No | Optional per-turn directory-access scope override (home or full). |
attachments | array | No | Optional inline base64 images (at most 4 per turn, each at most 4 MiB decoded). |
Responses
Section titled “Responses”{ "turn_id": "turn-9f2c4e1ab07d3355", "job_id": "job-9f2c4e1ab07d3355", "state": "accepted", "duplicate": false, "accepted_at": "2025-04-12T14:32:11Z", "result_url": "/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/turns/turn-9f2c4e1ab07d3355", "stream_url": "/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/stream", "notices": []}{ "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. | Omit the realm header on this route, or open a session to scope by realm. |
empty_prompt | Empty prompt | The turn has no text (empty or only whitespace) and no attachments, so there is nothing to send to the model. No turn was started. details.field is “text”. | Send the message text, or at least one image attachment. |
invalid_tool_mode | Invalid tool mode | tool_mode is not standard or orchestrator. Nothing was started. details.field is “tool_mode”. | Send standard or orchestrator, or omit tool_mode. |
invalid_dir_scope | Invalid directory scope | dir_scope is not home or full. Nothing was started. details.field is “dir_scope”. | Send home or full, or omit dir_scope. |
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 …. |
realm_not_allowed | Realm not allowed for this login | The agent is logged in with a token limited to some realms (GET /hoody/auth/status lists them in realm_ids), and this session’s realm is not one of them, or is the global scope (a session restored from before the login, or a realm the login was narrowed away from). The session can still be read; nothing was run. | Continue in a session of a realm the login serves, or log the agent in with a token that covers this session’s realm. |
{ "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": "turn_in_flight", "message": "a turn is already running on this session"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
gate_parked | Gate parked | A confirm or question gate is parked on this session, so a new turn is refused until it is answered. | Answer the parked gate and retry. |
turn_in_flight | Turn in flight | A turn is already running on this session; the single serial turn slot is occupied. | Wait for the running turn to finish, then retry. |
{ "code": "payload_too_large", "message": "request body exceeds the configured size limit"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
payload_too_large | Payload too large | The request body exceeds the size limit (default 8 MiB). | Reduce the request body below the configured limit; split a large payload into smaller requests. |
{ "code": "idempotency_key_reused", "message": "this idempotency key was used for a different request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
idempotency_key_reused | Idempotency key reused | The Idempotency-Key was already used for a DIFFERENT request on this session. A key binds to one request. | Use a fresh Idempotency-Key for a different request, or resend the identical request to get the original receipt. |
{ "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. | 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. |
admission_unconfirmed | Admission unconfirmed | The turn was sent but the daemon’s admission receipt did not arrive within the wait. It may still have been accepted: details.turn_id / details.result_url name the turn to poll. The gateway keeps the turn slot and job rather than inventing or discarding acceptance. | GET details.result_url; retry with the SAME Idempotency-Key only after it reports the turn was not recorded (404). |
admission_write_failed | Admission write failed | The daemon could not durably record the turn’s admission, so the turn was NOT run (fail-closed: nothing runs unrecorded). | Retry; if it persists, check the daemon’s session store (disk / permissions). |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction “unknown”), so it runs no work in any session until it can. Nothing was run. | Retry after the restriction is readable. |
SDK usage
Section titled “SDK usage”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 receipt = await client.agent.sessions.turns.create('sess-9f2c4e1ab07d3355', { text: 'Summarize the last 10 commits in the repo.'}, { IdempotencyKey: '01HX5G2EXAMPLE1234567890AB'});curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/turns" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 01HX5G2EXAMPLE1234567890AB" \ -d '{ "text": "Summarize the last 10 commits in the repo." }'Cancelling a turn
Section titled “Cancelling a turn”POST /api/v1/agent/sessions/{id}/cancel
Section titled “POST /api/v1/agent/sessions/{id}/cancel”Cancels the active turn on the session (the historical “Esc” behaviour), or, when a turn_id is supplied, cancels only that named turn.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The session id. |
turn_id | query | string | No | Cancel only this turn (alternative to the body field). |
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). |
X-Hoody-Realm | header | string | No | Per-request realm selector (global or a 24-hex id). |
realm | query | string | No | Per-request realm selector (query alias of X-Hoody-Realm). |
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
turn_id | string | No | The turn to cancel, as returned when it was dispatched. Cancels only that turn. Omit (or send {}) to cancel whatever is running. |
Responses
Section titled “Responses”{ "status": "ok", "turn_id": "turn-9f2c4e1ab07d3355", "matched": true}matched is present on a turn-scoped cancel: false when the named turn no longer held the slot, in which case nothing was cancelled.
{ "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. | 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": "payload_too_large", "message": "request body exceeds the configured size limit"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
payload_too_large | Payload too large | The request body exceeds the size limit (default 8 MiB). | Reduce the request body below the configured limit; split a large payload into smaller requests. |
{ "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. | 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. |
SDK usage
Section titled “SDK usage”import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
// Cancel a specific turn by id.const result = await client.agent.sessions.turns.cancel('sess-9f2c4e1ab07d3355', { turn_id: 'turn-9f2c4e1ab07d3355'});
// Cancel whatever turn is currently running on the session.await client.agent.sessions.turns.cancel('sess-9f2c4e1ab07d3355', {});# Cancel a specific turn.curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/cancel" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "turn_id": "turn-9f2c4e1ab07d3355" }'
# Cancel whatever turn is currently running.curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/cancel" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{}'Commands: reaching a session that may already be working
Section titled “Commands: reaching a session that may already be working”Reach a live session, working or idle, with a message, an interrupt, or a stop. Returns 202 with the command’s receipt once the agent has recorded it. The Idempotency-Key header is required: a retry with the same key and the same command returns the stored receipt (with duplicate: true) and admits nothing; the same key with a different command is 422 idempotency_key_reused.
Commands reach live sessions only: a closed session is 409 session_closed, an unknown session is 404, and a headless or internal session (a TODO run) is 409 command_unsupported. A configured UserPromptSubmit hook sees each message before delivery and can refuse it (state refused, reason hook). Stream events announce delivered commands (event.command_committed) and the ones that ended otherwise (event.command_settled).
POST /api/v1/agent/sessions/{id}/commands
Section titled “POST /api/v1/agent/sessions/{id}/commands”Sends a command to a live session.
Kind message delivers text to the session at its next step. An idle session starts a turn for the message. text is required, non-empty, and at most 32 KiB (more is 400 bad_request).
Kind interrupt stops the work the session was running when the interrupt was sent, then delivers text as a new turn. Background tasks, background workflow runs, and background shells keep running (a stop ends them). The receipt says whether work was running (interrupted). text is required, non-empty, and at most 32 KiB.
Kind stop stops all of the session’s work, pauses its loops, and supersedes every message and interrupt sent before it that has not been delivered. A stop carries no text. The receipt’s stopped lists what the stop acted on and how each ended. With close: true the session is closed once its work is stopped, and later commands are 409 session_closed.
order (optional, at least 0) sequences one caller’s commands: a message or interrupt with a lower order than a stop already received is superseded at once (reason: order). from and trigger are attribution only (they change how the message is introduced to the model); the agent does not verify them. The text waiting on one session is at most 128 KiB (more is 429 command_queue_full with Retry-After).
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The session id. |
Idempotency-Key | header | string | Yes | Retry key, required on this route; the SDK and CLI send one automatically. 1–255 printable ASCII, no whitespace. A retry with the same key and command returns the stored receipt and admits nothing; the same key with a different command is 422. The key is remembered while the command waits and for 24 hours after it settled. Past 4096 remembered keys a new key is 429 idempotency_keys_exhausted; a stop is never refused because of the remembered-key limit. |
X-Hoody-Cwd | header | string | No | Per-request working-directory scope: the .hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
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 (400) on routes with no container dimension. |
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. |
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
kind | string | Yes | message, interrupt, or stop. |
text | string | No | The message text (message and interrupt: required, non-empty, at most 32 KiB). Delivered verbatim. A stop takes none. |
close | boolean | No | A stop only: close the session once its work is stopped. |
on_gate | string | No | A message only: deny (default) declines a confirmation or question the session is waiting on so the message is read now; wait leaves it waiting. |
order | integer | No | Optional, at least 0: a sequence number for one caller’s commands. A message or interrupt with a lower order than a stop already received is superseded. |
from | object | No | Who sends it (kind enum: user, bot, delegate, system; optional id of at most 256 bytes). Attribution only, never authorization. |
trigger | string | No | What caused it (enum: human, wake, clear). Recorded on the receipt. Attribution only, never authorization. |
{ "kind": "message", "text": "Also update the README when you are done."}Responses
Section titled “Responses”{ "admitted_at": "2026-10-08T14:03:11.204Z", "command_id": "cmd_3f2a9c1e5b7d40a1c2e8f6d9", "kind": "message", "state": "queued"}A receipt carries the command’s state: queued, committed, superseded, refused, or failed. A committed receipt also carries turn_id and how (next_step, new_turn, or stop). A stop receipt’s stopped lists what it acted on (kind session, task, workflow_run, bash_job, or loop) and the outcome per item (stopped, already_done, or failed).
{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters (for example, a missing Idempotency-Key, an empty text on a message or interrupt, or text larger than 32 KiB). | Correct the request body or headers. |
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 |
|---|---|---|---|
realm_not_allowed | Realm not allowed for this login | The agent is logged in with a token limited to some realms (GET /hoody/auth/status lists them in realm_ids), and this session’s realm is not one of them, or is the global scope (a session restored from before the login, or a realm the login was narrowed away from). The session can still be read; nothing was run. | Continue in a session of a realm the login serves, or log the agent in with a token that covers this session’s realm. |
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": "session_closed", "message": "the session is being closed"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
session_closed | Session closed | The session is closed (not live), or a stop sent with close is closing it. Nothing was admitted. | Attach the session with POST /sessions (attach) and send the command again, or start another session. |
command_unsupported | Commands unsupported | This session does not take commands: a headless run or an internal session (a TODO run). Nothing was admitted. | Cancel it with POST /sessions/{id}/cancel instead. |
{ "code": "payload_too_large", "message": "request body exceeds the configured size limit"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
payload_too_large | Payload too large | The request body exceeds the size limit (default 8 MiB). | Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests. |
{ "code": "idempotency_key_reused", "message": "the idempotency key was used for a different command (cmd_3f2a9c1e5b7d40a1c2e8f6d9)"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
idempotency_key_reused | Idempotency key reused | The Idempotency-Key was already used for a DIFFERENT command on this session (another kind, text, close, on_gate, order, from or trigger). Nothing was admitted. The message names the command the key admitted. | Use a fresh Idempotency-Key for a different command, or resend the identical command to get its receipt. |
{ "code": "command_queue_full", "message": "too much text is queued on this session; retry after event.command_committed"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
command_queue_full | Command queue full | The text of the commands still waiting on this session would pass 128 KiB with this one. Nothing was admitted. Retry-After says when to retry. | Wait for event.command_committed (the queued commands reached the session), then send again with the same Idempotency-Key. |
idempotency_keys_exhausted | Too many remembered keys | The session already remembers 4096 Idempotency-Keys (each one while its command waits and for 24 hours after the command settled). Nothing was admitted. A stop is never refused because of the remembered-key limit. | Send the command later, when older keys have expired. |
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. |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction “unknown”), so it runs no work in any session until it can. Nothing was run. | Retry after the restriction is readable. |
admission_write_failed | Admission write failed | The agent could not record the command, so it was NOT admitted. | Retry with the same Idempotency-Key; if it persists, check the agent’s storage (disk space, permissions). |
admission_unconfirmed | Admission unconfirmed | The command was sent but the agent did not acknowledge it within the wait. It may have been admitted. | Resend the identical request with the same Idempotency-Key: if the command was admitted it answers the stored receipt (duplicate true), and nothing is admitted twice. |
SDK usage
Section titled “SDK usage”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 receipt = await client.agent.sessions.commands.send('sess-9f2c4e1ab07d3355', { kind: 'message', text: 'Also update the README when you are done.'}, { IdempotencyKey: '01HX5G2EXAMPLE1234567890AB'});
// Track a stop; the receipt's stopped tells you what it ended.const stop = await client.agent.sessions.commands.send('sess-9f2c4e1ab07d3355', { kind: 'stop', close: true}, { IdempotencyKey: '01HX5G2EXAMPLE1234567890CD'});# Send a message.curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/commands" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 01HX5G2EXAMPLE1234567890AB" \ -d '{ "kind": "message", "text": "Also update the README when you are done." }'
# Stop everything and close the session.curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/commands" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 01HX5G2EXAMPLE1234567890CD" \ -d '{ "kind": "stop", "close": true }'GET /api/v1/agent/sessions/{id}/commands/{command_id}
Section titled “GET /api/v1/agent/sessions/{id}/commands/{command_id}”Returns the receipt of a command sent to this session. state is queued until the command is delivered, then committed (with turn_id and how), superseded, refused, or failed. The session must be live: a closed session is 409 session_closed. A command id this session never admitted is 404, and so is one whose receipt is no longer kept (a receipt is kept while its command waits and for 24 hours after it settled). Cache-Control: no-store.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The session id. |
command_id | path | string | Yes | The command id. |
X-Hoody-Cwd | header | string | No | Per-request working-directory scope: the .hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; createTodo also accepts a body cwd). |
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 (400) on routes with no container dimension. |
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. |
Responses
Section titled “Responses”{ "admitted_at": "2026-10-08T14:03:11.204Z", "command_id": "cmd_3f2a9c1e5b7d40a1c2e8f6d9", "kind": "message", "state": "queued"}A committed receipt carries turn_id (the running turn when how is next_step, or the turn it started when how is new_turn; absent for a stop) and committed_at. A superseded receipt carries superseded_by (the command_id of the stop that superseded it); reason is stop (a stop sent after it) or order (a stop with a higher order). A refused receipt carries reason (hook, closed, or unsupported) and an optional detail (for example, the hook’s message). A failed receipt carries reason: write_failed. A stop receipt’s stopped lists what it acted on and how each ended; interrupted appears on an interrupt receipt (whether running work was stopped for it).
{ "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 command id is not for this session, or its receipt is no longer kept (the keep window is while the command waits and 24 hours after it settled). | Verify the path and identifier; resend the command if its keep window elapsed. |
{ "code": "session_closed", "message": "the session is being closed"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
session_closed | Session closed | The session is closed (not live), or a stop sent with close is closing it. Nothing was admitted. | Attach the session with POST /sessions (attach) and send the command again, or start another session. |
{ "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 |
|---|---|---|---|
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. |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction “unknown”), so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
SDK usage
Section titled “SDK usage”import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
// Poll the receipt returned by POST /commands.const r = await client.agent.sessions.commands.get('sess-9f2c4e1ab07d3355', 'cmd_3f2a9c1e5b7d40a1c2e8f6d9');const state = r.data.state;curl -X GET "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/commands/cmd_3f2a9c1e5b7d40a1c2e8f6d9" \ -H "Authorization: Bearer $HOODY_TOKEN"Stopping everything in the realm
Section titled “Stopping everything in the realm”POST /api/v1/agent/stop
Section titled “POST /api/v1/agent/stop”Stops, in one call, all running work in the realm the caller may see. It covers running or parked session turns (every live session, including system, resident, and headless ones; each is stopped like POST /sessions/{id}/cancel with the turn_id of the turn that was running when the call listed it, so a turn that starts later is never stopped, only reported under started_during_call), background tasks, workflow runs, todo runs (the lease is released as cancelled, as cancelTodoRun does), detached local background bash jobs, detached background bash jobs on bound containers, loops (PAUSED, never deleted: resume one with PATCH /sessions/{id}/loops/{loopId} {"paused":false}), and this gateway’s detached headless and gated tool jobs. Work it does not reach is named in not_covered.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
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. |
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
after | string | No | A next_cursor from an earlier call, unchanged (the cursor is opaque): act only on the items after it. Any other value is 400 bad_request. |
Responses
Section titled “Responses”{ "behind_cursor": [], "complete": true, "items": [ { "id": "5f0c2a91d3b44e7f", "kind": "session", "outcome": "stopped" }, { "id": "loop-3", "kind": "loop", "outcome": "stopped" } ], "not_covered": [ { "kind": "sync_request", "reason": "synchronous and streaming tool and headless requests end when their HTTP request is closed" } ], "started_during_call": []}{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters (for example, a non-opaque value in after). | 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": "payload_too_large", "message": "request body exceeds the configured size limit"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
payload_too_large | Payload too large | The request body exceeds the size limit (default 8 MiB). | Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests. |
{ "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 (GET /hoody/auth/status reports restriction “unknown”), so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
SDK usage
Section titled “SDK usage”import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
// Stop everything running in the realm (start from the first item).const result = await client.agent.stopAllWork({});
// Continue with the items behind an earlier next_cursor.const next = await client.agent.stopAllWork({ after: result.data.next_cursor });# Stop everything running in the realm.curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/stop" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{}'
# Continue with the items behind an earlier next_cursor.curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/stop" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "after": "eyJraW5kIjoic2Vzc2lvbiIsImlkIjoiNWYwYzJhOTFkM2I0NGU3ZiJ9" }'Sending input to a running workflow
Section titled “Sending input to a running workflow”POST /api/v1/agent/sessions/{id}/workflow/messages
Section titled “POST /api/v1/agent/sessions/{id}/workflow/messages”Injects user feedback or input into a running workflow on the session. The message is forwarded to the live session’s workflowMsgChan (not commandChan), so the running workflow receives it as input without being treated as a slash command. Use this only when a workflow is running on the session.
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). |
X-Hoody-Realm | header | string | No | Per-request realm selector (global or a 24-hex id). |
realm | query | string | No | Per-request realm selector (query alias of X-Hoody-Realm). |
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
text | string | No | Feedback or input text fed to the running workflow. |
Responses
Section titled “Responses”{ "status": "ok"}When a gate is parked, the response carries deferred: true and a note explaining that the message may not have taken effect yet (it is applied only between turns).
{ "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. | 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 …. |
realm_not_allowed | Realm not allowed for this login | The agent is logged in with a token limited to some realms (GET /hoody/auth/status lists them in realm_ids), and this session’s realm is not one of them, or is the global scope (a session restored from before the login, or a realm the login was narrowed away from). The session can still be read; nothing was run. | Continue in a session of a realm the login serves, or log the agent in with a token that covers this session’s realm. |
{ "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": "payload_too_large", "message": "request body exceeds the configured size limit"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
payload_too_large | Payload too large | The request body exceeds the size limit (default 8 MiB). | Reduce the request body below the configured limit; split a large payload into smaller requests. |
{ "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. | 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. |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction “unknown”), so it runs no work in any session until it can. Nothing was run. | Retry after the restriction is readable. |
admission_unconfirmed | Admission unconfirmed | The message was sent but the daemon’s answer did not arrive within the wait. It may still be delivered to the running workflow. | Watch the session stream before resending: a resent message replaces an undelivered one (the workflow keeps only the latest). |
| Error Code | Title | Description | Resolution |
|---|---|---|---|
no_active_workflow | No active workflow | No workflow is running in this session, so there is nothing to feed the message to. It was not delivered, and it is not kept for a workflow started later. | Start a workflow (POST /sessions/{id}/workflow) and send the message while it runs. |
SDK usage
Section titled “SDK usage”import { 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.workflows.sendMessage('sess-9f2c4e1ab07d3355', { text: 'Use the staged branch for the next attempt.'});curl -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/workflow/messages" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "text": "Use the staged branch for the next attempt." }'Reading turn receipts
Section titled “Reading turn receipts”GET /api/v1/agent/sessions/{id}/turns
Section titled “GET /api/v1/agent/sessions/{id}/turns”Lists the durable receipts of the session’s turns, newest first. Each receipt is the same record returned by GET /sessions/{id}/turns/{turn_id}. This is how a client that lost its turn ids after a disconnect finds them again. Works for a live or dormant session. Pass ?limit to cap the count (1 to 1000; the default is all retained rows). Keyed and client-identified rows are never pruned. Responses carry Cache-Control: no-store.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The session id. |
limit | query | integer | No | Return at most this many receipts, newest first (1 to 1000). A cap, not a page size: there is no next page. |
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). |
X-Hoody-Realm | header | string | No | Per-request realm selector (global or a 24-hex id). |
realm | query | string | No | Per-request realm selector (query alias of X-Hoody-Realm). |
Responses
Section titled “Responses”{ "session_id": "sess-9f2c4e1ab07d3355", "turns": [ { "turn_id": "turn-9f2c4e1ab07d3355", "job_id": "job-9f2c4e1ab07d3355", "state": "completed", "duplicate": false, "outcome": "completed", "effects_may_have_occurred": false, "accepted_at": "2025-04-12T14:32:11Z", "terminal_at": "2025-04-12T14:33:02Z", "result_url": "/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/turns/turn-9f2c4e1ab07d3355", "stream_url": "/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/stream", "notices": [] }, { "turn_id": "turn-3a2f1e0d9c8b7a6b", "job_id": "job-3a2f1e0d9c8b7a6b", "state": "failed", "duplicate": false, "outcome": "failed", "error_code": "no_credentials", "effects_may_have_occurred": false, "accepted_at": "2025-04-12T14:18:47Z", "terminal_at": "2025-04-12T14:19:01Z", "result_url": "/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/turns/turn-3a2f1e0d9c8b7a6b", "stream_url": "/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/stream", "notices": [] } ]}{ "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. | 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. | 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 (GET /hoody/auth/status reports restriction “unknown”), so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
SDK usage
Section titled “SDK usage”import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
// Most recent 50 receipts.const recent = await client.agent.sessions.turns.list('sess-9f2c4e1ab07d3355', { limit: 50 });const turns = recent.data.turns;
// All retained receipts.const all = await client.agent.sessions.turns.list('sess-9f2c4e1ab07d3355');curl -X GET "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/turns?limit=50" \ -H "Authorization: Bearer $HOODY_TOKEN"GET /api/v1/agent/sessions/{id}/turns/{turn_id}
Section titled “GET /api/v1/agent/sessions/{id}/turns/{turn_id}”Returns the durable record of a turn: state, terminal outcome, error_code once terminal, and effects_may_have_occurred for a turn interrupted by a restart after a tool may have acted. Works for a live or dormant session. A turn the ledger never recorded returns 404 turn_not_found. Responses carry Cache-Control: no-store.
The state field is one of accepted, dispatched, completed, failed, cancelled, or interrupted. outcome and error_code are absent until the turn is terminal.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | The session id. |
turn_id | path | string | Yes | The turn 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). |
X-Hoody-Realm | header | string | No | Per-request realm selector (global or a 24-hex id). |
realm | query | string | No | Per-request realm selector (query alias of X-Hoody-Realm). |
Responses
Section titled “Responses”{ "turn_id": "turn-9f2c4e1ab07d3355", "job_id": "job-9f2c4e1ab07d3355", "state": "completed", "duplicate": false, "outcome": "completed", "effects_may_have_occurred": false, "accepted_at": "2025-04-12T14:32:11Z", "terminal_at": "2025-04-12T14:33:02Z", "result_url": "/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/turns/turn-9f2c4e1ab07d3355", "stream_url": "/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/stream", "notices": []}{ "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. | 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": "turn_not_found", "message": "turn not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | The requested resource does not exist. | Verify the path and identifier. |
turn_not_found | Turn not found | No ledger row for this turn id on this session. | Use the turn_id from the dispatch receipt. |
{ "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. | 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 (GET /hoody/auth/status reports restriction “unknown”), so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
SDK usage
Section titled “SDK usage”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 receipt = await client.agent.sessions.turns.get('sess-9f2c4e1ab07d3355', 'turn-9f2c4e1ab07d3355');curl -X GET "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/sessions/sess-9f2c4e1ab07d3355/turns/turn-9f2c4e1ab07d3355" \ -H "Authorization: Bearer $HOODY_TOKEN"