Agent Studio
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

  1. Python functions - Flexible but requires deployment
  2. Declarative JSON actions - No-code tool creation
  3. JavaScript/Lua scripting - Flexible but complex security model
  4. 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

ActionDescription
context.setSet values in call context
context.getRead values from context
webhookCall external API endpoint
handoffTransfer to another agent
respondGenerate speech response
flag.setSet workflow flags
conditionalBranch based on condition
validateValidate 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)

On this page