Tools
A tool is an async function the model can call. Decorate it with @tool and hand it to
the LLM. The first parameter is a ToolContext; the rest are the arguments the model
fills in.
from pyrula.agents.decorators.tool import toolfrom pyrula.agents.context.tool_context import ToolContext
@tool(retry=0, timeout=10.0)async def search(ctx: ToolContext, query: str) -> str: return await backend.search(query)The signature and docstring become the tool schema the model sees, so type your
parameters and say what the tool does. Pass schema= to override it, or name= /
description= to set them directly.
Options
Section titled “Options”| Option | What it does |
|---|---|
retry | Times to retry the tool on failure. |
timeout | Per-call wall-clock cap. |
cache_ttl | Cache the result by arguments for this many seconds. |
writes | Marks the tool as having side effects (affects replay handling). |
name / description / schema | Override what the model sees. |
Idempotency
Section titled “Idempotency”A tool can run again on recovery if it was interrupted before its result was recorded.
For anything with a side effect, make it idempotent. ctx.idempotency_key is stable
across retries of the same call, so use it as a dedup key:
@toolasync def charge(ctx: ToolContext, amount_cents: int) -> str: return await payments.charge(amount_cents, key=ctx.idempotency_key)ctx.cache gives you a small get/set store scoped to the turn for cheaper dedup or
memoization within a single run.
This is the same constraint that applies to durable steps. See Determinism for the why.