# Chat

# Chat

Consumer-facing guide to `POST /chat` — the door for asking Amdahl anything that needs a multi-step, server-side investigation. (Renamed from Search: Chat is the multi-turn agentic lane; the name Search now belongs to the synchronous [fast-search endpoint](search.md), `POST /search` — reach for that when you want data back in one blocking call rather than a full agent run.) Every ask opens (or continues) a named **Chat** and runs one **Master** agent turn over your workspace data: warehouse queries, customer-voice themes, knowledge base, long-term memory, and (when enabled) external market search. For dispatching sub-agents inside a run see [Delegating to sub-agents](agents/delegate.md); for the human-in-the-loop pause see [Asking a human](agents/ask-a-human.md).

## The lifecycle

Chat has exactly one lifecycle on every surface:

```
START  →  { chat_id, run_id, status, stream_url, read_url, resume_url }
   │
   ├─ SUBSCRIBE (SSE on stream_url)   live events: tools, delegates, content blocks, pauses
   ├─ READ (GET read_url)             snapshot anytime; ?wait_ms= long-polls up to 30s
   └─ ANSWER (POST resume_url)        only when status is awaiting_input
```

**START never blocks for the final answer.** There is no `stream` flag and no synchronous mode — streaming is how you *watch* a run that already started, and "just give me the answer" is a client-side wait: START, then poll `read_url` until the run settles. Treat the three returned URLs as opaque handles.

| Status               | Meaning                              | What to do                                |
| -------------------- | ------------------------------------ | ----------------------------------------- |
| `queued` / `running` | Work in flight                       | Subscribe to `stream_url` or poll `read_url` |
| `awaiting_input`     | Paused on a human question           | Render the pause; POST the answer to `resume_url` |
| `complete`           | Final answer on the run              | Read `answer.answer_text` + `answer.content_blocks` |
| `error` / `canceled` | Dead                                 | Surface the error; do not resume          |

## START

```bash
curl -X POST https://api.amdahl.com/api/platform/v1/chat \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input": "What are the top objections in enterprise deals this quarter?",
    "name": "Enterprise objections",
    "config": { "depth": "standard" }
  }'
```

Response (immediately — the run is queued, not finished):

```json
{
  "success": true,
  "chat_id": "3f0e2a1b-…",
  "run_id": "9a8b7c6d-…",
  "status": "queued",
  "stream_url": "/api/platform/v1/conversations/3f0e2a1b-…/turns/9a8b7c6d-…/stream",
  "read_url": "/api/platform/v1/chats/3f0e2a1b-…/runs/9a8b7c6d-…",
  "resume_url": "/api/platform/v1/conversations/3f0e2a1b-…/turns/9a8b7c6d-…/resume"
}
```

Pass `chat_id` instead of `name` to continue an existing Chat — the new turn shares the Chat's memory (its substrate) with every earlier turn.

### Body fields

| Field          | Required | What it does                                                                    |
| -------------- | -------- | -------------------------------------------------------------------------------- |
| `input`        | yes      | The ask, in plain language.                                                      |
| `chat_id`      | no       | Continue an existing Chat.                                                       |
| `name`         | no       | Name a NEW Chat. Omitted: auto-titled from the input.                            |
| `agent`        | no       | Pin the turn to a named agent (library slug like `researcher`, or your own workspace agent). |
| `config`       | no       | The knobs below.                                                                 |

### Config (the only knobs)

