Handling personal agents
Talking to the AI assistants that act for your customers
A personal agent is an AI assistant acting for one person, like booking a demo or checking on an order. Some browse your website. Others talk to your agent directly over A2A, an open protocol for agents to message each other.
- How personal agents find your agent and talk to it
- What to double-check, and when the person needs to sign in
Your coding agent finds the tools that answer later, adds an in-turn path for AI callers, puts the pointers on your site, and tests it with you.
How a personal agent reaches your agent
Here's what happens when a personal agent runs an errand with your business:
- It finds your A2A agent card on your site: a short description of what your agent does and how to message it.
- It sends your agent a message, much like a customer typing in your chat widget. Your agent treats it as a guest.
- It reads your agent's reply, and sends its next message in the same conversation, until the errand is done.
- If the errand needs the person's account, your agent sends a link so the person can sign in on your site.
The card and the messages follow A2A. The rest, from how your site points to the card to the sign-in link, is built around it by Parlant, and keeps up as the protocols evolve.
Protocols are still being shaped by the industry. Parlant Dashboard keeps adapting this for you automatically, so you're prepared to handle them as they evolve and proliferate, and as the ecosystem evolves around them.
Today, they may require more aggressive notices to get them to engage with your agent, but industry-wide efforts are in progress to standardize how they initiate communication with your site, and Parlant Dashboard is kept consistently up to date with that.
Turning it on
In the Dashboard, open your project's Agents page and click the gear next to the agent's name. Agent settings has an AI agents section. Project Owners and Admins can change it.
A2A is included from the Basic plan up. Managed agents have it on by default, from the project's next deploy. Self-hosted agents stay off until you turn them on with Let AI agents talk to this agent. An Owner or Admin can turn it off at any time.
The same section has Copy A2A setup prompt. Paste it into your coding agent, and it publishes your card on your site, as described below.
Your plan caps how many replies your agent gives to AI agents:
| Plan | Messages per session | Messages per day |
|---|---|---|
| Basic | 5 | 200 |
| Plus | 10 | 1,000 |
| Scale | 20 | 4,000 |
| Enterprise | No limit | No limit |
Over a limit, the personal agent is told to try again later. Under Limits in the same section you can set lower values of your own. Pricing lists what each plan includes.
Doing this yourself?
Over a limit, the caller gets HTTP 429. One IP address can also start up to 60 new conversations a minute.
Each agent's address stays the same while A2A is on, and for 180 days after it's turned off. After that, turning it on gives a new address, and your site's redirect needs the new card URL.
The A2A agent card
An agent card is like a business card for your agent. It's a small file that tells personal agents who your agent is, what it can help with, and how to send it a message.
A personal agent reads it to decide whether your agent can help with its errand, and what to ask. You don't write the file yourself. the Dashboard makes it from your agent's settings, so it always matches them.
Here's what's on it:
| On the card | What it is |
|---|---|
| Name | The agent's name |
| Description (optional) | One or two sentences on what the agent does |
| Organization (optional) | Who provides the agent |
| Site (optional) | Your site, linked as the provider |
| Skills (optional) | What a personal agent can ask it for |
You can set the description, organization and site in the AI agents section of Agent settings. Your coding agent can also help set skills for you via the Dashboard CLI.
Doing this yourself?
All of them, skills included, can be set from the CLI:
parlant dashboard agent a2a get AGENT_ID
parlant dashboard agent a2a set AGENT_ID --description "Answers questions about Acme's orders and returns." --organization "Acme"
parlant dashboard agent a2a set AGENT_ID --site acme.com
parlant dashboard agent a2a set AGENT_ID --skills skills.json
--skills takes a JSON list of up to 10 skills, replacing the ones set before; an empty value (--skills "") removes them:
[
{
"id": "orders",
"name": "Orders",
"description": "Places orders and says where an order is.",
"tags": ["orders", "shipping"],
"examples": ["Where is my order?", "Order two of the lamps I bought last time."]
}
]
Publishing your card on your site
Personal agents look for a card on the site they're visiting, at https://<your-site>/.well-known/agent-card.json, not at agents.parlant.io. So put one there, and point to it from your pages, so an assistant that has never heard of A2A notices it.
A visible note on the page is found to help too, such as "For AI agents: talk to our site's agent directly via A2A."
Doing this yourself?
The best way is a 308 redirect from that path to your agent's card URL, which is in the setup prompt. The card then always matches your settings. With nginx, for example:
location = /.well-known/agent-card.json {
return 308 https://agents.parlant.io/a2a/<address>/.well-known/agent-card.json;
}
If your host can't redirect, serve a copy of the card at that path, and refresh it whenever the agent's settings change. Keep the instructions at the end of its description: they're what makes agents succeed.
Then add the pointers:
<!-- in the <head> of your homepage -->
<link rel="agent-card" href="/.well-known/agent-card.json">
<meta name="a2a-agent-card" content="/.well-known/agent-card.json">
Link: </.well-known/agent-card.json>; rel="agent-card"
<!-- in /llms.txt -->
## Are you an AI agent acting on behalf of a user?
- [Agent card](https://<your-site>/.well-known/agent-card.json): talk to our agent directly over A2A instead of filling in forms. It can book a meeting, check an order's status, and answer questions about our plans.
In that llms.txt line, list everything your agent can help with, such as booking meetings or answering specific questions. It's how a personal agent knows yours can finish its errand.
Check that robots.txt doesn't block /.well-known/.
Letting the customer sign in
Some errands need the customer's account, like placing an order or changing a booking. A personal agent can't sign in for the person, and your agent should never ask it for a password or a code.
Instead, your agent sends a sign-in link. The personal agent passes it to the person, who signs in on your own site, the way they always do. The conversation then carries on as them, and your agent can finish the errand.
For this, you need a sign-in page for these links on your site. Your coding agent can build it and set it up for you.
Doing this yourself?
It's the same switch from guest to customer your chat widget makes, with a link in place of the Sign in button:
-
A tool that needs the customer signed in returns
ToolResult.authorization_required(scopes=[...], reason=...), as in Signing in mid-conversation. Your agent says the person needs to sign in. When an AI agent speaks for the customer, it's told never to write a sign-in link itself. -
The gateway adds the link, never the model: your sign-in page, with the session and a one-time secret. The reply's text ends with it, and a data part carries it too:
{
"data": {
"type": "sign_in_required",
"url": "https://acme.com/a2a-sign-in?parlant_session=...&parlant_claim=...",
"expiresAt": "2026-11-02T10:15:00.000Z",
"reason": "place the order"
},
"mediaType": "application/json"
} -
The personal agent gives the link to its user, who signs in on your page. Your page claims the session for them.
-
Your agent answers the sign-in at once. The personal agent sees that answer at the start of its next reply, and continues with the same
contextId.
A link works once, for 15 minutes; a new one replaces it. The person stays signed in for that conversation until their customer token expires, at most an hour. Then the conversation ends, and the personal agent is told to start a new one.
Set your sign-in page with the CLI. It isn't in Agent settings yet:
parlant dashboard agent a2a set AGENT_ID --sign-in-url https://acme.com/a2a-sign-in
--sign-in-url "" removes it. Without a sign-in page, the reply says your agent doesn't offer sign-in to AI agents yet.
Your sign-in page needs your server to be set up for customer tokens, as in Authentication. Then the page:
- Reads
parlant_sessionandparlant_claimfrom its URL. - Signs the person in the way your site always does.
- Has your backend mint a short-lived customer token for them, with the scopes it allows, as in Wire it to your login.
- Claims the session on your Parlant server:
POST /sessions/{parlant_session}/claim
Authorization: Bearer <customer token>
Content-Type: application/json
{ "claim_secret": "<parlant_claim>" }
| Response | Meaning |
|---|---|
200 | The session is now the customer's. Tell the person they can go back to their assistant. |
403 | The secret is wrong, expired or replaced by a newer link, or the token isn't a customer's. Ask the personal agent to try again for a new link. |
409 | The session already belongs to a customer: the link was used. |
Never log or store parlant_claim. Parlant keeps only its hash.
Testing it
Start on your own server, without the gateway: check that the answer arrives in the reply, and that people still get the background path. Then test through the gateway.
Doing this yourself?
On your own server, create a session the way the gateway does, with the flag set:
POST /sessions
Content-Type: application/json
{ "agent_id": "your-agent-id", "customer_represented_by_an_agent": true }
Post a customer message that needs one of your slow tools, then read the session's events from the next offset:
POST /sessions/{session_id}/events
Content-Type: application/json
{ "kind": "message", "source": "customer", "message": "Do you support Python 3.13?" }
The answer should be in the agent messages that share your message's trace_id, the turn's id. No agent message should follow later with the real answer. Run the same message in a session without the flag, and check that people still get the background path. Managing chat sessions covers these endpoints.
Then test through the gateway. Fetch your card from your own site and check it redirects to your agent's card. Send a message the way the card describes:
curl -sS https://agents.parlant.io/a2a/<address>/message:send \
-H "Content-Type: application/json" \
-d '{"message": {"messageId": "test-1", "role": "ROLE_USER", "parts": [{"text": "Hi, what can you help me with?"}]}}'
The reply carries a contextId. Send the next message with it, and a new messageId, to continue the same conversation. Each reply counts toward your limits, so keep test runs short.
The JSON-RPC binding takes the same message at the agent's address:
curl -sS https://agents.parlant.io/a2a/<address> \
-H "Content-Type: application/json" -H "A2A-Version: 1.0" \
-d '{"jsonrpc": "2.0", "id": 1, "method": "SendMessage", "params": {"message": {"messageId": "test-2", "role": "ROLE_USER", "parts": [{"text": "Hi, what can you help me with?"}]}}}'
To test sign-in, ask for something a tool needs the customer signed in for. Check that the reply has the sign_in_required data part, open its link, sign in, and send the next message with the same contextId: it should start with your agent's answer to the sign-in.
Last, try a real personal agent on a plain errand, such as "book me a demo with this company", without mentioning A2A. Watch whether it finds the card, finishes over A2A, and doesn't also fill in the form.
Avoid background-running tools
Some tools take a while, so they run in the background: your agent says "Let me look into that", and the answer follows a moment later in its own message. That works well for a person in your chat widget.
A personal agent works differently. It sends a message and reads the reply to that message, and that's all. An answer that arrives later only reaches it with its next reply, out of context, or not at all.
So when a personal agent is on the other end, have those tools finish before your agent replies, and put the answer in that reply. Your tools can tell when an AI agent speaks for the customer, so people still get the background path. That's only about how to reply: it says nothing about who the person is or what they've allowed.
Doing this yourself?
Inside a tool, p.Customer.current.is_represented_by_an_agent tells you whether an AI agent speaks for the customer in this session. It's set when the session is created and doesn't change.
import asyncio
import parlant.sdk as p
IN_TURN_SECONDS = 60 # leaves time to write the reply within the agent's 90-second wait
@p.tool
async def look_up_docs(question: str) -> p.ToolResult:
if p.Customer.current.is_represented_by_an_agent:
try:
answer = await asyncio.wait_for(docs.ask(question), timeout=IN_TURN_SECONDS)
except Exception as error:
p.Server.current.logger.warning(f"In-turn lookup failed: {type(error).__name__}")
return p.ToolResult(
data={"answer": None},
rules=[{"content": (
"Say you couldn't look this up right now, and point them "
"to the documentation."
)}],
)
return p.ToolResult(data={"answer": answer.text, "sources": answer.sources})
... # for people: start the lookup in the background, and answer later with utter()
Here, docs is your knowledge service's client.
A few things matter:
- Fit the whole turn in the wait. A personal agent waits 60 seconds for a reply by default, and you can raise that to 90 in the agent's settings. The tool's timeout isn't the whole turn: your agent still has to write the reply after the tool returns, so keep the tool's budget well under the wait.
- Fail inside the reply. On a timeout or an error, return a clear fallback, so the personal agent gets an answer it can act on. If the work's outcome is unknown, such as a booking that timed out, say so; don't report it as done.
- Skip progress messages. "Still checking…" adds nothing when the reply arrives all at once.
Asynchronous responses covers the background pattern, with a booking tool adapted the same way.
What not to change
Leave the path for people as it is. A background lookup with a follow-up message is still the better experience in a chat widget, where a person can keep talking while they wait. The branch is for AI callers only, and a tool that already answers within a second or two needs no branch at all.