lightnode-sdk 0.6.0 → 0.6.2

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
@@ -16,6 +16,12 @@ npm install lightnode-sdk viem
16
16
  LightChain's own docs list official SDKs as "soon"; this fills the gap. Not
17
17
  affiliated with LightChain.
18
18
 
19
+ New to blockchain or Node.js? Read the
20
+ [Getting Started guide](../GETTING-STARTED.md) first. It covers wallets, testnet
21
+ vs mainnet, the `.env` file, and your first AI call in about 5 minutes. Then come
22
+ back here for the full reference. The rest of this README assumes you're
23
+ comfortable with TypeScript and a terminal.
24
+
19
25
  ## Five-line "hello world"
20
26
 
21
27
  ```ts
@@ -205,6 +211,84 @@ for await (const event of handle.events) {
205
211
  }
206
212
  ```
207
213
 
214
+ ### Batch runner (new in 0.6.0)
215
+
216
+ Fan out many prompts as parallel encrypted inferences. Capped concurrency, stable result order, per-slot errors so one stalled worker does not kill the batch.
217
+
218
+ ```ts
219
+ import { runInferenceBatch } from "lightnode-sdk";
220
+
221
+ const results = await runInferenceBatch({
222
+ network: "testnet",
223
+ privateKey: process.env.PRIVATE_KEY!,
224
+ model: "llama3-8b",
225
+ system: "Reply in one short sentence.",
226
+ concurrency: 4,
227
+ prompts: [
228
+ "one-line fact about the ocean",
229
+ "one-line fact about the moon",
230
+ "one-line fact about coffee",
231
+ ],
232
+ onSlotComplete: ({ index, result, error }) => {
233
+ console.log(`#${index}`, error?.message ?? result?.answer);
234
+ },
235
+ });
236
+
237
+ for (const r of results) {
238
+ if (r.error) console.warn(`slot ${r.index} failed:`, r.error.message);
239
+ else console.log(r.result.answer);
240
+ }
241
+ ```
242
+
243
+ Fits: batch evals, content scoring, RAG re-ranking, parallel rewrites. Pass `{signal}` (`AbortSignal`) to cancel queued slots mid-run.
244
+
245
+ ### Agent class (new in 0.6.0)
246
+
247
+ ReAct-style tool calling on top of `runInferenceWithKey`. The model emits `<tool>name {"k":"v"}</tool>` or `<answer>...</answer>`; the SDK parses, runs the handler, threads the observation back. Works on small open models (llama3-8b) without native function calling.
248
+
249
+ ```ts
250
+ import { Agent } from "lightnode-sdk";
251
+
252
+ const agent = new Agent({
253
+ network: "testnet",
254
+ privateKey: process.env.PRIVATE_KEY!,
255
+ model: "llama3-8b",
256
+ system: "You are a careful research assistant.",
257
+ tools: [
258
+ {
259
+ name: "add",
260
+ description: "Add two integers and return the sum.",
261
+ args: { a: "first integer", b: "second integer" },
262
+ handler: ({ a, b }) => Number(a) + Number(b),
263
+ },
264
+ ],
265
+ maxIterations: 3,
266
+ onStep: (step) => console.log(step.kind, step),
267
+ });
268
+
269
+ const { answer, steps, hitLimit } = await agent.run("What is 17 + 25?");
270
+ console.log(answer); // "42"
271
+ console.log(steps); // [{ kind: "tool_call", ... }, { kind: "answer", text: "42" }]
272
+ ```
273
+
274
+ Each iteration is one inference (one on-chain `submitJob`); cap `maxIterations` to keep wall-clock + cost bounded. Tool handlers are plain functions that may be async; return JSON-serializable data so the model can read the observation.
275
+
276
+ ### Cancellation (new in 0.6.0)
277
+
278
+ `runInference` and `runInferenceWithKey` accept an `AbortSignal`. In-flight on-chain transactions still settle (the protocol is the source of truth); the SDK just stops awaiting and rejects with `Error("aborted")`.
279
+
280
+ ```ts
281
+ const controller = new AbortController();
282
+ setTimeout(() => controller.abort(), 15_000);
283
+
284
+ await runInferenceWithKey({
285
+ network: "testnet",
286
+ privateKey: process.env.PRIVATE_KEY!,
287
+ prompt: "short answer please",
288
+ signal: controller.signal,
289
+ });
290
+ ```
291
+
208
292
  ### Typed errors
209
293
 
210
294
  ```ts
