@genkaku/protocol 0.0.0-stage → 0.1.1

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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bridge.d.ts +46 -0
  3. package/dist/bridge.d.ts.map +1 -0
  4. package/dist/bridge.js +609 -0
  5. package/dist/capabilities.d.ts +50 -0
  6. package/dist/capabilities.d.ts.map +1 -0
  7. package/dist/capabilities.js +66 -0
  8. package/dist/capabilities.test.d.ts +2 -0
  9. package/dist/capabilities.test.d.ts.map +1 -0
  10. package/dist/capabilities.test.js +39 -0
  11. package/dist/chain.d.ts +496 -0
  12. package/dist/chain.d.ts.map +1 -0
  13. package/dist/chain.js +763 -0
  14. package/dist/credit.d.ts +35 -0
  15. package/dist/credit.d.ts.map +1 -0
  16. package/dist/credit.js +112 -0
  17. package/dist/dial.d.ts +51 -0
  18. package/dist/dial.d.ts.map +1 -0
  19. package/dist/dial.js +287 -0
  20. package/dist/directory.d.ts +39 -0
  21. package/dist/directory.d.ts.map +1 -0
  22. package/dist/directory.js +149 -0
  23. package/dist/discovery.d.ts +20 -0
  24. package/dist/discovery.d.ts.map +1 -0
  25. package/dist/discovery.js +58 -0
  26. package/dist/hello.d.ts +14 -0
  27. package/dist/hello.d.ts.map +1 -0
  28. package/dist/hello.js +42 -0
  29. package/dist/index.d.ts +32 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +32 -0
  32. package/dist/keys.d.ts +30 -0
  33. package/dist/keys.d.ts.map +1 -0
  34. package/dist/keys.js +126 -0
  35. package/dist/llmBridge.d.ts +100 -0
  36. package/dist/llmBridge.d.ts.map +1 -0
  37. package/dist/llmBridge.js +284 -0
  38. package/dist/lp.d.ts +31 -0
  39. package/dist/lp.d.ts.map +1 -0
  40. package/dist/lp.js +116 -0
  41. package/dist/modeManager.d.ts +30 -0
  42. package/dist/modeManager.d.ts.map +1 -0
  43. package/dist/modeManager.js +81 -0
  44. package/dist/netconfig.d.ts +11 -0
  45. package/dist/netconfig.d.ts.map +1 -0
  46. package/dist/netconfig.js +39 -0
  47. package/dist/offer.d.ts +89 -0
  48. package/dist/offer.d.ts.map +1 -0
  49. package/dist/offer.js +86 -0
  50. package/dist/offer.test.d.ts +2 -0
  51. package/dist/offer.test.d.ts.map +1 -0
  52. package/dist/offer.test.js +33 -0
  53. package/dist/ollama.d.ts +53 -0
  54. package/dist/ollama.d.ts.map +1 -0
  55. package/dist/ollama.js +162 -0
  56. package/dist/p2pNode.d.ts +52 -0
  57. package/dist/p2pNode.d.ts.map +1 -0
  58. package/dist/p2pNode.js +454 -0
  59. package/dist/paywall.d.ts +30 -0
  60. package/dist/paywall.d.ts.map +1 -0
  61. package/dist/paywall.js +84 -0
  62. package/dist/publishOptIn.d.ts +15 -0
  63. package/dist/publishOptIn.d.ts.map +1 -0
  64. package/dist/publishOptIn.js +34 -0
  65. package/dist/receipts.d.ts +61 -0
  66. package/dist/receipts.d.ts.map +1 -0
  67. package/dist/receipts.js +135 -0
  68. package/dist/registry.d.ts +19 -0
  69. package/dist/registry.d.ts.map +1 -0
  70. package/dist/registry.js +50 -0
  71. package/dist/rpc.d.ts +24 -0
  72. package/dist/rpc.d.ts.map +1 -0
  73. package/dist/rpc.js +143 -0
  74. package/dist/status.d.ts +56 -0
  75. package/dist/status.d.ts.map +1 -0
  76. package/dist/status.js +354 -0
  77. package/dist/userSession.d.ts +77 -0
  78. package/dist/userSession.d.ts.map +1 -0
  79. package/dist/userSession.js +132 -0
  80. package/dist/workerSession.d.ts +87 -0
  81. package/dist/workerSession.d.ts.map +1 -0
  82. package/dist/workerSession.js +97 -0
  83. package/dist/workerSession.test.d.ts +2 -0
  84. package/dist/workerSession.test.d.ts.map +1 -0
  85. package/dist/workerSession.test.js +49 -0
  86. package/package.json +56 -3
  87. package/README.md +0 -3
