@xtrape/capsule-agent-node 0.1.0-public-review.1 → 0.2.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
@@ -285,7 +285,8 @@ Main exports:
285
285
  - `CapsuleAgent`
286
286
  - `FileTokenStore`
287
287
  - `AgentApiClient`
288
- - `AgentApiError`
288
+ - `AgentApiError`, `RegistrationError`, `AgentAuthError`, `NetworkError`
289
+ - `AgentLogLevel`, `AgentLogRecord`, `StructuredLogger`, `ConsoleLogger` (types)
289
290
  - SDK option and provider types from `types.ts`
290
291
 
291
292
  Core methods:
@@ -300,6 +301,90 @@ Core methods:
300
301
  | `.stop()` | Stops background loops. |
301
302
  | `.runHealth()` | Runs the configured health provider. |
302
303
 
304
+ ### Typed errors
305
+
306
+ > Added in `0.2.0`. Pre-`0.2` code that catches `AgentApiError` still works
307
+ > because the new classes extend it.
308
+
309
+ The SDK surfaces four error classes so callers can branch on the failure
310
+ mode without parsing messages:
311
+
312
+ | Class | When it's thrown | Should the caller retry? |
313
+ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
314
+ | `RegistrationError` | `/api/agents/register` rejects the one-time token (expired, revoked, already used, malformed). Needs operator action — mint a fresh registration token. | No. |
315
+ | `AgentAuthError` | Any non-register call returns 401/403. The agent token has been revoked or the agent disabled. Re-register before resuming. | No. |
316
+ | `NetworkError` | `fetch()` threw before getting an HTTP response (DNS failure, ECONNREFUSED, abort, timeout). `status === 0`; carries an optional `cause`. | Yes — SDK already does. |
317
+ | `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. |
318
+
319
+ The SDK's internal `retry()` loop already implements this policy: only
320
+ `NetworkError` and 5xx `AgentApiError` are retried with exponential backoff;
321
+ 4xx — including the two typed subclasses — surfaces immediately so the caller
322
+ isn't waiting on a backoff that can never succeed.
323
+
324
+ ```ts
325
+ import {
326
+ CapsuleAgent,
327
+ RegistrationError,
328
+ AgentAuthError,
329
+ NetworkError,
330
+ } from "@xtrape/capsule-agent-node";
331
+
332
+ try {
333
+ await agent.start();
334
+ } catch (err) {
335
+ if (err instanceof RegistrationError) {
336
+ console.error("registration token is no longer valid:", err.code);
337
+ process.exit(64);
338
+ }
339
+ if (err instanceof AgentAuthError) {
340
+ console.error("agent token rejected — was the agent revoked?");
341
+ process.exit(65);
342
+ }
343
+ if (err instanceof NetworkError) {
344
+ console.error("could not reach Opstage:", err.message);
345
+ process.exit(75);
346
+ }
347
+ throw err;
348
+ }
349
+ ```
350
+
351
+ ### Structured logger
352
+
353
+ > Added in `0.2.0`. Existing zero-config callers (no `logger` / no
354
+ > `structuredLogger`) keep using the global `console` exactly as before.
355
+
356
+ By default the SDK calls `console.debug` / `.info` / `.warn` / `.error`. For
357
+ JSON-line logging, pino, OpenTelemetry, or any in-house collector, pass a
358
+ `structuredLogger`:
359
+
360
+ ```ts
361
+ import { CapsuleAgent, type StructuredLogger } from "@xtrape/capsule-agent-node";
362
+
363
+ const collector: StructuredLogger = {
364
+ log({ level, message, fields }) {
365
+ process.stdout.write(JSON.stringify({ t: Date.now(), level, message, ...fields }) + "\n");
366
+ },
367
+ };
368
+
369
+ const agent = new CapsuleAgent({
370
+ // ...
371
+ structuredLogger: collector,
372
+ });
373
+ ```
374
+
375
+ Each record arrives as `{ level: "debug" | "info" | "warn" | "error",
376
+ message: string, fields?: Record<string, unknown> }`. The SDK normalizes the
377
+ optional context before emitting:
378
+
379
+ - `Error` instances become `{ error: { name, message, stack } }` so they
380
+ survive `JSON.stringify`.
381
+ - Primitives become `{ value: <primitive> }`.
382
+ - Plain objects pass through unchanged.
383
+
384
+ In all cases the redactor runs over `fields` first, so tokens and
385
+ secret-bearing keys never reach the collector. If both `structuredLogger`
386
+ and `logger` are configured, `structuredLogger` wins.
387
+
303
388
  ## Version Compatibility
304
389
 
305
390
  | 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
@@ -15,6 +15,36 @@ type ActionHandler = (payload: Record<string, unknown>) => Promise<{
15
15
  error?: Record<string, unknown>;
16
16
  } | Record<string, unknown>>;
17
17
  type ActionPrepareHandler = () => Promise<ActionPrepareResult | Record<string, unknown>> | ActionPrepareResult | Record<string, unknown>;
18
+ /**
19
+ * Log level emitted by the SDK. Aligns with the four standard `Console`
20
+ * methods so a plain `console` works as a logger with no adapter.
21
+ */
22
+ type AgentLogLevel = "debug" | "info" | "warn" | "error";
23
+ /**
24
+ * Structured log record passed to a `StructuredLogger`. The SDK populates
25
+ * `level` and `message`; `fields` carries an already-redacted Record of
26
+ * runtime context (agent id, error code, retry count, etc.).
27
+ */
28
+ interface AgentLogRecord {
29
+ level: AgentLogLevel;
30
+ message: string;
31
+ fields?: Record<string, unknown>;
32
+ }
33
+ /**
34
+ * A structured log sink. Useful when the host wants to forward SDK events
35
+ * into a JSON-line logger, pino, OpenTelemetry, or a custom collector
36
+ * without losing the per-field context to `console.warn`'s positional
37
+ * argument list.
38
+ */
39
+ interface StructuredLogger {
40
+ log(record: AgentLogRecord): void;
41
+ }
42
+ /**
43
+ * Console-shaped logger (subset of Node's `Console`). The SDK defaults to
44
+ * the global `console` when neither option is set; if both are provided,
45
+ * `structuredLogger` wins.
46
+ */
47
+ type ConsoleLogger = Pick<Console, "debug" | "info" | "warn" | "error">;
18
48
  interface CapsuleAgentOptions {
19
49
  backendUrl: string;
20
50
  registrationToken?: string;
@@ -42,7 +72,14 @@ interface CapsuleAgentOptions {
42
72
  heartbeatIntervalSeconds?: number;
43
73
  commandPollIntervalSeconds?: number;
44
74
  autoStartLoops?: boolean;
45
- logger?: Pick<Console, "debug" | "info" | "warn" | "error">;
75
+ /** Console-shaped sink; default is the global `console`. */
76
+ logger?: ConsoleLogger;
77
+ /**
78
+ * Structured sink. If provided, takes precedence over `logger`. The SDK
79
+ * still applies its own redaction before calling `log()`, so the host
80
+ * can route records as-is to a JSON stream.
81
+ */
82
+ structuredLogger?: StructuredLogger;
46
83
  failOnStartError?: boolean;
47
84
  }
48
85
  type HealthProvider = () => Promise<HealthReportInput> | HealthReportInput;
@@ -63,6 +100,7 @@ declare class CapsuleAgent {
63
100
  private readonly client;
64
101
  private readonly store;
65
102
  private readonly logger;
103
+ private readonly structuredLogger?;
66
104
  private agentId?;
67
105
  private token?;
68
106
  private stopped;
@@ -89,6 +127,7 @@ declare class CapsuleAgent {
89
127
  private withTimeout;
90
128
  private retry;
91
129
  private log;
130
+ private normalizeFields;
92
131
  }
93
132
 
94
133
  declare class FileTokenStore implements TokenStore {
@@ -99,6 +138,47 @@ declare class FileTokenStore implements TokenStore {
99
138
  clear(): Promise<void>;
100
139
  }
101
140
 
141
+ /**
142
+ * Base class for any failure that crosses the Agent ↔ Backend wire. Use
143
+ * `instanceof AgentApiError` to catch the family, and the more specific
144
+ * subclasses below when the caller wants to branch on the failure mode.
145
+ *
146
+ * `status === 0` is reserved for transport failures that never reached the
147
+ * backend (DNS failure, ECONNREFUSED, network timeout, etc.); use
148
+ * `NetworkError` for those.
149
+ */
150
+ declare class AgentApiError extends Error {
151
+ readonly status: number;
152
+ readonly body?: unknown | undefined;
153
+ readonly code?: string | undefined;
154
+ constructor(status: number, message: string, body?: unknown | undefined, code?: string | undefined);
155
+ }
156
+ /**
157
+ * The backend rejected the registration token (expired, revoked, already
158
+ * used, malformed). The agent cannot proceed without operator action: a
159
+ * fresh registration token must be created in Opstage.
160
+ */
161
+ declare class RegistrationError extends AgentApiError {
162
+ constructor(message: string, status: number, body?: unknown, code?: string);
163
+ }
164
+ /**
165
+ * The backend rejected the persisted agent token (UNAUTHORIZED, agent
166
+ * REVOKED, agent DISABLED). The agent must be re-registered with a fresh
167
+ * registration token before it can resume.
168
+ */
169
+ declare class AgentAuthError extends AgentApiError {
170
+ constructor(message: string, status: number, body?: unknown, code?: string);
171
+ }
172
+ /**
173
+ * Transport-level failure: the request did not reach the backend, or the
174
+ * backend's response was not a parseable HTTP envelope. The SDK retries
175
+ * these with exponential backoff; surface to the caller only after the
176
+ * retry budget is exhausted.
177
+ */
178
+ declare class NetworkError extends AgentApiError {
179
+ constructor(message: string, cause?: unknown);
180
+ }
181
+
102
182
  declare class AgentApiClient {
103
183
  private readonly backendUrl;
104
184
  constructor(backendUrl: string);
@@ -113,7 +193,7 @@ declare class AgentApiClient {
113
193
  heartbeat(agentId: string, token: string, req: AgentHeartbeatRequest): Promise<unknown>;
114
194
  reportServices(agentId: string, token: string, req: ServiceReportRequest): Promise<unknown>;
115
195
  pollCommands(agentId: string, token: string): Promise<{
116
- type: "ACTION_PREPARE" | "ACTION_EXECUTE" | "ACTION";
196
+ type: "ACTION_PREPARE" | "ACTION_EXECUTE";
117
197
  id: string;
118
198
  createdAt: string;
119
199
  status: "PENDING" | "RUNNING" | "SUCCEEDED" | "FAILED" | "EXPIRED" | "CANCELLED";
@@ -129,10 +209,4 @@ declare class AgentApiClient {
129
209
  reportResult(agentId: string, token: string, commandId: string, req: ReportCommandResultRequest): Promise<unknown>;
130
210
  }
131
211
 
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 };
212
+ export { type ActionHandler, type ActionPrepareHandler, type ActionPrepareResult, AgentApiClient, AgentApiError, AgentAuthError, type AgentLogLevel, type AgentLogRecord, type AgentMode, CapsuleAgent, type CapsuleAgentOptions, type ConfigProvider, type ConsoleLogger, FileTokenStore, type HealthProvider, NetworkError, type RegisteredAction, RegistrationError, type RuntimeKind, type ServiceSnapshot, type StructuredLogger, type TokenStore };
package/dist/index.d.ts CHANGED
@@ -15,6 +15,36 @@ type ActionHandler = (payload: Record<string, unknown>) => Promise<{
15
15
  error?: Record<string, unknown>;
16
16
  } | Record<string, unknown>>;
17
17
  type ActionPrepareHandler = () => Promise<ActionPrepareResult | Record<string, unknown>> | ActionPrepareResult | Record<string, unknown>;
18
+ /**
19
+ * Log level emitted by the SDK. Aligns with the four standard `Console`
20
+ * methods so a plain `console` works as a logger with no adapter.
21
+ */
22
+ type AgentLogLevel = "debug" | "info" | "warn" | "error";
23
+ /**
24
+ * Structured log record passed to a `StructuredLogger`. The SDK populates
25
+ * `level` and `message`; `fields` carries an already-redacted Record of
26
+ * runtime context (agent id, error code, retry count, etc.).
27
+ */
28
+ interface AgentLogRecord {
29
+ level: AgentLogLevel;
30
+ message: string;
31
+ fields?: Record<string, unknown>;
32
+ }
33
+ /**
34
+ * A structured log sink. Useful when the host wants to forward SDK events
35
+ * into a JSON-line logger, pino, OpenTelemetry, or a custom collector
36
+ * without losing the per-field context to `console.warn`'s positional
37
+ * argument list.
38
+ */
39
+ interface StructuredLogger {
40
+ log(record: AgentLogRecord): void;
41
+ }
42
+ /**
43
+ * Console-shaped logger (subset of Node's `Console`). The SDK defaults to
44
+ * the global `console` when neither option is set; if both are provided,
45
+ * `structuredLogger` wins.
46
+ */
47
+ type ConsoleLogger = Pick<Console, "debug" | "info" | "warn" | "error">;
18
48
  interface CapsuleAgentOptions {
19
49
  backendUrl: string;
20
50
  registrationToken?: string;
@@ -42,7 +72,14 @@ interface CapsuleAgentOptions {
42
72
  heartbeatIntervalSeconds?: number;
43
73
  commandPollIntervalSeconds?: number;
44
74
  autoStartLoops?: boolean;
45
- logger?: Pick<Console, "debug" | "info" | "warn" | "error">;
75
+ /** Console-shaped sink; default is the global `console`. */
76
+ logger?: ConsoleLogger;
77
+ /**
78
+ * Structured sink. If provided, takes precedence over `logger`. The SDK
79
+ * still applies its own redaction before calling `log()`, so the host
80
+ * can route records as-is to a JSON stream.
81
+ */
82
+ structuredLogger?: StructuredLogger;
46
83
  failOnStartError?: boolean;
47
84
  }
48
85
  type HealthProvider = () => Promise<HealthReportInput> | HealthReportInput;
@@ -63,6 +100,7 @@ declare class CapsuleAgent {
63
100
  private readonly client;
64
101
  private readonly store;
65
102
  private readonly logger;
103
+ private readonly structuredLogger?;
66
104
  private agentId?;
67
105
  private token?;
68
106
  private stopped;
@@ -89,6 +127,7 @@ declare class CapsuleAgent {
89
127
  private withTimeout;
90
128
  private retry;
91
129
  private log;
130
+ private normalizeFields;
92
131
  }
93
132
 
94
133
  declare class FileTokenStore implements TokenStore {
@@ -99,6 +138,47 @@ declare class FileTokenStore implements TokenStore {
99
138
  clear(): Promise<void>;
100
139
  }
101
140
 
141
+ /**
142
+ * Base class for any failure that crosses the Agent ↔ Backend wire. Use
143
+ * `instanceof AgentApiError` to catch the family, and the more specific
144
+ * subclasses below when the caller wants to branch on the failure mode.
145
+ *
146
+ * `status === 0` is reserved for transport failures that never reached the
147
+ * backend (DNS failure, ECONNREFUSED, network timeout, etc.); use
148
+ * `NetworkError` for those.
149
+ */
150
+ declare class AgentApiError extends Error {
151
+ readonly status: number;
152
+ readonly body?: unknown | undefined;
153
+ readonly code?: string | undefined;
154
+ constructor(status: number, message: string, body?: unknown | undefined, code?: string | undefined);
155
+ }
156
+ /**
157
+ * The backend rejected the registration token (expired, revoked, already
158
+ * used, malformed). The agent cannot proceed without operator action: a
159
+ * fresh registration token must be created in Opstage.
160
+ */
161
+ declare class RegistrationError extends AgentApiError {
162
+ constructor(message: string, status: number, body?: unknown, code?: string);
163
+ }
164
+ /**
165
+ * The backend rejected the persisted agent token (UNAUTHORIZED, agent
166
+ * REVOKED, agent DISABLED). The agent must be re-registered with a fresh
167
+ * registration token before it can resume.
168
+ */
169
+ declare class AgentAuthError extends AgentApiError {
170
+ constructor(message: string, status: number, body?: unknown, code?: string);
171
+ }
172
+ /**
173
+ * Transport-level failure: the request did not reach the backend, or the
174
+ * backend's response was not a parseable HTTP envelope. The SDK retries
175
+ * these with exponential backoff; surface to the caller only after the
176
+ * retry budget is exhausted.
177
+ */
178
+ declare class NetworkError extends AgentApiError {
179
+ constructor(message: string, cause?: unknown);
180
+ }
181
+
102
182
  declare class AgentApiClient {
103
183
  private readonly backendUrl;
104
184
  constructor(backendUrl: string);
@@ -113,7 +193,7 @@ declare class AgentApiClient {
113
193
  heartbeat(agentId: string, token: string, req: AgentHeartbeatRequest): Promise<unknown>;
114
194
  reportServices(agentId: string, token: string, req: ServiceReportRequest): Promise<unknown>;
115
195
  pollCommands(agentId: string, token: string): Promise<{
116
- type: "ACTION_PREPARE" | "ACTION_EXECUTE" | "ACTION";
196
+ type: "ACTION_PREPARE" | "ACTION_EXECUTE";
117
197
  id: string;
118
198
  createdAt: string;
119
199
  status: "PENDING" | "RUNNING" | "SUCCEEDED" | "FAILED" | "EXPIRED" | "CANCELLED";
@@ -129,10 +209,4 @@ declare class AgentApiClient {
129
209
  reportResult(agentId: string, token: string, commandId: string, req: ReportCommandResultRequest): Promise<unknown>;
130
210
  }
131
211
 
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 };
212
+ export { type ActionHandler, type ActionPrepareHandler, type ActionPrepareResult, AgentApiClient, AgentApiError, AgentAuthError, type AgentLogLevel, type AgentLogRecord, type AgentMode, CapsuleAgent, type CapsuleAgentOptions, type ConfigProvider, type ConsoleLogger, FileTokenStore, type HealthProvider, NetworkError, type RegisteredAction, RegistrationError, type RuntimeKind, 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,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtrape/capsule-agent-node",
3
- "version": "0.1.0-public-review.1",
3
+ "version": "0.2.0",
4
4
  "description": "Node.js Agent SDK for embedding Capsule Services into the Opstage governance loop.",
5
5
  "keywords": [
6
6
  "xtrape",
@@ -35,15 +35,8 @@
35
35
  "LICENSE",
36
36
  "NOTICE"
37
37
  ],
38
- "scripts": {
39
- "build": "tsup src/index.ts --format esm,cjs --dts",
40
- "prepack": "pnpm build",
41
- "test": "vitest run",
42
- "typecheck": "tsc --noEmit",
43
- "lint": "tsc --noEmit"
44
- },
45
38
  "dependencies": {
46
- "@xtrape/capsule-contracts-node": "^0.1.0-public-review.0"
39
+ "@xtrape/capsule-contracts-node": "^0.2.0"
47
40
  },
48
41
  "devDependencies": {
49
42
  "tsup": "^8.3.5",
@@ -58,5 +51,11 @@
58
51
  "registry": "https://registry.npmjs.org/",
59
52
  "access": "public"
60
53
  },
61
- "license": "Apache-2.0"
62
- }
54
+ "license": "Apache-2.0",
55
+ "scripts": {
56
+ "build": "tsup src/index.ts --format esm,cjs --dts",
57
+ "test": "vitest run",
58
+ "typecheck": "tsc --noEmit",
59
+ "lint": "tsc --noEmit"
60
+ }
61
+ }