@zhanla/sdk-ts
v0.3.14
Published
TypeScript SDK for the zhanla CLI — define and run AI components locally
Readme
@zhanla/sdk-ts
TypeScript SDK for defining Zhanla components in code.
Export component instances from a TypeScript module, then run or upload them with the main zhanla CLI.
Installation
npm install @zhanla/sdk-tsRequires Node >=18.
Install a provider SDK only when your component makes LLM calls:
npm install @anthropic-ai/sdk
npm install openai
npm install @google/genaiInstall the CLI separately:
pip install zhanlaThe package ships an internal helper binary named zhanla-sdk-ts, but user-facing workflows should invoke zhanla.
Quick start
Create components.ts:
import { CodeEval, Tool } from "@zhanla/sdk-ts";
export const priorityTool = new Tool({
name: "priority_tool",
description: "Classify support ticket priority.",
key: "priority-tool",
inputSchema: {
type: "object",
properties: {
message: { type: "string" },
customerTier: { type: "string" },
},
required: ["message"],
},
fn: (kwargs) => {
const { message, customerTier } = kwargs as {
message?: string;
customerTier?: string;
};
const urgent =
message?.toLowerCase().includes("urgent") || customerTier === "enterprise";
return { priority: urgent ? "high" : "normal" };
},
outputSchema: {
type: "object",
properties: {
priority: { type: "string" },
},
required: ["priority"],
},
});
export const priorityEval = new CodeEval({
name: "priority_eval",
description: "Check predicted priority.",
key: "priority-eval",
fn: ({ model_response, expected_output }) => {
const response = model_response ? JSON.parse(model_response) : {};
const expected = expected_output ? JSON.parse(expected_output) : {};
return {
score: response.priority === expected.priority ? 1.0 : 0.0,
};
},
});Create tickets.json:
[
{ "_schema": { "message": "string", "expected_output": "object" } },
{ "message": "urgent outage", "expected_output": { "priority": "high" } }
]Run locally:
zhanla validate components.ts
zhanla run components.ts:priority-tool --dataset tickets.json --eval components.ts:priority-eval --dry-run --yesAfter zhanla login, drop --dry-run to sync definitions, dataset rows, and results to the web app.
Authoring rules
- Export component instances at module scope.
- Give every component an explicit stable
key. - Keys must contain only lowercase letters, digits, and hyphens.
- CLI target suffixes use
key, not export variable name or displayname. - Use JSON Schema objects for
inputSchemaandoutputSchema. - Prompt-backed components need
modelplus exactly one ofclientorrunnerfor local execution. Skillis prompt-only configuration and cannot be run directly.
Public API
import {
Tool,
CodeEval,
Skill,
Agent,
LLMProcessor,
LLMEval,
Runner,
Orchestration,
Step,
Conditional,
Checklist,
EvalTree,
Branch,
Edge,
Leaf,
wrap,
parseJsonResponse,
runComponent,
executeComponent,
flush,
} from "@zhanla/sdk-ts";Provider-backed components
Use client for normal provider usage:
import Anthropic from "@anthropic-ai/sdk";
import { Agent } from "@zhanla/sdk-ts";
export const supportAgent = new Agent({
name: "support_agent",
description: "Respond to support requests.",
key: "support-agent",
instructions: 'Answer clearly. Return JSON: {"answer": "..."}',
model: "claude-sonnet-4-6",
client: new Anthropic(),
outputSchema: {
type: "object",
properties: { answer: { type: "string" } },
required: ["answer"],
},
jsonRepair: true,
});client creates a Runner internally. Use runner when you need custom runner behavior or want to share one runner:
import OpenAI from "openai";
import { Runner } from "@zhanla/sdk-ts";
const runner = new Runner({ client: new OpenAI() });Google Gemini:
import { GoogleGenAI } from "@google/genai";
const geminiClient = new GoogleGenAI({ apiKey: process.env.GOOGLE_API_KEY });OpenRouter through the OpenAI-compatible client:
import OpenAI from "openai";
const openRouterRunner = new Runner({
client: new OpenAI({
baseURL: "https://openrouter.ai/api/v1",
apiKey: process.env.OPENROUTER_API_KEY,
}),
});Components
Tool
Use a Tool for deterministic TypeScript logic.
export const lookupCustomer = new Tool({
name: "lookup_customer",
description: "Fetch a customer record.",
key: "lookup-customer",
inputSchema: {
type: "object",
properties: { customerId: { type: "string" } },
required: ["customerId"],
},
fn: (kwargs) => {
const { customerId } = kwargs as { customerId: string };
return {
id: customerId,
email: "[email protected]",
};
},
outputSchema: {
type: "object",
properties: {
id: { type: "string" },
email: { type: "string" },
},
required: ["id", "email"],
},
});fn can be sync or async. Local execution passes one kwargs object. Non-object returns are wrapped as { result: value }.
Skill
Use a Skill for reusable prompt instructions.
export const summarizeSkill = new Skill({
name: "summarize_skill",
description: "Reusable summarization instructions.",
key: "summarize-skill",
instructions: "Summarize the provided text in one short paragraph.",
tools: [lookupCustomer],
});Skills can be attached to agents or orchestrations, but they are not top-level local runtime targets.
Agent
Use an Agent for LLM-backed work with optional tools, skills, and nested agents.
export const supportAgent = new Agent({
name: "support_agent",
description: "Respond to support requests.",
key: "support-agent",
instructions: 'Answer clearly. Return JSON: {"answer": "..."}',
model: "gpt-4.1",
runner,
tools: [lookupCustomer],
skills: [summarizeSkill],
outputSchema: {
type: "object",
properties: { answer: { type: "string" } },
required: ["answer"],
},
});LLMProcessor
Use an LLMProcessor for one prompt-defined transformation.
export const intentClassifier = new LLMProcessor({
name: "intent_classifier",
description: "Classify intent.",
key: "intent-classifier",
instructions: 'Return JSON: {"intent": "billing|technical|other"}',
model: "claude-sonnet-4-6",
runner,
outputSchema: {
type: "object",
properties: { intent: { type: "string" } },
required: ["intent"],
},
});Orchestration
Use Step to compose components into a DAG.
export const supportPipeline = new Orchestration({
name: "support_pipeline",
description: "Classify intent, then reply.",
key: "support-pipeline",
steps: [
new Step({ name: "classify", component: intentClassifier, next: ["reply"] }),
new Step({ name: "reply", component: supportAgent }),
],
});Use Conditional for routing:
new Step({
name: "route",
component: new Conditional({
condition: (state) => state.intent === "billing",
ifTrue: "billing_reply",
ifFalse: "general_reply",
}),
});Evals
CodeEval
CodeEval functions receive one kwargs object:
type CodeEvalKwargs = {
model_input?: string;
model_response: string;
expected_output?: string;
};Correct:
fn: ({ model_response, expected_output }) => {
const response = model_response ? JSON.parse(model_response) : {};
const expected = expected_output ? JSON.parse(expected_output) : {};
return { score: response.priority === expected.priority ? 1.0 : 0.0 };
}Wrong:
fn: (modelResponse, expectedResponse) => ({ score: 1.0 })The constructor rejects multi-parameter CodeEval functions. The fields are snake_case, not camelCase.
Non-object returns are wrapped as { score: value }.
modelResponseFormat defaults to "JSON" and can be set to "TEXT" or "YAML" for synced eval metadata. Current local and CLI-managed eval-only execution still passes strings to the function, so parse structured values yourself.
LLMEval
Use either instructions or questions, not both.
export const toneEval = new LLMEval({
name: "tone_eval",
description: "Evaluate tone.",
key: "tone-eval",
instructions: 'Return JSON: {"score": 0.0, "reason": "..."}',
model: "gpt-4.1",
runner,
outputSchema: {
type: "object",
properties: {
score: { type: "number" },
reason: { type: "string" },
},
required: ["score", "reason"],
},
});Checklist and EvalTree
export const answerQuality = new Checklist({
name: "answer_quality",
description: "Combine evals.",
key: "answer-quality",
evals: [priorityEval, toneEval],
weights: [0.8, 0.2],
});CLI usage
zhanla validate workflow.ts
zhanla upload workflow.ts:support-pipeline
zhanla run workflow.ts:support-pipeline --dataset tickets.json --eval evals.ts:answer-quality --dry-run --yes
zhanla run workflow.ts:support-pipeline --dataset tickets.json --web-eval answer-quality --yesLocal JSON datasets must start with a _schema metadata row. CSV datasets use their header row as fields.
Programmatic execution
Run a component in your app with trajectory tracking:
import { runComponent } from "@zhanla/sdk-ts";
const output = await runComponent(supportAgent, { message: "Need help" });Set ZHANLA_API_KEY="bm_kid_....bm_sec_..." to export production LLM trajectories. The exporter is fail-silent and batches in the background.
Use executeComponent(component, input) for bare dispatch without trajectory export. In serverless environments, call flush() before shutdown.
Observability helpers
Use wrap(client) when calling a provider client directly outside a runner-backed component:
import Anthropic from "@anthropic-ai/sdk";
import { wrap } from "@zhanla/sdk-ts";
const client = wrap(new Anthropic());Runner wraps its client internally, so do not wrap the same client first.
Use parseJsonResponse(text) to parse bare JSON or fenced JSON from model output.
Utilities
Lower-level helpers:
import {
collectExportedComponents,
toManifest,
executeComponent,
executeRow,
TraceContext,
traceStorage,
} from "@zhanla/sdk-ts";In normal CLI usage, you do not need these.
Common gotchas
- Components must be exported for discovery.
- Every component needs an explicit valid
key. - CLI target suffixes use
key, not export name or displayname. Skillcannot run directly.- Prompt-backed local execution requires
modelplusclientorrunner, not both. Tool.inputSchemamust be a JSON Schema object withtype: "object"andproperties.Toolshould includeoutputSchema; the CLI requires one for validation/upload.CodeEval.fnmust destructure a single kwargs object.LLMEvalmust provide exactly one ofinstructionsorquestions.- Local JSON datasets must start with
_schema;{"schema": ..., "rows": ...}is not accepted by the CLI.
