@xtrape/capsule-agent-node 0.4.0 → 0.5.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 CHANGED
@@ -1,4 +1,4 @@
1
- Opstage CE
2
- Copyright 2026 Xtrape Capsule contributors
1
+ Xtrape Embedded Agent SDK for Node.js
2
+ Copyright 2026 Xtrape contributors
3
3
 
4
- This product includes software developed by the Xtrape Capsule contributors.
4
+ This product includes software developed by the Xtrape contributors.
package/README.md CHANGED
@@ -1,14 +1,21 @@
1
- # @xtrape/capsule-agent-node
1
+ # Xtrape Embedded Agent SDK for Node.js
2
2
 
3
- > Node.js embedded Agent SDK for connecting Capsule Services to Opstage.
3
+ > Node.js Embedded Agent SDK for connecting Workers to the Xtrape control plane.
4
+
5
+ The npm name `@xtrape/capsule-agent-node`, the `CapsuleAgent` export, and some
6
+ existing wire fields are legacy compatibility identifiers. `Capsule` is not a
7
+ current Xtrape concept. New integrations should use `XtrapeAgent` and the
8
+ `worker` option.
4
9
 
5
10
  [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](./LICENSE)
6
11
  [![Status: Public Review](https://img.shields.io/badge/status-Public%20Review-orange.svg)](https://xtrape-com.github.io/xtrape-capsule-site/)
7
12
  [![Docs](https://img.shields.io/badge/docs-xtrape--capsule--site-blue.svg)](https://xtrape-com.github.io/xtrape-capsule-site/agents/node-embedded-agent)
8
13
 
9
- `@xtrape/capsule-agent-node` supports the stable Node.js Embedded Agent SDK path for single Capsule Services. For multi-service OpHub functionality, use OpHub (Go runtime).
14
+ The package supports the Node.js Embedded Agent mode for one Worker. Multi-Worker
15
+ ownership belongs to a sidecar or external Agent. Gateway, when present, routes
16
+ Agent/control-plane traffic and does not replace the Agent role.
10
17
 
11
- > **Package status:** Xtrape Capsule is currently in **Public Review** before
18
+ > **Package status:** Xtrape is currently in **Public Review** before
12
19
  > the `v0.1.0 Public Preview` release. This package is published under the
13
20
  > `public-review` dist-tag. APIs, contracts, deployment instructions, and SDK
14
21
  > interfaces may still change.
@@ -36,32 +43,32 @@ pnpm build
36
43
 
37
44
  ## Minimal Example
38
45
 
39
- This example matches the current SDK API: create a `CapsuleAgent`, configure
46
+ This example uses the canonical alias `XtrapeAgent`, configures
40
47
  providers with `.health()` / `.configs()`, register Actions with `.action()`,
41
48
  then call `start()`.
42
49
 
43
50
  ```ts
44
- import { CapsuleAgent } from "@xtrape/capsule-agent-node";
51
+ import { XtrapeAgent } from "@xtrape/capsule-agent-node";
45
52
 
46
- const agent = new CapsuleAgent({
53
+ const agent = new XtrapeAgent({
47
54
  backendUrl: process.env.OPSTAGE_BACKEND_URL ?? "http://localhost:8080",
48
55
  registrationToken: process.env.OPSTAGE_REGISTRATION_TOKEN,
49
56
  tokenStore: { file: "./data/agent-token.txt" },
50
- // Optional. If omitted, the SDK derives agent identity from `service`.
51
- // Provide it explicitly when one Agent owns multiple Capsule Services on
57
+ // Optional. If omitted, the SDK derives Agent identity from `worker`.
58
+ // Provide it explicitly when one Agent owns multiple Workers on
52
59
  // the same host, or when you want a stable agent code that survives
53
60
  // service renames.
54
61
  agent: {
55
- code: "hello-capsule-agent",
56
- name: "Hello Capsule Agent",
62
+ code: "hello-worker-agent",
63
+ name: "Hello Worker Agent",
57
64
  runtime: "nodejs",
58
65
  },
59
- service: {
60
- code: "hello-capsule",
61
- name: "Hello Capsule",
66
+ worker: {
67
+ code: "hello-worker",
68
+ name: "Hello Worker",
62
69
  version: "0.1.0",
63
70
  runtime: "nodejs",
64
- description: "Minimal Capsule Service example",
71
+ description: "Minimal Worker example",
65
72
  },
66
73
  });
67
74
 
@@ -112,10 +119,12 @@ await agent.start();
112
119
 
113
120
  ## How Registration Works
114
121
 
115
- 1. An operator creates a single-use Registration Token in Opstage CE.
116
- 2. The service starts with `registrationToken` and calls the Agent registration
122
+ 1. An operator creates a single-use registration credential in Panel CE (the
123
+ current API calls it a Registration Token).
124
+ 2. The Worker starts with `registrationToken` and calls the Agent registration
117
125
  API.
118
- 3. Opstage returns an Agent ID and Agent token.
126
+ 3. The control plane returns an Agent ID and Agent credential (currently named
127
+ an Agent token).
119
128
  4. The SDK stores the issued credentials in `tokenStore.file` as `<agentId>:<agentToken>`.
120
129
  5. Future restarts reuse the stored Agent token; the Registration Token is not
121
130
  needed again.
@@ -123,14 +132,15 @@ await agent.start();
123
132
  If the token file is lost or the Agent is revoked, create a new Registration
124
133
  Token and restart the service with it.
125
134
 
126
- ## Service Manifest
135
+ ## Worker Manifest
127
136
 
128
- The `service` option describes the Capsule Service reported to Opstage:
137
+ The `worker` option describes the Worker reported to the control plane. The old
138
+ `service` option remains accepted for compatibility:
129
139
 
130
140
  ```ts
131
- service: {
132
- code: "hello-capsule", // stable unique service code
133
- name: "Hello Capsule", // operator-facing display name
141
+ worker: {
142
+ code: "hello-worker", // stable unique Worker code
143
+ name: "Hello Worker", // operator-facing display name
134
144
  description: "...", // optional
135
145
  version: "0.1.0", // service version
136
146
  runtime: "nodejs", // nodejs | java | python | go | other
@@ -138,8 +148,9 @@ service: {
138
148
  }
139
149
  ```
140
150
 
141
- The SDK wraps this as a `CapsuleService` manifest with `kind: "CapsuleService"`,
142
- `schemaVersion: "1.0"`, and `agentMode: "embedded"`.
151
+ The SDK wraps this as a Worker manifest with `kind: "Worker"`,
152
+ `schemaVersion: "1.0"`, and `agentMode: "embedded"`. Existing Panel versions
153
+ continue accepting it through the legacy reported-service wire envelope.
143
154
 
144
155
  ## Health Reporting
145
156
 
@@ -153,13 +164,13 @@ agent.health(async () => ({
153
164
  }));
154
165
  ```
155
166
 
156
- The provider runs for heartbeats and service reports. Do not include secrets in
167
+ The provider runs for heartbeats and Worker Runtime Reports. Do not include secrets in
157
168
  `details`.
158
169
 
159
170
  Agent health providers return protocol-level `HealthStatus` values: `UP`,
160
171
  `DEGRADED`, `DOWN`, `UNKNOWN`.
161
172
 
162
- Opstage may derive an operator-facing `effectiveStatus`: `HEALTHY`, `UNHEALTHY`,
173
+ Xtrape Panel may derive an operator-facing `effectiveStatus`: `HEALTHY`, `UNHEALTHY`,
163
174
  `STALE`, `OFFLINE`.
164
175
 
165
176
  ## Config Reporting
@@ -186,8 +197,9 @@ agent.configs(() => [
186
197
  ]);
187
198
  ```
188
199
 
189
- Configs are reported to Opstage for visibility; the current CE flow does not
190
- push config values from Opstage into the service.
200
+ Config Definitions and safe observations are reported to Panel for visibility.
201
+ The Worker or deployment platform remains the configuration owner; current CE
202
+ does not push arbitrary values into the Worker.
191
203
 
192
204
  ## Actions
193
205
 
@@ -207,12 +219,12 @@ agent.action({
207
219
  });
208
220
  ```
209
221
 
210
- Action metadata is published in the service report. The SDK intentionally strips
222
+ Action metadata is published in the Worker Runtime Report. The SDK intentionally strips
211
223
  runtime-only handler functions before reporting the Action Catalog.
212
224
 
213
225
  ## Action Prepare / Execute
214
226
 
215
- Opstage uses two Command types for actions:
227
+ Xtrape Panel uses two Command types for actions:
216
228
 
217
229
  1. `ACTION_PREPARE` — created when the UI opens an Action panel. The SDK calls
218
230
  `prepare()` if present and returns dynamic form metadata such as
@@ -229,8 +241,8 @@ The embedded Agent starts three loops by default:
229
241
 
230
242
  | Loop | Default | Purpose |
231
243
  | -------------- | ---------: | ---------------------------------- |
232
- | Heartbeat | 30 seconds | Agent liveness and latest health |
233
- | Service report | 60 seconds | Manifest, configs, actions, health |
244
+ | Heartbeat | 30 seconds | Agent liveness |
245
+ | Worker report | 60 seconds | Manifest, config observations, Actions, health |
234
246
  | Command poll | 5 seconds | Fetch and execute pending Commands |
235
247
 
236
248
  You can override intervals:
@@ -240,7 +252,7 @@ new CapsuleAgent({
240
252
  // ...
241
253
  intervals: {
242
254
  heartbeatMs: 30_000,
243
- serviceReportMs: 60_000,
255
+ workerReportMs: 60_000,
244
256
  commandPollMs: 5_000,
245
257
  },
246
258
  });
@@ -265,7 +277,7 @@ Security recommendations:
265
277
 
266
278
  - chmod the containing directory so only the service user can read it;
267
279
  - never commit token files;
268
- - rotate by revoking the Agent in Opstage and registering a new one;
280
+ - rotate by revoking the Agent in Xtrape Panel and registering a new one;
269
281
  - prefer secret managers for production wrappers when available.
270
282
 
271
283
  ## Security Notes
@@ -342,7 +354,7 @@ try {
342
354
  process.exit(65);
343
355
  }
344
356
  if (err instanceof NetworkError) {
345
- console.error("could not reach Opstage:", err.message);
357
+ console.error("could not reach Xtrape Panel:", err.message);
346
358
  process.exit(75);
347
359
  }
348
360
  throw err;
@@ -390,7 +402,7 @@ and `logger` are configured, `structuredLogger` wins.
390
402
 
391
403
  | Package | Compatible with |
392
404
  | ---------------------------------- | ------------------------------------------------------------- |
393
- | `@xtrape/capsule-agent-node@0.1.x` | Opstage CE `0.1.x` and `@xtrape/capsule-contracts-node@0.1.x` |
405
+ | `@xtrape/capsule-agent-node@0.1.x` | Xtrape Panel CE `0.1.x` and `@xtrape/capsule-contracts-node@0.1.x` |
394
406
 
395
407
  Use matching minor versions across CE, Agent SDK, and Contracts during Public
396
408
  Review and Public Preview. The wire protocol may still change before `v1.0`.
@@ -405,16 +417,16 @@ The token is single-use and short-lived.
405
417
  (registration tokens flip to `USED` on first use).
406
418
  - Check the token has not expired. Operators set `expiresInSeconds` when
407
419
  creating the token; the default in CE is short.
408
- - Check the token has not been revoked from the Opstage console.
420
+ - Check the token has not been revoked from the Xtrape Panel console.
409
421
  - Re-create a fresh token in the console and start the agent with it.
410
422
 
411
423
  ### Agent reports `ECONNREFUSED` / cannot reach backend
412
424
 
413
425
  - Confirm `OPSTAGE_BACKEND_URL` resolves from inside the container or host
414
426
  where the agent runs. The default of `http://localhost:8080` only works if
415
- the agent runs on the same host as Opstage.
427
+ the agent runs on the same host as Xtrape Panel.
416
428
  - If you sit behind a reverse proxy, point `backendUrl` at the proxy URL —
417
- not the internal Opstage container address.
429
+ not the internal Xtrape Panel container address.
418
430
  - Outbound HTTPS through corporate proxies: respect `HTTPS_PROXY` /
419
431
  `NO_PROXY` env vars (`undici` honors them via `setGlobalDispatcher`).
420
432
 
@@ -427,7 +439,7 @@ The token is single-use and short-lived.
427
439
  file exists but contains anything else, delete it and re-register with a
428
440
  fresh registration token.
429
441
  - Token revocation: an operator may have revoked the agent or the agent
430
- token from the console. Inspect the agent's status in Opstage; if it shows
442
+ token from the console. Inspect the agent's status in Xtrape Panel; if it shows
431
443
  `REVOKED` or `DISABLED`, that's the cause.
432
444
 
433
445
  ### Heartbeats succeed but action results never arrive
@@ -446,7 +458,7 @@ The token is single-use and short-lived.
446
458
  - Site: https://xtrape-com.github.io/xtrape-capsule-site/
447
459
  - Node embedded Agent guide: https://xtrape-com.github.io/xtrape-capsule-site/agents/node-embedded-agent
448
460
  - Action model: https://xtrape-com.github.io/xtrape-capsule-site/agents/action-model
449
- - Opstage CE: https://xtrape-com.github.io/xtrape-capsule-site/opstage-ce/overview
461
+ - Xtrape Panel CE: https://xtrape-com.github.io/xtrape-capsule-site/opstage-ce/overview
450
462
 
451
463
  ## Contributing
452
464
 
@@ -456,10 +468,10 @@ safety guidance.
456
468
 
457
469
  ## License
458
470
 
459
- Apache-2.0. "Xtrape", "Xtrape Capsule", and "Opstage" are trademarks of their
471
+ Apache-2.0. "Xtrape" and "Opstage" are trademarks of their
460
472
  respective owners; the open-source license does not grant trademark rights.
461
473
 
462
- ## v0.4 Experimental Capsule Bus Hook
474
+ ## v0.4 Experimental legacy event routing Hook
463
475
 
464
476
  The SDK exposes a minimal experimental publish hook into CE's built-in SQLite-backed in-process event-to-command router:
465
477
 
@@ -470,7 +482,7 @@ await agent.publishBusEvent({
470
482
  });
471
483
  ```
472
484
 
473
- The hook posts to Opstage CE as the registered embedded agent and defaults `sourceServiceCode` to the configured service code.
485
+ The hook posts to Xtrape Panel CE as the registered embedded agent and defaults `sourceServiceCode` to the configured service code.
474
486
 
475
487
  Typed errors for Bus failure modes:
476
488
 
@@ -478,4 +490,4 @@ Typed errors for Bus failure modes:
478
490
  - `BusRateLimitedError` — `BUS_RATE_LIMITED` (429)
479
491
  - `BusDepthExceededError` — `BUS_DEPTH_EXCEEDED` (422)
480
492
 
481
- Capsule Bus is experimental in v0.4: not a standalone Bus Server, external broker, workflow engine, or service mesh API.
493
+ legacy event routing is experimental in v0.4: not a standalone Bus Server, external broker, workflow engine, or service mesh API.
package/dist/index.cjs CHANGED
@@ -27,9 +27,11 @@ __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
33
35
  });
34
36
  module.exports = __toCommonJS(index_exports);
35
37
 
@@ -136,13 +138,22 @@ var AgentApiClient = class {
136
138
  reportServices(agentId, token, req) {
137
139
  return this.request(`/api/agents/${agentId}/services/report`, { method: "POST", token, body: JSON.stringify(req) });
138
140
  }
141
+ /** Report Workers through the canonical endpoint, with a legacy fallback. */
142
+ async reportWorkers(agentId, token, req) {
143
+ try {
144
+ return await this.request(`/api/agents/${agentId}/workers/report`, { method: "POST", token, body: JSON.stringify(req) });
145
+ } catch (error) {
146
+ if (!(error instanceof AgentApiError) || error.status !== 404) throw error;
147
+ return this.reportServices(agentId, token, { services: req.workers });
148
+ }
149
+ }
139
150
  pollCommands(agentId, token) {
140
151
  return this.request(`/api/agents/${agentId}/commands`, { method: "GET", token });
141
152
  }
142
153
  reportResult(agentId, token, commandId, req) {
143
154
  return this.request(`/api/agents/${agentId}/commands/${commandId}/result`, { method: "POST", token, body: JSON.stringify(req) });
144
155
  }
145
- /** Experimental v0.4 Capsule Bus publish hook. Subject to change before v1.0. */
156
+ /** Legacy v0.4 event-routing compatibility hook. Subject to change before v1.0. */
146
157
  publishBusEvent(agentId, token, req) {
147
158
  return this.request(`/api/agents/${agentId}/bus/events`, { method: "POST", token, body: JSON.stringify({ ...req, experimental: req.experimental ?? "v0.4-experimental" }) });
148
159
  }
@@ -215,6 +226,12 @@ function emitLog(consoleSink, structured, level, message, fields) {
215
226
  var CapsuleAgent = class {
216
227
  constructor(options) {
217
228
  this.options = options;
229
+ if (options.worker && options.service) {
230
+ throw new Error("Agent options accept `worker` or legacy `service`, not both.");
231
+ }
232
+ if (!options.worker && !options.service) {
233
+ throw new Error("Agent options require `worker` (or legacy `service`).");
234
+ }
218
235
  this.client = new AgentApiClient(options.backendUrl);
219
236
  this.store = new FileTokenStore(options.tokenStore?.file);
220
237
  this.logger = options.logger ?? console;
@@ -248,7 +265,7 @@ var CapsuleAgent = class {
248
265
  this.stopped = false;
249
266
  try {
250
267
  await this.ensureRegistered();
251
- await this.reportService();
268
+ await this.reportWorker();
252
269
  await this.heartbeat();
253
270
  await this.pollOnce();
254
271
  } catch (e) {
@@ -257,7 +274,7 @@ var CapsuleAgent = class {
257
274
  }
258
275
  if (this.options.autoStartLoops !== false) {
259
276
  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());
277
+ this.loop("worker-report", this.options.intervals?.workerReportMs ?? this.options.intervals?.serviceReportMs ?? 6e4, () => this.reportWorker());
261
278
  this.loop("command-poll", this.options.intervals?.commandPollMs ?? (this.options.commandPollIntervalSeconds ?? 5) * 1e3, () => this.pollOnce());
262
279
  }
263
280
  }
@@ -286,38 +303,45 @@ var CapsuleAgent = class {
286
303
  if (this.agentId) return;
287
304
  }
288
305
  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 };
306
+ const worker = this.workerDefinition();
307
+ const agent = this.options.agent ?? { code: worker.code, name: worker.name, runtime: worker.runtime };
290
308
  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
309
  this.agentId = res.agentId;
292
310
  this.token = res.agentToken;
293
311
  await this.store.save(`${this.agentId}:${this.token}`);
294
312
  this.log("info", `registered agent ${this.agentId}`);
295
313
  }
314
+ workerDefinition() {
315
+ if (this.options.worker) {
316
+ return this.options.worker;
317
+ }
318
+ return this.options.service;
319
+ }
296
320
  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) };
321
+ const worker = this.workerDefinition();
322
+ 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 ?? {} };
323
+ 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
324
  }
300
325
  async runHealth() {
301
326
  return this.healthProvider();
302
327
  }
303
- /** Experimental v0.4 Capsule Bus publish hook. Subject to change before v1.0. */
328
+ /** Legacy v0.4 event-routing compatibility hook. Subject to change before v1.0. */
304
329
  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;
330
+ if (!this.agentId || !this.token) throw new Error("Agent must be started before publishing routed events.");
331
+ const sourceServiceCode = input.sourceServiceCode ?? this.workerDefinition().code;
307
332
  const result = await this.retry(() => this.client.publishBusEvent(this.agentId, this.token, { ...input, sourceServiceCode }));
308
333
  this.log("info", "experimental bus event published", { eventId: result.eventId, eventType: input.eventType, sourceServiceCode, routedCommands: result.routedCommands.length });
309
334
  return result;
310
335
  }
311
- async reportService() {
336
+ async reportWorker() {
312
337
  if (!this.agentId || !this.token) return;
313
338
  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] }));
339
+ const worker = { ...this.serviceSnapshot(), health, configs };
340
+ await this.retry(() => this.client.reportWorkers(this.agentId, this.token, { workers: [worker] }));
316
341
  }
317
342
  async heartbeat() {
318
343
  if (!this.agentId || !this.token) return;
319
- const health = await this.runHealth();
320
- await this.retry(() => this.client.heartbeat(this.agentId, this.token, { health }));
344
+ await this.retry(() => this.client.heartbeat(this.agentId, this.token, {}));
321
345
  }
322
346
  async pollOnce() {
323
347
  if (!this.agentId || !this.token) return;
@@ -403,6 +427,122 @@ var CapsuleAgent = class {
403
427
  return { value: obj };
404
428
  }
405
429
  };
430
+
431
+ // src/ce/index.ts
432
+ var import_node_crypto = require("crypto");
433
+ var import_capsule_contracts_node = require("@xtrape/capsule-contracts-node");
434
+ var CeServiceAgent = class {
435
+ serviceId;
436
+ instanceId;
437
+ serverUrl;
438
+ manifest;
439
+ address;
440
+ tenant;
441
+ fetchImpl;
442
+ health = "HEALTHY";
443
+ hbTimer;
444
+ constructor(opts) {
445
+ this.serverUrl = opts.serverUrl.replace(/\/+$/, "");
446
+ this.manifest = opts.manifest;
447
+ this.address = opts.address;
448
+ this.tenant = opts.manifest.tenant ?? "default";
449
+ this.serviceId = opts.manifest.service.id;
450
+ this.instanceId = opts.instanceId ?? `${this.serviceId}-${(0, import_node_crypto.randomUUID)().slice(0, 8)}`;
451
+ this.fetchImpl = opts.fetchImpl ?? fetch;
452
+ }
453
+ /** The ServiceInstance this agent reports on register/heartbeat. */
454
+ instance() {
455
+ return import_capsule_contracts_node.ServiceInstanceSchema.parse({
456
+ contractVersion: import_capsule_contracts_node.CE_CONTRACT_VERSION,
457
+ serviceId: this.serviceId,
458
+ instanceId: this.instanceId,
459
+ tenant: this.tenant,
460
+ address: this.address,
461
+ version: this.manifest.service.version,
462
+ health: this.health
463
+ });
464
+ }
465
+ /** POST /v1/instances { manifest, instance } */
466
+ async register() {
467
+ await this.post("/v1/instances", { manifest: this.manifest, instance: this.instance() });
468
+ return { instanceId: this.instanceId };
469
+ }
470
+ setHealth(h) {
471
+ this.health = h;
472
+ }
473
+ /** POST /v1/instances/{id}/heartbeat */
474
+ async sendHeartbeat(metricsSummary) {
475
+ const hb = {
476
+ contractVersion: import_capsule_contracts_node.CE_CONTRACT_VERSION,
477
+ serviceId: this.serviceId,
478
+ instanceId: this.instanceId,
479
+ tenant: this.tenant,
480
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
481
+ health: this.health,
482
+ ...metricsSummary ? { metricsSummary } : {}
483
+ };
484
+ await this.post(`/v1/instances/${encodeURIComponent(this.instanceId)}/heartbeat`, hb);
485
+ }
486
+ /** Start a periodic heartbeat; returns a stop function. Timer is unref'd. */
487
+ startHeartbeat(intervalMs = 1e4, metrics) {
488
+ this.stopHeartbeat();
489
+ this.hbTimer = setInterval(() => {
490
+ void this.sendHeartbeat(metrics?.()).catch(() => {
491
+ });
492
+ }, intervalMs);
493
+ this.hbTimer.unref?.();
494
+ return () => this.stopHeartbeat();
495
+ }
496
+ stopHeartbeat() {
497
+ if (this.hbTimer) {
498
+ clearInterval(this.hbTimer);
499
+ this.hbTimer = void 0;
500
+ }
501
+ }
502
+ /** DELETE /v1/instances/{id} — graceful deregister. */
503
+ async deregister() {
504
+ await this.fetchJson(`/v1/instances/${encodeURIComponent(this.instanceId)}`, { method: "DELETE" });
505
+ }
506
+ /**
507
+ * Validate an inbound message, run the handler, and build the reply message
508
+ * (senderType=SERVICE, targeted back at the sender, with replyToMessageId).
509
+ */
510
+ async handleMessage(raw, handler) {
511
+ const msg = import_capsule_contracts_node.ConversationMessageSchema.parse(raw);
512
+ const out = await handler(msg);
513
+ const reply = typeof out === "string" ? { content: out } : out;
514
+ return import_capsule_contracts_node.ConversationMessageSchema.parse({
515
+ contractVersion: import_capsule_contracts_node.CE_CONTRACT_VERSION,
516
+ messageId: `msg_${(0, import_node_crypto.randomUUID)().slice(0, 12)}`,
517
+ conversationId: msg.conversationId,
518
+ tenant: msg.tenant ?? "default",
519
+ // reply back to whoever sent it (USER/SERVICE/SYSTEM are all valid targets)
520
+ targetType: msg.senderType,
521
+ targetId: msg.senderId,
522
+ senderType: "SERVICE",
523
+ senderId: this.serviceId,
524
+ messageType: reply.messageType ?? "SUMMARY",
525
+ content: reply.content,
526
+ replyToMessageId: msg.messageId,
527
+ ...reply.payload ? { payload: reply.payload } : {}
528
+ });
529
+ }
530
+ post(path, body) {
531
+ return this.fetchJson(path, { method: "POST", body: JSON.stringify(body) });
532
+ }
533
+ async fetchJson(path, init) {
534
+ const res = await this.fetchImpl(this.serverUrl + path, {
535
+ ...init,
536
+ headers: { "content-type": "application/json", ...init.headers ?? {} }
537
+ });
538
+ const text = await res.text();
539
+ const data = text ? JSON.parse(text) : void 0;
540
+ if (!res.ok) {
541
+ throw new Error(`server-ce ${path} -> ${res.status}: ${text}`);
542
+ }
543
+ return data;
544
+ }
545
+ };
406
546
  // Annotate the CommonJS export names for ESM import in node:
407
547
  0 && (module.exports = {
408
548
  AgentApiClient,
@@ -412,7 +552,9 @@ var CapsuleAgent = class {
412
552
  BusDisabledError,
413
553
  BusRateLimitedError,
414
554
  CapsuleAgent,
555
+ CeServiceAgent,
415
556
  FileTokenStore,
416
557
  NetworkError,
417
- RegistrationError
558
+ RegistrationError,
559
+ XtrapeAgent
418
560
  });
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,67 @@ 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
+ /** injectable fetch (tests); defaults to global fetch */
281
+ fetchImpl?: typeof fetch;
282
+ }
283
+ type CeMessageReply = string | {
284
+ content: string;
285
+ messageType?: ConversationMessage["messageType"];
286
+ payload?: Record<string, unknown>;
287
+ };
288
+ /** Handler invoked with a validated inbound message; returns the reply content. */
289
+ type CeMessageHandler = (msg: ConversationMessage) => CeMessageReply | Promise<CeMessageReply>;
290
+ declare class CeServiceAgent {
291
+ readonly serviceId: string;
292
+ readonly instanceId: string;
293
+ private readonly serverUrl;
294
+ private readonly manifest;
295
+ private readonly address;
296
+ private readonly tenant;
297
+ private readonly fetchImpl;
298
+ private health;
299
+ private hbTimer?;
300
+ constructor(opts: CeServiceAgentOptions);
301
+ /** The ServiceInstance this agent reports on register/heartbeat. */
302
+ instance(): ServiceInstance;
303
+ /** POST /v1/instances { manifest, instance } */
304
+ register(): Promise<{
305
+ instanceId: string;
306
+ }>;
307
+ setHealth(h: ServiceInstanceHealth): void;
308
+ /** POST /v1/instances/{id}/heartbeat */
309
+ sendHeartbeat(metricsSummary?: Record<string, unknown>): Promise<void>;
310
+ /** Start a periodic heartbeat; returns a stop function. Timer is unref'd. */
311
+ startHeartbeat(intervalMs?: number, metrics?: () => Record<string, unknown>): () => void;
312
+ stopHeartbeat(): void;
313
+ /** DELETE /v1/instances/{id} — graceful deregister. */
314
+ deregister(): Promise<void>;
315
+ /**
316
+ * Validate an inbound message, run the handler, and build the reply message
317
+ * (senderType=SERVICE, targeted back at the sender, with replyToMessageId).
318
+ */
319
+ handleMessage(raw: unknown, handler: CeMessageHandler): Promise<ConversationMessage>;
320
+ private post;
321
+ private fetchJson;
322
+ }
323
+
324
+ 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 ServiceSnapshot, type StructuredLogger, type TokenStore, type WorkerAgentOptions, type WorkerDefinition, type WorkerSnapshot, CapsuleAgent as XtrapeAgent, type XtrapeAgentOptions };
package/dist/index.d.ts 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,67 @@ 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
+ /** injectable fetch (tests); defaults to global fetch */
281
+ fetchImpl?: typeof fetch;
282
+ }
283
+ type CeMessageReply = string | {
284
+ content: string;
285
+ messageType?: ConversationMessage["messageType"];
286
+ payload?: Record<string, unknown>;
287
+ };
288
+ /** Handler invoked with a validated inbound message; returns the reply content. */
289
+ type CeMessageHandler = (msg: ConversationMessage) => CeMessageReply | Promise<CeMessageReply>;
290
+ declare class CeServiceAgent {
291
+ readonly serviceId: string;
292
+ readonly instanceId: string;
293
+ private readonly serverUrl;
294
+ private readonly manifest;
295
+ private readonly address;
296
+ private readonly tenant;
297
+ private readonly fetchImpl;
298
+ private health;
299
+ private hbTimer?;
300
+ constructor(opts: CeServiceAgentOptions);
301
+ /** The ServiceInstance this agent reports on register/heartbeat. */
302
+ instance(): ServiceInstance;
303
+ /** POST /v1/instances { manifest, instance } */
304
+ register(): Promise<{
305
+ instanceId: string;
306
+ }>;
307
+ setHealth(h: ServiceInstanceHealth): void;
308
+ /** POST /v1/instances/{id}/heartbeat */
309
+ sendHeartbeat(metricsSummary?: Record<string, unknown>): Promise<void>;
310
+ /** Start a periodic heartbeat; returns a stop function. Timer is unref'd. */
311
+ startHeartbeat(intervalMs?: number, metrics?: () => Record<string, unknown>): () => void;
312
+ stopHeartbeat(): void;
313
+ /** DELETE /v1/instances/{id} — graceful deregister. */
314
+ deregister(): Promise<void>;
315
+ /**
316
+ * Validate an inbound message, run the handler, and build the reply message
317
+ * (senderType=SERVICE, targeted back at the sender, with replyToMessageId).
318
+ */
319
+ handleMessage(raw: unknown, handler: CeMessageHandler): Promise<ConversationMessage>;
320
+ private post;
321
+ private fetchJson;
322
+ }
323
+
324
+ 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 ServiceSnapshot, type StructuredLogger, type TokenStore, type WorkerAgentOptions, type WorkerDefinition, type WorkerSnapshot, CapsuleAgent as XtrapeAgent, type XtrapeAgentOptions };
package/dist/index.js CHANGED
@@ -101,13 +101,22 @@ var AgentApiClient = class {
101
101
  reportServices(agentId, token, req) {
102
102
  return this.request(`/api/agents/${agentId}/services/report`, { method: "POST", token, body: JSON.stringify(req) });
103
103
  }
104
+ /** Report Workers through the canonical endpoint, with a legacy fallback. */
105
+ async reportWorkers(agentId, token, req) {
106
+ try {
107
+ return await this.request(`/api/agents/${agentId}/workers/report`, { method: "POST", token, body: JSON.stringify(req) });
108
+ } catch (error) {
109
+ if (!(error instanceof AgentApiError) || error.status !== 404) throw error;
110
+ return this.reportServices(agentId, token, { services: req.workers });
111
+ }
112
+ }
104
113
  pollCommands(agentId, token) {
105
114
  return this.request(`/api/agents/${agentId}/commands`, { method: "GET", token });
106
115
  }
107
116
  reportResult(agentId, token, commandId, req) {
108
117
  return this.request(`/api/agents/${agentId}/commands/${commandId}/result`, { method: "POST", token, body: JSON.stringify(req) });
109
118
  }
110
- /** Experimental v0.4 Capsule Bus publish hook. Subject to change before v1.0. */
119
+ /** Legacy v0.4 event-routing compatibility hook. Subject to change before v1.0. */
111
120
  publishBusEvent(agentId, token, req) {
112
121
  return this.request(`/api/agents/${agentId}/bus/events`, { method: "POST", token, body: JSON.stringify({ ...req, experimental: req.experimental ?? "v0.4-experimental" }) });
113
122
  }
@@ -180,6 +189,12 @@ function emitLog(consoleSink, structured, level, message, fields) {
180
189
  var CapsuleAgent = class {
181
190
  constructor(options) {
182
191
  this.options = options;
192
+ if (options.worker && options.service) {
193
+ throw new Error("Agent options accept `worker` or legacy `service`, not both.");
194
+ }
195
+ if (!options.worker && !options.service) {
196
+ throw new Error("Agent options require `worker` (or legacy `service`).");
197
+ }
183
198
  this.client = new AgentApiClient(options.backendUrl);
184
199
  this.store = new FileTokenStore(options.tokenStore?.file);
185
200
  this.logger = options.logger ?? console;
@@ -213,7 +228,7 @@ var CapsuleAgent = class {
213
228
  this.stopped = false;
214
229
  try {
215
230
  await this.ensureRegistered();
216
- await this.reportService();
231
+ await this.reportWorker();
217
232
  await this.heartbeat();
218
233
  await this.pollOnce();
219
234
  } catch (e) {
@@ -222,7 +237,7 @@ var CapsuleAgent = class {
222
237
  }
223
238
  if (this.options.autoStartLoops !== false) {
224
239
  this.loop("heartbeat", this.options.intervals?.heartbeatMs ?? (this.options.heartbeatIntervalSeconds ?? 30) * 1e3, () => this.heartbeat());
225
- this.loop("service-report", this.options.intervals?.serviceReportMs ?? 6e4, () => this.reportService());
240
+ this.loop("worker-report", this.options.intervals?.workerReportMs ?? this.options.intervals?.serviceReportMs ?? 6e4, () => this.reportWorker());
226
241
  this.loop("command-poll", this.options.intervals?.commandPollMs ?? (this.options.commandPollIntervalSeconds ?? 5) * 1e3, () => this.pollOnce());
227
242
  }
228
243
  }
@@ -251,38 +266,45 @@ var CapsuleAgent = class {
251
266
  if (this.agentId) return;
252
267
  }
253
268
  if (!this.options.registrationToken) throw new Error("OPSTAGE registration token is required for first registration");
254
- const agent = this.options.agent ?? { code: this.options.service.code, name: this.options.service.name, runtime: this.options.service.runtime };
269
+ const worker = this.workerDefinition();
270
+ const agent = this.options.agent ?? { code: worker.code, name: worker.name, runtime: worker.runtime };
255
271
  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() }));
256
272
  this.agentId = res.agentId;
257
273
  this.token = res.agentToken;
258
274
  await this.store.save(`${this.agentId}:${this.token}`);
259
275
  this.log("info", `registered agent ${this.agentId}`);
260
276
  }
277
+ workerDefinition() {
278
+ if (this.options.worker) {
279
+ return this.options.worker;
280
+ }
281
+ return this.options.service;
282
+ }
261
283
  serviceSnapshot() {
262
- 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 ?? {} };
263
- 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) };
284
+ const worker = this.workerDefinition();
285
+ 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 ?? {} };
286
+ 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) };
264
287
  }
265
288
  async runHealth() {
266
289
  return this.healthProvider();
267
290
  }
268
- /** Experimental v0.4 Capsule Bus publish hook. Subject to change before v1.0. */
291
+ /** Legacy v0.4 event-routing compatibility hook. Subject to change before v1.0. */
269
292
  async publishBusEvent(input) {
270
- if (!this.agentId || !this.token) throw new Error("Agent must be started before publishing Capsule Bus events.");
271
- const sourceServiceCode = input.sourceServiceCode ?? this.options.service.code;
293
+ if (!this.agentId || !this.token) throw new Error("Agent must be started before publishing routed events.");
294
+ const sourceServiceCode = input.sourceServiceCode ?? this.workerDefinition().code;
272
295
  const result = await this.retry(() => this.client.publishBusEvent(this.agentId, this.token, { ...input, sourceServiceCode }));
273
296
  this.log("info", "experimental bus event published", { eventId: result.eventId, eventType: input.eventType, sourceServiceCode, routedCommands: result.routedCommands.length });
274
297
  return result;
275
298
  }
276
- async reportService() {
299
+ async reportWorker() {
277
300
  if (!this.agentId || !this.token) return;
278
301
  const [health, configs] = await Promise.all([this.runHealth(), this.configProvider()]);
279
- const svc = { ...this.serviceSnapshot(), health, configs };
280
- await this.retry(() => this.client.reportServices(this.agentId, this.token, { services: [svc] }));
302
+ const worker = { ...this.serviceSnapshot(), health, configs };
303
+ await this.retry(() => this.client.reportWorkers(this.agentId, this.token, { workers: [worker] }));
281
304
  }
282
305
  async heartbeat() {
283
306
  if (!this.agentId || !this.token) return;
284
- const health = await this.runHealth();
285
- await this.retry(() => this.client.heartbeat(this.agentId, this.token, { health }));
307
+ await this.retry(() => this.client.heartbeat(this.agentId, this.token, {}));
286
308
  }
287
309
  async pollOnce() {
288
310
  if (!this.agentId || !this.token) return;
@@ -368,6 +390,126 @@ var CapsuleAgent = class {
368
390
  return { value: obj };
369
391
  }
370
392
  };
393
+
394
+ // src/ce/index.ts
395
+ import { randomUUID } from "crypto";
396
+ import {
397
+ CE_CONTRACT_VERSION,
398
+ ServiceInstanceSchema,
399
+ ConversationMessageSchema
400
+ } from "@xtrape/capsule-contracts-node";
401
+ var CeServiceAgent = class {
402
+ serviceId;
403
+ instanceId;
404
+ serverUrl;
405
+ manifest;
406
+ address;
407
+ tenant;
408
+ fetchImpl;
409
+ health = "HEALTHY";
410
+ hbTimer;
411
+ constructor(opts) {
412
+ this.serverUrl = opts.serverUrl.replace(/\/+$/, "");
413
+ this.manifest = opts.manifest;
414
+ this.address = opts.address;
415
+ this.tenant = opts.manifest.tenant ?? "default";
416
+ this.serviceId = opts.manifest.service.id;
417
+ this.instanceId = opts.instanceId ?? `${this.serviceId}-${randomUUID().slice(0, 8)}`;
418
+ this.fetchImpl = opts.fetchImpl ?? fetch;
419
+ }
420
+ /** The ServiceInstance this agent reports on register/heartbeat. */
421
+ instance() {
422
+ return ServiceInstanceSchema.parse({
423
+ contractVersion: CE_CONTRACT_VERSION,
424
+ serviceId: this.serviceId,
425
+ instanceId: this.instanceId,
426
+ tenant: this.tenant,
427
+ address: this.address,
428
+ version: this.manifest.service.version,
429
+ health: this.health
430
+ });
431
+ }
432
+ /** POST /v1/instances { manifest, instance } */
433
+ async register() {
434
+ await this.post("/v1/instances", { manifest: this.manifest, instance: this.instance() });
435
+ return { instanceId: this.instanceId };
436
+ }
437
+ setHealth(h) {
438
+ this.health = h;
439
+ }
440
+ /** POST /v1/instances/{id}/heartbeat */
441
+ async sendHeartbeat(metricsSummary) {
442
+ const hb = {
443
+ contractVersion: CE_CONTRACT_VERSION,
444
+ serviceId: this.serviceId,
445
+ instanceId: this.instanceId,
446
+ tenant: this.tenant,
447
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
448
+ health: this.health,
449
+ ...metricsSummary ? { metricsSummary } : {}
450
+ };
451
+ await this.post(`/v1/instances/${encodeURIComponent(this.instanceId)}/heartbeat`, hb);
452
+ }
453
+ /** Start a periodic heartbeat; returns a stop function. Timer is unref'd. */
454
+ startHeartbeat(intervalMs = 1e4, metrics) {
455
+ this.stopHeartbeat();
456
+ this.hbTimer = setInterval(() => {
457
+ void this.sendHeartbeat(metrics?.()).catch(() => {
458
+ });
459
+ }, intervalMs);
460
+ this.hbTimer.unref?.();
461
+ return () => this.stopHeartbeat();
462
+ }
463
+ stopHeartbeat() {
464
+ if (this.hbTimer) {
465
+ clearInterval(this.hbTimer);
466
+ this.hbTimer = void 0;
467
+ }
468
+ }
469
+ /** DELETE /v1/instances/{id} — graceful deregister. */
470
+ async deregister() {
471
+ await this.fetchJson(`/v1/instances/${encodeURIComponent(this.instanceId)}`, { method: "DELETE" });
472
+ }
473
+ /**
474
+ * Validate an inbound message, run the handler, and build the reply message
475
+ * (senderType=SERVICE, targeted back at the sender, with replyToMessageId).
476
+ */
477
+ async handleMessage(raw, handler) {
478
+ const msg = ConversationMessageSchema.parse(raw);
479
+ const out = await handler(msg);
480
+ const reply = typeof out === "string" ? { content: out } : out;
481
+ return ConversationMessageSchema.parse({
482
+ contractVersion: CE_CONTRACT_VERSION,
483
+ messageId: `msg_${randomUUID().slice(0, 12)}`,
484
+ conversationId: msg.conversationId,
485
+ tenant: msg.tenant ?? "default",
486
+ // reply back to whoever sent it (USER/SERVICE/SYSTEM are all valid targets)
487
+ targetType: msg.senderType,
488
+ targetId: msg.senderId,
489
+ senderType: "SERVICE",
490
+ senderId: this.serviceId,
491
+ messageType: reply.messageType ?? "SUMMARY",
492
+ content: reply.content,
493
+ replyToMessageId: msg.messageId,
494
+ ...reply.payload ? { payload: reply.payload } : {}
495
+ });
496
+ }
497
+ post(path, body) {
498
+ return this.fetchJson(path, { method: "POST", body: JSON.stringify(body) });
499
+ }
500
+ async fetchJson(path, init) {
501
+ const res = await this.fetchImpl(this.serverUrl + path, {
502
+ ...init,
503
+ headers: { "content-type": "application/json", ...init.headers ?? {} }
504
+ });
505
+ const text = await res.text();
506
+ const data = text ? JSON.parse(text) : void 0;
507
+ if (!res.ok) {
508
+ throw new Error(`server-ce ${path} -> ${res.status}: ${text}`);
509
+ }
510
+ return data;
511
+ }
512
+ };
371
513
  export {
372
514
  AgentApiClient,
373
515
  AgentApiError,
@@ -376,7 +518,9 @@ export {
376
518
  BusDisabledError,
377
519
  BusRateLimitedError,
378
520
  CapsuleAgent,
521
+ CeServiceAgent,
379
522
  FileTokenStore,
380
523
  NetworkError,
381
- RegistrationError
524
+ RegistrationError,
525
+ CapsuleAgent as XtrapeAgent
382
526
  };
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@xtrape/capsule-agent-node",
3
- "version": "0.4.0",
4
- "description": "Node.js Agent SDK for embedding Capsule Services into the Opstage governance loop.",
3
+ "version": "0.5.0",
4
+ "description": "Node.js Embedded Agent SDK for reporting Workers to the Xtrape control plane.",
5
5
  "keywords": [
6
6
  "xtrape",
7
- "capsule",
7
+ "worker",
8
+ "panel",
8
9
  "opstage",
9
10
  "agent-sdk",
10
11
  "automation",
@@ -39,11 +40,11 @@
39
40
  "build": "tsup src/index.ts --format esm,cjs --dts",
40
41
  "prepack": "pnpm build",
41
42
  "test": "vitest run",
42
- "typecheck": "tsc --noEmit",
43
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.type-tests.json",
43
44
  "lint": "tsc --noEmit"
44
45
  },
45
46
  "dependencies": {
46
- "@xtrape/capsule-contracts-node": "^0.4.0"
47
+ "@xtrape/capsule-contracts-node": "^0.5.0"
47
48
  },
48
49
  "devDependencies": {
49
50
  "tsup": "^8.3.5",