JavaRush/Courses/ChatGPT Apps/Agent tools (Tools): schemas, routing, errors

Agent tools (Tools): schemas, routing, errors

Open

1. Agent tool: what it really is

In previous modules, you already saw tools from the Apps SDK side — as “backend functions” that ChatGPT calls through your App. Now let’s change perspective: we’ll look at tools through the eyes of the agent in the Agents SDK and figure out how it chooses what to invoke and how to handle errors.

In a traditional backend, you’re used to thinking in terms of “endpoint,” “controller method,” “service function.” In the agent world, the basic unit of action is a tool. Agent tools and MCP-tools are different, though overlapping, things.

Strictly speaking: in the context of the ChatGPT Agents SDK, a tool is the description of a function that the model can ask to execute. The model doesn’t run code itself; it generates a structured request (usually JSON), and the runtime (your code, an MCP server, or the Agents SDK) executes the operation and returns the result.

In the ChatGPT Agents SDK ecosystem, a tool is described by configuration: it has a name, a description, and parameters (a JSON Schema for arguments). The agent sees this set of tools, keeps them in its context, and during reasoning decides which tool to call and with what arguments.

The agent (or ChatGPT as the host) receives this list, “remembers” it in its context, and during reasoning decides which tool to call for a given user request and with what arguments. That’s why specs keep repeating the mantra “tools are a contract” — tools are a contract between the model and your code, not just “a function in Python/TS.”

You can draw an analogy with a classic API. The route /api/gifts/search is pure syntax: URL, method, body format. The tool search_gifts is semantics: “search for gifts by profile and budget.” The tool description is effectively a prompt, only structured and designed for an LLM, not a human.

2. Types of tools: what exactly an LLM agent can do

To avoid drowning in a chaos of “functions that can do everything,” it helps to view tools as several common categories. This isn’t formal SDK typing, but an architectural way of thinking that will help a lot.

In our backend, LLM agents usually have three sources of tools.

  • Local business tools. These live in your backend: working with the database, domain logic (filtering, recommendations, scoring). For example, for GiftGenius we might have tools that fetch products from a PostgreSQL table or compute a personalized score for “how likely this gift will land well for this person.”
  • MCP tools. Here the MCP server acts as a provider of tools: it registers functions, resources, and prompts and exposes them to the client (ChatGPT, the LLM agent). Tools via MCP can call external APIs, work with files, or provide prompt templates.
  • Integration tools. These connect you with the rest of the world: ACP/commerce (order creation and checkout), sending emails, webhooks, writing to a CRM. Such tools are often riskier because they change the state of external systems, so you must treat them with special care for security and idempotency.

There’s another useful classification — by the nature of the action. Research on LLM tools typically highlights: data retrieval tools (search, RAG, get_*), tools that perform actions with side effects (create_order, send_email), pure computation (calculate_loan) and system/control (handoff_to_human, finish_task).

To cement this, it’s helpful to look at a small table.

Category Example in GiftGenius Side effect Risk
Data Retrieval
search_gifts, get_details
No Low
Action / Mutating
create_order, buy_gift
Yes High
Computation
estimate_delivery_cost
No Medium
System / Control
finish_recommendation
No Logical

Architecturally, the most important rule is: read‑only tools should be numerous and cheap, while mutating ones should be rare, extremely careful, logged, idempotent, and often require explicit user confirmation.

We’ll mostly talk about data retrieval and action tools, because they underpin GiftGenius’s logic.

3. JSON Schema as a contract between the model and your code

Now let’s dive into how a tool is described. In the ChatGPT Agents SDK (as in the Apps SDK), the standard format for describing tool parameters is JSON Schema: you describe type object, its properties, field types, required fields, constraints, and so on.

It’s important to understand: JSON Schema here is not only, or even primarily, about validation. It’s part of the model’s prompt. OpenAI’s official guides on tool (function) design say explicitly that the agent’s quality depends heavily on how detailed and unambiguous your field names and comments are.

Let’s look at an example for GiftGenius that already showed up in the course plan.

