Tool Groups: The Missing Abstraction Between Agents and Tools
How do we bridge the gap between Agents and Tools?
Most Agent libraries define two key abstractions: Agents and Tools. The task implementations involve an interplay between them: Agents call Tools and delegate to other Agents. However, there is a semantic gap between the Agent and Tools abstractions. Is an Agent simply an orchestrator over these tools? Can a Tool call another Agent’s Tools? How are Tools related to Agent delegation?
In this article, we bridge this gap by analyzing key components of Agent design and introducing Toolgroups as an intermediate abstraction. This enables us to understand the capabilities of different Agent-Tool architectures and also select an Agent architecture according to the task at hand, avoiding unnecessary complexity.
To illustrate the ideas, we will use this example of multiple agents that handle emails, slack and calendar tasks, with handoffs among them.
These will help execute tasks like:
Summarize my latest 10 emails, and send the summary to me as a DM on Slack
Read the latest messages on the #general, #random, and #todo channels in Slack and send a summary to my email
Add a meeting with Alex to my calendar for tomorrow at 2pm, send Alex a DM in Slack with the link to the meeting
The below implementation is not specific to any Agent library; it uses the standard abstraction that many (most?) agent libraries provide.
Agent as entities
No central orchestrator — Each agent responsible for orchestration
Delegation tools (Handoff ) to orchestrate between agents
google_agent = Agent(
name="Google Agent",
instructions="You are a helpful assistant that can assist using tools"
" to manage a Google account, contacts, and inbox.",
handoff_description="An agent equipped with Google tools",
model="gpt-4o",
tools=google_tools,
)
slack_agent = Agent(
name="Slack agent",
instructions="You are a helpful assistant that can assist using tools"
" to interact with Slack."
" You have tools to manage channels and send DMs.",
handoff_description="An agent equipped with Slack tools",
model="gpt-4o",
tools=slack_tools,
)
conversation_agent = Agent(
name="Conversation Agent",
instructions="You are a helpful assistant that can help with everyday"
" tasks. You can handoff to another agent with access to"
" Gmail tools if needed. You can also handoff to an agent"
" with Slack tools if needed. Handoff to the appropriate"
" agent based on the services required.",
model="gpt-4o",
handoffs=[google_agent, slack_agent],
)
google_agent.handoffs.extend([conversation_agent, slack_agent])
slack_agent.handoffs.extend([conversation_agent, google_agent])
From the code, we observe that each of these Agents correspond to a set of Tools, together with some category-specific instructions. So, let us start from Tools and build all the way up to Agents.
We first introduce a simpler, intermediate abstraction, a Toolgroup. A Toolgroup is a collection of tools from a domain/category. Is the toolgroup abstraction enough to implement equivalent functionality as defined above? Let us see.
For the task above, we define tool groups, one for each email, slack, calendar. Each tool group contains all the tools defined for each domain/category. Tasks are implemented by coordination (sequencing tools) among tool groups — we define a single orchestrator over the groups that takes care of all the coordination.
toolgroup_prompt = """
Implement task instructions involving one or more: email sending, summarizing, slack messaging, by calling appropriate tools.
Tool group defined for each sub-task.
- for reading, sending emails, using Email.* toolgroup
- for summarizing, use Summary.* toolgroup
- for slack actions, use Slack.* toolgroup
"""
@mcp.toolgroup()
class Email:
def send(..): ..
def read(..): ..
@mcp.toolgroup()
class Slack:
def sendDM(..): ..
def read_channel(..): ..
@mcp.toolgroup()
class Summarizer:
...
At this moment, the MCP protocol does not define toolgroups, but suppose we have this feature implemented in a higher-level library. Additionally, a toolgroup may provide LLM-based routing: route an instruction to one of the internal tools.
Toolgroups allow hierarchical selection of tools for each task. Narrow down first to a tool group, then select a tool. If you have hundreds of tools, this is more efficient and less error-prone.
Revisit Example
Task: Summarize my latest 10 emails, and send the summary to me as a DM on Slack
A coarse implementation of this task using above Orchestrator+Toolgroups (O+T) is here:
emails = Email.read(account_id, n=10, order_by=’date’)
summary = Summarizer.run(emails.to_json())
Slack.sendDM(acount_id, summary)
Pros Cons of each paradigm?
The Toolgroup appears closer to functional programming
Agents closer to OO programming with state
Agents need special handoff operator (essentially, a router)
Multi-level router required for Toolgroups (one picks the group, other picks the tool)
Central vs Distributed orchestrator (global vs local orchestration rules)
What is an Orchestrator?
Before we proceed, it is useful to unravel the elephant in the room, the heavily loaded term - Orchestrator. What does an orchestrator do? Is it same as a router or more?
Based on the usual cookbook examples, here are the different use cases. An orchestrator may
Pick the next agent to run among a team of agents
Pick the next tool to call among a group of tools
Decompose a task and call tools in appropriate sequence to accomplish the task
Aggregate results from a set of conversation checkpoints
Clearly, we’ve overloaded the orchestrator with a diverse set of responsibilities. In contrast, a Router is usually tasked with picking one among a set of items (a choice operator). Also, orchestrator is a mouthful term.
Because choosing is far more frequently executed task (than say sequential ordering), I prefer to simply use Router for the entity that is able to do all the tasks defined above. Also ok with O-Router (an orchestrating router), if you will.
Now let’s go back to bridging the gap between Agent and Tools. Here’s our first hypothesis:
Is an Agent equivalent to Orchestrator+Toolgroups ?
Many tasks can be implemented by either Agent or O+T. However, they are not equivalent. There is an inherent asymmetry here.
Asymmetry between orchestrator and tools: Can a Tool-call-Tool ?
One issue with Orchestrator+Toolgroup (O+T) abstraction is that it is asymmetric between the O and T. The orchestrator is certainly able to decide which Tool to call (via natural language). What if a Tool, in turn, needs to decide among one or more tools? It cannot — it needs a Tool router for orchestration. One possibility: each Toolgroup also includes a Tool router, which enables local tool-calls-tool functionality inside the Toolgroup.
This makes the O+T abstraction less powerful than the multi-Agent abstraction. In the latter, each Agent has a delegation router in-built. So, an Agent’s Toolgroup can call another Agent’s Toolgroup via delegation. That’s not exactly same as one Tool calling another directly, but is quite close.
With these observations, we come to our next hypothesis:
Is Agent = a Toolgroup + Routers?
This seems plausible, with a few notes:
Each Agent owns a Toolgroup together with two routers
an Intra-group router — decides which Tool in the group should be called
an Inter-group or Inter-agent router. This router may be central (O in O+T) or distributed, as the agent delegation router.
Viewing an agent as a composition of a toolgroup and two routers, helps decouple the role of agents from mere tool-routing. Also, if you've been struggling like me to understand the "semantics" of what these agent libraries offer, I suggest take a moment of reflection here. How well do the semantics of library-of-your-choice map to this toolgroup+routers abstraction? Would love to have your feedback.
Nested tool calls
One remaining tricky scenario is enabling Nested tool calls between disjoint sets of tools
Tool1 from Toolgroup1 needs to call Tool2 from Toolgroup2
This appears to be tricky with either abstraction. Tool1 cannot call Tool2 directly. One way to do this is to transform the nested calls into a sequence of calls, which are then handled at the higher level using Inter-group routers.
So, as we move up the abstractions, calling one Tool from another requires navigating through a hierarchy of routers. It does not remain as simple as calling one function from another. I think this needs a more powerful inter-agent communication primitives than allowed by the popular libraries. More on this in another article.
How does this Agent architectural analysis help?
As we noted earlier, most agent-builder libraries use Agent and Tools as fundamental primitives. However, there is a semantic gap between Agent and Tools abstractions, which appears as soon as we try decomposing our task into these primitives. Unfortunately, none of the agent libraries clarify this semantic dilemma for us. Even worse, some add a few more semantically unclear primitives for us to "vibe-code" through. 💁♂️
In this article, we try to understand and deconstruct the relationship between agent and tools from first principles. Bridge this gap by analyzing key components of Agent design and introducing Toolgroups as an intermediate abstraction. This enables us to understand Tool call patterns among Agents as well as tune a (multi-) Agent instance to the task at hand and avoid unnecessary complexity.
Here are some Agent architecture patterns, ordered by complexity:
In the simplest version, a Single orchestrator Agent picks from (routes to) a single Tool group
More complex: Single Orchestrator Agent routes to multiple Toolgroups
More complex: Multiple agents, each with a coarse router. Each Agent has both a Toolgroup and an inter-Agent router (delegator).
More complex: Multiple agents, each with a fine-grained router. Each Agent can route to both internal and external Tools seamlessly.
Revisiting the tasks defined at the beginning of this article, the second setup (single orchestrator with multiple Toolgroups) is good enough to implement them. Implementing them via an inter-Agent delegator (as in the Agent-based implementation above) introduces unnecessary complexity for this task.
We hope that this writeup will help developers avoid complexity and choose a design pattern just enough for specific tasks. Unlike the article by Anthropic, where they distinguish architectures based on control flow or determinism (autonomy), the layers here focus on different levels of autonomous communication among Agents and Tools.
Thanks for reading! Did this article resonate with you? Give us your feedback or leave a comment to let us know what you think.
notes on tool-tool communication
https://grok.com/chat/8acfb1f3-5905-4e8b-9244-941404ac14bd