· 5 min read ·

Your Agent's Tool Description Is Its API Contract

Source: simonwillison

Simon Willison wrote about a new capability in Codex worth examining from a specific angle: the ability to define custom agents and expose them as tools that an orchestrating agent can call. The architecture posts have covered why this pattern exists (context isolation, parallelism, specialization). What they tend to skip is where the design work actually lives once you sit down to build one of these systems.

The description string you write when registering a subagent as a tool is not documentation. It is the interface.

What the Orchestrator Actually Sees

The OpenAI Agents SDK provides an as_tool() method for turning any Agent into a callable tool that an orchestrating agent can invoke. The invocation looks like this:

security_reviewer = Agent(
    name="security-reviewer",
    instructions="Review code for OWASP Top 10 vulnerabilities. "
                 "Be thorough about SQL injection, XSS, SSRF, and broken "
                 "access control patterns. Return structured findings.",
    model="o4",
    tools=[read_file]
)

orchestrator = Agent(
    name="coding-orchestrator",
    instructions="Implement requested features and coordinate quality checks.",
    tools=[
        security_reviewer.as_tool(
            tool_name="review_for_security",
            tool_description="Review a code snippet for security vulnerabilities."
        )
    ]
)

The orchestrating model never sees security_reviewer.instructions. It sees the tool_name and tool_description you passed to as_tool(). When it reasons about which tools to call and when, those two strings are the entirety of its knowledge about what this subagent does.

That description in the example above is poor. “Review a code snippet for security vulnerabilities” tells the orchestrator almost nothing about when to call this tool, what to pass in, or what it will get back. The orchestrator will make inferences from context, some of them wrong.

Three Things a Good Description Communicates

A function signature in compiled code communicates precisely: the name (intent), the parameters (input structure), and the return type (output contract). The compiler enforces all three; a mismatch is an error at compile time.

Tool descriptions communicate the same three things in natural language, with no enforcement. The orchestrating model infers intent, expected inputs, and expected outputs entirely from text. Descriptions that produce reliable behavior are explicit about all three:

Weak:
"Review code for security issues."

Strong:
"Review a provided code block or relative file path for security vulnerabilities, 
focusing on OWASP Top 10 patterns: SQL injection, XSS, SSRF, insecure authentication, 
and broken access control. Input should be either a raw code string or a path to a 
single file. Returns a structured list of findings, each with severity 
(critical/high/medium/low), vulnerability type, and line number where applicable. 
Call this after implementing any feature that accepts user input, handles 
authentication, or accesses persistent storage."

The stronger version does something the weak version omits entirely: it communicates triggering conditions. Orchestrators have to decide not just how to use a tool but when it applies. If the description says nothing about when to call the tool, the model falls back to inference from the name and general task context. That inference is less reliable than an explicit statement. Adding “call this after implementing any feature that accepts user input” is not padding; it is the primary mechanism by which the orchestrator learns the tool’s appropriate scope.

The Silent Failure Mode

When a multi-agent system produces wrong behavior, the fault is often in the interface description rather than in the subagent’s instructions.

Consider: the orchestrator calls review_for_security on a utility function that generates cache key strings. The security reviewer runs, finds nothing concerning (correctly), and returns an empty findings list. Nothing errored. The log shows a successful tool invocation. But the orchestrator consumed an extra API call on an irrelevant target and, if it was supposed to review an authentication endpoint it wrote earlier in the same session, may skip that review because it considers the security check done.

This failure is invisible at the subagent level. The subagent did exactly what it was asked. The orchestrator made a calling decision based on an imprecise description, and the result was plausible-looking output that masked a gap in the actual work.

Debugging this requires reasoning about what the orchestrator understood from each tool description when it made each decision. The Agents SDK run trace surfaces the model’s reasoning turns alongside tool calls, which makes this inspectable. The pattern to look for: when the orchestrator’s stated reason for calling a tool does not match the triggering conditions you intended, the description needs revision, not the agent’s instructions.

The Microservice Parallel

This design problem is recognizable from distributed systems work. Microservice architecture places most of the engineering difficulty at service boundaries: what each service owns, what it accepts, what it returns, and when a caller should prefer it over adjacent services. The implementation of each service is the easier part once the boundaries are clearly defined.

REST APIs addressed the documentation problem with OpenAPI/Swagger: a structured format describing endpoints, parameters, and response schemas. Well-maintained specs function as machine-readable interface contracts, stable enough to generate clients from and run validation against.

Tool descriptions for LLM agents serve the same function, but the “machine” consuming them reasons in natural language rather than parsing a schema. The description has to work on two levels simultaneously: as documentation for humans writing orchestrator instructions, and as inference substrate for a model choosing which tools to invoke and how.

There is no Swagger validation equivalent here yet. If the orchestrator passes a code block to a tool that expects a file path, the subagent may handle it gracefully, produce a degraded result, or fail in an opaque way. The SDK does not enforce input contracts at the type level because inputs often arrive as strings by design. The description carries the full weight of communicating expected input structure, output format, and appropriate scope.

Some teams working with multi-agent systems have started writing tool descriptions with the same rigor they bring to OpenAPI specs: enumerating input types explicitly, documenting output structure, and writing “when to use” sections the way Stripe and Twilio document API endpoints. The analogy is apt and the practice improves orchestrator behavior measurably.

What This Means for How You Build

The Codex subagent feature is part of a broader trend toward agent composition as a normal software development practice. The SDK mechanics are stable and well-documented. The design work is the part that requires careful thought: defining what each agent is responsible for, how to scope its inputs and outputs, and how to describe it precisely enough that an orchestrator uses it correctly.

That is an interface design problem. It looks more like writing an API specification than it looks like writing a system prompt. The relevant skills are the ones that make microservices maintainable: clear boundary definition, explicit contracts, and documentation written for a caller that will make decisions based on it without the opportunity to ask clarifying questions.

Teams that bring interface design discipline to custom agent definitions get more reliable orchestrator behavior. Teams that treat the description as an afterthought get agents that are called at the wrong times, passed the wrong inputs, and whose outputs get misinterpreted. The architecture is sound. The craft is in the description.

Was this interesting?