# 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.

## 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://app.amdahl.co/api/platform/v1/chat \
  -H "X-API-Key: $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`              | `deep`     | `quick` \| `standard` \| `deep`. A real investigation tier — see [Depth](#depth) below. |
| `actions_allowed`    | (absent = all) | Outbound actions this run may fire (e.g. `email_member`). **Absent = every cataloged action is allowed** (the default); pass a list to narrow, or `[]` to disable actions entirely. |
| `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).                     |

**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 middle tier — 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. **The default is `deep`**: an unspecified `depth` runs the full investigation tier, on the reasoning that a Chat exists to be right, not fast. Pass `standard` or `quick` explicitly when latency or cost matters more than depth on a given ask.

## Watching a run

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

```bash
curl -N -H "X-API-Key: $API_KEY" \
  "https://app.amdahl.co$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 "X-API-Key: $API_KEY" \
  "https://app.amdahl.co$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": "…"
          }
        ],
        "follow_ups": [
          "Break the pricing objections down by segment?",
          "How did objections shift since Q1?"
        ]
      },
      "usage": { "tokens": { "input": 51234, "output": 2210 }, "turns_used": 7 }
    }
  }
}
```

Two more fields ride the READ body:

- **`answered_asks`** — always present. Every `ask_a_human` exchange that was already answered in this run, as `{ question, answer, option_id }`, rebuilt from the run's own log so a reloaded transcript can replay its Q&A history (an unanswered ask surfaces via `pending_input` instead).
- **`events`** — opt-in via `?include=events`. The persisted activity trace: the run's tool calls (`tool_start` / `tool_complete` / `tool_error`, with the same plain-language `label`s the live stream carries) plus every sub-agent's tool calls wrapped in the live stream's `child_progress` / `child_terminated` envelope shape, merged in timestamp order and capped to the newest 500. Use it to re-render "what the run did" after a reload; leave it off on `wait_ms` poll loops — the default body stays lean.

## 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.

`chart_spec.chart_type` is a closed vocabulary the agent picks per data shape: `bar` (comparisons), `line` / `area` (trends), `pie` / `treemap` (share-of-total), `funnel` (stage progressions), `radar` (multi-dimension profiles), `scatter` (correlations), and `gauge` (one value against a target — renders the first data row only: `x` is the label, the first `y` is the value, an optional second `y` is the max, defaulting to 100). All shapes share the same `{ x, y, series? }` encoding over the snapshot rows.

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.

### Suggested follow-ups

`answer.follow_ups` (always present; empty when none) is a short list — at most 4 — of suggested next questions the agent attached to its final answer block. Each entry is a complete, runnable question grounded in what the answer just found **and scoped to what the workspace can actually answer** — the agent only suggests asks its own tools (warehouse queries, theme search, knowledge base, web research) or a dispatchable agent can carry out, never something needing an unconnected data source or a disallowed action. A client can render them as one-click chips that send the question as the next turn (the console does exactly that), and an API or MCP consumer can feed one straight back into `chat.start` / `respond`. The agent supplies them on every normal answer; a turn that ended in plain streamed prose may come back with an empty list.

### Intent enrichment

Before the agent's first turn on a Chat ask (Master lane, `standard` / `deep` depth), a bounded enrichment phase interprets the plain query against light tenant context — business profile, the living-doc catalog, the warehouse surface names — and produces an **intent brief**: the original query verbatim, the interpreted intent, a fully-specified expanded question, and hints at the relevant surfaces. The brief is injected into the model request as advisory guidance (the persisted user message is never modified; the agent is instructed to answer the ORIGINAL question and override the brief where it misreads the ask) and is fail-open: a timeout or error simply skips it. When produced, it surfaces on the run READ as an optional `intent_brief` field (`{ original_query, interpreted_intent, expanded_question, hints[] }`) so clients can render a collapsed "How we understood your question" card; absent means the turn ran unenriched.

### Linked data phrases

Block and prose markdown may hyperlink a specific figure **or phrase** — a metric, a theme label, a company, a segment, anything a query backs — to a suggested follow-up using the closed `amdahl:q` link scheme — a standard markdown link whose destination is `amdahl:q?...`. For the phrase "pipeline grew $42k", the link destination is:

```
amdahl:q?fu=break%20this%20down%20by%20segment&block=blk_1
```

`fu` (required, ≤300 chars decoded) is the follow-up question; `block` optionally points at a presented content block whose `query` + snapshot back the phrase; `sql` (≤2000 chars decoded) is the inline fallback when no block does. Rich clients render the phrase as an interactive chip (hover previews the query, click seeds the composer with `fu`); everything else can treat it as a plain link or ignore it. The grammar is enforced server-side — malformed `amdahl:` links are unwrapped to plain text before persisting, and `answer_text` always arrives with the links stripped to clean prose, so non-rendering consumers never see the scheme.

### Query detail on tool events

Query-shaped tool events (live SSE, persisted, and the `?include=events` replay) carry an optional `queryDetail` — the FULL query behind the call (`sql` capped at 4000 chars, or the natural-language `question`, plus `surface` / `asOf` where relevant) — because the generic `inputSummary` is truncated at 200 chars. `tool_complete` on a query op may also carry `resultStats` (`rowCount`, `truncated`, `cached`). Both fields are additive and absent on non-query tools.

## 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. 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=` and `?include=events`).                    |
| `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.
