@cheela/sdk
The vocabulary every other package shares. @cheela/sdk holds no runtime behaviour beyond validation — it is types, two factories, and the naming rules the CLI and the control plane both enforce.
Install
npm install @cheela/sdkUsually installed alongside @cheela/runtime, which re-exports nothing from it — you import types from here and the Runtime class from there.
Capability
What a capability is. Everything here is published to Cheela at deploy time.
namestringrequiredUnique within the runtime. Must match ^[A-Za-z][A-Za-z0-9-]{0,63}$ — see below.
descriptionstringRead by the model to decide when to call this. Write it as a decision rule, not a label.
versionstringSemver. Required by the Agent Discovery Specification, so cheela deploy requires it too — even when a capability has no schemas.
inputSchema<TInput>Validates input before the handler runs, and is serialized to JSON Schema for the model.
outputSchema<TOutput>Validates whatever the handler returns, before it goes back.
requiresEndUserbooleanMarks a capability that acts on behalf of a person. The runtime refuses the call before the handler runs when no credential is present, and the public broker refuses it before metering.
metadataRecord<string, unknown>Arbitrary context, stored with the deployment.
inputJsonSchemaunknownPre-serialized JSON Schema. Set by the server when rebuilding a capability from a stored manifest, where the original schema object no longer exists. Locally-defined capabilities set input and leave this alone.
outputJsonSchemaunknownAs above, for output.
Action
What a capability does. Never leaves your infrastructure.
namestringrequiredIdentifies the action within the capability.
descriptionstringFor your own readers. Published with the deployment.
permissionsreadonly string[]Checked against the permission set the Runtime was constructed with. A missing one throws before the handler runs.
handler(context, input) => Promise<TOutput> | TOutputrequiredYour implementation. May be sync or async.
metadataRecord<string, unknown>Arbitrary context.
Dispatch is by capability name alone, so a second action would be unreachable. The deployment API accepts at most one and rejects more rather than dropping them silently.
ActionContext
What a handler is told about the call it is serving.
interface ActionContext {
readonly executionId: string;
readonly capability: string;
readonly startedAt: number;
readonly endUserToken?: string;
}| FIELD | NOTES |
|---|---|
| executionId | Cheela's id for this execution. Log it to join your own logs to the trace. |
| capability | The capability being served. |
| startedAt | Epoch milliseconds, taken when dispatch began. |
| endUserToken | The caller's credential, untouched. Undefined means anonymous — which cannot happen for a capability marked requiresEndUser. |
This lives in the SDK rather than the runtime because the handler signature does. Typing it as unknown meant nobody could read the context without a cast, which in practice meant nobody read it.
createCapability, createAction
Identity functions that pin generics, so schema types survive to your handler.
import { createCapability, createAction } from "@cheela/sdk";
const searchCapability = createCapability({
name: "catalog-search",
version: "1.0.0",
input: z.object({ query: z.string(), limit: z.number().default(10) }),
});
const searchAction = createAction({
name: "search",
async handler(context, input) {
// input.query is string, input.limit is number — no cast.
return catalog.search(input.query, input.limit);
},
});
runtime.register(searchCapability, searchAction);Without the generic, these collapse to Capability<unknown, unknown> and every handler has to open with a cast the API left no way to avoid. Passing object literals directly to register() infers just as well; the factories are for when you want to define them separately.
Schema and validate
The entire schema contract is one method.
interface Schema<T> {
parse(value: unknown): T;
}Zod satisfies it as-is. So does anything you write:
const positiveInt: Schema<number> = {
parse(value) {
if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
throw new Error("Expected a positive integer");
}
return value;
},
};validate(schema, value) runs a schema and normalises whatever it throws into a ValidationError, preserving the original as cause. The runtime calls it for you on both input and output.
Capability names
^[A-Za-z][A-Za-z0-9-]{0,63}$isValidCapabilityName(name: string) => booleanTests a name against the pattern.
describeCapabilityNameError(name: string) => stringExplains a rejection in terms of what was probably typed, and suggests a repaired name. Used by the runtime, the CLI, and the control plane so all three give the same answer.
isValidCapabilityName("catalog-search"); // true
isValidCapabilityName("catalog.search"); // false
isValidCapabilityName("catalog_search"); // false
isValidCapabilityName("2fa-verify"); // false — must start with a letterThe pattern is the intersection of two rules: tool-calling APIs reject dots, and the Agent Discovery Specification rejects underscores. Hyphens satisfy both. Full reasoning in Capabilities.
ValidationError
Thrown when an SDK definition or a validated value is invalid.
import { ValidationError } from "@cheela/sdk";
try {
await runtime.execute("catalog-search", { query: 42 });
} catch (error) {
if (error instanceof ValidationError) {
console.error(error.message, error.cause);
}
}The prototype chain is repaired in the constructor, so instanceof works across module and bundler boundaries where a plain extends Error would not.