@holokai/neuron-sdk 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -17,7 +17,21 @@ var CapabilityScopeSchema = z.union([
17
17
  var CapabilitySchema = z.object({
18
18
  type: CapabilityTypeSchema,
19
19
  scope: CapabilityScopeSchema,
20
- concurrency: z.number().int().positive()
20
+ concurrency: z.number().int().positive(),
21
+ /**
22
+ * Per-capability lease TTL override in milliseconds (HOL-356).
23
+ * The gateway uses this when minting a lease's `expiresAt`; absent
24
+ * → falls back to the gateway's default (`leaseTtlMs` on the
25
+ * dispatcher). Lets a slow capability (e.g. a scripted run with a
26
+ * 10-minute deadline) declare a longer TTL without tuning the
27
+ * whole gateway.
28
+ *
29
+ * Bounded by reasonable extremes: at least 1s (anything shorter
30
+ * than a heartbeat is meaningless) and at most 1h (the reaper +
31
+ * heartbeat budget should be the limiter, not a single
32
+ * capability).
33
+ */
34
+ leaseTtlMs: z.number().int().min(1e3).max(60 * 60 * 1e3).optional()
21
35
  }).strict();
22
36
  var JsonSchemaSchema = z.unknown();
23
37
  var LeaseIdSchema = z.string().min(1);
@@ -182,6 +196,7 @@ function defineCapability(init) {
182
196
  outputSchema: zodToJsonSchema(init.output, { $refStrategy: "none" }),
183
197
  scope: init.scope,
184
198
  concurrency: init.concurrency,
199
+ leaseTtlMs: init.leaseTtlMs,
185
200
  _zod: { input: init.input, output: init.output }
186
201
  };
187
202
  }
@@ -206,7 +221,13 @@ function toRegistrationCapability(def, opts) {
206
221
  `capability ${def.type}: concurrency is required for registration \u2014 set it on defineCapability(...) or at neuron.handle(def, { concurrency }, fn)`
207
222
  );
208
223
  }
209
- return { type: def.type, scope, concurrency };
224
+ const leaseTtlMs = opts?.leaseTtlMs ?? def.leaseTtlMs;
225
+ return {
226
+ type: def.type,
227
+ scope,
228
+ concurrency,
229
+ ...typeof leaseTtlMs === "number" ? { leaseTtlMs } : {}
230
+ };
210
231
  }
211
232
 
212
233
  // src/heartbeat.ts
@@ -252,155 +273,6 @@ var HeartbeatLoop = class {
252
273
  }
253
274
  };
254
275
 
