@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.js CHANGED
@@ -24,7 +24,21 @@ var CapabilityScopeSchema = zod.z.union([
24
24
  var CapabilitySchema = zod.z.object({
25
25
  type: CapabilityTypeSchema,
26
26
  scope: CapabilityScopeSchema,
27
- concurrency: zod.z.number().int().positive()
27
+ concurrency: zod.z.number().int().positive(),
28
+ /**
29
+ * Per-capability lease TTL override in milliseconds (HOL-356).
30
+ * The gateway uses this when minting a lease's `expiresAt`; absent
31
+ * → falls back to the gateway's default (`leaseTtlMs` on the
32
+ * dispatcher). Lets a slow capability (e.g. a scripted run with a
33
+ * 10-minute deadline) declare a longer TTL without tuning the
34
+ * whole gateway.
35
+ *
36
+ * Bounded by reasonable extremes: at least 1s (anything shorter
37
+ * than a heartbeat is meaningless) and at most 1h (the reaper +
38
+ * heartbeat budget should be the limiter, not a single
39
+ * capability).
40
+ */
41
+ leaseTtlMs: zod.z.number().int().min(1e3).max(60 * 60 * 1e3).optional()
28
42
  }).strict();
29
43
  var JsonSchemaSchema = zod.z.unknown();
30
44
  var LeaseIdSchema = zod.z.string().min(1);
@@ -189,6 +203,7 @@ function defineCapability(init) {
189
203
  outputSchema: zodToJsonSchema.zodToJsonSchema(init.output, { $refStrategy: "none" }),
190
204
  scope: init.scope,
191
205
  concurrency: init.concurrency,
206
+ leaseTtlMs: init.leaseTtlMs,
192
207
  _zod: { input: init.input, output: init.output }
193
208
  };
194
209
  }
@@ -213,7 +228,13 @@ function toRegistrationCapability(def, opts) {
213
228
  `capability ${def.type}: concurrency is required for registration \u2014 set it on defineCapability(...) or at neuron.handle(def, { concurrency }, fn)`
214
229
  );
215
230
  }
216
- return { type: def.type, scope, concurrency };
231
+ const leaseTtlMs = opts?.leaseTtlMs ?? def.leaseTtlMs;
232
+ return {
233
+ type: def.type,
234
+ scope,
235
+ concurrency,
236
+ ...typeof leaseTtlMs === "number" ? { leaseTtlMs } : {}
237
+ };
217
238
  }
218
239
 
219
240
  // src/heartbeat.ts
@@ -259,155 +280,6 @@ var HeartbeatLoop = class {
259
280
  }
260
281
  };
261
282
 
