> ## Documentation Index
> Fetch the complete documentation index at: https://docs.plungeai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Agents plane — /v1/agents

> Execute any active platform agent with a plain prompt. The gateway wraps your call in a one-task workflow, so every execution yields a workflowid + taskid pair — that pair is the handle for async redemption and for traces.


<!-- sources-of-truth: orchestration/api-gateway/openapi.ts, orchestration/api-gateway/routes/agents.ts, docs/ONE-API-DEVELOPER-GUIDE-2.0.md | last-synced: 2026-09-24 (re-verified error catalogue, 403 agent_not_active / 404 unknown_agent pre-dispatch fence, and outcomeStatus mapping against routes/agents.ts, lib/agent-fence.ts and routes/_util.ts — all match, no drift found) -->
Execute any active platform agent with a plain prompt. The gateway wraps your
call in a one-task workflow, so every execution yields a `workflow_id` +
`task_id` pair — that pair is the handle for async redemption and for traces.

Auth: `Authorization: Bearer ozk_YOUR_KEY` on every route here.

## GET /v1/agents — list active agents

Query params: `limit` (default 50, max 100), `offset` (default 0).

```bash
curl -s "https://api.plungeai.com/v1/agents?limit=2" \
  -H "Authorization: Bearer ozk_YOUR_KEY"
```

```json
{
  "agents": [
    {
      "id": "llm-agent",
      "name": "LLM Agent",
      "type": "agent",
      "category": "ai",
      "status": "active",
      "description": "General-purpose LLM completion agent",
      "tags": ["llm", "completion"]
    },
    {
      "id": "brave-agent",
      "name": "Brave Search",
      "type": "agent",
      "category": "search",
      "status": "active",
      "description": "Web search via Brave",
      "tags": ["search", "web"]
    }
  ],
  "count": 2
}
```

Each entry is a registry `Card` (summary fields). `count` is the returned
page's length, not the catalog total — page until a short page, never until
`offset >= count`. A failed registry hop returns `502 upstream_error`. The
catalog is live — take ids verbatim from this listing or from
`GET /v1/discovery/search`; never from memory. For richer capability search
(semantic, filtered, quality scores) use the discovery plane — see the
`plungeai-discovery` skill.

## GET /v1/agents/categories — agent categories with counts

Agent categories with active-agent counts, aggregated from the registry (the
registry has no categories endpoint of its own). **Not** a cache-stats
pass-through — an older version of this route proxied `/api/registry/stats`
and returned cache statistics instead; that shape is gone.

```bash
curl -s https://api.plungeai.com/v1/agents/categories \
  -H "Authorization: Bearer ozk_YOUR_KEY"
```

```json
{
  "categories": [
    { "name": "search", "count": 12 },
    { "name": "documents", "count": 9 }
  ],
  "count": 2
}
```

`categories[]` is sorted by count descending, then name. `count` is the
number of distinct categories, not the total agent count. The aggregation
queries only the first 100 active agents from the registry (one page,
`limit=100&offset=0`) — on a catalog past 100 active agents this is a
representative sample of category names, not a guaranteed-complete count
over every active agent. A failed registry hop returns `502 upstream_error`,
same as `GET /v1/agents`.

## POST /v1/agents/{id}/execute — run an agent

Body fields:

| Field | Type | Notes |
|---|---|---|
| `prompt` | string | The instruction. **Required** (or `input`) |
| `input` | string | Alias for `prompt` |
| `persona` | string | Optional persona overlay |
| `provider` | string | Provider id (`anthropic`, `openai`, `gemini`, …); required alongside a pinned `model` |
| `model` | string | Optional model override for LLM-driven agents |
| `maxTokens` | integer | Optional completion cap (`max_tokens` accepted as an alias — OpenAI habits work) |
| `max_tokens` | integer | Alias for `maxTokens` |
| `temperature` | number | Sampling temperature, forwarded to the provider |
| `top_p` | number | Nucleus sampling probability, forwarded to the provider |
| `reasoning_effort` | string | Reasoning-effort hint (e.g. `low`/`medium`/`high`) for reasoning models |
| `thinking_level` | string | Extended-thinking level hint for models that support it |
| `system` | string | System prompt / instructions for the run |
| `messages` | array | Prior chat messages (`[{role, content}]`) instead of a single `prompt` |
| `sync` | boolean | Default `true`. `false` → 202 pointer |
| `stream` | boolean | Default `false`. `true` → an OpenAI-shaped `chat.completion.chunk` SSE instead of a JSON result (see "Streaming" below). Ignored on the async pointer path |
| `format` | string | Response negotiation: `json` (default) \| `yaml` \| `markdown` \| `text` — same values as an `Accept` header (`application/json`, `text/yaml`, `text/markdown`, `text/plain`) or the request `Content-Type` mirror |

### Sync (default) — result inline

```bash
curl -s -X POST https://api.plungeai.com/v1/agents/llm-agent/execute \
  -H "Authorization: Bearer ozk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prompt": "Summarize in one line: Cloudflare Workers are serverless."}'
```

```json
{
  "content": "Cloudflare Workers run code at the edge without managing servers.",
  "workflow_id": "00000000-0000-4000-8000-000000000001",
  "task_id": "t1",
  "request_id": "00000000-0000-4000-8000-000000000004"
}
```

### Async — 202 + pointer

Use for long-running prompts, or when your caller can't hold a connection.

