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": "api_call", "url": "https://api.example.com/meals", "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 |
context.delete | Delete values from context |
api_call | Call external API endpoint with retry support |
handoff | Transfer to another agent |
respond | Generate speech response |
flag.set | Set workflow flags |
flag.clear | Clear workflow flags |
conditional | Branch based on condition |
validate | Validate parameters |
transform | Transform data (map, filter, reduce) |
log | Log messages for debugging |
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):
self.context = context
self.action_handlers = {
"context.set": self._handle_context_set,
"context.get": self._handle_context_get,
"context.delete": self._handle_context_delete,
"api_call": self._handle_api_call,
"handoff": self._handle_handoff,
"respond": self._handle_respond,
"conditional": self._handle_conditional,
"flag.set": self._handle_flag_set,
"flag.clear": self._handle_flag_clear,
"validate": self._handle_validate,
"transform": self._handle_transform,
"log": self._handle_log,
}
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)API Call Action
The api_call action makes HTTP requests to external APIs with retry support:
{
"type": "api_call",
"url": "https://api.example.com/v1/meals",
"method": "POST",
"body": {
"userId": "{{user.id}}",
"mealType": "{{params.meal_type}}",
"dishes": "{{params.dishes}}"
},
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer {{user.auth_token}}"
},
"timeout": 15,
"retry_count": 3,
"retry_delay": 0.5,
"on_error": "fail",
"response_path": "workflow.api_response"
}Note: API URLs are hardcoded in tool definitions. Authentication tokens are passed via
user_contextat call dispatch time and resolved using{{user.auth_token}}. See Backend Integration Guide for the full authentication flow.
| Field | Description | Default |
|---|---|---|
url | Full URL or template | Required |
method | HTTP method | POST |
body | Request body (JSON) | {} |
headers | HTTP headers | {} |
timeout | Request timeout (seconds) | 30 |
retry_count | Number of retry attempts | 3 |
retry_delay | Initial delay between retries (exponential backoff) | 0.5 |
on_error | "fail" or "continue" | "fail" |
response_path | Context path to store response | Optional |
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)