{
  "name": "search_gifts",
  "description": "Finds gifts by recipient type, interests, and budget.",
  "parameters": {
    "type": "object",
    "properties": {
      "recipient_type": {
        "type": "string",
        "description": "Who is the gift recipient (e.g., 'man', 'woman', 'child')."
      },
      "interests": {
        "type": "array",
        "items": { "type": "string" },
        "description": "Key interests (sports, books, technology, etc.)."
      },
      "budget": {
        "type": "number",
        "description": "Maximum budget in the user's currency."
      }
    },
    "required": ["recipient_type", "budget"]
  }
}

There are a few important points here.

  • First, name and description. For the model, this is the main signal for when to use this tool at all. Documentation on semantic routing emphasizes that the tool description is effectively the model’s API: if you name it func1 and label it “does something useful,” the model truly won’t understand when to call it. If you name it search_gifts and add a clear description, the choice becomes much easier.
  • Second, parameters. Field names and their descriptions are crucial. For an LLM, recipient_type is far clearer than type. A good description like “Who is the gift recipient…” hints that the value should be the recipient type, not, say, the gift wrapping format.
  • Third, required. This is not only validation on your side, but also a hint to the model: it will try to fill in required fields and may skip optional ones if the context is unclear. This reduces the number of “empty” or incorrect tool calls.

Official Apps SDK guides explicitly recommend: make tools narrow, with a single responsibility, clear names and descriptions, and avoid “do everything about gifts” tools that try to merge different tasks.

4. Designing GiftGenius tools: from schema to code

Let’s take our GiftGenius and add two key LLM-agent tools that will be needed in most scenarios:

  • suggest_gifts(profile, budget) — returns a list of candidates;
  • get_gift_details(gift_id) — reveals details for a specific gift.

Our suggest_gifts and get_gift_details are a typical example of local business tools from the previous classification, mainly in the Data Retrieval category.

Schema for suggest_gifts

Let’s start with a plain JSON Schema and then show how this might look in TypeScript code in the backend/agent runtime.

{
  "name": "suggest_gifts",
  "description": "Selects a list of gifts based on the recipient profile and budget.",
  "parameters": {
    "type": "object",
    "properties": {
      "age": {
        "type": "integer",
        "minimum": 0,
        "maximum": 120,
        "description": "Recipient's age in years."
      },
      "relationship": {
        "type": "string",
        "enum": ["friend", "coworker", "partner", "family"],
        "description": "Relationship to the recipient: friend, coworker, partner, family."
      },
      "interests": {
        "type": "array",
        "items": { "type": "string" },
        "description": "Recipient's interests (sports, books, technology, etc.)."
      },
      "budget": {
        "type": "number",
        "minimum": 1,
        "description": "Maximum budget in the user's currency."
      }
    },
    "required": ["budget"]
  }
}

Here we use an enum for relationship to prevent the model from inventing arbitrary strings like "bad coworker" and passing them further into the code. Such a careful schema design helps both the model (it sees the allowed options) and the developer (fewer surprises at runtime).

Now suppose we have an MCP server on Node.js with some McpServer. Tool registration might look like this:

// simplified example of registering a tool in an MCP server
server.registerTool(
  {
    name: "suggest_gifts",
    description: "Selects gifts by profile and budget.",
    inputSchema: suggestGiftsSchema
  },
  async (input, ctx) => {
    const gifts = await findGiftsInDb(input, ctx.userLocale);
    return { items: gifts }; // JSON the agent will then see
  }
);

The code is heavily simplified, but the idea is clear: in one place — the contract (name, description, schema), elsewhere — the implementation.

Schema for get_gift_details

The second tool needed in almost any storefront:

{
  "name": "get_gift_details",
  "description": "Retrieves full details about a gift by its identifier.",
  "parameters": {
    "type": "object",
    "properties": {
      "gift_id": {
        "type": "string",
        "description": "UUID of the gift in the GiftGenius database."
      }
    },
    "required": ["gift_id"]
  }
}

And a similar registration:

server.registerTool(
  {
    name: "get_gift_details",
    description: "Returns detailed information about a gift.",
    inputSchema: getGiftDetailsSchema
  },
  async ({ gift_id }) => {
    const gift = await db.gifts.findById(gift_id);
    if (!gift) return { notFound: true };
    return { gift };
  }
);

Notice that we immediately show that the tool can return notFound: true. These are the beginnings of semantic (business) errors, which we’ll discuss below. The agent will be able to see “gift not found” and decide what to do: for example, try another ID or ask the user to pick a different item.

5. How the agent decides which tool to call