262
- // src/lease-runner.ts
263
- var TerminalFailure = class extends Error {
264
- constructor(reason, code) {
265
- super(reason);
266
- this.reason = reason;
267
- this.code = code;
268
- this.name = "TerminalFailure";
269
- }
270
- };
271
- function validateInput(zod, validator, jsonSchema, data) {
272
- if (zod) {
273
- const result = zod.safeParse(data);
274
- return result.success ? { ok: true, value: result.data } : { ok: false, errors: result.error.issues };
275
- }
276
- const errors = validator.validate(
277
- jsonSchema,
278
- data
279
- );
280
- return errors === null ? { ok: true, value: data } : { ok: false, errors };
281
- }
282
- async function runLease(opts) {
283
- const { lease, handler, zodInput, zodOutput, transport, validator, logger, signal } = opts;
284
- const inputCheck = validateInput(zodInput, validator, lease.inputSchema, lease.task.input);
285
- if (!inputCheck.ok) {
286
- await sendNack(
287
- transport,
288
- lease.leaseId,
289
- "schema-mismatch",
290
- "lease input does not match inputSchema",
291
- { code: "INPUT_SCHEMA_MISMATCH", details: { errors: inputCheck.errors } },
292
- logger
293
- );
294
- return;
295
- }
296
- const parsedInput = inputCheck.value;
297
- const progressFn = makeProgressFn(transport, lease.leaseId, logger);
298
- const ctx = {
299
- task: lease.task,
300
- leaseId: lease.leaseId,
301
- signal,
302
- log: scopedLogger(logger, lease),
303
- progress: progressFn,
304
- fail: (reason, opts2) => {
305
- throw new TerminalFailure(reason, opts2?.code);
306
- }
307
- };
308
- let output;
309
- try {
310
- output = await handler(parsedInput, ctx);
311
- } catch (err) {
312
- if (signal.aborted || err.name === "AbortError") {
313
- await sendNack(
314
- transport,
315
- lease.leaseId,
316
- "cancelled",
317
- "handler aborted",
318
- { code: "CANCELLED" },
319
- logger
320
- );
321
- return;
322
- }
323
- if (err instanceof TerminalFailure) {
324
- await sendNack(
325
- transport,
326
- lease.leaseId,
327
- "terminal",
328
- err.reason,
329
- { code: err.code ?? "HANDLER_TERMINAL" },
330
- logger
331
- );
332
- return;
333
- }
334
- const message = err instanceof Error ? err.message : String(err);
335
- logger.error({ err, leaseId: lease.leaseId }, "handler threw; nacking retryable");
336
- await sendNack(
337
- transport,
338
- lease.leaseId,
339
- "retryable",
340
- message,
341
- { code: "HANDLER_THREW" },
342
- logger
343
- );
344
- return;
345
- }
346
- const outputCheck = validateInput(zodOutput, validator, lease.outputSchema, output);
347
- if (!outputCheck.ok) {
348
- await sendNack(
349
- transport,
350
- lease.leaseId,
351
- "schema-mismatch",
352
- "handler output does not match outputSchema",
353
- { code: "OUTPUT_SCHEMA_MISMATCH", details: { errors: outputCheck.errors } },
354
- logger
355
- );
356
- return;
357
- }
358
- const parsedOutput = outputCheck.value;
359
- await transport.post(POST_PATH.ack, {
360
- type: "ack",
361
- leaseId: lease.leaseId,
362
- output: parsedOutput,
363
- completedAt: (/* @__PURE__ */ new Date()).toISOString()
364
- }).catch((err) => {
365
- logger.warn(
366
- { err, leaseId: lease.leaseId },
367
- "ack POST rejected by gateway; lease will be reclaimed by reaper"
368
- );
369
- });
370
- }
371
- async function sendNack(transport, leaseId, kind, reason, extras = {}, logger) {
372
- await transport.post(POST_PATH.nack, {
373
- type: "nack",
374
- leaseId,
375
- kind,
376
- reason,
377
- code: extras.code,
378
- details: extras.details,
379
- failedAt: (/* @__PURE__ */ new Date()).toISOString()
380
- }).catch((err) => {
381
- logger?.warn(
382
- { err, leaseId, kind },
383
- "nack POST rejected by gateway; lease will be reclaimed by reaper"
384
- );
385
- });
386
- }
387
- function makeProgressFn(transport, leaseId, logger) {
388
- return (update) => {
389
- void transport.post(POST_PATH.progress, {
390
- type: "progress",
391
- leaseId,
392
- percent: update.percent,
393
- message: update.message,
394
- data: update.data,
395
- sentAt: (/* @__PURE__ */ new Date()).toISOString()
396
- }).catch((err) => {
397
- logger.warn({ err, leaseId }, "progress POST failed; continuing");
398
- });
399
- };
400
- }
401
- function scopedLogger(logger, lease) {
402
- const scope = { leaseId: lease.leaseId, taskId: lease.task.taskId };
403
- return {
404
- debug: (meta, msg) => logger.debug({ ...scope, ...meta }, msg),
405
- info: (meta, msg) => logger.info({ ...scope, ...meta }, msg),
406
- warn: (meta, msg) => logger.warn({ ...scope, ...meta }, msg),
407
- error: (meta, msg) => logger.error({ ...scope, ...meta }, msg)
408
- };
409
- }
410
-
411
283
  // src/logger.ts