package/dist/status.js ADDED
@@ -0,0 +1,354 @@
1
+ // GENKAKU: worker-side status surface — GET /genkaku (dashboard) and
2
+ // GET /genkaku/status (JSON). Snapshot is called per request so the page and
3
+ // the JSON always reflect the live sidecar, never a boot-time copy.
4
+ import { createServer } from 'node:http';
5
+ /** Self-contained dashboard; mirrors the user bridge style and polls the JSON. */
6
+ const DASHBOARD_HTML = `<!doctype html>
7
+ <html lang="en">
8
+ <head>
9
+ <meta charset="utf-8">
10
+ <meta name="viewport" content="width=device-width,initial-scale=1">
11
+ <title>GENKAKU worker</title>
12
+ <style>
13
+ body{font:14px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;margin:24px;background:#0b0d10;color:#d7dde5}
14
+ h1{font-size:18px;margin:0 0 16px}
15
+ .m{color:#7d8794;font-weight:400}
16
+ .card{border:1px solid #222832;border-radius:8px;padding:12px;margin:12px 0;background:#11151b}
17
+ b{color:#7d8794;font-weight:500;text-transform:uppercase;font-size:11px;letter-spacing:.08em;display:block;margin-bottom:4px}
18
+ table{border-collapse:collapse;width:100%}
19
+ td,th{text-align:left;padding:4px 8px;border-bottom:1px solid #1c222b}
20
+ th{color:#7d8794;font-weight:500}
21
+ input.in{background:#0b0d10;color:#d7dde5;border:1px solid #2a323d;border-radius:4px;padding:4px 6px;width:100%;box-sizing:border-box}
22
+ input.num{text-align:right}
23
+ button{background:#1b2330;color:#d7dde5;border:1px solid #2a323d;border-radius:4px;padding:4px 10px;cursor:pointer}
24
+ button.primary{background:#1f6f3f;border-color:#2a8a50}
25
+ .actions{display:flex;gap:8px;margin-top:8px}
26
+ td button{padding:2px 8px}
27
+ </style>
28
+ </head>
29
+ <body>
30
+ <h1>GENKAKU <span class="m">worker</span></h1>
31
+ <div id="info">loading...</div>
32
+ <div class="card" id="offers-card"></div>
33
+ <script>
34
+ var e=function(v){return String(v==null?'':v).replace(/[&<>"']/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]})};
35
+ function usdc(v){var n=Number(v);return isFinite(n)?(n/1e6).toFixed(2):e(v)}
36
+ function short(v){v=String(v||'');return v.length>12?v.slice(0,6)+'...'+v.slice(-4):v}
37
+ function card(label,value){return '<div class="card"><b>'+label+'</b>'+e(value)+'</div>'}
38
+ var rows=null;
39
+ function microToUsdc(v){var n=Number(v);return isFinite(n)?(n/1e6).toFixed(2):'0.00'}
40
+ function usdcToMicro(v){var n=Number(v);return isFinite(n)?Math.round(n*1e6):0}
41
+ function offersHtml(){
42
+ var body=(rows||[]).map(function(r,i){
43
+ return '<tr><td><input class="in" value="'+e(r.model)+'" oninput="setModel('+i+',this.value)"></td>'
44
+ +'<td><input class="in num" inputmode="decimal" value="'+e(r.price)+'" oninput="setPrice('+i+',this.value)"></td>'
45
+ +'<td><button type="button" onclick="removeRow('+i+')">remove</button></td></tr>';
46
+ }).join('');
47
+ return '<b>offers</b><table><tr><th>model</th><th>USDC/hour</th><th></th></tr>'
48
+ +(body||'<tr><td colspan="3">no offers</td></tr>')
49
+ +'</table><div class="actions"><button type="button" onclick="addRow()">add offer</button>'
50
+ +'<button type="button" class="primary" onclick="saveCatalog()">save</button></div>';
51
+ }
52
+ function renderOffers(){
53
+ var el=document.getElementById('offers-card');
54
+ if(el){el.innerHTML=offersHtml()}
55
+ }
56
+ function setModel(i,v){if(rows&&rows[i]){rows[i].model=v}}
57
+ function setPrice(i,v){if(rows&&rows[i]){rows[i].price=v}}
58
+ function addRow(){if(!rows){rows=[]}rows.push({model:'',price:'0.00'});renderOffers()}
59
+ function removeRow(i){if(rows){rows.splice(i,1);renderOffers()}}
60
+ function seedRows(offers){
61
+ rows=(offers||[]).map(function(r){var o=r.offer||{};return {model:String(o.model||''),price:microToUsdc(o.pricePerHour)}});
62
+ }
63
+ function switchMode(mode){
64
+ fetch('/genkaku/mode',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({mode:mode})})
65
+ .then(function(res){return res.json().then(function(body){return {ok:res.ok,body:body}})})
66
+ .then(function(r){
67
+ if(!r.ok||!r.body.ok){throw new Error((r.body.error&&r.body.error.message)||'mode switch failed')}
68
+ setTimeout(function(){location.reload()},2000);
69
+ })
70
+ .catch(function(err){alert('Mode switch failed: '+(err&&err.message?err.message:String(err)))});
71
+ }
72
+ function roleRow(role){
73
+ return '<div class="card"><b>role: '+e(role)+'</b><div class="actions">'
74
+ +'<button type="button" data-mode="user">user</button>'
75
+ +'<button type="button" data-mode="localPlusSharing">localPlusSharing</button>'
76
+ +'<button type="button" data-mode="workerDedicated">workerDedicated</button>'
77
+ +'</div></div>';
78
+ }
79
+ document.addEventListener('click',function(ev){
80
+ var t=ev.target;
81
+ if(t&&t.getAttribute&&t.getAttribute('data-mode')){switchMode(t.getAttribute('data-mode'))}
82
+ });
83
+ function renderInfo(s){
84
+ document.getElementById('info').innerHTML=roleRow(s.role)+card('peer id',s.peerId)
85
+ +card('peer addresses',(s.peerAddrs||[]).join(', '))+card('wallet',s.wallet)
86
+ +card('active sessions',s.activeSessions)
87
+ +card('discovery',s.reachable
88
+ ? (s.publishAddress
89
+ ? 'global directory (public address published)'
90
+ : 'public address available — publishing is OFF (privacy); enable publishAddress to be found')
91
+ : 'this network only — not listed in the global directory');
92
+ }
93
+ async function load(){
94
+ try{
95
+ var res=await fetch('/genkaku/status',{cache:'no-store'});
96
+ var s=await res.json();
97
+ renderInfo(s);
98
+ if(rows===null){seedRows(s.offers);renderOffers()}
99
+ }catch(err){
100
+ document.getElementById('info').innerHTML='<div class="card">status unavailable: '+e(err&&err.message?err.message:String(err))+'</div>';
101
+ }
102
+ }
103
+ async function saveCatalog(){
104
+ var payload=(rows||[]).map(function(r){return {model:String(r.model||'').trim(),pricePerHour:usdcToMicro(r.price)}});
105
+ try{
106
+ var res=await fetch('/genkaku/catalog',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({catalog:payload})});
107
+ var body=await res.json();
108
+ if(!res.ok){throw new Error((body.error&&body.error.message)||String(res.status))}
109
+ seedRows(body.offers);
110
+ renderInfo(body);
111
+ renderOffers();
112
+ }catch(err){
113
+ alert('Save failed: '+(err&&err.message?err.message:String(err)));
114
+ }
115
+ }
116
+ load();
117
+ setInterval(load,5000);
118
+ </script>
119
+ </body>
120
+ </html>`;
121
+ function json(res, body) {
122
+ res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
123
+ res.end(JSON.stringify(body));
124
+ }
125
+ function failure(res, status, message) {
126
+ res.writeHead(status, { 'content-type': 'application/json' });
127
+ res.end(JSON.stringify({ error: { message } }));
128
+ }
129
+ async function readJson(req) {
130
+ const chunks = [];
131
+ let bytes = 0;
132
+ for await (const chunk of req) {
133
+ bytes += chunk.length;
134
+ if (bytes > 1024 * 1024)
135
+ throw new Error('request body too large');
136
+ chunks.push(chunk);
137
+ }
138
+ return JSON.parse(Buffer.concat(chunks).toString('utf8') || 'null');
139
+ }
140
+ /** Validate {catalog:[{model, pricePerHour, ...}]}; price may be a numeric string. */
141
+ function parseCatalog(body) {
142
+ if (typeof body !== 'object' || body === null || !Array.isArray(body.catalog)) {
143
+ return { ok: false, error: 'body must be {"catalog": [...]}' };
144
+ }
145
+ const catalog = [];
146
+ for (const row of body.catalog) {
147
+ if (typeof row !== 'object' || row === null)
148
+ return { ok: false, error: 'catalog rows must be objects' };
149
+ const model = row.model;
150
+ if (typeof model !== 'string' || model.trim() === '') {
151
+ return { ok: false, error: 'catalog row model must be a non-empty string' };
152
+ }
153
+ const raw = row.pricePerHour;
154
+ const price = typeof raw === 'number'
155
+ ? raw
156
+ : typeof raw === 'string' && raw.trim() !== '' ? Number(raw) : Number.NaN;
157
+ if (!Number.isFinite(price)) {
158
+ return { ok: false, error: 'catalog row pricePerHour must be a finite number' };
159
+ }
160
+ catalog.push({ ...row, model: model.trim(), pricePerHour: price });
161
+ }
162
+ return { ok: true, catalog };
163
+ }
164
+ /** Serve the dashboard and live status JSON on 127.0.0.1. */
165
+ export async function startStatusServer(port, snapshot, onCatalog, onMode, ollama, onPublish) {
166
+ const server = createServer((req, res) => {
167
+ void (async () => {
168
+ try {
169
+ const path = (req.url ?? '/').split('?')[0] ?? '/';
170
+ if (req.method === 'GET' || req.method === 'POST' || req.method === 'OPTIONS') {
171
+ res.setHeader('access-control-allow-origin', '*');
172
+ res.setHeader('access-control-allow-methods', 'GET, POST, OPTIONS');
173
+ res.setHeader('access-control-allow-headers', 'content-type');
174
+ }
175
+ if (req.method === 'OPTIONS') {
176
+ res.writeHead(204);
177
+ res.end();
178
+ return;
179
+ }
180
+ if (req.method === 'POST' && (path === '/genkaku/mode' || path === '/genkaku/mode/')) {
181
+ if (onMode == null) {
182
+ failure(res, 501, 'mode switching is not enabled');
183
+ return;
184
+ }
185
+ let body;
186
+ try {
187
+ body = await readJson(req);
188
+ }
189
+ catch {
190
+ failure(res, 400, 'invalid JSON body');
191
+ return;
192
+ }
193
+ const mode = body.mode;
194
+ if (mode !== 'user' && mode !== 'localPlusSharing' && mode !== 'workerDedicated') {
195
+ failure(res, 400, 'mode must be one of user, localPlusSharing, workerDedicated');
196
+ return;
197
+ }
198
+ await onMode(mode);
199
+ json(res, { ok: true });
200
+ return;
201
+ }
202
+ if (req.method === 'POST' && (path === '/genkaku/catalog' || path === '/genkaku/catalog/')) {
203
+ if (onCatalog == null) {
204
+ failure(res, 501, 'catalog updates are not enabled');
205
+ return;
206
+ }
207
+ let body;
208
+ try {
209
+ body = await readJson(req);
210
+ }
211
+ catch {
212
+ failure(res, 400, 'invalid JSON body');
213
+ return;
214
+ }
215
+ const parsed = parseCatalog(body);
216
+ if (!parsed.ok) {
217
+ failure(res, 400, parsed.error);
218
+ return;
219
+ }
220
+ await onCatalog(parsed.catalog);
221
+ json(res, await snapshot());
222
+ return;
223
+ }
224
+ if (req.method === 'POST' && (path === '/genkaku/publish' || path === '/genkaku/publish/')) {
225
+ if (onPublish == null) {
226
+ failure(res, 501, 'directory publishing is not enabled');
227
+ return;
228
+ }
229
+ let body;
230
+ try {
231
+ body = await readJson(req);
232
+ }
233
+ catch {
234
+ failure(res, 400, 'invalid JSON body');
235
+ return;
236
+ }
237
+ if (typeof body.publish !== 'boolean') {
238
+ failure(res, 400, 'publish must be a boolean');
239
+ return;
240
+ }
241
+ await onPublish(body.publish);
242
+ json(res, { ok: true });
243
+ return;
244
+ }
245
+ if (path === '/genkaku/ollama' || path === '/genkaku/ollama/') {
246
+ if (ollama == null) {
247
+ failure(res, 501, 'ollama onboarding is not enabled in this role');
248
+ return;
249
+ }
250
+ if (req.method === 'GET') {
251
+ json(res, await ollama.state());
252
+ return;
253
+ }
254
+ if (req.method === 'POST') {
255
+ json(res, await ollama.ensureRunning());
256
+ return;
257
+ }
258
+ }
259
+ if (req.method === 'POST' && (path === '/genkaku/ollama/pull' || path === '/genkaku/ollama/pull/')) {
260
+ if (ollama == null) {
261
+ failure(res, 501, 'ollama onboarding is not enabled in this role');
262
+ return;
263
+ }
264
+ let body;
265
+ try {
266
+ body = await readJson(req);
267
+ }
268
+ catch {
269
+ failure(res, 400, 'invalid JSON body');
270
+ return;
271
+ }
272
+ if (typeof body.model !== 'string' || body.model.trim() === '') {
273
+ failure(res, 400, 'model must be a non-empty string');
274
+ return;
275
+ }
276
+ res.writeHead(200, { 'content-type': 'application/x-ndjson', 'cache-control': 'no-store' });
277
+ const frame = (value) => { res.write(`${JSON.stringify(value)}\n`); };
278
+ try {
279
+ const result = await ollama.pull(body.model.trim(), (progress) => { frame(progress); });
280
+ frame({ done: true, status: result.status });
281
+ }
282
+ catch (error) {
283
+ frame({ error: { message: error instanceof Error ? error.message : String(error) } });
284
+ }
285
+ res.end();
286
+ return;
287
+ }
288
+ if (req.method === 'POST' && (path === '/genkaku/ollama/delete' || path === '/genkaku/ollama/delete/')) {
289
+ if (ollama == null) {
290
+ failure(res, 501, 'ollama onboarding is not enabled in this role');
291
+ return;
292
+ }
293
+ let body;
294
+ try {
295
+ body = await readJson(req);
296
+ }
297
+ catch {
298
+ failure(res, 400, 'invalid JSON body');
299
+ return;
300
+ }
301
+ if (typeof body.model !== 'string' || body.model.trim() === '') {
302
+ failure(res, 400, 'model must be a non-empty string');
303
+ return;
304
+ }
305
+ await ollama.remove(body.model.trim());
306
+ json(res, { ok: true });
307
+ return;
308
+ }
309
+ if (req.method === 'GET' && (path === '/genkaku/status' || path === '/genkaku/status/')) {
310
+ json(res, await snapshot());
311
+ return;
312
+ }
313
+ if (req.method === 'GET' && (path === '/genkaku' || path === '/genkaku/')) {
314
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
315
+ res.end(DASHBOARD_HTML);
316
+ return;
317
+ }
318
+ failure(res, 404, 'unknown route: ' + (req.method ?? 'GET') + ' ' + path);
319
+ }
320
+ catch (error) {
321
+ const message = error instanceof Error ? error.message : String(error);
322
+ if (!res.headersSent)
323
+ failure(res, 500, message);
324
+ else if (!res.writableEnded)
325
+ res.end();
326
+ }
327
+ })();
328
+ });
329
+ await new Promise((resolve, reject) => {
330
+ const onError = (error) => { reject(error); };
331
+ server.once('error', onError);
332
+ server.listen(port, '127.0.0.1', () => {
333
+ server.off('error', onError);
334
+ resolve();
335
+ });
336
+ });
337
+ const bound = server.address();
338
+ const boundPort = typeof bound === 'object' && bound !== null ? bound.port : port;
339
+ return {
340
+ url: `http://127.0.0.1:${boundPort}/genkaku`,
341
+ close: async () => {
342
+ await new Promise((resolve, reject) => {
343
+ server.close((error) => {
344
+ if (error == null)
345
+ resolve();
346
+ else
347
+ reject(error);
348
+ });
349
+ server.closeAllConnections();
350
+ });
351
+ },
352
+ };
353
+ }
354
+ //# sourceMappingURL=status.js.map
@@ -0,0 +1,77 @@
1
+ import type { WorkerTransport } from './dial.ts';
2
+ import type { ChatMessage } from './llmBridge.ts';
3
+ import type { ModeManager } from './modeManager.ts';
4
+ import { type NetEnvironment } from './netconfig.ts';
5
+ import type { Offer } from './offer.ts';
6
+ import { type ReceiptPayloadV1 } from './receipts.ts';
7
+ export type UserSessionEvent = {
8
+ type: 'session-open';
9
+ jobId: string;
10
+ worker: string;
11
+ model: string;
12
+ pricePerHour: bigint;
13
+ } | {
14
+ type: 'token';
15
+ delta: string;
16
+ }
17
+ /** A tool call produced by the model, relayed by an open/1 worker. */
18
+ | {
19
+ type: 'tool-delta';
20
+ index: number;
21
+ id: string;
22
+ name: string;
23
+ argumentsDelta: string;
24
+ } | {
25
+ type: 'receipt-sent';
26
+ seq: number;
27
+ amount: bigint;
28
+ receiptHash: string;
29
+ sig: string;
30
+ } | {
31
+ type: 'session-end';
32
+ jobId: string;
33
+ elapsedMs: number;
34
+ billed: bigint;
35
+ } | {
36
+ type: 'session-error';
37
+ message: string;
38
+ };
39
+ export type UserSessionOptions = {
40
+ jobId: string;
41
+ /** the row the user picked from the sorted marketplace list */
42
+ offer: Offer;
43
+ prompt: string;
44
+ system?: string;
45
+ maxTokens?: number;
46
+ /** Whole conversation; when present the session opens as open/1. */
47
+ messages?: ChatMessage[];
48
+ /** JSON schemas the model may call. Requires `messages`. */
49
+ tools?: unknown[];
50
+ transport: WorkerTransport;
51
+ modeManager: ModeManager;
52
+ onToken?: (delta: string) => void;
53
+ emit?: (e: UserSessionEvent) => void;
54
+ /** injectable clock for tests; defaults to wall time */
55
+ now?: () => number;
56
+ /** billing + receipt tick; MUST divide 3,600,000 (default 60 s = per minute) */
57
+ unitMs?: number;
58
+ /** decimal string of the REAL JobEscrow session id; "0" = unpriced self-test */
59
+ escrowId?: string;
60
+ /** data-plane chain the receipts bind to (default base-sepolia) */
61
+ env?: NetEnvironment;
62
+ /** the user wallet's receipt signer — P4.4: receipts are signed evidence */
63
+ signReceipt?: (receipt: ReceiptPayloadV1) => Promise<string>;
64
+ };
65
+ export type UserSessionResult = {
66
+ jobId: string;
67
+ elapsedMs: number;
68
+ billed: bigint;
69
+ tokens: number;
70
+ /** set when the worker refused or the stream broke before any answer */
71
+ failure?: {
72
+ message: string;
73
+ code?: string;
74
+ };
75
+ };
76
+ export declare function runUserSession(opts: UserSessionOptions): Promise<UserSessionResult>;
77
+ //# sourceMappingURL=userSession.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"userSession.d.ts","sourceRoot":"","sources":["../src/userSession.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACnD,OAAO,EAAuB,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACzE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAEvC,OAAO,EAKL,KAAK,gBAAgB,EAEtB,MAAM,eAAe,CAAA;AAGtB,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAC5F;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AAClC,sEAAsE;GACpE;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,GACvF;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACvF;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACzE;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9C,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,+DAA+D;IAC/D,KAAK,EAAE,KAAK,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAA;IACxB,4DAA4D;IAC5D,KAAK,CAAC,EAAE,OAAO,EAAE,CAAA;IACjB,SAAS,EAAE,eAAe,CAAA;IAC1B,WAAW,EAAE,WAAW,CAAA;IACxB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,gBAAgB,KAAK,IAAI,CAAA;IACpC,wDAAwD;IACxD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,mEAAmE;IACnE,GAAG,CAAC,EAAE,cAAc,CAAA;IACpB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;CAC7D,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,wEAAwE;IACxE,OAAO,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAC7C,CAAA;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA+HzF"}
@@ -0,0 +1,132 @@
1
+ // GENKAKU: user session service (mode A, P4.3; escrow-wired P4.4).
2
+ // Consumes a remote worker's LLM with TIME-BASED billing: the offer quotes a
3
+ // price PER HOUR (worker's free choice); the user pays per STARTED MINUTE via
4
+ // incremental /genkaku/receipt/0 offers. Worker's max unbilled exposure is one
5
+ // minute. P4.4: priced sessions carry a REAL escrowId and every receipt is
6
+ // EIP-191-signed by the user's wallet over the packed digest (JobEscrow._verify
7
+ // shape); escrowless self-test sessions ("0") ride unsigned for compat.
8
+ import { CHAIN_ID, CONTRACTS } from "./netconfig.js";
9
+ import { cumulativeBilled } from "./offer.js";
10
+ import { digestForHex, inputHashOf, outputHashOf, ZERO_HASH, sessionIdHex, } from "./receipts.js";
11
+ export async function runUserSession(opts) {
12
+ const unitMs = opts.unitMs ?? 60_000;
13
+ const now = opts.now ?? (() => Date.now());
14
+ const emit = (e) => opts.emit?.(e);
15
+ let lastAmount = 0n;
16
+ let seq = 0;
17
+ let tokens = 0;
18
+ let failure;
19
+ let paying = false;
20
+ // D-block: remote consumption is legal ONLY in user mode — throws otherwise.
21
+ opts.modeManager.beginRemoteSession();
22
+ const escrowId = opts.escrowId ?? '0';
23
+ const env = opts.env ?? 'base-sepolia';
24
+ const sessionId = sessionIdHex(opts.jobId, escrowId);
25
+ const inputHash = inputHashOf({
26
+ model: opts.offer.model,
27
+ prompt: opts.prompt,
28
+ system: opts.system,
29
+ maxTokens: opts.maxTokens,
30
+ });
31
+ let output = '';
32
+ try {
33
+ const common = {
34
+ jobId: opts.jobId,
35
+ escrowId,
36
+ model: opts.offer.model,
37
+ prompt: opts.prompt,
38
+ system: opts.system,
39
+ maxTokens: opts.maxTokens,
40
+ clientPeer: 'user', // replaced by the real peer id at app wiring
41
+ billing: { mode: 'minutes', unitMs, pricePerHour: opts.offer.pricePerHour.toString(), currency: 'USDC' },
42
+ };
43
+ // A conversation with tool schemas is served by Ollama's chat API; the
44
+ // single-prompt form cannot carry either.
45
+ const envelope = opts.messages == null
46
+ ? { proto: 'open/0', ...common }
47
+ : { proto: 'open/1', ...common, messages: opts.messages, ...(opts.tools === undefined ? {} : { tools: opts.tools }) };
48
+ emit({ type: 'session-open', jobId: opts.jobId, worker: opts.offer.workerName, model: opts.offer.model, pricePerHour: opts.offer.pricePerHour });
49
+ await opts.transport.open(envelope);
50
+ opts.transport.onToolCall((call) => {
51
+ emit({ type: 'tool-delta', ...call });
52
+ });
53
+ opts.transport.onToken((delta) => {
54
+ tokens += 1;
55
+ output += delta;
56
+ opts.onToken?.(delta);
57
+ emit({ type: 'token', delta });
58
+ });
59
+ const startMs = now();
60
+ const pay = async (final = false) => {
61
+ if (paying) {
62
+ return;
63
+ }
64
+ paying = true;
65
+ try {
66
+ const amount = cumulativeBilled(opts.offer.pricePerHour, unitMs, now() - startMs);
67
+ // The through-end receipt is sent even when the amount did not move:
68
+ // it is what commits the OUTPUT hash, so settle can be challenged.
69
+ if (amount > lastAmount || final) {
70
+ seq += 1;
71
+ lastAmount = amount;
72
+ // P4.4 signed receipt: EIP-191 sig over the packed digest — the escrow
73
+ // binds this EXACT hash at settle (receiptHashFor in chain.ts).
74
+ const receipt = {
75
+ chainId: CHAIN_ID[env],
76
+ escrow: (CONTRACTS[env].escrow || '0x0000000000000000000000000000000000000000'),
77
+ sessionId,
78
+ seq,
79
+ amount,
80
+ inputHash,
81
+ outputHash: final ? outputHashOf(output) : ZERO_HASH,
82
+ };
83
+ let sig = '';
84
+ if (escrowId !== '0' && opts.signReceipt != null) {
85
+ sig = await opts.signReceipt(receipt);
86
+ }
87
+ // A dead transport must not kill the billing loop — report and keep
88
+ // the ticker alive until the session's own end/stop path resolves.
89
+ try {
90
+ await opts.transport.sendReceipt(opts.jobId, seq, amount, sig, receipt.inputHash, receipt.outputHash);
91
+ emit({ type: 'receipt-sent', seq, amount, receiptHash: digestForHex(receipt), sig });
92
+ }
93
+ catch (receiptErr) {
94
+ emit({ type: 'session-error', message: `receipt ${seq}: ${receiptErr instanceof Error ? receiptErr.message : String(receiptErr)}` });
95
+ }
96
+ }
97
+ }
98
+ finally {
99
+ paying = false;
100
+ }
101
+ };
102
+ const ticker = setInterval(() => { void pay(); }, unitMs);
103
+ try {
104
+ await new Promise((resolve, reject) => {
105
+ opts.transport.onEnd(() => resolve());
106
+ opts.transport.onError(reject);
107
+ });
108
+ }
109
+ catch (err) {
110
+ const message = err instanceof Error ? err.message : String(err);
111
+ const code = err instanceof Error ? err.code : undefined;
112
+ failure = { message, ...(code != null ? { code } : {}) };
113
+ emit({ type: 'session-error', message });
114
+ }
115
+ finally {
116
+ clearInterval(ticker);
117
+ // An in-flight tick must finish before the final one, or the through-end
118
+ // receipt (the only one that commits the output) would be dropped.
119
+ while (paying) {
120
+ await new Promise(resolve => setTimeout(resolve, 10));
121
+ }
122
+ await pay(true); // final receipt: settle through the last started minute + commit output
123
+ emit({ type: 'session-end', jobId: opts.jobId, elapsedMs: now() - startMs, billed: lastAmount });
124
+ }
125
+ return { jobId: opts.jobId, elapsedMs: now() - startMs, billed: lastAmount, tokens, ...(failure != null ? { failure } : {}) };
126
+ }
127
+ finally {
128
+ await opts.transport.close();
129
+ opts.modeManager.endRemoteSession();
130
+ }
131
+ }
132
+ //# sourceMappingURL=userSession.js.map
@@ -0,0 +1,87 @@
1
+ import type { ChatMessage, LocalChatSource, LocalCompletionSource } from './llmBridge.ts';
2
+ import type { ModeManager } from './modeManager.ts';
3
+ import type { ReceiptLedger } from './receipts.ts';
4
+ export type OpenEnvelopeV0 = {
5
+ proto: 'open/0';
6
+ jobId: string;
7
+ /** uint256 escrow id as a decimal string; "0" = unpriced self-test session */
8
+ escrowId: string;
9
+ model: string;
10
+ prompt: string;
11
+ system?: string;
12
+ maxTokens?: number;
13
+ /** libp2p PeerId of the dialing client; identity→wallet binding lands P4.4 */
14
+ clientPeer: string;
15
+ /**
16
+ * Time-based billing (P4.3): the CLIENT's view of the worker's quoted price.
17
+ * The worker records these receipts; at settle (P4.4) its own catalog price +
18
+ * measured wall time are what the escrow actually pays out against.
19
+ */
20
+ billing?: {
21
+ mode: 'minutes';
22
+ unitMs: number;
23
+ pricePerHour: string;
24
+ currency: 'USDC';
25
+ };
26
+ };
27
+ /**
28
+ * Chat revision: carries whole messages and tool schemas, which `/api/generate`
29
+ * cannot express. A worker that receives this serves through Ollama's
30
+ * `/api/chat` so an agent can actually call tools.
31
+ */
32
+ export type OpenEnvelopeV1 = Omit<OpenEnvelopeV0, 'proto'> & {
33
+ proto: 'open/1';
34
+ messages: ChatMessage[];
35
+ tools?: unknown[];
36
+ };
37
+ export type OpenEnvelope = OpenEnvelopeV0 | OpenEnvelopeV1;
38
+ export type SessionEvent = {
39
+ type: 'session-open';
40
+ sessionId: string;
41
+ escrowId: string;
42
+ model: string;
43
+ } | {
44
+ type: 'token-count';
45
+ tokens: number;
46
+ } | {
47
+ type: 'receipt-accepted';
48
+ seq: number;
49
+ amount: bigint;
50
+ } | {
51
+ type: 'session-end';
52
+ sessionId: string;
53
+ tokens: number;
54
+ } | {
55
+ type: 'session-error';
56
+ message: string;
57
+ };
58
+ export type WorkerSessionDeps = {
59
+ modeManager: ModeManager;
60
+ source: LocalCompletionSource;
61
+ /** Required to serve `open/1`; absent means chat requests are refused. */
62
+ chatSource?: LocalChatSource;
63
+ /** Relay one tool call produced by the model. */
64
+ onToolCall?: (call: {
65
+ index: number;
66
+ id: string;
67
+ name: string;
68
+ argumentsDelta: string;
69
+ }) => void;
70
+ ledger: ReceiptLedger;
71
+ emit: (e: SessionEvent) => void;
72
+ onDelta: (delta: string) => void;
73
+ /** live receipts the client has offered so far (seq → cumulative amount) */
74
+ latestReceipt: () => {
75
+ seq: number;
76
+ amount: bigint;
77
+ } | null;
78
+ abortSignal?: AbortSignal;
79
+ };
80
+ export type WorkerSession = {
81
+ sessionId: string;
82
+ tokens: () => number;
83
+ abort(): void;
84
+ run(): Promise<void>;
85
+ };
86
+ export declare function createWorkerSession(envelope: OpenEnvelope, deps: WorkerSessionDeps): WorkerSession;
87
+ //# sourceMappingURL=workerSession.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workerSession.d.ts","sourceRoot":"","sources":["../src/workerSession.ts"],"names":[],"mappings":"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"}