Skip to content
3 min read · 529 words

The Tool System

Tools are capability grants, not code plugins: a comma-separated allow-list on the agent decides which endpoints may act. Enforcement is a string-membership check that guards every tool-bearing route.

The four canonical tools

Tool IDGrantsEnforced atEffect when missing
email_analyzeremail analysisPOST .../email/analyze, POST .../workflows/email403 — both endpoints refuse
task_creatorpersisting tasks from action itemsinside analyze + workflow stage 2stage silently skipped (no error)
document_searchvector search & RAG answersPOST .../documents/search, POST .../documents/ask403
reply_generatorthe dedicated Reply Agentworkflow stage 3fallback to the analysis's suggested_reply

Note the two enforcement styles: gate tools (email_analyzer, document_search) reject the request outright; enhancement tools (task_creator, reply_generator) degrade behavior gracefully.

enabled_tools is parsed by splitting on commas and trimming whitespace — "email_analyzer, task_creator" works, but "Email_Analyzer" or "email-analyzer" silently grants nothing. There is no validation at agent creation; a typo is discovered as a 403 later. The create-agent UI avoids this by using checkboxes bound to the canonical IDs.

Mechanics

Storage — one Text column on the agent:

enabled_tools = "email_analyzer,task_creator,document_search"

Enforcement — duplicated in both routers (acknowledged debt):

python
def agent_has_tool(agent: models.Agent, tool_name: str) -> bool:
    if not agent.enabled_tools:
        return False
    tools = [tool.strip() for tool in agent.enabled_tools.split(",")]
    return tool_name in tools

Gate usage:

python
if not agent_has_tool(agent, "email_analyzer"):
    raise HTTPException(status_code=403,
        detail="This agent does not have email_analyzer tool enabled")

Why a string and not a join table: Design Decisions.

Tools and observability

Every tool execution logs a ToolCall whose tool_name sometimes matches the tool ID (document_search) and sometimes names the pipeline stage (multi_agent_reply_agent). The full mapping lives in Observability — don't assume ToolCall names equal tool IDs.

Recipes

Agent purposeenabled_tools
Pure chat assistant"" (chat needs no tools)
Email triage onlyemail_analyzer
Email triage that files workemail_analyzer,task_creator
Full email operationsemail_analyzer,task_creator,reply_generator
Knowledge-base Q&Adocument_search
Everythingemail_analyzer,task_creator,document_search,reply_generator

Adding a fifth tool

The complete checklist (worked example: Add a Tool):

  1. Choose a snake_case ID; document it in this table and the Glossary.
  2. Gate the new endpoint(s) with agent_has_tool(agent, "<id>")403.
  3. Log a ToolCall per execution — success and failure — per the contract.
  4. Add the tool to toolOptions in CreateAgentForm (id, label, description) so it is grantable from the UI.
  5. Decide gate vs. enhancement semantics deliberately, and document which.

Security framing

Tool gating is the platform's only authorization layer today — it controls what an agent may do, but nothing controls who may use the agent (Security). Treat enabled_tools as a blast-radius limiter: an agent without task_creator cannot write tasks no matter what an email says.