Skip to main content

Accessing state

Reading the live turn from a tool​

State access lets a tool know who it is serving and what is going on in the conversation. The tool reads the live session, customer, agent, and server directly. The model never has to guess a customer ID or copy data into arguments. Use it whenever a tool should act for the right person.

What you'll learn
  • Which accessors give a tool the live session, customer, agent, and server
  • How to read the customer's last message and follow the turn's effort
  • How to act for the right customer and check whether they are a persona
  • When to log through the server, and what a remote tool uses instead
Talk this page through with your coding agent

Consider a tool that opens a complaint. It needs to know who submitted the complaint and which files they attached. You wouldn't want the model to guess at the customer's ID, or to repeat the attachment data back into the tool's arguments. It might get it wrong, and it's extra work for no gain.

Instead, inside a running turn, Parlant gives your tool a set of .current accessors (i.e., properties that resolve to whatever is currently active) to read that information directly:

AccessorWhat it is
p.Session.currentThe live session: interaction (the conversation so far), effort (the turn's resolved level), mode, metadata, labels.
p.Customer.currentThe customer of the active session: id, name, metadata, groups, kind, and is_persona.
p.Agent.currentThe agent serving the session.
p.Server.currentThe running server, for SDK operations from inside a tool. It also exposes logger.

Four of the session fields are Parlant terms, so let's gloss them quickly. Effort is how much model work the turn gets, from MIN to MAX. Mode is whether Parlant replies on its own (auto) or stays silent so that a person can reply instead (manual).

Labels are short markers, and metadata is free-form key-value data. Both are stored on the session so you can find it and annotate it later.

Note that all four accessors are scoped to the active asyncio task (that is, the task Parlant is running your tool in). If you access one outside a tool or an engine callback, Parlant raises a RuntimeError.

The session​

Now that we know what's available, let's start with the session: the accessor your tools will reach for most often.

The session tells your tool what the customer just said. Here, a tool picks up the attachments from the customer's last message and forwards them along with the complaint:

import parlant.sdk as p


@p.tool
async def file_complaint(summary: str) -> p.ToolResult:
last = p.Session.current.interaction.last_customer_message
attachments = last.attachments if last else []
return p.ToolResult(data=await crm.open_complaint(summary, attachments))

Following the turn's effort​

The session also tells your tool how much to spend. Your agent sets a baseline effort level, and a matched rule can raise it for a single turn.

A tool that reads p.Session.current.effort follows that same judgment. For example, a search tool can look wider on a demanding turn and stay cheap on a simple one, with no change to the policy that calls it:

@p.tool
async def search_knowledge_base(query: str) -> p.ToolResult:
limit = 20 if p.Session.current.effort >= p.Effort.HIGH else 5
return p.ToolResult(data=await kb.search(query, limit=limit))
Learn more about effort

Adaptive effort covers how the baseline is set and how rules raise it.

Metadata and labels​

You read and write session metadata and labels the same way from a tool as from anywhere else: await session.metadata.set(...), await session.labels.add(...).

info

Session analytics shows what to do with labels once they're set, and Managing chat sessions has the full session surface, including a handoff tool that uses them.

The customer​

Once your tool knows what's happening in the session, the next question is who it's happening with.

p.Customer.current is how a tool acts on the right person's behalf, without the model ever handling an ID. Here, a tool looks up the customer's plan and builds a billing summary for them:

@p.tool
async def get_account_summary() -> p.ToolResult:
plan = await p.Customer.current.metadata.get("plan")
summary = await billing.summarize(p.Customer.current.id, plan=plan)
return p.ToolResult(data=summary)

Note that there are two ways to read customer metadata, and they differ in freshness. await metadata.get("plan") refreshes from the store, so you get the latest value. Plain metadata["plan"] reads the snapshot Parlant took at the start of the turn. Writes through metadata.set() and metadata.delete() persist immediately.

The customer's groups is a live collection as well. It supports len, indexing, iteration, and in (with a Group or its id), and await groups.add(...), await groups.remove(...), and await groups.get() to change or refresh it.

A customer also has a kind: p.CustomerKind.REAL for a real person, or p.CustomerKind.PERSONA for a stand-in you test against. customer.is_persona gives the same answer as a boolean, which is handy as a guard in a tool that must never touch real data during a test run.

info

Guests and customers covers where the customer record comes from in the first place.

The server and its logger​

Sometimes a tool needs more than the session or customer in front of it. It needs the server itself.

p.Server.current gives your tool the same SDK surface your startup code has: fetch a session by id, create a customer, look up a group with get_group, and so on.

It also exposes a logger. The purpose of using it, rather than your own logging, is that it writes into Parlant's own log. Your tool's output lands right next to the engine's trace for the same turn, instead of in a separate file:

@p.tool
async def sync_crm(ticket_id: str) -> p.ToolResult:
logger = p.Server.current.logger
logger.info(f"Syncing ticket {ticket_id} for customer {p.Customer.current.id}")

try:
await crm.sync(ticket_id)
except CrmUnavailable as e:
logger.warning(f"CRM sync failed for {ticket_id}: {e}")
return p.ToolResult(data=(
"The CRM is unavailable and the ticket was not synced. "
"Ask the customer to try again later."
))

return p.ToolResult(data=f"Ticket {ticket_id} synced.")

Here, crm and CrmUnavailable stand in for your CRM client and its service-unavailable exception. Notice that no follow-up job is created when the sync fails. So the result asks the customer to retry, rather than promising a callback that would never come.

A good rule of thumb: log at warning for a failure the model will handle, and keep the customer-facing explanation in data.

tip

Everything a turn produces, from the customer's message to the reply, shares one trace_id. When a reply surprises you later, the log and the session's tool events for that trace_id tell the same story from both sides.

Tools running elsewhere​

To bring it all together, there's one boundary you should know about: the accessors only work for tools running inside the Parlant server process.

A tool hosted on a separate service can't use them, because there's no live turn in its process to read from. Instead, it declares an optional first parameter, context: p.ToolContext, which carries the session, agent, and customer IDs over the network:

@p.tool
async def remote_lookup(context: p.ToolContext, query: str) -> p.ToolResult:
return p.ToolResult(data=await search(query, customer_id=context.customer_id))

Note that in-process tools don't need ToolContext at all; prefer the accessors there.