@xtrape/capsule-agent-node 0.3.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,5 +468,26 @@ 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.
473
+
474
+ ## v0.4 Experimental legacy event routing Hook
475
+
476
+ The SDK exposes a minimal experimental publish hook into CE's built-in SQLite-backed in-process event-to-command router:
477
+
478
+ ```ts
479
+ await agent.publishBusEvent({
480
+ eventType: "demo.item.created",
481
+ payload: { itemId: "item-1" },
482
+ });
483
+ ```
484
+
485
+ The hook posts to Xtrape Panel CE as the registered embedded agent and defaults `sourceServiceCode` to the configured service code.
486
+
487
+ Typed errors for Bus failure modes:
488
+
489
+ - `BusDisabledError` — `CAPSULE_BUS_DISABLED` (404)
490
+ - `BusRateLimitedError` — `BUS_RATE_LIMITED` (429)
491
+ - `BusDepthExceededError` — `BUS_DEPTH_EXCEEDED` (422)
492
+
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
@@ -23,10 +23,15 @@ __export(index_exports, {
23
23
  AgentApiClient: () => AgentApiClient,
24
24
  AgentApiError: () => AgentApiError,
25
25
  AgentAuthError: () => AgentAuthError,
26
+ BusDepthExceededError: () => BusDepthExceededError,
27
+ BusDisabledError: () => BusDisabledError,
28
+ BusRateLimitedError: () => BusRateLimitedError,
26
29
  CapsuleAgent: () => CapsuleAgent,
30
+ CeServiceAgent: () => CeServiceAgent,
27
31
  FileTokenStore: () => FileTokenStore,
28
32
  NetworkError: () => NetworkError,
29
- RegistrationError: () => RegistrationError
33
+ RegistrationError: () => RegistrationError,
34
+ XtrapeAgent: () => CapsuleAgent
30
35
  });
31
36
  module.exports = __toCommonJS(index_exports);
32
37
 
@@ -64,9 +69,30 @@ var NetworkError = class extends AgentApiError {
64
69
  }
65
70
  }
66
71
  };
72
+ var BusDisabledError = class extends AgentApiError {
73
+ constructor(message, status, body) {
74
+ super(status, message, body, "CAPSULE_BUS_DISABLED");
75
+ this.name = "BusDisabledError";
76
+ }
77
+ };
78
+ var BusRateLimitedError = class extends AgentApiError {
79
+ constructor(message, status, body) {
80
+ super(status, message, body, "BUS_RATE_LIMITED");
81
+ this.name = "BusRateLimitedError";
82
+ }
83
+ };
84
+ var BusDepthExceededError = class extends AgentApiError {
85
+ constructor(message, status, body) {
86
+ super(status, message, body, "BUS_DEPTH_EXCEEDED");
87
+ this.name = "BusDepthExceededError";
88
+ }
89
+ };
67
90
  function classifyAgentApiError(status, message, body, code, path) {
68
91
  if (status === 401 && path.endsWith("/register")) return new RegistrationError(message, status, body, code);
69
92
  if (status === 401 || status === 403) return new AgentAuthError(message, status, body, code);
93
+ if (code === "CAPSULE_BUS_DISABLED") return new BusDisabledError(message, status, body);
94
+ if (code === "BUS_RATE_LIMITED") return new BusRateLimitedError(message, status, body);
95
+ if (code === "BUS_DEPTH_EXCEEDED") return new BusDepthExceededError(message, status, body);
70
96
  return new AgentApiError(status, message, body, code);
71
97
  }
72
98
 
@@ -112,12 +138,25 @@ var AgentApiClient = class {
112
138
  reportServices(agentId, token, req) {
113
139
  return this.request(`/api/agents/${agentId}/services/report`, { method: "POST", token, body: JSON.stringify(req) });
114
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
+ }
115
150
  pollCommands(agentId, token) {
116
151
  return this.request(`/api/agents/${agentId}/commands`, { method: "GET", token });
117
152
  }
118
153
  reportResult(agentId, token, commandId, req) {
119
154
  return this.request(`/api/agents/${agentId}/commands/${commandId}/result`, { method: "POST", token, body: JSON.stringify(req) });
120
155
  }
156
+ /** Legacy v0.4 event-routing compatibility hook. Subject to change before v1.0. */
157
+ publishBusEvent(agentId, token, req) {
158
+ return this.request(`/api/agents/${agentId}/bus/events`, { method: "POST", token, body: JSON.stringify({ ...req, experimental: req.experimental ?? "v0.4-experimental" }) });
159
+ }
121
160
  };