255
- // src/lease-runner.ts
256
- var TerminalFailure = class extends Error {
257
- constructor(reason, code) {
258
- super(reason);
259
- this.reason = reason;
260
- this.code = code;
261
- this.name = "TerminalFailure";
262
- }
263
- };
264
- function validateInput(zod, validator, jsonSchema, data) {
265
- if (zod) {
266
- const result = zod.safeParse(data);
267
- return result.success ? { ok: true, value: result.data } : { ok: false, errors: result.error.issues };
268
- }
269
- const errors = validator.validate(
270
- jsonSchema,
271
- data
272
- );
273
- return errors === null ? { ok: true, value: data } : { ok: false, errors };
274
- }
275
- async function runLease(opts) {
276
- const { lease, handler, zodInput, zodOutput, transport, validator, logger, signal } = opts;
277
- const inputCheck = validateInput(zodInput, validator, lease.inputSchema, lease.task.input);
278
- if (!inputCheck.ok) {
279
- await sendNack(
280
- transport,
281
- lease.leaseId,
282
- "schema-mismatch",
283
- "lease input does not match inputSchema",
284
- { code: "INPUT_SCHEMA_MISMATCH", details: { errors: inputCheck.errors } },
285
- logger
286
- );
287
- return;
288
- }
289
- const parsedInput = inputCheck.value;
290
- const progressFn = makeProgressFn(transport, lease.leaseId, logger);
291
- const ctx = {
292
- task: lease.task,
293
- leaseId: lease.leaseId,
294
- signal,
295
- log: scopedLogger(logger, lease),
296
- progress: progressFn,
297
- fail: (reason, opts2) => {
298
- throw new TerminalFailure(reason, opts2?.code);
299
- }
300
- };
301
- let output;
302
- try {
303
- output = await handler(parsedInput, ctx);
304
- } catch (err) {
305
- if (signal.aborted || err.name === "AbortError") {
306
- await sendNack(
307
- transport,
308
- lease.leaseId,
309
- "cancelled",
310
- "handler aborted",
311
- { code: "CANCELLED" },
312
- logger
313
- );
314
- return;
315
- }
316
- if (err instanceof TerminalFailure) {
317
- await sendNack(
318
- transport,
319
- lease.leaseId,
320
- "terminal",
321
- err.reason,
322
- { code: err.code ?? "HANDLER_TERMINAL" },
323
- logger
324
- );
325
- return;
326
- }
327
- const message = err instanceof Error ? err.message : String(err);
328
- logger.error({ err, leaseId: lease.leaseId }, "handler threw; nacking retryable");
329
- await sendNack(
330
- transport,
331
- lease.leaseId,
332
- "retryable",
333
- message,
334
- { code: "HANDLER_THREW" },
335
- logger
336
- );
337
- return;
338
- }
339
- const outputCheck = validateInput(zodOutput, validator, lease.outputSchema, output);
340
- if (!outputCheck.ok) {
341
- await sendNack(
342
- transport,
343
- lease.leaseId,
344
- "schema-mismatch",
345
- "handler output does not match outputSchema",
346
- { code: "OUTPUT_SCHEMA_MISMATCH", details: { errors: outputCheck.errors } },
347
- logger
348
- );
349
- return;
350
- }
351
- const parsedOutput = outputCheck.value;
352
- await transport.post(POST_PATH.ack, {
353
- type: "ack",
354
- leaseId: lease.leaseId,
355
- output: parsedOutput,
356
- completedAt: (/* @__PURE__ */ new Date()).toISOString()
357
- }).catch((err) => {
358
- logger.warn(
359
- { err, leaseId: lease.leaseId },
360
- "ack POST rejected by gateway; lease will be reclaimed by reaper"
361
- );
362
- });
363
- }
364
- async function sendNack(transport, leaseId, kind, reason, extras = {}, logger) {
365
- await transport.post(POST_PATH.nack, {
366
- type: "nack",
367
- leaseId,
368
- kind,
369
- reason,
370
- code: extras.code,
371
- details: extras.details,
372
- failedAt: (/* @__PURE__ */ new Date()).toISOString()
373
- }).catch((err) => {
374
- logger?.warn(
375
- { err, leaseId, kind },
376
- "nack POST rejected by gateway; lease will be reclaimed by reaper"
377
- );
378
- });
379
- }
380
- function makeProgressFn(transport, leaseId, logger) {
381
- return (update) => {
382
- void transport.post(POST_PATH.progress, {
383
- type: "progress",
384
- leaseId,
385
- percent: update.percent,
386
- message: update.message,
387
- data: update.data,
388
- sentAt: (/* @__PURE__ */ new Date()).toISOString()
389
- }).catch((err) => {
390
- logger.warn({ err, leaseId }, "progress POST failed; continuing");
391
- });
392
- };
393
- }
394
- function scopedLogger(logger, lease) {
395
- const scope = { leaseId: lease.leaseId, taskId: lease.task.taskId };
396
- return {
397
- debug: (meta, msg) => logger.debug({ ...scope, ...meta }, msg),
398
- info: (meta, msg) => logger.info({ ...scope, ...meta }, msg),
399
- warn: (meta, msg) => logger.warn({ ...scope, ...meta }, msg),
400
- error: (meta, msg) => logger.error({ ...scope, ...meta }, msg)
401
- };
402
- }
403
-
404
276
  // src/logger.ts