package/dist/add.js CHANGED
@@ -341,7 +341,9 @@ function depsNeeded(template) {
341
341
  return ["lightnode-sdk", "viem", "ws"];
342
342
  if (template === "hono")
343
343
  return ["lightnode-sdk", "viem", "ws"];
344
- return ["lightnode-sdk", "viem", "ws"];
344
+ // The node/script template is run with `npx tsx <file>.ts`, so tsx must be
345
+ // installed too, otherwise `tsx ...` fails with "command not found".
346
+ return ["lightnode-sdk", "viem", "ws", "tsx"];
345
347
  }
346
348
  /**
347
349
  * Implementation called by `lightnode add inference [...]`.
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { LightNode, modelStatsCsv, workerStatsCsv, workerJobsCsv, runInferenceWithKey, isStalledWorker, workerPreflight, workerWatch, BRIDGE_ROUTE, DAO, DAO_ADDRESSES } from "./index.js";
2
+ import { LightNode, modelStatsCsv, workerStatsCsv, workerJobsCsv, runInferenceWithKey, runInferenceBatch, Agent, isStalledWorker, workerPreflight, workerWatch, BRIDGE_ROUTE, DAO, DAO_ADDRESSES } from "./index.js";
3
3
  import { addInference, addAnalyticsDashboard, addNftMint, addChat, addAgent } from "./add.js";
4
4
  import { createPublicClient, http, parseEther } from "viem";
5
5
  import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
@@ -22,6 +22,10 @@ const HELP = `lightnode <command> [--net mainnet|testnet]
22
22
  Run one inference (needs PRIVATE_KEY in env):
23
23
  chat <prompt> stream one encrypted inference answer to stdout
24
24
  ([--model llama3-8b] [--key 0x...])
25
+ batch <prompts.json> run N prompts in parallel, JSON line per result
26
+ ([--model] [--concurrency 4])
27
+ agent <task> ReAct-style agent with built-in add + now tools
28
+ ([--model] [--max-iter 4])
25
29
 
26
30
  Wallet helpers:
27
31
  wallet new generate a fresh testnet key, print it
@@ -343,7 +347,7 @@ async function main() {
343
347
  console.log("\nNothing to do - all target files already exist. Pass --force to overwrite.");
344
348
  }
345
349
  else {
346
- console.log(`\nNext steps:`);
350
+ console.log(`\nNext steps (these files were added to your CURRENT folder, not a new project):`);
347
351
  console.log(` 1. ${result.install}`);
348
352
  if (sub === "nft-mint-with-inference" || sub === "inference" || sub === "chat" || sub === "agent") {
349
353
  console.log(` 2. cp .env.example .env (and put a funded ${result.network} PRIVATE_KEY in it)`);
@@ -352,14 +356,14 @@ async function main() {
352
356
  console.log(` 4. Deploy. Vercel Cron fires /api/agent on the schedule in vercel.json`);
353
357
  }
354
358
  else if (sub === "agent") {
355
- console.log(` 3. AGENT_INTERVAL_MS=3600000 tsx agent.ts # or run under pm2/systemd`);
359
+ console.log(` 3. AGENT_INTERVAL_MS=3600000 npx tsx agent.ts # or run under pm2/systemd`);
356
360
  }
357
361
  else if (sub === "chat" && result.template === "nextjs-api") {
358
362
  console.log(` 3. Make sure /api/inference is mounted too (run: npx lightnode add inference)`);
359
363
  console.log(` 4. npm run dev, open /chat`);
360
364
  }
361
365
  else if (sub === "chat") {
362
- console.log(` 3. tsx chat-repl.ts (interactive terminal chat)`);
366
+ console.log(` 3. npx tsx chat-repl.ts (interactive terminal chat)`);
363
367
  }
364
368
  else if (sub === "nft-mint-with-inference" && result.template === "nextjs-api") {
365
369
  console.log(` 3. Make sure /api/inference is mounted too (run: npx lightnode add inference)`);
@@ -372,10 +376,10 @@ async function main() {
372
376
  console.log(` 3. wire inferenceHandler into your Hono app, then start it`);
373
377
  }
374
378
  else if (sub === "nft-mint-with-inference") {
375
- console.log(` 3. tsx nft-metadata.ts "My NFT" "concept goes here"`);
379
+ console.log(` 3. npx tsx nft-metadata.ts "My NFT" "concept goes here"`);
376
380
  }
377
381
  else {
378
- console.log(` 3. tsx lightchain-inference.ts "your prompt"`);
382
+ console.log(` 3. npx tsx lightchain-inference.ts "your prompt"`);
379
383
  }
380
384
  }
381
385
  else {
@@ -384,11 +388,114 @@ async function main() {
384
388
  console.log(` 2. npm run dev, open /lightnode-analytics`);
385
389
  }
386
390
  else {
387
- console.log(` 2. tsx lightnode-analytics.ts`);
391
+ console.log(` 2. npx tsx lightnode-analytics.ts`);
388
392
  }
389
393
  }
394
+ if (result.network === "testnet") {
395
+ console.log(`\nNo wallet yet? Make one: npx lightnode wallet new then fund it free below.`);
396
+ }
390
397
  console.log(`\nFree testnet LCAI: https://lightfaucet.ai`);
391
398
  console.log(`Builder docs: https://lightnode.app/build`);
399
+ console.log(`New to all this? See GETTING-STARTED.md in the lightnode repo.`);
400
+ }
401
+ break;
402
+ }
403
+ case "batch": {
404
+ // `lightnode batch <prompts.json>` or `lightnode batch -` (stdin).
405
+ // Input shape: ["prompt one","prompt two"] OR
406
+ // { "model": "llama3-8b", "system": "...", "prompts": ["...", "..."] }
407
+ // Output: one JSON object per line (index, answer or error). Composes
408
+ // with `jq` for downstream processing.
409
+ const arg = positionals[1] ?? "";
410
+ if (!arg)
411
+ die("usage: lightnode batch <prompts.json> (or `lightnode batch -` to read stdin)");
412
+ const raw = await (arg === "-" ? readStdin() : readFile(arg));
413
+ let parsed;
414
+ try {
415
+ parsed = JSON.parse(raw);
416
+ }
417
+ catch (e) {
418
+ die("invalid JSON: " + e.message);
419
+ }
420
+ const cfg = normalizeBatchInput(parsed);
421
+ const model = flag("--model") ?? cfg.model ?? "llama3-8b";
422
+ const concurrency = Number(flag("--concurrency") ?? "4") || 4;
423
+ const privateKey = pickKey();
424
+ process.stderr.write(`Running ${cfg.prompts.length} prompts on ${net} via ${model} (concurrency=${concurrency})\n`);
425
+ const results = await runInferenceBatch({
426
+ network: net,
427
+ privateKey,
428
+ model,
429
+ system: cfg.system,
430
+ concurrency,
431
+ prompts: cfg.prompts,
432
+ onSlotComplete: ({ index, result, error }) => {
433
+ process.stdout.write(JSON.stringify({
434
+ index,
435
+ ok: error == null,
436
+ answer: result?.answer ?? null,
437
+ error: error?.message ?? null,
438
+ jobId: result?.jobId.toString() ?? error?.jobId ?? null,
439
+ }) + "\n");
440
+ },
441
+ });
442
+ const okCount = results.filter((r) => r.error == null).length;
443
+ process.stderr.write(`Done: ${okCount}/${results.length} succeeded\n`);
444
+ break;
445
+ }
446
+ case "agent": {
447
+ // Quick one-shot agent demo: built-in `add` + `now` tools. Bring the
448
+ // model your prompt as positional args (or pipe via stdin) and watch
449
+ // the step trace on stderr while the answer streams to stdout.
450
+ const inline = positionals.slice(1).join(" ").trim();
451
+ const task = inline || (await readStdin()).trim();
452
+ if (!task)
453
+ die("usage: lightnode agent <task> (or pipe the task to stdin)");
454
+ const model = flag("--model") ?? "llama3-8b";
455
+ const maxIter = Number(flag("--max-iter") ?? "4") || 4;
456
+ const privateKey = pickKey();
457
+ // A tiny built-in toolset so the command is runnable without writing
458
+ // a wrapper. For real tools, import Agent from the SDK and pass your own.
459
+ const tools = [
460
+ {
461
+ name: "add",
462
+ description: "Add two integers and return the sum.",
463
+ args: { a: "first integer", b: "second integer" },
464
+ handler: ({ a, b }) => Number(a) + Number(b),
465
+ },
466
+ {
467
+ name: "now",
468
+ description: "Return the current ISO timestamp.",
469
+ args: {},
470
+ handler: () => new Date().toISOString(),
471
+ },
472
+ ];
473
+ const agent = new Agent({
474
+ network: net,
475
+ privateKey,
476
+ model,
477
+ system: "You are a careful assistant. Use tools when they help; otherwise answer directly.",
478
+ tools,
479
+ maxIterations: maxIter,
480
+ onStep: (step) => {
481
+ if (step.kind === "tool_call") {
482
+ process.stderr.write(`[tool] ${step.name}(${JSON.stringify(step.args)}) -> ${JSON.stringify(step.result)}\n`);
483
+ }
484
+ else if (step.kind === "tool_error") {
485
+ process.stderr.write(`[tool-error] ${step.name}: ${step.error}\n`);
486
+ }
487
+ else if (step.kind === "thought") {
488
+ process.stderr.write(`[think] ${step.text.slice(0, 200)}\n`);
489
+ }
490
+ },
491
+ });
492
+ try {
493
+ const { answer, steps, iterations, hitLimit } = await agent.run(task);
494
+ process.stdout.write(answer + "\n");
495
+ process.stderr.write(JSON.stringify({ iterations, steps: steps.length, hitLimit }) + "\n");
496
+ }
497
+ catch (e) {
498
+ die("agent failed: " + e.message);
392
499
  }
393
500
  break;
394
501
  }
@@ -396,4 +503,37 @@ async function main() {
396
503
  console.log(HELP);
397
504
  }
398
505
  }
506
+ async function readStdin() {
507
+ return new Promise((resolve) => {
508
+ let buf = "";
509
+ process.stdin.setEncoding("utf8");
510
+ process.stdin.on("data", (d) => (buf += d));
511
+ process.stdin.on("end", () => resolve(buf));
512
+ });
513
+ }
514
+ async function readFile(path) {
515
+ const fs = await import("node:fs/promises");
516
+ return fs.readFile(path, "utf8");
517
+ }
518
+ function normalizeBatchInput(parsed) {
519
+ if (Array.isArray(parsed)) {
520
+ const prompts = parsed.filter((p) => typeof p === "string");
521
+ if (!prompts.length)
522
+ die("batch input: array must contain at least one string prompt");
523
+ return { prompts };
524
+ }
525
+ if (parsed && typeof parsed === "object") {
526
+ const obj = parsed;
527
+ const prompts = Array.isArray(obj.prompts) ? obj.prompts.filter((p) => typeof p === "string") : [];
528
+ if (!prompts.length)
529
+ die("batch input: object must have a `prompts` array of strings");
530
+ return {
531
+ prompts,
532
+ system: typeof obj.system === "string" ? obj.system : undefined,
533
+ model: typeof obj.model === "string" ? obj.model : undefined,
534
+ };
535
+ }
536
+ die("batch input: expected JSON array of strings OR object { prompts, system?, model? }");
537
+ return { prompts: [] };
538
+ }
399
539
  main().catch((e) => die(String(e?.message ?? e)));
package/dist/index.d.ts CHANGED
@@ -90,7 +90,7 @@ export declare class LightNode {
90
90
  * (especially in registry-proxy environments like StackBlitz where lockfiles
91
91
  * may pin an older minor than the local install command suggests).
92
92
  */
