Skip to content

Chat API

The Chat API is what a third-party chatbot integration calls to drive a conversation through a Hadron agent. Hadron compiles the system message and tool schema; the caller hosts the LLM, runs the turn, and reports the result back. Hadron handles routing, stage transitions, data extraction, history, and summarization.

For the conceptual model behind the per-turn fields (topics, conversations, stages, edges, goals), see Conversation routing. For the same surface exposed via MCP tools rather than GraphQL, see MCP tools — Chat.

When to use this API vs. the portal

  • Use the portal Chat tab for manual testing or when the agent is meant to be driven from inside the portal. See Portal chat testing.
  • Use this API when an external app (a website, a native client, an IVR) hosts the chat surface and the LLM call. The app passes user messages in and renders the assistant's replies; the routing logic stays in Hadron.

Surface

Four GraphQL mutations make up the Chat API:

Operation Purpose
startChat Open a chat. Returns the initial system message and tool schema.
sendChatMessage Save a user message and return a recompiled system message + history for the next LLM turn.
processChatResponse Save the LLM's structured response. Extracts data, evaluates routing edges, returns transitions.
saveChatSummary Persist a summary the caller produced when summarizationNeeded fired.

The same operations are available as MCP tools (h-chat-start, h-chat-send, h-chat-process); they call the same resolvers and return the same shapes.

Authentication

Every call carries an identity in ctx. The caller authenticates as one of:

Identity How What it binds
User (JWT) OAuth bearer token in the Authorization header. ctx.userId. Requires the user to hold at least the READER role on the agent's organization.
App App key in the Authorization header (or your platform's app-key conventions). ctx.appId. Resolves to App.agentId. Personal-class memory is auto-keyed on (app_id, user_id).
Anonymous No identity. ctx.userId = "anonymous". No AgentSubscription is auto-provisioned; reads / writes that require one will fail.

App-keyed identity is the typical shape for third-party chatbot integrations — your service holds the App key, and you pass each end user's ID through to bind their personal memory. See Memory access for how user-vs-app identity flows through the four-gate access chain.

Per-turn loop

A typical session is one startChat plus N user turns. Each user turn is one sendChatMessage followed by one processChatResponse.

sequenceDiagram
    participant App as Caller
    participant Hadron
    participant LLM

    App->>Hadron: startChat(agentId, userId?, conversationName?)
    Hadron-->>App: {chatId, systemMessage, tools, conversationName, stageName}
    App->>LLM: systemMessage + tools (no user msg yet)
    LLM-->>App: respond({message, data?, next_stage?})
    App->>Hadron: processChatResponse(chatId, {message, data, next_stage})
    Hadron-->>App: {displayMessage, stageTransitioned, ...}
    Note over App: render welcome to user

    loop Per user turn
      App->>Hadron: sendChatMessage(chatId, userMessage)
      Hadron-->>App: {systemMessage, tools, messageHistory, summarizationNeeded?}
      App->>LLM: systemMessage + tools + messageHistory + userMessage
      LLM-->>App: respond({message, data?, next_stage?})
      App->>Hadron: processChatResponse(chatId, {message, data, next_stage})
      Hadron-->>App: {displayMessage, stageTransitioned, newStageName?, ...}
      Note over App: render displayMessage to user
    end

The LLM is always tool-calling the canonical respond tool — the schema for it is included in the tools field that startChat and sendChatMessage return. If your LLM returns plain text instead of a tool call, treat it as a bug in your provider adapter; the Hadron surface assumes structured responses.

Operations

startChat

Open a new chat with an agent. Hadron creates the chat node, provisions per-user memory if needed, and runs the initial routing to pick a starting conversation and stage.

Inputs

Field Type Required Notes
agentId ID yes The agent to start the chat with.
userId ID no The end-user's ID. Defaults to ctx.userId for JWT identities. App-keyed callers should always pass this so personal memory is keyed correctly.
conversationName String no Force-pick a conversation by name. If omitted, the routing engine picks based on the agent's setup conversation or the welcome flow.

Returns

Field Type What it is
chatId ID Pass this to every subsequent call.
systemMessage String Compiled system prompt for the LLM — injected stages, agent persona, extraction spec, route history.
tools JSON The tool schema for the respond tool. Pass as-is to your LLM.
conversationName String The conversation the chat started in.
stageName String The starting stage.

After startChat, run one LLM turn with no user message — the welcome turn — and feed the LLM's respond tool call into processChatResponse. The chat begins from the assistant's side.

sendChatMessage

Save the user's next message and return a refreshed compilation of the system message and history.

Inputs

Field Type Required
chatId ID yes
userMessage String yes

Returns

Field Type What it is
systemMessage String Recompiled — reflects the current stage and any goals on the stack.
tools JSON Same schema as startChat. Resend on every turn — agents can mutate.
messageHistory [Message!] Full chat so far in the order the LLM should see.
summarizationNeeded Object? Non-null every 20 messages. Shape: { messageCount, prompt }. When set, the caller should run an extra LLM turn against prompt to generate a summary, then call saveChatSummary.

processChatResponse

Save the LLM's structured response, extract any data fields into the user's memory, evaluate stage and conversation transitions, and return what the caller should render.

Inputs

Field Type Required Notes
chatId ID yes
message String yes The reply text the LLM produced.
data JSON no Extraction payload — keys are merged into the user's per-agent memory data node (auto-created if missing).
next_stage String no The stage the LLM thinks the chat should move to. Hadron decides whether to actually transition (it may stay, advance, or follow an edge).

Returns

Field Type What it is
displayMessage String The text to render to the user. Usually equal to message; may be rewritten if a transition produced a hand-off.
stageTransitioned Boolean True if the next turn will start in a different stage than this turn ended in.
newStageName String? The new stage if stageTransitioned.
conversationHandoff String? The new conversation if an edge fired (rare; today fires on conversation-end edges).
onTrack Boolean False when the LLM signalled onTrack: false in the tool call — the conversation has drifted. Triggers re-routing on the next sendChatMessage.
offTrackReason String? The LLM's explanation when onTrack is false.
chatEnded Boolean True if the conversation has reached a terminal stage.

saveChatSummary

When sendChatMessage returned a non-null summarizationNeeded, the caller runs an extra LLM turn against the prompt and stores the result via this mutation. Hadron uses the summary to compress prior history on subsequent turns.

Inputs

Field Type Required
chatId ID yes
summary String yes

Returns true. Skip this call if you don't want history summarization — the chat will keep growing the message history unbounded, which the LLM context window will eventually clip.

Errors

The Chat API throws plain Error instances with text messages — no structured error codes in v1. The most common shapes you'll see at the GraphQL boundary:

Message When
Forbidden The caller's role doesn't satisfy the agent's gate. JWT callers need at least READER on the agent's org. App-keyed callers need an active AgentOrgGrant (cross-org) and active AgentSubscription for the user (personal-class). See Debug PERMISSION_DENIED errors.
Chat <chatId> not found The chat ID is invalid, soft-deleted, or scoped to a different user/app than the caller.
No system memory available The agent doesn't have a chatbot system memory attached. See Building a chatbot agent.
No target memory available for this chat The chat's personal-class memory could not be resolved or auto-provisioned. Usually a configuration issue with the App's Agent.memoryProvisioning.

If you need to disambiguate failure modes programmatically, match on the message prefix. A typed-error contract is on the roadmap but not in v1.

Spec drift

The canonical specification at hadron-concept/hadron-specs/Features/Chat-API.md is older than the implementation and is not authoritative. Treat the resolvers in hadron-server as the source of truth.

Concrete deltas you'll see if you compare:

  • processChatResponse returns more fields than the spec documents — conversationHandoff, chatEnded, onTrack, offTrackReason are all current and not in the spec.
  • data and next_stage are optional on the tool call. The spec implies all three.
  • Fallback rerouting on every turn. If the current conversation is marked isFallback: true and a non-fallback conversation matches via goalSignals, Hadron re-routes there on the next turn. The spec describes routing on session start only.
  • summarizationNeeded shape. Implementation returns a simple { messageCount, prompt }; the spec is vaguer.
  • No <metadata>JSON</metadata> fallback. The spec mentions it; the implementation only supports tool-call extraction.