lightnode-sdk 0.4.9 → 0.5.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.
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Bridge SDK: typed wrapper around the LightChain bridge (Hyperlane Warp
3
+ * Route). Bridging LCAI between Ethereum mainnet and LightChain mainnet
4
+ * (chain 9200) is the main flow today; the addresses below were extracted
5
+ * from `lightchain-protocol/bridge-ui/src/consts/warpRoutes.ts`.
6
+ *
7
+ * The protocol:
8
+ *
9
+ * Ethereum (chain 1) ─┐ ┌─ LightChain (chain 9200)
10
+ * LCAI ERC-20 │ │ native LCAI
11
+ * 0x9cA8...8927 │ │
12
+ * │ │
13
+ * user.approve() │ transferRemote(9200, recipient, amt) │
14
+ * ───────────► HypERC20Collateral 0x01f80b...e353 ──────────┼───► HypNative 0xEc7096...A6f1
15
+ * │ (locks LCAI in collateral vault) │ (mints / releases native LCAI)
16
+ * └────────────────────────────────────────┘
17
+ *
18
+ * Reverse: user calls transferRemote on HypNative (with native value =
19
+ * amount + quoteGasPayment) to send LCAI back to Ethereum.
20
+ *
21
+ * Both sides expose:
22
+ * - transferRemote(uint32 destination, bytes32 recipient, uint256 amount)
23
+ * payable returns (bytes32 messageId)
24
+ * - quoteGasPayment(uint32 destination) view returns (uint256)
25
+ *
26
+ * For the Ethereum -> LightChain direction the user must first call
27
+ * `approve(0x01f80b...e353, amount)` on the LCAI ERC-20.
28
+ */
29
+ export type BridgeChain = "ethereum" | "lightchain-mainnet";
30
+ export interface BridgeEndpoints {
31
+ /** Numeric chain id (mainnet ETH = 1, LightChain mainnet = 9200). */
32
+ chainId: number;
33
+ /** Hyperlane domain id (matches chainId for these two routes). */
34
+ hyperlaneDomain: number;
35
+ /** The user-facing router (HypERC20Collateral on Ethereum, HypNative on LightChain). */
36
+ router: `0x${string}`;
37
+ /**
38
+ * Underlying ERC-20 the router collateralizes. Null on the synthetic
39
+ * side (LightChain mainnet uses native LCAI). On Ethereum this is the
40
+ * LCAI ERC-20 the user must `approve` before calling `transferRemote`.
41
+ */
42
+ underlying: `0x${string}` | null;
43
+ /** Hyperlane mailbox (the message dispatch contract; useful for tracking). */
44
+ mailbox: `0x${string}`;
45
+ /** Block explorer for this side. */
46
+ explorer: string;
47
+ /** RPC endpoint we know about. */
48
+ rpc: string;
49
+ /** Human label for logs. */
50
+ label: string;
51
+ }
52
+ /** Live LCAI mainnet bridge route. From bridge-ui/src/consts/warpRoutes.ts. */
53
+ export declare const BRIDGE_ROUTE: Record<BridgeChain, BridgeEndpoints>;
54
+ /** Hyperlane TokenRouter ABI (subset we use). */
55
+ export declare const HYPERLANE_ROUTER_ABI: readonly [{
56
+ readonly name: "transferRemote";
57
+ readonly type: "function";
58
+ readonly stateMutability: "payable";
59
+ readonly inputs: readonly [{
60
+ readonly type: "uint32";
61
+ readonly name: "destination";
62
+ }, {
63
+ readonly type: "bytes32";
64
+ readonly name: "recipient";
65
+ }, {
66
+ readonly type: "uint256";
67
+ readonly name: "amount";
68
+ }];
69
+ readonly outputs: readonly [{
70
+ readonly type: "bytes32";
71
+ readonly name: "messageId";
72
+ }];
73
+ }, {
74
+ readonly name: "quoteGasPayment";
75
+ readonly type: "function";
76
+ readonly stateMutability: "view";
77
+ readonly inputs: readonly [{
78
+ readonly type: "uint32";
79
+ readonly name: "destination";
80
+ }];
81
+ readonly outputs: readonly [{
82
+ readonly type: "uint256";
83
+ }];
84
+ }, {
85
+ readonly name: "balanceOf";
86
+ readonly type: "function";
87
+ readonly stateMutability: "view";
88
+ readonly inputs: readonly [{
89
+ readonly type: "address";
90
+ readonly name: "account";
91
+ }];
92
+ readonly outputs: readonly [{
93
+ readonly type: "uint256";
94
+ }];
95
+ }];
96
+ /** Minimal ERC-20 ABI for the LCAI approval step on the Ethereum side. */
97
+ export declare const ERC20_ABI: readonly [{
98
+ readonly name: "approve";
99
+ readonly type: "function";
100
+ readonly stateMutability: "nonpayable";
101
+ readonly inputs: readonly [{
102
+ readonly type: "address";
103
+ readonly name: "spender";
104
+ }, {
105
+ readonly type: "uint256";
106
+ readonly name: "amount";
107
+ }];
108
+ readonly outputs: readonly [{
109
+ readonly type: "bool";
110
+ }];
111
+ }, {
112
+ readonly name: "allowance";
113
+ readonly type: "function";
114
+ readonly stateMutability: "view";
115
+ readonly inputs: readonly [{
116
+ readonly type: "address";
117
+ readonly name: "owner";
118
+ }, {
119
+ readonly type: "address";
120
+ readonly name: "spender";
121
+ }];
122
+ readonly outputs: readonly [{
123
+ readonly type: "uint256";
124
+ }];
125
+ }, {
126
+ readonly name: "balanceOf";
127
+ readonly type: "function";
128
+ readonly stateMutability: "view";
129
+ readonly inputs: readonly [{
130
+ readonly type: "address";
131
+ readonly name: "account";
132
+ }];
133
+ readonly outputs: readonly [{
134
+ readonly type: "uint256";
135
+ }];
136
+ }, {
137
+ readonly name: "decimals";
138
+ readonly type: "function";
139
+ readonly stateMutability: "view";
140
+ readonly inputs: readonly [];
141
+ readonly outputs: readonly [{
142
+ readonly type: "uint8";
143
+ }];
144
+ }];
145
+ /** Pad a 20-byte EVM address to a 32-byte (bytes32) Hyperlane recipient. */
146
+ export declare function addressToBytes32(addr: `0x${string}`): `0x${string}`;
147
+ interface MinimalPublicClient {
148
+ readContract: (args: {
149
+ address: `0x${string}`;
150
+ abi: readonly unknown[];
151
+ functionName: string;
152
+ args?: readonly unknown[];
153
+ }) => Promise<unknown>;
154
+ }
155
+ /**
156
+ * Get the Hyperlane gas-payment quote for delivering ONE bridge message
157
+ * from `from` to `to`. Returned in wei of the FROM chain's gas token
158
+ * (ETH on Ethereum, LCAI on LightChain). Add this to the `value` of the
159
+ * `transferRemote` call.
160
+ */
161
+ export declare function quoteBridgeFee(client: MinimalPublicClient, from: BridgeChain, to: BridgeChain): Promise<bigint>;
162
+ /**
163
+ * Read the underlying token balance of `account` on the FROM side. Returned
164
+ * in raw wei (18 decimals for LCAI on both sides). For Ethereum -> LightChain
165
+ * use `from: "ethereum"` (returns ERC-20 balance). For the reverse use
166
+ * `from: "lightchain-mainnet"` and pass the chain's native-balance reader
167
+ * separately - HypNative does not expose `balanceOf`.
168
+ */
169
+ export declare function bridgeableBalance(client: MinimalPublicClient, from: BridgeChain, account: `0x${string}`): Promise<bigint>;
170
+ /** Read the LCAI ERC-20 allowance the user has approved for the bridge router. */
171
+ export declare function bridgeAllowance(client: MinimalPublicClient, account: `0x${string}`): Promise<bigint>;
172
+ interface MinimalWalletClient {
173
+ writeContract: (args: {
174
+ address: `0x${string}`;
175
+ abi: readonly unknown[];
176
+ functionName: string;
177
+ args: readonly unknown[];
178
+ value?: bigint;
179
+ gas?: bigint;
180
+ }) => Promise<`0x${string}`>;
181
+ }
182
+ /**
183
+ * Approve the Ethereum bridge router to spend LCAI on the user's behalf.
184
+ * Required ONCE before the first Ethereum -> LightChain transfer. The
185
+ * standard pattern is to approve `MaxUint256` so subsequent transfers do
186
+ * not need a second approve. Returns the tx hash.
187
+ */
188
+ export declare function approveBridge(wallet: MinimalWalletClient, amount?: bigint): Promise<`0x${string}`>;
189
+ export interface BridgeTransferArgs {
190
+ /** Source chain. */
191
+ from: BridgeChain;
192
+ /** Destination chain. */
193
+ to: BridgeChain;
194
+ /** Amount to bridge in raw wei (18 decimals for LCAI on both sides). */
195
+ amount: bigint;
196
+ /** Recipient EVM address on the destination chain. Defaults to the signer's address. */
197
+ recipient: `0x${string}`;
198
+ /**
199
+ * Bridge fee in wei to attach as `msg.value`. Get from `quoteBridgeFee()`.
200
+ * On the LightChain side (HypNative) the SDK adds the `amount` to this so
201
+ * the total `value` passed to transferRemote equals `amount + fee`.
202
+ */
203
+ fee: bigint;
204
+ }
205
+ /**
206
+ * Build and send the bridge transferRemote call. For Ethereum -> LightChain,
207
+ * `approveBridge()` must have run first. For LightChain -> Ethereum, no
208
+ * approval is needed (native LCAI is attached as value).
209
+ */
210
+ export declare function bridgeTransfer(wallet: MinimalWalletClient, args: BridgeTransferArgs): Promise<`0x${string}`>;
211
+ /**
212
+ * Convenience wrapper that bundles read + write helpers and exposes the
213
+ * mainnet route addresses as fields. Pass a viem PublicClient for reads
214
+ * and (optionally) a WalletClient for writes.
215
+ */
216
+ export declare class Bridge {
217
+ private readonly publicClient;
218
+ private readonly walletClient?;
219
+ /** Confirmed mainnet route. Currently the only live bridge. */
220
+ readonly route: Record<BridgeChain, BridgeEndpoints>;
221
+ constructor(publicClient: MinimalPublicClient, walletClient?: MinimalWalletClient | undefined);
222
+ /** See `quoteBridgeFee` standalone. */
223
+ quoteFee(from: BridgeChain, to: BridgeChain): Promise<bigint>;
224
+ /** See `bridgeableBalance` standalone. */
225
+ balance(from: BridgeChain, account: `0x${string}`): Promise<bigint>;
226
+ /** See `bridgeAllowance` standalone (Ethereum side only). */
227
+ allowance(account: `0x${string}`): Promise<bigint>;
228
+ /** Approve the Ethereum bridge router. Requires a wallet. */
229
+ approve(amount?: bigint): Promise<`0x${string}`>;
230
+ /** Send a bridge transfer. Requires a wallet. */
231
+ transfer(args: BridgeTransferArgs): Promise<`0x${string}`>;
232
+ }
233
+ export {};
package/dist/bridge.js ADDED
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Bridge SDK: typed wrapper around the LightChain bridge (Hyperlane Warp
3
+ * Route). Bridging LCAI between Ethereum mainnet and LightChain mainnet
4
+ * (chain 9200) is the main flow today; the addresses below were extracted
5
+ * from `lightchain-protocol/bridge-ui/src/consts/warpRoutes.ts`.
6
+ *
7
+ * The protocol:
8
+ *
9
+ * Ethereum (chain 1) ─┐ ┌─ LightChain (chain 9200)
10
+ * LCAI ERC-20 │ │ native LCAI
11
+ * 0x9cA8...8927 │ │
12
+ * │ │
13
+ * user.approve() │ transferRemote(9200, recipient, amt) │
14
+ * ───────────► HypERC20Collateral 0x01f80b...e353 ──────────┼───► HypNative 0xEc7096...A6f1
15
+ * │ (locks LCAI in collateral vault) │ (mints / releases native LCAI)
16
+ * └────────────────────────────────────────┘
17
+ *
18
+ * Reverse: user calls transferRemote on HypNative (with native value =
19
+ * amount + quoteGasPayment) to send LCAI back to Ethereum.
20
+ *
21
+ * Both sides expose:
22
+ * - transferRemote(uint32 destination, bytes32 recipient, uint256 amount)
23
+ * payable returns (bytes32 messageId)
24
+ * - quoteGasPayment(uint32 destination) view returns (uint256)
25
+ *
26
+ * For the Ethereum -> LightChain direction the user must first call
27
+ * `approve(0x01f80b...e353, amount)` on the LCAI ERC-20.
28
+ */
29
+ import { parseAbi } from "viem";
30
+ /** Live LCAI mainnet bridge route. From bridge-ui/src/consts/warpRoutes.ts. */
31
+ export const BRIDGE_ROUTE = {
32
+ ethereum: {
33
+ chainId: 1,
34
+ hyperlaneDomain: 1,
35
+ router: "0x01f80bb8e78e79881E8Ec7832fB6C2c59f64e353",
36
+ underlying: "0x9cA8530CA349c966Fe9ef903Df17a75B8A778927", // LCAI ERC-20
37
+ mailbox: "0x287cf56E5b1435Ae59BF9Ce6443F055A0321a063",
38
+ explorer: "https://etherscan.io",
39
+ rpc: "https://eth.llamarpc.com",
40
+ label: "Ethereum",
41
+ },
42
+ "lightchain-mainnet": {
43
+ chainId: 9200,
44
+ hyperlaneDomain: 9200,
45
+ router: "0xEc7096A3116EE769457C939617375Ec1785AA6f1",
46
+ underlying: null, // HypNative: amount IS the native LCAI value
47
+ mailbox: "0x142a9CEf00ACcAddB76283c49A1Bf37f20c1F00e",
48
+ explorer: "https://mainnet.lightscan.app",
49
+ rpc: "https://rpc.mainnet.lightchain.ai",
50
+ label: "LightChain mainnet",
51
+ },
52
+ };
53
+ /** Hyperlane TokenRouter ABI (subset we use). */
54
+ export const HYPERLANE_ROUTER_ABI = parseAbi([
55
+ "function transferRemote(uint32 destination, bytes32 recipient, uint256 amount) external payable returns (bytes32 messageId)",
56
+ "function quoteGasPayment(uint32 destination) external view returns (uint256)",
57
+ "function balanceOf(address account) external view returns (uint256)",
58
+ ]);
59
+ /** Minimal ERC-20 ABI for the LCAI approval step on the Ethereum side. */
60
+ export const ERC20_ABI = parseAbi([
61
+ "function approve(address spender, uint256 amount) external returns (bool)",
62
+ "function allowance(address owner, address spender) external view returns (uint256)",
63
+ "function balanceOf(address account) external view returns (uint256)",
64
+ "function decimals() external view returns (uint8)",
65
+ ]);
66
+ /** Pad a 20-byte EVM address to a 32-byte (bytes32) Hyperlane recipient. */
67
+ export function addressToBytes32(addr) {
68
+ const hex = addr.toLowerCase().replace(/^0x/, "");
69
+ if (hex.length !== 40)
70
+ throw new Error("bridge: recipient must be a 20-byte EVM address");
71
+ return (`0x${"0".repeat(24)}${hex}`);
72
+ }
73
+ /**
74
+ * Get the Hyperlane gas-payment quote for delivering ONE bridge message
75
+ * from `from` to `to`. Returned in wei of the FROM chain's gas token
76
+ * (ETH on Ethereum, LCAI on LightChain). Add this to the `value` of the
77
+ * `transferRemote` call.
78
+ */
79
+ export async function quoteBridgeFee(client, from, to) {
80
+ if (from === to)
81
+ throw new Error("bridge: source and destination must differ");
82
+ const src = BRIDGE_ROUTE[from];
83
+ const dst = BRIDGE_ROUTE[to];
84
+ const result = (await client.readContract({
85
+ address: src.router,
86
+ abi: HYPERLANE_ROUTER_ABI,
87
+ functionName: "quoteGasPayment",
88
+ args: [dst.hyperlaneDomain],
89
+ }));
90
+ return result;
91
+ }
92
+ /**
93
+ * Read the underlying token balance of `account` on the FROM side. Returned
94
+ * in raw wei (18 decimals for LCAI on both sides). For Ethereum -> LightChain
95
+ * use `from: "ethereum"` (returns ERC-20 balance). For the reverse use
96
+ * `from: "lightchain-mainnet"` and pass the chain's native-balance reader
97
+ * separately - HypNative does not expose `balanceOf`.
98
+ */
99
+ export async function bridgeableBalance(client, from, account) {
100
+ const side = BRIDGE_ROUTE[from];
101
+ if (!side.underlying) {
102
+ throw new Error(`bridge: ${from} bridges native LCAI; query getBalance(account) on the RPC directly`);
103
+ }
104
+ return (await client.readContract({
105
+ address: side.underlying,
106
+ abi: ERC20_ABI,
107
+ functionName: "balanceOf",
108
+ args: [account],
109
+ }));
110
+ }
111
+ /** Read the LCAI ERC-20 allowance the user has approved for the bridge router. */
112
+ export async function bridgeAllowance(client, account) {
113
+ const eth = BRIDGE_ROUTE.ethereum;
114
+ if (!eth.underlying)
115
+ throw new Error("bridge: unreachable - Ethereum side has no underlying");
116
+ return (await client.readContract({
117
+ address: eth.underlying,
118
+ abi: ERC20_ABI,
119
+ functionName: "allowance",
120
+ args: [account, eth.router],
121
+ }));
122
+ }
123
+ /**
124
+ * Approve the Ethereum bridge router to spend LCAI on the user's behalf.
125
+ * Required ONCE before the first Ethereum -> LightChain transfer. The
126
+ * standard pattern is to approve `MaxUint256` so subsequent transfers do
127
+ * not need a second approve. Returns the tx hash.
128
+ */
129
+ export async function approveBridge(wallet, amount = (1n << 256n) - 1n) {
130
+ const eth = BRIDGE_ROUTE.ethereum;
131
+ if (!eth.underlying)
132
+ throw new Error("bridge: unreachable");
133
+ return wallet.writeContract({
134
+ address: eth.underlying,
135
+ abi: ERC20_ABI,
136
+ functionName: "approve",
137
+ args: [eth.router, amount],
138
+ });
139
+ }
140
+ /**
141
+ * Build and send the bridge transferRemote call. For Ethereum -> LightChain,
142
+ * `approveBridge()` must have run first. For LightChain -> Ethereum, no
143
+ * approval is needed (native LCAI is attached as value).
144
+ */
145
+ export async function bridgeTransfer(wallet, args) {
146
+ if (args.from === args.to)
147
+ throw new Error("bridge: source and destination must differ");
148
+ const src = BRIDGE_ROUTE[args.from];
149
+ const dst = BRIDGE_ROUTE[args.to];
150
+ // HypNative requires `value = amount + fee`. HypERC20Collateral takes the
151
+ // ERC-20 from the user's allowance and only requires the fee as `value`.
152
+ const value = src.underlying ? args.fee : args.amount + args.fee;
153
+ return wallet.writeContract({
154
+ address: src.router,
155
+ abi: HYPERLANE_ROUTER_ABI,
156
+ functionName: "transferRemote",
157
+ args: [dst.hyperlaneDomain, addressToBytes32(args.recipient), args.amount],
158
+ value,
159
+ gas: 500000n,
160
+ });
161
+ }
162
+ // =============================================================================
163
+ // LightNode-style facade so consumers can do `new Bridge()` and read fields.
164
+ // =============================================================================
165
+ /**
166
+ * Convenience wrapper that bundles read + write helpers and exposes the
167
+ * mainnet route addresses as fields. Pass a viem PublicClient for reads
168
+ * and (optionally) a WalletClient for writes.
169
+ */
170
+ export class Bridge {
171
+ constructor(publicClient, walletClient) {
172
+ this.publicClient = publicClient;
173
+ this.walletClient = walletClient;
174
+ /** Confirmed mainnet route. Currently the only live bridge. */
175
+ this.route = BRIDGE_ROUTE;
176
+ }
177
+ /** See `quoteBridgeFee` standalone. */
178
+ quoteFee(from, to) {
179
+ return quoteBridgeFee(this.publicClient, from, to);
180
+ }
181
+ /** See `bridgeableBalance` standalone. */
182
+ balance(from, account) {
183
+ return bridgeableBalance(this.publicClient, from, account);
184
+ }
185
+ /** See `bridgeAllowance` standalone (Ethereum side only). */
186
+ allowance(account) {
187
+ return bridgeAllowance(this.publicClient, account);
188
+ }
189
+ /** Approve the Ethereum bridge router. Requires a wallet. */
190
+ approve(amount) {
191
+ if (!this.walletClient)
192
+ throw new Error("bridge: no wallet client; pass one to the Bridge constructor for writes");
193
+ return approveBridge(this.walletClient, amount);
194
+ }
195
+ /** Send a bridge transfer. Requires a wallet. */
196
+ transfer(args) {
197
+ if (!this.walletClient)
198
+ throw new Error("bridge: no wallet client; pass one to the Bridge constructor for writes");
199
+ return bridgeTransfer(this.walletClient, args);
200
+ }
201
+ }
package/dist/chat.d.ts ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Multi-turn conversation helper. The LightChain inference protocol is
3
+ * single-turn at the session level (one createSession + one submitJob =
4
+ * one answer), but stateful chat is the most common builder need. So this
5
+ * keeps the conversation HISTORY client-side, serializes it into a single
6
+ * prompt per turn, and runs one full encrypted inference per turn under
7
+ * the hood. To the protocol it looks like N independent jobs; to the
8
+ * caller it reads as a coherent chat.
9
+ *
10
+ * Usage:
11
+ *
12
+ * const chat = new Conversation({ network: "testnet", privateKey: "0x..." });
13
+ * const a = await chat.send("Who wrote 'The Great Gatsby'?");
14
+ * const b = await chat.send("In what year?"); // 'b' sees the prior turn
15
+ * console.log(chat.messages()); // full transcript
16
+ */
17
+ import { type RunInferenceWithKeyArgs, type RunInferenceResult } from "./inference.js";
18
+ export type ChatRole = "system" | "user" | "assistant";
19
+ export interface ChatMessage {
20
+ role: ChatRole;
21
+ content: string;
22
+ }
23
+ export interface ConversationOptions extends Omit<RunInferenceWithKeyArgs, "prompt"> {
24
+ /**
25
+ * Initial system message (optional). Prepended to the serialized prompt on
26
+ * every turn. Use for persona, response constraints, or guardrails.
27
+ */
28
+ system?: string;
29
+ /**
30
+ * Cap how many prior turns are serialized into each new prompt. Older
31
+ * turns drop off in FIFO order. Default 20. Models tolerate longer
32
+ * histories but per-call fees scale with prompt length on token-priced
33
+ * networks.
34
+ */
35
+ maxHistoryTurns?: number;
36
+ }
37
+ export interface ConversationSendResult extends RunInferenceResult {
38
+ /** Updated transcript after this turn (includes the latest user + assistant pair). */
39
+ messages: ChatMessage[];
40
+ }
41
+ /**
42
+ * One round of `send` returns the assistant's reply plus all the
43
+ * on-chain receipts. `messages()` exposes the running transcript so a UI
44
+ * can render it; `reset()` clears history.
45
+ */
46
+ export declare class Conversation {
47
+ private readonly opts;
48
+ private readonly history;
49
+ constructor(opts: ConversationOptions);
50
+ /** Read-only snapshot of the conversation so far. */
51
+ messages(): ChatMessage[];
52
+ /** Drop the running history (the next send becomes a fresh first turn). */
53
+ reset(): void;
54
+ /**
55
+ * Push a single user message and run one full inference. Returns the
56
+ * assistant's reply plus the standard runInference result (txs, worker,
57
+ * jobId). The assistant's reply is automatically appended to history so
58
+ * the next send sees it.
59
+ */
60
+ send(message: string): Promise<ConversationSendResult>;
61
+ /**
62
+ * Format the current history as a single text prompt the model can read.
63
+ * Chat-style turn markers ("User:" / "Assistant:") since the protocol's
64
+ * llama3-8b serving stack treats prompts as raw text and any reasonable
65
+ * formatting works. Uses the configured max-history-turns cap.
66
+ */
67
+ private serialize;
68
+ }
69
+ /** Functional shortcut for `new Conversation(opts)` so it reads inline. */
70
+ export declare function chat(opts: ConversationOptions): Conversation;
package/dist/chat.js ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Multi-turn conversation helper. The LightChain inference protocol is
3
+ * single-turn at the session level (one createSession + one submitJob =
4
+ * one answer), but stateful chat is the most common builder need. So this
5
+ * keeps the conversation HISTORY client-side, serializes it into a single
6
+ * prompt per turn, and runs one full encrypted inference per turn under
7
+ * the hood. To the protocol it looks like N independent jobs; to the
8
+ * caller it reads as a coherent chat.
9
+ *
10
+ * Usage:
11
+ *
12
+ * const chat = new Conversation({ network: "testnet", privateKey: "0x..." });
13
+ * const a = await chat.send("Who wrote 'The Great Gatsby'?");
14
+ * const b = await chat.send("In what year?"); // 'b' sees the prior turn
15
+ * console.log(chat.messages()); // full transcript
16
+ */
17
+ import { runInferenceWithKey } from "./inference.js";
18
+ /**
19
+ * One round of `send` returns the assistant's reply plus all the
20
+ * on-chain receipts. `messages()` exposes the running transcript so a UI
21
+ * can render it; `reset()` clears history.
22
+ */
23
+ export class Conversation {
24
+ constructor(opts) {
25
+ this.history = [];
26
+ if (!opts.network)
27
+ throw new Error("Conversation: network is required");
28
+ if (!opts.privateKey)
29
+ throw new Error("Conversation: privateKey is required");
30
+ this.opts = opts;
31
+ }
32
+ /** Read-only snapshot of the conversation so far. */
33
+ messages() {
34
+ return [...this.history];
35
+ }
36
+ /** Drop the running history (the next send becomes a fresh first turn). */
37
+ reset() {
38
+ this.history.length = 0;
39
+ }
40
+ /**
41
+ * Push a single user message and run one full inference. Returns the
42
+ * assistant's reply plus the standard runInference result (txs, worker,
43
+ * jobId). The assistant's reply is automatically appended to history so
44
+ * the next send sees it.
45
+ */
46
+ async send(message) {
47
+ if (!message?.trim())
48
+ throw new Error("Conversation.send: message is empty");
49
+ // 1. Add the new user turn BEFORE serializing so the model sees it.
50
+ this.history.push({ role: "user", content: message });
51
+ // 2. Build the serialized prompt: system (if any) + last N turns.
52
+ const prompt = this.serialize();
53
+ // 3. Run one full encrypted inference with the conversation as prompt.
54
+ const result = await runInferenceWithKey({
55
+ ...this.opts,
56
+ prompt,
57
+ });
58
+ // 4. Append the assistant's reply and return.
59
+ this.history.push({ role: "assistant", content: result.answer });
60
+ return { ...result, messages: this.messages() };
61
+ }
62
+ /**
63
+ * Format the current history as a single text prompt the model can read.
64
+ * Chat-style turn markers ("User:" / "Assistant:") since the protocol's
65
+ * llama3-8b serving stack treats prompts as raw text and any reasonable
66
+ * formatting works. Uses the configured max-history-turns cap.
67
+ */
68
+ serialize() {
69
+ const cap = this.opts.maxHistoryTurns ?? 20;
70
+ // A "turn" here is one message; cap*2 messages = cap user+assistant pairs.
71
+ const recent = this.history.slice(Math.max(0, this.history.length - cap * 2));
72
+ const sys = this.opts.system?.trim();
73
+ const lines = [];
74
+ if (sys)
75
+ lines.push(`System: ${sys}`);
76
+ for (const m of recent) {
77
+ const tag = m.role === "user" ? "User" : m.role === "assistant" ? "Assistant" : "System";
78
+ lines.push(`${tag}: ${m.content}`);
79
+ }
80
+ // Trailing prompt for the model to continue from.
81
+ lines.push("Assistant:");
82
+ return lines.join("\n");
83
+ }
84
+ }
85
+ /** Functional shortcut for `new Conversation(opts)` so it reads inline. */
86
+ export function chat(opts) {
87
+ return new Conversation(opts);
88
+ }