| Field                | Default      | Notes                                                                  |
| -------------------- | ------------ | ----------------------------------------------------------------------- |
| `depth`              | `standard`   | `quick` \| `standard` \| `deep`. A real investigation tier — see [Depth](#depth) below. |
| `actions_allowed`    | `[]`         | Outbound actions this run may fire (e.g. `email_member`). Empty = none. |
| `write_outputs`      | `false`      | Allow living-doc commits.                                                |
| `write_memory`       | `false`      | Allow long-term memory commits.                                          |
| `on_question`        | `auto`       | `pause` (interactive), `auto` (proceed on judgment), `none` (headless fail-fast). |
| `external_search`    | `false`      | Allow paid market fan-outs (`deep` forces this on).                      |
| `include_divergence` | `false`      | Fuse the internal-vs-market divergence map (`deep` forces this on).      |
| `max_cost_cents`     | chat default | Cost ceiling for a NEW chat; the platform default applies when omitted. |

**There is no `stream`, `model`, `temperature`, `evidence`, or `as_of` field.** The model is picked by the `depth` tier, not by you. **Evidence** (citations and the query behind every data-backed block) is always attached — it is not a toggle. **As-of / time-travel** is not a Chat knob: an interactive Chat always reads your *current* data; historical "as we understood it on date X" reads belong to workflow backtests. Unknown fields are rejected with a structured `invalid_argument` error so a typo never silently changes behavior.

### Depth

`depth` is a genuine investigation tier — the platform sets the model, the turn budget, and the toolset for you:

| Tier       | Model  | Turn budget | Toolset                                          | Reach for it when                                             |
| ---------- | ------ | ----------- | ------------------------------------------------ | ------------------------------------------------------------- |
| `quick`    | fast   | ~10         | lean (fast data reads + fast search)             | a focused lookup you want back quickly; no fan-out, no delegation. |
| `standard` | default| ~50         | full                                             | the everyday default — the full toolset at the profile's own model. |
| `deep`     | most capable | ~75    | full, and `external_search` + `include_divergence` are **forced on** | a hard, multi-part question worth decomposing and cross-checking; the run is told to break the ask into sub-questions and self-verify every figure before answering. |

You never pass a `model` — the tier picks it. `deep` costs more and takes longer; it is the tier to reach for when correctness matters more than speed.

## Watching a run

Subscribe (SSE) — the same event stream the console renders:

```bash
curl -N -H "Authorization: Bearer $API_KEY" \
  "https://api.amdahl.com$STREAM_URL"
```

Or poll — `wait_ms` long-polls up to 30 seconds per request, returning the same body a plain GET returns (never a second START shape):

```bash
# repeat until status is complete | awaiting_input | error | canceled
curl -H "Authorization: Bearer $API_KEY" \
  "https://api.amdahl.com$READ_URL?wait_ms=30000"
```

The READ body carries `status`, the pause payload when awaiting input, usage, and the answer envelope:

```json
{
  "data": {
    "run": {
      "chat_id": "…",
      "run_id": "…",
      "status": "complete",
      "pending_input": null,
      "answer": {
        "answer_text": "Enterprise objections cluster around…",
        "content_blocks": [
          { "id": "b1", "type": "text", "markdown": "…", "presented_at": "…" },
          {
            "id": "b2",
            "type": "chart_spec",
            "chart_type": "bar",
            "encoding": { "x": "objection", "y": "n" },
            "query": { "kind": "data.query", "surface": "interactions", "sql": "SELECT …" },
            "data": [{ "objection": "pricing", "n": 42 }],
            "presented_at": "…"
          }
        ]
      },
      "usage": { "tokens": { "input": 51234, "output": 2210 }, "estimated_cost_cents": 12, "turns_used": 7 }
    }
  }
}
```

## Answers are content blocks

Answers are an ordered list of typed **content blocks** — `text`, `callout`, `citation`, `table`, `chart_spec`, `metric` — not one opaque string (`answer_text` is the flattened markdown for logs and dumb clients). Data-backed blocks (`table`, `chart_spec`, query-backed `metric`) always carry **both** the declared `query` (re-run it live via `data.query` for current numbers) and the snapshot `data` (exactly what the agent saw when it presented the block), on every surface. That pair is what powers Snapshot / Live / Diff rendering: show the snapshot instantly, re-run the query for live values, diff the two over time.

Blocks arrive mid-run as `content_block` events on the SSE stream and reconcile into the terminal `answer.content_blocks` in presentation order. Ignore block types you do not recognize — the catalog grows additively.

## Answering a human question

With `on_question: "pause"`, a run that calls `ask_a_human` lands in `awaiting_input`: the READ body's `pending_input` carries the structured question (multiple choice always includes an **Other** write-in, or free form). Answer by POSTing the resume body to `resume_url` — the standard turn resume described in [Asking a human](agents/ask-a-human.md). The SSE stream stays open across the pause. With the default `on_question: "auto"`, runs never pause — the agent states its assumption and proceeds.

## Chats

Chats are the named threads runs ride on. List, inspect, and rename them:

| Verb                                    | What it does                                                            |
| --------------------------------------- | ------------------------------------------------------------------------ |
| `GET /chats`                            | List chats, newest activity first (`status`, `limit`, `offset`).         |
| `GET /chats/:id`                        | One chat: snapshot, runs with read handles, active runs, pending human asks (each with its `resume_url`). |
| `GET /chats/:id/runs/:run_id`           | The READ endpoint from the START handles (supports `?wait_ms=`).         |
| `PATCH /chats/:id`                      | Rename: `{ "name": "…" }`.                                               |

Every turn in a Chat shares one **substrate** — the scratch memory `context.remember` / `context.query_substrate` write and read — so follow-up asks build on earlier findings, and any sub-agents dispatched inside a run share it too.

## Over MCP

The same lifecycle is on the Amdahl MCP server as four actions on the `agents` tool, never blocking a full run inside one tool call (MCP tool calls have a hard time ceiling; a deep Chat can take minutes):

| Action        | Role                                                                                          |
| ------------- | ---------------------------------------------------------------------------------------------- |
| `start_chat`  | START — same body as `POST /chat` (`input`, optional `chat_id` / `name` / `agent` / `config`); returns the handles immediately. |
| `chat_status` | READ — `{ chat_id, run_id, wait_ms? }`; the same snapshot as `read_url`, `wait_ms` long-polls up to 30s. |
| `respond`     | ANSWER — `{ run_id, response }` where `response` matches the pause schema `chat_status` surfaced. |
| `cancel_chat` | Cancel one run — `{ run_id, reason? }`.                                                         |

The check-in loop: `start_chat` → poll `chat_status` (or read the `chat://<id>/runs/<run_id>` MCP resource) until the run settles → `respond` if it paused on a question. Chat reads are also MCP resources: `chat://list` and `chat://<id>`. Scopes: `conversations:write` to start, `conversations:read` to read, `workflows:write` to respond/cancel — all on the customer-agent key bundle (existing keys were backfilled).

## Per-surface defaults

| Caller                | `write_memory` | `write_outputs` | `on_question` | `external_search` |
| --------------------- | -------------- | ---------------- | ------------- | ------------------ |
| Bare API / MCP key    | `false`        | `false`          | `auto`        | `false`            |
| Console (session JWT) | `true`         | `false`          | `pause`       | `true`             |
| Routine fire          | per recipe     | usually `true`   | `none`        | per recipe         |

The server default is the bare-API row; the console and routines set their own knobs explicitly on each START.
