Courses / Building AI Agents

Session 1 of 11

Tool Use and Function Calling

Overview

Chapter 1 — Tool Use and Function Calling

Course: Building AI Agents

Prerequisites: The course Getting Started with AI. You can read and run a short Python script, and you can read a JSON document. You have called an HTTP API from code. You know that a language model takes text in and produces text out, one token at a time, and that a model’s response carries a stop reason field saying why it stopped. Everything else this chapter uses (tool definitions, JSON Schema, tool results, tool choice and the risk categories for tools) is defined in the notes before it is used.

What the reader can do after this chapter:

  • Trace one tool call end to end, naming which part the model produces, which part your application executes, and which message carries the result back.
  • Write a tool definition (name, description and input schema) that tells a model what the tool does, when to use it and what arguments it accepts, and validate a model’s arguments against it in code.
  • Bound what a tool-using model can reach by removing tools it does not need, narrowing each tool’s permissions, and requiring a person’s approval before a high-impact action runs.

Core question this chapter answers: When a model “takes an action”, what actually carries it out, and which decisions in your design limit what that action is allowed to reach?

Connections:

  • Builds on: a model that only returns text for your code to read. This chapter gives it a way to ask for an action in a fixed, machine-readable shape that your code can check before anything happens.
  • Leads into: Chapter 2, which takes the tool definition you write in this chapter’s project and exposes it through a standard protocol, so that any compatible AI application can discover and call it without a custom integration.

Your answers are scored

This chapter is assessed. The quiz is marked against a key held separately from your materials. Questions 1–8 are multiple choice and are marked by comparing your letter with the key. Questions 9–10 ask for a short answer in an exact format, and each one is marked by a rule printed with the question, so you can check that your answer has the right shape before you submit it.

  • Nothing is judged by reading it for style. An answer either matches the rule or it does not.
  • All three project tasks are scored from an evidence file of key: value lines. Every line’s accepted value, or the rule that decides it, follows from what the task’s code prints, so you can check your own file before you submit it.

Notes

Building AI Agents — Chapter 1: Tool Use and Function Calling

Before you start

Nothing to install for the reading. For the project you need Python 3 and nothing beyond its standard library. No task needs an account with a model provider, an API key or any payment.

Keep one question in mind while you read: if the model asked for this action, what in my code would stop it if it were the wrong action? By the end of the chapter you should be able to answer it for any tool you give a model: from the tool’s definition, the checks your code runs on each call and the permissions the tool holds.

Summary

A model with tools still only produces output for your application to read. What changes is that some of that output is a structured request (a tool name and a set of arguments) that your application can recognise, check and choose to carry out. The model never runs anything itself: your code executes the operation and sends the result back as a new message, and the model continues from there. That round trip is a contract with three parts you control: the tool definition that tells the model what the tool does and what arguments it takes, the code that validates and executes each call, and the permissions that code holds on the systems behind it. The risks of tool use come from giving a model more of any of these than the task needs: more tools, more permissions on each tool, or the freedom to take a high-impact action with nobody checking it first. This chapter teaches you to build the round trip and to keep each of those three parts as small as the task allows.

Key concepts

Term Definition
Tool (function) An operation your application offers to a model, described by a name, a plain-language description and a schema for its arguments. Some providers call it a function; the idea is the same.
Tool call The structured request a model produces when it wants a tool used: the tool’s name, the arguments, and an id that links the request to its result.
Tool result The message your application sends back after executing a tool call. It carries the call’s id and the output, or an error.
Client tool A tool your own application executes. The model’s response asks for the call; your code runs it and returns the result.
Server tool A tool the model provider executes on its own infrastructure, so your application receives the result without running anything.
JSON Schema A standard format for describing the shape of a JSON value: which fields exist, their types, which are required, and which values are allowed. Tool arguments are described with it.
Input schema The JSON Schema in a tool definition that states which arguments the tool accepts.
Tool choice A request setting that says whether the model may call tools, must call one, must call a named one, or may not call any.
Strict mode A setting some providers offer that makes the model’s tool arguments match the input schema exactly, instead of on a best-effort basis.
Excessive agency The risk category for damage done by a model’s actions because it was given more functionality, more permissions or more freedom to act without checks than its task required.
Least privilege Giving each component only the access its task needs and nothing more, so that a mistake or a misuse can reach as little as possible.
Indirect prompt injection Instructions hidden inside content a tool returns (a web page, an email, a file) that try to redirect the model when that content reaches it.

