Cheela Labs
← ALL POSTS
5 MIN READViren Tanti

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

ADS required capability names to contain a dot. OpenAI, Anthropic and Google all reject a dot in tool function names. No conformant name was usable as a tool. How we found it and what ADS-2 changed.

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"}}

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})+$

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}$

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"
}

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

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.

agent discoverytool callingopen standardsADSLLM APIs