Skip to content

Connect a calling agent

Another AI agent can hold a conversation with one of yours and act on what comes back. It sends a message through ask_agent, or calls a routine you have exposed as a named tool, and gets a structured reply envelope beside the prose: what the answer covered, who owns the conversation, and where a routine landed. That envelope is what lets a calling model decide its next move instead of parsing sentences.

Start with MCP server to issue the credential and point a client at the server, or with Publish and open an agent if you want callers to reach the agent with no credential at all. This page covers what the client does once it is connected.

Agent converse setup

The dashboard credential is bound to one agent and the MCP audience. Send its original secret to standalone /mcp; the server exchanges it for a short-lived session token. If a process restarts or its cache expires, it exchanges the original credential again and the backend resumes the conversation mapped to that credential version in PostgreSQL. A session token returned by the internal converse endpoint is for direct calls to that endpoint, not for /mcp. Revoke the credential from the dashboard when the client should lose access; the server re-checks credential state for every request.

The converse surface provides ask_agent, which sends a message through the agent’s full turn loop and continues its conversation. Retrieval can participate in that turn when the agent’s configuration calls for it. Direct document search and grounded-answer generation remain REST API workflows.

What ask_agent returns

The tool’s text content is the agent’s answer followed by the reply envelope as JSON; its structuredContent is that agent reply envelope alone, so a calling model can act on the outcome instead of guessing from prose:

json
{
  "conversationId": "5f3c…",
  "answer": { "text": "You can return an order within 30 days of delivery.", "citations": [] },
  "answerCoverage": { "availability": "assessed", "coverage": "answered", "reason": "sufficient_evidence", "originatingTurnId": "c3…", "originatingRequestId": "c3…" },
  "ownership": { "state": "ai_owned", "suppressed": false },
  "routine": { "name": "Book a demo", "status": "waiting_for_input", "pendingInput": [{ "key": "email", "type": "email", "required": true }] },
  "traceId": "d4…"
}
  • answerCoverage is the same coverage verdict the dashboard trace shows: coverage is answered, partial, unanswered, or unclear, and reason explains it. availability: "not_recorded" means no assessment ran for that turn.
  • ownership says who owns the conversation. human_owned with suppressed: true means a person has taken over and the agent generated nothing on this turn; come back for the reply.
  • routine is present when the turn touched a routine: the one it ran, or one waiting for an operator’s approval. status is active, waiting_for_input, waiting_for_approval, completed, or abandoned; pendingInput lists every required slot still open plus the current step’s optional ones, each with key, type, required, and description, so the client can supply all of them in one message.
  • invocation is present only on a routine tool call (next section): toolName plus an outcome.
  • traceId is the turn’s trace id, the one an operator sees in Activity.

The REST agent channel, POST /api/v1/agents/{agentId}/chat, returns the same answerCoverage, ownership, routine, invocation, and traceId fields beside its answer string and citations array. Both routes publish the envelope as AgentReplyEnvelopeCore in the OpenAPI document.

Routines as tools

A routine an operator has exposed as a tool is listed for the session by GET /api/v1/mcp/converse/tools, one AgentToolDescriptor per routine with a JSON Schema inputSchema built from the routine’s declared slots. A direct converse client invokes it by sending routine instead of message on the ask route:

json
{ "routine": { "toolName": "start_return", "input": { "orderId": "A-1001" } } }

The routine starts with those slots filled, skips the steps that would have asked for them, and reports where it landed in routine (toolName names the tool) and what the call did in invocation.outcome: started; reentered (a completed routine started again, as its reentry setting allows); declined (completed under Once per conversation — the turn answers normally and routine still reports status: "completed"); not_started (another routine was mid-flight or awaiting approval and kept the turn — routine describes it); or unknown_tool. Input is validated before anything is recorded: an unknown tool returns 404 with error.details.code routine_tool_unknown; mismatched input returns 400 with error.details.code routine_invocation_invalid and one { path, code } entry per field in error.details.errors (required, type, format, unknown_field, too_long over 2000 characters; a blank string counts as missing). Both routes check the tool name against the release the conversation is pinned to, while GET /api/v1/mcp/converse/tools lists the current published catalog, so a routine exposed after the conversation began can be listed yet answer routine_tool_unknown.

An MCP client gets the same catalog as tools. The standalone server reads it once, when it exchanges the credential for a session, and pins the result, so the session’s tools/list is stable for its lifetime: ask_agent, the two documentation tools, and one tool per exposed routine — start_return(orderId, reason?) above — each carrying the operator’s description and the descriptor’s JSON Schema. Calling a routine tool checks the arguments against that schema first (a miss is a tool error before the backend is involved, audited with the tool name only), then runs the invocation above. The result’s structuredContent is the full envelope and its text content is answer.text followed by that envelope as JSON, exactly as for ask_agent.

A routine exposed, renamed, or withdrawn after the session opened appears when the client’s next session is established; the server sends no notifications/tools/list_changed.

The converse session does not grant access to other agents, workspace administration, account management, API-access settings, Ray, the skill catalogue, connector secrets, or provider keys.

Come back after a handoff

When a turn hands off to a person, ownership.state becomes human_owned and the agent stops answering. A calling agent cannot sit in the chat, so it comes back: get_conversation_updates({ cursor, waitMs }) returns what has happened since your cursor, and waits for it.

json
{
  "messages": [{ "id": "9a…", "author": "human", "createdAt": "2026-09-22T10:14:02.117Z", "text": "I have refunded the order." }],
  "cursor": "eyJ2ZXJza…",
  "ownership": { "state": "human_owned" }
}

author is human for a person’s reply, agent otherwise. The cursor is opaque: pass back the one the last reply gave you; with none, the call returns the most recent page. The page reports ownership as a state alone — suppressed is a fact about a turn, and a read runs none. waitMs holds the call open for up to 25 seconds and returns an empty messages list at the deadline — nothing new yet, not a failure. Poll again with the same cursor.

Over standalone MCP the session must survive between calls. The server returns an Mcp-Session-Id header on first contact; echo it on every later request to stay in the same conversation. A client that drops it gets a fresh conversation each call.

  • Publish and open an agent — the agent’s public id, its discovery documents, and credential-free access.
  • MCP server — issue the agent credential, connect a client, and run the standalone server.
  • Author a routine — expose a routine so a calling agent can start it by name.
  • Agents and skills — configure the agent behind the conversation.