Explanations

The failure, before the explanation

Here is an illustrative staging incident, of the kind this chapter is designed to prevent. A team builds an internal assistant for the on-call engineer. To save time, they give it one tool, run_sql, which runs any SQL statement against the support database and returns the rows. The connection the tool uses is the same one the team uses for maintenance, so it can read, insert, update and delete.

An engineer types: “The test sessions from yesterday’s load run are cluttering the dashboard — clean them up.” The assistant replies that it has removed them. It ran:

DELETE FROM sessions WHERE created_at >= '2026-10-02';

That statement removed every session created since the start of that day, real customers’ sessions included, not only the load-test ones.

The engineer’s first reaction is that “the AI deleted the data”. That is not quite what happened. The model produced a string. The team’s code took the string and ran it, with credentials that allowed a delete, on a table that held production data, with no check in between. Each of those was a design decision, and each is something this chapter gives you a way to make differently.

The model does not run your tools

Start with what physically happens. You send a request that includes a list of tools. The model answers with ordinary text, with a tool call, or with both in one response (the provider examples below show text followed by a tool call). One provider’s documentation states the boundary plainly: “The model never executes anything on its own. It emits a structured request, your code (or Anthropic’s servers) runs the operation, and the result flows back into the conversation” (Anthropic, How tool use works). Another provider says the same thing from the developer’s side: “When the model calls a function, you must execute it and return the result” (OpenAI, Function calling).

A first way to picture it: the model is a colleague who can only write work orders. It can write “restart the API service” on a ticket, but it has no access to the servers. Someone with access reads the ticket and decides whether to do it. In a tool-using system, your application is that someone, and it decides in code, on every call.

A second way: a tool call is like a form submitted from a web page. The browser can put anything in the fields, and a backend engineer would never run a form’s contents without validating them and checking the user’s permissions first. A tool call deserves the same treatment, because its arguments were written by a model rather than typed by a person, and you cannot predict them in advance.

The round trip, step by step

Here is one complete exchange, using a tool that looks up recent log lines.

  1. Your application sends the user’s message and the list of tools.
  2. The model responds with a tool call instead of a final answer. On one provider, the response’s stop reason is tool_use and its content holds a tool_use block with an id, a name and an input object (Anthropic, Handle tool calls).
  3. Your code reads the name and arguments, runs the matching operation, and builds a result.
  4. Your application sends a new request containing the conversation so far, the model’s tool call, and a tool_result block whose tool_use_id matches the call’s id.
  5. The model reads the result and either answers or asks for another tool.

Another provider describes the same flow in five steps: make a request with the tools the model could call; receive a tool call; “execute code on the application side with input from the tool call”; make a second request with the tool output; and receive a final response “(or more tool calls)” (OpenAI, Function calling). The field names differ between providers. The shape — the model asks, your code acts, the result goes back — is the same.

Two details in that shape matter in production:

  • The id ties a result to its request. One provider requires tool result blocks to “immediately follow their corresponding tool use blocks in the message history”, and rejects a user message whose text comes before its tool results (Anthropic, Handle tool calls). When a model asks for several tools at once, the ids are how your code and the model keep each result with the right call.
  • Results are input to the model, and they may not be trustworthy. The same documentation warns that tool results “often carry content from sources outside your control: web pages, inbound email, user uploads, third-party APIs”, that an attacker who can influence that content “may embed instructions that try to redirect Claude (indirect prompt injection)”, and advises keeping such content inside tool result blocks rather than in the system prompt (Anthropic, Handle tool calls). A tool that reads a web page has just let that page’s author write into your model’s context.

The definition is the contract

Here is the same tool described two ways. The poor description reads “Gets the stock price for a ticker.” The good one says what the tool returns (the latest trade price in US dollars), which tickers are valid (symbols on a major US exchange), when to use it (when the user asks for the current or most recent price of a specific stock), and what it will not do (“It will not provide any other information about the stock or company”). Both come from one provider’s documentation (Anthropic, Define tools). Faced with the poor one, a model may have to guess what “price” means, which tickers work and whether to use it for a question about earnings.

