Running consequential tools
Acting only when the customer meant it
An order lookup can run a hundred times without harm. A money transfer, a cancellation, or a booking cannot. A consequential tool is a tool you mark as one whose call has real-world effects. Parlant then makes the agent confirm with the customer before calling it, and checks every proposed call against your policies before it runs. This playbook builds a bank transfer tool and shows the settings that keep it safe.
- What marking a tool consequential makes Parlant do
- How to require that parameter values come from the customer
- How to attach the tool to an observation so it is found in context
- Which tools should not be consequential
For actions like these, the agent should double-check if and how they should run, and confirm with the customer first.
Parlant handles such sensitive actions with a single setting: marking a tool as consequential.
What the playbook uses
This playbook builds a bank transfer. It relies on concepts from Customizing execution for the tool options and Tool reviews for the check.
A tool is a Python function the agent can call during a reply, attached to an observation or policy that says when it is relevant. Its parameters come from its function's signature, and the agent gathers their values from the conversation and customer context (if any) before calling it.
Marking a tool consequential makes the agent confirm with the customer before calling it, and sends every proposed call through a tool review: a separate model pass that checks the call and its arguments against the context and the agent's policies before it runs, and rejects it if it should not.
You can also annotate a parameter with options. A parameter's source says where its value may come from, and narration lets you customize the status line the customer sees while the tool is being prepared.
The tool
Three things on the tool do the work. consequential=True makes the agent confirm before running it and puts every step that proposes it under review. The source="customer" annotations mean the amount and the recipient must be stated by the customer, never inferred or guessed from context.
The per-parameter settings ride on Python's standard Annotated type, which wraps a type with extra information; Parlant reads a p.ToolParameterOptions there. description tells the agent what the value means.
Inside the tool, p.Customer.current is the customer this session belongs to, so the tool looks up their account securely and deterministically, without being told who they are. Here, bank is your banking integration, which your coding agent connects:
from typing import Annotated
import parlant.sdk as p
@p.tool(consequential=True, narration="Sending your transfer")
async def transfer_money(
amount: Annotated[float, p.ToolParameterOptions(
description="The amount to send, in the account's currency",
source="customer", # MUST be stated by the customer in this session, not inferred
)],
to_account: Annotated[str, p.ToolParameterOptions(
description="The recipient's account name",
source="customer",
)],
) -> p.ToolResult:
account = await bank.primary_account(p.Customer.current.id) # the session's customer
if account.balance < amount:
return p.ToolResult(
data=f"Insufficient funds: the balance is {account.balance:.2f}."
)
receipt = await bank.transfer(account.id, to_account, amount)
return p.ToolResult(
data=f"Transfer of {amount:.2f} to {to_account} sent. Reference {receipt.id}.",
metadata={"receipt": {"id": receipt.id, "amount": amount, "to": to_account}},
)
A tool returns a p.ToolResult. Its data is what the model reads. The unhappy path returns a sentence there rather than raising an error, so the model knows what to say and what not to do next.
The receipt goes on metadata, which your frontend can read. Unlike data, metadata is never sent to the model, only to the frontend client; Rich output shows how the UI picks it up, and the frontend can use it to display the receipt details.
Allowing the tool to be discovered and used
Tools can only be discovered and used by an agent if they are attached to a rule, such as an observation or policy which itself has been discovered. This makes sure that tools are not available at random and are only used in the right context. Here is how to attach our transfer_money tool to a contextual observation:
await agent.create_observation(
condition="the customer wants to send money to another account",
tools=[transfer_money],
)
Raising the bar further
For a tool where a wrong call is expensive, two more settings help. Both sit on the observation rather than the tool, because they change how the whole turn runs, not just one call.
Review the surrounding turn too. Add trigger_tool_review=True to the observation, and every tool call on a turn where it matches is reviewed, not just the consequential one. That catches a lookup with the wrong account before the transfer is even proposed.
Spend more on these turns. Every turn runs at an effort level, the setting that decides how much model work a turn gets. The agent sets a baseline, and a matched observation or policy can raise it for specific, matching turns. Add trigger_effort=p.Effort.HIGH as well, and generation and review run at high effort, on a bigger model.
await agent.create_observation(
condition="the customer wants to send money to another account",
tools=[transfer_money],
trigger_tool_review=True, # review every tool call on this turn
trigger_effort=p.Effort.HIGH, # this turn runs at HIGH effort
)
What not to mark consequential
A balance check, an order lookup, a price query: none of these should be consequential. The agent runs routine lookups directly in service of the request, and asking permission for each one makes the conversation tedious without making it safer. Reserve the flag for actions with real-world effects the customer would want to approve.