Now the most interesting part: routing. In a traditional web app, routing is rigid: URL → specific controller. In the world of ChatGPT Apps and agents, tool selection is semantic and probabilistic.

A high-level cycle can be visualized like this:

flowchart TD
  U[User message] --> M["Model (agent)"]
  M -->|request analysis| C{Tool needed?}
  C -->|no| T[Text response]
  C -->|yes| S[Tool selection]
  S --> K[Construct JSON arguments]
  K --> R[Execute tool]
  R --> M2[Model sees result]
  M2 --> T2[Final answer or next step]

At each step, the agent sees several things:

  • First, system instructions (the agent’s role, constraints);
  • Second, the dialogue history;
  • And finally, the list of tools with their name, description, inputSchema.

When the next user message arrives, the model matches the request’s meaning to the tool descriptions (semantic matching). If the request is “pick a gift for a friend up to $50,” the description of suggest_gifts is far more relevant than get_gift_details, and the agent will likely choose the former.

Official guides emphasize two things that strongly affect routing quality.

  • First, avoid semantically overlapping tools: if you have search_gifts and find_gifts described roughly the same, the model will be confused.
  • Second, stick to the single-responsibility principle for a tool: one tool — one clear task, not “pick gifts and create an order and send an email.”

Different LLM agents provide mechanisms to control tool selection modes: for example, “auto” (the model decides if a tool is needed), “required” (a tool call is mandatory), “none” (tools disabled). This helps in complex workflows (multi-step scenarios) when, say, at a particular step you want to force a call to suggest_gifts rather than let the model just chat.

GiftGenius semantic routing example

Suppose our agent has at least two tools: suggest_gifts and get_gift_details.

  1. The user writes: “Pick a gift for a coworker up to $30; he likes board games.”
  2. The agent sees the goal “pick a gift,” budget information, and interests. The description of suggest_gifts fits perfectly — we call this tool.
  3. The tool returns a list of five gifts with their IDs, names, and short descriptions.
  4. The user then writes: “Tell me more about the third option.” The agent maps “third option” to an ID from the previous result, and now the get_gift_details tool fits by meaning — it is called.

Note that nowhere in the code did you explicitly write “if the request contains the word ‘pick,’ call suggest_gifts.” The model handles this based on your descriptions and the dialogue history. Your responsibility as a developer is to make the choice obvious both to the model and to a human.

6. Tool errors: not 500, but a signal to the model

Remember how in get_gift_details we already showed notFound: true? That’s an example of a business error that the agent should see and handle meaningfully, rather than receiving a bare 500.

Now to the painful part. In a typical REST API, if something deep in the backend crashes, we return 500 Internal Server Error, log the stack trace, and the user somehow deals with it. For an agent, that approach doesn’t work well.

Practical guides and materials on the Agents SDK recommend treating tool errors as observable events, not just crashes. This is often called the “Error as Observation” pattern.

Roughly speaking, you shouldn’t “crash” without explanation; you should return a structured response that explains what went wrong so the model can adapt its behavior: rephrase the request, ask the user, try a different tool, and so on.

Errors are usually split into three groups.

  • Argument validation errors. The model can generate incorrect parameters: omit a required field, pass a string instead of a number, or go out of allowed bounds. Use your schema and validation not only to throw, but to produce a meaningful response: for example, return which field is invalid and why.
  • Business errors. These are expected situations like “item not found,” “region unavailable,” “budget too small for this gift category.” From an API perspective they’re still errors, but they should be returned in a normal response with a clear code and message — not as a crash.
  • System errors. Timeouts of external services, network issues, database failures. Here the agent typically needs a careful, generalized message like “service temporarily unavailable, try again later.” No stack traces, table names, or other details that the model doesn’t need and that may be risky from a security perspective.

Official Agents SDK materials even suggest a special mechanism failure_error_function that lets you neatly form the error text the model will see instead of just throwing an exception up the stack.

Structure of a “friendly” error

In an agent tool (in your backend), you can agree that any error is returned as, for example, this object:

type ToolError = {
  code: string;      // 'VALIDATION_ERROR', 'OUT_OF_STOCK', ...
  message: string;   // for the model
  retryable: boolean;
};

And the tool result — as a union:

type SuggestGiftsResult =
  | {
      ok: true;
      items: GiftSummary[];
    }
  | {
      ok: false;
      error: ToolError;
    };

