Skip to main content

Tools

Tools are how your agent takes action—querying databases, calling APIs, processing payments.

In Parlant, tools are part of the context delivery system: they only enter the agent's context when a relevant observation fires, rather than being "always available."

This eliminates the false-positive tool calls that plague most LLM tooling, where agents eagerly call tools out of context.

Understanding False-Positive Invocations

When tools are "always available" in an agent's context, LLMs don't just occasionally misuse them—they do so at alarming rates.

Yang et al., 2024 found that when critical information was missing from a user's request, models still attempted to call a tool 97% of the time (Mistral-7B), with even GPT-4 doing so 91% of the time—compared to human participants who only failed to recognize the incomplete scenario ~20% of the time.

The Berkeley Function Calling Leaderboard (UC Berkeley) specifically benchmarks "irrelevance detection"—whether a model can refrain from calling a function when none of the provided functions are relevant—and reports scores as low as 49–55% for large models like Claude Sonnet and GPT, which is essentially a coin flip.

ToolACE (ICLR 2025) further documents this pattern, identifying "hallucinated function calls" as a core failure mode where models fabricate invocations to tools in their context despite lacking the information or instructions to do so.

Connecting Tools to Observations

The simplest way to connect a tool is through an observation. Only when the observation's condition matches does the tool become available to the agent. The agent uses the tool's own description to decide how to call it:

@p.tool
async def query_docs(context: p.ToolContext, user_query: str) -> p.ToolResult:
"""Query the documentation knowledge base to answer user questions."""
...

await agent.create_observation(
condition="The user asks about service features",
tools=[query_docs],
)

When the tool isn't self-explanatory—or when you need to guide how it's used—pair it with a guideline instead. The guideline's action provides additional context that shapes the tool call:

await agent.create_guideline(
condition="The customer asks about the newest laptops",
action="First recommend the latest Mac laptops",
# The action ensures the tool is called with the right query
tools=[find_products_in_stock],
)
Conversational UI Analogy

You can think of observations and tools like widgets and event handlers in graphical UI frameworks.

A GUI button has an onClick event which we can associate with some API function to say, "When this button is clicked, run this function." In the same way, in Parlant the observation is like the button, the tool is like the API function, and the association connects the two to say, "When this condition is detected, make this tool available."

Agentic API Design

To learn about best practices for designing your agent's tools, we recommend reading our blog post on Agentic Backends.

Writing Tools

To write a tool, you need to define a function that takes a p.ToolContext as its first argument, followed by any other parameters you want to pass to the tool. The function should return a p.ToolResult.

Here's the basic structure of any tool in a Parlant agent:

import parlant.sdk as p

@p.tool
async def tool_name(context: p.ToolContext, param1: str, param2: int) -> p.ToolResult:
"""Multi-line tool description.

This is readable by the agent and helps it decide if/when
to run this tool (even when it's contextually relevant).
"""
...

To illustrate that more concretely, here's a simple example of a tool that fetches products into the agent's context:

import parlant.sdk as p

@p.tool
async def find_products(context: p.ToolContext, query: str) -> p.ToolResult:
"""Fetch products based on a natural-language search query."""

# Simulate fetching the balance from a database or API
products = await MY_DB.get_products(query=query)

return p.ToolResult(products)

Optional Parameters

You can also define optional parameters in your tool by using the Optional type from the typing module. This allows the tool to be called without providing a value for that parameter.

from typing import Optional
import parlant.sdk as p

@p.tool
async def find_products(
context: p.ToolContext,
query: str,
limit: Optional[int]
) -> p.ToolResult:
default_limit = 10
products = await MY_DB.get_products(query=query, limit=limit or default_limit)

return p.ToolResult(products)

Consequential Tools

A tool can be marked as consequential or non-consequential, depending on whether it has side effects that require careful evaluation before execution, or whether its parameterization must undergo special efforts to ensure validity and correctness. This mark lets Parlant know how accurately you need it to examine the tool whenever it's in context.

