@zhanla/sdk-ts 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,6 +14,8 @@ Requires Node `>=18`.
14
14
 
15
15
  Provider packages such as `@anthropic-ai/sdk`, `openai`, and `@google/genai` are optional. Install them only if you use `Runner` with those clients.
16
16
 
17
+ Every component requires an explicit stable `key`. Use a lowercase, hyphenated identifier such as `support-agent`.
18
+
17
19
  ## Quick Start
18
20
 
19
21
  ```ts
@@ -27,6 +29,7 @@ const runner = new Runner({
27
29
  export const supportAgent = new Agent({
28
30
  name: "support_agent",
29
31
  description: "Respond to support requests.",
32
+ key: "support-agent",
30
33
  instructions: 'Answer clearly. Return JSON: {"answer": "..."}',
31
34
  model: "claude-sonnet-4-6",
32
35
  runner,
@@ -42,6 +45,7 @@ export const supportAgent = new Agent({
42
45
  export const supportEval = new CodeEval({
43
46
  name: "support_eval",
44
47
  description: "Check whether an answer was returned.",
48
+ key: "support-eval",
45
49
  fn: (kwargs: unknown) => {
46
50
  const { model_response } = kwargs as { model_response?: string };
47
51
  const parsed = model_response ? JSON.parse(model_response) : {};
@@ -53,7 +57,7 @@ export const supportEval = new CodeEval({
53
57
  Run it with the CLI:
54
58
 
55
59
  ```bash
56
- bench run components.ts:support_agent --dataset tickets.json --eval components.ts:support_eval
60
+ zhanla run components.ts:support_agent --dataset tickets.json --eval components.ts:support_eval
57
61
  ```
58
62
 
59
63
  ## Public API
@@ -92,6 +96,7 @@ Use a `Tool` for deterministic TypeScript logic.
92
96
  export const lookupCustomer = new Tool({
93
97
  name: "lookup_customer",
94
98
  description: "Fetch a customer record by ID.",
99
+ key: "lookup-customer",
95
100
  inputSchema: { type: "object", properties: {} },
96
101
  fn: (kwargs: unknown) => {
97
102
  const { customerId } = kwargs as { customerId: string };
@@ -122,6 +127,7 @@ Use a `Skill` for reusable instructions and tool access.
122
127
  export const summarizeSkill = new Skill({
123
128
  name: "summarize_skill",
124
129
  description: "Reusable summarization instructions.",
130
+ key: "summarize-skill",
125
131
  instructions: "Summarize the provided text in one short paragraph.",
126
132
  tools: [lookupCustomer],
127
133
  });
@@ -144,6 +150,7 @@ const runner = new Runner({
144
150
  export const supportAgent = new Agent({
145
151
  name: "support_agent",
146
152
  description: "Respond to support requests.",
153
+ key: "support-agent",
147
154
  instructions: 'Answer clearly. Return JSON: {"answer": "..."}',
148
155
  model: "gpt-4.1-mini",
149
156
  runner,
@@ -174,6 +181,7 @@ const runner = new Runner({
174
181
  export const intentClassifier = new LLMProcessor({
175
182
  name: "intent_classifier",
176
183
  description: "Classify intent.",
184
+ key: "intent-classifier",
177
185
  instructions: 'Return JSON: {"intent": "billing|technical|other"}',
178
186
  model: "claude-haiku-4-5",
179
187
  runner,
@@ -195,6 +203,7 @@ Use an `LLMEval` for LLM-backed evaluation logic.
195
203
  export const toneEval = new LLMEval({
196
204
  name: "tone_eval",
197
205
  description: "Evaluate tone.",
206
+ key: "tone-eval",
198
207
  instructions: 'Return JSON: {"score": 0.0, "reason": "..."}',
199
208
  model: "gpt-4.1-mini",
200
209
  runner,
@@ -217,8 +226,13 @@ Use an `Orchestration` to compose steps into a DAG.
217
226
  export const supportPipeline = new Orchestration({
218
227
  name: "support_pipeline",
219
228
  description: "Classify intent, then draft a reply.",
229
+ key: "support-pipeline",
220
230
  steps: [
221
- new Step({ name: "classify", component: intentClassifier, next: ["reply"] }),
231
+ new Step({
232
+ name: "classify",
233
+ component: intentClassifier,
234
+ next: ["reply"],
235
+ }),
222
236
  new Step({ name: "reply", component: supportAgent }),
223
237
  ],
224
238
  });
@@ -260,7 +274,7 @@ const runner = new Runner({
260
274
 
261
275
  - `constructor({ client })` wraps the client internally with `wrap(...)`
262
276
  - `buildMessages(component, row)` defaults to `[system instructions, user JSON row]`
263
- - `callLlm({ messages, model, tools, outputSchema })` supports Anthropic, OpenAI-compatible chat clients, and Gemini
277
+ - `callLlm({ messages, model, tools, outputSchema, temperature, topK })` supports Anthropic, OpenAI-compatible chat clients, and Gemini
264
278
 
265
279
  Current local execution behavior:
266
280
 
@@ -268,7 +282,7 @@ Current local execution behavior:
268
282
  - `model` must be set explicitly
269
283
  - response text is parsed as JSON when possible, otherwise wrapped as `{ result: text }`
270
284
  - `outputSchema` is used for validation
271
- - returned tool calls raise a not-implemented error for now
285
+ - returned tool calls are exposed as `_toolCalls` on the local execution output
272
286
 
273
287
  ## Observability
274
288
 
@@ -303,7 +317,7 @@ if (ctx) {
303
317
  TypeScript discovery loads your module and collects exported component instances.
304
318
 
305
319
  ```bash
