On this page4 sections
“Is the catalog service healthy?” is a small question with a missing piece: someone has to look.
An assistant can produce a plausible answer without that lookup. Give it a status tool, though, and it can ask the application to check. The application decides whether the request is allowed, runs the operation and returns a result for the model to read. Now the next answer has an observation behind it.
An agent application can repeat this exchange, using a result to choose another step. The model interprets and proposes; the surrounding software keeps the tools, state and limits. The word “agent” alone does not tell you whether it can read a status, restart a service or do neither. That depends on the application carrying out its requests.
Where “please check” becomes a lookup
Consider a hypothetical assistant with one read-only function, get_service_status, for two invented services. Its user asks for the catalog service’s recorded health. The model needs a description of the function and the inputs it accepts, called a tool schema, so it knows how to ask. That description cannot perform the lookup or grant permission; the application still has to decide and execute.
The OpenAI function-calling guide describes this cycle of a tool request, application execution and a returned result. Other providers offer similar ideas in different message formats. We can see the application’s part with a small Python fixture, without calling any of them.
The request below is constructed, not a captured model response, and the status values are synthetic. Run it with Python 3. It uses only the built-in json module, needs no extra package or API key, and makes no live infrastructure query.
import json
status = {"catalog": "healthy", "search": "degraded"}
call = {
"id": "call-example-1",
"name": "get_service_status",
"arguments": '{"service":"catalog"}',
}
def execute(call):
if call["name"] != "get_service_status":
raise ValueError("unsupported tool")
args = json.loads(call["arguments"])
if not isinstance(args, dict) or set(args) != {"service"}:
raise ValueError("unexpected arguments")
service = args["service"]
if not isinstance(service, str) or service not in status:
raise ValueError("service outside allowed scope")
return {"service": service, "status": status[service],
"provenance": "synthetic fixture"}
result = execute(call)
reply = {"call_id": call["id"], "output": json.dumps(result)}
print(json.dumps(reply, indent=2))
Follow the request into execute. The function name has to be the one the application supports. The arguments must contain exactly a service name, and that name must be a string identifying one of the two allowed services. Only then does the code read the dictionary.
The returned result carries the same call ID as the request, so the application can tell which question it answers. In a real integration, that result must be sent back in the provider’s documented continuation format. This plain dictionary keeps the exchange visible without claiming that every API uses the same message shape.
The fixture prints:
{
"call_id": "call-example-1",
"output": "{\"service\": \"catalog\", \"status\": \"healthy\", \"provenance\": \"synthetic fixture\"}"
}
There are two layers to that reply. The outside identifies the call. Inside output is text containing another JSON object, which is why its quotation marks have backslashes. That inner object holds the observation: catalog, healthy, synthetic fixture. Printing it finishes this little program; it does not resume a model.
A useful model continuation would say that the fixture records catalog as healthy. It would keep “synthetic fixture” attached to the claim, rather than announce that someone had checked a real service. Replacing the dictionary with a status API would also require a trustworthy observation time, error handling and permission to read that service.
Now change the fixture’s catalog entry to degraded and run it again. The returned value changes without any retraining. The ordinary function supplies the value; the model’s job, in a connected application, is to interpret the question and explain what came back.
That separation is useful when something goes wrong. Keep the proposed operation and arguments, the application’s execution decision, the result and the explanation. You can then tell whether the assistant asked the wrong question, the lookup failed, or the answer misread a perfectly good result.
The request the application turns down
Our status function knows how to look up two names. What if a request arrives for restart_service instead? The first check rejects it. Add a force argument and the argument check rejects it. Ask for an unlisted service and the scope check does the same. Restore the original fixture between these tests so each result has one cause.
The lookup never gets that far for those requests. This is software enforcing what the application will allow, rather than relying on a sentence asking the model to use only safe tools. Such guidance can help the model choose, but the checks still need to exist if it chooses badly.
Nothing mysterious happened inside the tool. It remains a deterministic dictionary lookup, returning the stored value for an allowed name. The model can explain that result without being responsible for inventing the value. That gives an investigation somewhere firmer to start than how confident the answer sounded.
Looking is one thing. Restarting is another.
A read-only status check lets us learn the exchange without changing a service. A restart introduces a different question: was this particular action approved against the state that still exists now? “Improve service health” does not automatically authorize restarting every component the assistant thinks might be involved.
Then imagine the restart request times out. The destination may already have acted while its reply was lost. Sending the same request again can repeat work that succeeded the first time. Keep the outcome marked unknown and inspect the destination before deciding to retry; the write-reconciliation guide follows that recovery problem.
Some effects offer no simple way back. An external notification may already have reached another person. The application needs recovery behavior suited to the action, not an assumption that every tool call can be undone.
Does the next step need an interpreter?
The catalog dictionary needs no model to return its value. If a stable rule already tells the application what to do next, ordinary automation is usually easier to inspect. Adding a model would add another decision without necessarily making the job better.
An agent loop becomes useful when the question is less structured, or when interpreting one result helps choose among bounded next steps. The model might help decide which lookup answers the question; the application still controls the available operations and their limits.
Before expanding that list of tools, try a missing result and a question the available data cannot answer. Set limits on steps and spending, and keep each call paired with its reply. Then return to the small question that started this: is catalog healthy? You should be able to follow the answer back through the lookup to what was actually observed, including when the application had nothing useful to report.
Sources & context
Sources linked in this article. Read alongside the author’s analysis; a citation does not independently verify a publisher’s claims.
- OpenAI function-calling guidedevelopers.openai.com