93
- export declare const SDK_VERSION = "0.6.0";
93
+ export declare const SDK_VERSION = "0.6.1";
94
94
  export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei, computeModelId as modelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, GatewayClient, GatewayHttpError, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, crypto, runInference, runInferenceWithKey, runInferenceStream, Conversation, chat, runInferenceBatch, Agent, parseAgentOutput, workerPreflight, workerWatch, Bridge, BRIDGE_ROUTE, HYPERLANE_ROUTER_ABI, ERC20_ABI, addressToBytes32, quoteBridgeFee, bridgeableBalance, bridgeAllowance, approveBridge, bridgeTransfer, DAO, DAO_ADDRESSES, ProposalState, PROPOSAL_STATE_LABEL, VoteSupport, GOVERNOR_ABI, VOTES_ABI, OnchainModelRegistry, AIVM_MODEL_REGISTRY_ABI, BENCHMARK_REGISTRY_ABI, ModelStatus, MODEL_STATUS_LABEL, StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker, };
95
95
  export type { BearerSource, GatewayClientOptions, SelectSessionResult, PrepareSessionResult, UploadBlobResult, SessionTokenResult } from "./gateway.js";
96
96
  export type { SessionPreparation, RunInferenceArgs, RunInferenceResult, RunInferenceWithKeyArgs, RunInferenceStreamResult } from "./inference.js";
package/dist/index.js CHANGED
@@ -145,7 +145,7 @@ export class LightNode {
145
145
  * (especially in registry-proxy environments like StackBlitz where lockfiles
146
146
  * may pin an older minor than the local install command suggests).
147
147
  */
148
- export const SDK_VERSION = "0.6.0";
148
+ export const SDK_VERSION = "0.6.1";
149
149
  export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei, computeModelId as modelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost,
150
150
  // v0.3 inference-submit surface (BETA - see README "Submitting inference").
151
151
  GatewayClient, GatewayHttpError, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, crypto,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightnode-sdk",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Read-only TypeScript client for LightChain AI: workers, jobs, models, on-chain registration, and per-model network analytics. Independent, community-built (not an official LightChain package).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",