306
- bench run workflow.ts:support_pipeline --dataset tickets.json --eval evals.ts:answer_quality
320
+ zhanla run workflow.ts:support_pipeline --dataset tickets.json --eval evals.ts:answer_quality
307
321
  ```
308
322
 
309
323
  The package also ships a helper CLI:
package/bin/discover.js CHANGED
@@ -1,16 +1,201 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * zhanla-sdk-ts discover <file.ts>
3
+ * zhanla-sdk-ts discover <file.ts[:ExportName]>
4
+ *
5
+ * Loads a TypeScript module via tsx, walks the transitive closure from a
6
+ * named root (or all exported roots if no name given), and emits the full
7
+ * closure as a JSON array to stdout (leaf-first order).
4
8
  *
5
- * Loads a TypeScript module via tsx, collects exported BaseComponent instances,
6
- * emits their manifests as a JSON array to stdout.
7
9
  * Exits non-zero if no components are found or the file fails to load.
8
10
  */
9
11
 
10
12
  import { pathToFileURL } from "url";
11
13
  import { resolve } from "path";
12
14
 
13
- export async function runDiscover(filePath) {
15
+ function isComponentLike(value) {
16
+ return (
17
+ value != null &&
18
+ typeof value === "object" &&
19
+ typeof value.componentType === "string" &&
20
+ typeof value.name === "string" &&
21
+ typeof value.key === "string"
22
+ );
23
+ }
24
+
25
+ function isConditionalLike(value) {
26
+ return (
27
+ value != null &&
28
+ typeof value === "object" &&
29
+ typeof value.condition === "function" &&
30
+ typeof value.ifTrue === "string" &&
31
+ typeof value.ifFalse === "string"
32
+ );
33
+ }
34
+
35
+ function getBranchChildren(node) {
36
+ if (!node) return [];
37
+ const children = [];
38
+ if (node.eval && isComponentLike(node.eval)) children.push(node.eval);
39
+ for (const edge of [...(node.ifPass || []), ...(node.ifFail || [])]) {
40
+ if (edge && edge.node) children.push(...getBranchChildren(edge.node));
41
+ }
42
+ return children;
43
+ }
44
+
45
+ function getChildren(comp) {
46
+ const children = [];
47
+ for (const attr of ["tools", "skills", "agents", "evals"]) {
48
+ if (Array.isArray(comp[attr])) {
49
+ children.push(...comp[attr].filter(isComponentLike));
50
+ }
51
+ }
52
+ if (comp.componentType === "orchestration" && Array.isArray(comp.steps)) {
53
+ for (const step of comp.steps) {
54
+ if (step.component && !isConditionalLike(step.component) && isComponentLike(step.component)) {
55
+ children.push(step.component);
56
+ }
57
+ }
58
+ }
59
+ if (comp.componentType === "eval_tree" && comp.root) {
60
+ children.push(...getBranchChildren(comp.root));
61
+ }
62
+ return children;
63
+ }
64
+
65
+ function stableSerialize(value) {
66
+ if (Array.isArray(value)) {
67
+ return `[${value.map((entry) => stableSerialize(entry)).join(",")}]`;
68
+ }
69
+ if (value != null && typeof value === "object") {
70
+ const entries = Object.entries(value)
71
+ .filter(([, entry]) => entry !== undefined)
72
+ .sort(([left], [right]) => left.localeCompare(right));
73
+ return `{${entries
74
+ .map(([key, entry]) => `${JSON.stringify(key)}:${stableSerialize(entry)}`)
75
+ .join(",")}}`;
76
+ }
77
+ return JSON.stringify(value);
78
+ }
79
+
80
+ function sortRefsByKey(refs) {
81
+ return [...refs].sort((left, right) => left.key.localeCompare(right.key));
82
+ }
83
+
84
+ function behaviorFields(manifest) {
85
+ const fields = {
86
+ component_type: manifest.component_type,
87
+ execution_mode: manifest.execution_mode,
88
+ };
89
+
90
+ if (manifest.instructions != null) fields.instructions = manifest.instructions;
91
+ if (manifest.model != null) fields.model = manifest.model;
92
+ if (manifest.temperature != null) fields.temperature = manifest.temperature;
93
+ if (manifest.output_schema != null) fields.output_schema = manifest.output_schema;
94
+ if (manifest.tool_refs != null) fields.tool_refs = sortRefsByKey(manifest.tool_refs);
95
+ if (manifest.skill_refs != null) fields.skill_refs = sortRefsByKey(manifest.skill_refs);
96
+ if (manifest.agent_refs != null) fields.agent_refs = sortRefsByKey(manifest.agent_refs);
97
+ if (manifest.steps != null) fields.steps = manifest.steps;
98
+ if (manifest.eval_refs != null) fields.eval_refs = sortRefsByKey(manifest.eval_refs);
99
+ if (manifest.weights != null) fields.weights = manifest.weights;
100
+ if (manifest.root != null) fields.root = manifest.root;
101
+ if (manifest.source_code != null) fields.source_code = manifest.source_code;
102
+ if (manifest.model_response_format != null) {
103
+ fields.model_response_format = manifest.model_response_format;
104
+ }
105
+ if (manifest.fn_present != null) fields.fn_present = manifest.fn_present;
106
+ if (manifest.is_async != null) fields.is_async = manifest.is_async;
107
+
108
+ return fields;
109
+ }
110
+
111
+ function manifestBehaviorSignature(manifest) {
112
+ return stableSerialize(behaviorFields(manifest));
113
+ }
114
+
115
+ function getManifestOptions(comp, filePath, symbolByComponent, fallbackToName = false) {
116
+ const symbolName = symbolByComponent.get(comp);
117
+ return {
118
+ filePath,
119
+ symbolName: symbolName ?? (fallbackToName ? comp.name : undefined),
120
+ };
121
+ }
122
+
123
+ export function collectClosureManifests(roots, toManifest, filePath, symbolByComponent) {
124
+ const seen = new Map();
125
+ const signaturesByKey = new Map();
126
+ const order = [];
127
+
128
+ function visit(comp) {
129
+ const closureKey = `${comp.componentType}:${comp.key}`;
130
+ const existing = seen.get(closureKey);
131
+ if (existing) {
132
+ if (existing !== comp) {
133
+ const signature = manifestBehaviorSignature(
134
+ toManifest(comp, getManifestOptions(comp, filePath, symbolByComponent))
135
+ );
136
+ if (signaturesByKey.get(closureKey) !== signature) {
137
+ throw new Error(
138
+ `Conflict: two different ${comp.componentType} objects claim key '${comp.key}' but have different behavior.`
139
+ );
140
+ }
141
+ }
142
+ return;
143
+ }
144
+
145
+ seen.set(closureKey, comp);
146
+ const manifest = toManifest(comp, getManifestOptions(comp, filePath, symbolByComponent));
147
+ signaturesByKey.set(closureKey, manifestBehaviorSignature(manifest));
148
+
149
+ for (const child of getChildren(comp)) {
150
+ visit(child);
151
+ }
152
+ order.push(comp);
153
+ }
154
+
155
+ for (const root of roots) {
156
+ visit(root);
157
+ }
158
+
159
+ return order.map((comp) =>
160
+ toManifest(comp, {
161
+ filePath,
162
+ symbolName: symbolByComponent.get(comp) ?? comp.name,
163
+ })
164
+ );
165
+ }
166
+
167
+ export function buildDiscoveredManifests({
168
+ allComponents,
169
+ symbolByComponent,
170
+ symbolName,
171
+ toManifest,
172
+ filePath,
173
+ }) {
174
+ if (symbolName) {
175
+ const root = allComponents.find(
176
+ (c) => symbolByComponent.get(c) === symbolName || c.name === symbolName
177
+ );
178
+ if (!root) {
179
+ const available = allComponents.map((c) => symbolByComponent.get(c) ?? c.name).join(", ");
180
+ throw new Error(`Component '${symbolName}' not found in ${filePath}. Available: ${available}`);
181
+ }
182
+ return collectClosureManifests([root], toManifest, filePath, symbolByComponent);
183
+ }
184
+
185
+ return collectClosureManifests(allComponents, toManifest, filePath, symbolByComponent);
186
+ }
187
+
188
+ export async function runDiscover(fileSpec) {
189
+ let filePath = fileSpec;
190
+ let symbolName = null;
191
+
192
+ const colonIndex = fileSpec.lastIndexOf(":");
193
+ // Handle Windows drive letters (e.g. C:\...) — only split if colon is not at position 1
194
+ if (colonIndex > 1) {
195
+ filePath = fileSpec.slice(0, colonIndex);
196
+ symbolName = fileSpec.slice(colonIndex + 1) || null;
197
+ }
198
+
14
199
  const absolutePath = resolve(filePath);
15
200
  const fileUrl = pathToFileURL(absolutePath).href;
16
201
 
@@ -22,26 +207,25 @@ export async function runDiscover(filePath) {
22
207
  process.exit(1);
23
208
  }
24
209
 
25
- // Dynamically import from the built SDK (or tsx will resolve from node_modules)
26
210
  let collectExportedComponents, toManifest;
27
211
  try {
28
212
  const sdk = await import("@zhanla/sdk-ts");
29
213
  collectExportedComponents = sdk.collectExportedComponents;
30
214
  toManifest = sdk.toManifest;
31
215
  } catch {
32
- // Fallback: try loading from relative path (dev mode)
33
216
  const sdk = await import(new URL("../dist/index.js", import.meta.url).href);
34
217
  collectExportedComponents = sdk.collectExportedComponents;
35
218
  toManifest = sdk.toManifest;
36
219
  }
37
220
 
38
- const components = collectExportedComponents(moduleExports);
221
+ const allComponents = collectExportedComponents(moduleExports);
39
222
 
40
- if (components.length === 0) {
223
+ if (allComponents.length === 0) {
41
224
  console.error(`No zhanla components found in ${filePath}`);
42
225
  process.exit(1);
43
226
  }
44
227
 
228
+ // Build symbol name map for exported components
45
229
  const symbolByComponent = new Map();
46
230
  const namespaces = [moduleExports];
47
231
  if (
@@ -59,12 +243,19 @@ export async function runDiscover(filePath) {
59
243
  }
60
244
  }
61
245
 
62
- const manifests = components.map((comp) =>
63
- toManifest(comp, {
246
+ let manifests;
247
+ try {
248
+ manifests = buildDiscoveredManifests({
249
+ allComponents,
250
+ symbolByComponent,
251
+ symbolName,
252
+ toManifest,
64
253
  filePath: absolutePath,
65
- symbolName: symbolByComponent.get(comp) ?? comp.name,
66
- })
67
- );
254
+ });
255
+ } catch (err) {
256
+ console.error(err instanceof Error ? err.message : String(err));
257
+ process.exit(1);
258
+ }
68
259
 
69
260
  process.stdout.write(JSON.stringify(manifests) + "\n");
70
261
  }
package/dist/executor.js CHANGED
@@ -213,6 +213,8 @@ function validateSchemaValue(value, schema, path = "$") {
213
213
  return typeof value === "string" ? [] : [`${path} must be a string`];
214
214
  case "number":
215
215
  return typeof value === "number" ? [] : [`${path} must be a number`];
216
+ case "integer":
217
+ return typeof value === "number" && Number.isInteger(value) ? [] : [`${path} must be an integer`];
216
218
  case "boolean":
217
219
  return typeof value === "boolean" ? [] : [`${path} must be a boolean`];
218
220
  case "null":
@@ -241,6 +243,8 @@ async function runRunnerComponent(comp, kwargs) {
241
243
  tools: "tools" in comp ? comp.tools : [],
242
244
  outputSchema: comp.outputSchema,
243
245
  jsonRepair: comp.jsonRepair,
246
+ temperature: "temperature" in comp ? comp.temperature : undefined,
247
+ topK: "topK" in comp ? comp.topK : undefined,
244
248
  });
245
249
  const hasTextOutput = response.text.trim() !== "";
246
250
  const hasToolCalls = response.toolCalls.length > 0;
@@ -2,10 +2,10 @@
2
2
  * Manifest emission — serialize SDK component instances into ComponentManifest objects.
3
3
  * The manifest shape mirrors the Python CLI's ComponentManifest dataclass.
4
4
  */
5
- import { BaseComponent, JsonSchema } from "./types.js";
5
+ import { BaseComponent, ComponentRef, JsonSchema } from "./types.js";
6
6
  export interface StepManifest {
7
7
  name: string;
8
- component: string;
8
+ component_ref?: ComponentRef;
9
9
  next: string[];
10
10
  is_conditional?: boolean;
11
11
  if_true?: string;
@@ -17,11 +17,11 @@ export interface EdgeManifest {
17
17
  }
18
18
  export interface LeafManifest {
19
19
  type: "leaf";
20
- eval: string;
20
+ eval_ref: ComponentRef;
21
21
  }
22
22
  export interface BranchManifest {
23
23
  type: "branch";
24
- eval: string;
24
+ eval_ref: ComponentRef;
25
25
  threshold: number;
26
26
  if_pass: EdgeManifest[];
27
27
  if_fail: EdgeManifest[];
@@ -30,19 +30,28 @@ export interface ComponentManifest {
30
30
  name: string;
31
31
  description: string;
32
32
  component_type: string;
33
+ /** Stable component identity — explicit if dev provided key, otherwise slugified from name. */
34
+ key: string;
35
+ /** "explicit" | "implicit" */
36
+ key_source: string;
33
37
  language: "typescript";
34
38
  is_runnable: boolean;
35
39
  is_eval: boolean;
36
- version_hash: string;
40
+ execution_mode: string;
37
41
  instructions?: string;
38
42
  model?: string;
43
+ temperature?: number;
39
44
  fn_present?: boolean;
40
45
  is_async?: boolean;
41
- tools?: string[];
42
- skills?: string[];
43
- agents?: string[];
46
+ /** Typed tool references. */
47
+ tool_refs?: ComponentRef[];
48
+ /** Typed skill references. */
49
+ skill_refs?: ComponentRef[];
50
+ /** Typed agent references. */
51
+ agent_refs?: ComponentRef[];
44
52
  steps?: StepManifest[];
45
- evals?: string[];
53
+ /** Typed eval references for Checklist. */
54
+ eval_refs?: ComponentRef[];
46
55
  weights?: number[];
47
56
  root?: BranchManifest;
48
57
  output_schema?: JsonSchema;
package/dist/manifest.js CHANGED
@@ -12,7 +12,8 @@ function isComponentLike(value) {
12
12
  typeof value.componentType === "string" &&
13
13
  typeof value.name === "string" &&
14
14
  typeof value.description === "string" &&
15
- typeof value.versionHash === "function");
15
+ typeof value.isRunnable === "boolean" &&
16
+ typeof value.isEval === "boolean");
16
17
  }
17
18
  function isConditionalLike(value) {
18
19
  return (value != null &&
@@ -27,18 +28,27 @@ function isLeafLike(value) {
27
28
  "eval" in value &&
28
29
  !("threshold" in value));
29
30
  }
31
+ function resolveComponentRef(component) {
32
+ return {
33
+ key: component.key,
34
+ component_type: component.componentType,
35
+ name: component.name,
36
+ language: "typescript",
37
+ execution_mode: EXECUTION_MODES[component.componentType] ?? "unknown",
38
+ };
39
+ }
30
40
  function serializeEdge(edge) {
31
41
  return {
32
42
  weight: edge.weight,
33
43
  node: isLeafLike(edge.node)
34
- ? { type: "leaf", eval: edge.node.eval.name }
44
+ ? { type: "leaf", eval_ref: resolveComponentRef(edge.node.eval) }
35
45
  : serializeBranch(edge.node),
36
46
  };
37
47
  }
38
48
  function serializeBranch(branch) {
39
49
  return {
40
50
  type: "branch",
41
- eval: branch.eval.name,
51
+ eval_ref: resolveComponentRef(branch.eval),
42
52
  threshold: branch.threshold,
43
53
  if_pass: branch.ifPass.map(serializeEdge),
44
54
  if_fail: branch.ifFail.map(serializeEdge),
@@ -128,16 +138,32 @@ function extractExportedComponentSource(filePath, symbolName) {
128
138
  // ---------------------------------------------------------------------------
129
139
  // to_manifest
130
140
  // ---------------------------------------------------------------------------
141
+ const EXECUTION_MODES = {
142
+ tool: "code",
143
+ code_eval: "code",
144
+ skill: "prompt",
145
+ agent: "prompt",
146
+ llm_processor: "prompt",
147
+ llm_eval: "prompt",
148
+ orchestration: "dag",
149
+ checklist: "composite",
150
+ eval_tree: "composite",
151
+ };
131
152
  export function toManifest(component, opts = {}) {
132
153
  const componentSource = extractExportedComponentSource(opts.filePath, opts.symbolName);
154
+ const key = component.key;
155
+ const keySource = "explicit";
156
+ const executionMode = EXECUTION_MODES[component.componentType] ?? "unknown";
133
157
  const base = {
134
158
  name: component.name,
135
159
  description: component.description,
136
160
  component_type: component.componentType,
161
+ key,
162
+ key_source: keySource,
137
163
  language: "typescript",
138
164
  is_runnable: component.isRunnable,
139
165
  is_eval: component.isEval,
140
- version_hash: component.versionHash(),
166
+ execution_mode: executionMode,
141
167
  file_path: opts.filePath,
142
168
  symbol_name: opts.symbolName,
143
169
  };
@@ -173,7 +199,7 @@ export function toManifest(component, opts = {}) {
173
199
  instructions: skill.instructions,
174
200
  fn_present: skill.fn != null,
175
201
  is_async: skill.isAsync,
176
- tools: skill.tools.map((t) => t.name),
202
+ tool_refs: skill.tools.length > 0 ? skill.tools.map(resolveComponentRef) : undefined,
177
203
  output_schema: skill.outputSchema,
178
204
  source_code: componentSource || skill.fn?.toString(),
179
205
  source_language: "typescript",
@@ -186,9 +212,10 @@ export function toManifest(component, opts = {}) {
186
212
  ...base,
187
213
  instructions: agent.instructions,
188
214
  model: agent.model,
189
- tools: agent.tools.map((t) => t.name),
190
- skills: agent.skills.map((s) => s.name),
191
- agents: agent.agents.map((a) => a.name),
215
+ temperature: agent.temperature,
216
+ tool_refs: agent.tools.length > 0 ? agent.tools.map(resolveComponentRef) : undefined,
217
+ skill_refs: agent.skills.length > 0 ? agent.skills.map(resolveComponentRef) : undefined,
218
+ agent_refs: agent.agents.length > 0 ? agent.agents.map(resolveComponentRef) : undefined,
192
219
  output_schema: agent.outputSchema,
193
220
  };
194
221
  }
@@ -198,6 +225,7 @@ export function toManifest(component, opts = {}) {
198
225
  ...base,
199
226
  instructions: llmProcessor.instructions,
200
227
  model: llmProcessor.model,
228
+ temperature: llmProcessor.temperature,
201
229
  output_schema: llmProcessor.outputSchema,
202
230
  };
203
231
  }
@@ -207,6 +235,7 @@ export function toManifest(component, opts = {}) {
207
235
  ...base,
208
236
  instructions: llmEval.instructions,
209
237
  model: llmEval.model,
238
+ temperature: llmEval.temperature,
210
239
  output_schema: llmEval.outputSchema,
211
240
  };
212
241
  }
@@ -216,16 +245,15 @@ export function toManifest(component, opts = {}) {
216
245
  if (isConditionalLike(s.component)) {
217
246
  return {
218
247
  name: s.name,
219
- component: "conditional",
220
- next: s.next,
221
248
  is_conditional: true,
222
249
  if_true: s.component.ifTrue,
223
250
  if_false: s.component.ifFalse,
251
+ next: s.next,
224
252
  };
225
253
  }
226
254
  return {
227
255
  name: s.name,
228
- component: s.component.name,
256
+ component_ref: resolveComponentRef(s.component),
229
257
  next: s.next,
230
258
  };
231
259
  });
@@ -235,7 +263,7 @@ export function toManifest(component, opts = {}) {
235
263
  const checklist = component;
236
264
  return {
237
265
  ...base,
238
- evals: checklist.evals.map((e) => e.name),
266
+ eval_refs: checklist.evals.length > 0 ? checklist.evals.map(resolveComponentRef) : undefined,
239
267
  weights: checklist.weights,
240
268
  };
241
269
  }
package/dist/types.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * Mirrors the Python SDK's class-based component model.
4
4
  */
5
5
  export type JsonSchemaScalar = {
6
- type: "string" | "number" | "boolean" | "null";
6
+ type: "string" | "number" | "integer" | "boolean" | "null";
7
7
  };
8
8
  export type JsonSchemaObject = {
9
9
  type: "object";
@@ -17,8 +17,19 @@ export type JsonSchemaArray = {
17
17
  };
18
18
  export type JsonSchema = JsonSchemaScalar | JsonSchemaObject | JsonSchemaArray | Record<string, unknown>;
19
19
  export type ComponentType = "tool" | "code_eval" | "skill" | "agent" | "llm_processor" | "llm_eval" | "orchestration" | "checklist" | "eval_tree";
20
+ export interface ComponentRef {
21
+ key: string;
22
+ component_type: string;
23
+ name: string;
24
+ language: string;
25
+ execution_mode: string;
26
+ }
20
27
  export declare const RUNNABLE_TYPES: Set<ComponentType>;
21
28
  export declare const EVAL_TYPES: Set<ComponentType>;
29
+ /** Slugify a component name into a key-safe string (implicit key derivation). */
30
+ export declare function slugifyKey(name: string): string;
31
+ /** Throw if `key` does not meet the component key format requirements. */
32
+ export declare function validateKey(key: string, componentName: string): void;
22
33
  export interface ToolCall {
23
34
  name: string;
24
35
  input: Record<string, unknown>;
@@ -40,6 +51,8 @@ export interface RunnerCallOptions {
40
51
  tools?: Tool[];
41
52
  outputSchema?: JsonSchema;
42
53
  jsonRepair?: boolean;
54
+ temperature?: number;
55
+ topK?: number;
43
56
  }
44
57
  export interface RunnerOptions {
45
58
  client: unknown;
@@ -65,9 +78,9 @@ export declare abstract class BaseComponent {
65
78
  abstract readonly componentType: ComponentType;
66
79
  abstract readonly name: string;
67
80
  abstract readonly description: string;
81
+ abstract readonly key: string;
68
82
  get isRunnable(): boolean;
69
83
  get isEval(): boolean;
70
- abstract versionHash(): string;
71
84
  }
72
85
  export interface ToolOptions {
73
86
  name: string;
@@ -75,6 +88,7 @@ export interface ToolOptions {
75
88
  fn: (...args: unknown[]) => unknown;
76
89
  inputSchema: JsonSchema;
77
90
  outputSchema?: JsonSchema;
91
+ key: string;
78
92
  }
79
93
  export declare class Tool extends BaseComponent {
80
94
  readonly componentType: "tool";
@@ -84,14 +98,15 @@ export declare class Tool extends BaseComponent {
84
98
  readonly inputSchema: JsonSchema;
85
99
  readonly outputSchema?: JsonSchema;
86
100
  readonly isAsync: boolean;
101
+ readonly key: string;
87
102
  constructor(opts: ToolOptions);
88
- versionHash(): string;
89
103
  }
90
104
  export interface CodeEvalOptions {
91
105
  name: string;
92
106
  description: string;
93
107
  fn: (...args: unknown[]) => unknown;
94
108
  modelResponseFormat?: "JSON" | "TEXT" | "YAML";
109
+ key: string;
95
110
  }
96
111
  export declare class CodeEval extends BaseComponent {
97
112
  readonly componentType: "code_eval";
@@ -100,8 +115,8 @@ export declare class CodeEval extends BaseComponent {
100
115
  readonly fn: (...args: unknown[]) => unknown;
101
116
  readonly isAsync: boolean;
102
117
  readonly modelResponseFormat: "JSON" | "TEXT" | "YAML";
118
+ readonly key: string;
103
119
  constructor(opts: CodeEvalOptions);
104
- versionHash(): string;
105
120
  }
106
121
  export interface SkillOptions {
107
122
  name: string;
@@ -110,6 +125,7 @@ export interface SkillOptions {
110
125
  tools?: Tool[];
111
126
  fn?: (...args: unknown[]) => unknown;
112
127
  outputSchema?: JsonSchema;
128
+ key: string;
113
129
  }
114
130
  export declare class Skill extends BaseComponent {
115
131
  readonly componentType: "skill";
@@ -120,8 +136,8 @@ export declare class Skill extends BaseComponent {
120
136
  readonly fn?: (...args: unknown[]) => unknown;
121
137
  readonly outputSchema?: JsonSchema;
122
138
  readonly isAsync: boolean;
139
+ readonly key: string;
123
140
  constructor(opts: SkillOptions);
124
- versionHash(): string;
125
141
  }
126
142
  export interface AgentOptions {
127
143
  name: string;
@@ -134,6 +150,9 @@ export interface AgentOptions {
134
150
  agents?: Agent[];
135
151
  outputSchema?: JsonSchema;
136
152
  jsonRepair?: boolean;
153
+ temperature?: number;
154
+ topK?: number;
155
+ key: string;
137
156
  }
138
157
  export declare class Agent extends BaseComponent {
139
158
  readonly componentType: "agent";
@@ -147,8 +166,10 @@ export declare class Agent extends BaseComponent {
147
166
  readonly agents: Agent[];
148
167
  readonly outputSchema?: JsonSchema;
149
168
  readonly jsonRepair: boolean;
169
+ readonly temperature?: number;
170
+ readonly topK?: number;
171
+ readonly key: string;
150
172
  constructor(opts: AgentOptions);
151
- versionHash(): string;
152
173
  }
153
174
  export interface LLMProcessorOptions {
154
175
  name: string;
@@ -158,6 +179,9 @@ export interface LLMProcessorOptions {
158
179
  runner?: Runner;
159
180
  outputSchema?: JsonSchema;
160
181
  jsonRepair?: boolean;
182
+ temperature?: number;
183
+ topK?: number;
184
+ key: string;
161
185
  }
162
186
  export declare class LLMProcessor extends BaseComponent {
163
187
  readonly componentType: "llm_processor";
@@ -168,8 +192,10 @@ export declare class LLMProcessor extends BaseComponent {
168
192
  readonly runner?: Runner;
169
193
  readonly outputSchema?: JsonSchema;
170
194
  readonly jsonRepair: boolean;
195
+ readonly temperature?: number;
196
+ readonly topK?: number;
197
+ readonly key: string;
171
198
  constructor(opts: LLMProcessorOptions);
172
- versionHash(): string;
173
199
  }
174
200
  export interface LLMEvalOptions {
175
201
  name: string;
@@ -179,6 +205,9 @@ export interface LLMEvalOptions {
179
205
  runner?: Runner;
180
206
  outputSchema?: JsonSchema;
181
207
  jsonRepair?: boolean;
208
+ temperature?: number;
209
+ topK?: number;
210
+ key: string;
182
211
  }
183
212
  export declare class LLMEval extends BaseComponent {
184
213
  readonly componentType: "llm_eval";
@@ -189,8 +218,10 @@ export declare class LLMEval extends BaseComponent {
189
218
  readonly runner?: Runner;
190
219
  readonly outputSchema?: JsonSchema;
191
220
  readonly jsonRepair: boolean;
221
+ readonly temperature?: number;
222
+ readonly topK?: number;
223
+ readonly key: string;
192
224
  constructor(opts: LLMEvalOptions);
193
- versionHash(): string;
194
225
  }
195
226
  export interface ConditionalOptions {
196
227
  condition: (ctx: Record<string, unknown>) => boolean;
@@ -218,14 +249,15 @@ export interface OrchestrationOptions {
218
249
  name: string;
219
250
  description: string;
220
251
  steps: Step[];
252
+ key: string;
221
253
  }
222
254
  export declare class Orchestration extends BaseComponent {
223
255
  readonly componentType: "orchestration";
224
256
  readonly name: string;
225
257
  readonly description: string;
226
258
  readonly steps: Step[];
259
+ readonly key: string;
227
260
  constructor(opts: OrchestrationOptions);
228
- versionHash(): string;
229
261
  }
230
262
  export declare class Leaf {
231
263
  readonly eval: BaseComponent;
@@ -258,6 +290,7 @@ export interface ChecklistOptions {
258
290
  description: string;
259
291
  evals: BaseComponent[];
260
292
  weights?: number[];
293
+ key: string;
261
294
  }
262
295
  export declare class Checklist extends BaseComponent {
263
296
  readonly componentType: "checklist";
@@ -265,19 +298,20 @@ export declare class Checklist extends BaseComponent {
265
298
  readonly description: string;
266
299
  readonly evals: BaseComponent[];
267
300
  readonly weights?: number[];
301
+ readonly key: string;
268
302
  constructor(opts: ChecklistOptions);
269
- versionHash(): string;
270
303
  }
271
304
  export interface EvalTreeOptions {
272
305
  name: string;
273
306
  description: string;
274
307
  root: Branch;
308
+ key: string;
275
309
  }
276
310
  export declare class EvalTree extends BaseComponent {
277
311
  readonly componentType: "eval_tree";
278
312
  readonly name: string;
279
313
  readonly description: string;
280
314
  readonly root: Branch;
315
+ readonly key: string;
281
316
  constructor(opts: EvalTreeOptions);
282
- versionHash(): string;
283
317
  }
package/dist/types.js CHANGED
@@ -2,7 +2,6 @@
2
2
  * Core types for the bench TypeScript SDK.
3
3
  * Mirrors the Python SDK's class-based component model.
4
4
  */
5
- import { createHash } from "crypto";
6
5
  import { parseJsonResponse } from "./json.js";
7
6
  import { isAnthropicClient, isGeminiClient, isOpenAIClient, wrap } from "./wrap.js";
8
7
  export const RUNNABLE_TYPES = new Set([
@@ -18,30 +17,29 @@ export const EVAL_TYPES = new Set([
18
17
  "eval_tree",
19
18
  ]);
20
19
  // ---------------------------------------------------------------------------
21
- // Hashing utilities
20
+ // Key validation
22
21
  // ---------------------------------------------------------------------------
23
- function hashFields(...parts) {
24
- const h = createHash("sha256");
25
- for (const p of parts) {
26
- h.update(p, "utf8");
27
- }
28
- return h.digest("hex");
22
+ const KEY_PATTERN = /^[a-z0-9-]+$/;
23
+ const KEY_MAX_LEN = 64;
24
+ /** Slugify a component name into a key-safe string (implicit key derivation). */
25
+ export function slugifyKey(name) {
26
+ let slug = name.toLowerCase().trim();
27
+ slug = slug.replace(/[^\w\s-]/g, "");
28
+ slug = slug.replace(/[\s_]+/g, "-");
29
+ slug = slug.replace(/-+/g, "-").replace(/^-|-$/g, "");
30
+ return slug.slice(0, KEY_MAX_LEN);
29
31
  }
30
- function sortedStringify(value) {
31
- if (value === null || typeof value !== "object") {
32
- return JSON.stringify(value);
32
+ /** Throw if `key` does not meet the component key format requirements. */
33
+ export function validateKey(key, componentName) {
34
+ if (!key) {
35
+ throw new Error(`Component '${componentName}': key must not be empty`);
33
36
  }
34
- if (Array.isArray(value)) {
35
- return "[" + value.map(sortedStringify).join(",") + "]";
37
+ if (!KEY_PATTERN.test(key)) {
38
+ throw new Error(`Component '${componentName}': key '${key}' must contain only lowercase letters, digits, and hyphens ([a-z0-9-])`);
39
+ }
40
+ if (key.length > KEY_MAX_LEN) {
41
+ throw new Error(`Component '${componentName}': key '${key}' is too long (max ${KEY_MAX_LEN} chars, got ${key.length})`);
36
42
  }
37
- const keys = Object.keys(value).sort();
38
- const parts = keys.map((k) => `${JSON.stringify(k)}:${sortedStringify(value[k])}`);
39
- return "{" + parts.join(",") + "}";
40
- }
41
- function normalizeSchema(schema) {
42
- if (schema == null)
43
- return "";
44
- return sortedStringify(schema);
45
43
  }
46
44
  function isPlainRecord(value) {
47
45
  return value != null && typeof value === "object" && !Array.isArray(value);
@@ -84,11 +82,6 @@ function previewText(text, limit = 160) {
84
82
  const normalized = text.replace(/\s+/g, " ").trim();
85
83
  return normalized.length <= limit ? normalized : `${normalized.slice(0, limit)}...`;
86
84
  }
87
- function fnSource(fn) {
88
- if (fn == null)
89
- return "";
90
- return fn.toString();
91
- }
92
85
  function contentPartToText(part) {
93
86
  if (typeof part === "string") {
94
87
  return part;
@@ -289,6 +282,8 @@ export class Runner {
289
282
  description: tool.description,
290
283
  input_schema: withClosedAdditionalProperties(tool.inputSchema),
291
284
  })),
285
+ ...(opts.temperature !== undefined ? { temperature: opts.temperature } : {}),
286
+ ...(opts.topK !== undefined ? { top_k: opts.topK } : {}),
292
287
  });
293
288
  const content = Array.isArray(response.content)
294
289
  ? response.content
@@ -335,6 +330,7 @@ export class Runner {
335
330
  parameters: withClosedAdditionalProperties(tool.inputSchema),
336
331
  },
337
332
  })),
333
+ ...(opts.temperature !== undefined ? { temperature: opts.temperature } : {}),
338
334
  });
339
335
  const choice = Array.isArray(response.choices)
340
336
  ? response.choices[0]
@@ -385,6 +381,8 @@ export class Runner {
385
381
  parameters: toGeminiSchema(tool.inputSchema),
386
382
  }],
387
383
  })),
384
+ ...(opts.temperature !== undefined ? { temperature: opts.temperature } : {}),
385
+ ...(opts.topK !== undefined ? { topK: opts.topK } : {}),
388
386
  },
389
387
  });
390
388
  const candidates = Array.isArray(response.candidates)
@@ -444,6 +442,7 @@ export class Tool extends BaseComponent {
444
442
  inputSchema;
445
443
  outputSchema;
446
444
  isAsync;
445
+ key;
447
446
  constructor(opts) {
448
447
  super();
449
448
  this.name = opts.name;
@@ -452,9 +451,8 @@ export class Tool extends BaseComponent {
452
451
  this.inputSchema = validateToolInputSchema(opts.inputSchema);
453
452
  this.outputSchema = opts.outputSchema;
454
453
  this.isAsync = opts.fn.constructor.name === "AsyncFunction";
455
- }
456
- versionHash() {
457
- return hashFields(this.componentType, fnSource(this.fn), normalizeSchema(this.inputSchema), normalizeSchema(this.outputSchema));
454
+ validateKey(opts.key, opts.name);
455
+ this.key = opts.key;
458
456
  }
459
457
  }
460
458
  export class CodeEval extends BaseComponent {
@@ -464,6 +462,7 @@ export class CodeEval extends BaseComponent {
464
462
  fn;
465
463
  isAsync;
466
464
  modelResponseFormat;
465
+ key;
467
466
  constructor(opts) {
468
467
  super();
469
468
  this.name = opts.name;
@@ -471,9 +470,8 @@ export class CodeEval extends BaseComponent {
471
470
  this.fn = opts.fn;
472
471
  this.isAsync = opts.fn.constructor.name === "AsyncFunction";
473
472
  this.modelResponseFormat = opts.modelResponseFormat ?? "JSON";
474
- }
475
- versionHash() {
476
- return hashFields(this.componentType, fnSource(this.fn), this.modelResponseFormat);
473
+ validateKey(opts.key, opts.name);
474
+ this.key = opts.key;
477
475
  }
478
476
  }
479
477
  export class Skill extends BaseComponent {
@@ -485,6 +483,7 @@ export class Skill extends BaseComponent {
485
483
  fn;
486
484
  outputSchema;
487
485
  isAsync;
486
+ key;
488
487
  constructor(opts) {
489
488
  super();
490
489
  this.name = opts.name;
@@ -494,10 +493,8 @@ export class Skill extends BaseComponent {
494
493
  this.fn = opts.fn;
495
494
  this.outputSchema = opts.outputSchema;
496
495
  this.isAsync = opts.fn?.constructor.name === "AsyncFunction";
497
- }
498
- versionHash() {
499
- const toolRefs = [...this.tools.map((t) => t.name)].sort().join(",");
500
- return hashFields(this.componentType, this.instructions, toolRefs, fnSource(this.fn));
496
+ validateKey(opts.key, opts.name);
497
+ this.key = opts.key;
501
498
  }
502
499
  }
503
500
  export class Agent extends BaseComponent {
@@ -512,6 +509,9 @@ export class Agent extends BaseComponent {
512
509
  agents;
513
510
  outputSchema;
514
511
  jsonRepair;
512
+ temperature;
513
+ topK;
514
+ key;
515
515
  constructor(opts) {
516
516
  super();
517
517
  this.name = opts.name;
@@ -524,12 +524,10 @@ export class Agent extends BaseComponent {
524
524
  this.agents = opts.agents ?? [];
525
525
  this.outputSchema = opts.outputSchema;
526
526
  this.jsonRepair = opts.jsonRepair ?? false;
527
- }
528
- versionHash() {
529
- const toolRefs = [...this.tools.map((t) => t.name)].sort().join(",");
530
- const skillRefs = [...this.skills.map((s) => s.name)].sort().join(",");
531
- const agentRefs = [...this.agents.map((a) => a.name)].sort().join(",");
532
- return hashFields(this.componentType, this.instructions, this.model, toolRefs, skillRefs, agentRefs, normalizeSchema(this.outputSchema));
527
+ this.temperature = opts.temperature;
528
+ this.topK = opts.topK;
529
+ validateKey(opts.key, opts.name);
530
+ this.key = opts.key;
533
531
  }
534
532
  }
535
533
  export class LLMProcessor extends BaseComponent {
@@ -541,6 +539,9 @@ export class LLMProcessor extends BaseComponent {
541
539
  runner;
542
540
  outputSchema;
543
541
  jsonRepair;
542
+ temperature;
543
+ topK;
544
+ key;
544
545
  constructor(opts) {
545
546
  super();
546
547
  this.name = opts.name;
@@ -550,9 +551,10 @@ export class LLMProcessor extends BaseComponent {
550
551
  this.runner = opts.runner;
551
552
  this.outputSchema = opts.outputSchema;
552
553
  this.jsonRepair = opts.jsonRepair ?? false;
553
- }
554
- versionHash() {
555
- return hashFields(this.componentType, this.instructions, this.model, normalizeSchema(this.outputSchema));
554
+ this.temperature = opts.temperature;
555
+ this.topK = opts.topK;
556
+ validateKey(opts.key, opts.name);
557
+ this.key = opts.key;
556
558
  }
557
559
  }
558
560
  export class LLMEval extends BaseComponent {
@@ -564,6 +566,9 @@ export class LLMEval extends BaseComponent {
564
566
  runner;
565
567
  outputSchema;
566
568
  jsonRepair;
569
+ temperature;
570
+ topK;
571
+ key;
567
572
  constructor(opts) {
568
573
  super();
569
574
  this.name = opts.name;
@@ -573,9 +578,10 @@ export class LLMEval extends BaseComponent {
573
578
  this.runner = opts.runner;
574
579
  this.outputSchema = opts.outputSchema;
575
580
  this.jsonRepair = opts.jsonRepair ?? false;
576
- }
577
- versionHash() {
578
- return hashFields(this.componentType, this.instructions, this.model, normalizeSchema(this.outputSchema));
581
+ this.temperature = opts.temperature;
582
+ this.topK = opts.topK;
583
+ validateKey(opts.key, opts.name);
584
+ this.key = opts.key;
579
585
  }
580
586
  }
581
587
  export class Conditional {
@@ -603,18 +609,14 @@ export class Orchestration extends BaseComponent {
603
609
  name;
604
610
  description;
605
611
  steps;
612
+ key;
606
613
  constructor(opts) {
607
614
  super();
608
615
  this.name = opts.name;
609
616
  this.description = opts.description;
610
617
  this.steps = opts.steps;
611
- }
612
- versionHash() {
613
- const stepParts = this.steps.map((s) => {
614
- const compRef = s.component instanceof Conditional ? "conditional" : s.component.name;
615
- return `${s.name}:${compRef}:${s.next.join(",")}`;
616
- });
617
- return hashFields(this.componentType, stepParts.join("|"));
618
+ validateKey(opts.key, opts.name);
619
+ this.key = opts.key;
618
620
  }
619
621
  }
620
622
  // ---------------------------------------------------------------------------
@@ -652,46 +654,29 @@ export class Checklist extends BaseComponent {
652
654
  description;
653
655
  evals;
654
656
  weights;
657
+ key;
655
658
  constructor(opts) {
656
659
  super();
657
660
  this.name = opts.name;
658
661
  this.description = opts.description;
659
662
  this.evals = opts.evals;
660
663
  this.weights = opts.weights;
664
+ validateKey(opts.key, opts.name);
665
+ this.key = opts.key;
661
666
  }
662
- versionHash() {
663
- const evalRefs = this.evals.map((e) => e.name).join(",");
664
- const w = this.weights ? this.weights.join(",") : "";
665
- return hashFields(this.componentType, evalRefs, w);
666
- }
667
- }
668
- // ---------------------------------------------------------------------------
669
- // EvalTree
670
- // ---------------------------------------------------------------------------
671
- function treeHashParts(node) {
672
- if (node instanceof Leaf) {
673
- return `leaf:${node.eval.name}`;
674
- }
675
- if (node instanceof Edge) {
676
- return `edge:${node.weight}:${treeHashParts(node.node)}`;
677
- }
678
- // Branch
679
- const passParts = node.ifPass.map(treeHashParts).join("|");
680
- const failParts = node.ifFail.map(treeHashParts).join("|");
681
- return `branch:${node.eval.name}:${node.threshold}:[${passParts}]:[${failParts}]`;
682
667
  }
683
668
  export class EvalTree extends BaseComponent {
684
669
  componentType = "eval_tree";
685
670
  name;
686
671
  description;
687
672
  root;
673
+ key;
688
674
  constructor(opts) {
689
675
  super();
690
676
  this.name = opts.name;
691
677
  this.description = opts.description;
692
678
  this.root = opts.root;
693
- }
694
- versionHash() {
695
- return hashFields(this.componentType, treeHashParts(this.root));
679
+ validateKey(opts.key, opts.name);
680
+ this.key = opts.key;
696
681
  }
697
682
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhanla/sdk-ts",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "TypeScript SDK for the zhanla CLI — define and run AI components locally",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -21,7 +21,6 @@
21
21
  "dist",
22
22
  "bin"
23
23
  ],
24
-
25
24
  "devDependencies": {
26
25
  "typescript": "^5.4.0",
27
26
  "vitest": "^1.4.0",