412
284
  var noopLogger = {
413
285
  debug() {
@@ -419,43 +291,6 @@ var noopLogger = {
419
291
  error() {
420
292
  }
421
293
  };
422
- var Ajv = AjvDefault__default.default.default ?? AjvDefault__default.default;
423
- var addFormats = addFormatsDefault__default.default.default ?? addFormatsDefault__default.default;
424
- var SchemaValidator = class {
425
- ajv;
426
- cache = /* @__PURE__ */ new WeakMap();
427
- constructor() {
428
- this.ajv = new Ajv({ allErrors: true, strict: false });
429
- addFormats(this.ajv);
430
- }
431
- /**
432
- * Returns null if the value matches the schema (or no schema given);
433
- * otherwise an array of structured errors.
434
- */
435
- validate(schema, data) {
436
- if (!schema || typeof schema !== "object") return null;
437
- let validator = this.cache.get(schema);
438
- if (!validator) {
439
- try {
440
- validator = this.ajv.compile(schema);
441
- } catch (err) {
442
- return [
443
- {
444
- instancePath: "",
445
- schemaPath: "",
446
- keyword: "schema",
447
- params: {},
448
- message: `schema failed to compile: ${err instanceof Error ? err.message : String(err)}`
449
- }
450
- ];
451
- }
452
- this.cache.set(schema, validator);
453
- }
454
- const v = validator;
455
- const ok = v(data);
456
- return ok ? null : v.errors ?? [];
457
- }
458
- };
459
294
 
460
295
  // src/sse-parser.ts
461
296
  async function* parseSSEStream(stream, signal) {
@@ -505,10 +340,10 @@ function parseBlock(block) {
505
340
  return { event, data: dataLines.join("\n") };
506
341
  }
507
342
 
508
- // src/transport.ts
509
- var Transport = class {
343
+ // src/http-transport.ts
344
+ var HttpTransport = class {
510
345
  opts;
511
- callbacks;
346
+ callbacks = {};
512
347
  abortController = null;
513
348
  stopped = false;
514
349
  connected = false;
@@ -524,13 +359,9 @@ var Transport = class {
524
359
  logger: opts.logger ?? noopLogger,
525
360
  fetch: opts.fetch ?? globalThis.fetch.bind(globalThis)
526
361
  };
527
- this.callbacks = {
528
- onOpen: opts.onOpen,
529
- onClose: opts.onClose,
530
- onFrame: opts.onFrame,
531
- onError: opts.onError,
532
- onReconnecting: opts.onReconnecting
533
- };
362
+ }
363
+ subscribe(callbacks) {
364
+ this.callbacks = callbacks;
534
365
  }
535
366
  isConnected() {
536
367
  return this.connected;
@@ -685,6 +516,192 @@ var TransportError = class extends Error {
685
516
  }
686
517
  };
687
518
 
519
+ // src/lease-runner.ts
520
+ var TerminalFailure = class extends Error {
521
+ constructor(reason, code) {
522
+ super(reason);
523
+ this.reason = reason;
524
+ this.code = code;
525
+ this.name = "TerminalFailure";
526
+ }
527
+ };
528
+ function validateInput(zod, validator, jsonSchema, data) {
529
+ if (zod) {
530
+ const result = zod.safeParse(data);
531
+ return result.success ? { ok: true, value: result.data } : { ok: false, errors: result.error.issues };
532
+ }
533
+ const errors = validator.validate(
534
+ jsonSchema,
535
+ data
536
+ );
537
+ return errors === null ? { ok: true, value: data } : { ok: false, errors };
538
+ }
539
+ async function runLease(opts) {
540
+ const { lease, handler, zodInput, zodOutput, transport, validator, logger, signal } = opts;
541
+ const inputCheck = validateInput(zodInput, validator, lease.inputSchema, lease.task.input);
542
+ if (!inputCheck.ok) {
543
+ await sendNack(
544
+ transport,
545
+ lease.leaseId,
546
+ "schema-mismatch",
547
+ "lease input does not match inputSchema",
548
+ { code: "INPUT_SCHEMA_MISMATCH", details: { errors: inputCheck.errors } },
549
+ logger
550
+ );
551
+ return;
552
+ }
553
+ const parsedInput = inputCheck.value;
554
+ const progressFn = makeProgressFn(transport, lease.leaseId, logger);
555
+ const ctx = {
556
+ task: lease.task,
557
+ leaseId: lease.leaseId,
558
+ signal,
559
+ log: scopedLogger(logger, lease),
560
+ progress: progressFn,
561
+ fail: (reason, opts2) => {
562
+ throw new TerminalFailure(reason, opts2?.code);
563
+ }
564
+ };
565
+ let output;
566
+ try {
567
+ output = await handler(parsedInput, ctx);
568
+ } catch (err) {
569
+ if (signal.aborted || err.name === "AbortError") {
570
+ await sendNack(
571
+ transport,
572
+ lease.leaseId,
573
+ "cancelled",
574
+ "handler aborted",
575
+ { code: "CANCELLED" },
576
+ logger
577
+ );
578
+ return;
579
+ }
580
+ if (err instanceof TerminalFailure) {
581
+ await sendNack(
582
+ transport,
583
+ lease.leaseId,
584
+ "terminal",
585
+ err.reason,
586
+ { code: err.code ?? "HANDLER_TERMINAL" },
587
+ logger
588
+ );
589
+ return;
590
+ }
591
+ const message = err instanceof Error ? err.message : String(err);
592
+ logger.error({ err, leaseId: lease.leaseId }, "handler threw; nacking retryable");
593
+ await sendNack(
594
+ transport,
595
+ lease.leaseId,
596
+ "retryable",
597
+ message,
598
+ { code: "HANDLER_THREW" },
599
+ logger
600
+ );
601
+ return;
602
+ }
603
+ const outputCheck = validateInput(zodOutput, validator, lease.outputSchema, output);
604
+ if (!outputCheck.ok) {
605
+ await sendNack(
606
+ transport,
607
+ lease.leaseId,
608
+ "schema-mismatch",
609
+ "handler output does not match outputSchema",
610
+ { code: "OUTPUT_SCHEMA_MISMATCH", details: { errors: outputCheck.errors } },
611
+ logger
612
+ );
613
+ return;
614
+ }
615
+ const parsedOutput = outputCheck.value;
616
+ await transport.post(POST_PATH.ack, {
617
+ type: "ack",
618
+ leaseId: lease.leaseId,
619
+ output: parsedOutput,
620
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
621
+ }).catch((err) => {
622
+ logger.warn(
623
+ { err, leaseId: lease.leaseId },
624
+ "ack POST rejected by gateway; lease will be reclaimed by reaper"
625
+ );
626
+ });
627
+ }
628
+ async function sendNack(transport, leaseId, kind, reason, extras = {}, logger) {
629
+ await transport.post(POST_PATH.nack, {
630
+ type: "nack",
631
+ leaseId,
632
+ kind,
633
+ reason,
634
+ code: extras.code,
635
+ details: extras.details,
636
+ failedAt: (/* @__PURE__ */ new Date()).toISOString()
637
+ }).catch((err) => {
638
+ logger?.warn(
639
+ { err, leaseId, kind },
640
+ "nack POST rejected by gateway; lease will be reclaimed by reaper"
641
+ );
642
+ });
643
+ }
644
+ function makeProgressFn(transport, leaseId, logger) {
645
+ return (update) => {
646
+ void transport.post(POST_PATH.progress, {
647
+ type: "progress",
648
+ leaseId,
649
+ percent: update.percent,
650
+ message: update.message,
651
+ data: update.data,
652
+ sentAt: (/* @__PURE__ */ new Date()).toISOString()
653
+ }).catch((err) => {
654
+ logger.warn({ err, leaseId }, "progress POST failed; continuing");
655
+ });
656
+ };
657
+ }
658
+ function scopedLogger(logger, lease) {
659
+ const scope = { leaseId: lease.leaseId, taskId: lease.task.taskId };
660
+ return {
661
+ debug: (meta, msg) => logger.debug({ ...scope, ...meta }, msg),
662
+ info: (meta, msg) => logger.info({ ...scope, ...meta }, msg),
663
+ warn: (meta, msg) => logger.warn({ ...scope, ...meta }, msg),
664
+ error: (meta, msg) => logger.error({ ...scope, ...meta }, msg)
665
+ };
666
+ }
667
+ var Ajv = AjvDefault__default.default.default ?? AjvDefault__default.default;
668
+ var addFormats = addFormatsDefault__default.default.default ?? addFormatsDefault__default.default;
669
+ var SchemaValidator = class {
670
+ ajv;
671
+ cache = /* @__PURE__ */ new WeakMap();
672
+ constructor() {
673
+ this.ajv = new Ajv({ allErrors: true, strict: false });
674
+ addFormats(this.ajv);
675
+ }
676
+ /**
677
+ * Returns null if the value matches the schema (or no schema given);
678
+ * otherwise an array of structured errors.
679
+ */
680
+ validate(schema, data) {
681
+ if (!schema || typeof schema !== "object") return null;
682
+ let validator = this.cache.get(schema);
683
+ if (!validator) {
684
+ try {
685
+ validator = this.ajv.compile(schema);
686
+ } catch (err) {
687
+ return [
688
+ {
689
+ instancePath: "",
690
+ schemaPath: "",
691
+ keyword: "schema",
692
+ params: {},
693
+ message: `schema failed to compile: ${err instanceof Error ? err.message : String(err)}`
694
+ }
695
+ ];
696
+ }
697
+ this.cache.set(schema, validator);
698
+ }
699
+ const v = validator;
700
+ const ok = v(data);
701
+ return ok ? null : v.errors ?? [];
702
+ }
703
+ };
704
+
688
705
  // src/worker-pool.ts
689
706
  var WorkerPool = class {
690
707
  constructor(maxConcurrency) {
@@ -784,6 +801,9 @@ var ShutdownError = class extends Error {
784
801
  };
785
802
 
786
803
  // src/neuron.ts
804
+ function isWithTransport(opts) {
805
+ return "transport" in opts && opts.transport !== void 0;
806
+ }
787
807
  var Neuron = class extends eventemitter3.EventEmitter {
788
808
  transport;
789
809
  handlers = /* @__PURE__ */ new Map();
@@ -797,27 +817,25 @@ var Neuron = class extends eventemitter3.EventEmitter {
797
817
  heartbeat;
798
818
  validator;
799
819
  logger;
800
- opts;
820
+ neuronId;
801
821
  started = false;
802
822
  stopping = false;
803
823
  hasOpenedOnce = false;
804
824
  constructor(opts) {
805
825
  super();
806
- this.opts = opts;
807
826
  this.logger = opts.logger ?? noopLogger;
827
+ this.neuronId = opts.neuronId;
808
828
  this.validator = new SchemaValidator();
809
- this.transport = new Transport({
829
+ this.transport = isWithTransport(opts) ? opts.transport : new HttpTransport({
810
830
  gatewayUrl: opts.gatewayUrl,
811
831
  neuronId: opts.neuronId,
812
832
  auth: opts.auth,
813
833
  logger: this.logger,
814
834
  initialReconnectDelayMs: opts.initialReconnectDelayMs,
815
835
  maxReconnectDelayMs: opts.maxReconnectDelayMs,
816
- fetch: opts.fetch,
817
- // Internal callbacks — Transport has a single subscriber (this
818
- // Neuron) so a callbacks-on-options surface is simpler than an
819
- // event emitter. The Neuron is the one that re-publishes lifecycle
820
- // signals to its own consumers via the public emitter.
836
+ fetch: opts.fetch
837
+ });
838
+ this.transport.subscribe({
821
839
  onOpen: () => {
822
840
  this.emit("connection:open");
823
841
  void this.handleConnectionOpen();
@@ -911,7 +929,7 @@ var Neuron = class extends eventemitter3.EventEmitter {
911
929
  if (this.started && this.transport.isConnected()) {
912
930
  await this.transport.post(POST_PATH.updateCapability, {
913
931
  type: "update-capability",
914
- neuronId: this.opts.neuronId,
932
+ neuronId: this.neuronId,
915
933
  capability: entry.capability
916
934
  }).catch((err) => {
917
935
  this.logger.warn(
@@ -1000,7 +1018,7 @@ var Neuron = class extends eventemitter3.EventEmitter {
1000
1018
  async postRegister() {
1001
1019
  await this.transport.post(POST_PATH.register, {
1002
1020
  type: "register",
1003
- neuronId: this.opts.neuronId,
1021
+ neuronId: this.neuronId,
1004
1022
  protocolVersion: PROTOCOL_VERSION,
1005
1023
  capabilities: Array.from(this.handlers.values()).map((h) => h.capability)
1006
1024
  });
@@ -1110,11 +1128,11 @@ var Neuron = class extends eventemitter3.EventEmitter {
1110
1128
  };
1111
1129
  var SSE_EVENT_NAMES = SSE_EVENT;
1112
1130
 
1131
+ exports.HttpTransport = HttpTransport;
1113
1132
  exports.Neuron = Neuron;
1114
1133
  exports.SSE_EVENT_NAMES = SSE_EVENT_NAMES;
1115
1134
  exports.SchemaValidator = SchemaValidator;
1116
1135
  exports.ShutdownError = ShutdownError;
1117
- exports.Transport = Transport;
1118
1136
  exports.TransportError = TransportError;
1119
1137
  exports.WorkerPool = WorkerPool;
1120
1138
  exports.defineCapability = defineCapability;