@zhanla/sdk-ts
v0.3.13
Published
TypeScript SDK for the zhanla CLI — define and run AI components locally
Readme
@zhanla/sdk-ts
@zhanla/sdk-ts is the TypeScript SDK for defining zhanla components in code.
You export component instances from a TypeScript module, then run them with zhanla or the bundled @zhanla/sdk-ts helper CLI.
Installation
npm install @zhanla/sdk-tsRequires Node >=18.
Provider packages such as @anthropic-ai/sdk, openai, and @google/genai are optional. Install them only if you use Runner with those clients.
Every component requires an explicit stable key. Use a lowercase, hyphenated identifier such as support-agent.
Quick Start
import Anthropic from "@anthropic-ai/sdk";
import { Agent, CodeEval, Runner } from "@zhanla/sdk-ts";
const runner = new Runner({
client: new Anthropic(),
});
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",
runner,
outputSchema: {
type: "object",
properties: {
answer: { type: "string" },
},
required: ["answer"],
},
});
export const supportEval = new CodeEval({
name: "support_eval",
description: "Check whether an answer was returned.",
key: "support-eval",
fn: (kwargs: unknown) => {
const { model_response } = kwargs as { model_response?: string };
const parsed = model_response ? JSON.parse(model_response) : {};
return { score: typeof parsed.answer === "string" ? 1.0 : 0.0 };
},
});Run it with the CLI:
zhanla run components.ts:support_agent --dataset tickets.json --eval components.ts:support_evalPublic API
import {
Tool,
CodeEval,
Skill,
Agent,
LLMProcessor,
LLMEval,
Runner,
Orchestration,
Step,
Conditional,
Checklist,
EvalTree,
Branch,
Edge,
Leaf,
wrap,
parseJsonResponse,
} from "@zhanla/sdk-ts";TypeScript discovery is export-based. Only exported component instances are discoverable.
Components
Tool
Use a Tool for deterministic TypeScript logic.
export const lookupCustomer = new Tool({
name: "lookup_customer",
description: "Fetch a customer record by ID.",
key: "lookup-customer",
inputSchema: { type: "object", properties: {} },
fn: (kwargs: unknown) => {
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"],
},
});Notes:
fncan be sync or async.- Local execution passes a single
kwargsobject. - Non-object return values are wrapped as
{ result: value }.
Skill
Use a Skill for reusable instructions and tool access.
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],
});Skill is a non-executable configuration construct. It is not a local runtime entry point.
Agent
Use an Agent for LLM-backed execution with instructions, tools, skills, and nested agents.
import OpenAI from "openai";
import { Agent, Runner } from "@zhanla/sdk-ts";
const runner = new Runner({
client: new OpenAI(),
});
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-mini",
runner,
tools: [lookupCustomer],
skills: [summarizeSkill],
outputSchema: {
type: "object",
properties: {
answer: { type: "string" },
},
required: ["answer"],
},
});LLMProcessor
Use an LLMProcessor for prompt-defined transformation steps.
import Anthropic from "@anthropic-ai/sdk";
import { LLMProcessor, Runner } from "@zhanla/sdk-ts";
const runner = new Runner({
client: new Anthropic(),
});
export const intentClassifier = new LLMProcessor({
name: "intent_classifier",
description: "Classify intent.",
key: "intent-classifier",
instructions: 'Return JSON: {"intent": "billing|technical|other"}',
model: "claude-haiku-4-5",
runner,
outputSchema: {
type: "object",
properties: {
intent: { type: "string" },
},
required: ["intent"],
},
});LLMEval
Use an LLMEval for LLM-backed evaluation logic.
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-mini",
runner,
outputSchema: {
type: "object",
properties: {
score: { type: "number" },
reason: { type: "string" },
},
required: ["score", "reason"],
},
});Orchestration
Use an Orchestration to compose steps into a DAG.
export const supportPipeline = new Orchestration({
name: "support_pipeline",
description: "Classify intent, then draft a reply.",
key: "support-pipeline",
steps: [
new Step({
name: "classify",
component: intentClassifier,
next: ["reply"],
}),
new Step({ name: "reply", component: supportAgent }),
],
});Conditional
Use Conditional inside an orchestration to branch.
new Step({
name: "route",
component: new Conditional({
condition: (state) => state.intent === "billing",
ifTrue: "billing_reply",
ifFalse: "general_reply",
}),
});CodeEval, Checklist, EvalTree
CodeEval runs deterministic TypeScript scoring logic. Checklist combines evals with optional weights. EvalTree supports score-based branching across eval nodes.
Runner
Runner is the local execution bridge for Agent, LLMProcessor, and LLMEval.
import Anthropic from "@anthropic-ai/sdk";
import { Runner } from "@zhanla/sdk-ts";
const runner = new Runner({
client: new Anthropic(),
});Runner behavior:
constructor({ client })wraps the client internally withwrap(...)buildMessages(component, row)defaults to[system instructions, user JSON row]callLlm({ messages, model, tools, outputSchema, temperature, topK })supports Anthropic, OpenAI-compatible chat clients, and Gemini
Current local execution behavior:
runneris required forAgent,LLMProcessor, andLLMEvalmodelmust be set explicitly- response text is parsed as JSON when possible, otherwise wrapped as
{ result: text } outputSchemais used for validation- returned tool calls are exposed as
_toolCallson the local execution output
Observability
wrap(client)
Wrap an LLM client so calls are recorded against the active trace context.
import Anthropic from "@anthropic-ai/sdk";
import { wrap } from "@zhanla/sdk-ts";
const client = wrap(new Anthropic());wrap() is observational only. It does not change your application logic. Runner calls wrap() internally, so you do not need to wrap the same client first.
Trace Context
The CLI sets a TraceContext before runner execution. Wrapped clients read the active context automatically via AsyncLocalStorage.
import { traceStorage } from "@zhanla/sdk-ts";
const ctx = traceStorage.getStore();
if (ctx) {
console.log(ctx.trajectoryId);
}Discovery And CLI Usage
TypeScript discovery loads your module and collects exported component instances.
zhanla run workflow.ts:support_pipeline --dataset tickets.json --eval evals.ts:answer_qualityThe package also ships a helper CLI:
npx @zhanla/sdk-ts discover components.ts
npx @zhanla/sdk-ts run components.ts:support_agent --eval evals.ts:support_evalValidation Rules
When run through zhanla, TypeScript component manifests are validated before execution.
Toolmust provide a callable implementation and anoutputSchemaCodeEvalmust provide a callable implementationSkill,Agent,LLMProcessor, andLLMEvalmust provideinstructionsAgent,LLMProcessor, andLLMEvalmust providemodel
Utilities
The package also exports lower-level helpers:
import {
collectExportedComponents,
toManifest,
executeComponent,
executeRow,
wrap,
parseJsonResponse,
TraceContext,
traceStorage,
} from "@zhanla/sdk-ts";
import type { LLMCall, LLMResponse, ToolCall } from "@zhanla/sdk-ts";collectExportedComponents(...)collects component instances from a loaded moduletoManifest(...)serializes a component into the CLI manifest shapeexecuteComponent(...)runs one component locallyexecuteRow(...)runs one component plus optional eval for a single rowparseJsonResponse(text)extracts JSON from raw or fenced model text
Production Trajectory Tracking
When your components run in a deployed app (outside zhanla eval runs), the
SDK can ship every LLM call to Zhanla so you can monitor real user usage.
No code changes are needed — set one environment variable:
export ZHANLA_API_KEY="bm_kid_....bm_sec_..." # SDK key from SettingsEvery call made through a Runner (or a client you wrapped with wrap()),
including calls made inside executeComponent/executeRow, is then batched
and sent in the background to Zhanla as a production trajectory: provider,
model, input messages, output, tool calls, token counts, latency, and stop
reason. Raw provider responses are not sent unless you opt in with
ZHANLA_CAPTURE_RAW=1.
The exporter is fail-silent: it never throws into your application, bounds its memory, and drops events rather than blocking your requests.
In serverless environments (Lambda, Vercel), flush before the function freezes:
import { flush } from "@zhanla/sdk-ts";
await flush();Optional environment variables:
ZHANLA_BASE_URL— override the Zhanla app URLZHANLA_CAPTURE_RAW— include raw provider responses (off by default)