That difference is what a tool definition is for. It has three required parts on one provider: a name that must match ^[a-zA-Z0-9_-]{1,128}$, a description (“A detailed plaintext description of what the tool does, when it should be used, and how it behaves”), and an input_schema, which is “A JSON Schema object defining the expected parameters for the tool” (Anthropic, Define tools). The documentation calls detailed descriptions “by far the most important factor in tool performance” and asks for “at least 3–4 sentences for each tool description, more if the tool is complex”.

A second way to see why: the description is the main documentation the model has for the tool. A human engineer joining your team can ask a colleague what update_status does to a closed ticket. The model cannot ask. If the description does not say it, the model has to guess.

The input schema is the part your code can enforce. A schema like this one:

{
  "type": "object",
  "properties": {
    "service": {"type": "string", "enum": ["api", "worker", "billing"]},
    "query":   {"type": "string"},
    "limit":   {"type": "integer", "minimum": 1, "maximum": 50}
  },
  "required": ["service", "query"],
  "additionalProperties": false
}

states that service and query must be present, that service can only be one of three values, that limit, if given, is a whole number from 1 to 50, and that no other field is allowed. One provider’s guidance for defining functions is to “use enums and object structure to prevent invalid states” (OpenAI, Function calling). An enum turns “which service?” from free text that could be anything into a choice your code can check in one comparison.

Strict mode closes the gap between the schema and the call. Without it, one provider describes adherence as “best effort”, so a model’s arguments are not guaranteed to match the schema. One provider says that setting strict to true “will ensure function calls reliably adhere to the function schema, instead of being best effort” (OpenAI, Function calling); another says strict: true “guarantees that tool inputs will always match your schema exactly” (Anthropic, Handle tool calls). Strict mode makes the arguments well-formed. It does not make them wise: {"service": "api", "query": "*", "limit": 50} is valid and may still be the wrong request. Your code still decides whether a valid call should run.

Choosing whether the model calls a tool

Two examples show why the setting exists. A data-extraction pipeline that must always return a filled record forces one tool, so the reply always arrives in that tool’s argument shape. A chat assistant that should answer small talk without touching the ticket system leaves the choice with the model.

By default the model decides for itself whether a turn needs a tool. A request setting changes that. One provider documents four options: auto lets the model decide; any “tells Claude that it must use one of the provided tools, but doesn’t force a particular tool”; tool “forces Claude to always use a particular tool”; and none “prevents Claude from using any tools” (Anthropic, Define tools). Another provider lists matching modes under different names: auto, required (call one or more functions) and a forced function (call exactly one named function), among others (OpenAI, Function calling).

Note one caution from that documentation: “Not every model and setting supports forced tool use”, so check the model you are calling before relying on it (Anthropic, Define tools).

When a call goes wrong

Take a bad call, {"service": "payments", "query": "refund"}, to the logs tool from earlier. Two things can go wrong with a call like it: the arguments can be invalid (as here), or the operation can fail when it runs. In both cases the model needs to be told, and told usefully.

One provider’s mechanism is a field on the result: when a tool fails, you “return the error message in the content along with "is_error": true”, and the model then works the error into its reply (Anthropic, Handle tool calls). For invalid arguments, the same page says the model “will retry 2-3 times with corrections before apologizing to the user”, and it gives advice that applies to any error you return: “Write instructive error messages. Instead of generic errors like "failed", include what went wrong and what Claude should try next” — for example, “Rate limit exceeded. Retry after 60 seconds.”

Compare two error results for that same bad call:

  • "failed" — the model knows something is wrong but not what to change.
  • "service must be one of: api, worker, billing. 'payments' is not a service this tool can search." — the model can correct the argument, or explain to the user that payments logs are out of reach.

The second is also what you would want in your own application logs. An error message written for the model is an error message a person can debug from.

Bounding what a tool can reach

