Adr
ADR-003: Tool Action System
Declarative JSON-based tool execution
ADR-003: Tool Action System
Status
Accepted
Context
Voice agents need to execute tools (functions) based on user requests. In the original codebase, tools contained hardcoded business logic, making it impossible to add new tools without code changes.
We need a system where:
- Tools can be created/modified via API without deployment
- Business logic is externalized to webhooks
- Common operations (context updates, handoffs) are built-in
- Tools are tenant-specific and reusable across agents
Options Considered
- Python functions - Flexible but requires deployment
- Declarative JSON actions - No-code tool creation
- JavaScript/Lua scripting - Flexible but complex security model
- Workflow engine (Temporal) - Overkill for tool execution
Decision
We will use a declarative JSON-based action system:
{
"name": "save_meal",
"description": "Log a meal the user consumed",
"parameters": [
{ "name": "meal_type", "type": "string", "enum": ["breakfast", "lunch", "dinner"] },
{ "name": "dishes", "type": "array", "required": true }
],
"actions": [
{ "type": "validate", "rules": [...] },
{ "type": "context.set", "data": { "logged_meals[+]": "{{params}}" } },
{ "type": "webhook", "endpoint": "meal.log", "body": { "user_id": "{{user.id}}", "meal": "{{params}}" } }
],
"on_success": [{ "type": "respond", "message": "I've logged your {{params.meal_type}}!" }],
"on_failure": [{ "type": "respond", "message": "Sorry, I couldn't log that meal." }]
}Supported Action Types
| Action | Description |
|---|---|
context.set | Set values in call context |
context.get | Read values from context |
webhook | Call external API endpoint |
handoff | Transfer to another agent |
respond | Generate speech response |
flag.set | Set workflow flags |
conditional | Branch based on condition |
validate | Validate parameters |
Variable Resolution
Templates use {{path}} syntax:
{{params.meal_type}}- Tool parameters{{user.id}}- User context{{workflow.logged_meals}}- Shared workflow state{{agents.meal.last_dish}}- Agent-specific state
Array append uses [+]:
logged_meals[+]appends to array
Tool Executor Implementation
class ToolExecutor:
def __init__(self, context: CallContext, webhook_client: WebhookClient):
self.context = context
self.webhook = webhook_client
self.action_handlers = {
"context.set": self._handle_context_set,
"context.get": self._handle_context_get,
"webhook": self._handle_webhook,
"handoff": self._handle_handoff,
"respond": self._handle_respond,
"conditional": self._handle_conditional,
"flag.set": self._handle_flag_set,
"validate": self._handle_validate,
}
async def execute(self, tool: ToolConfig, params: dict) -> ToolResult:
for action in tool.actions:
handler = self.action_handlers[action["type"]]
result = await handler(action, params)
if not result.success:
return await self._handle_failure(tool, result)
return await self._handle_success(tool)Consequences
Positive
- Tools created/modified via API without deployment
- No hardcoded business logic
- Easy to understand and debug
- Consistent execution model
- Webhook integration for external systems
Negative
- Limited to predefined action types
- Complex logic requires multiple webhooks
- No loops or recursion (by design)
- Template syntax has learning curve
Neutral
- Actions execute sequentially (not parallel)
- Webhook failures stop execution (configurable)