405
277
  var noopLogger = {
406
278
  debug() {
@@ -412,43 +284,6 @@ var noopLogger = {
412
284
  error() {
413
285
  }
414
286
  };
415
- var Ajv = AjvDefault.default ?? AjvDefault;
416
- var addFormats = addFormatsDefault.default ?? addFormatsDefault;
417
- var SchemaValidator = class {
418
- ajv;
419
- cache = /* @__PURE__ */ new WeakMap();
420
- constructor() {
421
- this.ajv = new Ajv({ allErrors: true, strict: false });
422
- addFormats(this.ajv);
423
- }
424
- /**
425
- * Returns null if the value matches the schema (or no schema given);
426
- * otherwise an array of structured errors.
427
- */
428
- validate(schema, data) {
429
- if (!schema || typeof schema !== "object") return null;
430
- let validator = this.cache.get(schema);
431
- if (!validator) {
432
- try {
433
- validator = this.ajv.compile(schema);
434
- } catch (err) {
435
- return [
436
- {
437
- instancePath: "",
438
- schemaPath: "",
439
- keyword: "schema",
440
- params: {},
441
- message: `schema failed to compile: ${err instanceof Error ? err.message : String(err)}`
442
- }
443
- ];
444
- }
445
- this.cache.set(schema, validator);
446
- }
447
- const v = validator;
448
- const ok = v(data);
449
- return ok ? null : v.errors ?? [];
450
- }
451
- };
452
287
 
453
288
  // src/sse-parser.ts
454
289
  async function* parseSSEStream(stream, signal) {
@@ -498,10 +333,10 @@ function parseBlock(block) {
498
333
  return { event, data: dataLines.join("\n") };
499
334
  }
500
335
 
501
- // src/transport.ts
502
- var Transport = class {
336
+ // src/http-transport.ts
337
+ var HttpTransport = class {
503
338
  opts;
504
- callbacks;
339
+ callbacks = {};
505
340
  abortController = null;
506
341
  stopped = false;
507
342
  connected = false;
@@ -517,13 +352,9 @@ var Transport = class {
517
352
  logger: opts.logger ?? noopLogger,
518
353
  fetch: opts.fetch ?? globalThis.fetch.bind(globalThis)
519
354
  };
520
- this.callbacks = {
521
- onOpen: opts.onOpen,
522
- onClose: opts.onClose,
523
- onFrame: opts.onFrame,
524
- onError: opts.onError,
525
- onReconnecting: opts.onReconnecting
526
- };
355
+ }
356
+ subscribe(callbacks) {
357
+ this.callbacks = callbacks;
527
358
  }
528
359
  isConnected() {
529
360
  return this.connected;
@@ -678,6 +509,192 @@ var TransportError = class extends Error {
678
509
  }
679
510
  };
680
511
 
512
+ // src/lease-runner.ts
513
+ var TerminalFailure = class extends Error {
514
+ constructor(reason, code) {
515
+ super(reason);
516
+ this.reason = reason;
517
+ this.code = code;
518
+ this.name = "TerminalFailure";
519
+ }
520
+ };
521
+ function validateInput(zod, validator, jsonSchema, data) {
522
+ if (zod) {
523
+ const result = zod.safeParse(data);
524
+ return result.success ? { ok: true, value: result.data } : { ok: false, errors: result.error.issues };
525
+ }
526
+ const errors = validator.validate(
527
+ jsonSchema,
528
+ data
529
+ );
530
+ return errors === null ? { ok: true, value: data } : { ok: false, errors };
531
+ }
532
+ async function runLease(opts) {
533
+ const { lease, handler, zodInput, zodOutput, transport, validator, logger, signal } = opts;
534
+ const inputCheck = validateInput(zodInput, validator, lease.inputSchema, lease.task.input);
535
+ if (!inputCheck.ok) {
536
+ await sendNack(
537
+ transport,
538
+ lease.leaseId,
539
+ "schema-mismatch",
540
+ "lease input does not match inputSchema",
541
+ { code: "INPUT_SCHEMA_MISMATCH", details: { errors: inputCheck.errors } },
542
+ logger
543
+ );
544
+ return;
545
+ }
546
+ const parsedInput = inputCheck.value;
547
+ const progressFn = makeProgressFn(transport, lease.leaseId, logger);
548
+ const ctx = {
549
+ task: lease.task,
550
+ leaseId: lease.leaseId,
551
+ signal,
552
+ log: scopedLogger(logger, lease),
553
+ progress: progressFn,
554
+ fail: (reason, opts2) => {
555
+ throw new TerminalFailure(reason, opts2?.code);
556
+ }
557
+ };
558
+ let output;
559
+ try {
560
+ output = await handler(parsedInput, ctx);
561
+ } catch (err) {
562
+ if (signal.aborted || err.name === "AbortError") {
563
+ await sendNack(
564
+ transport,
565
+ lease.leaseId,
566
+ "cancelled",
567
+ "handler aborted",
568
+ { code: "CANCELLED" },
569
+ logger
570
+ );
571
+ return;
572
+ }
573
+ if (err instanceof TerminalFailure) {
574
+ await sendNack(
575
+ transport,
576
+ lease.leaseId,
577
+ "terminal",
578
+ err.reason,
579
+ { code: err.code ?? "HANDLER_TERMINAL" },
580
+ logger
581
+ );
582
+ return;
583
+ }
584
+ const message = err instanceof Error ? err.message : String(err);
585
+ logger.error({ err, leaseId: lease.leaseId }, "handler threw; nacking retryable");
586
+ await sendNack(
587
+ transport,
588
+ lease.leaseId,
589
+ "retryable",
590
+ message,
591
+ { code: "HANDLER_THREW" },
592
+ logger
593
+ );
594
+ return;
595
+ }
596
+ const outputCheck = validateInput(zodOutput, validator, lease.outputSchema, output);
597
+ if (!outputCheck.ok) {
598
+ await sendNack(
599
+ transport,
600
+ lease.leaseId,
601
+ "schema-mismatch",
602
+ "handler output does not match outputSchema",
603
+ { code: "OUTPUT_SCHEMA_MISMATCH", details: { errors: outputCheck.errors } },
604
+ logger
605
+ );
606
+ return;
607
+ }
608
+ const parsedOutput = outputCheck.value;
609
+ await transport.post(POST_PATH.ack, {
610
+ type: "ack",
611
+ leaseId: lease.leaseId,
612
+ output: parsedOutput,
613
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
614
+ }).catch((err) => {
615
+ logger.warn(
616
+ { err, leaseId: lease.leaseId },
617
+ "ack POST rejected by gateway; lease will be reclaimed by reaper"
618
+ );
619
+ });
620
+ }
621
+ async function sendNack(transport, leaseId, kind, reason, extras = {}, logger) {
622
+ await transport.post(POST_PATH.nack, {
623
+ type: "nack",
624
+ leaseId,
625
+ kind,
626
+ reason,
627
+ code: extras.code,
628
+ details: extras.details,
629
+ failedAt: (/* @__PURE__ */ new Date()).toISOString()
630
+ }).catch((err) => {
631
+ logger?.warn(
632
+ { err, leaseId, kind },
633
+ "nack POST rejected by gateway; lease will be reclaimed by reaper"
634
+ );
635
+ });
636
+ }
637
+ function makeProgressFn(transport, leaseId, logger) {
638
+ return (update) => {
639
+ void transport.post(POST_PATH.progress, {
640
+ type: "progress",
641
+ leaseId,
642
+ percent: update.percent,
643
+ message: update.message,
644
+ data: update.data,
645
+ sentAt: (/* @__PURE__ */ new Date()).toISOString()
646
+ }).catch((err) => {
647
+ logger.warn({ err, leaseId }, "progress POST failed; continuing");
648
+ });
649
+ };
650
+ }
651
+ function scopedLogger(logger, lease) {
652
+ const scope = { leaseId: lease.leaseId, taskId: lease.task.taskId };
653
+ return {
654
+ debug: (meta, msg) => logger.debug({ ...scope, ...meta }, msg),
655
+ info: (meta, msg) => logger.info({ ...scope, ...meta }, msg),
656
+ warn: (meta, msg) => logger.warn({ ...scope, ...meta }, msg),
657
+ error: (meta, msg) => logger.error({ ...scope, ...meta }, msg)
658
+ };
659
+ }
660
+ var Ajv = AjvDefault.default ?? AjvDefault;
661
+ var addFormats = addFormatsDefault.default ?? addFormatsDefault;
662
+ var SchemaValidator = class {
663
+ ajv;
664
+ cache = /* @__PURE__ */ new WeakMap();
665
+ constructor() {
666
+ this.ajv = new Ajv({ allErrors: true, strict: false });
667
+ addFormats(this.ajv);
668
+ }
669
+ /**
670
+ * Returns null if the value matches the schema (or no schema given);
671
+ * otherwise an array of structured errors.
672
+ */
673
+ validate(schema, data) {
674
+ if (!schema || typeof schema !== "object") return null;
675
+ let validator = this.cache.get(schema);
676
+ if (!validator) {
677
+ try {
678
+ validator = this.ajv.compile(schema);
679
+ } catch (err) {
680
+ return [
681
+ {
682
+ instancePath: "",
683
+ schemaPath: "",
684
+ keyword: "schema",
685
+ params: {},
686
+ message: `schema failed to compile: ${err instanceof Error ? err.message : String(err)}`
687
+ }
688
+ ];
689
+ }
690
+ this.cache.set(schema, validator);
691
+ }
692
+ const v = validator;
693
+ const ok = v(data);
694
+ return ok ? null : v.errors ?? [];
695
+ }
696
+ };
697
+
681
698
  // src/worker-pool.ts
682
699
  var WorkerPool = class {
683
700
  constructor(maxConcurrency) {
@@ -777,6 +794,9 @@ var ShutdownError = class extends Error {
777
794
  };
778
795
 
779
796
  // src/neuron.ts
797
+ function isWithTransport(opts) {
798
+ return "transport" in opts && opts.transport !== void 0;
799
+ }
780
800
  var Neuron = class extends EventEmitter {
781
801
  transport;
782
802
  handlers = /* @__PURE__ */ new Map();
@@ -790,27 +810,25 @@ var Neuron = class extends EventEmitter {
790
810
  heartbeat;
791
811
  validator;
792
812
  logger;
793
- opts;
813
+ neuronId;
794
814
  started = false;
795
815
  stopping = false;
796
816
  hasOpenedOnce = false;
797
817
  constructor(opts) {
798
818
  super();
799
- this.opts = opts;
800
819
  this.logger = opts.logger ?? noopLogger;
820
+ this.neuronId = opts.neuronId;
801
821
  this.validator = new SchemaValidator();
802
- this.transport = new Transport({
822
+ this.transport = isWithTransport(opts) ? opts.transport : new HttpTransport({
803
823
  gatewayUrl: opts.gatewayUrl,
804
824
  neuronId: opts.neuronId,
805
825
  auth: opts.auth,
806
826
  logger: this.logger,
807
827
  initialReconnectDelayMs: opts.initialReconnectDelayMs,
808
828
  maxReconnectDelayMs: opts.maxReconnectDelayMs,
809
- fetch: opts.fetch,
810
- // Internal callbacks — Transport has a single subscriber (this
811
- // Neuron) so a callbacks-on-options surface is simpler than an
812
- // event emitter. The Neuron is the one that re-publishes lifecycle
813
- // signals to its own consumers via the public emitter.
829
+ fetch: opts.fetch
830
+ });
831
+ this.transport.subscribe({
814
832
  onOpen: () => {
815
833
  this.emit("connection:open");
816
834
  void this.handleConnectionOpen();
@@ -904,7 +922,7 @@ var Neuron = class extends EventEmitter {
904
922
  if (this.started && this.transport.isConnected()) {
905
923
  await this.transport.post(POST_PATH.updateCapability, {
906
924
  type: "update-capability",
907
- neuronId: this.opts.neuronId,
925
+ neuronId: this.neuronId,
908
926
  capability: entry.capability
909
927
  }).catch((err) => {
910
928
  this.logger.warn(
@@ -993,7 +1011,7 @@ var Neuron = class extends EventEmitter {
993
1011
  async postRegister() {
994
1012
  await this.transport.post(POST_PATH.register, {
995
1013
  type: "register",
996
- neuronId: this.opts.neuronId,
1014
+ neuronId: this.neuronId,
997
1015
  protocolVersion: PROTOCOL_VERSION,
998
1016
  capabilities: Array.from(this.handlers.values()).map((h) => h.capability)
999
1017
  });
@@ -1103,6 +1121,6 @@ var Neuron = class extends EventEmitter {
1103
1121
  };
1104
1122
  var SSE_EVENT_NAMES = SSE_EVENT;
1105
1123
 
1106
- export { Neuron, SSE_EVENT_NAMES, SchemaValidator, ShutdownError, Transport, TransportError, WorkerPool, defineCapability, toPlanShape, toRegistrationCapability };
1124
+ export { HttpTransport, Neuron, SSE_EVENT_NAMES, SchemaValidator, ShutdownError, TransportError, WorkerPool, defineCapability, toPlanShape, toRegistrationCapability };
1107
1125
  //# sourceMappingURL=index.mjs.map
1108
1126
  //# sourceMappingURL=index.mjs.map