@xtrape/capsule-agent-node 0.1.0-public-review.1 → 0.3.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/README.md CHANGED
@@ -6,10 +6,7 @@
6
6
  [![Status: Public Review](https://img.shields.io/badge/status-Public%20Review-orange.svg)](https://xtrape-com.github.io/xtrape-capsule-site/)
7
7
  [![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
8
 
9
- `@xtrape/capsule-agent-node` embeds an Opstage Agent inside a Node.js service.
10
- The Agent registers with Opstage CE, persists its Agent token, sends heartbeats,
11
- reports service metadata, exposes configs and health, and polls Commands for
12
- operator-triggered Actions.
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).
13
10
 
14
11
  > **Package status:** Xtrape Capsule is currently in **Public Review** before
15
12
  > the `v0.1.0 Public Preview` release. This package is published under the
@@ -26,6 +23,10 @@ pnpm add @xtrape/capsule-agent-node@public-review
26
23
 
27
24
  The current Public Review version may change before `v0.1.0`.
28
25
 
26
+ During v0.3 development, this repository may use a local workspace, GitHub
27
+ branch dependency, or `0.3.0-rc.x` package for
28
+ `@xtrape/capsule-contracts-node` until the final `0.3.0` package is published.
29
+
29
30
  For this repository itself:
30
31
 
31
32
  ```bash
@@ -285,7 +286,8 @@ Main exports:
285
286
  - `CapsuleAgent`
286
287
  - `FileTokenStore`
287
288
  - `AgentApiClient`
288
- - `AgentApiError`
289
+ - `AgentApiError`, `RegistrationError`, `AgentAuthError`, `NetworkError`
290
+ - `AgentLogLevel`, `AgentLogRecord`, `StructuredLogger`, `ConsoleLogger` (types)
289
291
  - SDK option and provider types from `types.ts`
290
292
 
291
293
  Core methods:
@@ -300,6 +302,90 @@ Core methods:
300
302
  | `.stop()` | Stops background loops. |
301
303
  | `.runHealth()` | Runs the configured health provider. |
302
304
 
305
+ ### Typed errors
306
+
307
+ > Added in `0.2.0`. Pre-`0.2` code that catches `AgentApiError` still works
308
+ > because the new classes extend it.
309
+
310
+ The SDK surfaces four error classes so callers can branch on the failure
311
+ mode without parsing messages:
312
+
313
+ | Class | When it's thrown | Should the caller retry? |
314
+ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
315
+ | `RegistrationError` | `/api/agents/register` rejects the one-time token (expired, revoked, already used, malformed). Needs operator action — mint a fresh registration token. | No. |
316
+ | `AgentAuthError` | Any non-register call returns 401/403. The agent token has been revoked or the agent disabled. Re-register before resuming. | No. |
317
+ | `NetworkError` | `fetch()` threw before getting an HTTP response (DNS failure, ECONNREFUSED, abort, timeout). `status === 0`; carries an optional `cause`. | Yes — SDK already does. |
318
+ | `AgentApiError` | Catch-all parent. Direct instances carry HTTP 4xx/5xx that don't fit the more specific cases (e.g. 409 conflict, 422 validation, 5xx server error). | Only on 5xx. |
319
+
320
+ The SDK's internal `retry()` loop already implements this policy: only
321
+ `NetworkError` and 5xx `AgentApiError` are retried with exponential backoff;
322
+ 4xx — including the two typed subclasses — surfaces immediately so the caller
323
+ isn't waiting on a backoff that can never succeed.
324
+
325
+ ```ts
326
+ import {
327
+ CapsuleAgent,
328
+ RegistrationError,
329
+ AgentAuthError,
330
+ NetworkError,
331
+ } from "@xtrape/capsule-agent-node";
332
+
333
+ try {
334
+ await agent.start();
335
+ } catch (err) {
336
+ if (err instanceof RegistrationError) {
337
+ console.error("registration token is no longer valid:", err.code);
338
+ process.exit(64);
339
+ }
340
+ if (err instanceof AgentAuthError) {
341
+ console.error("agent token rejected — was the agent revoked?");
342
+ process.exit(65);
343
+ }
344
+ if (err instanceof NetworkError) {
345
+ console.error("could not reach Opstage:", err.message);
346
+ process.exit(75);
347
+ }
348
+ throw err;
349
+ }
350
+ ```
351
+
352
+ ### Structured logger
353
+
354
+ > Added in `0.2.0`. Existing zero-config callers (no `logger` / no
355
+ > `structuredLogger`) keep using the global `console` exactly as before.
356
+
357
+ By default the SDK calls `console.debug` / `.info` / `.warn` / `.error`. For
358
+ JSON-line logging, pino, OpenTelemetry, or any in-house collector, pass a
359
+ `structuredLogger`:
360
+
361
+ ```ts
362
+ import { CapsuleAgent, type StructuredLogger } from "@xtrape/capsule-agent-node";
363
+
364
+ const collector: StructuredLogger = {
365
+ log({ level, message, fields }) {
366
+ process.stdout.write(JSON.stringify({ t: Date.now(), level, message, ...fields }) + "\n");
367
+ },
368
+ };
369
+
370
+ const agent = new CapsuleAgent({
371
+ // ...
372
+ structuredLogger: collector,
373
+ });
374
+ ```
375
+
376
+ Each record arrives as `{ level: "debug" | "info" | "warn" | "error",
377
+ message: string, fields?: Record<string, unknown> }`. The SDK normalizes the
378
+ optional context before emitting:
379
+
380
+ - `Error` instances become `{ error: { name, message, stack } }` so they
381
+ survive `JSON.stringify`.
382
+ - Primitives become `{ value: <primitive> }`.
383
+ - Plain objects pass through unchanged.
384
+
385
+ In all cases the redactor runs over `fields` first, so tokens and
386
+ secret-bearing keys never reach the collector. If both `structuredLogger`
387
+ and `logger` are configured, `structuredLogger` wins.
388
+
303
389
  ## Version Compatibility
304
390
 
305
391
  | Package | Compatible with |
package/dist/index.cjs CHANGED
@@ -22,21 +22,53 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  AgentApiClient: () => AgentApiClient,
24
24
  AgentApiError: () => AgentApiError,
25
+ AgentAuthError: () => AgentAuthError,
25
26
  CapsuleAgent: () => CapsuleAgent,
26
- FileTokenStore: () => FileTokenStore
27
+ FileTokenStore: () => FileTokenStore,
28
+ NetworkError: () => NetworkError,
29
+ RegistrationError: () => RegistrationError
27
30
  });
28
31
  module.exports = __toCommonJS(index_exports);
29
32
 
30
33
  // src/client/errors.ts
31
34
  var AgentApiError = class extends Error {
32
- constructor(status, message, body) {
35
+ constructor(status, message, body, code) {
33
36
  super(message);
34
37
  this.status = status;
35
38
  this.body = body;
39
+ this.code = code;
40
+ this.name = "AgentApiError";
36
41
  }
37
42
  status;
38
43
  body;
44
+ code;
39
45
  };
46
+ var RegistrationError = class extends AgentApiError {
47
+ constructor(message, status, body, code) {
48
+ super(status, message, body, code);
49
+ this.name = "RegistrationError";
50
+ }
51
+ };
52
+ var AgentAuthError = class extends AgentApiError {
53
+ constructor(message, status, body, code) {
54
+ super(status, message, body, code);
55
+ this.name = "AgentAuthError";
56
+ }
57
+ };
58
+ var NetworkError = class extends AgentApiError {
59
+ constructor(message, cause) {
60
+ super(0, message, void 0, "NETWORK_ERROR");
61
+ this.name = "NetworkError";
62
+ if (cause !== void 0) {
63
+ this.cause = cause;
64
+ }
65
+ }
66
+ };
67
+ function classifyAgentApiError(status, message, body, code, path) {
68
+ if (status === 401 && path.endsWith("/register")) return new RegistrationError(message, status, body, code);
69
+ if (status === 401 || status === 403) return new AgentAuthError(message, status, body, code);
70
+ return new AgentApiError(status, message, body, code);
71
+ }
40
72
 
41
73
  // src/client/agent-api-client.ts
42
74
  var AgentApiClient = class {
@@ -51,10 +83,24 @@ var AgentApiClient = class {
51
83
  const headers = new Headers(init.headers);
52
84
  headers.set("content-type", "application/json");
53
85
  if (init.token) headers.set("authorization", `Bearer ${init.token}`);
54
- const res = await fetch(this.url(path), { ...init, headers });
86
+ let res;
87
+ try {
88
+ res = await fetch(this.url(path), { ...init, headers });
89
+ } catch (cause) {
90
+ const reason = cause instanceof Error ? cause.message : String(cause);
91
+ throw new NetworkError(`Failed to reach ${path}: ${reason}`, cause);
92
+ }
55
93
  const text = await res.text();
56
- const body = text ? JSON.parse(text) : void 0;
57
- if (!res.ok) throw new AgentApiError(res.status, body?.error?.message ?? res.statusText, body);
94
+ let body;
95
+ try {
96
+ body = text ? JSON.parse(text) : void 0;
97
+ } catch {
98
+ body = { error: { message: res.statusText } };
99
+ }
100
+ if (!res.ok) {
101
+ const err = body?.error;
102
+ throw classifyAgentApiError(res.status, err?.message ?? res.statusText, body, err?.code, path);
103
+ }
58
104
  return body?.data ?? body;
59
105
  }
60
106
  register(req) {
@@ -130,17 +176,27 @@ function redact(value) {
130
176
 
131
177
  // src/capsule-agent.ts
132
178
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
179
+ function emitLog(consoleSink, structured, level, message, fields) {
180
+ if (structured) {
181
+ structured.log({ level, message, fields });
182
+ return;
183
+ }
184
+ if (fields === void 0) consoleSink[level](message);
185
+ else consoleSink[level](message, fields);
186
+ }
133
187
  var CapsuleAgent = class {
134
188
  constructor(options) {
135
189
  this.options = options;
136
190
  this.client = new AgentApiClient(options.backendUrl);
137
191
  this.store = new FileTokenStore(options.tokenStore?.file);
138
192
  this.logger = options.logger ?? console;
193
+ this.structuredLogger = options.structuredLogger;
139
194
  }
140
195
  options;
141
196
  client;
142
197
  store;
143
198
  logger;
199
+ structuredLogger;
144
200
  agentId;
145
201
  token;
146
202
  stopped = true;
@@ -288,20 +344,36 @@ var CapsuleAgent = class {
288
344
  return await fn();
289
345
  } catch (e) {
290
346
  last = e;
291
- if (e instanceof AgentApiError && e.status < 500) throw e;
347
+ if (e instanceof NetworkError) {
348
+ } else if (e instanceof AgentApiError && e.status >= 500) {
349
+ } else {
350
+ throw e;
351
+ }
292
352
  await sleep(Math.min(1e3 * 2 ** i, 15e3));
293
353
  }
294
354
  }
295
355
  throw last;
296
356
  }
297
357
  log(level, msg, obj) {
298
- this.logger[level](msg, obj ? redact(obj) : void 0);
358
+ const fields = obj === void 0 ? void 0 : this.normalizeFields(obj);
359
+ const redacted = fields ? redact(fields) : void 0;
360
+ emitLog(this.logger, this.structuredLogger, level, msg, redacted);
361
+ }
362
+ normalizeFields(obj) {
363
+ if (obj instanceof Error) {
364
+ return { error: { message: obj.message, name: obj.name, stack: obj.stack } };
365
+ }
366
+ if (typeof obj === "object" && obj !== null) return obj;
367
+ return { value: obj };
299
368
  }
300
369
  };
301
370
  // Annotate the CommonJS export names for ESM import in node:
302
371
  0 && (module.exports = {
303
372
  AgentApiClient,
304
373
  AgentApiError,
374
+ AgentAuthError,
305
375
  CapsuleAgent,
306
- FileTokenStore
376
+ FileTokenStore,
377
+ NetworkError,
378
+ RegistrationError
307
379
  });
package/dist/index.d.cts CHANGED
@@ -1,12 +1,5 @@
1
- import { HealthReportInput, ConfigItemInput, ActionDefinitionInput, ReportedService, RegisterAgentRequest, AgentHeartbeatRequest, ServiceReportRequest, ReportCommandResultRequest } from '@xtrape/capsule-contracts-node';
1
+ import { ActionPrepareResult, RuntimeKind, HealthReportInput, ConfigItemInput, ActionDefinitionInput, ReportedService, RegisterAgentRequest, AgentHeartbeatRequest, ServiceReportRequest, ReportCommandResultRequest } from '@xtrape/capsule-contracts-node';
2
2
 
3
- type RuntimeKind = "nodejs" | "java" | "python" | "go" | "other";
4
- type ActionPrepareResult = {
5
- action?: Record<string, unknown>;
6
- inputSchema?: Record<string, unknown>;
7
- initialPayload?: Record<string, unknown>;
8
- currentState?: Record<string, unknown>;
9
- };
10
3
  type AgentMode = "embedded";
11
4
  type ActionHandler = (payload: Record<string, unknown>) => Promise<{
12
5
  success?: boolean;
@@ -15,6 +8,36 @@ type ActionHandler = (payload: Record<string, unknown>) => Promise<{
15
8
  error?: Record<string, unknown>;
16
9
  } | Record<string, unknown>>;
17
10
  type ActionPrepareHandler = () => Promise<ActionPrepareResult | Record<string, unknown>> | ActionPrepareResult | Record<string, unknown>;
11
+ /**
12
+ * Log level emitted by the SDK. Aligns with the four standard `Console`
13
+ * methods so a plain `console` works as a logger with no adapter.
14
+ */
15
+ type AgentLogLevel = "debug" | "info" | "warn" | "error";
16
+ /**
17
+ * Structured log record passed to a `StructuredLogger`. The SDK populates
18
+ * `level` and `message`; `fields` carries an already-redacted Record of
19
+ * runtime context (agent id, error code, retry count, etc.).
20
+ */
21
+ interface AgentLogRecord {
22
+ level: AgentLogLevel;
23
+ message: string;
24
+ fields?: Record<string, unknown>;
25
+ }
26
+ /**
27
+ * A structured log sink. Useful when the host wants to forward SDK events
28
+ * into a JSON-line logger, pino, OpenTelemetry, or a custom collector
29
+ * without losing the per-field context to `console.warn`'s positional
30
+ * argument list.
31
+ */
32
+ interface StructuredLogger {
33
+ log(record: AgentLogRecord): void;
34
+ }
35
+ /**
36
+ * Console-shaped logger (subset of Node's `Console`). The SDK defaults to
37
+ * the global `console` when neither option is set; if both are provided,
38
+ * `structuredLogger` wins.
39
+ */
40
+ type ConsoleLogger = Pick<Console, "debug" | "info" | "warn" | "error">;
18
41
  interface CapsuleAgentOptions {
19
42
  backendUrl: string;
20
43
  registrationToken?: string;
@@ -42,7 +65,14 @@ interface CapsuleAgentOptions {
42
65
  heartbeatIntervalSeconds?: number;
43
66
  commandPollIntervalSeconds?: number;
44
67
  autoStartLoops?: boolean;
45
- logger?: Pick<Console, "debug" | "info" | "warn" | "error">;
68
+ /** Console-shaped sink; default is the global `console`. */
69
+ logger?: ConsoleLogger;
70
+ /**
71
+ * Structured sink. If provided, takes precedence over `logger`. The SDK
72
+ * still applies its own redaction before calling `log()`, so the host
73
+ * can route records as-is to a JSON stream.
74
+ */
75
+ structuredLogger?: StructuredLogger;
46
76
  failOnStartError?: boolean;
47
77
  }
48
78
  type HealthProvider = () => Promise<HealthReportInput> | HealthReportInput;
@@ -63,6 +93,7 @@ declare class CapsuleAgent {
63
93
  private readonly client;
64
94
  private readonly store;
65
95
  private readonly logger;
96
+ private readonly structuredLogger?;
66
97
  private agentId?;
67
98
  private token?;
68
99
  private stopped;
@@ -89,6 +120,7 @@ declare class CapsuleAgent {
89
120
  private withTimeout;
90
121
  private retry;
91
122
  private log;
123
+ private normalizeFields;
92
124
  }
93
125
 
94
126
  declare class FileTokenStore implements TokenStore {
@@ -99,6 +131,47 @@ declare class FileTokenStore implements TokenStore {
99
131
  clear(): Promise<void>;
100
132
  }
101
133
 
134
+ /**
135
+ * Base class for any failure that crosses the Agent ↔ Backend wire. Use
136
+ * `instanceof AgentApiError` to catch the family, and the more specific
137
+ * subclasses below when the caller wants to branch on the failure mode.
138
+ *
139
+ * `status === 0` is reserved for transport failures that never reached the
140
+ * backend (DNS failure, ECONNREFUSED, network timeout, etc.); use
141
+ * `NetworkError` for those.
142
+ */
143
+ declare class AgentApiError extends Error {
144
+ readonly status: number;
145
+ readonly body?: unknown | undefined;
146
+ readonly code?: string | undefined;
147
+ constructor(status: number, message: string, body?: unknown | undefined, code?: string | undefined);
148
+ }
149
+ /**
150
+ * The backend rejected the registration token (expired, revoked, already
151
+ * used, malformed). The agent cannot proceed without operator action: a
152
+ * fresh registration token must be created in Opstage.
153
+ */
154
+ declare class RegistrationError extends AgentApiError {
155
+ constructor(message: string, status: number, body?: unknown, code?: string);
156
+ }
157
+ /**
158
+ * The backend rejected the persisted agent token (UNAUTHORIZED, agent
159
+ * REVOKED, agent DISABLED). The agent must be re-registered with a fresh
160
+ * registration token before it can resume.
161
+ */
162
+ declare class AgentAuthError extends AgentApiError {
163
+ constructor(message: string, status: number, body?: unknown, code?: string);
164
+ }
165
+ /**
166
+ * Transport-level failure: the request did not reach the backend, or the
167
+ * backend's response was not a parseable HTTP envelope. The SDK retries
168
+ * these with exponential backoff; surface to the caller only after the
169
+ * retry budget is exhausted.
170
+ */
171
+ declare class NetworkError extends AgentApiError {
172
+ constructor(message: string, cause?: unknown);
173
+ }
174
+
102
175
  declare class AgentApiClient {
103
176
  private readonly backendUrl;
104
177
  constructor(backendUrl: string);
@@ -113,7 +186,7 @@ declare class AgentApiClient {
113
186
  heartbeat(agentId: string, token: string, req: AgentHeartbeatRequest): Promise<unknown>;
114
187
  reportServices(agentId: string, token: string, req: ServiceReportRequest): Promise<unknown>;
115
188
  pollCommands(agentId: string, token: string): Promise<{
116
- type: "ACTION_PREPARE" | "ACTION_EXECUTE" | "ACTION";
189
+ type: "ACTION_PREPARE" | "ACTION_EXECUTE";
117
190
  id: string;
118
191
  createdAt: string;
119
192
  status: "PENDING" | "RUNNING" | "SUCCEEDED" | "FAILED" | "EXPIRED" | "CANCELLED";
@@ -129,10 +202,4 @@ declare class AgentApiClient {
129
202
  reportResult(agentId: string, token: string, commandId: string, req: ReportCommandResultRequest): Promise<unknown>;
130
203
  }
131
204
 
132
- declare class AgentApiError extends Error {
133
- readonly status: number;
134
- readonly body?: unknown | undefined;
135
- constructor(status: number, message: string, body?: unknown | undefined);
136
- }
137
-
138
- export { type ActionHandler, type ActionPrepareHandler, type ActionPrepareResult, AgentApiClient, AgentApiError, type AgentMode, CapsuleAgent, type CapsuleAgentOptions, type ConfigProvider, FileTokenStore, type HealthProvider, type RegisteredAction, type RuntimeKind, type ServiceSnapshot, type TokenStore };
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 };
package/dist/index.d.ts CHANGED
@@ -1,12 +1,5 @@
1
- import { HealthReportInput, ConfigItemInput, ActionDefinitionInput, ReportedService, RegisterAgentRequest, AgentHeartbeatRequest, ServiceReportRequest, ReportCommandResultRequest } from '@xtrape/capsule-contracts-node';
1
+ import { ActionPrepareResult, RuntimeKind, HealthReportInput, ConfigItemInput, ActionDefinitionInput, ReportedService, RegisterAgentRequest, AgentHeartbeatRequest, ServiceReportRequest, ReportCommandResultRequest } from '@xtrape/capsule-contracts-node';
2
2
 
3
- type RuntimeKind = "nodejs" | "java" | "python" | "go" | "other";
4
- type ActionPrepareResult = {
5
- action?: Record<string, unknown>;
6
- inputSchema?: Record<string, unknown>;
7
- initialPayload?: Record<string, unknown>;
8
- currentState?: Record<string, unknown>;
9
- };
10
3
  type AgentMode = "embedded";
11
4
  type ActionHandler = (payload: Record<string, unknown>) => Promise<{
12
5
  success?: boolean;
@@ -15,6 +8,36 @@ type ActionHandler = (payload: Record<string, unknown>) => Promise<{
15
8
  error?: Record<string, unknown>;
16
9
  } | Record<string, unknown>>;
17
10
  type ActionPrepareHandler = () => Promise<ActionPrepareResult | Record<string, unknown>> | ActionPrepareResult | Record<string, unknown>;
11
+ /**
12
+ * Log level emitted by the SDK. Aligns with the four standard `Console`
13
+ * methods so a plain `console` works as a logger with no adapter.
14
+ */
15
+ type AgentLogLevel = "debug" | "info" | "warn" | "error";
16
+ /**
17
+ * Structured log record passed to a `StructuredLogger`. The SDK populates
18
+ * `level` and `message`; `fields` carries an already-redacted Record of
19
+ * runtime context (agent id, error code, retry count, etc.).
20
+ */
21
+ interface AgentLogRecord {
22
+ level: AgentLogLevel;
23
+ message: string;
24
+ fields?: Record<string, unknown>;
25
+ }
26
+ /**
27
+ * A structured log sink. Useful when the host wants to forward SDK events
28
+ * into a JSON-line logger, pino, OpenTelemetry, or a custom collector
29
+ * without losing the per-field context to `console.warn`'s positional
30
+ * argument list.
31
+ */
32
+ interface StructuredLogger {
33
+ log(record: AgentLogRecord): void;
34
+ }
35
+ /**
36
+ * Console-shaped logger (subset of Node's `Console`). The SDK defaults to
37
+ * the global `console` when neither option is set; if both are provided,
38
+ * `structuredLogger` wins.
39
+ */
40
+ type ConsoleLogger = Pick<Console, "debug" | "info" | "warn" | "error">;
18
41
  interface CapsuleAgentOptions {
19
42
  backendUrl: string;
20
43
  registrationToken?: string;
@@ -42,7 +65,14 @@ interface CapsuleAgentOptions {
42
65
  heartbeatIntervalSeconds?: number;
43
66
  commandPollIntervalSeconds?: number;
44
67
  autoStartLoops?: boolean;
45
- logger?: Pick<Console, "debug" | "info" | "warn" | "error">;
68
+ /** Console-shaped sink; default is the global `console`. */
69
+ logger?: ConsoleLogger;
70
+ /**
71
+ * Structured sink. If provided, takes precedence over `logger`. The SDK
72
+ * still applies its own redaction before calling `log()`, so the host
73
+ * can route records as-is to a JSON stream.
74
+ */
75
+ structuredLogger?: StructuredLogger;
46
76
  failOnStartError?: boolean;
47
77
  }
48
78
  type HealthProvider = () => Promise<HealthReportInput> | HealthReportInput;
@@ -63,6 +93,7 @@ declare class CapsuleAgent {
63
93
  private readonly client;
64
94
  private readonly store;
65
95
  private readonly logger;
96
+ private readonly structuredLogger?;
66
97
  private agentId?;
67
98
  private token?;
68
99
  private stopped;
@@ -89,6 +120,7 @@ declare class CapsuleAgent {
89
120
  private withTimeout;
90
121
  private retry;
91
122
  private log;
123
+ private normalizeFields;
92
124
  }
93
125
 
94
126
  declare class FileTokenStore implements TokenStore {
@@ -99,6 +131,47 @@ declare class FileTokenStore implements TokenStore {
99
131
  clear(): Promise<void>;
100
132
  }
101
133
 
134
+ /**
135
+ * Base class for any failure that crosses the Agent ↔ Backend wire. Use
136
+ * `instanceof AgentApiError` to catch the family, and the more specific
137
+ * subclasses below when the caller wants to branch on the failure mode.
138
+ *
139
+ * `status === 0` is reserved for transport failures that never reached the
140
+ * backend (DNS failure, ECONNREFUSED, network timeout, etc.); use
141
+ * `NetworkError` for those.
142
+ */
143
+ declare class AgentApiError extends Error {
144
+ readonly status: number;
145
+ readonly body?: unknown | undefined;
146
+ readonly code?: string | undefined;
147
+ constructor(status: number, message: string, body?: unknown | undefined, code?: string | undefined);
148
+ }
149
+ /**
150
+ * The backend rejected the registration token (expired, revoked, already
151
+ * used, malformed). The agent cannot proceed without operator action: a
152
+ * fresh registration token must be created in Opstage.
153
+ */
154
+ declare class RegistrationError extends AgentApiError {
155
+ constructor(message: string, status: number, body?: unknown, code?: string);
156
+ }
157
+ /**
158
+ * The backend rejected the persisted agent token (UNAUTHORIZED, agent
159
+ * REVOKED, agent DISABLED). The agent must be re-registered with a fresh
160
+ * registration token before it can resume.
161
+ */
162
+ declare class AgentAuthError extends AgentApiError {
163
+ constructor(message: string, status: number, body?: unknown, code?: string);
164
+ }
165
+ /**
166
+ * Transport-level failure: the request did not reach the backend, or the
167
+ * backend's response was not a parseable HTTP envelope. The SDK retries
168
+ * these with exponential backoff; surface to the caller only after the
169
+ * retry budget is exhausted.
170
+ */
171
+ declare class NetworkError extends AgentApiError {
172
+ constructor(message: string, cause?: unknown);
173
+ }
174
+
102
175
  declare class AgentApiClient {
103
176
  private readonly backendUrl;
104
177
  constructor(backendUrl: string);
@@ -113,7 +186,7 @@ declare class AgentApiClient {
113
186
  heartbeat(agentId: string, token: string, req: AgentHeartbeatRequest): Promise<unknown>;
114
187
  reportServices(agentId: string, token: string, req: ServiceReportRequest): Promise<unknown>;
115
188
  pollCommands(agentId: string, token: string): Promise<{
116
- type: "ACTION_PREPARE" | "ACTION_EXECUTE" | "ACTION";
189
+ type: "ACTION_PREPARE" | "ACTION_EXECUTE";
117
190
  id: string;
118
191
  createdAt: string;
119
192
  status: "PENDING" | "RUNNING" | "SUCCEEDED" | "FAILED" | "EXPIRED" | "CANCELLED";
@@ -129,10 +202,4 @@ declare class AgentApiClient {
129
202
  reportResult(agentId: string, token: string, commandId: string, req: ReportCommandResultRequest): Promise<unknown>;
130
203
  }
131
204
 
132
- declare class AgentApiError extends Error {
133
- readonly status: number;
134
- readonly body?: unknown | undefined;
135
- constructor(status: number, message: string, body?: unknown | undefined);
136
- }
137
-
138
- export { type ActionHandler, type ActionPrepareHandler, type ActionPrepareResult, AgentApiClient, AgentApiError, type AgentMode, CapsuleAgent, type CapsuleAgentOptions, type ConfigProvider, FileTokenStore, type HealthProvider, type RegisteredAction, type RuntimeKind, type ServiceSnapshot, type TokenStore };
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 };
package/dist/index.js CHANGED
@@ -1,13 +1,42 @@
1
1
  // src/client/errors.ts
2
2
  var AgentApiError = class extends Error {
3
- constructor(status, message, body) {
3
+ constructor(status, message, body, code) {
4
4
  super(message);
5
5
  this.status = status;
6
6
  this.body = body;
7
+ this.code = code;
8
+ this.name = "AgentApiError";
7
9
  }
8
10
  status;
9
11
  body;
12
+ code;
10
13
  };
14
+ var RegistrationError = class extends AgentApiError {
15
+ constructor(message, status, body, code) {
16
+ super(status, message, body, code);
17
+ this.name = "RegistrationError";
18
+ }
19
+ };
20
+ var AgentAuthError = class extends AgentApiError {
21
+ constructor(message, status, body, code) {
22
+ super(status, message, body, code);
23
+ this.name = "AgentAuthError";
24
+ }
25
+ };
26
+ var NetworkError = class extends AgentApiError {
27
+ constructor(message, cause) {
28
+ super(0, message, void 0, "NETWORK_ERROR");
29
+ this.name = "NetworkError";
30
+ if (cause !== void 0) {
31
+ this.cause = cause;
32
+ }
33
+ }
34
+ };
35
+ function classifyAgentApiError(status, message, body, code, path) {
36
+ if (status === 401 && path.endsWith("/register")) return new RegistrationError(message, status, body, code);
37
+ if (status === 401 || status === 403) return new AgentAuthError(message, status, body, code);
38
+ return new AgentApiError(status, message, body, code);
39
+ }
11
40
 
12
41
  // src/client/agent-api-client.ts
13
42
  var AgentApiClient = class {
@@ -22,10 +51,24 @@ var AgentApiClient = class {
22
51
  const headers = new Headers(init.headers);
23
52
  headers.set("content-type", "application/json");
24
53
  if (init.token) headers.set("authorization", `Bearer ${init.token}`);
25
- const res = await fetch(this.url(path), { ...init, headers });
54
+ let res;
55
+ try {
56
+ res = await fetch(this.url(path), { ...init, headers });
57
+ } catch (cause) {
58
+ const reason = cause instanceof Error ? cause.message : String(cause);
59
+ throw new NetworkError(`Failed to reach ${path}: ${reason}`, cause);
60
+ }
26
61
  const text = await res.text();
27
- const body = text ? JSON.parse(text) : void 0;
28
- if (!res.ok) throw new AgentApiError(res.status, body?.error?.message ?? res.statusText, body);
62
+ let body;
63
+ try {
64
+ body = text ? JSON.parse(text) : void 0;
65
+ } catch {
66
+ body = { error: { message: res.statusText } };
67
+ }
68
+ if (!res.ok) {
69
+ const err = body?.error;
70
+ throw classifyAgentApiError(res.status, err?.message ?? res.statusText, body, err?.code, path);
71
+ }
29
72
  return body?.data ?? body;
30
73
  }
31
74
  register(req) {
@@ -101,17 +144,27 @@ function redact(value) {
101
144
 
102
145
  // src/capsule-agent.ts
103
146
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
147
+ function emitLog(consoleSink, structured, level, message, fields) {
148
+ if (structured) {
149
+ structured.log({ level, message, fields });
150
+ return;
151
+ }
152
+ if (fields === void 0) consoleSink[level](message);
153
+ else consoleSink[level](message, fields);
154
+ }
104
155
  var CapsuleAgent = class {
105
156
  constructor(options) {
106
157
  this.options = options;
107
158
  this.client = new AgentApiClient(options.backendUrl);
108
159
  this.store = new FileTokenStore(options.tokenStore?.file);
109
160
  this.logger = options.logger ?? console;
161
+ this.structuredLogger = options.structuredLogger;
110
162
  }
111
163
  options;
112
164
  client;
113
165
  store;
114
166
  logger;
167
+ structuredLogger;
115
168
  agentId;
116
169
  token;
117
170
  stopped = true;
@@ -259,19 +312,35 @@ var CapsuleAgent = class {
259
312
  return await fn();
260
313
  } catch (e) {
261
314
  last = e;
262
- if (e instanceof AgentApiError && e.status < 500) throw e;
315
+ if (e instanceof NetworkError) {
316
+ } else if (e instanceof AgentApiError && e.status >= 500) {
317
+ } else {
318
+ throw e;
319
+ }
263
320
  await sleep(Math.min(1e3 * 2 ** i, 15e3));
264
321
  }
265
322
  }
266
323
  throw last;
267
324
  }
268
325
  log(level, msg, obj) {
269
- this.logger[level](msg, obj ? redact(obj) : void 0);
326
+ const fields = obj === void 0 ? void 0 : this.normalizeFields(obj);
327
+ const redacted = fields ? redact(fields) : void 0;
328
+ emitLog(this.logger, this.structuredLogger, level, msg, redacted);
329
+ }
330
+ normalizeFields(obj) {
331
+ if (obj instanceof Error) {
332
+ return { error: { message: obj.message, name: obj.name, stack: obj.stack } };
333
+ }
334
+ if (typeof obj === "object" && obj !== null) return obj;
335
+ return { value: obj };
270
336
  }
271
337
  };
272
338
  export {
273
339
  AgentApiClient,
274
340
  AgentApiError,
341
+ AgentAuthError,
275
342
  CapsuleAgent,
276
- FileTokenStore
343
+ FileTokenStore,
344
+ NetworkError,
345
+ RegistrationError
277
346
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xtrape/capsule-agent-node",
3
- "version": "0.1.0-public-review.1",
4
- "description": "Node.js Agent SDK for embedding Capsule Services into the Opstage governance loop.",
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).",
5
5
  "keywords": [
6
6
  "xtrape",
7
7
  "capsule",
@@ -43,13 +43,13 @@
43
43
  "lint": "tsc --noEmit"
44
44
  },
45
45
  "dependencies": {
46
- "@xtrape/capsule-contracts-node": "^0.1.0-public-review.0"
46
+ "@xtrape/capsule-contracts-node": "0.3.0"
47
47
  },
48
48
  "devDependencies": {
49
+ "@types/node": "^22.10.2",
49
50
  "tsup": "^8.3.5",
50
51
  "typescript": "^5.6.3",
51
- "vitest": "^2.1.9",
52
- "@types/node": "^22.10.2"
52
+ "vitest": "^2.1.9"
53
53
  },
54
54
  "engines": {
55
55
  "node": ">=20"