# Consolidate Tools

> Merge thin, overlapping tools into one task-shaped tool that returns only the fields the task reads, and describe when to use it - so consumers pick right the first time and stop chaining calls.

- kind: technique
- category: simplifying-method-calls
- aliases: Merge Tools, Task-Shaped Tools
- canonical: https://aipatterns.dev/refactoring/techniques/consolidate-tools
- inspired-by: https://www.anthropic.com/engineering/writing-tools-for-agents

## Problem

An agent-facing surface grew one tool per upstream endpoint. Answering one real question now takes a chain of calls - user, then transactions, then notes - each returning provider-shaped payloads with fields nobody reads. Consumers guess between overlapping tools, misfire, and burn tokens filtering noise down to the three values they wanted.

## Solution

Group the tools by the tasks consumers actually perform, and replace each group with one task-shaped tool: it calls the upstream endpoints internally, joins what the task needs, and returns only the high-signal fields - names and values, not cursors and metadata. Write its description as a trigger rule: when to use it, and when not.

## Before

```ts
// One tool per endpoint; the caller chains and filters.
const tools = {
  getUser: (userId: string) =>
    fetch(`${crm}/users/${userId}`).then((r) => r.json()),
  listTransactions: (userId: string) =>
    fetch(`${crm}/users/${userId}/transactions`).then((r) => r.json()),
  listNotes: (userId: string) =>
    fetch(`${crm}/users/${userId}/notes`).then((r) => r.json()),
};

// Three calls, three payloads, a manual join:
const user = await tools.getUser("u-9");
const txns = await tools.listTransactions("u-9");
const lastPurchase = (await tools.listNotes("u-9")).items[0]?.body;
```

## After

```ts
// One tool per task: it chains internally, the caller gets the answer.
const tools = {
  // USE for "who is this customer / what did they last buy" questions.
  // NOT for editing customer records - use update_customer for that.
  getCustomerContext: async (userId: string) => {
    const [user, txns, notes] = await Promise.all([
      fetch(`${crm}/users/${userId}`).then((r) => r.json()),
      fetch(`${crm}/users/${userId}/transactions`).then((r) => r.json()),
      fetch(`${crm}/users/${userId}/notes`).then((r) => r.json()),
    ]);
    return {
      name: user.display_name,
      email: user.primary_email,
      lastPurchaseCents: txns.items[0]?.amount_cents ?? 0,
      latestNote: notes.items[0]?.body ?? "",
    };
  },
};

// One call, the answer, nothing to filter:
const context = await tools.getCustomerContext("u-9");
```

## Why

A consumer that dispatches on descriptions and pays per token needs the surface shaped for its tasks, not for the upstream's org chart. Task-shaped tools cut calls-per-task, cut response tokens to what is read, and make the right choice guessable from the description alone - which is what turns tool errors into rare events instead of a retry genre. The same instinct as [Hide Delegate](https://aipatterns.dev/refactoring/techniques/hide-delegate.md), applied across a network instead of a field access.

## Steps

1. Inventory the tools and the tasks: for each real consumer task, list which tools it chains today. Groups of two or more tools serving one task are your candidates.
2. Design the task-shaped tool first as its return type: the fields the task reads, named for the domain, defaults and pagination decided up front. If two candidate tasks need the same shape, they are one task.
3. Implement the consolidation so it calls the same upstream endpoints - no behavior change beyond the join, and errors from upstream surface with their origin intact.
4. Write the description as a trigger rule naming when to use and when not to, plus the one-line shape of the return. A description that says what it calls is a smell ([Thin Tool Wrapper](https://aipatterns.dev/refactoring/smells/thin-tool-wrapper.md)).
5. Switch every caller in the same change and delete the old tools. Overlapping tools left "for compatibility" re-create the guess the consolidation exists to remove.
6. Re-run the consumer's evaluation set: calls-per-task and output tokens should drop, task success should hold. Numbers or it did not happen.

## Watch out

- Do not merge tools with different permission or billing characteristics - a read-everything tool can become a privilege escalation, and a "one big tool" that does everything fails the distinct-purpose rule from the other side.
- Keep the upstream call semantics inside: consolidation must not turn three independent failures into one retried mega-call with surprise side effects.
- If a raw variant genuinely serves its own distinct task (debugging, export), leave it alone and say so in its description - the goal is one tool per task, not fewest tools.
- Consumers outside your control may still call removed tools; deprecate with an execution error naming the replacement before you delete.

## Related

- [Thin Tool Wrapper](https://aipatterns.dev/refactoring/smells/thin-tool-wrapper.md): the smell this technique treats.
- [Hide Delegate](https://aipatterns.dev/refactoring/techniques/hide-delegate.md): the same move one layer down - hiding `a.getB().getC()` behind the object that owns the chain.
- [Remove Middle Man](https://aipatterns.dev/refactoring/techniques/remove-middle-man.md): the counterweight - consolidate when the consumer needs the task, expose the delegate when the consumer needs the control.

## Agent checklist

- [ ] Groups were derived from real consumer tasks, not from endpoint adjacency.
- [ ] The return type contains only fields at least one task reads.
- [ ] The description is a trigger rule: when to use, when not, one-line return shape.
- [ ] Every caller moved and the old tools deleted in the same change.
- [ ] Calls-per-task and output tokens were measured before and after.
