Skip to content
AGNT

Backend · Core modules

Tool executor.

The central tool dispatcher. Registers 16 LLM-callable tools into a single registry and runs them with a strict safety envelope: input key stripping, length clamping, and a 5-second timeout. File: agnt-backend/app/core/tool_executor.py.

What it is

When the Anthropic Messages API returns a tool_use block in its response, tool_executor.execute is the only function allowed to turn that into an actual side effect. It holds the registry, applies the safety rules, and formats the result back into the tool_result shape Anthropic expects.

Registered tools

ToolModulePurpose
venue_searchtools/venue_search.pyIntent-based venue search across 35 patterns.
create_bookingtools/booking.pyCreate a booking. Routes through ClawPulse when the venue is on the network.
check_bookingtools/booking.pyLook up the current status of a booking the user already made.
cancel_bookingtools/booking.pyCancel an existing booking and fire the associated state transition.
get_user_bookingstools/booking.pyList the user's bookings past and upcoming.
rate_bookingtools/booking.pySubmit a post-visit rating with comment.
set_remindertools/reminders.pySchedule a reminder delivered by the deliver_reminders scheduler job.
save_preferencetools/crm_write.pyPersist a taste / dietary / lifestyle preference to UserMemory.
dupe_search_by_texttools/dupe_search.pyFind cheaper alternatives across Shopee, Tokopedia, Lazada.
set_price_watchertools/dupe_search.pyWatch a product and alert on price drops.
calorie_scan_by_texttools/calorie_scan.pyResolve a text meal description to macros via Nutritionix.
get_nutrition_summarytools/calorie_scan.pyDaily calorie + macro summary for the user's food diary.
get_ridetools/transport.pyUber / Grab / GoJek deep links for a route.
get_delivery_quotetools/transport.pyLalamove delivery quote (read-only).
book_lalamove_deliverytools/transport.pyConfirm a Lalamove delivery. Requires explicit user confirmation.
generate_cardtools/generate_card.pyGenerate a shareable interaction card with a public URL.

A 17th optional tool, knowledge_lookup, is registered only when KNOWLEDGE_LOOKUP_URL is set.

Registration

Tools are registered at import time. The registry is a module-level dict keyed by tool name. Optional tools conditionally append their schema to TOOL_SCHEMAS so the LLM only ever sees the tools that are actually callable.

pythonapp/core/tool_executor.py — registration
from app.core.tools import TOOL_REGISTRY
from app.core.tools import venue_search as _vs
from app.core.tools import booking as _bk
from app.core.tools import reminders as _rm
from app.core.tools import crm_write as _crm
from app.core.tools import dupe_search as _ds
from app.core.tools import calorie_scan as _cal
from app.core.tools import transport as _tr
from app.core.tools import generate_card as _gc

# Register tool functions
TOOL_REGISTRY["venue_search"] = _vs.venue_search
TOOL_REGISTRY["create_booking"] = _bk.create_booking
TOOL_REGISTRY["check_booking"] = _bk.check_booking
# ...

# Optional public-knowledge tool — only registered when configured.
if _settings.KNOWLEDGE_LOOKUP_URL:
    TOOL_REGISTRY["knowledge_lookup"] = _kl.knowledge_lookup

The execute function

The execution path is intentionally boring. Safe input gets prepared, a 5-second timeout is applied, exceptions are caught and returned as strings, and timeouts turn into a specific error message.

pythonapp/core/tool_executor.py — execute
async def execute(tool_call: ToolCall, user_id: uuid.UUID, db: AsyncSession) -> str:
    fn = TOOL_REGISTRY.get(tool_call.name)
    if not fn:
        return f"Error: Unknown tool '{tool_call.name}'"

    try:
        # Strip reserved keys from LLM-generated input to prevent injection
        safe_input = {k: v for k, v in tool_call.input.items() if k not in ("user_id", "db")}

        # Validate tool parameters: constrain string lengths to prevent DoS
        _MAX_PARAM_LENGTH = 1000
        for k, v in safe_input.items():
            if isinstance(v, str) and len(v) > _MAX_PARAM_LENGTH:
                safe_input[k] = v[:_MAX_PARAM_LENGTH]
            elif isinstance(v, list) and len(v) > 50:
                safe_input[k] = v[:50]

        result = await asyncio.wait_for(
            fn(**safe_input, user_id=str(user_id), db=db),
            timeout=5.0,
        )
        return result
    except asyncio.TimeoutError:
        return f"Error: Tool '{tool_call.name}' timed out after 5 seconds"
    except Exception as e:
        return f"Error executing {tool_call.name}: {e}"

Safety rules

  1. Reserved keys are strippeduser_id and dbare passed by the executor, not by the LLM. If the LLM tried to inject them, they're silently discarded.
  2. Strings clamp to 1000 chars to prevent token-based DoS on downstream APIs.
  3. Lists clamp to 50 items for the same reason.
  4. 5 second timeout via asyncio.wait_for. Tools that need longer must be split into smaller steps or moved to a background job.
  5. Exceptions are swallowed into a readable error string instead of bubbling back to the LLM. This keeps the conversation alive and lets the model recover.

Running a batch

The LLM gateway calls run(tool_calls, user_id, db)which just sequentially executes each tool and shapes the results into Anthropic's tool_result content blocks.

pythonapp/core/tool_executor.py — run
async def run(tool_calls: list[ToolCall], user_id: uuid.UUID, db: AsyncSession) -> list[dict]:
    """Execute all tool calls and return Anthropic-format tool_result content blocks."""
    results = []
    for tc in tool_calls:
        output = await execute(tc, user_id, db)
        results.append({
            "type": "tool_result",
            "tool_use_id": tc.id,
            "content": output,
        })
    return results

Adding a new tool

  1. Write the function in app/core/tools/your_tool.py. Signature: async def your_tool(**kwargs, user_id: str, db: AsyncSession) -> str
  2. Add its schema to TOOL_SCHEMAS in app/core/tools/__init__.py.
  3. Register the function in tool_executor.py under a unique name.
  4. Write a test in tests/ that covers the happy path and at least one failure path.

Related