Cheela Labs
REFERENCE

@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

Terminal
npm install @cheela/sdk

Usually 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.

namestringrequired

Unique within the runtime. Must match ^[A-Za-z][A-Za-z0-9-]{0,63}$ — see below.

descriptionstring

Read by the model to decide when to call this. Write it as a decision rule, not a label.

versionstring

Semver. 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.

requiresEndUserboolean

Marks 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.

inputJsonSchemaunknown

Pre-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.

outputJsonSchemaunknown

As above, for output.

Action

What a capability does. Never leaves your infrastructure.

namestringrequired

Identifies the action within the capability.

descriptionstring

For 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> | TOutputrequired

Your implementation. May be sync or async.

metadataRecord<string, unknown>

Arbitrary context.

One capability, one action

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.

TypeScript
interface ActionContext {
  readonly executionId: string;
  readonly capability: string;
  readonly startedAt: number;
  readonly endUserToken?: string;
}
FIELDNOTES
executionIdCheela's id for this execution. Log it to join your own logs to the trace.
capabilityThe capability being served.
startedAtEpoch milliseconds, taken when dispatch began.
endUserTokenThe 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.

TypeScript
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.

TypeScript
interface Schema<T> {
  parse(value: unknown): T;
}

Zod satisfies it as-is. So does anything you write:

TypeScript
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

Pattern
^[A-Za-z][A-Za-z0-9-]{0,63}$
isValidCapabilityName(name: string) => boolean

Tests a name against the pattern.

describeCapabilityNameError(name: string) => string

Explains 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.

TypeScript
isValidCapabilityName("catalog-search"); // true
isValidCapabilityName("catalog.search"); // false
isValidCapabilityName("catalog_search"); // false
isValidCapabilityName("2fa-verify");     // false — must start with a letter

The 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.

TypeScript
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.