Skip to content

Tool Handlers (Default & Replace)

Applies to: 4.0.8.1+

This page explains how default handlers work and how to replace them.

1) Two critical handlers

Tool Loop has two main extension points:

  1. plan_analysis_handler: decision stage (next_action + execution_commands)
  2. tool_execution_handler: execution stage (run commands and return records)

2) Default behavior

2.1 Default Plan Handler

Default logic:

  1. builds a standalone ModelRequest for planning
  2. constrains output to next_action + execution_commands[]
  3. listens on instant; short-circuits if next_action=response

Recommended decision shape:

python
{
    "next_action": "execute" | "response",
    "execution_commands": [
        {
            "purpose": str,
            "tool_name": str,
            "tool_kwargs": dict,
            "todo_suggestion": str,
        }
    ],
}

2.2 Default Execution Handler

Default logic:

  1. runs tool calls with tool.loop.concurrency
  2. calls async_call_tool(tool_name, tool_kwargs) per command
  3. emits normalized records (success/result/error)

3) Replace handlers at Agent level

python
from agently import Agently

agent = Agently.create_agent()

async def custom_plan_handler(
    prompt,
    settings,
    tool_list,
    done_plans,
    last_round_records,
    round_index,
    max_rounds,
    agent_name,
):
    if round_index > 0:
        return {"next_action": "response", "execution_commands": []}
    return {
        "next_action": "execute",
        "execution_commands": [
            {
                "purpose": "calc",
                "tool_name": "add",
                "tool_kwargs": {"a": 1, "b": 2},
                "todo_suggestion": "respond after this",
            }
        ],
    }


async def custom_execution_handler(
    tool_commands,
    settings,
    async_call_tool,
    done_plans,
    round_index,
    concurrency,
    agent_name,
):
    records = []
    for command in tool_commands:
        result = await async_call_tool(command["tool_name"], command.get("tool_kwargs", {}))
        records.append(
            {
                "purpose": command.get("purpose", ""),
                "tool_name": command.get("tool_name", ""),
                "kwargs": command.get("tool_kwargs", {}),
                "todo_suggestion": command.get("todo_suggestion", ""),
                "success": True,
                "result": result,
                "error": "",
            }
        )
    return records


agent.register_tool_plan_analysis_handler(custom_plan_handler)
agent.register_tool_execution_handler(custom_execution_handler)

Reset to defaults:

python
agent.register_tool_plan_analysis_handler(None)
agent.register_tool_execution_handler(None)

4) Replace at Core level (global)

python
from agently import Agently

Agently.tool.register_plan_analysis_handler(custom_plan_handler)
Agently.tool.register_tool_execution_handler(custom_execution_handler)

5) Override per call

You can also pass handlers for one call:

  • Tool.async_plan_and_execute(..., plan_analysis_handler=..., tool_execution_handler=...)
  • Tool.async_generate_tool_command(..., plan_analysis_handler=...)

6) Design recommendations

  • keep plan handler decision-only
  • keep execution handler execution-only
  • always return auditable records (success/result/error)
  • prefer execution_commands as canonical field