122
161
 
123
162
  // src/token-store/file-token-store.ts
@@ -187,6 +226,12 @@ function emitLog(consoleSink, structured, level, message, fields) {
187
226
  var CapsuleAgent = class {
188
227
  constructor(options) {
189
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
+ }
190
235
  this.client = new AgentApiClient(options.backendUrl);
191
236
  this.store = new FileTokenStore(options.tokenStore?.file);
192
237
  this.logger = options.logger ?? console;
@@ -220,7 +265,7 @@ var CapsuleAgent = class {
220
265
  this.stopped = false;
221
266
  try {
222
267
  await this.ensureRegistered();
223
- await this.reportService();
268
+ await this.reportWorker();
224
269
  await this.heartbeat();
225
270
  await this.pollOnce();
226
271
  } catch (e) {
@@ -229,7 +274,7 @@ var CapsuleAgent = class {
229
274
  }
230
275
  if (this.options.autoStartLoops !== false) {
231
276
  this.loop("heartbeat", this.options.intervals?.heartbeatMs ?? (this.options.heartbeatIntervalSeconds ?? 30) * 1e3, () => this.heartbeat());
232
- 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());
233
278
  this.loop("command-poll", this.options.intervals?.commandPollMs ?? (this.options.commandPollIntervalSeconds ?? 5) * 1e3, () => this.pollOnce());
234
279
  }
235
280
  }
@@ -258,30 +303,45 @@ var CapsuleAgent = class {
258
303
  if (this.agentId) return;
259
304
  }
260
305
  if (!this.options.registrationToken) throw new Error("OPSTAGE registration token is required for first registration");
261
- 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 };
262
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() }));
263
309
  this.agentId = res.agentId;
264
310
  this.token = res.agentToken;
265
311
  await this.store.save(`${this.agentId}:${this.token}`);
266
312
  this.log("info", `registered agent ${this.agentId}`);
267
313
  }
314
+ workerDefinition() {
315
+ if (this.options.worker) {
316
+ return this.options.worker;
317
+ }
318
+ return this.options.service;
319
+ }
268
320
  serviceSnapshot() {
269
- 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 ?? {} };
270
- 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) };
271
324
  }
272
325
  async runHealth() {
273
326
  return this.healthProvider();
274
327
  }
275
- async reportService() {
328
+ /** Legacy v0.4 event-routing compatibility hook. Subject to change before v1.0. */
329
+ async publishBusEvent(input) {
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;
332
+ const result = await this.retry(() => this.client.publishBusEvent(this.agentId, this.token, { ...input, sourceServiceCode }));
333
+ this.log("info", "experimental bus event published", { eventId: result.eventId, eventType: input.eventType, sourceServiceCode, routedCommands: result.routedCommands.length });
334
+ return result;
335
+ }
336
+ async reportWorker() {
276
337
  if (!this.agentId || !this.token) return;
277
338
  const [health, configs] = await Promise.all([this.runHealth(), this.configProvider()]);
278
- const svc = { ...this.serviceSnapshot(), health, configs };
279
- 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] }));
280
341
  }
281
342
  async heartbeat() {
282
343
  if (!this.agentId || !this.token) return;
283
- const health = await this.runHealth();
284
- await this.retry(() => this.client.heartbeat(this.agentId, this.token, { health }));
344
+ await this.retry(() => this.client.heartbeat(this.agentId, this.token, {}));
285
345
  }
286
346
  async pollOnce() {
287
347
  if (!this.agentId || !this.token) return;
@@ -367,13 +427,134 @@ var CapsuleAgent = class {
367
427
  return { value: obj };
368
428
  }
369
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
+ };
370
546
  // Annotate the CommonJS export names for ESM import in node:
