Skip to main content

Calling agents

Applications invoke an agent ID. An employee ID identifies shared configuration; it is not a runnable route. Use an agent you created or one your operator made available, and verify it with Listing agents.

Choose the interface​

InterfaceAgent selectorText in a successful non-streaming response
Python client.messages.create(...)agent_id="intake"response.text (SDK helper)
POST /v1/messagesmetadata.agent_idText blocks in content[]
POST /v1/chat/completionsmodel: "intake" for a registered agentchoices[0].message.content
POST /agents/v1/intake/chatAgent route ID in the pathresponse
POST /agents/v1/intake/jobsAgent route ID in the pathReturns a job receipt; retrieve the result later

These endpoints do not share one request or response schema. In particular, the Python parameter agent_id becomes metadata.agent_id on the Messages wire format. A top-level agent_id is not valid for /v1/messages.

Messages-compatible request​

Set LIBRA_OS_URL to your server URL and LIBRA_OS_API_KEY to an authorized bearer credential. This example assumes an agent named intake already exists:

curl --fail-with-body -sS "$LIBRA_OS_URL/v1/messages" \
-H "Authorization: Bearer $LIBRA_OS_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"model": "intake",
"max_tokens": 500,
"metadata": {"agent_id": "intake"},
"messages": [{"role": "user", "content": "What can you help me with?"}]
}'

Here metadata.agent_id selects the behavior and model repeats the agent ID so its configured model can be used. A supported provider model ID in model can instead override the answer model for that call; it does not select the agent or change all planner/skill tiers. Omitting metadata.agent_id selects the server's default agent, even if model looks like another agent ID.

To read text from the raw response:

text = "".join(
block.get("text", "")
for block in response["content"]
if block.get("type") == "text"
)

Keep non-text blocks when using tools. Do not parse the entire content array as a string or discard tool results your application needs.

Native chat request​

curl --fail-with-body -sS "$LIBRA_OS_URL/agents/v1/intake/chat" \
-H "Authorization: Bearer $LIBRA_OS_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"message":"What can you help me with?"}'

Native chat uses message for a simple text turn and returns fields such as response, agent_id, conversation_id, retrieved_chunks, cited_sources, and grounding. cited_sources is always present: an empty list when the answer cited nothing. A retrieved document counts as cited when the answer names it by its source, its title, or its file name without the extension, with dashes and underscores read as spaces: shipping-policy.md is cited by "Shipping policy, clause 2.1" (whole phrase, any case; names shorter than 4 characters or made only of digits are not matched).

Continuing a conversation​

Save the returned conversation_id and send it with the next message to continue that thread:

{
"conversation_id": "chat_4f1c9a0e7b2d4c6a8e3f5b1d9c7a2e60",
"message": "Translate that into French."
}

The model then sees the thread's earlier user and assistant turns in front of the new message. Only the newest turns that fit a token budget are sent; older turns are dropped whole, never cut in half. The budget is an operator setting, LIBRA_OS_CONVERSATION_REPLAY_MAX_TOKENS (default 8000; 0 turns replay off).

A conversation belongs to the caller who started it and to the agent it was recorded with. A request that names another user's conversation, or a conversation recorded with a different agent, is refused with the same 404 before the turn runs, whether it sends message or messages[]. A conversation continues only on the agent it was recorded with; an agent's built-in tool loop counts as that agent, but a separately defined agent never does, whatever its name. Conversations recorded before agents were tracked are accepted on any agent, but nothing from them is replayed. When you send no conversation ID, the one the server issues is random (chat_ followed by 32 hex characters), so it cannot be guessed.

If you send messages[] instead of message, you are supplying the history yourself and nothing stored is added. Use one approach or the other.

Replay re-sends each earlier message exactly as it was sent, including any context your backend assembled into it. If you build each turn's message from selected pages, operator guidance or drafting instructions, every later turn gets those earlier assemblies back too, including pages the user has since deselected and guidance meant for an earlier draft. Such a backend should send the history it wants in messages[], or set "no_history": true to skip replay for that turn. The model then sees only the current message, and the turn is still recorded in the conversation.

When the request names a conversation, the response carries two counts:

  • history_replayed: the number of earlier user and assistant messages that were added (0 on a first turn, a messages[] request, with no_history: true, or with replay off).
  • history_dropped: the number of earlier messages left out because they did not fit the budget. A single turn larger than the whole budget is left out, not cut, so history_replayed: 0 with history_dropped above 0 means the history exists but did not fit.

Both are absent when the request names no conversation. Streaming turns carry them on the final done event.

Optional request fields for backends that assemble their own prompts:

FieldTypeEffect
retrieval_querystringWhat to search the knowledge base for, when that differs from the message. Empty or absent: the query is derived from the message (see below).
no_historybooleanDo not replay the conversation's earlier turns on this turn. The turn is still recorded. See Continuing a conversation.
no_toolsbooleanRun the turn with no tools attached, even when the agent declares direct tools. Knowledge retrieval before the turn still runs.
no_memorybooleanObservational memory is neither read nor written on this turn.
no_retrievalbooleanNo knowledge-base retrieval on this turn: no excerpts are added before the turn, the knowledge search tool is not offered, and the automatic search before saying "not found" does not run. Attachments sent in the conversation can still be read.