By default, tools are considered non-consequential. The combination of this flag and whether the tool has parameters determines how Parlant evaluates it:

Has ParametersNo Parameters
ConsequentialSlower, accurate evaluation of whether the tool should be called and how to parameterize itAccurate evaluation of whether the tool should be called
Non-consequential (default)Faster, shallow evaluation of whether the tool should be called and how to parameterize itInstant optimistic execution—the tool is always called when it's brought into context

If a tool has side effects that require careful evaluation before execution, mark it as consequential:

@p.tool(consequential=True)
async def process_payment(context: p.ToolContext, amount: float) -> p.ToolResult:
"""Process a payment transaction."""
...

Tool Result

The ToolResult is a special object that encapsulates the result of a tool call.

Tool Result Properties

Let's look at each of these properties, what they're used for, and how to use them:

Data

The data property contains the main output of the tool.

This can be any JSON-serializable type, such as a string, list, or dictionary.

This is the only property of the ToolResult that is always required, as it is the only one that the agent itself uses to understand the history of interaction events. Meaning, if you don't return anything in the data property, the agent will not be informed about your result, and it will not be able to use it to navigate the interaction.

async def check_flight_status(context: p.ToolContext, flight_number: str) -> p.ToolResult:
...
return p.ToolResult(data="Flight ticket booked successfully!")
async def find_appointments(context: p.ToolContext, dates: list[datetime]) -> p.ToolResult:
...
return p.ToolResult(data={"appointments": [
{ "id": "123", "date": "2023-10-01 10:00" },
{ "id": "456", "date": "2023-10-02 11:00" },
]})

Metadata

The metadata property is an optional dictionary that can be used to store additional information about the tool call.

The agent is not aware of this metadata at all, but you can fetch it from the response using the REST API client. This makes it useful for sending back additional information about the response that can add value in your frontend.

A classic use case here is to return RAG information sources (e.g., URLs, document IDs, etc.) that can be used to display the source of the information in the frontend. Another one is to return image links to generated charts or other visualizations that can be displayed in the frontend.

return p.ToolResult(
data=ANSWER,
metadata={ "sources": [{"url": s.url, "title": s.title} for s in ANSWER_SOURCES]},
)
return p.ToolResult(
data="The profit margin is 20%",
metadata={ "generated_chart_url": "https://example.com/chart.png" },
)
Mind the Lifespan

Since metadata is primarily useful when accessing session events, it generally only makes sense to use it with lifespan: "session" (the default). If you use lifespan: "response", the metadata will not be available in the session events, hence not accessible to the frontend.

Control

The control property lets you specify control directives for the engine.

  • "lifespan": p.Lifespan: This controls how long the ToolResult should live. There are two options:
    • "session": The result will be saved and made available to the agent for the entire session. This is the default.
    • "response": The result will only be available for the current response. This is useful for temporary results that are not needed beyond the current response, such as reporting errors, or providing very transient information.
return p.ToolResult(
data="Encountered an error while fetching data",
# This tool result will not be saved in the session.
# The agent will only be aware of it during the current response.
control={ "lifespan": "response" },
)

Updating the Session

Tools can update session properties directly via the Session object. This is particularly useful for human-handoff scenarios, where you want to pause the agent's automatic responses and let a human operator take over:

session = p.Session.current

# Switch to manual mode — the agent will stop generating automatic responses
await session.update(mode="manual")

return p.ToolResult(data="Transferring to a human agent")

You can also transfer the conversation to a different agent:

billing_agent = await p.Server.current.get_agent(id="billing_specialist")

await p.Session.current.update(agent=billing_agent)

return p.ToolResult(data="Transferring you to our billing specialist")

Canned Responses

Tools can also return complete canned responses for consideration, as well as fields to be substituted during canned response rendering. For more information about canned response properties, refer to the Canned Responses section.

Tool Result Lifespan

Tool results are saved in the session by default, and are therefore available for the agent's reference throughout the entire interaction session. This means that subsequent observation matching and tool calls are automatically informed by the previous results of tools. This is useful for results that need to be referenced later, such as account balances, item IDs, or other product information.

