lightnode-sdk 0.8.9 → 0.9.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/dist/add.js CHANGED
@@ -963,7 +963,7 @@ const NEXTJS_CHAT_WEB3_PAGE = `// app/chat-web3/page.tsx
963
963
 
964
964
  import { useEffect, useRef, useState } from "react";
965
965
  import { useAccount, useWalletClient, usePublicClient } from "wagmi";
966
- import { siweSignIn, GatewayClient, runInference, estimateJobFee, NETWORKS } from "lightnode-sdk";
966
+ import { siweSignIn, GatewayClient, LightChatSession, estimateJobFee, NETWORKS } from "lightnode-sdk";
967
967
  import { Streamdown } from "streamdown";
968
968
  import { ConnectButton } from "@/components/connect-button";
969
969
  import { LcaiMark } from "@/components/lcai-mark";
@@ -997,6 +997,9 @@ export default function ChatWeb3() {
997
997
  const [err, setErr] = useState<string | null>(null);
998
998
  const [feeLcai, setFeeLcai] = useState<number | null>(null);
999
999
  const endRef = useRef<HTMLDivElement>(null);
1000
+ // Reused across turns so follow-ups skip SIWE + createSession.
1001
+ const sessionRef = useRef<LightChatSession | null>(null);
1002
+ const sessionKeyRef = useRef<string>("");
1000
1003
 
1001
1004
  // Read the on-chain fee for the connected network so we can show the
1002
1005
  // visitor the real cost per turn before they click Send.
@@ -1037,6 +1040,31 @@ export default function ChatWeb3() {
1037
1040
  });
1038
1041
  }
1039
1042
 
1043
+ /**
1044
+ * Open a session on the first turn (or after expiry / a model or wallet
1045
+ * change), then reuse it so every follow-up turn skips SIWE + createSession.
1046
+ */
1047
+ async function ensureSession(): Promise<LightChatSession> {
1048
+ if (!walletClient || !publicClient || !address || !network) throw new Error("connect a wallet first");
1049
+ const key = \`\${address}:\${network}:\${model}\`;
1050
+ const existing = sessionRef.current;
1051
+ if (existing && !existing.expired && sessionKeyRef.current === key) return existing;
1052
+ setBusyStage("Sign in with your wallet (SIWE)...");
1053
+ const siwe = await siweSignIn(walletClient as unknown as Parameters<typeof siweSignIn>[0], network);
1054
+ setBusyStage("Approve createSession in your wallet (one-time per session)...");
1055
+ const gateway = new GatewayClient({ network, bearer: siwe.bearer });
1056
+ const chat = await LightChatSession.open({
1057
+ gateway,
1058
+ wallet: walletClient as unknown as Parameters<typeof LightChatSession.open>[0]["wallet"],
1059
+ publicClient: publicClient as unknown as Parameters<typeof LightChatSession.open>[0]["publicClient"],
1060
+ network: NETWORKS[network],
1061
+ model,
1062
+ });
1063
+ sessionRef.current = chat;
1064
+ sessionKeyRef.current = key;
1065
+ return chat;
1066
+ }
1067
+
1040
1068
  async function send() {
1041
1069
  if (!walletClient || !publicClient || !address || !network) {
1042
1070
  setErr("Connect a wallet on LightChain mainnet (9200) or testnet (8200) first.");
@@ -1053,26 +1081,21 @@ export default function ChatWeb3() {
1053
1081
  try {
1054
1082
  const system = "You are a concise assistant. Reply in one or two short sentences.";
1055
1083
  const prompt = composePrompt(history, next, system);
1056
-
1057
- setBusyStage("Sign in with your wallet (SIWE)...");
1058
- const session = await siweSignIn(walletClient as unknown as Parameters<typeof siweSignIn>[0], network);
1059
-
1060
- setBusyStage("Approve the createSession transaction in your wallet...");
1061
- const gateway = new GatewayClient({ network, bearer: session.bearer });
1062
- const result = await runInference({
1063
- prompt,
1064
- gateway,
1065
- wallet: walletClient as unknown as Parameters<typeof runInference>[0]["wallet"],
1066
- publicClient: publicClient as unknown as Parameters<typeof runInference>[0]["publicClient"],
1067
- network: NETWORKS[network],
1068
- model,
1069
- jobCompletedTimeoutMs: 120_000,
1070
- maxRetries: 1,
1071
- // Stream each decrypted chunk into the assistant bubble as it arrives.
1072
- onChunk: (_chunk, totalSoFar) => {
1073
- setBusyStage("");
1074
- patchLastAssistant({ text: totalSoFar });
1075
- },
1084
+ const onChunk = (_chunk: string, totalSoFar: string) => {
1085
+ setBusyStage("");
1086
+ patchLastAssistant({ text: totalSoFar });
1087
+ };
1088
+
1089
+ const chat = await ensureSession();
1090
+ setBusyStage("Approve the per-turn transaction in your wallet...");
1091
+ const result = await chat.send(prompt, { onChunk }).catch(async () => {
1092
+ // Session expired or the worker stopped serving - reopen once and retry.
1093
+ sessionRef.current = null;
1094
+ patchLastAssistant({ text: "" });
1095
+ setBusyStage("Re-opening session...");
1096
+ const fresh = await ensureSession();
1097
+ setBusyStage("Approve the per-turn transaction in your wallet...");
1098
+ return fresh.send(prompt, { onChunk });
1076
1099
  });
1077
1100
 
1078
1101
  patchLastAssistant({
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS } from "./networks.js";
2
2
  import { fetchWorkerModels, fromWei, resolveJobTransactions } from "./subgraph.js";
3
3
  import { aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv } from "./analytics.js";
4
- import { modelId as computeModelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, runInference, runInferenceWithKey, runInferenceStream } from "./inference.js";
4
+ import { modelId as computeModelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, runInference, runInferenceWithKey, runInferenceStream, openSession, runJobOnSession, LightChatSession } from "./inference.js";
5
5
  import { Conversation, chat } from "./chat.js";
6
6
  import { runInferenceBatch } from "./batch.js";
7
7
  import { Agent, parseAgentOutput } from "./agent.js";
@@ -135,7 +135,7 @@ export declare class LightNode {
135
135
  * may pin an older minor than the local install command suggests).
136
136
  */
137
137
  export declare const SDK_VERSION = "0.7.20";
138
- export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei, resolveJobTransactions, siweSignIn, siweChallenge, siweVerify, fetchWorkerModels, 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, WorkerOperator, WORKER_REGISTRY_ABI, JOB_REGISTRY_OPERATOR_ABI, AI_CONFIG_ABI, JOB_STATE, decodeWorkerError, WorkerOpError, isWorkerOpError, };
138
+ export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei, resolveJobTransactions, siweSignIn, siweChallenge, siweVerify, fetchWorkerModels, computeModelId as modelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, GatewayClient, GatewayHttpError, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, crypto, runInference, runInferenceWithKey, runInferenceStream, openSession, runJobOnSession, LightChatSession, 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, WorkerOperator, WORKER_REGISTRY_ABI, JOB_REGISTRY_OPERATOR_ABI, AI_CONFIG_ABI, JOB_STATE, decodeWorkerError, WorkerOpError, isWorkerOpError, };
139
139
  export type { BearerSource, GatewayClientOptions, SelectSessionResult, PrepareSessionResult, UploadBlobResult, SessionTokenResult } from "./gateway.js";
140
140
  export type { SessionPreparation, RunInferenceArgs, RunInferenceResult, RunInferenceWithKeyArgs, RunInferenceStreamResult } from "./inference.js";
141
141
  export type { ChatRole, ChatMessage, ConversationOptions, ConversationSendResult } from "./chat.js";
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS } from "./networks.js";
2
2
  import { fetchWorker, fetchWorkerJobs, fetchWorkerModels, fetchRecentJobs, fetchJob, fetchModels, fetchWorkers, summarize, fromWei, resolveJobTransactions, } from "./subgraph.js";
3
3
  import { isRegistered, fetchOnchainEligibleModels } from "./onchain.js";
4
4
  import { aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, } from "./analytics.js";
5
- import { modelId as computeModelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, runInference, runInferenceWithKey, runInferenceStream, } from "./inference.js";
5
+ import { modelId as computeModelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, runInference, runInferenceWithKey, runInferenceStream, openSession, runJobOnSession, LightChatSession, } from "./inference.js";
6
6
  import { Conversation, chat } from "./chat.js";
7
7
  import { runInferenceBatch } from "./batch.js";
8
8
  import { Agent, parseAgentOutput } from "./agent.js";
@@ -233,6 +233,8 @@ runInference,
233
233
  runInferenceWithKey,
234
234
  // v0.4.9 AsyncIterable<string> wrapper around runInferenceWithKey.
235
235
  runInferenceStream,
236
+ // v0.9.0 session reuse: open once, run many jobs (follow-ups skip createSession).
237
+ openSession, runJobOnSession, LightChatSession,
236
238
  // v0.5.0 multi-turn conversation helper (history client-side; one inference per turn).
237
239
  Conversation, chat,
238
240
  // v0.6.0 batch runner: many prompts, capped parallelism, stable result order.
@@ -204,6 +204,72 @@ export interface RunInferenceResult {
204
204
  submitTx: `0x${string}`;
205
205
  }>;
206
206
  }
207
+ /** A live, on-chain session. Open once, then run many jobs through it - each
208
+ * follow-up turn skips SIWE + createSession, leaving just the submitJob tx. */
209
+ export interface OpenSession {
210
+ readonly gateway: GatewayClient;
211
+ readonly wallet: MinimalWalletClient;
212
+ readonly publicClient: MinimalPublicClient;
213
+ readonly network: NetworkConfig;
214
+ readonly model: string;
215
+ readonly fee: number;
216
+ readonly sessionId: bigint;
217
+ readonly sessionKey: Uint8Array;
218
+ readonly worker: `0x${string}`;
219
+ readonly createTx: `0x${string}`;
220
+ /** Unix seconds when the on-chain session window closes. */
221
+ readonly expirySec: number;
222
+ }
223
+ export interface OpenSessionArgs {
224
+ gateway: GatewayClient;
225
+ wallet: MinimalWalletClient;
226
+ publicClient: MinimalPublicClient;
227
+ network: NetworkConfig;
228
+ model?: string;
229
+ }
230
+ /**
231
+ * prepareSession + the on-chain createSession tx. Do this once, then run many
232
+ * jobs through the handle with `runJobOnSession`. Re-open when `expirySec`
233
+ * passes or the chosen worker stops serving.
234
+ */
235
+ export declare function openSession(args: OpenSessionArgs): Promise<OpenSession>;
236
+ export interface RunJobOpts {
237
+ onChunk?: (chunk: string, totalSoFar: string) => void;
238
+ jobCompletedTimeoutMs?: number;
239
+ WebSocket?: WebSocketCtor;
240
+ relayUrl?: string;
241
+ signal?: AbortSignal;
242
+ }
243
+ /**
244
+ * Run ONE job against an already-open session: submitPrompt + submitJob + relay
245
+ * stream + wait for JobCompleted. No SIWE, no createSession.
246
+ */
247
+ export declare function runJobOnSession(session: OpenSession, prompt: string, opts?: RunJobOpts, attempt?: number): Promise<RunInferenceResult>;
248
+ /**
249
+ * A reusable, wallet-signed inference session. Open it once (SIWE happens before
250
+ * this; createSession happens here), then call `.send()` per turn - each
251
+ * follow-up turn skips createSession, leaving just the submitJob tx. Re-open
252
+ * (call `LightChatSession.open(...)` again) when `expired` is true or a
253
+ * `.send()` throws because the worker stopped serving.
254
+ *
255
+ * @example
256
+ * ```ts
257
+ * const session = await LightChatSession.open({ gateway, wallet, publicClient, network, model: "llama3-8b" });
258
+ * const a = await session.send("Who wrote The Great Gatsby?", { onChunk });
259
+ * const b = await session.send("In what year?", { onChunk }); // no createSession
260
+ * ```
261
+ */
262
+ export declare class LightChatSession {
263
+ private readonly session;
264
+ private constructor();
265
+ static open(args: OpenSessionArgs): Promise<LightChatSession>;
266
+ get sessionId(): bigint;
267
+ get worker(): `0x${string}`;
268
+ get model(): string;
269
+ /** true once the on-chain session window has closed; re-open before sending. */
270
+ get expired(): boolean;
271
+ send(prompt: string, opts?: RunJobOpts): Promise<RunInferenceResult>;
272
+ }
207
273
  /**
208
274
  * One call, full encrypted inference. Same code path the live playground at
209
275
  * lightnode.app/playground drives, condensed into a single function.
package/dist/inference.js CHANGED
@@ -228,14 +228,15 @@ function pickWebSocket(provided) {
228
228
  function topicAsUint(hex) {
229
229
  return BigInt(hex);
230
230
  }
231
- async function runOneAttempt(args, attempt) {
232
- const { prompt, gateway, wallet, publicClient, network, model = "llama3-8b", onChunk, jobCompletedTimeoutMs = 120000, } = args;
233
- const WS = pickWebSocket(args.WebSocket);
234
- const relayUrl = args.relayUrl ?? `wss://relay.${network.id}.lightchain.ai/ws`;
235
- // 1. prepareSession
231
+ /**
232
+ * prepareSession + the on-chain createSession tx. Do this once, then run many
233
+ * jobs through the handle with `runJobOnSession`. Re-open when `expirySec`
234
+ * passes or the chosen worker stops serving.
235
+ */
236
+ export async function openSession(args) {
237
+ const { gateway, wallet, publicClient, network, model = "llama3-8b" } = args;
236
238
  const prepared = await prepareSession(gateway, model);
237
239
  const fee = await estimateJobFee(network, model);
238
- // 2. createSession on-chain
239
240
  const createTx = await wallet.writeContract({
240
241
  address: network.jobRegistry,
241
242
  abi: JOB_REGISTRY_ABI_PARSED,
@@ -256,7 +257,31 @@ async function runOneAttempt(args, attempt) {
256
257
  const createLog = (await publicClient.getLogs({ address: network.jobRegistry, blockHash: createReceipt.blockHash })).find((l) => l.transactionHash === createTx && l.topics[0] === SESSION_CREATED_TOPIC);
257
258
  if (!createLog)
258
259
  throw new Error("SessionCreated log missing in createSession receipt");
259
- const sessionId = topicAsUint(createLog.topics[1]);
260
+ return {
261
+ gateway,
262
+ wallet,
263
+ publicClient,
264
+ network,
265
+ model,
266
+ fee,
267
+ sessionId: topicAsUint(createLog.topics[1]),
268
+ sessionKey: prepared.sessionKey,
269
+ worker: prepared.createSessionArgs.worker,
270
+ createTx,
271
+ expirySec: Number(prepared.createSessionArgs.expiry),
272
+ };
273
+ }
274
+ /**
275
+ * Run ONE job against an already-open session: submitPrompt + submitJob + relay
276
+ * stream + wait for JobCompleted. No SIWE, no createSession.
277
+ */
278
+ export async function runJobOnSession(session, prompt, opts = {}, attempt = 1) {
279
+ const { gateway, wallet, publicClient, network, sessionId, sessionKey, worker, fee, createTx } = session;
280
+ const { onChunk, jobCompletedTimeoutMs = 120000 } = opts;
281
+ const WS = pickWebSocket(opts.WebSocket);
282
+ const relayUrl = opts.relayUrl ?? `wss://relay.${network.id}.lightchain.ai/ws`;
283
+ // Shim so the job body below can keep referencing prepared.* unchanged.
284
+ const prepared = { sessionKey, createSessionArgs: { worker } };
260
285
  // 3. relay token + WebSocket
261
286
  let relayToken;
262
287
  for (let i = 0; i < 30 && !relayToken; i++) {
@@ -293,6 +318,8 @@ async function runOneAttempt(args, attempt) {
293
318
  setTimeout(() => reject(new Error("relay WebSocket open timeout")), 20000);
294
319
  });
295
320
  const chunks = [];
321
+ let streamDone = false;
322
+ let streamDoneAt = null;
296
323
  const handleMessage = async (rawData) => {
297
324
  const raw = typeof rawData === "string"
298
325
  ? rawData
@@ -308,20 +335,26 @@ async function runOneAttempt(args, attempt) {
308
335
  catch {
309
336
  return;
310
337
  }
311
- if (!frame?.payload)
312
- return;
313
- if (frame.type === "chunk") {
314
- try {
315
- const piece = await decryptResponse(prepared.sessionKey, frame.payload);
316
- chunks.push(piece);
317
- if (onChunk)
318
- onChunk(piece, chunks.join(""));
319
- }
320
- catch {
321
- /* control frame */
338
+ // "complete" marks end-of-stream (it may or may not carry a payload).
339
+ // Record it so the JobCompleted wait can stop promptly instead of polling
340
+ // the full grace window after the answer is already in hand.
341
+ if (frame.type === "complete") {
342
+ streamDone = true;
343
+ streamDoneAt = Date.now();
344
+ if (chunks.length === 0 && frame.payload) {
345
+ try {
346
+ const piece = await decryptResponse(prepared.sessionKey, frame.payload);
347
+ chunks.push(piece);
348
+ if (onChunk)
349
+ onChunk(piece, chunks.join(""));
350
+ }
351
+ catch {
352
+ /* ignore */
353
+ }
322
354
  }
355
+ return;
323
356
  }
324
- else if (frame.type === "complete" && chunks.length === 0) {
357
+ if (frame.type === "chunk" && frame.payload) {
325
358
  try {
326
359
  const piece = await decryptResponse(prepared.sessionKey, frame.payload);
327
360
  chunks.push(piece);
@@ -329,7 +362,7 @@ async function runOneAttempt(args, attempt) {
329
362
  onChunk(piece, chunks.join(""));
330
363
  }
331
364
  catch {
332
- /* ignore */
365
+ /* control frame */
333
366
  }
334
367
  }
335
368
  };
@@ -376,7 +409,8 @@ async function runOneAttempt(args, attempt) {
376
409
  // answer with txs.jobCompleted=null (the answer is still session-key
377
410
  // authentic; the on-chain proof can be polled for separately by callers).
378
411
  const deadline = Date.now() + jobCompletedTimeoutMs;
379
- const POST_CHUNKS_GRACE_MS = 45000;
412
+ const POST_CHUNKS_GRACE_MS = 45000; // fallback if the relay never sends a 'complete' frame
413
+ const POST_DONE_GRACE_MS = 8000; // once the answer is fully in, the worker commits JobCompleted within ~seconds
380
414
  const waitStart = Date.now();
381
415
  let firstChunkAt = chunks.length > 0 ? waitStart : null;
382
416
  const jobIdTopic = (`0x${jobId.toString(16).padStart(64, "0")}`);
@@ -385,9 +419,14 @@ async function runOneAttempt(args, attempt) {
385
419
  const now = Date.now();
386
420
  if (now >= deadline)
387
421
  break;
422
+ // Answer fully received (relay sent 'complete'): wait only briefly for the
423
+ // on-chain proof, then return. The answer is already session-key authentic;
424
+ // callers get txs.jobCompleted=null and can poll the proof later if needed.
425
+ if (streamDone && now - (streamDoneAt ?? now) >= POST_DONE_GRACE_MS)
426
+ break;
388
427
  if (firstChunkAt != null && now - firstChunkAt >= POST_CHUNKS_GRACE_MS)
389
428
  break;
390
- await new Promise((res) => setTimeout(res, 3000));
429
+ await new Promise((res) => setTimeout(res, 1500));
391
430
  if (firstChunkAt == null && chunks.length > 0)
392
431
  firstChunkAt = Date.now();
393
432
  const logs = await publicClient.getLogs({
@@ -414,8 +453,9 @@ async function runOneAttempt(args, attempt) {
414
453
  feeLcai: fee,
415
454
  });
416
455
  }
417
- // 7. grace period for the last relay frame, then close
418
- await new Promise((res) => setTimeout(res, 4000));
456
+ // 7. grace period for the last relay frame, then close. If the stream already
457
+ // signaled 'complete', no more frames are coming - skip the wait.
458
+ await new Promise((res) => setTimeout(res, streamDone ? 300 : 4000));
419
459
  try {
420
460
  ws.close();
421
461
  }
@@ -436,6 +476,53 @@ async function runOneAttempt(args, attempt) {
436
476
  stalled: [],
437
477
  };
438
478
  }
479
+ /** One attempt = open a fresh session, then run one job through it. */
480
+ async function runOneAttempt(args, attempt) {
481
+ const session = await openSession({
482
+ gateway: args.gateway,
483
+ wallet: args.wallet,
484
+ publicClient: args.publicClient,
485
+ network: args.network,
486
+ model: args.model,
487
+ });
488
+ return runJobOnSession(session, args.prompt, {
489
+ onChunk: args.onChunk,
490
+ jobCompletedTimeoutMs: args.jobCompletedTimeoutMs,
491
+ WebSocket: args.WebSocket,
492
+ relayUrl: args.relayUrl,
493
+ signal: args.signal,
494
+ }, attempt);
495
+ }
496
+ /**
497
+ * A reusable, wallet-signed inference session. Open it once (SIWE happens before
498
+ * this; createSession happens here), then call `.send()` per turn - each
499
+ * follow-up turn skips createSession, leaving just the submitJob tx. Re-open
500
+ * (call `LightChatSession.open(...)` again) when `expired` is true or a
501
+ * `.send()` throws because the worker stopped serving.
502
+ *
503
+ * @example
504
+ * ```ts
505
+ * const session = await LightChatSession.open({ gateway, wallet, publicClient, network, model: "llama3-8b" });
506
+ * const a = await session.send("Who wrote The Great Gatsby?", { onChunk });
507
+ * const b = await session.send("In what year?", { onChunk }); // no createSession
508
+ * ```
509
+ */
510
+ export class LightChatSession {
511
+ constructor(session) {
512
+ this.session = session;
513
+ }
514
+ static async open(args) {
515
+ return new LightChatSession(await openSession(args));
516
+ }
517
+ get sessionId() { return this.session.sessionId; }
518
+ get worker() { return this.session.worker; }
519
+ get model() { return this.session.model; }
520
+ /** true once the on-chain session window has closed; re-open before sending. */
521
+ get expired() { return Date.now() / 1000 >= this.session.expirySec; }
522
+ send(prompt, opts = {}) {
523
+ return runJobOnSession(this.session, prompt, opts);
524
+ }
525
+ }
439
526
  /**
440
527
  * One call, full encrypted inference. Same code path the live playground at
441
528
  * lightnode.app/playground drives, condensed into a single function.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightnode-sdk",
3
- "version": "0.8.9",
3
+ "version": "0.9.0",
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",