Without retrieval_query, a long or multi-line message is searched by one line taken from it, normally its last line that is not an instruction. If that search finds nothing, the full message is searched once. If your backend sends a ticket followed by instructions, the line chosen may still be the wrong one, and the full-message search is a blunter fallback. Set retrieval_query to the question you want answered from the knowledge base; a query you supply is searched as given and never replaced:

{
"message": "Ticket:\n...\n\nDraft a reply in our house style.",
"retrieval_query": "Late delivery: can I get a refund for an order that arrived two weeks late?",
"no_tools": true,
"no_memory": true
}

Use no_retrieval when your backend puts all the context the answer may use into the message and wants "this isn't covered" to come back as written. Without it, an agent with bound collections searches them anyway: on a knowledge-bound agent, a draft that says the material isn't there, written when no knowledge search ran on that turn, is not returned. The runtime has the model search once and then keeps whatever it answers. That is usually what you want, but it overrides a caller that asked for an answer from its own pages only.

These fields exist only on native chat; /v1/messages and /v1/chat/completions do not accept them.

Native chat responses (and the final done event of a streaming turn) also carry these fields:

FieldMeaning
history_replayedEarlier messages replayed in front of this turn. Present when the request names a conversation.
history_droppedEarlier messages left out by the replay budget. Present when the request names a conversation.
retrieval.queryThe text the knowledge base was searched for on this turn.
retrieval.query_sourceWhere that text came from: explicit (your retrieval_query), derived (one line taken from the message), full_message_fallback (the derived line found nothing, so the full message was searched), or message (the message as sent).
cited_sourcesRetrieved documents the answer cites, by source, title or file name. Always present; an empty list when nothing was cited.
groundingHow the answer relates to evidence. retrieval_not_attempted means no knowledge search ran on this turn, because the agent has no bound collections or the request set no_retrieval.

retrieval.query and retrieval.query_source are omitted when no retrieval ran. Read them when an answer says the material isn't there: they show what was actually searched.

Authentication, context, and memory​

The credential identifies the caller and its permissions. A conversation ID identifies a thread. An agent ID selects executable behavior. None substitutes for another.

With the Python Messages API, sending message history explicitly is a portable way to continue a conversation. For stored Messages threads, the server also accepts metadata.conversation_id; use a stable, authorized ID and follow the endpoint's history behavior. Observational memory is separately enabled and identity-scoped. See Managing memory.

Backend integrations serving several people must use the deployment's authorized identity mechanism. X-End-User and X-Canonical-User are honored only for callers with the relevant impersonation capability; see API keys. Memory is kept per canonical user, so it is X-Canonical-User that keeps one person's memory from another's; see Managing memory. Never treat an employee ownership link or a client-supplied ID as a grant of access.

API keys​

An admin can mint two kinds of API key for backend integrations:

  • Agent keys, POST /api/agents/{id}/keys, reach one agent's /agents/v1/{agent}/... routes.
  • Service keys, POST /api/service-keys, are organization-wide keys for inference.

The key's secret is returned once, in the mint response; store it then.

curl --fail-with-body -sS "$LIBRA_OS_URL/api/agents/intake/keys" \
-H "Authorization: Bearer $LIBRA_OS_ADMIN_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name": "ticket-backend",
"owner_user_id": "<service-account user id>",
"allow_impersonation": true
}'

A key acts as its owner. By default it cannot act for anyone else:

  • allow_impersonation (both endpoints, default false): only a key minted with "allow_impersonation": true and owned by an admin may set X-End-User or X-Canonical-User to act for other end users. A shared-credential backend that serves many people through one key needs it. Keys that existed before this option was added kept the capability. Key list responses show allow_impersonation for each key.
  • owner_user_id (agent keys only): mint the key for another user, typically a service account, instead of the admin making the call. The user must exist, be active, and belong to the same organization (404 otherwise, 400 when inactive). A key owned by a non-admin never impersonates, whatever its allow_impersonation says.

Your operator can narrow impersonation further by key type.

Handle outcomes​

OutcomeWhat to check
401 / 403Credential validity and permission for the endpoint, agent, and resource.
404Correct route/agent ID and visibility; an upstream model can also be unavailable.
409 creating an agentThe name may already exist; reuse or deliberately update the agent.
426 on /v1/agentsSupply the managed-agent beta header or use the SDK.
Answer has no sourcesCheck knowledge bindings, ingestion, retrieval capabilities, and grounding metadata.
Tool action awaits approvalFollow the action's decision/execution status; a completed chat is not evidence that the action executed.

For asynchronous progress, reconnect rules, and job statuses, use the background-task guide.