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.
| Responsibility | Conversational Triage | Your host |
|---|---|---|
| CT API access (credentials, token) | — | Yes |
contextId lifecycle | Consumes | Generates and persists |
| Interview and triage reasoning | Yes | — |
| Handoff detection (when policies enabled) | Yes | Reacts to conversation.handoff.requested |
| Post-handoff or post-recommendation UX | — | Yes |
Use any stack with an A2A client library, or JSON-RPC and streaming directly against the Agent Card URL.
Endpoints
| Resource | Method | Path |
|---|---|---|
| Agent Card | GET | /a2a/.well-known/agent-card.json |
| A2A JSON-RPC | GET, 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 "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>| Field | Value / meaning |
|---|---|
name | Conversational Triage |
capabilities.streaming | true — use streaming message/send |
defaultInputModes / defaultOutputModes | text/plain, application/json |
supportedInterfaces | protocolBinding: JSONRPC, url → the JSON-RPC endpoint |
skills[].id | symptom-assessment |
securitySchemes.bearerAuth | Bearer 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/sendwith 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:
{
"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 state | Text part | Result |
|---|---|---|
New contextId | Omitted or whitespace only | Session created from conversation.create; task input-required (opening message when enabled). |
| Existing session | Whitespace only | Not allowed — task failed, metadata code: invalid_request. |
| Existing session | Non-empty text | Normal 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:
{
"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:
| Outcome | conversation.handoff.requested | conversation.completed | A2A task status | Same contextId for more triage? |
|---|---|---|---|---|
| Mid-triage | No | No | input-required | Yes |
| Handoff | Yes | No | completed | No — orchestrator takes over |
| Recommendation | No | Yes | completed | No |
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 code | Typical cause |
|---|---|
turn_failed | Processing error |
turn_interrupted | Superseded by newer input |
invalid_request | Invalid conversation.create payload, or empty user text on an existing session |
unknown_evidence | conversation.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
- Auth — obtain a Bearer token server-side; never expose credentials to the browser.
- Discovery — fetch the Agent Card, confirm the
symptom-assessmentskill andcapabilities.streaming. - First turn — new
contextId, optionalconversation.create+ user text; handleconversation.createdand streaming assistant text. - Turn loop — on
input-required, send the next user message with the samecontextId; refresh state from everyconversation.assessment.updated. - Terminal outcomes — on
conversation.handoff.requestedorconversation.completedplus taskcompleted, stop CT messaging and run your own routing. - Hardening — handle the error codes above, set
handoffPoliciesdeliberately, and logconversation.idalongsidecontextIdfor support.
Related documentation
- Schemas — every field on
Assessment,Recommendation, and settings. - Handoff — policies and reasons, shared across all transports.