Now return to the run_sql incident. Security guidance for applications built on language models names this risk category directly. The OWASP Top 10 for LLM Applications (2025) lists it as LLM06:2025 Excessive Agency, which it describes as the vulnerability that enables “damaging actions to be performed in response to unexpected, ambiguous or manipulated outputs from an LLM”, and it says the root cause is “one or more of: excessive functionality; excessive permissions; excessive autonomy” (OWASP, LLM06:2025 Excessive Agency).

The incident had all three:

  • Excessive functionality. A tool that runs any SQL statement can do far more than “find test sessions” needs. A narrow tool — sessions_list_test_runs(run_id) — could only do that.
  • Excessive permissions. OWASP’s own example is an extension intended to read data that “connects to a database server using an identity that not only has SELECT permissions, but also UPDATE, INSERT and DELETE permissions” (OWASP, LLM06:2025). That is run_sql on the maintenance connection.
  • Excessive autonomy. A delete ran with nobody approving it. OWASP’s mitigation is to “Utilise human-in-the-loop control to require a human to approve high-impact actions before they are taken” (OWASP, LLM06:2025).

A second way to hold the idea: think of the keys on a building manager’s ring. The cleaner gets the keys to the floors they clean, not the master key; the master key opens the server room too. Narrowing the tool is choosing which doors exist on the ring. Narrowing the permissions is cutting each key so it opens only its door. Requiring approval is having a second person turn the key for the rooms where a mistake is expensive.

Three design habits keep the tool set small and clear, all from provider guidance:

  • Fewer, more capable tools. One provider suggests consolidating related operations: rather than create_pr, review_pr and merge_pr, “group them into a single tool with an action parameter”, because “Fewer, more capable tools reduce selection ambiguity” (Anthropic, Define tools). Another describes keeping fewer than 20 functions available at the start of a turn as “just a soft suggestion” (OpenAI, Function calling).
  • Namespaced names. When tools span several services, “prefix names with the service (for example, github_list_prs, slack_send_message)” so the model is not choosing between two ambiguous search tools (Anthropic, Define tools).
  • High-signal results. Return “only the fields Claude needs to reason about its next step”; bloated responses “waste context and make it harder for Claude to extract what matters” (Anthropic, Define tools). An engineering write-up from the same provider makes the same point with a contact list: a search_contacts tool beats a list_contacts tool, because listing makes the agent read irrelevant entries (Anthropic, Writing effective tools for AI agents).

Notice that consolidation and narrowness pull in different directions. One tickets tool with an action of get, comment or update_status is fewer tools; a separate, unexposed tickets_delete is a narrower surface. The rule that reconciles them is to group the operations that carry the same risk and keep high-impact ones separate, so that your code can apply a different policy to each.

Three situations you will meet, and what they mean for your design

Before the table, three situations:

  1. A QA engineer adds a browser_fetch tool so the assistant can read release notes. A week later the assistant summarises a vendor page and, in the same reply, tells the user to disable a security setting. The instruction came from text on the fetched page.
  2. A backend engineer exposes the ticket system to the assistant as one tool, tickets, whose action can be get, comment, update_status or delete. Nothing in the code treats delete differently from get.
  3. A DevOps engineer adds deploy_rollback(service, to_version). During an incident the assistant proposes rolling back api to v41, but v41 is the version that caused last month’s outage.
What you see What the design allowed What to do instead
The model repeats instructions from a fetched page (situation 1) Untrusted tool output reached the model and was acted on Keep fetched content inside tool results; treat it as data; require approval for any action it prompts
A delete runs as easily as a read (situation 2) Consolidation put two risk levels behind one policy Keep high-impact actions in separate tools, or check action in code and apply a stricter policy
A valid but harmful call is proposed (situation 3) Validation proved the call well-formed, not correct Require a person’s approval for high-impact tools; show them the exact arguments before it runs

Flashcards

Say the answer aloud before revealing it. Speaking it is what exposes the gaps that reading past a written answer hides.

When a model "calls a tool", what does the model itself actually produce?

A structured request: the tool’s name, its arguments and an id. Nothing runs until your application reads that request, decides to execute it and sends the result back. The model never executes anything on its own.

What links a tool result to the tool call it answers?

The call’s id. On one provider the result carries a tool_use_id equal to the call’s id, and the results must come immediately after the calls in the message history.

What are the three parts of a tool definition, and which one does the model rely on most?