```bash
curl -s -X POST https://api.plungeai.com/v1/agents/llm-agent/execute \
  -H "Authorization: Bearer ozk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prompt": "Write a 2000-word market analysis of edge computing", "sync": false}'
```

HTTP **202**:

```json
{
  "workflow_id": "00000000-0000-4000-8000-000000000001",
  "task_id": "t1",
  "request_id": "00000000-0000-4000-8000-000000000004"
}
```

### Streaming (`stream: true`)

Instead of a JSON result, the 200 response is `text/event-stream`: an
OpenAI-shaped `chat.completion.chunk` per line —

```
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1788574344,"model":"llm-agent","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1788574344,"model":"llm-agent","choices":[{"index":0,"delta":{"content":"391"},"finish_reason":null}]}

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1788574344,"model":"llm-agent","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

data: [DONE]
```

One primer chunk (`delta.role: "assistant"`), then a `delta.content` chunk
per provider token, then a final chunk (`delta: {}`, `finish_reason: "stop"`
or `"length"` when truncated at `max_tokens`), then a literal `data: [DONE]`
line. `usage` is emitted on the final chunk only when the provider reports
the prompt/completion split (often omitted for streamed runs). Comment
keepalives (`: OCEAN PROCESSING`) may appear every ~15s while idle. Response
header `X-Execution-Id` carries the execution id for result redemption /
polling regardless of `stream`. An error before the first byte is a normal
JSON error envelope (not SSE); an error after the first byte is a single
`data: {"error":{…}}` frame and the stream closes WITHOUT `[DONE]`.

### Errors

| Status | Code | Meaning |
|---|---|---|
| 400 | `missing_prompt` / `invalid_format` | No prompt/input given, or `format` isn't one of `json`/`yaml`/`markdown`/`text` |
| 401 | `unauthorized` | Bad/missing `ozk_` key |
| 403 | `agent_not_active` | The agent card is not `status:active` (refused pre-dispatch) |
| 404 | `unknown_agent` | No such agent id in the registry (refused pre-dispatch) |
| 409 | `duplicate_execution_id` | The `x-trace-id` header was already used by an earlier run — send a fresh UUID |
| 413 | `payload_too_large` | Body over 1 MiB (`MAX_REQUEST_SIZE`) |
| 422 | `invalid_params` / `unknown_model` / `empty_completion` | Missing/invalid input named by the agent; pinned model absent/inactive in the catalog; or the run completed but the model returned no visible content — raise `max_tokens` ≥ 64 or change the model |
| 424 | `connection_required` / `credential_required` | The agent needs a connection or key the user hasn't set up |
| 429 | `rate_limited` | `Retry-After` header |
| 502 | `engine_error` / `result_unavailable` | Engine dispatch failed, or sync redemption couldn't fetch the stored result |
| 502 | `upstream_error` | A downstream hop threw (catch-all, any route in this plane) |
| 503 | `agent_unavailable` | Agent temporarily down |

A `502 result_unavailable` on a sync call does NOT always mean the run failed —
the result may land late, and its error body carries `workflow_id` + `task_id`.
Redeem with those ids at the results route below before re-running.

## GET /v1/agents/results/{workflowId}/{taskId} — redeem an async result

```bash
curl -s https://api.plungeai.com/v1/agents/results/00000000-0000-4000-8000-000000000005/t1 \
  -H "Authorization: Bearer ozk_YOUR_KEY"
```

Ready — HTTP 200 (`StoredResult`):

```json
{
  "content": "# Edge Computing Market Analysis\n\n...",
  "content_type": "text/markdown",
  "workflow_id": "00000000-0000-4000-8000-000000000001",
  "task_id": "t1"
}
```

Not ready yet — HTTP 404:

```json
{ "error": { "code": "not_ready", "message": "result not available (yet)" } }
```

Results are tenant-scoped: you can only redeem pointers created by your own
key's identity.

### Polling pattern

`404 not_ready` is the normal in-flight state. Poll with backoff, cap the wait:

```bash
WF=00000000-0000-4000-8000-000000000005; TASK=t1
for i in 1 2 3 4 5 6 7 8; do
  sleep $((i * 2))
  BODY=$(curl -s -w '\n%{http_code}' \
    "https://api.plungeai.com/v1/agents/results/$WF/$TASK" \
    -H "Authorization: Bearer ozk_YOUR_KEY")
  [ "$(echo "$BODY" | tail -1)" = "200" ] && { echo "$BODY" | sed '$d'; break; }
done
```

```python
import time, requests

def redeem(wf, task, key, tries=8):
    for i in range(1, tries + 1):
        r = requests.get(
            f"https://api.plungeai.com/v1/agents/results/{wf}/{task}",
            headers={"Authorization": f"Bearer {key}"})
        if r.status_code == 200:
            return r.json()["content"]
        if r.status_code != 404:
            r.raise_for_status()
        time.sleep(2 * i)          # backoff; 404 not_ready is normal
    raise TimeoutError("result not ready")
```

## Choosing sync vs async

- **sync (default)**: single quick task, interactive callers. Simplest code.
- **async (`sync:false`)**: anything that may run long; queue workers; retries
  survive process restarts because the pointer is durable.
- Send an `x-trace-id` header on the execute call and you can watch the run's
  spans at `GET /v1/traces/<your-trace-id>` while polling — see the
  `plungeai-results-traces` skill.