The model (or the agent runtime) will see such JSON and can decide: if retryable: true, try again with small changes; if it’s a non-retryable business-level error, it’s better to go back to the user and explain what went wrong.

7. Examples: validation, business error, and system error

Let’s return to our backend/agent tools and see how the same ideas could be implemented in code.

Validation error

Suppose the suggest_gifts tool arrives, but the model for some reason passes a negative budget.

async function handleSuggestGifts(input: SuggestGiftsInput)
  : Promise<SuggestGiftsResult> {

  if (input.budget <= 0) {
    return {
      ok: false,
      error: {
        code: "VALIDATION_ERROR",
        message: "budget must be a positive number.",
        retryable: false
      }
    };
  }

  const items = await findGiftsInDb(input);
  return { ok: true, items };
}

Here we deliberately don’t throw an exception; we return a structured error. The agent can rethink the request: maybe it decides it mixed up the currency and asks the user, or simply states it can’t pick a gift with that budget.

Business error

Now an example with get_gift_details. A gift with the given ID may simply not exist.

async function handleGetGiftDetails(input: { gift_id: string }) {
  const gift = await db.gifts.findById(input.gift_id);

  if (!gift) {
    return {
      ok: false,
      error: {
        code: "GIFT_NOT_FOUND",
        message: "A gift with this identifier was not found.",
        retryable: false
      }
    };
  }

  return { ok: true, gift };
}

In the model’s response, you might expect something like: “It looks like the selected gift is no longer available. I can suggest a few alternatives from a similar category.” For this, the agent doesn’t need SQL errors and stack traces — just a clear code and message.

System error

Finally, a system error example. Suppose your tool calls an external delivery API that sometimes “goes down.”

async function handleEstimateDelivery(input: EstimateDeliveryInput) {
  try {
    const eta = await callDeliveryApi(input);
    return { ok: true, eta_days: eta };
  } catch (e) {
    return {
      ok: false,
      error: {
        code: "DELIVERY_SERVICE_UNAVAILABLE",
        message: "The delivery service is temporarily unavailable.",
        retryable: true
      }
    };
  }
}

The agent might decide: “It looks like the delivery service is unavailable right now. I’ll still show you gifts, but the exact delivery time may vary. Would you like to continue?”

8. Security and idempotency of tools (a quick tour from the tools side)

We’ll cover security and permissions fully in a separate topic, but agent tools are too tightly coupled with them to skip entirely here.

First, separate read tools from write tools. In descriptions, schemas, and permissions, explicitly state which tools only read data and are absolutely safe, and which ones can charge money, modify orders, etc. Documentation and forum discussions about agent scenarios explicitly recommend splitting ReadOnly and Mutating tools.

Second, for mutating tools you must think about idempotency. An agent or MCP client can easily repeat a call (for example, because of a network error), and you don’t want create_order to create two orders instead of one. Typical patterns:

  • an idempotency key passed as a tool argument;
  • checking for an existing operation before executing;
  • splitting steps into “create order draft” and “confirm order.”

All of this is tightly connected to how you design the tool contract: if the JSON Schema doesn’t have a field for an idempotency key, adding idempotency later will be much more painful.

9. A brief look at the Agents SDK: what it looks like in the agent runtime

This section is a short overview for those who will work with a TypeScript-oriented Agents SDK. Although most of our course focuses on MCP, it’s useful to understand how similar tools are seen by the Agents SDK and what a typical tool looks like at runtime.

The official documentation typically describes something like a “functional tool”: any function described via a configuration object (or a helper like tool(...)) and annotated with types can be automatically turned into a tool for which the SDK will generate a JSON Schema and description.

Conceptually, it’s the same thing we’ve already discussed: the function name, its parameters, and its comment/description play the role of the tool’s name, schema, and description. The difference is that the SDK and/or a schema helper library (e.g., Zod or JSON Schema) does most of the “mechanical” work for you.

Hypothetical example (pseudo-TypeScript, simplified):

type Gift = {
  id: string;
  title: string;
  // ...
};

