@xtrape/capsule-agent-node 0.4.0 → 0.6.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/NOTICE +3 -3
- package/README.md +137 -63
- package/dist/index.cjs +315 -16
- package/dist/index.d.cts +167 -17
- package/dist/index.d.ts +167 -17
- package/dist/index.js +315 -15
- package/package.json +9 -8
package/dist/index.cjs
CHANGED
|
@@ -27,9 +27,12 @@ __export(index_exports, {
|
|
|
27
27
|
BusDisabledError: () => BusDisabledError,
|
|
28
28
|
BusRateLimitedError: () => BusRateLimitedError,
|
|
29
29
|
CapsuleAgent: () => CapsuleAgent,
|
|
30
|
+
CeServiceAgent: () => CeServiceAgent,
|
|
30
31
|
FileTokenStore: () => FileTokenStore,
|
|
31
32
|
NetworkError: () => NetworkError,
|
|
32
|
-
RegistrationError: () => RegistrationError
|
|
33
|
+
RegistrationError: () => RegistrationError,
|
|
34
|
+
XtrapeAgent: () => CapsuleAgent,
|
|
35
|
+
serveCeService: () => serveCeService
|
|
33
36
|
});
|
|
34
37
|
module.exports = __toCommonJS(index_exports);
|
|
35
38
|
|
|
@@ -136,13 +139,22 @@ var AgentApiClient = class {
|
|
|
136
139
|
reportServices(agentId, token, req) {
|
|
137
140
|
return this.request(`/api/agents/${agentId}/services/report`, { method: "POST", token, body: JSON.stringify(req) });
|
|
138
141
|
}
|
|
142
|
+
/** Report Workers through the canonical endpoint, with a legacy fallback. */
|
|
143
|
+
async reportWorkers(agentId, token, req) {
|
|
144
|
+
try {
|
|
145
|
+
return await this.request(`/api/agents/${agentId}/workers/report`, { method: "POST", token, body: JSON.stringify(req) });
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (!(error instanceof AgentApiError) || error.status !== 404) throw error;
|
|
148
|
+
return this.reportServices(agentId, token, { services: req.workers });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
139
151
|
pollCommands(agentId, token) {
|
|
140
152
|
return this.request(`/api/agents/${agentId}/commands`, { method: "GET", token });
|
|
141
153
|
}
|
|
142
154
|
reportResult(agentId, token, commandId, req) {
|
|
143
155
|
return this.request(`/api/agents/${agentId}/commands/${commandId}/result`, { method: "POST", token, body: JSON.stringify(req) });
|
|
144
156
|
}
|
|
145
|
-
/**
|
|
157
|
+
/** Legacy v0.4 event-routing compatibility hook. Subject to change before v1.0. */
|
|
146
158
|
publishBusEvent(agentId, token, req) {
|
|
147
159
|
return this.request(`/api/agents/${agentId}/bus/events`, { method: "POST", token, body: JSON.stringify({ ...req, experimental: req.experimental ?? "v0.4-experimental" }) });
|
|
148
160
|
}
|
|
@@ -215,6 +227,12 @@ function emitLog(consoleSink, structured, level, message, fields) {
|
|
|
215
227
|
var CapsuleAgent = class {
|
|
216
228
|
constructor(options) {
|
|
217
229
|
this.options = options;
|
|
230
|
+
if (options.worker && options.service) {
|
|
231
|
+
throw new Error("Agent options accept `worker` or legacy `service`, not both.");
|
|
232
|
+
}
|
|
233
|
+
if (!options.worker && !options.service) {
|
|
234
|
+
throw new Error("Agent options require `worker` (or legacy `service`).");
|
|
235
|
+
}
|
|
218
236
|
this.client = new AgentApiClient(options.backendUrl);
|
|
219
237
|
this.store = new FileTokenStore(options.tokenStore?.file);
|
|
220
238
|
this.logger = options.logger ?? console;
|
|
@@ -248,7 +266,7 @@ var CapsuleAgent = class {
|
|
|
248
266
|
this.stopped = false;
|
|
249
267
|
try {
|
|
250
268
|
await this.ensureRegistered();
|
|
251
|
-
await this.
|
|
269
|
+
await this.reportWorker();
|
|
252
270
|
await this.heartbeat();
|
|
253
271
|
await this.pollOnce();
|
|
254
272
|
} catch (e) {
|
|
@@ -257,7 +275,7 @@ var CapsuleAgent = class {
|
|
|
257
275
|
}
|
|
258
276
|
if (this.options.autoStartLoops !== false) {
|
|
259
277
|
this.loop("heartbeat", this.options.intervals?.heartbeatMs ?? (this.options.heartbeatIntervalSeconds ?? 30) * 1e3, () => this.heartbeat());
|
|
260
|
-
this.loop("
|
|
278
|
+
this.loop("worker-report", this.options.intervals?.workerReportMs ?? this.options.intervals?.serviceReportMs ?? 6e4, () => this.reportWorker());
|
|
261
279
|
this.loop("command-poll", this.options.intervals?.commandPollMs ?? (this.options.commandPollIntervalSeconds ?? 5) * 1e3, () => this.pollOnce());
|
|
262
280
|
}
|
|
263
281
|
}
|
|
@@ -286,38 +304,45 @@ var CapsuleAgent = class {
|
|
|
286
304
|
if (this.agentId) return;
|
|
287
305
|
}
|
|
288
306
|
if (!this.options.registrationToken) throw new Error("OPSTAGE registration token is required for first registration");
|
|
289
|
-
const
|
|
307
|
+
const worker = this.workerDefinition();
|
|
308
|
+
const agent = this.options.agent ?? { code: worker.code, name: worker.name, runtime: worker.runtime };
|
|
290
309
|
const res = await this.retry(() => this.client.register({ registrationToken: this.options.registrationToken, agent: { code: agent.code, name: agent.name, mode: "embedded", runtime: agent.runtime ?? "nodejs" }, service: this.serviceSnapshot() }));
|
|
291
310
|
this.agentId = res.agentId;
|
|
292
311
|
this.token = res.agentToken;
|
|
293
312
|
await this.store.save(`${this.agentId}:${this.token}`);
|
|
294
313
|
this.log("info", `registered agent ${this.agentId}`);
|
|
295
314
|
}
|
|
315
|
+
workerDefinition() {
|
|
316
|
+
if (this.options.worker) {
|
|
317
|
+
return this.options.worker;
|
|
318
|
+
}
|
|
319
|
+
return this.options.service;
|
|
320
|
+
}
|
|
296
321
|
serviceSnapshot() {
|
|
297
|
-
const
|
|
298
|
-
|
|
322
|
+
const worker = this.workerDefinition();
|
|
323
|
+
const manifest = { kind: "Worker", schemaVersion: "1.0", code: worker.code, name: worker.name, description: worker.description, version: worker.version, runtime: worker.runtime, agentMode: "embedded", ...worker.manifest ?? {} };
|
|
324
|
+
return { code: worker.code, name: worker.name, description: worker.description, version: worker.version, runtime: worker.runtime, manifest, actions: [...this.actions.values()].map(({ handler, prepare, inputSchema, outputSchema, ...a }) => a) };
|
|
299
325
|
}
|
|
300
326
|
async runHealth() {
|
|
301
327
|
return this.healthProvider();
|
|
302
328
|
}
|
|
303
|
-
/**
|
|
329
|
+
/** Legacy v0.4 event-routing compatibility hook. Subject to change before v1.0. */
|
|
304
330
|
async publishBusEvent(input) {
|
|
305
|
-
if (!this.agentId || !this.token) throw new Error("Agent must be started before publishing
|
|
306
|
-
const sourceServiceCode = input.sourceServiceCode ?? this.
|
|
331
|
+
if (!this.agentId || !this.token) throw new Error("Agent must be started before publishing routed events.");
|
|
332
|
+
const sourceServiceCode = input.sourceServiceCode ?? this.workerDefinition().code;
|
|
307
333
|
const result = await this.retry(() => this.client.publishBusEvent(this.agentId, this.token, { ...input, sourceServiceCode }));
|
|
308
334
|
this.log("info", "experimental bus event published", { eventId: result.eventId, eventType: input.eventType, sourceServiceCode, routedCommands: result.routedCommands.length });
|
|
309
335
|
return result;
|
|
310
336
|
}
|
|
311
|
-
async
|
|
337
|
+
async reportWorker() {
|
|
312
338
|
if (!this.agentId || !this.token) return;
|
|
313
339
|
const [health, configs] = await Promise.all([this.runHealth(), this.configProvider()]);
|
|
314
|
-
const
|
|
315
|
-
await this.retry(() => this.client.
|
|
340
|
+
const worker = { ...this.serviceSnapshot(), health, configs };
|
|
341
|
+
await this.retry(() => this.client.reportWorkers(this.agentId, this.token, { workers: [worker] }));
|
|
316
342
|
}
|
|
317
343
|
async heartbeat() {
|
|
318
344
|
if (!this.agentId || !this.token) return;
|
|
319
|
-
|
|
320
|
-
await this.retry(() => this.client.heartbeat(this.agentId, this.token, { health }));
|
|
345
|
+
await this.retry(() => this.client.heartbeat(this.agentId, this.token, {}));
|
|
321
346
|
}
|
|
322
347
|
async pollOnce() {
|
|
323
348
|
if (!this.agentId || !this.token) return;
|
|
@@ -403,6 +428,277 @@ var CapsuleAgent = class {
|
|
|
403
428
|
return { value: obj };
|
|
404
429
|
}
|
|
405
430
|
};
|
|
431
|
+
|
|
432
|
+
// src/ce/index.ts
|
|
433
|
+
var import_node_crypto = require("crypto");
|
|
434
|
+
var import_capsule_contracts_node = require("@xtrape/capsule-contracts-node");
|
|
435
|
+
var CeInboundMessageValidationError = class extends Error {
|
|
436
|
+
constructor(message, options) {
|
|
437
|
+
super(message, options);
|
|
438
|
+
this.name = "CeInboundMessageValidationError";
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
var CeServiceAgent = class {
|
|
442
|
+
serviceId;
|
|
443
|
+
instanceId;
|
|
444
|
+
serverUrl;
|
|
445
|
+
manifest;
|
|
446
|
+
address;
|
|
447
|
+
tenant;
|
|
448
|
+
registrationToken;
|
|
449
|
+
fetchImpl;
|
|
450
|
+
health = "HEALTHY";
|
|
451
|
+
hbTimer;
|
|
452
|
+
constructor(opts) {
|
|
453
|
+
this.serverUrl = opts.serverUrl.replace(/\/+$/, "");
|
|
454
|
+
this.manifest = opts.manifest;
|
|
455
|
+
this.address = opts.address;
|
|
456
|
+
this.tenant = opts.manifest.tenant ?? "default";
|
|
457
|
+
this.serviceId = opts.manifest.service.id;
|
|
458
|
+
this.instanceId = opts.instanceId ?? `${this.serviceId}-${(0, import_node_crypto.randomUUID)().slice(0, 8)}`;
|
|
459
|
+
this.registrationToken = opts.registrationToken;
|
|
460
|
+
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
461
|
+
}
|
|
462
|
+
/** The ServiceInstance this agent reports on register/heartbeat. */
|
|
463
|
+
instance() {
|
|
464
|
+
return import_capsule_contracts_node.ServiceInstanceSchema.parse({
|
|
465
|
+
contractVersion: import_capsule_contracts_node.CE_CONTRACT_VERSION,
|
|
466
|
+
serviceId: this.serviceId,
|
|
467
|
+
instanceId: this.instanceId,
|
|
468
|
+
tenant: this.tenant,
|
|
469
|
+
address: this.address,
|
|
470
|
+
version: this.manifest.service.version,
|
|
471
|
+
health: this.health
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
/** POST /v1/instances { manifest, instance } (sends the registration token if set). */
|
|
475
|
+
async register() {
|
|
476
|
+
const headers = this.registrationToken ? { "x-xtrape-registration-token": this.registrationToken } : void 0;
|
|
477
|
+
await this.post("/v1/instances", { manifest: this.manifest, instance: this.instance() }, headers);
|
|
478
|
+
return { instanceId: this.instanceId };
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Lifecycle convenience: register, then start periodic heartbeats. Returns a
|
|
482
|
+
* `stop()` that stops heartbeats and best-effort deregisters — so a service
|
|
483
|
+
* does not have to wire register/heartbeat/deregister by hand.
|
|
484
|
+
*/
|
|
485
|
+
async start(opts = {}) {
|
|
486
|
+
await this.register();
|
|
487
|
+
if (opts.heartbeatMs !== 0) this.startHeartbeat(opts.heartbeatMs ?? 1e4, opts.metrics);
|
|
488
|
+
return async () => {
|
|
489
|
+
this.stopHeartbeat();
|
|
490
|
+
await this.deregister().catch(() => {
|
|
491
|
+
});
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
setHealth(h) {
|
|
495
|
+
this.health = h;
|
|
496
|
+
}
|
|
497
|
+
/** POST /v1/instances/{id}/heartbeat */
|
|
498
|
+
async sendHeartbeat(metricsSummary) {
|
|
499
|
+
const hb = {
|
|
500
|
+
contractVersion: import_capsule_contracts_node.CE_CONTRACT_VERSION,
|
|
501
|
+
serviceId: this.serviceId,
|
|
502
|
+
instanceId: this.instanceId,
|
|
503
|
+
tenant: this.tenant,
|
|
504
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
505
|
+
health: this.health,
|
|
506
|
+
...metricsSummary ? { metricsSummary } : {}
|
|
507
|
+
};
|
|
508
|
+
await this.post(`/v1/instances/${encodeURIComponent(this.instanceId)}/heartbeat`, hb);
|
|
509
|
+
}
|
|
510
|
+
/** Start a periodic heartbeat; returns a stop function. Timer is unref'd. */
|
|
511
|
+
startHeartbeat(intervalMs = 1e4, metrics) {
|
|
512
|
+
this.stopHeartbeat();
|
|
513
|
+
this.hbTimer = setInterval(() => {
|
|
514
|
+
void this.sendHeartbeat(metrics?.()).catch(() => {
|
|
515
|
+
});
|
|
516
|
+
}, intervalMs);
|
|
517
|
+
this.hbTimer.unref?.();
|
|
518
|
+
return () => this.stopHeartbeat();
|
|
519
|
+
}
|
|
520
|
+
stopHeartbeat() {
|
|
521
|
+
if (this.hbTimer) {
|
|
522
|
+
clearInterval(this.hbTimer);
|
|
523
|
+
this.hbTimer = void 0;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
/** DELETE /v1/instances/{id} — graceful deregister. */
|
|
527
|
+
async deregister() {
|
|
528
|
+
await this.fetchJson(`/v1/instances/${encodeURIComponent(this.instanceId)}`, { method: "DELETE" });
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Validate an inbound message, run the handler, and build the reply message
|
|
532
|
+
* (senderType=SERVICE, targeted back at the sender, with replyToMessageId).
|
|
533
|
+
*/
|
|
534
|
+
async handleMessage(raw, handler) {
|
|
535
|
+
let msg;
|
|
536
|
+
try {
|
|
537
|
+
msg = import_capsule_contracts_node.ConversationMessageSchema.parse(raw);
|
|
538
|
+
} catch (error) {
|
|
539
|
+
throw new CeInboundMessageValidationError(
|
|
540
|
+
error instanceof Error ? error.message : "invalid inbound ConversationMessage",
|
|
541
|
+
{ cause: error }
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
const out = await handler(msg);
|
|
545
|
+
const reply = typeof out === "string" ? { content: out } : out;
|
|
546
|
+
return import_capsule_contracts_node.ConversationMessageSchema.parse({
|
|
547
|
+
contractVersion: import_capsule_contracts_node.CE_CONTRACT_VERSION,
|
|
548
|
+
messageId: `msg_${(0, import_node_crypto.randomUUID)().slice(0, 12)}`,
|
|
549
|
+
conversationId: msg.conversationId,
|
|
550
|
+
tenant: msg.tenant ?? "default",
|
|
551
|
+
// reply back to whoever sent it (USER/SERVICE/SYSTEM are all valid targets)
|
|
552
|
+
targetType: msg.senderType,
|
|
553
|
+
targetId: msg.senderId,
|
|
554
|
+
senderType: "SERVICE",
|
|
555
|
+
senderId: this.serviceId,
|
|
556
|
+
messageType: reply.messageType ?? "SUMMARY",
|
|
557
|
+
content: reply.content,
|
|
558
|
+
replyToMessageId: msg.messageId,
|
|
559
|
+
...reply.payload ? { payload: reply.payload } : {}
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
post(path, body, headers) {
|
|
563
|
+
return this.fetchJson(path, { method: "POST", body: JSON.stringify(body), ...headers ? { headers } : {} });
|
|
564
|
+
}
|
|
565
|
+
async fetchJson(path, init) {
|
|
566
|
+
const res = await this.fetchImpl(this.serverUrl + path, {
|
|
567
|
+
...init,
|
|
568
|
+
headers: { "content-type": "application/json", ...init.headers ?? {} }
|
|
569
|
+
});
|
|
570
|
+
const text = await res.text();
|
|
571
|
+
if (!res.ok) {
|
|
572
|
+
throw new Error(`server-ce ${path} -> ${res.status}: ${text}`);
|
|
573
|
+
}
|
|
574
|
+
if (!text) return void 0;
|
|
575
|
+
try {
|
|
576
|
+
return JSON.parse(text);
|
|
577
|
+
} catch (error) {
|
|
578
|
+
throw new Error(`server-ce ${path} returned non-JSON success response: ${text}`, { cause: error });
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
// src/ce/serve.ts
|
|
584
|
+
var import_node_http = require("http");
|
|
585
|
+
function send(res, status, body) {
|
|
586
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
587
|
+
res.end(JSON.stringify(body));
|
|
588
|
+
}
|
|
589
|
+
function readBody(req) {
|
|
590
|
+
return new Promise((resolve, reject) => {
|
|
591
|
+
let data = "";
|
|
592
|
+
req.on("data", (c) => data += c);
|
|
593
|
+
req.on("end", () => resolve(data));
|
|
594
|
+
req.on("error", reject);
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
function isLocalOnlyHost(host) {
|
|
598
|
+
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
599
|
+
}
|
|
600
|
+
function defaultLocalAddress(host, port) {
|
|
601
|
+
if (host === "::1") return `http://[::1]:${port}`;
|
|
602
|
+
return `http://${host}:${port}`;
|
|
603
|
+
}
|
|
604
|
+
function listen(server, port, host) {
|
|
605
|
+
return new Promise((resolve, reject) => {
|
|
606
|
+
const onError = (error) => {
|
|
607
|
+
server.off("listening", onListening);
|
|
608
|
+
reject(error);
|
|
609
|
+
};
|
|
610
|
+
const onListening = () => {
|
|
611
|
+
server.off("error", onError);
|
|
612
|
+
resolve();
|
|
613
|
+
};
|
|
614
|
+
server.once("error", onError);
|
|
615
|
+
server.once("listening", onListening);
|
|
616
|
+
server.listen(port, host);
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
function sendMessageError(res, error) {
|
|
620
|
+
if (error instanceof CeInboundMessageValidationError) {
|
|
621
|
+
send(res, 400, { code: "VALIDATION_FAILED", message: error.message, retryable: false });
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const message = error instanceof Error ? error.message : "message handler failed";
|
|
625
|
+
send(res, 500, { code: "HANDLER_FAILED", message, retryable: true });
|
|
626
|
+
}
|
|
627
|
+
async function serveCeService(opts) {
|
|
628
|
+
const host = opts.host ?? "0.0.0.0";
|
|
629
|
+
let agent;
|
|
630
|
+
const server = (0, import_node_http.createServer)((req, res) => {
|
|
631
|
+
void (async () => {
|
|
632
|
+
try {
|
|
633
|
+
if (req.method === "GET" && req.url === "/health") {
|
|
634
|
+
send(res, 200, { ok: true, service: opts.manifest.service.id });
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
if (req.method === "POST" && req.url === "/messages") {
|
|
638
|
+
if (!agent) {
|
|
639
|
+
send(res, 503, { code: "NOT_READY", message: "service not registered yet", retryable: true });
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
const raw = await readBody(req);
|
|
643
|
+
let payload;
|
|
644
|
+
try {
|
|
645
|
+
payload = raw ? JSON.parse(raw) : {};
|
|
646
|
+
} catch (error) {
|
|
647
|
+
send(res, 400, {
|
|
648
|
+
code: "VALIDATION_FAILED",
|
|
649
|
+
message: error instanceof Error ? error.message : "invalid JSON request body",
|
|
650
|
+
retryable: false
|
|
651
|
+
});
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
const reply = await agent.handleMessage(payload, opts.handler);
|
|
655
|
+
send(res, 200, reply);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
send(res, 404, { code: "NOT_FOUND", message: "not found", retryable: false });
|
|
659
|
+
} catch (e) {
|
|
660
|
+
sendMessageError(res, e);
|
|
661
|
+
}
|
|
662
|
+
})();
|
|
663
|
+
});
|
|
664
|
+
await listen(server, opts.port ?? 0, host);
|
|
665
|
+
const info = server.address();
|
|
666
|
+
const port = typeof info === "object" && info ? info.port : opts.port ?? 0;
|
|
667
|
+
const address = opts.address ?? (isLocalOnlyHost(host) ? defaultLocalAddress(host, port) : void 0);
|
|
668
|
+
if (!address) {
|
|
669
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
670
|
+
throw new Error(
|
|
671
|
+
`serveCeService requires an explicit address when host=${host}. For deployed/containerized use, pass a reachable callback URL such as http://demo-service:8080.`
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
try {
|
|
675
|
+
agent = new CeServiceAgent({
|
|
676
|
+
serverUrl: opts.serverUrl,
|
|
677
|
+
manifest: opts.manifest,
|
|
678
|
+
address,
|
|
679
|
+
instanceId: opts.instanceId,
|
|
680
|
+
registrationToken: opts.registrationToken,
|
|
681
|
+
fetchImpl: opts.fetchImpl
|
|
682
|
+
});
|
|
683
|
+
await agent.register();
|
|
684
|
+
if (opts.heartbeatMs !== 0) agent.startHeartbeat(opts.heartbeatMs ?? 1e4);
|
|
685
|
+
} catch (error) {
|
|
686
|
+
agent?.stopHeartbeat();
|
|
687
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
688
|
+
throw error;
|
|
689
|
+
}
|
|
690
|
+
return {
|
|
691
|
+
agent,
|
|
692
|
+
address,
|
|
693
|
+
port,
|
|
694
|
+
async close() {
|
|
695
|
+
agent?.stopHeartbeat();
|
|
696
|
+
await agent?.deregister().catch(() => {
|
|
697
|
+
});
|
|
698
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
}
|
|
406
702
|
// Annotate the CommonJS export names for ESM import in node:
|
|
407
703
|
0 && (module.exports = {
|
|
408
704
|
AgentApiClient,
|
|
@@ -412,7 +708,10 @@ var CapsuleAgent = class {
|
|
|
412
708
|
BusDisabledError,
|
|
413
709
|
BusRateLimitedError,
|
|
414
710
|
CapsuleAgent,
|
|
711
|
+
CeServiceAgent,
|
|
415
712
|
FileTokenStore,
|
|
416
713
|
NetworkError,
|
|
417
|
-
RegistrationError
|
|
714
|
+
RegistrationError,
|
|
715
|
+
XtrapeAgent,
|
|
716
|
+
serveCeService
|
|
418
717
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ActionPrepareResult, PublishBusEventRequest, RuntimeKind, HealthReportInput, ConfigItemInput, ActionDefinitionInput, PublishBusEventResponse, ReportedService, RegisterAgentRequest, AgentHeartbeatRequest, ServiceReportRequest, ReportCommandResultRequest } from '@xtrape/capsule-contracts-node';
|
|
1
|
+
import { ActionPrepareResult, PublishBusEventRequest, RuntimeKind, HealthReportInput, ConfigItemInput, ActionDefinitionInput, PublishBusEventResponse, ReportedService, RegisterAgentRequest, AgentHeartbeatRequest, ServiceReportRequest, ReportCommandResultRequest, ConversationMessage, ServiceManifest, ServiceInstance, ServiceInstanceHealth } from '@xtrape/capsule-contracts-node';
|
|
2
2
|
|
|
3
3
|
type AgentMode = "embedded";
|
|
4
4
|
type ActionHandler = (payload: Record<string, unknown>) => Promise<{
|
|
@@ -38,7 +38,15 @@ interface StructuredLogger {
|
|
|
38
38
|
* `structuredLogger` wins.
|
|
39
39
|
*/
|
|
40
40
|
type ConsoleLogger = Pick<Console, "debug" | "info" | "warn" | "error">;
|
|
41
|
-
interface
|
|
41
|
+
interface WorkerDefinition {
|
|
42
|
+
code: string;
|
|
43
|
+
name: string;
|
|
44
|
+
description?: string;
|
|
45
|
+
version: string;
|
|
46
|
+
runtime: RuntimeKind;
|
|
47
|
+
manifest?: Record<string, unknown>;
|
|
48
|
+
}
|
|
49
|
+
interface CapsuleAgentOptionsBase {
|
|
42
50
|
backendUrl: string;
|
|
43
51
|
registrationToken?: string;
|
|
44
52
|
tokenStore?: {
|
|
@@ -49,17 +57,12 @@ interface CapsuleAgentOptions {
|
|
|
49
57
|
name?: string;
|
|
50
58
|
runtime?: RuntimeKind;
|
|
51
59
|
};
|
|
52
|
-
service: {
|
|
53
|
-
code: string;
|
|
54
|
-
name: string;
|
|
55
|
-
description?: string;
|
|
56
|
-
version: string;
|
|
57
|
-
runtime: RuntimeKind;
|
|
58
|
-
manifest?: Record<string, unknown>;
|
|
59
|
-
};
|
|
60
60
|
intervals?: {
|
|
61
61
|
heartbeatMs?: number;
|
|
62
62
|
commandPollMs?: number;
|
|
63
|
+
/** Canonical Worker Runtime Report interval. */
|
|
64
|
+
workerReportMs?: number;
|
|
65
|
+
/** @deprecated Compatibility alias for `workerReportMs`. */
|
|
63
66
|
serviceReportMs?: number;
|
|
64
67
|
};
|
|
65
68
|
heartbeatIntervalSeconds?: number;
|
|
@@ -75,6 +78,19 @@ interface CapsuleAgentOptions {
|
|
|
75
78
|
structuredLogger?: StructuredLogger;
|
|
76
79
|
failOnStartError?: boolean;
|
|
77
80
|
}
|
|
81
|
+
/** Canonical Embedded Agent options: exactly one runtime unit via `worker`. */
|
|
82
|
+
type WorkerAgentOptions = CapsuleAgentOptionsBase & {
|
|
83
|
+
worker: WorkerDefinition;
|
|
84
|
+
/** @deprecated Legacy alias; do not combine with `worker`. */
|
|
85
|
+
service?: never;
|
|
86
|
+
};
|
|
87
|
+
/** Legacy Embedded Agent options: exactly one runtime unit via `service`. */
|
|
88
|
+
type LegacyServiceAgentOptions = CapsuleAgentOptionsBase & {
|
|
89
|
+
/** @deprecated Legacy compatibility alias for `worker`. */
|
|
90
|
+
service: WorkerDefinition;
|
|
91
|
+
worker?: never;
|
|
92
|
+
};
|
|
93
|
+
type CapsuleAgentOptions = WorkerAgentOptions | LegacyServiceAgentOptions;
|
|
78
94
|
type HealthProvider = () => Promise<HealthReportInput> | HealthReportInput;
|
|
79
95
|
type ConfigProvider = () => Promise<ConfigItemInput[]> | ConfigItemInput[];
|
|
80
96
|
interface RegisteredAction extends ActionDefinitionInput {
|
|
@@ -87,9 +103,12 @@ interface TokenStore {
|
|
|
87
103
|
clear(): Promise<void>;
|
|
88
104
|
}
|
|
89
105
|
type ServiceSnapshot = ReportedService;
|
|
90
|
-
|
|
91
|
-
type
|
|
106
|
+
type WorkerSnapshot = ReportedService;
|
|
107
|
+
type XtrapeAgentOptions = CapsuleAgentOptions;
|
|
108
|
+
/** Legacy v0.4 event-routing publish input. Subject to change before v1.0. */
|
|
109
|
+
type PublishBusEventInput = Omit<PublishBusEventRequest, "sourceServiceCode" | "experimental"> & {
|
|
92
110
|
sourceServiceCode?: string;
|
|
111
|
+
experimental?: PublishBusEventRequest["experimental"];
|
|
93
112
|
};
|
|
94
113
|
type PublishBusEventResult = PublishBusEventResponse;
|
|
95
114
|
|
|
@@ -114,11 +133,12 @@ declare class CapsuleAgent {
|
|
|
114
133
|
stop(): Promise<void>;
|
|
115
134
|
private loop;
|
|
116
135
|
private ensureRegistered;
|
|
136
|
+
private workerDefinition;
|
|
117
137
|
private serviceSnapshot;
|
|
118
138
|
runHealth(): Promise<HealthReportInput>;
|
|
119
|
-
/**
|
|
139
|
+
/** Legacy v0.4 event-routing compatibility hook. Subject to change before v1.0. */
|
|
120
140
|
publishBusEvent(input: PublishBusEventInput): Promise<PublishBusEventResult>;
|
|
121
|
-
private
|
|
141
|
+
private reportWorker;
|
|
122
142
|
private heartbeat;
|
|
123
143
|
private pollOnce;
|
|
124
144
|
private execute;
|
|
@@ -156,7 +176,8 @@ declare class AgentApiError extends Error {
|
|
|
156
176
|
/**
|
|
157
177
|
* The backend rejected the registration token (expired, revoked, already
|
|
158
178
|
* used, malformed). The agent cannot proceed without operator action: a
|
|
159
|
-
* fresh registration
|
|
179
|
+
* a fresh registration credential must be created in Panel. The current API
|
|
180
|
+
* still calls this value a registration token.
|
|
160
181
|
*/
|
|
161
182
|
declare class RegistrationError extends AgentApiError {
|
|
162
183
|
constructor(message: string, status: number, body?: unknown, code?: string);
|
|
@@ -201,6 +222,10 @@ declare class AgentApiClient {
|
|
|
201
222
|
}>;
|
|
202
223
|
heartbeat(agentId: string, token: string, req: AgentHeartbeatRequest): Promise<unknown>;
|
|
203
224
|
reportServices(agentId: string, token: string, req: ServiceReportRequest): Promise<unknown>;
|
|
225
|
+
/** Report Workers through the canonical endpoint, with a legacy fallback. */
|
|
226
|
+
reportWorkers(agentId: string, token: string, req: {
|
|
227
|
+
workers: ReportedService[];
|
|
228
|
+
}): Promise<unknown>;
|
|
204
229
|
pollCommands(agentId: string, token: string): Promise<{
|
|
205
230
|
type: "ACTION_EXECUTE" | "ACTION_PREPARE";
|
|
206
231
|
id: string;
|
|
@@ -216,7 +241,7 @@ declare class AgentApiClient {
|
|
|
216
241
|
completedAt?: string | null | undefined;
|
|
217
242
|
}[]>;
|
|
218
243
|
reportResult(agentId: string, token: string, commandId: string, req: ReportCommandResultRequest): Promise<unknown>;
|
|
219
|
-
/**
|
|
244
|
+
/** Legacy v0.4 event-routing compatibility hook. Subject to change before v1.0. */
|
|
220
245
|
publishBusEvent(agentId: string, token: string, req: PublishBusEventInput): Promise<{
|
|
221
246
|
experimental: "v0.4-experimental";
|
|
222
247
|
eventId: string;
|
|
@@ -233,4 +258,129 @@ declare class AgentApiClient {
|
|
|
233
258
|
}>;
|
|
234
259
|
}
|
|
235
260
|
|
|
236
|
-
|
|
261
|
+
/**
|
|
262
|
+
* Xtrape CE Phase 0 service agent.
|
|
263
|
+
*
|
|
264
|
+
* Framework-agnostic SDK for an `xtrape-service` to:
|
|
265
|
+
* - register an instance to `xtrape-server-ce`;
|
|
266
|
+
* - send heartbeats;
|
|
267
|
+
* - validate inbound ConversationMessages and build replies.
|
|
268
|
+
*
|
|
269
|
+
* See xtrape-docs runtime-platform/04 (registration & heartbeat) and /03 (message).
|
|
270
|
+
*/
|
|
271
|
+
interface CeServiceAgentOptions {
|
|
272
|
+
/** xtrape-server-ce base URL, e.g. http://localhost:3000 */
|
|
273
|
+
serverUrl: string;
|
|
274
|
+
/** this service's manifest */
|
|
275
|
+
manifest: ServiceManifest;
|
|
276
|
+
/** this instance's reachable address (URL) */
|
|
277
|
+
address: string;
|
|
278
|
+
/** optional fixed instance id; defaults to `<serviceId>-<rand>` */
|
|
279
|
+
instanceId?: string;
|
|
280
|
+
/**
|
|
281
|
+
* Shared registration token. When server-ce has `SERVER_CE_REGISTRATION_TOKEN`
|
|
282
|
+
* set, registration requires it; the agent sends it as the
|
|
283
|
+
* `x-xtrape-registration-token` header on register.
|
|
284
|
+
*/
|
|
285
|
+
registrationToken?: string;
|
|
286
|
+
/** injectable fetch (tests); defaults to global fetch */
|
|
287
|
+
fetchImpl?: typeof fetch;
|
|
288
|
+
}
|
|
289
|
+
type CeMessageReply = string | {
|
|
290
|
+
content: string;
|
|
291
|
+
messageType?: ConversationMessage["messageType"];
|
|
292
|
+
payload?: Record<string, unknown>;
|
|
293
|
+
};
|
|
294
|
+
/** Handler invoked with a validated inbound message; returns the reply content. */
|
|
295
|
+
type CeMessageHandler = (msg: ConversationMessage) => CeMessageReply | Promise<CeMessageReply>;
|
|
296
|
+
declare class CeServiceAgent {
|
|
297
|
+
readonly serviceId: string;
|
|
298
|
+
readonly instanceId: string;
|
|
299
|
+
private readonly serverUrl;
|
|
300
|
+
private readonly manifest;
|
|
301
|
+
private readonly address;
|
|
302
|
+
private readonly tenant;
|
|
303
|
+
private readonly registrationToken?;
|
|
304
|
+
private readonly fetchImpl;
|
|
305
|
+
private health;
|
|
306
|
+
private hbTimer?;
|
|
307
|
+
constructor(opts: CeServiceAgentOptions);
|
|
308
|
+
/** The ServiceInstance this agent reports on register/heartbeat. */
|
|
309
|
+
instance(): ServiceInstance;
|
|
310
|
+
/** POST /v1/instances { manifest, instance } (sends the registration token if set). */
|
|
311
|
+
register(): Promise<{
|
|
312
|
+
instanceId: string;
|
|
313
|
+
}>;
|
|
314
|
+
/**
|
|
315
|
+
* Lifecycle convenience: register, then start periodic heartbeats. Returns a
|
|
316
|
+
* `stop()` that stops heartbeats and best-effort deregisters — so a service
|
|
317
|
+
* does not have to wire register/heartbeat/deregister by hand.
|
|
318
|
+
*/
|
|
319
|
+
start(opts?: {
|
|
320
|
+
heartbeatMs?: number;
|
|
321
|
+
metrics?: () => Record<string, unknown>;
|
|
322
|
+
}): Promise<() => Promise<void>>;
|
|
323
|
+
setHealth(h: ServiceInstanceHealth): void;
|
|
324
|
+
/** POST /v1/instances/{id}/heartbeat */
|
|
325
|
+
sendHeartbeat(metricsSummary?: Record<string, unknown>): Promise<void>;
|
|
326
|
+
/** Start a periodic heartbeat; returns a stop function. Timer is unref'd. */
|
|
327
|
+
startHeartbeat(intervalMs?: number, metrics?: () => Record<string, unknown>): () => void;
|
|
328
|
+
stopHeartbeat(): void;
|
|
329
|
+
/** DELETE /v1/instances/{id} — graceful deregister. */
|
|
330
|
+
deregister(): Promise<void>;
|
|
331
|
+
/**
|
|
332
|
+
* Validate an inbound message, run the handler, and build the reply message
|
|
333
|
+
* (senderType=SERVICE, targeted back at the sender, with replyToMessageId).
|
|
334
|
+
*/
|
|
335
|
+
handleMessage(raw: unknown, handler: CeMessageHandler): Promise<ConversationMessage>;
|
|
336
|
+
private post;
|
|
337
|
+
private fetchJson;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* One-call bootstrap for an `xtrape-service`: start an HTTP listener that serves
|
|
342
|
+
* `POST /messages` (and `GET /health`), register the instance to xtrape-server-ce,
|
|
343
|
+
* and run heartbeats. Returns a handle whose `close()` stops heartbeats,
|
|
344
|
+
* deregisters, and closes the listener.
|
|
345
|
+
*
|
|
346
|
+
* Uses only `node:http` so it stays framework-agnostic. A service that already
|
|
347
|
+
* has an HTTP server can use `CeServiceAgent` directly instead.
|
|
348
|
+
*/
|
|
349
|
+
interface ServeCeServiceOptions {
|
|
350
|
+
/** xtrape-server-ce base URL, e.g. http://localhost:3000 */
|
|
351
|
+
serverUrl: string;
|
|
352
|
+
manifest: ServiceManifest;
|
|
353
|
+
/** business handler invoked per inbound ConversationMessage */
|
|
354
|
+
handler: CeMessageHandler;
|
|
355
|
+
/** shared registration token, if server-ce requires one */
|
|
356
|
+
registrationToken?: string;
|
|
357
|
+
/** listen port; 0 picks an ephemeral port (default 0) */
|
|
358
|
+
port?: number;
|
|
359
|
+
/** listen host (default 0.0.0.0) */
|
|
360
|
+
host?: string;
|
|
361
|
+
/**
|
|
362
|
+
* Address server-ce should call back.
|
|
363
|
+
*
|
|
364
|
+
* Required for deployed/containerized use. When omitted, the SDK only derives
|
|
365
|
+
* a default address for explicit local-only hosts such as `127.0.0.1` or
|
|
366
|
+
* `localhost`.
|
|
367
|
+
*/
|
|
368
|
+
address?: string;
|
|
369
|
+
/** heartbeat interval ms; 0 disables (default 10000) */
|
|
370
|
+
heartbeatMs?: number;
|
|
371
|
+
instanceId?: string;
|
|
372
|
+
/** injectable fetch (tests); defaults to global fetch */
|
|
373
|
+
fetchImpl?: typeof fetch;
|
|
374
|
+
}
|
|
375
|
+
interface ServeCeServiceHandle {
|
|
376
|
+
agent: CeServiceAgent;
|
|
377
|
+
/** advertised address registered with server-ce */
|
|
378
|
+
address: string;
|
|
379
|
+
/** actual listening port */
|
|
380
|
+
port: number;
|
|
381
|
+
/** stop heartbeats, deregister (best-effort), and close the HTTP server */
|
|
382
|
+
close(): Promise<void>;
|
|
383
|
+
}
|
|
384
|
+
declare function serveCeService(opts: ServeCeServiceOptions): Promise<ServeCeServiceHandle>;
|
|
385
|
+
|
|
386
|
+
export { type ActionHandler, type ActionPrepareHandler, AgentApiClient, AgentApiError, AgentAuthError, type AgentLogLevel, type AgentLogRecord, type AgentMode, BusDepthExceededError, BusDisabledError, BusRateLimitedError, CapsuleAgent, type CapsuleAgentOptions, type CeMessageHandler, type CeMessageReply, CeServiceAgent, type CeServiceAgentOptions, type ConfigProvider, type ConsoleLogger, FileTokenStore, type HealthProvider, type LegacyServiceAgentOptions, NetworkError, type PublishBusEventInput, type PublishBusEventResult, type RegisteredAction, RegistrationError, type ServeCeServiceHandle, type ServeCeServiceOptions, type ServiceSnapshot, type StructuredLogger, type TokenStore, type WorkerAgentOptions, type WorkerDefinition, type WorkerSnapshot, CapsuleAgent as XtrapeAgent, type XtrapeAgentOptions, serveCeService };
|