Managing chat sessions
Conversations as event logs
A session is one conversation between a customer and your agent. Parlant keeps everything about that conversation in one place: the messages, what the agent did, and any notes your product adds. Your app reads and changes a conversation through its session, and that is also how you pass a conversation to a person.
- What a session stores: the event log, turns, mode, summary, and your own state
- How to read and change a session from code, including handing it to a person
- How a frontend creates sessions, posts messages, and reads replies over HTTP
- How to stop a reply, regenerate from an earlier point, and handle message bursts
A customer can have many sessions with the same agent. Today's support chat and last week's billing question are separate sessions, each with its own history and state.
Everything your product does with conversations goes through sessions, from showing a chat window to routing a customer to a person. So before the API, here is what a session actually holds.
What a session stores
The event log
A session is an event log. Everything that happens in a conversation is recorded as an event: customer messages, agent messages, tool calls, status updates, and custom events your app adds.
Events are append-only, and Parlant orders them by offset, a position number that grows by one with each event. Your client reads a conversation by loading events from the last offset it saw, so it never has to reload the whole history.
Turns
A turn is a group of events. When a customer sends a message, everything Parlant produces in response shares one trace_id: the message itself, the status updates, the tool calls, and the reply.
That id is how you connect a reply to the tool calls behind it. If you want to show why the agent said what it said, the trace_id is where you start.
Mode
A session has a mode. In auto mode, the default, Parlant responds to each customer message. In manual mode, Parlant stays silent: it still records messages, but it generates no reply.
Manual mode is how you hand a conversation to a person. The example below shows it.
Summary
A session may carry a summary. When a conversation grows long, Parlant compacts its earlier part into a summary, stored as a system message on the log. Later turns then read the summary plus the events after it, rather than the entire history.
Compacting long sessions explains when Parlant compacts a session.
Your own state
A session carries your state. Parlant gives you two places for it. metadata holds JSON for your application, such as a CRM ticket id. labels are markers for finding sessions: the ones that need a human, the ones that touched pricing, the ones from yesterday's campaign.
A session either carries a label or it doesn't, and adding one twice changes nothing.
Every label becomes a filter on the session list and a handle for bulk operations. That covers routing queues, topic analytics, and lifecycle jobs like archiving or exporting.
The two serve different jobs: use metadata for values you read one session at a time, and use labels for traits you query across sessions.
Read and update a session from code
This is how your code, or your coding agent's code, reads and changes a session.
Getting hold of the session
Inside a tool or an engine callback, the active session is p.Session.current. Outside one, accessing it raises RuntimeError, because there is no "current" conversation. Elsewhere, fetch a session by id with await server.get_session(id=...).
A session exposes id, interaction, customer, agent, mode, title, metadata, labels, and usage.
A worked example: handing off to a human
Here's a tool that transfers the conversation to a human. A tool is a Python function marked with @p.tool that the agent can call during a reply. It returns a p.ToolResult, whose data is what the model reads and whose control changes the session after the reply.
Creating tools covers tools in full.
Suppose your support console lists sessions labeled needs-human and lets an operator reply in them. The tool below records the reason for the handoff, adds that label, and switches the session to manual mode after the agent's reply. The label makes the session findable; your support console provides the queue and the operator workflow.
import parlant.sdk as p
@p.tool
async def transfer_to_human(reason: str) -> p.ToolResult:
session = p.Session.current
await session.metadata.set("handoff_reason", reason)
await session.labels.add("needs-human")
return p.ToolResult(
data=(
"Transfer requested. Let the customer know a human agent "
"will pick the conversation up shortly."
),
control={"mode": "manual"},
)
The operator reads the same session and posts replies as human_agent events (described below). When they finish, their console can hand the session back to the agent and remove the queue label:
session = await server.get_session(id=session_id)
await session.update(mode="auto", title=f"Resolved: {session.title}")
await session.metadata.delete("handoff_reason")
await session.labels.remove("needs-human")
How reads and writes behave
update takes mode and title but not labels. For labels, use labels.add and labels.remove, and test membership with label in session.labels.
Reads and writes also differ in freshness. A read like session.metadata["handoff_reason"] uses the snapshot Parlant took at the start of the turn, whereas await session.metadata.get(...) refreshes from the store. Writes persist immediately.
Letting rules apply labels
You don't have to add every label by hand. Rules can apply them for you. A rule is either a policy, which gives instructions, or an observation, which only names a situation. Each turn, Parlant judges a policy's content or an observation's condition against the conversation.
Give either one labels=[...], and Parlant adds those labels to the session on a confirmed match, not just because the rule came up as a candidate:
await agent.create_observation(
condition="The customer is asking about pricing",
labels=["pricing-interest"], # added to the session when the condition holds
)
Labels build up across the session. Parlant doesn't remove them when the rule stops matching, so remove temporary ones yourself.
Add labels only when you'll act on them or count them. Every label is one more line in every report. Session analytics builds a label set and the queries that read it.
Speaking without the model
A session can also speak without the model. session.emit_message(...) posts a verbatim message, emit_status posts a status, and emit_custom posts an event for your frontend.
These are how a handoff or a budget cutoff tells the customer what's happening, with no generation involved.
Over HTTP
Once your backend can work with sessions, your frontend needs to as well. Over HTTP, both work with sessions through two resources: /sessions for the conversation, and /sessions/{id}/events for what's in it.
| Method | Path | Purpose |
|---|---|---|
POST | /sessions | Create a session for an agent. Add ?allow_greeting=true to let the agent speak first. Set customer_represented_by_an_agent when an AI agent speaks for the customer (see why). |
GET | /sessions?labels=needs-human&limit=25 | List and filter sessions, with cursor pagination. |
PATCH | /sessions/{id} | Change title or mode; set and unset metadata; add and remove labels. |
POST | /sessions/{id}/events | Post a customer message, a human agent's message, a status, or a custom event. |
GET | /sessions/{id}/events?min_offset=12&wait_for_data=30 | Read events from an offset, long-polling up to the timeout. Add sse=true to stream instead. |
DELETE | /sessions/{id}/events?min_offset=12 | Delete from an offset onward, to regenerate a reply from an earlier point. |
Posting a message and reading the reply
Posting a customer message returns that message's event immediately, not the reply. The reply arrives on the event feed. So a chat UI posts the message, and then reads from the next offset.
Everything a turn produces shares the message's trace_id, which is how a UI shows a tool's output next to the reply it belongs to.
Who may do what on these endpoints depends on the caller. Customers and guests see only their own sessions and get internal fields redacted, while your backend uses the admin key. Authorization has the full table.
Other kinds of events
A customer message is the common case, but it's not the only thing you can post. POST /sessions/{id}/events accepts several kinds of event, selected by kind and source:
| Post | Effect |
|---|---|
kind: "message", source: "ai_agent" | Asks the agent to respond to the session as it stands, with no new customer message. Add rules with an action and a rationale (buy_time, follow_up, or unspecified) to request a specific kind of reply. |
kind: "message", source: "human_agent", with participant: {id, display_name} | Records a human operator's message. No agent processing. |
kind: "message", source: "human_agent_on_behalf_of_ai_agent" | Records a human-written message under the agent's name. |
kind: "custom" with data | Any JSON your frontend wants on the feed, such as a page view. No agent processing. |
kind: "status" | A status of your own: acknowledged, processing, typing, ready, error, or the special cancelled. |
Changing an event's metadata
An event's metadata is patchable on its own, with PATCH /sessions/{id}/events/{event_id} and its set and unset maps. Use it for review flags, moderation annotations, or integration bookkeeping.
The event's content never changes, only its metadata does.
Interrupting and regenerating
Sometimes you need to step in on a turn that is still running, or redo one that has finished. Parlant supports both.
Stopping a reply in flight
To stop a reply in flight, post a status event with status: "cancelled". Parlant cancels the run, emits a cancelled event, and then a terminal ready so that your UI can clear its "thinking" state.
Regenerating from an earlier point
To regenerate from an earlier point, delete events from the offset of the customer message you want to replay, and then post the replacement.
The offset must be the first event of its trace, so a turn is never left half-deleted.
Bursts of short messages
Customers who send several short messages in a row get one reply, not several. Parlant waits about a second before processing the second message in a burst, and three seconds for later ones, so that it responds to the whole thought rather than to each fragment. An agent reply resets the count.
Recommended client patterns
These are the patterns we recommend for the most common kinds of client. Your coding agent can build any of them from this page:
- A chat UI: create the session, post the customer's message, stream events with
sse=truefrom the next offset, render status events as "thinking" and message events as bubbles. - Remember what was shown: store the highest offset rendered in session metadata under your own key, and resume from it on reload.
- Human handoff: switch the session to manual, add a
needs-humanlabel, have the operator post withsource: "human_agent", and switch back to auto when done. Human handoff builds the whole flow. - A sync job: list sessions with
min_modified_utcandlimit, follownext_cursor, and read each changed session's events withwait_for_data=0.