@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/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
- /** Experimental v0.4 Capsule Bus publish hook. Subject to change before v1.0. */
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.reportService();
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("service-report", this.options.intervals?.serviceReportMs ?? 6e4, () => this.reportService());
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 agent = this.options.agent ?? { code: this.options.service.code, name: this.options.service.name, runtime: this.options.service.runtime };
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 manifest = { kind: "CapsuleService", schemaVersion: "1.0", code: this.options.service.code, name: this.options.service.name, description: this.options.service.description, version: this.options.service.version, runtime: this.options.service.runtime, agentMode: "embedded", ...this.options.service.manifest ?? {} };
298
- return { code: this.options.service.code, name: this.options.service.name, description: this.options.service.description, version: this.options.service.version, runtime: this.options.service.runtime, manifest, actions: [...this.actions.values()].map(({ handler, prepare, inputSchema, outputSchema, ...a }) => a) };
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
- /** Experimental v0.4 Capsule Bus publish hook. Subject to change before v1.0. */
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 Capsule Bus events.");
306
- const sourceServiceCode = input.sourceServiceCode ?? this.options.service.code;
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 reportService() {
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 svc = { ...this.serviceSnapshot(), health, configs };
315
- await this.retry(() => this.client.reportServices(this.agentId, this.token, { services: [svc] }));
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
- const health = await this.runHealth();
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 CapsuleAgentOptions {
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
- /** Experimental v0.4 Capsule Bus publish input. Subject to change before v1.0. */
91
- type PublishBusEventInput = Omit<PublishBusEventRequest, "sourceServiceCode"> & {
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
- /** Experimental v0.4 Capsule Bus publish hook. Subject to change before v1.0. */
139
+ /** Legacy v0.4 event-routing compatibility hook. Subject to change before v1.0. */
120
140
  publishBusEvent(input: PublishBusEventInput): Promise<PublishBusEventResult>;
121
- private reportService;
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 token must be created in Opstage.
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
- /** Experimental v0.4 Capsule Bus publish hook. Subject to change before v1.0. */
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
- export { type ActionHandler, type ActionPrepareHandler, AgentApiClient, AgentApiError, AgentAuthError, type AgentLogLevel, type AgentLogRecord, type AgentMode, BusDepthExceededError, BusDisabledError, BusRateLimitedError, CapsuleAgent, type CapsuleAgentOptions, type ConfigProvider, type ConsoleLogger, FileTokenStore, type HealthProvider, NetworkError, type PublishBusEventInput, type PublishBusEventResult, type RegisteredAction, RegistrationError, type ServiceSnapshot, type StructuredLogger, type TokenStore };
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 };