Rohan OS
> /blog/ai-tools-designing
13 min read— views

Beyond Tool Calling: Designing Tools AI Agents Can Actually Use

AI AgentsTool CallingLLMTypeScript

Lessons from auditing a 70-tool agent workflow: six practices for tool design, from a navigable inventory to explicit contracts and observability.

I've been working on AI agents for the past six months. One of the workflows I worked on involves an agent coordinating multiple steps, calling internal APIs, and interacting with external services. The workflow has around 70 tools available to it.

While working with this setup, I started looking more closely at the tools themselves. I found duplicate tools, tools that weren't being used, and tools consuming context without contributing much to the workflow.

That raised a question for me: Are we making our agents more capable by giving them more tools, or are we making it harder for them to choose and use the right ones?

Having more tools isn't automatically a problem. A complex workflow may genuinely need many capabilities. But when tools overlap, descriptions are unclear, or responses contain unnecessary information, the agent has more to work through before it can make its next decision.

This also got me thinking about tool design beyond selection and context. Once an agent chooses a tool, it still needs to provide valid inputs, understand the result, and decide what to do if something goes wrong.

A tool is more than a function that an LLM can call. It's part of the interface between the model and the systems it operates.

Here are six areas I'd focus on when designing tools for AI agents. Some address the structure of the tool ecosystem itself; others make individual tools more predictable, understandable, and easier to operate.


1. Design a Tool Inventory the Agent Can Navigate

When an application calls a function through explicit code, the developer determines which function runs. With tool calling, the model often has to select from the available tools based on their names, descriptions, schemas, and the current task.

That makes the tool inventory part of the agent's interface.

Consider a workflow with tools named searchRecords, findRecords, lookupRecords, and getRecords. These names might represent genuinely different operations, but they might also overlap. If their descriptions don't explain the distinction, the model has to infer which tool is appropriate.

The problem isn't the number of tools alone. It's the amount of ambiguity and unnecessary information the agent has to navigate.

When reviewing a tool inventory, I'd look for four things.

Duplicate capabilities. Do multiple tools perform essentially the same operation? If so, can they be consolidated without losing meaningful functionality?

Unused tools. Are tools still registered even though the current workflow no longer needs them? If they aren't required elsewhere, consider removing them from the relevant tool set.

Unclear boundaries. Can someone understand the difference between two similar tools just by reading their descriptions?

Unnecessary context. Do tool definitions or responses include information that doesn't help the agent choose an action or make its next decision?

Start by reviewing the available tools, grouping them by purpose, and identifying overlaps. Check actual usage where possible, but don't assume a tool is obsolete just because it hasn't been called recently. It might support a rare but important workflow.

If your framework supports dynamically selecting a smaller set of tools for each task, that can also help. A planning step may not need access to the same tools as an execution step.

2. Make Tool Contracts Explicit

Once an agent has selected a tool, it needs to know exactly what to send.

Vague names, incomplete descriptions, and permissive schemas make that harder. A tool should clearly communicate its purpose, required parameters, supported values, and constraints.

I'd start with three basics:

  • A specific name: Prefer a verb and a noun, such as update_user_preferences.
  • A useful description: Explain what the tool does, when to use it, and when not to use it.
  • A strict schema: Define required fields, types, allowed values, and validation rules.

For example:

import { z } from "zod";
 
const UpdatePreferencesSchema = z.object({
  userId: z.string().min(1),
  categories: z.array(z.string()).optional(),
  notificationsEnabled: z.boolean().optional(),
}).strict();
 
type UpdatePreferences = z.infer<
  typeof UpdatePreferencesSchema
>;

One detail worth knowing: Zod object schemas strip unknown keys by default. Adding .strict() makes unexpected fields a validation error instead.

Suppose the model sends notificationEnabled when the tool expects notificationsEnabled. Without strict validation, the unsupported field may be stripped, potentially leaving the operation with different inputs than the model intended. With strict validation, the tool can reject the request explicitly.

That doesn't mean every unknown field should always be rejected. The policy should match the tool's contract. But when unexpected input could change the meaning of an operation, silently accepting it is rarely helpful.

Descriptions matter just as much as schemas. If two tools have overlapping capabilities, explain the distinction rather than relying on the model to infer it from their names.

For example:

  • get_user_preferences: Retrieves the current preferences without changing them.
  • update_user_preferences: Changes the user's stored preferences.

These descriptions establish different responsibilities.

3. Make Results Unambiguous and Keep Them Small

A tool's response determines what the agent believes happened and what it does next. Two design decisions matter here: whether the result clearly communicates the outcome, and whether it returns only the information needed.

Distinguish empty results from failures

Imagine a tool that searches for records matching a set of criteria.

