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 ID | Grants | Enforced at | Effect when missing |
|---|---|---|---|
email_analyzer | email analysis | POST .../email/analyze, POST .../workflows/email | 403 — both endpoints refuse |
task_creator | persisting tasks from action items | inside analyze + workflow stage 2 | stage silently skipped (no error) |
document_search | vector search & RAG answers | POST .../documents/search, POST .../documents/ask | 403 |
reply_generator | the dedicated Reply Agent | workflow stage 3 | fallback 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):
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 toolsGate usage:
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 purpose | enabled_tools |
|---|---|
| Pure chat assistant | "" (chat needs no tools) |
| Email triage only | email_analyzer |
| Email triage that files work | email_analyzer,task_creator |
| Full email operations | email_analyzer,task_creator,reply_generator |
| Knowledge-base Q&A | document_search |
| Everything | email_analyzer,task_creator,document_search,reply_generator |
Adding a fifth tool
The complete checklist (worked example: Add a Tool):
- Choose a snake_case ID; document it in this table and the Glossary.
- Gate the new endpoint(s) with
agent_has_tool(agent, "<id>")→403. - Log a
ToolCallper execution — success and failure — per the contract. - Add the tool to
toolOptionsinCreateAgentForm(id, label, description) so it is grantable from the UI. - 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.
Related pages
- Agents Overview — the entity that holds the list
- Email Workflow — three tools interacting
- Guides: Add a Tool