Designing AI Agent Tools That Are Safe to Use


An agent becomes useful when it can do more than generate text: retrieve a record, create a draft, request a refund review, or update a ticket. That power arrives through tools. And every tool is a capability boundary between a probabilistic decision-maker and a real system.
Treating tools as ordinary helper functions is how agents end up with ambiguous actions, overbroad permissions, unhelpful failures, and traces that cannot explain what happened. Treating them as product APIs produces a better result: the model can discover the right action, the platform can enforce the right policy, and operators can inspect and improve the workflow.
Tools Are Capability Boundaries
OpenAI’s practical guide to building agents (opens in new tab) divides agent tools into data, action, and orchestration tools, and recommends standardized, well-documented, tested, reusable definitions. That is a helpful starting taxonomy, but the core engineering question is simpler: what exact capability does this tool grant?
| Tool category | Example | Main risk |
|---|---|---|
| Data | Find an order by an authorized account id | Overexposure or misleading context |
| Action | Create a refund-review request | Unauthorized or irreversible changes |
| Orchestration | Hand off a specialized task | Lost context, unclear ownership, looping |
The model chooses whether to propose a tool call. The tool implementation chooses whether that proposal is valid and authorized. Those are deliberately separate decisions.
model proposal → schema validation → authorization → domain validation
→ execution → audit event → structured resultNo prompt can replace the middle of this flow. “Only refund eligible orders” is useful instruction, but the server must independently verify order ownership, policy eligibility, amount limits, and approval state before it changes anything.
Make the Intended Action Obvious
Models select tools from names, descriptions, available context, and schemas. Design each one so a person unfamiliar with implementation can answer three questions: when should it be used, what does it change or retrieve, and what conditions limit it?
Avoid a vague tool such as this:
// Too much authority and too little guidance
manageCustomer({ action, customerId, payload });It combines unrelated read and write actions, hides possible payloads, and makes authorization difficult to reason about. Prefer narrow, domain-named operations:
getCustomerOrder({ customerId, orderId });
createRefundReview({ orderId, reasonCode });
addInternalOrderNote({ orderId, note });Narrowness is not ceremony. Each tool can have its own risk tier, validation rules, evaluation cases, ownership, and audit event. It also gives the agent a much cleaner choice. OpenAI’s experience with its in-house data agent describes restricting and consolidating tool calls to reduce ambiguity; the case study (opens in new tab) is a useful reminder that a smaller, clearer capability surface often beats a giant menu.
Write a description around observable preconditions and result, not implementation trivia:
create_refund_review
Create an internal review request for an already-delivered order. Use only after
retrieving the order and identifying a policy-supported reason. This does not
issue a refund. Returns the review id and its current status.The tool is safely limited even if a model ignores the description, but the description reduces avoidable wrong calls and makes traces readable for humans.
Validate and Authorize Outside the Model
Schema validation answers whether arguments have the expected shape. It is necessary and insufficient. A JSON object can contain a well-formed orderId that belongs to another customer, a stale version, or a state in which the action is forbidden.
Use structured schemas for the input and output, then enforce domain rules on the server:
type RefundReviewInput = {
orderId: string;
reasonCode: "damaged" | "late_delivery" | "wrong_item";
};
async function createRefundReview(
actor: { userId: string; organizationId: string },
input: RefundReviewInput
) {
const parsed = refundReviewSchema.parse(input);
const order = await orders.findVisibleToOrganization(
parsed.orderId,
actor.organizationId
);
if (!order) return { ok: false as const, code: "ORDER_NOT_FOUND" };
if (order.status !== "delivered") {
return { ok: false as const, code: "ORDER_NOT_ELIGIBLE" };
}
const review = await reviews.create({ ...parsed, requestedBy: actor.userId });
return { ok: true as const, reviewId: review.id, status: review.status };
}The actor should come from trusted session or workload identity, not a model-supplied userId. Treat tool arguments as untrusted input even when strict schemas are enabled. Strict structured output can make the model’s function-call arguments match a supplied schema, but it cannot determine business permission.
Never Hand an Agent a General-Purpose Escape Hatch
A raw database query, arbitrary URL fetcher, unconstrained shell command, or generic “update record” endpoint collapses many capabilities into one hard-to-govern tool. Build narrow adapters and grant the least privilege needed for the workflow.
Return Feedback That Supports Recovery
Tool results are part of the agent’s environment. A successful result should contain the identifier, stable status, and next information the agent needs—without dumping an entire database record into context. A failure should tell the agent whether retry is useful, a missing input is needed, a user confirmation is required, or a handoff should occur.
type ToolResult =
| { ok: true; reviewId: string; status: "pending" }
| { ok: false; code: "ORDER_NOT_FOUND"; retryable: false }
| { ok: false; code: "UPSTREAM_UNAVAILABLE"; retryable: true };Avoid returning a generic "failed". It forces the model to guess, encourages blind retries, and leaves users with confident but unsupported explanations. Equally, do not return raw stack traces or credentials. Give the model a stable code and safe detail; put rich diagnostics in the server-side trace.
Design for idempotency before exposing meaningful writes. Network failures and retries happen. If creating a review succeeds but the response is lost, the next call should not create a duplicate. An idempotency key tied to the run and intended action lets the tool return the original result safely.
const review = await reviews.createOnce({
idempotencyKey: `${runId}:${orderId}:refund-review`,
orderId,
reasonCode,
});For long-running actions, return a job id and explicit state rather than making the model wait on an uncertain operation. A separate read-only status tool can then report progress without accidentally repeating the write.
Risk-Tier Tools and Observe Every Action
Not all tools deserve the same agent autonomy. OpenAI’s guide recommends assessing tool risk from properties such as read versus write access, reversibility, account permissions, and financial impact, then adding confirmation or human escalation for higher-risk actions.
| Tier | Example | Typical control |
|---|---|---|
| Low | Read public product details | Automatic execution, rate limits, trace. |
| Medium | Add an internal note | Scoped authorization, validation, audit. |
| High | Cancel an order or move money | Explicit confirmation, policy check, human review. |
Risk tier is not a static label on a function name. It depends on the actor, the record, the amount, the environment, and the consequence. A write to a test ticket is not equivalent to a write to a customer’s production account.
Every execution should emit a structured audit event with run id, tool name/version, actor, sanitized arguments, authorization decision, latency, result code, and correlation ids. The Codex safety write-up (opens in new tab) illustrates the operational value of recording approval and tool-execution events. Observability is how you discover that a description is confusing, an agent is retrying too aggressively, or a policy is blocking legitimate work.
Test the Tool as an API and as an Agent Surface
Unit-test validation, authorization, idempotency, and domain outcomes without an LLM. Then add agent-level evaluations for tool selection and recovery.
Tool API cases
• Wrong schema → structured validation failure.
• Other organization’s order → no data and no action.
• Ineligible state → stable non-retryable result.
• Duplicate request key → same review, not a duplicate.
Agent cases
• Missing order id → agent asks one targeted question.
• Eligible order → agent reads first, then proposes the review.
• High-risk action → agent requests required confirmation.
• Transient outage → bounded retry or graceful handoff.Review tool additions like an API-security change. Who owns it? Which agents see it? Which resource scopes does it require? What is the rollback or compensating action? Which trace proves the result? A good agent tool does not merely make an action possible; it makes the safe action easy to choose and the unsafe action difficult to perform.
Key Takeaways
- Agent tools are capability boundaries, not ordinary convenience functions.
- Give each tool a narrow, domain-specific name, schema, precondition, result, and owner.
- Validate shape, authorization, and domain state on the server; never trust model-supplied identity or a prompt as enforcement.
- Return structured, safe outcomes that distinguish retryable failure, missing input, policy denial, and success.
- Risk-tier tools, add confirmation for consequential actions, and trace every execution so the system can be governed and improved.
Next, we will return to TypeScript and write our own declaration files—the public contracts that make JavaScript libraries understandable without pretending their runtime behaviour is already proven.