An empty result can mean the search completed successfully but found no matching records. It can also mean an upstream service failed and the application returned an empty array as a fallback.

Those outcomes should not look the same.

A consistent result type makes the distinction explicit:

type ToolResult<T = unknown> =
  | {
      success: true;
      data: T;
      summary: string;
    }
  | {
      success: false;
      error: string;
      summary: string;
    };
 
const ok = <T>(
  data: T,
  summary: string
): ToolResult<T> => ({
  success: true,
  data,
  summary,
});
 
const fail = (
  error: string,
  summary: string
): ToolResult<never> => ({
  success: false,
  error,
  summary,
});

A successful search with no matches could return:

return ok(
  [],
  "Search completed successfully. No matching records found."
);

A failed search could return:

return fail(
  "SEARCH_SERVICE_UNAVAILABLE",
  "Could not retrieve records because the search service is unavailable."
);

The exact response structure can vary by framework. What matters is that all tools follow a consistent convention.

A few other details are worth making explicit:

  • If an operation partially succeeds, report which parts succeeded and which failed.
  • If a write operation must complete before the workflow proceeds, verify the relevant outcome before reporting success.
  • If the operation is asynchronous, distinguish “accepted for processing” from “completed successfully.”

The last point is particularly important for operations that interact with external systems. An accepted request isn't necessarily a completed operation.

Return the information needed for the next decision

Now consider a search tool that finds 134 records. If the agent only needs the first 20 to determine the next step, returning the entire dataset adds unnecessary context.

A better response can include a bounded result set and a clear summary:

return ok(
  {
    items: result.items,
    total: result.total,
    limit: 20,
    offset: 0,
  },
  `Returned ${result.items.length} of ${result.total} results.`
);

This example assumes that result.items has already been limited by the underlying query. Ideally, pagination and field selection happen at the data source rather than fetching the entire dataset and trimming it afterward.

Useful practices include:

  • Set reasonable limits on the number of results.
  • Return only the fields needed for the next decision.
  • Support pagination when the full dataset isn't required immediately.
  • Cache suitable read operations when freshness requirements allow it.
  • Summarize large results while preserving access to the underlying data when necessary.

There's a balance to strike. Overly small responses can force the agent to make repeated calls; overly large ones can consume context and obscure useful information.

4. Make Execution Predictable, Safe, and Bounded

A tool may have a clear contract and still fail during execution. External APIs time out, services impose rate limits, permissions change, and network requests return unexpected errors.

The important question is how the tool handles these situations without leaving the agent uncertain about the outcome.

Retry only when it makes sense

Retries can help with transient failures, but not every error should be retried.

A timeout or rate limit may be recoverable. An invalid request or permission error usually requires a different action.

A simplified retry policy might look like this:

async function callWithRetry<T>(
  operation: () => Promise<T>,
  maxAttempts = 3
): Promise<T> {
  let lastError: unknown;
 
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      return await operation();
    } catch (error) {
      lastError = error;
 
      if (
        !isRetryable(error) ||
        attempt === maxAttempts - 1
      ) {
        throw error;
      }
 
      const delayMs =
        500 * 2 ** attempt + Math.random() * 200;
 
      await new Promise((resolve) =>
        setTimeout(resolve, delayMs)
      );
    }
  }
 
  throw lastError;
}

Here, isRetryable is application-specific. It should inspect the error type or status code rather than retrying every exception. A production implementation should also respect upstream retry guidance, where available, and stay within an overall execution deadline.

Write operations need additional care. A request can time out after the remote service has already processed it. Retrying blindly may create a duplicate action.

For operations that create or modify external records, use idempotency keys or another appropriate deduplication mechanism where supported.

Apply permissions at the tool boundary

An agent shouldn't be the only thing preventing an unauthorized operation.

Read-only operations, reversible changes, and high-impact external writes may require different safeguards. Validate the inputs, establish the caller's permissions using trusted application context, and apply any required approval mechanism before executing a sensitive action.

Don't treat a model-generated confirm: true field as proof that a human approved an operation. Approval must come from a trusted source outside the model's own response.

Set timeouts

A slow dependency can hold up the rest of a workflow. Set explicit timeouts for external calls and make sure retries fit within the overall time budget.

For example, in a Node.js runtime that supports AbortSignal.timeout():

async function fetchRecordData(recordId: string) {
  const response = await fetch(
    `${RECORD_API_URL}/records/${encodeURIComponent(recordId)}`,
    {
      signal: AbortSignal.timeout(5_000),
    }
  );
 
  if (!response.ok) {
    throw new Error(
      `Record API returned status ${response.status}`
    );
  }
 
  return response.json();
}

This gives the HTTP request a five-second timeout. The surrounding tool should handle timeout errors and convert them into the standard result format.