A name, a description and an input schema. The description: one provider calls detailed descriptions the most important factor in tool performance and asks for at least three to four sentences per tool.

What does an `enum` in an input schema buy you?

It limits an argument to a fixed list of values, so an invalid value can be caught with one comparison in your code, and the model is told exactly which values exist.

Strict mode is on and the arguments match the schema. Is the call safe to run?

Not necessarily. Strict mode guarantees the arguments are well-formed, not that the action is the right one. A valid rollback to a bad version is still valid. Your code and, for high-impact actions, a person still decide.

What do the tool-choice settings `auto`, `any`, `tool` and `none` do?

auto lets the model decide whether to call a tool; any makes it call one of the tools; tool makes it call one named tool; none stops it calling any. Not every model supports forcing a call.

A tool fails. What should the result you send back contain?

An error flag (on one provider, is_error: true) and an instructive message: what went wrong and what to try next, such as the allowed values or when to retry, not just “failed”.

Name the three root causes of excessive agency.

Excessive functionality (the tool can do more than the task needs), excessive permissions (the tool’s identity can do more on the system behind it than needed) and excessive autonomy (high-impact actions run without a person approving them).

Why is a fetched web page a security concern for a tool-using model?

Its text becomes input to the model. If an attacker controls that text, they can plant instructions that try to redirect the model — indirect prompt injection. Treat tool output as untrusted data.

Why namespace tool names such as `github_list_prs` and `slack_send_message`?

When tools come from several services, a prefix makes each name unambiguous, so the model is not choosing between two tools that are both called search.

Model answer

The question: “Our assistant can now update tickets and roll back deploys. Walk me through what happens when it decides to roll back a service, and how you stop it rolling back the wrong thing.” This is the question a lead engineer or a security reviewer asks before tool access goes live. A strong answer hits five beats, in this order:

  1. Request — the model only proposes a structured tool call: a name, arguments and an id.
  2. Execution — my application, not the model, decides whether to run it and sends the result back linked by the id.
  3. Validation — the code checks the arguments against the tool’s schema before anything runs, and returns an instructive error if they fail.
  4. Scope — the tool can only do what this task needs, and its credentials can only touch what that operation needs.
  5. Approval — a high-impact action waits for a person to approve the exact arguments before it runs.

“When the model wants a rollback, it doesn’t do anything itself — it returns a structured request with the tool name, the arguments and an id. My application reads that request and decides whether to run it, and the result goes back in a message carrying the same id. Before anything runs, the code validates the arguments against the tool’s schema — the service has to be one of our known services, the version has to be present — and if they fail, the model gets an error that says exactly what was wrong. Then scope: the rollback tool can only roll back, nothing else, and the identity it uses can only touch deployments, not databases. And because a rollback is high-impact, it doesn’t run on the model’s say-so: the on-call engineer sees the exact service and version and approves it first. A well-formed call isn’t the same as a correct one, so the last check is a person’s.”

Missing a beat is a failure state. Without request, the answer blames “the AI” for actions code performed. Without execution, nobody owns the decision to run the call. Without validation, malformed arguments reach real systems. Without scope, a small mistake reaches everything the credentials can touch. Without approval, a valid but wrong action, such as rolling back to a known-bad version, runs with nobody seeing it first.

Why this matters

  • Incident prevention. In the run_sql incident, each cause was a design decision made in advance: which tools exist, what their credentials allow, and which actions wait for a person.
  • Security review. A reviewer can ask the three excessive-agency questions about each tool, and an engineer who can answer them from the tool list is better placed to get access approved.
  • Debuggability. Validation errors written for the model are also readable by the on-call engineer, so a failed call explains itself in the logs.
  • Talking to customers. When a customer asks “can the assistant delete our data?”, the answer is in your tool list and your permissions, and you can show it.

After this chapter

The project for this chapter is Put a Gate in Front of the Tools (project.md). You will write a validator that checks a model’s tool arguments against an input schema, a dispatcher that decides for each call whether to run it, refuse it or hold it for approval, and finally a tool definition for one real operation from your own work.

Chapter 2 starts from the tool definition you write in the last task. It exposes that tool through a standard protocol so that any compatible AI application can discover it and call it, without a custom integration for each one.

Project

