Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.xpander.ai/llms.txt

Use this file to discover all available pages before exploring further.

@on_tool_before, @on_tool_after, and @on_tool_error register hooks that fire around tool invocations: useful for logging, validation, caching, alerting, or analytics. The hooks run during every Tool.ainvoke (which means around every framework-driven tool call too).
from xpander_sdk import on_tool_before, on_tool_after, on_tool_error

@on_tool_before
async def log_invoke(tool, payload, payload_extension, tool_call_id, agent_version):
    print(f"→ {tool.name} {payload}")

@on_tool_after
async def log_success(tool, payload, payload_extension, tool_call_id, agent_version, result):
    print(f"✓ {tool.name}{result}")

@on_tool_error
async def log_error(tool, payload, payload_extension, tool_call_id, agent_version, error):
    print(f"✗ {tool.name} failed: {error}")
You can register multiple hooks of each type: they fire in registration order. Sync and async functions are both supported.

Required signatures

DecoratorParametersNotes
@on_tool_beforetool, payload, payload_extension, tool_call_id, agent_versionFires before the tool runs.
@on_tool_aftertool, payload, payload_extension, tool_call_id, agent_version, resultFires after a successful invocation. result is the tool’s response.
@on_tool_errortool, payload, payload_extension, tool_call_id, agent_version, errorFires when the invocation raises. error is the exception.
ParameterTypeDescription
toolToolThe tool being invoked.
payloadAnyThe input payload after schema validation.
payload_extensiondict | NoneExtra fields that will be deep-merged into the payload.
tool_call_idstr | NoneCorrelation id matching the LLM tool-call.
agent_versionstr | NoneAgent version pinned for the call.
result (after only)AnyThe tool’s response.
error (error only)ExceptionThe raised exception.

Decorator forms

@on_tool_before
def fn(...): ...

@on_tool_before(configuration=config)
def fn(...): ...
The same form applies to @on_tool_after and @on_tool_error. The optional configuration parameter is reserved for future use; hooks don’t currently read it.

Examples

Log every tool call

import time
import contextvars

start_time = contextvars.ContextVar("start_time")

@on_tool_before
def begin(tool, payload, *_):
    start_time.set(time.perf_counter())
    print(f"[{tool.name}] start")

@on_tool_after
def done(tool, payload, *_, result=None):
    elapsed = time.perf_counter() - start_time.get()
    print(f"[{tool.name}] done in {elapsed*1000:.0f}ms")

@on_tool_error
def failed(tool, payload, *_, error=None):
    elapsed = time.perf_counter() - start_time.get()
    print(f"[{tool.name}] failed in {elapsed*1000:.0f}ms: {error}")

Block invocations matching a payload pattern

Hooks can’t cancel invocations directly (the runtime catches their exceptions and continues), but you can mutate state or short-circuit by raising a guard before the call:
@on_tool_before
def block_test_payloads(tool, payload, *_):
    if isinstance(payload, dict) and payload.get("test_mode"):
        raise RuntimeError("test_mode payloads are blocked")
Hooks that raise are caught by the runtime: the exception is logged but doesn’t propagate to the caller. To genuinely block a tool, validate at the framework layer or in @register_tool function bodies.

Cache successful results

cache: dict = {}

@on_tool_after
def cache_result(tool, payload, _ext, _id, _ver, result):
    if hasattr(payload, "items"):
        key = (tool.id, frozenset(payload.items()))
        cache[key] = result

Alert on failures

@on_tool_error
async def alert(tool, payload, _ext, _id, _ver, error):
    await pagerduty.fire(f"tool {tool.id} failed: {error}")

Notes

  • Hooks fire once per tool invocation, regardless of whether the tool is local or remote.
  • Exceptions in hooks are caught and logged via loguru: they never crash the invocation pipeline.
  • Hooks are class-level on ToolHooksRegistry (process-wide singleton). There’s no scoping to a specific repository.
  • The auto-emitted ToolCallRequest / ToolCallResult events on task.aevents() are independent of these hooks. The hooks fire even when report_activity=False in Tool.ainvoke.