371
547
  0 && (module.exports = {
372
548
  AgentApiClient,
373
549
  AgentApiError,
374
550
  AgentAuthError,
551
+ BusDepthExceededError,
552
+ BusDisabledError,
553
+ BusRateLimitedError,
375
554
  CapsuleAgent,
555
+ CeServiceAgent,
376
556
  FileTokenStore,
377
557
  NetworkError,
378
- RegistrationError
558
+ RegistrationError,
559
+ XtrapeAgent
379
560
  });
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { ActionPrepareResult, RuntimeKind, HealthReportInput, ConfigItemInput, ActionDefinitionInput, 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,6 +103,14 @@ interface TokenStore {
87
103
  clear(): Promise<void>;
88
104
  }
89
105
  type ServiceSnapshot = ReportedService;
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"> & {
110
+ sourceServiceCode?: string;
111
+ experimental?: PublishBusEventRequest["experimental"];
112
+ };
113
+ type PublishBusEventResult = PublishBusEventResponse;
90
114
 
91
115
  declare class CapsuleAgent {
92
116
  private readonly options;
@@ -109,9 +133,12 @@ declare class CapsuleAgent {
109
133
  stop(): Promise<void>;
110
134
  private loop;
111
135
  private ensureRegistered;
136
+ private workerDefinition;
112
137
  private serviceSnapshot;
113
138
  runHealth(): Promise<HealthReportInput>;
114
- private reportService;
139
+ /** Legacy v0.4 event-routing compatibility hook. Subject to change before v1.0. */
140
+ publishBusEvent(input: PublishBusEventInput): Promise<PublishBusEventResult>;
141
+ private reportWorker;
115
142
  private heartbeat;
116
143
  private pollOnce;
117
144
  private execute;
@@ -149,7 +176,8 @@ declare class AgentApiError extends Error {
149
176
  /**
150
177
  * The backend rejected the registration token (expired, revoked, already
151
178
  * used, malformed). The agent cannot proceed without operator action: a
152
- * 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.
153
181
  */
154
182
  declare class RegistrationError extends AgentApiError {
155
183
  constructor(message: string, status: number, body?: unknown, code?: string);
@@ -171,6 +199,15 @@ declare class AgentAuthError extends AgentApiError {
171
199
  declare class NetworkError extends AgentApiError {
172
200
  constructor(message: string, cause?: unknown);
173
201
  }
202
+ declare class BusDisabledError extends AgentApiError {
203
+ constructor(message: string, status: number, body?: unknown);
204
+ }
205
+ declare class BusRateLimitedError extends AgentApiError {
206
+ constructor(message: string, status: number, body?: unknown);
207
+ }
208
+ declare class BusDepthExceededError extends AgentApiError {
209
+ constructor(message: string, status: number, body?: unknown);
210
+ }
174
211
 
175
212
  declare class AgentApiClient {
176
213
  private readonly backendUrl;
@@ -185,8 +222,12 @@ declare class AgentApiClient {
185
222
  }>;
186
223
  heartbeat(agentId: string, token: string, req: AgentHeartbeatRequest): Promise<unknown>;
187
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>;
188
229
  pollCommands(agentId: string, token: string): Promise<{
189
- type: "ACTION_PREPARE" | "ACTION_EXECUTE";
230
+ type: "ACTION_EXECUTE" | "ACTION_PREPARE";
190
231
  id: string;
191
232
  createdAt: string;
192
233
  status: "PENDING" | "RUNNING" | "SUCCEEDED" | "FAILED" | "EXPIRED" | "CANCELLED";
@@ -200,6 +241,84 @@ declare class AgentApiClient {
200
241
  completedAt?: string | null | undefined;
201
242
  }[]>;
202
243
  reportResult(agentId: string, token: string, commandId: string, req: ReportCommandResultRequest): Promise<unknown>;
244
+ /** Legacy v0.4 event-routing compatibility hook. Subject to change before v1.0. */
245
+ publishBusEvent(agentId: string, token: string, req: PublishBusEventInput): Promise<{
246
+ experimental: "v0.4-experimental";
247
+ eventId: string;
248
+ routedCommands: {
249
+ status: "FAILED" | "DRY_RUN" | "CREATED" | "SKIPPED";
250
+ dryRun: boolean;
251
+ actionName: string;
252
+ routeId: string;
253
+ targetServiceCode: string;
254
+ commandId?: string | undefined;
255
+ errorCode?: string | undefined;
256
+ errorMessage?: string | undefined;
257
+ }[];
258
+ }>;
259
+ }
260
+
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;
203
322
  }
204
323
 
205
- export { type ActionHandler, type ActionPrepareHandler, AgentApiClient, AgentApiError, AgentAuthError, type AgentLogLevel, type AgentLogRecord, type AgentMode, CapsuleAgent, type CapsuleAgentOptions, type ConfigProvider, type ConsoleLogger, FileTokenStore, type HealthProvider, NetworkError, type RegisteredAction, RegistrationError, type ServiceSnapshot, type StructuredLogger, type TokenStore };
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, RuntimeKind, HealthReportInput, ConfigItemInput, ActionDefinitionInput, 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,6 +103,14 @@ interface TokenStore {
87
103
  clear(): Promise<void>;
88
104
  }
89
105
  type ServiceSnapshot = ReportedService;
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"> & {
110
+ sourceServiceCode?: string;
111
+ experimental?: PublishBusEventRequest["experimental"];
112
+ };
113
+ type PublishBusEventResult = PublishBusEventResponse;
90
114
 
91
115
  declare class CapsuleAgent {
92
116
  private readonly options;
@@ -109,9 +133,12 @@ declare class CapsuleAgent {
109
133
  stop(): Promise<void>;
110
134
  private loop;
111
135
  private ensureRegistered;
136
+ private workerDefinition;
112
137
  private serviceSnapshot;
113
138
  runHealth(): Promise<HealthReportInput>;
114
- private reportService;
139
+ /** Legacy v0.4 event-routing compatibility hook. Subject to change before v1.0. */
140
+ publishBusEvent(input: PublishBusEventInput): Promise<PublishBusEventResult>;
141
+ private reportWorker;
115
142
  private heartbeat;
116
143
  private pollOnce;
117
144
  private execute;
@@ -149,7 +176,8 @@ declare class AgentApiError extends Error {
149
176
  /**
150
177
  * The backend rejected the registration token (expired, revoked, already
151
178
  * used, malformed). The agent cannot proceed without operator action: a
152
- * 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.
153
181
  */
154
182
  declare class RegistrationError extends AgentApiError {
155
183
  constructor(message: string, status: number, body?: unknown, code?: string);
@@ -171,6 +199,15 @@ declare class AgentAuthError extends AgentApiError {
171
199
  declare class NetworkError extends AgentApiError {
172
200
  constructor(message: string, cause?: unknown);
173
201
  }
202
+ declare class BusDisabledError extends AgentApiError {
203
+ constructor(message: string, status: number, body?: unknown);
204
+ }
205
+ declare class BusRateLimitedError extends AgentApiError {
206
+ constructor(message: string, status: number, body?: unknown);
207
+ }
208
+ declare class BusDepthExceededError extends AgentApiError {
209
+ constructor(message: string, status: number, body?: unknown);
210
+ }
174
211
 
175
212
  declare class AgentApiClient {
176
213
  private readonly backendUrl;
@@ -185,8 +222,12 @@ declare class AgentApiClient {
185
222
  }>;
186
223
  heartbeat(agentId: string, token: string, req: AgentHeartbeatRequest): Promise<unknown>;
187
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>;
188
229
  pollCommands(agentId: string, token: string): Promise<{
189
- type: "ACTION_PREPARE" | "ACTION_EXECUTE";
230
+ type: "ACTION_EXECUTE" | "ACTION_PREPARE";
190
231
  id: string;
191
232
  createdAt: string;
192
233
  status: "PENDING" | "RUNNING" | "SUCCEEDED" | "FAILED" | "EXPIRED" | "CANCELLED";
@@ -200,6 +241,84 @@ declare class AgentApiClient {
200
241
  completedAt?: string | null | undefined;
201
242
  }[]>;
202
243
  reportResult(agentId: string, token: string, commandId: string, req: ReportCommandResultRequest): Promise<unknown>;
244
+ /** Legacy v0.4 event-routing compatibility hook. Subject to change before v1.0. */
245
+ publishBusEvent(agentId: string, token: string, req: PublishBusEventInput): Promise<{
246
+ experimental: "v0.4-experimental";
247
+ eventId: string;
248
+ routedCommands: {
249
+ status: "FAILED" | "DRY_RUN" | "CREATED" | "SKIPPED";
250
+ dryRun: boolean;
251
+ actionName: string;
252
+ routeId: string;
253
+ targetServiceCode: string;
254
+ commandId?: string | undefined;
255
+ errorCode?: string | undefined;
256
+ errorMessage?: string | undefined;
257
+ }[];
258
+ }>;
259
+ }
260
+
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;
203
322
  }
204
323
 
205
- export { type ActionHandler, type ActionPrepareHandler, AgentApiClient, AgentApiError, AgentAuthError, type AgentLogLevel, type AgentLogRecord, type AgentMode, CapsuleAgent, type CapsuleAgentOptions, type ConfigProvider, type ConsoleLogger, FileTokenStore, type HealthProvider, NetworkError, type RegisteredAction, RegistrationError, type ServiceSnapshot, type StructuredLogger, type TokenStore };
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
@@ -32,9 +32,30 @@ var NetworkError = class extends AgentApiError {
32
32
  }
33
33
  }
34
34
  };
35
+ var BusDisabledError = class extends AgentApiError {
36
+ constructor(message, status, body) {
37
+ super(status, message, body, "CAPSULE_BUS_DISABLED");
38
+ this.name = "BusDisabledError";
39
+ }
40
+ };
41
+ var BusRateLimitedError = class extends AgentApiError {
42
+ constructor(message, status, body) {
43
+ super(status, message, body, "BUS_RATE_LIMITED");
44
+ this.name = "BusRateLimitedError";
45
+ }
46
+ };
47
+ var BusDepthExceededError = class extends AgentApiError {
48
+ constructor(message, status, body) {
49
+ super(status, message, body, "BUS_DEPTH_EXCEEDED");
50
+ this.name = "BusDepthExceededError";
51
+ }
52
+ };
35
53
  function classifyAgentApiError(status, message, body, code, path) {
36
54
  if (status === 401 && path.endsWith("/register")) return new RegistrationError(message, status, body, code);
37
55
  if (status === 401 || status === 403) return new AgentAuthError(message, status, body, code);
56
+ if (code === "CAPSULE_BUS_DISABLED") return new BusDisabledError(message, status, body);
57
+ if (code === "BUS_RATE_LIMITED") return new BusRateLimitedError(message, status, body);
58
+ if (code === "BUS_DEPTH_EXCEEDED") return new BusDepthExceededError(message, status, body);
38
59
  return new AgentApiError(status, message, body, code);
39
60
  }
40
61
 
@@ -80,12 +101,25 @@ var AgentApiClient = class {
80
101
  reportServices(agentId, token, req) {
81
102
  return this.request(`/api/agents/${agentId}/services/report`, { method: "POST", token, body: JSON.stringify(req) });
82
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
+ }
83
113
  pollCommands(agentId, token) {
84
114
  return this.request(`/api/agents/${agentId}/commands`, { method: "GET", token });
85
115
  }
86
116
  reportResult(agentId, token, commandId, req) {
87
117
  return this.request(`/api/agents/${agentId}/commands/${commandId}/result`, { method: "POST", token, body: JSON.stringify(req) });
88
118
  }
119
+ /** Legacy v0.4 event-routing compatibility hook. Subject to change before v1.0. */
120
+ publishBusEvent(agentId, token, req) {
121
+ return this.request(`/api/agents/${agentId}/bus/events`, { method: "POST", token, body: JSON.stringify({ ...req, experimental: req.experimental ?? "v0.4-experimental" }) });
122
+ }
89
123
  };
90
124
 
91
125
  // src/token-store/file-token-store.ts
@@ -155,6 +189,12 @@ function emitLog(consoleSink, structured, level, message, fields) {
155
189
  var CapsuleAgent = class {
156
190
  constructor(options) {
157
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
+ }
158
198
  this.client = new AgentApiClient(options.backendUrl);
159
199
  this.store = new FileTokenStore(options.tokenStore?.file);
160
200
  this.logger = options.logger ?? console;
@@ -188,7 +228,7 @@ var CapsuleAgent = class {
188
228
  this.stopped = false;
189
229
  try {
190
230
  await this.ensureRegistered();
191
- await this.reportService();
231
+ await this.reportWorker();
192
232
  await this.heartbeat();
193
233
  await this.pollOnce();
194
234
  } catch (e) {
@@ -197,7 +237,7 @@ var CapsuleAgent = class {
197
237
  }
198
238
  if (this.options.autoStartLoops !== false) {
199
239
  this.loop("heartbeat", this.options.intervals?.heartbeatMs ?? (this.options.heartbeatIntervalSeconds ?? 30) * 1e3, () => this.heartbeat());
200
- 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());
201
241
  this.loop("command-poll", this.options.intervals?.commandPollMs ?? (this.options.commandPollIntervalSeconds ?? 5) * 1e3, () => this.pollOnce());
202
242
  }
203
243
  }
@@ -226,30 +266,45 @@ var CapsuleAgent = class {
226
266
  if (this.agentId) return;
227
267
  }
228
268
  if (!this.options.registrationToken) throw new Error("OPSTAGE registration token is required for first registration");
229
- 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 };
230
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() }));
231
272
  this.agentId = res.agentId;
232
273
  this.token = res.agentToken;
233
274
  await this.store.save(`${this.agentId}:${this.token}`);
234
275
  this.log("info", `registered agent ${this.agentId}`);
235
276
  }
277
+ workerDefinition() {
278
+ if (this.options.worker) {
279
+ return this.options.worker;
280
+ }
281
+ return this.options.service;
282
+ }
236
283
  serviceSnapshot() {
237
- 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 ?? {} };
238
- 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) };
239
287
  }
240
288
  async runHealth() {
241
289
  return this.healthProvider();
242
290
  }
243
- async reportService() {
291
+ /** Legacy v0.4 event-routing compatibility hook. Subject to change before v1.0. */
292
+ async publishBusEvent(input) {
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;
295
+ const result = await this.retry(() => this.client.publishBusEvent(this.agentId, this.token, { ...input, sourceServiceCode }));
296
+ this.log("info", "experimental bus event published", { eventId: result.eventId, eventType: input.eventType, sourceServiceCode, routedCommands: result.routedCommands.length });
297
+ return result;
298
+ }
299
+ async reportWorker() {
244
300
  if (!this.agentId || !this.token) return;
245
301
  const [health, configs] = await Promise.all([this.runHealth(), this.configProvider()]);
246
- const svc = { ...this.serviceSnapshot(), health, configs };
247
- 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] }));
248
304
  }
