@loomcycle/client 0.8.19

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/dist/client.js ADDED
@@ -0,0 +1,366 @@
1
+ /**
2
+ * LoomcycleClient — the single public class exported by
3
+ * @loomcycle/client. Speaks HTTP+SSE to a running loomcycle sidecar.
4
+ *
5
+ * hooks-connector PR C: full Python-adapter parity + hook management.
6
+ * 27 methods total — 26 async (run streaming, continuation, agent
7
+ * metadata, transcript, health, users, pause/resume/state, snapshot
8
+ * lifecycle capture / list / get / restore / delete, memory admin,
9
+ * interruption listing + resolve, hook registration / list / delete)
10
+ * plus one synchronous helper (exportSnapshotURL builds a URL string
11
+ * without issuing a request).
12
+ *
13
+ * Construction:
14
+ *
15
+ * const client = new LoomcycleClient({
16
+ * baseUrl: "http://127.0.0.1:8787", // or process.env.LOOMCYCLE_BASE_URL
17
+ * authToken: "...", // or process.env.LOOMCYCLE_AUTH_TOKEN
18
+ * });
19
+ *
20
+ * Streaming methods (`runStreaming`, `continueSession`) return
21
+ * AsyncIterable<AgentEvent>; non-streaming methods return
22
+ * Promise<T>. Non-2xx responses throw typed errors from errors.ts
23
+ * via fetch-helpers.ts:raiseFromResponse — see README.md for the
24
+ * full mapping table.
25
+ */
26
+ import { deleteRequest, jsonFetch, postJSON, raiseFromResponse, } from "./fetch-helpers.js";
27
+ import { parseSSE } from "./stream.js";
28
+ export class LoomcycleClient {
29
+ ctx;
30
+ constructor(opts = {}) {
31
+ this.ctx = {
32
+ baseUrl: (opts.baseUrl ?? "http://127.0.0.1:8787").replace(/\/$/, ""),
33
+ authToken: opts.authToken,
34
+ fetchImpl: opts.fetch ?? fetch,
35
+ };
36
+ }
37
+ // ---- Run lifecycle ----
38
+ /**
39
+ * Run an agent and stream events. Returns AsyncIterable<AgentEvent>;
40
+ * the iterator completes when the server closes the SSE stream.
41
+ *
42
+ * Errors during the run surface as `{ type: "error", error }` events;
43
+ * only transport / HTTP-level failures throw — and those throw typed
44
+ * errors (e.g. AuthError for 401, BackpressureError for 429).
45
+ */
46
+ async *runStreaming(opts) {
47
+ // Build the body conditionally so omitted fields stay off the wire.
48
+ // The pointer-vs-empty distinction on allowed_hosts is preserved by
49
+ // treating `null` as "omit" — same as the server's nil semantics —
50
+ // so callers threading a possibly-unset slice don't accidentally
51
+ // send `allowed_hosts: null` (which JSON-decodes to a deny-all on
52
+ // some implementations).
53
+ const body = {
54
+ agent: opts.agent,
55
+ segments: opts.segments,
56
+ };
57
+ if (opts.allowedTools !== undefined)
58
+ body.allowed_tools = opts.allowedTools;
59
+ if (opts.allowedHosts !== undefined && opts.allowedHosts !== null) {
60
+ body.allowed_hosts = opts.allowedHosts;
61
+ }
62
+ if (opts.webSearchFilter !== undefined)
63
+ body.web_search_filter = opts.webSearchFilter;
64
+ if (opts.sessionId !== undefined)
65
+ body.session_id = opts.sessionId;
66
+ if (opts.tenantId !== undefined)
67
+ body.tenant_id = opts.tenantId;
68
+ if (opts.userId !== undefined)
69
+ body.user_id = opts.userId;
70
+ if (opts.agentId !== undefined)
71
+ body.agent_id = opts.agentId;
72
+ if (opts.userTier !== undefined)
73
+ body.user_tier = opts.userTier;
74
+ if (opts.userBearer !== undefined)
75
+ body.user_bearer = opts.userBearer;
76
+ yield* this.streamSSE("/v1/runs", body, opts.signal);
77
+ }
78
+ /**
79
+ * Continue an existing session with a new run. The session's prior
80
+ * transcript is replayed into the model's context server-side;
81
+ * this iterator yields only the NEW events from the continuation.
82
+ *
83
+ * Raises SessionNotFoundError when sessionId is unknown,
84
+ * SessionBusyError when another request is in flight on the same
85
+ * session.
86
+ */
87
+ async *continueSession(opts) {
88
+ const body = {
89
+ segments: opts.segments,
90
+ };
91
+ if (opts.allowedTools !== undefined)
92
+ body.allowed_tools = opts.allowedTools;
93
+ if (opts.allowedHosts !== undefined && opts.allowedHosts !== null) {
94
+ body.allowed_hosts = opts.allowedHosts;
95
+ }
96
+ if (opts.webSearchFilter !== undefined)
97
+ body.web_search_filter = opts.webSearchFilter;
98
+ if (opts.agentId !== undefined)
99
+ body.agent_id = opts.agentId;
100
+ if (opts.userTier !== undefined)
101
+ body.user_tier = opts.userTier;
102
+ if (opts.userBearer !== undefined)
103
+ body.user_bearer = opts.userBearer;
104
+ yield* this.streamSSE(`/v1/sessions/${encodeURIComponent(opts.sessionId)}/messages`, body, opts.signal);
105
+ }
106
+ // ---- Agent metadata ----
107
+ /** Read one agent's status + usage stats. Raises AgentNotFoundError
108
+ * when the agent_id is unknown. */
109
+ async getAgent(agentId, opts) {
110
+ return jsonFetch(this.ctx, `/v1/agents/${encodeURIComponent(agentId)}`, opts);
111
+ }
112
+ /** Cancel a live agent (cascades to children via parent_agent_id).
113
+ * Returns count of agents cancelled. Idempotent — already-terminated
114
+ * agents return 0. */
115
+ async cancelAgent(agentId, opts) {
116
+ const resp = await postJSON(this.ctx, `/v1/agents/${encodeURIComponent(agentId)}/cancel`, { reason: opts?.reason ?? "" }, opts);
117
+ return { cancelledCount: resp.cancelled_count };
118
+ }
119
+ /** List a user's recent agent runs, optionally filtered by status. */
120
+ async listUserAgents(userId, opts) {
121
+ const q = opts?.status ? `?status=${encodeURIComponent(opts.status)}` : "";
122
+ const resp = await jsonFetch(this.ctx, `/v1/users/${encodeURIComponent(userId)}/agents${q}`, opts);
123
+ return resp.agents ?? [];
124
+ }
125
+ /** Read the full event log for a session. Each entry has seq,
126
+ * run_id, ts_ns, type, event (the providers.Event payload). */
127
+ async getTranscript(sessionId, opts) {
128
+ return jsonFetch(this.ctx, `/v1/sessions/${encodeURIComponent(sessionId)}/transcript`, opts);
129
+ }
130
+ /** Liveness probe. Unauthenticated. Returns build info + uptime.
131
+ * Hits /healthz, not /v1/. */
132
+ async health(opts) {
133
+ return jsonFetch(this.ctx, "/healthz", opts);
134
+ }
135
+ /** Admin: list known users with running-count summary. Drives the
136
+ * Web UI's user picker; operators with bearer auth can call too. */
137
+ async listUsers(opts) {
138
+ return jsonFetch(this.ctx, "/v1/_users", opts);
139
+ }
140
+ // ---- v0.8.17/8.18 Pause / Resume / State ----
141
+ /** Quiesce the runtime. Idempotent tools cancel immediately;
142
+ * non-idempotent + external tools get a grace window then
143
+ * force-cancel. Raises AlreadyPausingError on 409,
144
+ * PauseNotConfiguredError on 503. */
145
+ async pauseRuntime(opts) {
146
+ const body = opts?.timeoutMs && opts.timeoutMs > 0
147
+ ? { timeout_ms: opts.timeoutMs }
148
+ : undefined;
149
+ return postJSON(this.ctx, "/v1/_pause", body, opts);
150
+ }
151
+ /** Release the runtime quiesce. Raises NotPausedError on 409. */
152
+ async resumeRuntime(opts) {
153
+ return postJSON(this.ctx, "/v1/_resume", undefined, opts);
154
+ }
155
+ /** Current runtime state. Cheap query — atomic state + a
156
+ * bounded snapshots count. */
157
+ async getRuntimeState(opts) {
158
+ return jsonFetch(this.ctx, "/v1/_state", opts);
159
+ }
160
+ // ---- Snapshot lifecycle ----
161
+ /** Capture running-state into a per-section-semver JSON envelope.
162
+ * Raises SnapshotTooLargeError on 413 when the envelope exceeds
163
+ * LOOMCYCLE_SNAPSHOT_MAX_BYTES (default 512 MiB). */
164
+ async createSnapshot(opts) {
165
+ const body = {};
166
+ if (opts?.label)
167
+ body.label = opts.label;
168
+ if (opts?.includeHistory)
169
+ body.include_history = true;
170
+ if (opts?.includeHistorySince)
171
+ body.include_history_since = opts.includeHistorySince;
172
+ if (opts?.maxBytes && opts.maxBytes > 0)
173
+ body.max_bytes = opts.maxBytes;
174
+ return postJSON(this.ctx, "/v1/_snapshots", body, opts);
175
+ }
176
+ /** List captured snapshots (most-recent first). Capped at 200
177
+ * server-side; the limit param defaults to 200 too. */
178
+ async listSnapshots(opts) {
179
+ const params = new URLSearchParams();
180
+ if (opts?.limit && opts.limit > 0)
181
+ params.set("limit", String(opts.limit));
182
+ if (opts?.labelContains)
183
+ params.set("label_contains", opts.labelContains);
184
+ const qs = params.toString();
185
+ const path = qs ? `/v1/_snapshots?${qs}` : "/v1/_snapshots";
186
+ const resp = await jsonFetch(this.ctx, path, opts);
187
+ return resp.entries ?? [];
188
+ }
189
+ /** Fetch the full snapshot envelope including JSON content.
190
+ * Distinct from exportSnapshot (which is operator-facing
191
+ * "where did this land on the host" semantics with a download
192
+ * URL). Raises SnapshotNotFoundError on 404. */
193
+ async getSnapshot(snapshotId, opts) {
194
+ return jsonFetch(this.ctx, `/v1/_snapshots/${encodeURIComponent(snapshotId)}`, opts);
195
+ }
196
+ /** Returns the URL of the snapshot's canonical envelope —
197
+ * synchronous and side-effect-free; does NOT issue an HTTP
198
+ * request. The endpoint is bearer-authed like every other
199
+ * `/v1/_snapshots/*` route, so callers must attach the same
200
+ * `Authorization: Bearer <token>` header when fetching this
201
+ * URL (e.g. `curl -H "Authorization: Bearer $TOKEN" ...`).
202
+ * There is no token query-param fallback. */
203
+ exportSnapshotURL(snapshotId) {
204
+ return `${this.ctx.baseUrl}/v1/_snapshots/${encodeURIComponent(snapshotId)}/export`;
205
+ }
206
+ /** Restore from a same-instance snapshot id OR an inline
207
+ * envelope JSON. Idempotent: ON CONFLICT DO NOTHING per row;
208
+ * the returned counters reflect rows actually written.
209
+ * Raises SnapshotVersionError on 422 when a section's
210
+ * declared version is newer than the reader supports. */
211
+ async restoreSnapshot(opts) {
212
+ if (!opts.snapshotId && opts.json === undefined) {
213
+ // Client-side validation — match Python adapter's
214
+ // InvalidArgumentError pattern but the typed-error layer
215
+ // lives in errors.ts; for a thrown plain error here the
216
+ // method's catchers just see the message.
217
+ throw new Error("restoreSnapshot: pass snapshotId or json (one is required)");
218
+ }
219
+ if (opts.snapshotId && opts.json !== undefined) {
220
+ throw new Error("restoreSnapshot: pass only one of snapshotId or json");
221
+ }
222
+ // When json is supplied the id path-segment is ignored
223
+ // server-side; we use a placeholder "inline" segment to keep
224
+ // the URL well-formed.
225
+ const id = opts.snapshotId ?? "inline";
226
+ const body = {};
227
+ if (opts.includeHistory)
228
+ body.include_history = true;
229
+ if (opts.json !== undefined)
230
+ body.json = opts.json;
231
+ return postJSON(this.ctx, `/v1/_snapshots/${encodeURIComponent(id)}/restore`, body, opts);
232
+ }
233
+ /** Delete a snapshot. Idempotent — succeeds whether or not the
234
+ * row existed (server returns 204 in both cases). */
235
+ async deleteSnapshot(snapshotId, opts) {
236
+ await deleteRequest(this.ctx, `/v1/_snapshots/${encodeURIComponent(snapshotId)}`, opts);
237
+ }
238
+ // ---- Memory admin ----
239
+ /** List the kinds of memory scopes the server knows about
240
+ * (agent, user — or whatever the operator yaml declares). */
241
+ async listMemoryScopes(opts) {
242
+ return jsonFetch(this.ctx, "/v1/_memory/scopes", opts);
243
+ }
244
+ /** List the scope_ids that have at least one memory row under
245
+ * a given scope. */
246
+ async listMemoryScopeIDs(scope, opts) {
247
+ return jsonFetch(this.ctx, `/v1/_memory/scopes/${encodeURIComponent(scope)}`, opts);
248
+ }
249
+ /** List memory entries under a (scope, scope_id) tuple.
250
+ * Optional prefix narrows by key prefix. */
251
+ async listMemoryEntries(scope, scopeID, opts) {
252
+ const params = new URLSearchParams();
253
+ if (opts?.prefix)
254
+ params.set("prefix", opts.prefix);
255
+ // Guard against `limit: 0` (falsy but valid-looking) and negatives —
256
+ // both would either send `limit=0` (server treats as default but the
257
+ // semantic is unclear) or `limit=-N` (server rejects). Only send the
258
+ // param when the caller passed a meaningful positive number.
259
+ if (opts?.limit && opts.limit > 0)
260
+ params.set("limit", String(opts.limit));
261
+ const qs = params.toString();
262
+ const path = `/v1/_memory/scopes/${encodeURIComponent(scope)}/${encodeURIComponent(scopeID)}/keys${qs ? "?" + qs : ""}`;
263
+ return jsonFetch(this.ctx, path, opts);
264
+ }
265
+ /** Read a single memory entry by (scope, scope_id, key). */
266
+ async getMemoryEntry(scope, scopeID, key, opts) {
267
+ return jsonFetch(this.ctx, `/v1/_memory/scopes/${encodeURIComponent(scope)}/${encodeURIComponent(scopeID)}/keys/${encodeURIComponent(key)}`, opts);
268
+ }
269
+ // ---- Interruption ----
270
+ /** List interrupts addressable to a user_id. Default filter is
271
+ * status=pending. */
272
+ async listUserInterrupts(userId, opts) {
273
+ const status = opts?.status ?? "pending";
274
+ return jsonFetch(this.ctx, `/v1/users/${encodeURIComponent(userId)}/interrupts?status=${encodeURIComponent(status)}`, opts);
275
+ }
276
+ /** List interrupts emitted by a specific run. */
277
+ async listRunInterrupts(runId, opts) {
278
+ const status = opts?.status ?? "pending";
279
+ return jsonFetch(this.ctx, `/v1/runs/${encodeURIComponent(runId)}/interrupts?status=${encodeURIComponent(status)}`, opts);
280
+ }
281
+ /** Resolve a pending Interruption.ask from outside the agent
282
+ * loop. Lets a TS-side dashboard or service act as the human
283
+ * answerer when operator yaml configures the consumer-MCP
284
+ * backend. */
285
+ async resolveInterrupt(runId, interruptId, opts) {
286
+ return postJSON(this.ctx, `/v1/runs/${encodeURIComponent(runId)}/interrupts/${encodeURIComponent(interruptId)}/resolve`, {
287
+ kind: opts.kind ?? "question",
288
+ answer: opts.answer,
289
+ resolved_by: opts.resolvedBy ?? "client",
290
+ }, opts);
291
+ }
292
+ // ---- Hook management (hooks-connector series, PR C) ----
293
+ /** Register a pre- or post-tool webhook. The callback_url must be
294
+ * an http:// or https:// endpoint the CONSUMER runs — loomcycle
295
+ * POSTs PreHookCall / PostHookCall payloads to it. This method
296
+ * manages registration only; the receiver is the consumer's own
297
+ * HTTP framework (Express, Next.js, etc.).
298
+ *
299
+ * Re-registering the same (owner, name) replaces the prior entry
300
+ * with a fresh id (idempotent app-restart contract).
301
+ *
302
+ * Raises InvalidArgumentError on 400 (bad URL / phase / missing
303
+ * required fields). */
304
+ async registerHook(opts) {
305
+ const body = {
306
+ owner: opts.owner,
307
+ name: opts.name,
308
+ phase: opts.phase,
309
+ callback_url: opts.callbackUrl,
310
+ };
311
+ if (opts.agents !== undefined)
312
+ body.agents = opts.agents;
313
+ if (opts.tools !== undefined)
314
+ body.tools = opts.tools;
315
+ if (opts.failMode !== undefined)
316
+ body.fail_mode = opts.failMode;
317
+ if (opts.timeoutMs !== undefined && opts.timeoutMs > 0) {
318
+ body.timeout_ms = opts.timeoutMs;
319
+ }
320
+ return postJSON(this.ctx, "/v1/hooks", body, opts);
321
+ }
322
+ /** List every currently-registered hook. Returns the array
323
+ * unwrapped (the wire envelope is `{hooks: [...]}` — we strip
324
+ * the envelope to match listUserAgents). In-memory only — empty
325
+ * after a loomcycle restart. */
326
+ async listHooks(opts) {
327
+ const resp = await jsonFetch(this.ctx, "/v1/hooks", opts);
328
+ return resp.hooks ?? [];
329
+ }
330
+ /** Delete a registered hook by id. Raises HookNotFoundError on
331
+ * 404. Returns void on success (the HTTP 200 body `{deleted: id}`
332
+ * is dropped — callers already know the id they passed). */
333
+ async deleteHook(id, opts) {
334
+ await deleteRequest(this.ctx, `/v1/hooks/${encodeURIComponent(id)}`, opts);
335
+ }
336
+ // ---- Internal helpers ----
337
+ /** Shared SSE POST → stream-of-AgentEvent path. Used by
338
+ * runStreaming + continueSession. */
339
+ async *streamSSE(path, body, signal) {
340
+ const headers = {
341
+ "Content-Type": "application/json",
342
+ // Accept BOTH text/event-stream (the success path) AND
343
+ // application/json (the error path — non-2xx responses come
344
+ // back as JSON so raiseFromResponse can extract typed errors).
345
+ // Per the Streamable HTTP spec; strict reverse proxies in
346
+ // front of the sidecar 406 otherwise. Same rationale as the
347
+ // v0.8.x MCP HTTP-transport hardening note in CLAUDE.md.
348
+ Accept: "text/event-stream, application/json",
349
+ };
350
+ if (this.ctx.authToken)
351
+ headers.Authorization = `Bearer ${this.ctx.authToken}`;
352
+ const resp = await this.ctx.fetchImpl(this.ctx.baseUrl + path, {
353
+ method: "POST",
354
+ headers,
355
+ body: JSON.stringify(body),
356
+ signal,
357
+ });
358
+ if (!resp.ok) {
359
+ await raiseFromResponse(resp);
360
+ }
361
+ if (!resp.body) {
362
+ throw new Error("loomcycle: response has no body");
363
+ }
364
+ yield* parseSSE(resp.body.getReader());
365
+ }
366
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Typed exceptions raised by LoomcycleClient. Mirrors the Python
3
+ * adapter's `errors.py` taxonomy 1:1 — same names, same semantics,
4
+ * just adapted to HTTP status codes (no gRPC StatusCode equivalents).
5
+ *
6
+ * Every error stores the raw HTTP status (`status`) and the raw
7
+ * response body (`bodyText`, truncated to 1 KiB) for log correlation
8
+ * when the typed class doesn't carry enough.
9
+ *
10
+ * Dispatch from raw HTTP response to typed error lives in
11
+ * `fetch-helpers.ts:raiseFromResponse` — that's the one place to
12
+ * look when adding a new error type.
13
+ *
14
+ * PR 5a foundation: classes defined; dispatch wiring lands here +
15
+ * in fetch-helpers.ts. The current `runStreaming` (the only public
16
+ * method in v0.1.0-alpha) throws a plain Error today; PR 5a keeps
17
+ * that behavior. PR 5b switches `runStreaming` + every new method
18
+ * to raise typed errors via `raiseFromResponse`.
19
+ */
20
+ export declare class LoomcycleError extends Error {
21
+ readonly status?: number;
22
+ readonly bodyText?: string;
23
+ constructor(message: string, opts?: {
24
+ status?: number;
25
+ bodyText?: string;
26
+ });
27
+ }
28
+ /** Base class for every HTTP 404 the client surfaces. Lets callers
29
+ * catch any not-found case with a single `instanceof NotFoundError`
30
+ * check, regardless of which specific resource was missing
31
+ * (agent / session / snapshot / generic 404 like a missing memory
32
+ * row or interrupt). */
33
+ export declare class NotFoundError extends LoomcycleError {
34
+ constructor(message: string, opts?: {
35
+ status?: number;
36
+ bodyText?: string;
37
+ });
38
+ }
39
+ export declare class AgentNotFoundError extends NotFoundError {
40
+ constructor(message: string, opts?: {
41
+ status?: number;
42
+ bodyText?: string;
43
+ });
44
+ }
45
+ export declare class SessionNotFoundError extends NotFoundError {
46
+ constructor(message: string, opts?: {
47
+ status?: number;
48
+ bodyText?: string;
49
+ });
50
+ }
51
+ export declare class SessionBusyError extends LoomcycleError {
52
+ constructor(message: string, opts?: {
53
+ status?: number;
54
+ bodyText?: string;
55
+ });
56
+ }
57
+ export declare class AgentIDInUseError extends LoomcycleError {
58
+ constructor(message: string, opts?: {
59
+ status?: number;
60
+ bodyText?: string;
61
+ });
62
+ }
63
+ export declare class BackpressureError extends LoomcycleError {
64
+ constructor(message: string, opts?: {
65
+ status?: number;
66
+ bodyText?: string;
67
+ });
68
+ }
69
+ export declare class AuthError extends LoomcycleError {
70
+ constructor(message: string, opts?: {
71
+ status?: number;
72
+ bodyText?: string;
73
+ });
74
+ }
75
+ export declare class UnavailableError extends LoomcycleError {
76
+ constructor(message: string, opts?: {
77
+ status?: number;
78
+ bodyText?: string;
79
+ });
80
+ }
81
+ export declare class InvalidArgumentError extends LoomcycleError {
82
+ constructor(message: string, opts?: {
83
+ status?: number;
84
+ bodyText?: string;
85
+ });
86
+ }
87
+ /** Subclasses UnavailableError for back-compat: code that broadly
88
+ * catches UnavailableError keeps working when this more-specific
89
+ * variant fires. */
90
+ export declare class PauseNotConfiguredError extends UnavailableError {
91
+ constructor(message: string, opts?: {
92
+ status?: number;
93
+ bodyText?: string;
94
+ });
95
+ }
96
+ export declare class AlreadyPausingError extends LoomcycleError {
97
+ constructor(message: string, opts?: {
98
+ status?: number;
99
+ bodyText?: string;
100
+ });
101
+ }
102
+ export declare class NotPausedError extends LoomcycleError {
103
+ constructor(message: string, opts?: {
104
+ status?: number;
105
+ bodyText?: string;
106
+ });
107
+ }
108
+ export declare class SnapshotNotFoundError extends NotFoundError {
109
+ constructor(message: string, opts?: {
110
+ status?: number;
111
+ bodyText?: string;
112
+ });
113
+ }
114
+ export declare class SnapshotTooLargeError extends LoomcycleError {
115
+ constructor(message: string, opts?: {
116
+ status?: number;
117
+ bodyText?: string;
118
+ });
119
+ }
120
+ export declare class SnapshotVersionError extends LoomcycleError {
121
+ constructor(message: string, opts?: {
122
+ status?: number;
123
+ bodyText?: string;
124
+ });
125
+ }
126
+ /** HookNotFoundError — raised by deleteHook when no hook has the
127
+ * supplied id (HTTP 404 with "hook" in the body). Extends
128
+ * NotFoundError so consumers catching the broader category get this
129
+ * one too. */
130
+ export declare class HookNotFoundError extends NotFoundError {
131
+ constructor(message: string, opts?: {
132
+ status?: number;
133
+ bodyText?: string;
134
+ });
135
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Typed exceptions raised by LoomcycleClient. Mirrors the Python
3
+ * adapter's `errors.py` taxonomy 1:1 — same names, same semantics,
4
+ * just adapted to HTTP status codes (no gRPC StatusCode equivalents).
5
+ *
6
+ * Every error stores the raw HTTP status (`status`) and the raw
7
+ * response body (`bodyText`, truncated to 1 KiB) for log correlation
8
+ * when the typed class doesn't carry enough.
9
+ *
10
+ * Dispatch from raw HTTP response to typed error lives in
11
+ * `fetch-helpers.ts:raiseFromResponse` — that's the one place to
12
+ * look when adding a new error type.
13
+ *
14
+ * PR 5a foundation: classes defined; dispatch wiring lands here +
15
+ * in fetch-helpers.ts. The current `runStreaming` (the only public
16
+ * method in v0.1.0-alpha) throws a plain Error today; PR 5a keeps
17
+ * that behavior. PR 5b switches `runStreaming` + every new method
18
+ * to raise typed errors via `raiseFromResponse`.
19
+ */
20
+ export class LoomcycleError extends Error {
21
+ status;
22
+ bodyText;
23
+ constructor(message, opts) {
24
+ super(message);
25
+ this.name = "LoomcycleError";
26
+ this.status = opts?.status;
27
+ this.bodyText = opts?.bodyText;
28
+ }
29
+ }
30
+ /** Base class for every HTTP 404 the client surfaces. Lets callers
31
+ * catch any not-found case with a single `instanceof NotFoundError`
32
+ * check, regardless of which specific resource was missing
33
+ * (agent / session / snapshot / generic 404 like a missing memory
34
+ * row or interrupt). */
35
+ export class NotFoundError extends LoomcycleError {
36
+ constructor(message, opts) {
37
+ super(message, opts);
38
+ this.name = "NotFoundError";
39
+ }
40
+ }
41
+ export class AgentNotFoundError extends NotFoundError {
42
+ constructor(message, opts) {
43
+ super(message, opts);
44
+ this.name = "AgentNotFoundError";
45
+ }
46
+ }
47
+ export class SessionNotFoundError extends NotFoundError {
48
+ constructor(message, opts) {
49
+ super(message, opts);
50
+ this.name = "SessionNotFoundError";
51
+ }
52
+ }
53
+ export class SessionBusyError extends LoomcycleError {
54
+ constructor(message, opts) {
55
+ super(message, opts);
56
+ this.name = "SessionBusyError";
57
+ }
58
+ }
59
+ export class AgentIDInUseError extends LoomcycleError {
60
+ constructor(message, opts) {
61
+ super(message, opts);
62
+ this.name = "AgentIDInUseError";
63
+ }
64
+ }
65
+ export class BackpressureError extends LoomcycleError {
66
+ constructor(message, opts) {
67
+ super(message, opts);
68
+ this.name = "BackpressureError";
69
+ }
70
+ }
71
+ export class AuthError extends LoomcycleError {
72
+ constructor(message, opts) {
73
+ super(message, opts);
74
+ this.name = "AuthError";
75
+ }
76
+ }
77
+ export class UnavailableError extends LoomcycleError {
78
+ constructor(message, opts) {
79
+ super(message, opts);
80
+ this.name = "UnavailableError";
81
+ }
82
+ }
83
+ export class InvalidArgumentError extends LoomcycleError {
84
+ constructor(message, opts) {
85
+ super(message, opts);
86
+ this.name = "InvalidArgumentError";
87
+ }
88
+ }
89
+ // ---- v0.8.18 — Pause/Snapshot typed errors ----
90
+ /** Subclasses UnavailableError for back-compat: code that broadly
91
+ * catches UnavailableError keeps working when this more-specific
92
+ * variant fires. */
93
+ export class PauseNotConfiguredError extends UnavailableError {
94
+ constructor(message, opts) {
95
+ super(message, opts);
96
+ this.name = "PauseNotConfiguredError";
97
+ }
98
+ }
99
+ export class AlreadyPausingError extends LoomcycleError {
100
+ constructor(message, opts) {
101
+ super(message, opts);
102
+ this.name = "AlreadyPausingError";
103
+ }
104
+ }
105
+ export class NotPausedError extends LoomcycleError {
106
+ constructor(message, opts) {
107
+ super(message, opts);
108
+ this.name = "NotPausedError";
109
+ }
110
+ }
111
+ export class SnapshotNotFoundError extends NotFoundError {
112
+ constructor(message, opts) {
113
+ super(message, opts);
114
+ this.name = "SnapshotNotFoundError";
115
+ }
116
+ }
117
+ export class SnapshotTooLargeError extends LoomcycleError {
118
+ constructor(message, opts) {
119
+ super(message, opts);
120
+ this.name = "SnapshotTooLargeError";
121
+ }
122
+ }
123
+ export class SnapshotVersionError extends LoomcycleError {
124
+ constructor(message, opts) {
125
+ super(message, opts);
126
+ this.name = "SnapshotVersionError";
127
+ }
128
+ }
129
+ /** HookNotFoundError — raised by deleteHook when no hook has the
130
+ * supplied id (HTTP 404 with "hook" in the body). Extends
131
+ * NotFoundError so consumers catching the broader category get this
132
+ * one too. */
133
+ export class HookNotFoundError extends NotFoundError {
134
+ constructor(message, opts) {
135
+ super(message, opts);
136
+ this.name = "HookNotFoundError";
137
+ }
138
+ }