Capabilities
A capability is one thing your product can do, described precisely enough that a model can decide when to call it and what to pass.
The shape
A registration is two objects. The first describes the capability and is published to Cheela. The second implements it and never leaves your infrastructure.
runtime.register(
{
// Published — this is what the model sees.
name: "catalog-search",
description: "Searches the product catalog by free text",
version: "1.2.0",
input: z.object({
query: z.string(),
limit: z.number().int().max(50).default(10),
}),
output: z.object({ products: z.array(productSchema) }),
},
{
// Private — this runs on your server.
name: "search",
async handler(context, input) {
return { products: await catalog.search(input.query, input.limit) };
},
},
);The split matters. Everything in the first object is a public contract: it goes into the deployment, gets handed to the model as a tool definition, and — if you publish a manifest — becomes readable by strangers. The second object is yours.
Naming rules
Capability names must match ^[A-Za-z][A-Za-z0-9-]{0,63}$: start with a letter, then letters, digits, and hyphens, up to 64 characters.
That is narrower than it looks, because it is the intersection of two rules that do not overlap:
- Tool-calling APIs require
^[a-zA-Z0-9_-]{1,64}$. A dot is rejected outright — OpenAI and every OpenAI-compatible endpoint answers 400 rather than ignoring it. A name the model cannot be given is a capability that can never be invoked. - The Agent Discovery Specification builds published names as
namespace.capability, and each segment must match[A-Za-z][A-Za-z0-9-]{0,63}. Underscores are not permitted.
Hyphens are the only separator that satisfies both. catalog-search is legal; catalog.search fails the first rule and catalog_search the second. The dots the discovery spec wants come from your namespace in cheela.config.ts, not from the capability name.
Runtime.register() throws on a bad name immediately, and the error names the offending character and suggests a replacement. The alternative is a provider 400 on your first real execution — long after the name has been published in a manifest that strangers may already have cached.
Schemas
input and output accept anything with a parse(value) method, which is the whole Schema interface. Zod satisfies it; so does anything you write yourself.
export interface Schema<T> {
parse(value: unknown): T;
}Schemas do three separate jobs:
| JOB | WHEN |
|---|---|
| Tell the model | Serialized to JSON Schema at deploy time and attached to the tool definition. |
| Validate input | Before your handler runs. A bad shape fails the call, not your code. |
| Validate output | After your handler returns, before the result goes back to the model. |
That is legitimate for something like store-hours, and a bug for anything else. cheela deploy prints a warning naming every capability it published without one — check the schema is exported and serializable if you see your capability listed.
Generic types are preserved through registration, so input in your handler is typed from the schema. You should never need input as { query: string }.
Versions
version is required by the Agent Discovery Specification and therefore by cheela deploy, even when a capability has no schemas. Use semver, and bump it when the input or output shape changes.
Deployments are versioned separately and independently: each cheela deploy creates a new deployment version covering the whole capability set, while a capability’s own version describes just its contract.
Writing descriptions
The description is not documentation for humans. It is the only thing telling a model when to reach for this capability rather than another one, so it should read as a decision rule.
Say when, not just what
- Weak:
"Order lookup" - Better:
"Looks up one order by its id. Use when the customer names a specific order."
Disambiguate near neighbours
If you have both order-status and order-history, each description should say what the other one is for. Models pick wrong far more often between two plausible tools than between a right one and an irrelevant one.
Describe fields too
Field descriptions survive into the JSON Schema the model reads. A limit with a documented default and maximum gets passed sensibly; a bare number gets guessed at.
One capability, one action
A capability maps to exactly one action. Runtime.register() keys on the capability name and dispatch is by that name alone, so a second action would have no way of being reached. The deployment API accepts at most one and rejects more, rather than silently dropping the extras.
The action carries the handler, an optional description, and optional permissions — string tags checked against the permission set the Runtime was constructed with.
const runtime = new Runtime({ permissions: ["orders:read"] });
runtime.register(capability, {
name: "lookup",
permissions: ["orders:read"], // present, so this passes
handler,
});A missing permission throws before the handler runs. This is a local guard for your own code — it is not a Cheela-side authorization system, and it is not how you protect user data. For that, see End-user identity.