Agent: Headless
Section titled “Agent: Headless”The Headless agent endpoint drives the full agent loop once over an ephemeral, gateway-owned session. It is a one-shot entry point that supports two output forms:
- Async job (default,
format: text | json) — the gateway returns202with ajob_id; the run executes after the ack and the outcome is delivered through the Jobs API (GET /jobs/{id}andGET /jobs/{id}/result). - SSE stream (
format: stream-jsonorstream: true) — the run is streamed over Server-Sent Events; the initial200ack is followed bystart → result/error → endframes.
POST /api/v1/agent/headless/runs
Section titled “POST /api/v1/agent/headless/runs”Create a headless one-shot agent run.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Hoody-Cwd | header | string | No | Per-request working-directory scope (§6.1): the .hoody project layer / record cwd / tool+workflow cwd. |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override (§6.1) selecting which on-disk .hoody install a stateless read/write resolves. |
X-Hoody-Container | header | string | No | Per-request bound remote container (§6.1; omitted = local). Rejected (400) on routes with no container dimension. |
X-Hoody-Realm | header | string | No | Per-request realm selector (§6.1): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on routes without a realm dimension. |
realm | query | string | No | In-query alias of the X-Hoody-Realm header (read only when the header is absent). |
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
prompt | string | Yes | The prompt to drive the ephemeral session. |
workflow | string | No | Optional workflow name to run instead of (or alongside) the prompt. |
model | string | No | Optional model spec for the run. |
format | string | No | Output rendering: text | json | stream-json. stream-json (or stream: true) streams the run over SSE; otherwise the run is an async job. |
stream | boolean | No | Force SSE streaming (equivalent to format: stream-json). |
timeout_ms | integer | No | Optional run timeout in milliseconds (clamped to the hard ceiling). |
Request Example
Section titled “Request Example”{ "prompt": "Refactor src/api/handlers.ts to extract the validation logic into a separate module, and add unit tests for the new module.", "model": "claude-sonnet-4", "format": "json", "timeout_ms": 120000}Responses
Section titled “Responses”SSE stream of the headless run (format: stream-json / stream: true).
event: startdata: {"job_id": "run_01HZX7W8M3K5PBV6A2FQY9JT4C"}
event: resultdata: {"status": "ok", "text": "Refactored 3 files; created src/api/validation.ts and tests/api/validation.test.ts."}
event: enddata: {}This status has no error codes.
Run accepted as an async job (format: text | json). The run executes after this ack; its outcome — including any daemon-side failure (admin_unauthorized, timeout) — is delivered via GET /jobs/{id}/result, not as an HTTP status on this operation.
{ "job_id": "run_01HZX7W8M3K5PBV6A2FQY9JT4C"}This status has no error codes.
{ "code": "bad_request", "message": "prompt is required"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |
{ "code": "forbidden", "message": "request must arrive through the Hoody proxy"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
forbidden | Forbidden (not via the Hoody proxy) | The request did not arrive through hoody-proxy: its source IP is private/local (in-container loopback, the bridge, the host, or a sibling container). The gateway has no service-level auth — authorization is owned by hoody-proxy. | Reach the agent through hoody-proxy (e.g. hoody agent … → platform → proxy), not by connecting to the container directly. |
{ "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 configured size cap (MaxBodyBytes). The gateway rejects an oversized body at the edge before the handler reads it. | 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": "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”const { job_id } = await client.agent.headless.createHeadlessRun({ prompt: "Refactor src/api/handlers.ts to extract the validation logic into a separate module, and add unit tests for the new module.", model: "claude-sonnet-4", format: "json", timeout_ms: 120000});
// Poll for the captured resultconst result = await client.jobs.getResult({ id: job_id });// Streaming form — returns an SSE streamconst stream = await client.agent.headless.createHeadlessRun({ prompt: "Summarize the diff in src/api/handlers.ts.", format: "stream-json"});
for await (const event of stream) { if (event.type === "result") console.log(event.data); if (event.type === "error") throw new Error(event.data.message);}