249
305
  async heartbeat() {
250
306
  if (!this.agentId || !this.token) return;
251
- const health = await this.runHealth();
252
- await this.retry(() => this.client.heartbeat(this.agentId, this.token, { health }));
307
+ await this.retry(() => this.client.heartbeat(this.agentId, this.token, {}));
253
308
  }
254
309
  async pollOnce() {
255
310
  if (!this.agentId || !this.token) return;
@@ -335,12 +390,137 @@ var CapsuleAgent = class {
335
390
  return { value: obj };
336
391
  }
337
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
+ };
338
513
  export {
339
514
  AgentApiClient,
340
515
  AgentApiError,
341
516
  AgentAuthError,
517
+ BusDepthExceededError,
518
+ BusDisabledError,
519
+ BusRateLimitedError,
342
520
  CapsuleAgent,
521
+ CeServiceAgent,
343
522
  FileTokenStore,
344
523
  NetworkError,
345
- RegistrationError
524
+ RegistrationError,
525
+ CapsuleAgent as XtrapeAgent
346
526
  };
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@xtrape/capsule-agent-node",
3
- "version": "0.3.0",
4
- "description": "Node.js Embedded Agent SDK for embedding single Capsule Services into the Opstage governance loop. For multi-service OpHub functionality, use OpHub (Go runtime).",
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,17 +40,17 @@
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.3.0"
47
+ "@xtrape/capsule-contracts-node": "^0.5.0"
47
48
  },
48
49
  "devDependencies": {
49
- "@types/node": "^22.10.2",
50
50
  "tsup": "^8.3.5",
51
51
  "typescript": "^5.6.3",
52
- "vitest": "^2.1.9"
52
+ "vitest": "^2.1.9",
53
+ "@types/node": "^22.10.2"
53
54
  },
54
55
  "engines": {
55
56
  "node": ">=20"