Hands-on Project — Put a Gate in Front of the Tools

Objective

Build the code that sits between a model’s tool call and the operation it asks for: a validator that checks arguments against an input schema, a dispatcher that decides for each call whether to run it, refuse it or hold it for a person, and a tool definition for one real operation from your own work, so that you can show, line by line, what stops a wrong call.

What this project needs: Python 3 and nothing beyond its standard library (json, re). No task needs an account with a model provider, an API key or any payment, and no task calls a model: the “model’s calls” are fixed data given below. All three tasks are required.

Your evidence file. Keep one plain-text file, chapter1-evidence.txt. Each task tells you which lines to add. Every line has the form key: value, one per line, with the key in lower case exactly as printed. Write what your code printed, not what you expected it to print.

Task 1 — Write a validator

The schema below is the one used in the notes. Save it in your script as a Python dictionary.

{
  "type": "object",
  "properties": {
    "service": {"type": "string", "enum": ["api", "worker", "billing"]},
    "query":   {"type": "string"},
    "limit":   {"type": "integer", "minimum": 1, "maximum": 50}
  },
  "required": ["service", "query"],
  "additionalProperties": false
}

Write validate(schema, args). It returns a list of error messages, and the list is empty when the arguments pass. Support only the features this schema uses: type (string and integer), enum, minimum, maximum, required and additionalProperties: false. Apply the checks in this order, and use these exact message shapes, so that each message tells the model what to change:

  1. For each name in required that is missing from the arguments: missing required field: <name>
  2. For each argument name not listed in properties: unexpected field: <name>
  3. For each property that is present, in the order the schema lists them, and only the first failing check of that property:
    • wrong type: <field> must be of type <type>
    • value not in the enum: <field> must be one of: <values separated by comma and space>
    • below the minimum: <field> must be at least <n>
    • above the maximum: <field> must be at most <n>

One trap to handle: in Python, True is an instance of int, but a JSON true is not an integer. Your validator must treat a boolean as the wrong type for an integer field. (The Python documentation says the bool class is a subclass of int; JSON Schema lists boolean and integer as separate types.)

Check the ten calls below, numbered 1 to 10 in this order. Parse each as JSON (json.loads), run validate, and print the call number with its list of messages.

CALLS = [
    '{"service": "api", "query": "timeout", "limit": 10}',    # 1
    '{"service": "worker", "query": "retry"}',                 # 2
    '{"service": "payments", "query": "refund"}',              # 3
    '{"service": "billing", "query": "invoice", "limit": 0}',  # 4
    '{"service": "api", "limit": 5}',                          # 5
    '{"service": "api", "query": "oom", "limit": 51}',         # 6
    '{"service": "api", "query": "x", "limit": true}',         # 7
    '{"service": "api", "query": "x", "env": "prod"}',         # 8
    '{"service": "billing", "query": "", "limit": 50}',        # 9
    '{"service": "worker", "query": 123}',                     # 10
]

Add to your evidence file. For the message lines, write the first message your code returned for that call, exactly as printed:

t1_valid_count: <how many of the ten calls returned an empty list>
t1_valid_calls: <the numbers of those calls, ascending, comma-separated>
t1_msg_c3: <first message for call 3>
t1_msg_c4: <first message for call 4>
t1_msg_c5: <first message for call 5>
t1_msg_c6: <first message for call 6>
t1_msg_c7: <first message for call 7>
t1_msg_c8: <first message for call 8>
t1_msg_c10: <first message for call 10>

Stop at ten calls and the six features listed: no nested objects, no arrays, no other JSON Schema keywords.

Output: a script with one function, validate, and nine evidence lines.

Task 2 — Write a dispatcher

You now decide, for each call a model proposes, what your application does. Build a registry of four tools. logs_search uses the schema from Task 1. The others are:

Tool Required arguments Risk
logs_search as in Task 1 (limit optional) low
tickets_get ticket_id (string) low
tickets_delete ticket_id (string) high
deploy_rollback service (one of api, worker, billing), to_version (string) high

Each of those three schemas rejects any argument it does not list. Write decide(call), where a call is a tool name plus an arguments dictionary. It applies these rules in order and returns run, hold or refuse:

  1. The tool name is not in the registry: refuse.
  2. The arguments fail the tool’s schema (use your Task 1 validate): refuse.
  3. The tool’s risk is high: hold.
  4. Otherwise: run.

