DEV Community

Cover image for The spec required a dot. Every tool-calling API rejects it.
Viren Tanti
Viren Tanti

Posted on • Originally published at blogs.cheelalabs.com on

The spec required a dot. Every tool-calling API rejects it.

We shipped a specification with a rule that made it unusable for the single thing
almost everyone would want to do with it. It took an HTTP 400 to notice.

Here is the error, from a request built directly out of a conformant manifest:

HTTP 400
{"error":{"code":"invalid_request_error",
          "message":"tool function names must match ^[a-zA-Z0-9_-]{1,64}$",
          "param":"tools[0].function.name"}}
Enter fullscreen mode Exit fullscreen mode

Nothing was malformed. The manifest validated. The capability name was exactly
what the specification required it to be. That was the problem.

Two rules that cannot both be satisfied

The Agent Discovery Specification defines capability names as namespacedName:

^[A-Za-z][A-Za-z0-9-]{0,63}(\.[A-Za-z][A-Za-z0-9-]{0,63})+$
Enter fullscreen mode Exit fullscreen mode

The trailing + is the whole story. It makes at least one dot mandatory.
com.example.lookupOrder is conformant. lookupOrder is not.

OpenAI's Chat Completions API constrains tools[].function.name to:

^[a-zA-Z0-9_-]{1,64}$
Enter fullscreen mode Exit fullscreen mode

No dot. Anthropic's tools[].name and Google's functionDeclarations[].name
impose equivalent restrictions, and every OpenAI-compatible endpoint inherits
the first one — in practice that means OpenRouter, Together, Groq, and most
self-hosted gateways.

These rules are not merely awkward together. They are mutually exclusive. The
specification requires the character the tool APIs forbid. No conformant
capability name could ever be passed to a tool-calling API.

The ADS capability name pattern, whose trailing plus makes at least one dot mandatory, shown above the tool-calling API pattern which permits only letters, digits, underscore and hyphen. com.example.lookupOrder is conformant under the first and rejected by the second.

This is not one vendor's quirk, and it is not something to wait out. It is the
industry's settled convention, and we wrote a spec that could not meet it.

The part that is worse than the 400

An error at the boundary is the good case. You see it immediately and you fix it.

The real damage was quieter. Any client can cope — stripping a namespace is
trivial. But the specification never said how, so every client invented its own
mapping, and clients that agreed perfectly on the manifest stopped agreeing the
moment they built tools from it.

Given com.example.orders.get:

Strategy Resulting tool name
Take the last segment get
Replace dots with hyphens com-example-orders-get
Take the last two segments orders-get

Three clients, one manifest, three different tool names. A model trained or
prompted against one client's names does not transfer to another's.

Then it gets sharper. Consider a manifest advertising both:

  • com.example.orders.get
  • com.example.refunds.get

Under the last-segment strategy, both become get. The two capabilities collapse
into one identifier, silently. The model sees a single tool where two were
advertised, and which one actually executes depends on how the client happened to
build its dictionary. No error is raised. Nothing validates as broken. A refund
runs where an order lookup was intended.

One manifest advertising two capabilities that share a leaf segment, read by three clients. The client that takes the last segment maps both to the single name get, collapsing them silently. The client that replaces dots with hyphens keeps them distinct, which is what ADS-2 now requires. The client that takes the last two segments keeps them distinct here but is still truncating, which ADS-2 prohibits.

There is also a length trap that has nothing to do with dots. namespacedName
permits 64-character segments with no limit on segment count, so a perfectly
conformant name can exceed the 64-character tool-name ceiling on its own, before
any transformation is applied.

What ADS-2 changed

The fix is deliberately small. ADS-2
adds one optional field, invocationName, and — more importantly — makes the
derivation rule normative so clients stop diverging.

{
  "name": "com.example.shop.orders.get",
  "invocationName": "orders-get",
  "version": "1.0.0"
}
Enter fullscreen mode Exit fullscreen mode

invocationName must match ^[A-Za-z][A-Za-z0-9_-]{0,63}$ and must be unique
across the manifest. Note that underscores are permitted here and prohibited in
name — the two fields have genuinely different jobs, and no single string could
have done both.

When it is absent, a client that needs a constrained identifier must derive
one by replacing every . with -, and must not derive one by truncating to
a subset of segments.

Truncation is prohibited rather than discouraged, and that distinction was the
main thing worth arguing about. Discouraging it would have left the orders.get
/ refunds.get collision legal. The failure it produces is silent, produces a
wrong action rather than an error, and is invisible in the manifest — so it has
to be a MUST NOT.

If the derived identifier still exceeds 64 characters, the client must treat the
capability as not invocable through that interface and should say why, rather
than truncating to fit. Failing loudly beats guessing.

One decision we are fairly confident about: invocationName carries no identity.
name remains the sole identifier. Two manifests describing the same capability
must agree on name; they need not agree on invocationName. Presentation
concerns should not become identity, or you get two competing identifiers and the
same divergence problem one layer up.

Every manifest valid under 0.2.0 remains valid under 0.3.0. The field is
optional, and the derivation rule describes what careful clients were already
doing.

What this looks like in practice

Our reference implementation serves 14 capabilities, each carrying both:

com.example.cheelashop.cart-add-item     →  cart-add-item
com.example.cheelashop.cart-clear        →  cart-clear
com.example.cheelashop.cart-remove-item  →  cart-remove-item
Enter fullscreen mode Exit fullscreen mode

The left column is identity, stable and namespaced. The right column is what
gets handed to a model. They are related by an explicit rule rather than by each
client's guess.

The lesson we would like to have had earlier

A discovery format is not consumed in isolation. It is consumed by something,
and that something has its own constraints — which are not negotiable just
because your specification is elegant.

We validated our names against our own schema. We did not validate them against
the API that the overwhelmingly common use of a discovered capability leads
straight to. The schema was correct and the design was wrong, and no amount of
internal validation would have caught it. It took building a client and getting
a 400.

If you are writing a spec, the question is not only "is this internally
consistent." It is "what will the first person to use this pipe it into, and what
will that thing say."


ADS is MIT-licensed and open to
proposals.
If you think the derivation rule is wrong, or you have a case where prohibiting
truncation causes more harm than it prevents, that is exactly the kind of
disagreement the process exists for.

If you want to try the spec end to end, implementing agent discovery takes about
ten minutes
.

Top comments (0)