@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 +3 -3
- package/README.md +76 -43
- package/dist/index.cjs +193 -12
- package/dist/index.d.cts +133 -14
- package/dist/index.d.ts +133 -14
- package/dist/index.js +191 -11
- package/package.json +8 -7
package/NOTICE
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
Copyright 2026 Xtrape
|
|
1
|
+
Xtrape Embedded Agent SDK for Node.js
|
|
2
|
+
Copyright 2026 Xtrape contributors
|
|
3
3
|
|
|
4
|
-
This product includes software developed by the Xtrape
|
|
4
|
+
This product includes software developed by the Xtrape contributors.
|
package/README.md
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Xtrape Embedded Agent SDK for Node.js
|
|
2
2
|
|
|
3
|
-
> Node.js
|
|
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)
|
|
6
11
|
[](https://xtrape-com.github.io/xtrape-capsule-site/)
|
|
7
12
|
[](https://xtrape-com.github.io/xtrape-capsule-site/agents/node-embedded-agent)
|
|
8
13
|
|
|
9
|
-
|
|
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
|
|
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
|
|
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 {
|
|
51
|
+
import { XtrapeAgent } from "@xtrape/capsule-agent-node";
|
|
45
52
|
|
|
46
|
-
const agent = new
|
|
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
|
|
51
|
-
// Provide it explicitly when one Agent owns multiple
|
|
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-
|
|
56
|
-
name: "Hello
|
|
62
|
+
code: "hello-worker-agent",
|
|
63
|
+
name: "Hello Worker Agent",
|
|
57
64
|
runtime: "nodejs",
|
|
58
65
|
},
|
|
59
|
-
|
|
60
|
-
code: "hello-
|
|
61
|
-
name: "Hello
|
|
66
|
+
worker: {
|
|
67
|
+
code: "hello-worker",
|
|
68
|
+
name: "Hello Worker",
|
|
62
69
|
version: "0.1.0",
|
|
63
70
|
runtime: "nodejs",
|
|
64
|
-
description: "Minimal
|
|
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
|
|
116
|
-
|
|
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.
|
|
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
|
-
##
|
|
135
|
+
## Worker Manifest
|
|
127
136
|
|
|
128
|
-
The `
|
|
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
|
-
|
|
132
|
-
code: "hello-
|
|
133
|
-
name: "Hello
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
190
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
233
|
-
|
|
|
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
|
-
|
|
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
|
|
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
|
|
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` |
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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"
|
|
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.
|
|
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("
|
|
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
|
|
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
|
|
270
|
-
|
|
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
|
-
|
|
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
|
|
279
|
-
await this.retry(() => this.client.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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: "
|
|
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
|
|
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
|
-
|
|
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
|
|
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: "
|
|
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.
|
|
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("
|
|
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
|
|
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
|
|
238
|
-
|
|
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
|
-
|
|
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
|
|
247
|
-
await this.retry(() => this.client.
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "Node.js Embedded Agent SDK for
|
|
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
|
-
"
|
|
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.
|
|
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"
|