For example, if you call a tool that returns product names and IDs, and—after the customer responds by selecting a specific product—you then call another tool that takes a product_id parameter, the agent will be able to use the previous tool's result to fill in that parameter automatically from context:

@p.tool
async def get_products(context: p.ToolContext, query: str) -> p.ToolResult:
products = await MY_DB.get_products(query=query)
return p.ToolResult(data=products)

@p.tool
async def get_product_details(context: p.ToolContext, product_id: str) -> p.ToolResult:
# The agent will be able to parameterize the right `product_id`
# if the previous tool call was already made in the session.
product = await MY_DB.get_product(product_id=product_id)
return p.ToolResult(data=product)

Reevaluation Based on Tool Results

In some cases, tool results can influence which observations and guidelines become relevant. Here's an example:

Consider a banking agent handling transfers. When a user requests a transfer, an observation with the condition the user wants to make a transfer activates the get_user_account_balance() tool to check available funds. This tool returns the current balance, which can then trigger additional matches based on its return value.

For instance, if the balance is below $500, we might have a low-balance guideline activate, instructing the agent to say something like: "I see your current balance is low. Are you sure you want to proceed with this transfer? This transaction might put you at risk of overdraft fees."

You can mark certain observations or guidelines for reevaluation after a tool call. This means that once the tool is called, the matcher will re-evaluate the session to see if any new observations or guidelines should be activated based on the tool's results—after running the tool but before generating the response.

# Reevaluate this observation/guideline after running this tool
await observation.reevaluate_after(my_tool)

You can also reevaluate an entire tag of guidelines after a tool call, which is useful when a tool's results may affect multiple related guidelines at once:

calculation_triggers = await agent.get_tag(name="calculation-triggers")
await calculation_triggers.reevaluate_after(fetch_latest_credit_score_formulae)

Tool Context

The ToolContext parameter is a special object that provides the tool with contextual information and utilities.

Let's look at some of the most useful attributes and methods available in the ToolContext:

  1. agent_id: The unique identifier of the agent that is calling the tool.
  2. customer_id: The unique identifier of the customer interacting with the agent.
  3. session_id: The unique identifier of the current session.
  4. emit_message(message: str): A method to send a message back to the customer. This can be used to report progress during a long-running tool call.
  5. emit_status(status: p.SessionStatus): A method to update the session status.

Accessing Current Entities

Within tools (and other engine hooks), you can access the current server, agent, customer, and session using convenient .current accessors:

import parlant.sdk as p

@p.tool
async def my_tool(context: p.ToolContext) -> p.ToolResult:
server = p.Server.current
agent = p.Agent.current
customer = p.Customer.current
session = p.Session.current

if variable := agent.find_variable(name="my_variable"):
value = variable.get_value()

return p.ToolResult(...)

Secure Data Access

Suppose you need to build a tool that retrieves or displays data private to different customers.

A naive approach would be to ask the customer to identify themselves and use that as an access token into the right data. But this approach is highly insecure, as it relies on the LLM for identifying the user. The LLM can get it wrong or, worse yet, be manipulated by malicious users.

A better and more reliable way to do this is to register your customers with Parlant and use the information available programmatically, which is contained in the ToolContext parameter of your tool.

Here’s how that would look in practice:

@p.tool
async def get_transactions(context: p.ToolContext) -> p.ToolResult:
transactions = await DB.get_transactions(context.customer_id)
return p.ToolResult(transactions)

Tool Insights and Parameter Options

Because Parlant's architecture is radically modular, components like observation matching, tool calling and message composition operate independently. While this non-monolithic approach offers many advantages in managing its complex semantic logic, it also requires it to communicate contextual awareness across these components.

Tool Insights is a bridging component between tool calling and message composition, ensuring the composition component is informed when a tool couldn't be called for some reason—for example, due to missing required parameters.

