# Agents

Consumer-facing guide to the **agent surface**: the library of named, reusable agents your workspace can dispatch, and the run-control verbs (`resume` / `cancel`) that drive a live run from the outside. This is one of the three doors of the async surface — the other two are [Chat](./chat.md) (start an investigation, poll for the answer) and [Routines](./routines.md) (fire a Chat on a cron), and both can run *as* an agent from the library documented here.

## The model

An **agent** is a named, reusable prompt — a specialization the platform's Master runtime executes with its own full toolkit. Two kinds live side by side in one roster:

- **Amdahl library agents** ship in code — curated specializations like `researcher`. They are `locked`: you can dispatch them, pin them to Chats, and schedule them, but not edit or delete them.
- **Workspace agents** are yours. Create one with a `prompt` (and optionally a `tool_blocklist`), get back a stable kebab-case `slug`, and dispatch it by that slug anywhere an `agent` parameter is accepted.

An agent is *not* a separate execution engine. When a Chat or Routine runs "as" an agent, it is one Master turn whose specialization is the agent's prompt — same toolkit, same data access, same [answer envelope](./chat.md#answers-are-content-blocks). The agent is the reusable instruction set, not a sandbox.

Where an agent plugs in:

| Surface | How |
| --- | --- |
| A Chat turn | `POST /chat` with `"agent": "<slug or id>"` — the turn runs as that agent. |
| A Routine | `config.agents` on the routine — one ref pins the fire to that agent; several restrict delegation to that roster. |
| Delegation | A Master turn may delegate sub-work to agents from the roster mid-run. |

## The library

| Verb | REST | Operation | Scope |
| --- | --- | --- | --- |
| List the roster | `GET /agents` | `agents.list_agents` | `agents:read` |
| Read one agent (full prompt) | `GET /agents/:id` | `agents.get_agent` | `agents:read` |
| Create | `POST /agents` | `agents.create_agent` | `agents:write` |
| Update | `PATCH /agents/:id` | `agents.update_agent` | `agents:write` |
| Delete | `DELETE /agents/:id` | `agents.delete_agent` | `agents:write` |

### List the roster

```bash
curl "https://app.amdahl.co/api/platform/v1/agents" \
  -H "X-API-Key: $API_KEY"
```

The listing is a lean projection — everything except the prompt body — with the Amdahl library merged ahead of your workspace's own rows:

```json
{
  "agents": [
    {
      "id": "3c1e…",
      "slug": "researcher",
      "name": "Researcher",
      "description": "Rigorous multi-step research with citations.",
      "authored_by": "amdahl",
      "locked": true,
      "created_at": null
    },
    {
      "id": "9f2a…",
      "slug": "pipeline-analyst",
      "name": "Pipeline Analyst",
      "description": "Weekly pipeline health readout in our house style.",
      "authored_by": "tenant",
      "locked": false,
      "created_at": "2026-07-02T18:11:40.000Z"
    }
  ]
}
```

`authored_by` is the provenance (`amdahl` = code-defined library, `tenant` = yours); `locked` tells you up front whether an edit will be refused. Read one agent by UUID **or** slug to get the full `prompt`:

```bash
curl "https://app.amdahl.co/api/platform/v1/agents/pipeline-analyst" \
  -H "X-API-Key: $API_KEY"
```

### Create an agent

```bash
curl -X POST "https://app.amdahl.co/api/platform/v1/agents" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Pipeline Analyst",
    "description": "Weekly pipeline health readout in our house style.",
    "prompt": "You are our pipeline analyst. Always segment by deal size band, lead with the risks, and end with the three deals most worth attention this week."
  }'
```

| Field | Required | Notes |
| --- | --- | --- |
| `name` | yes | Display name, ≤120 chars. |
| `prompt` | yes | The instruction set the agent runs with. |
| `slug` | no | Stable kebab-case dispatch key. Omitted: derived from the name and uniquified automatically (`-2` / `-3` on collision). Supplied: must be well-formed and free — a collision returns a typed `slug_conflict` instead of guessing. |
| `description` | no | One-line blurb for the roster, ≤2000 chars. |
| `tool_blocklist` | no | Operation ids this agent may NOT call. Each id is validated against the live registry — an unknown id is refused up front (`invalid_argument`) rather than persisting as a silent no-op. |

```json
{
  "success": true,
  "agent": {
    "id": "9f2a…",
    "slug": "pipeline-analyst",
    "name": "Pipeline Analyst",
    "description": "Weekly pipeline health readout in our house style.",
    "authored_by": "tenant",
    "locked": false,
    "created_at": "2026-07-02T18:11:40.000Z"
  }
}
```

A slug that would shadow an Amdahl library agent is rejected before any row is written — library agents ship in code and cannot be replaced.

### Update and delete