Then write render_for_approval(call), which returns the text a person would be shown before approving a held call: the tool name, one space, then the arguments as json.dumps(args, sort_keys=True). Showing the exact arguments, not a summary, is what lets the person catch a call that is valid but wrong.

Run these eight calls, numbered 1 to 8:

CALLS = [
    ("logs_search", {"service": "worker", "query": "retry", "limit": 5}),     # 1
    ("tickets_delete", {"ticket_id": "T-1"}),                                  # 2
    ("tickets_get", {"ticket_id": "T-2"}),                                     # 3
    ("deploy_rollback", {"service": "billing", "to_version": "v7"}),           # 4
    ("logs_search", {"service": "payments", "query": "x"}),                    # 5
    ("shell_exec", {"cmd": "ls"}),                                             # 6
    ("tickets_delete", {}),                                                    # 7
    ("tickets_get", {"ticket_id": "T-3", "verbose": True}),                    # 8
]

Finally simulate the approval step. Process the calls in order. A run call is executed at once, which here means appending its number to a list called executed. A hold call is put in a queue and does nothing yet. After all eight calls are processed, a person approves call 2 and rejects call 4: append approved calls’ numbers to executed in the order they were queued, and discard the rejected one. A refuse call never executes.

Add to your evidence file:

t2_decisions: <the eight decisions in call order, comma-separated>
t2_count_run: <how many calls were decided run>
t2_count_hold: <how many calls were decided hold>
t2_count_refuse: <how many calls were decided refuse>
t2_approval_text_call4: <the text render_for_approval returns for call 4, exactly>
t2_executed: <the final executed list, comma-separated, in order>

Output: a script with decide, render_for_approval and the simulation, and six evidence lines.

Task 3 — Define one real tool

Pick one operation from your own work that you would let a model ask for: reading a log, looking up a record, restarting a service. One operation, not a toolbox.

  1. Write its definition as a JSON file, tool.json, with three keys: name, description and input_schema. Each rule below can be checked by counting, so check it yourself before you record it:
    • name matches the pattern ^[a-zA-Z0-9_-]{1,128}$ and contains at least one underscore, so that it can carry a system prefix (for example tickets_get).
    • description has at least three sentences and contains both the exact phrase Use this when and the exact phrase Does not, so that it says when to use the tool and what it will not do.
    • input_schema uses only the features your Task 1 validator supports, lists at least one required argument, includes at least one enum, minimum or maximum, and sets additionalProperties to false.
  2. Apply this rule for risk: if the operation changes or deletes anything, its risk is high; if it only reads, its risk is low. The decision your Task 2 dispatcher gives a schema-valid call to it follows: run for low, hold for high.
  3. Write three sample calls: two that pass your schema and one that does not. Run all three through your Task 1 validate and record the counts.

Add to your evidence file. A sentence is counted as a full stop that is followed by a space or by the end of the text. A constraint is counted as each enum, minimum or maximum key that appears in the schema:

t3_name: <the tool name>
t3_name_has_underscore: <true or false>
t3_description_has_phrases: <true if the description contains both exact phrases, else false>
t3_description_sentences: <number of sentences in the description>
t3_required_count: <number of required arguments>
t3_constraint_count: <number of enum, minimum and maximum keys in the schema>
t3_additional_properties_false: <true if your schema sets additionalProperties to false, else false>
t3_operation_changes_state: <yes or no>
t3_risk: <low or high>
t3_valid_decision: <run or hold>
t3_valid_calls_passed: <how many of your two good calls validate returned no errors for>
t3_invalid_calls_rejected: <how many of your one bad call validate returned errors for>

Output: tool.json, three sample calls, and twelve evidence lines.

What carries forward

Keep tool.json, your validate script and chapter1-evidence.txt. The next chapter opens with the tool definition from Task 3: it exposes that tool through a standard protocol so that any compatible AI application can discover it and call it, and it starts from the name, description and schema you wrote here.

TechEazy Consulting training material — not affiliated with or endorsed by OpenAI, Anthropic or any other company named in this course.