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, 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_inputSTART 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
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):
{
"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 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:
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):
# 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:
{
"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. Everyask_a_humanexchange 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 viapending_inputinstead).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-languagelabels the live stream carries) plus every sub-agent's tool calls wrapped in the live stream'schild_progress/child_terminatedenvelope 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 onwait_mspoll 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_1fu (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.