`PATCH /agents/:id` revises `name` / `description` / `prompt` / `tool_blocklist` in place, keeping the slug stable — refine the prompt after seeing output, and every Chat pin and Routine that references the slug picks up the new behavior on its next run. `DELETE /agents/:id` removes a workspace agent; past runs are untouched. Both refuse a `locked` library agent with a typed error rather than silently ignoring the edit. A Routine whose roster references a deleted agent skips its next fire loudly instead of running with a partial roster (see [Routines: reliability semantics](./routines.md#reliability-semantics)).

### Dispatch it

```bash
curl -X POST "https://app.amdahl.co/api/platform/v1/chat" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input": "How healthy is the pipeline this week?",
    "agent": "pipeline-analyst"
  }'
```

The response is the standard Chat START envelope — handles, never a blocking answer. From here the lifecycle is entirely [Chat's](./chat.md#the-lifecycle): poll `read_url`, stream `stream_url`, answer pauses on `resume_url`.

## Run control: resume and cancel

Two verbs drive a live run from the outside. They are the primitives *behind* the Chat lifecycle — `resume_url` posts to resume; the MCP `respond` / `cancel_chat` actions dispatch the same operations — exposed directly so any client can drive the loop.

| Verb | REST | Operation | Scope |
| --- | --- | --- | --- |
| Answer a pause | `POST /agents/:session_id/resume` | `agents.resume` | `workflows:write` |
| Cancel a run | `POST /agents/:session_id/cancel` | `agents.cancel` | `workflows:write` |

`:session_id` is the `run_id` from the Chat START envelope.

### Resume

A run paused in `awaiting_input` carries a `pending_input` object on its READ body: `pending_input_type`, `pending_input_schema` (the JSON schema your reply must match), and `pending_input_context` (the question / reason). POST the reply as `input`:

```bash
curl -X POST "https://app.amdahl.co/api/platform/v1/agents/$RUN_ID/resume" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "input": { "response": "Focus on enterprise; ignore self-serve." } }'
```

The three pause shapes you will meet:

| `pending_input_type` | The run is asking | Reply shape |
| --- | --- | --- |
| `question` | A free-form question (`ask_a_human`). | `{ "response": "…" }` — or the pause's structured options when it offered multiple choice. |
| `approval` | Sign-off on a draft or a destructive step. | `{ "approved": true }` or `{ "approved": false, "feedback": "…" }`. |
| `continue_or_finish` | The turn budget ran out. | `{ "action": "continue", "additional_turns": 10 }` or `{ "action": "finish" }`. |

On success:

```json
{
  "session_id": "9a8b7c6d-…",
  "conversation_id": "3f0e2a1b-…",
  "status": "queued",
  "message": "Resume job enqueued",
  "job_id": "agent-session-9a8b7c6d-…-resume"
}
```

Two failure shapes worth handling: a reply that does not match `pending_input_schema` returns `status: "invalid_input"` with a `validation_error` naming the mismatch — fix the payload and retry; a resume against a run that is not `awaiting_input` returns `status: "invalid_state"` with the current status — refetch the run instead of retrying.

Note that pauses are opt-in: a Chat started with the default `on_question: "auto"` never parks (the agent states its assumption and proceeds), and Routine fires are always headless. You will only ever resume runs you started with `on_question: "pause"` — see [Chat: answering a human question](./chat.md#answering-a-human-question).

### Cancel

```bash
curl -X POST "https://app.amdahl.co/api/platform/v1/agents/$RUN_ID/cancel" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Scope changed; will re-ask with a new brief" }'
```

Cancellation is safe at any non-terminal state: the run flips to `canceled`, the worker stops at its next safe checkpoint, and any sub-agents the run dispatched are canceled with it. An already-terminal run returns `cannot_cancel` with the current status echoed back. `reason` is optional and recorded in operator logs. A canceled turn stays in the Chat's history — a follow-up turn on the same Chat continues with full context rather than cold-starting.

## Over MCP

The whole surface rides the Amdahl MCP server's single `agents` tool — the library CRUD, the Chat lifecycle, and Routines as actions on one tool:

| Family | Actions |
| --- | --- |
| Library | `list_agents`, `get_agent`, `create_agent`, `update_agent`, `delete_agent` |
| Chat | `start_chat`, `chat_status`, `respond`, `cancel_chat` |
| Routines | `list_routines`, `get_routine`, `create_routine`, `update_routine`, `delete_routine`, `run_routine_now` |

`respond` and `cancel_chat` dispatch the same `agents.resume` / `agents.cancel` operations as the REST verbs above. The library reads are also MCP resources: `agent://list` (lean roster) and `agent://<id>` (one agent with its full prompt).

```json
{ "action": "create_agent", "name": "Pipeline Analyst", "prompt": "You are our pipeline analyst. …" }
```

```json
{ "action": "start_chat", "input": "How healthy is the pipeline this week?", "agent": "pipeline-analyst" }
```

## Scopes

| Scope | Grants | Default role |
| --- | --- | --- |
| `agents:read` | List the roster, read one agent. | viewer |
| `agents:write` | Create / update / delete workspace agents. | editor |
| `workflows:write` | `agents.resume` + `agents.cancel` (drive a live run). | editor |
| `conversations:write` / `conversations:read` | Start / read the Chats agents run in. | editor / viewer |

All are on the customer-agent key bundle. A read-only key can browse the roster and read runs, but cannot author agents or drive a run.

## Design notes for agent authors

- **Write the prompt as a role, not a script.** The agent already has the full toolkit and the platform's grounding rules; your prompt adds the specialization — voice, segmentation habits, what to lead with, what to never do. Short and opinionated beats long and procedural.
- **Use `tool_blocklist` to subtract, not to sandbox.** Blocklisting keeps an agent focused (a pure-analysis agent that must never send email blocklists the action ops). It is not a security boundary — the caller's own scopes are; an agent can never do what the caller could not.
- **Iterate against real output.** `PATCH` the prompt, re-dispatch the same ask, diff the answers. The slug stays stable, so nothing referencing the agent changes while you tune it.
- **Slugs are the API surface.** Everything dispatches by slug; pick names you are happy to see in config files (`pipeline-analyst`, not `test-agent-v2-final`).

## See also

- [Chat](./chat.md) — the lifecycle every agent run rides on: START, poll, stream, the answer envelope.
- [Routines](./routines.md) — schedule an agent to run on a cadence.
- [Endpoints](./endpoints.md) — the synchronous verbs an agent composes over.
- [Tool catalog](./api-reference/tool-catalog.md) — the generated per-operation reference.