This allows the agent to respond more intelligently. For example, if it had no knowledge of when an appropriate tool couldn't be called, it might generate a misleading response. But with tool insights, the agent recognizes missing information and, if needed, can prompt the customer for the required tool arguments automatically.

Tool Parameter Options

To allow you to enhance the baseline behavior of Tool Insights, you can make use of ToolParameterOptions, a special parameter annotation which adds more control over how tool parameters are handled and communicated.

While Tool Insights helps the agent recognize when and why a tool call fails, ToolParameterOptions goes a step further by guiding the agent on when and how to explain specific missing parameters.

from typing import Annotated
import parlant.sdk as p

@p.tool
async def transfer_money(
context: p.ToolContext,
amount: Annotated[float, p.ToolParameterOptions(
source="customer", # Only the customer can provide this value - the agent cannot infer it
)],
recipient: Annotated[str, p.ToolParameterOptions(
source="customer",
)]
) -> ToolResult:
# ...

The ToolParameterOptions consists of several optional arguments, each refining the agent’s understanding and application of the parameter:

  • hidden If set to True, this parameter will not be exposed to message composition. This means the agent won't notify the customer if it’s missing. It's commonly used for internal parameters like opaque product IDs or any other information that should remain behind the scenes.

  • precedence When a tool has multiple required parameters, the tool insights communicated to the customer can be overwhelming (e.g., asking for 5 different items in a single message). Precedence lets you create groups (which share the same value) such that the customer would only learn about a few (the ones sharing a precedence value) at a time—in the order you choose.

  • source Defines the source of the argument. Should the agent request the value directly from the customer ("customer"), or should it be inferred from the surrounding context ("context")? If not specified, the default is "any", meaning the agent can retrieve it from anywhere.

  • description This helps the agent interpret the parameter correctly when extracting its argument from the context. Fill this if the parameter name is ambigious or unclear.

  • significance A customer-facing description of why this parameter is required. This helps customers understand and relate to what information they need to provide and why.

  • examples A list of sample values illustrating how the argument should be extracted. This is useful for enforcing formats (e.g., a date format like "YYYY-MM-DD").

  • adapter A function that converts the inferred value into the correct type before passing it to the tool. If provided, the agent will run the extracted argument through this function to ensure it matches the expected format. Use when the parameter type is a custom type in your codebase.

  • choice_provider A function that provides valid choices for the parameter's argument. Use this to constrain the agent to dynamically choose a value from a specific set returned by this function.

github Questions? Reach out!

Parameter Value Constraints

In cases where you need a tool's argument to fall into a specific set of choices, Parlant can help you ensure that the tool-call is parameterized according to those choices. There are three ways to go about it:

  1. Use enums when you are able to provide hard-coded choices
  2. Use choice_provider when the choices are dynamic (e.g., customer-specific)
  3. Use a Pydantic model when the parameter follows a more complex structure

Enum Parameters

Specify a fixed set of choices that are known ahead of time, using an Enum class.

import enum

class ProductCategory(enum.Enum):
LAPTOPS = "laptops"
PERIPHERALS = "peripherals"
MONITORS = "monitors"

@p.tool
async def get_products(
context: p.ToolContext,
category: ProductCategory,
) -> p.ToolResult:
# your code here
return p.ToolResult(returned_data)

Choice Provider

Dynamically offer a set of choices based on the current execution context.

async def get_last_order_ids(context: p.ToolContext) -> list[str]:
return await load_last_order_ids_from_db(customer_id=context.customer_id)

@p.tool
async def load_order(
context: p.ToolContext,
order_id: Annotated[Optional[str], p.ToolParameterOptions(
choice_provider=get_last_order_ids,
)],
) -> p.ToolResult:
# your code here
return p.ToolResult({...})

Pydantic Model

Use a Pydantic model to define a complex structure for the parameter, which can include validation and constraints.

from pydantic import BaseModel

class ProductSearchQuery(BaseModel):
category: str
price_range: tuple[float, float]

@p.tool
async def search_products(
context: p.ToolContext,
query: ProductSearchQuery,
) -> p.ToolResult:
# your code here