const suggestGifts = tool({
  name: "suggest_gifts",
  description: "Selects a list of gifts by recipient type and budget.",
  parameters: {
    type: "object",
    properties: {
      recipient_type: {
        type: "string",
        description: "Who is the gift recipient (e.g., 'man', 'woman', 'child')."
      },
      budget: {
        type: "number",
        description: "Maximum budget in the user's currency."
      }
    },
    required: ["recipient_type", "budget"]
  }
}, async (args: { recipient_type: string; budget: number }): Promise<Gift[]> => {
  // Inside — your domain logic
  return findGifts(args.recipient_type, args.budget);
});

The SDK (or your tool helper) will build the JSON Schema from the parameters object and pass it to the agent, while the runtime takes care of validating and marshalling arguments back and forth. Conceptually, this is exactly what you were doing manually in a TypeScript MCP server; now the tool is “wired” directly into the agent runtime.

What matters here isn’t memorizing the helper’s exact syntax, but catching the idea: good typing + clear description/comments = a good tool.

Putting it together: a good agent tool is a narrow, clearly described function with a well-thought-out JSON Schema, a clear description for the model, and careful error handling. Semantic routing will only work if tools don’t overlap semantically. And mutating operations must be safe and idempotent, or else your production agent will quickly become a source of surprises.

10. Common mistakes when designing agent tools

Mistake #1: Tools that are too broad (“do_everything”).
It’s tempting to cram everything into a single tool manage_gifts that searches for gifts, shows details, creates an order, and sends an email. This makes things hard for the model: the description becomes vague, semantic routing degrades, and the agent starts calling this tool “just in case,” even where a simple search is needed. It’s better to split tasks into separate tools with one well-understood responsibility.

Mistake #2: Tools that overlap semantically.
If you have search_gifts and find_gifts, both “search gifts by interests,” the model will randomly choose between them. As a result, behavior becomes unstable: identical requests sometimes go to one tool, sometimes to the other. Try to ensure each name and description occupies a unique “niche” in semantic space.

Mistake #3: Poor or missing descriptions and schema fields.
A name like func1, the description “Does something,” and a parameter data: string is a classic way to make the agent dumb. The model isn’t telepathic and can’t read your source code. It relies on the schema’s description, properties, and each property’s description. If you don’t explain what recipient_type is, the model will guess — and fail.

Mistake #4: Focusing only on the happy path and ignoring errors.
Many tool implementations assume “we’ll always get correct arguments and an available service.” In the real world, the model can easily generate wrong parameters, external services go down, and the database occasionally says “timeout.” If you don’t design error formats and return a meaningful message to the agent, it cannot adjust and will either fail silently or hallucinate.

Mistake #5: Sending a raw 500 and a stack trace to the LLM.
In REST APIs, we’re used to logging full stack traces for faster debugging. In the agent context, a stack trace sent to the model is both useless (the model doesn’t know what SQLException is in your specific library) and potentially dangerous (extra implementation details and possibly confidential information). It’s much better to catch the exception, log the details, and send the model a neat code and message.

Mistake #6: Missing idempotency for mutating tools.
A create_order tool without an idempotency key is a direct invitation to duplicate orders, especially with network glitches and automatic retries. If your agent operates in a commercial scenario, money-related tools must be designed so repeated calls don’t cause extra charges or duplicates.

Mistake #7: Storing secrets and technical details in the schema or description.
Sometimes a developer habitually writes in the description: “Internally calls service X at https://internal-api.example.com.” The model doesn’t need this information, and neither does the user. Schemas and descriptions are part of the prompt, they live in the model’s context, and you shouldn’t put internal service URLs, private table names, and especially secrets there.

Mistake #8: Passing everything into tools instead of a well-thought-out set of fields.
It’s easy to fall for the idea “we’ll just pass the entire user prompt as a string and figure it out inside.” That way, you lose the benefit of structuring via JSON Schema: the model no longer understands which parts of the request matter for the logic, and you lose validation and predictability. It’s better to extract explicit fields from the request (budget, interests, user_location) and describe them as part of the tool contract.

1
Task
ChatGPT Apps,  12 level,  1 lecture
Locked
Suggest_gifts tool contract (Zod schema + clear descriptions)
Suggest_gifts tool contract (Zod schema + clear descriptions)
1
Task
ChatGPT Apps,  12 level,  1 lecture
Locked
Errors as “observation” in get_gift_details (validation + notFound without 500)
Errors as “observation” in get_gift_details (validation + notFound without 500)
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet