Documentation
Conversational Triage API
A2A integration

A2A integration

Conversational Triage is also reachable as a remote A2A (agent-to-agent) agent, for orchestrators built on the A2A protocol (opens in a new tab) rather than on the HTTP or WebSocket transports directly. This page covers the A2A transport only — for field-level definitions of Assessment, ConversationSettings, and shared enums, see Schemas.

ℹ️

A2A is one more transport onto the same conversation model described in the Quickstart. Use it when your host application is already an A2A orchestrator; otherwise pick blocking HTTP, streaming, or WebSocket instead.

Architecture

Your application is the orchestrator (A2A host). Conversational Triage is a remote agent that conducts a symptom assessment interview, streams assistant text and structured JSON artifacts, and either asks for more input, hands control back to you, or completes with a triage recommendation.

Your host              Infermedica API gateway            CT A2A agent
  |                               |                             |
  |------ POST /auth/token ------>|                             |
  |<-------- access_token --------|                             |
  |                               |                             |
  |------------- GET /.well-known/agent-card.json ------------->|
  |<------------------------ Agent Card ------------------------|
  |                               |                             |
  |------------- message/send (stream, contextId) ------------->|
  |<------------------ text + JSON artifacts -------------------|
  |<------------------- task: input-required -------------------|  mid-triage (loop)
  |                               |                             |
  |-------------- message/send (same contextId) --------------->|
  |                               |                             |
  |<-------------- conversation.handoff.requested --------------|  optional
  |<--------------------- task: completed ----------------------|  handoff → your routing
  |                               |                             |
  |<-------- conversation.completed (+ recommendation) ---------|  triage finished
  |<--------------------- task: completed ----------------------|

In A2A terms your system is the host: it owns the user session, calls remote agents, and decides what happens after each turn. Conversational Triage is one remote agent (skill id symptom-assessment on the Agent Card) — how much routing logic you add on top is your product's choice; Conversational Triage does not ship a host framework.

Thin host (Conversational Triage only) — enough when triage is the only AI capability: a backend service holds credentials and calls the CT JSON-RPC endpoint (never expose secrets to the browser); the UI sends user text through your API, which maps one stable contextId per visit and streams text and JSON artifacts back.

Host with multiple capabilities — same integration, plus your own router after handoff or alongside triage. Keep a session record (contextId, whether CT is the active agent, last assessment snapshot). While CT is active, every user message goes to CT with the same contextId. On conversation.handoff.requested, treat the CT task as finished, read handoff.reason and handoff.userRequest, and route in your own stack — human queue, scheduling API, another agent. Do not infer a handoff from user text yourself; only conversation.handoff.requested is authoritative.

ResponsibilityConversational TriageYour host
CT API access (credentials, token)Yes
contextId lifecycleConsumesGenerates and persists
Interview and triage reasoningYes
Handoff detection (when policies enabled)YesReacts to conversation.handoff.requested
Post-handoff or post-recommendation UXYes

Use any stack with an A2A client library, or JSON-RPC and streaming directly against the Agent Card URL.

Endpoints

ResourceMethodPath
Agent CardGET/a2a/.well-known/agent-card.json
A2A JSON-RPCGET, POST/a2a

Both are relative to the same base URL as the rest of the API — https://api.infermedica.com/api/ct/v2 in production, https://api.dev.infermedica.com/api/ct/v2 in development. Always resolve the JSON-RPC URL from the Agent Card's supportedInterfaces[].url rather than hard-coding it.

Authentication

Authentication follows the same bearer-token flow as the rest of the API — see Authentication. Exchange your credentials for a token, then send it as Authorization: Bearer <token> on both the Agent Card and JSON-RPC requests:

cURL
curl "https://api.dev.infermedica.com/api/auth/token" \
  -X "POST" \
  -H "Content-Type: application/json" \
  -H "Instance-Id: <your_instance_id>" \
  -u "<app_id>:<app_key>" \
  -d '{"grant_type": "client_credentials"}'

Refresh the token before expiry, or on a 401 from any A2A call. Each contextId is scoped to your instance — one active conversation per (instance, contextId) pair.

Agent Card

Discover capabilities before sending messages:

GET /a2a/.well-known/agent-card.json
Authorization: Bearer <token>
FieldValue / meaning
nameConversational Triage
capabilities.streamingtrue — use streaming message/send
defaultInputModes / defaultOutputModestext/plain, application/json
supportedInterfacesprotocolBinding: JSONRPC, url → the JSON-RPC endpoint
skills[].idsymptom-assessment
securitySchemes.bearerAuthBearer token, as above

Most A2A client SDKs (for example @a2a-js/sdk) resolve the Agent Card and inject auth headers for you.

⚠️

Fields on the A2A wire — contextId, handoffPolicies, userRequest, conversationId — use camelCase, unlike the snake_case used by the HTTP and WebSocket transports (handoff_policies, user_request). The underlying schemas are the same; only the casing convention differs by transport.

Sessions and messages

  • Binding: JSON-RPC 2.0 on /a2a
  • Primary method: message/send with streaming enabled (Server-Sent Events, or your SDK's equivalent)
  • Resume key: the A2A contextId — generate and persist one stable value per end-user triage session (a UUID works well)

First message

On the first message/send for a new contextId, Conversational Triage has no session mapping and creates a conversation. Optionally include a data part of type conversation.create to seed settings and assessment, alongside a text part for the user's first utterance:

JSON
{
  "messageId": "<uuid>",
  "role": "user",
  "kind": "message",
  "contextId": "<orchestrator-session-id>",
  "parts": [
    {
      "kind": "data",
      "data": {
        "type": "conversation.create",
        "payload": {
          "settings": {
            "language": "en",
            "channel": "text",
            "handoffPolicies": ["stop_intent_handoff", "pre_triage_handoff"]
          },
          "assessment": {
            "age": { "value": 42, "unit": "year" },
            "sex": "female",
            "evidence": [{ "id": "s_21", "state": "present" }]
          }
        }
      }
    },
    { "kind": "text", "text": "I have had a headache since yesterday." }
  ]
}

If you send only the data part with no text, Conversational Triage registers the session, emits conversation.created, and ends the task at input-required — no triage turn runs until the user sends text. When conversation.create is omitted entirely, both pre_triage_handoff and stop_intent_handoff are enabled by default.

Follow-up messages

Send another message/send with the same contextId and a text part; do not repeat conversation.create — it is ignored once the conversation exists. Conversational Triage looks up the conversation for that contextId and continues the interview.

Session stateText partResult
New contextIdOmitted or whitespace onlySession created from conversation.create; task input-required (opening message when enabled).
Existing sessionWhitespace onlyNot allowed — task failed, metadata code: invalid_request.
Existing sessionNon-empty textNormal turn.

Streaming artifacts

During a turn you receive text artifacts (assistant reply, possibly chunked) and JSON artifacts — named after the same conversation events used on the other transports (conversation.created, conversation.assessment.updated, …), each wrapped in an envelope { "type": "<event>", "payload": { ... } } with mediaType: application/json.

Your host                                                 CT A2A agent
  |------- message/send (conversation.create [+ text]) -------->|
  |<---------------- JSON: conversation.created ----------------|
  |<----------------- text: opening (optional) -----------------|
  |<------------------- task: input-required -------------------|  if no user text on first send
  |                                                             |
  |----------- message/send (text, same contextId) ------------>|  non-terminal turn
  |<--------------- text: assistant (stream, ×N) ---------------|
  |<---------- JSON: conversation.assessment.updated -----------|
  |<----------- JSON: conversation.handoff.requested -----------|  (optional)
  |<------------------- task: input-required -------------------|
  |                                                             |
  |   (repeat for each non-terminal turn)                       |
  |                                                             |
  |----------- message/send (text, same contextId) ------------>|  terminal turn
  |<---------------- text: assistant (optional) ----------------|
  |<---------- JSON: conversation.assessment.updated -----------|
  |<----------- JSON: conversation.handoff.requested -----------|  (optional; handoff path)
  |<--------------- JSON: conversation.completed ---------------|  (recommendation path only)
  |<--------------------- task: completed ----------------------|

On a handoff turn, conversation.completed is omitted. On a mid-triage turn, the task ends with input-required instead of completed. Drive your UI from the task reaching a terminal state, not from the assistant text you already received.

Handoff

Handoff detection and policies (stop_intent_handoff, pre_triage_handoff) work exactly as described in Handoff — set handoffPolicies in conversation.create, or send [] to disable detection entirely. On this transport the record arrives as a conversation.handoff.requested JSON artifact:

JSON
{
  "type": "conversation.handoff.requested",
  "payload": {
    "handoff": {
      "reason": "stop_intent_detected",
      "userRequest": "I want to speak to a doctor"
    }
  }
}

The orchestrator does not infer handoff from free text; it reacts to this event. When it arrives: treat the A2A task as terminal (completed), read handoff.reason and handoff.userRequest for routing, and stop sending triage messages on that contextId — start a new contextId if the product later restarts triage.

Mapping task status to conversation outcomes

A2A exposes task status separately from the conversation events above. Map the two together:

Outcomeconversation.handoff.requestedconversation.completedA2A task statusSame contextId for more triage?
Mid-triageNoNoinput-requiredYes
HandoffYesNocompletedNo — orchestrator takes over
RecommendationNoYescompletedNo

The recommendation itself lives in the conversation.assessment.updated artifact that precedes conversation.completed (payload.assessment.recommendation), not in conversation.completed itself, which only carries conversationId.

Errors

On turn failure, interruption, or an unexpected executor error, the task is set to failed with agent-message metadata:

Metadata codeTypical cause
turn_failedProcessing error
turn_interruptedSuperseded by newer input
invalid_requestInvalid conversation.create payload, or empty user text on an existing session
unknown_evidenceconversation.create seeded an evidence id the knowledge base does not know

HTTP-level failures follow the rest of the API: 401 for missing or invalid authentication (refresh the token, check the instance), 400 for a malformed request. On turn_interrupted, show the latest stream rather than retrying; on turn_failed, log the correlation ids and optionally retry once with the same user text.

Checklist

  1. Auth — obtain a Bearer token server-side; never expose credentials to the browser.
  2. Discovery — fetch the Agent Card, confirm the symptom-assessment skill and capabilities.streaming.
  3. First turn — new contextId, optional conversation.create + user text; handle conversation.created and streaming assistant text.
  4. Turn loop — on input-required, send the next user message with the same contextId; refresh state from every conversation.assessment.updated.
  5. Terminal outcomes — on conversation.handoff.requested or conversation.completed plus task completed, stop CT messaging and run your own routing.
  6. Hardening — handle the error codes above, set handoffPolicies deliberately, and log conversation.id alongside contextId for support.

Related documentation

  • Schemas — every field on Assessment, Recommendation, and settings.
  • Handoff — policies and reasons, shared across all transports.

Was this page helpful?