Treat retrieved content as untrusted data

Documents, web pages, and uploaded files may contain text that looks like an instruction to the agent. Retrieving that content doesn't make it trusted.

Keep retrieved content separate from system instructions, derive identity and permissions from trusted runtime context, and avoid exposing credentials or internal implementation details in tool responses.

These are related but distinct concerns. Retries address transient failures, permissions control who can perform an operation, timeouts bound execution, and untrusted-data handling protects the boundary between external content and system instructions.

They don't need elaborate implementations in every tool. They do need deliberate policies appropriate to the operation.

5. Make Tool Calls Observable

When an agent performs a sequence of tool calls, debugging a problem requires more than knowing that the final workflow failed.

You need to understand what the agent tried to do, how the tool interpreted the request, and what happened during execution.

For example, a useful record of a tool call should answer:

  • Which tool did the agent select?
  • What arguments did it generate?
  • Which arguments passed validation?
  • Did the operation execute, fail, or partially complete?
  • What did the tool return?
  • Did an external write happen before an error occurred?

A basic wrapper can capture common metadata:

async function withLogging<T>(
  toolName: string,
  traceId: string,
  operation: () => Promise<T>
): Promise<T> {
  const startedAt = Date.now();
 
  logger.info("Tool started", {
    toolName,
    traceId,
  });
 
  try {
    const result = await operation();
 
    logger.info("Tool completed", {
      toolName,
      traceId,
      durationMs: Date.now() - startedAt,
      outcome: "success",
    });
 
    return result;
  } catch (error) {
    logger.error("Tool failed", {
      toolName,
      traceId,
      durationMs: Date.now() - startedAt,
      outcome: "failure",
      errorType: getErrorType(error),
    });
 
    throw error;
  }
}

This wrapper is a starting point, not a complete observability solution. It doesn't automatically capture the agent's selected arguments or the returned result. Those need to be recorded explicitly at the tool boundary.

I'd also record a workflow or trace ID and propagate it across downstream services wherever possible. That makes it easier to follow related calls through a multi-step workflow.

Be careful about logging sensitive inputs and outputs. Record validated arguments and relevant result metadata with appropriate redaction. For large responses, a summary or result identifier is often more useful than storing the entire payload in application logs.

The goal is to distinguish between different kinds of problems:

  • The agent selected the wrong tool.
  • The agent selected the right tool but generated invalid arguments.
  • The tool executed but the underlying service failed.
  • The operation succeeded, but the response was ambiguous.
  • The tool returned valid data, but the next step interpreted it incorrectly.

These require different fixes. Better prompts won't fix an API timeout, and retries won't fix a tool-selection problem.

6. Turn the Principles into a Practical Audit

You don't need to redesign every tool in a system at once. Start with a review that identifies unnecessary complexity and the most important gaps in tool behaviour.

Here is the checklist I'd use.

Tool inventory

  • Identify duplicate and overlapping capabilities.
  • Review unused tools against actual workflow requirements before removing them.
  • Make the purpose of similar tools easy to distinguish.
  • Check whether each workflow needs the full tool set or a smaller, task-specific set.

Contracts and results

  • Validate inputs against explicit schemas.
  • Reject unexpected fields where the contract requires it.
  • Use a consistent result format across tools.
  • Distinguish successful empty results from failures.
  • Report partial completion and asynchronous status explicitly.

Context and execution

  • Bound output sizes and return only relevant fields.
  • Add pagination where large results are possible.
  • Retry only appropriate failures and protect writes against duplicate execution.
  • Enforce permissions and approval requirements at the tool boundary.
  • Set timeouts that fit the overall workflow budget.

Observability

  • Capture the selected tool and validated arguments.
  • Record outcome, duration, and relevant response metadata.
  • Propagate trace IDs across related tool calls.
  • Redact sensitive data from logs.

You can begin with the first two categories and then work through the others based on the risks and needs of your system.

Final Thoughts

Working with AI agents has made me think more carefully about the tools we expose to them.

It's easy to focus on the model's reasoning or the quality of the prompt. But the surrounding tool ecosystem matters too. Duplicate capabilities can make tool selection harder, unnecessary output can consume context, and ambiguous results can leave the agent unsure about what happened.

Not every problem needs a more sophisticated agent. Sometimes the right improvement is a clearer tool description, a stricter contract, a more useful response, or better visibility into what happened during execution.

The principles in this article aren't a guarantee of perfect agent behaviour, and not every one of them came from a single debugging incident. They're engineering practices worth considering when building systems in which a model chooses and invokes tools dynamically.

If you're already building AI agents, take a look at the tools you have today. Start by reviewing what the agent can access, what each tool expects, what it returns, and how you'll know when something goes wrong.