AI agents that do real work, not API wrappers
Most so called agents are one prompt behind a text box. What separates those from a system that can actually be trusted with a workflow: tools, state, boundaries and a way to fail safely.
There is a large gap between calling a language model and building something that can be handed a workflow and left alone with it. Most projects described as agents sit on the near side of that gap: a prompt, a text box, and a response.
The difference is not the model. It is everything around it.
A model that only talks cannot do anything
An agent needs tools, and a tool is just a function you are willing to let the model call. The discipline is in the description. The model chooses a tool from its description alone, so a vague one produces a wrong call.
{
name: "refund_order",
// Say when NOT to use it. This is the line that stops
// the model reaching for it on a general pricing question.
description:
"Issue a refund for a delivered order. Only for orders " +
"already marked delivered and within the 30 day window. " +
"Do not use for cancellations before dispatch.",
input_schema: {
type: "object",
properties: {
order_id: { type: "string" },
reason: { type: "string" },
},
required: ["order_id", "reason"],
},
}Give it less power than you think it needs
The instinct is to hand the agent broad access so it can handle anything. That is exactly backwards. Every tool you expose is something that can be called at the wrong moment, and the blast radius of a wrong call is the whole point of the design.
- Read widely, write narrowly. Let it query freely. Let it change very little.
- Put a confirmation step in front of anything that moves money, sends a message to a customer, or deletes something.
- Scope credentials to the agent, not to your admin account, so a mistake is bounded by permissions rather than by the prompt.
- Log every tool call with its inputs. When something goes wrong you need the call, not a summary of the conversation.
State belongs outside the model
A conversation is not a database. If the agent needs to know that a ticket is open, that a job is halfway through, or that a step already ran, that belongs in your own store and gets passed in. Relying on the transcript to remember it means a truncated context quietly loses the fact.
This also makes the thing testable. With state outside, you can replay a run from a known point instead of trying to recreate a conversation.
Decide what happens when it is wrong
It will be wrong. The question a design has to answer is what that costs. An agent that drafts a reply for review and an agent that sends it are the same code with very different risk.
Start with drafting. Watch what it produces for a while against real input. Move to sending only for the categories where the record is good, and keep the rest under review. That progression is the actual work, and it is the part that gets skipped.
A working checklist
- Write the tool descriptions before the prompt. If a tool is hard to describe, it is the wrong shape.
- Keep durable state in your own store and pass it in.
- Cap and summarise history rather than letting it grow.
- Make every write reversible, or put a human in front of it.
- Log tool calls with inputs and outputs from the first day.
- Define the handover path before you launch, not after the first bad answer.
None of this is about the model being clever enough. It is about building a system that stays useful on the days the model is not.