@loomcycle/client 0.9.3 → 0.10.1

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.
@@ -0,0 +1,246 @@
1
+ "use strict";
2
+ /**
3
+ * Typed exceptions raised by LoomcycleClient. Mirrors the Python
4
+ * adapter's `errors.py` taxonomy 1:1 — same names, same semantics,
5
+ * just adapted to HTTP status codes (no gRPC StatusCode equivalents).
6
+ *
7
+ * Every error stores the raw HTTP status (`status`) and the raw
8
+ * response body (`bodyText`, truncated to 1 KiB) for log correlation
9
+ * when the typed class doesn't carry enough.
10
+ *
11
+ * Dispatch from raw HTTP response to typed error lives in
12
+ * `fetch-helpers.ts:raiseFromResponse` — that's the one place to
13
+ * look when adding a new error type.
14
+ *
15
+ * PR 5a foundation: classes defined; dispatch wiring lands here +
16
+ * in fetch-helpers.ts. The current `runStreaming` (the only public
17
+ * method in v0.1.0-alpha) throws a plain Error today; PR 5a keeps
18
+ * that behavior. PR 5b switches `runStreaming` + every new method
19
+ * to raise typed errors via `raiseFromResponse`.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.SubstrateToolRefusedError = exports.ChannelCursorRegressionError = exports.HookNotFoundError = exports.SnapshotVersionError = exports.SnapshotTooLargeError = exports.SnapshotNotFoundError = exports.NotPausedError = exports.AlreadyPausingError = exports.PauseNotConfiguredError = exports.InvalidArgumentError = exports.UnavailableError = exports.AuthError = exports.PerUserQuotaExhaustedError = exports.BackpressureError = exports.AgentIDInUseError = exports.SessionBusyError = exports.SessionNotFoundError = exports.AgentNotFoundError = exports.NotFoundError = exports.LoomcycleError = void 0;
23
+ class LoomcycleError extends Error {
24
+ status;
25
+ bodyText;
26
+ constructor(message, opts) {
27
+ super(message);
28
+ this.name = "LoomcycleError";
29
+ this.status = opts?.status;
30
+ this.bodyText = opts?.bodyText;
31
+ }
32
+ }
33
+ exports.LoomcycleError = LoomcycleError;
34
+ /** Base class for every HTTP 404 the client surfaces. Lets callers
35
+ * catch any not-found case with a single `instanceof NotFoundError`
36
+ * check, regardless of which specific resource was missing
37
+ * (agent / session / snapshot / generic 404 like a missing memory
38
+ * row or interrupt). */
39
+ class NotFoundError extends LoomcycleError {
40
+ constructor(message, opts) {
41
+ super(message, opts);
42
+ this.name = "NotFoundError";
43
+ }
44
+ }
45
+ exports.NotFoundError = NotFoundError;
46
+ class AgentNotFoundError extends NotFoundError {
47
+ constructor(message, opts) {
48
+ super(message, opts);
49
+ this.name = "AgentNotFoundError";
50
+ }
51
+ }
52
+ exports.AgentNotFoundError = AgentNotFoundError;
53
+ class SessionNotFoundError extends NotFoundError {
54
+ constructor(message, opts) {
55
+ super(message, opts);
56
+ this.name = "SessionNotFoundError";
57
+ }
58
+ }
59
+ exports.SessionNotFoundError = SessionNotFoundError;
60
+ class SessionBusyError extends LoomcycleError {
61
+ constructor(message, opts) {
62
+ super(message, opts);
63
+ this.name = "SessionBusyError";
64
+ }
65
+ }
66
+ exports.SessionBusyError = SessionBusyError;
67
+ class AgentIDInUseError extends LoomcycleError {
68
+ constructor(message, opts) {
69
+ super(message, opts);
70
+ this.name = "AgentIDInUseError";
71
+ }
72
+ }
73
+ exports.AgentIDInUseError = AgentIDInUseError;
74
+ class BackpressureError extends LoomcycleError {
75
+ constructor(message, opts) {
76
+ super(message, opts);
77
+ this.name = "BackpressureError";
78
+ }
79
+ }
80
+ exports.BackpressureError = BackpressureError;
81
+ /**
82
+ * PerUserQuotaExhaustedError signals that the caller has hit their
83
+ * per-user cap on in-flight (active+queued) runs. Distinct from
84
+ * BackpressureError because the appropriate retry strategy differs:
85
+ * backpressure is operator-wide load (exponential backoff with jitter),
86
+ * per-user quota is "you specifically need to wait" (fixed window —
87
+ * server hint: `Retry-After: 5` seconds).
88
+ *
89
+ * v0.10.1+. Maps from HTTP 429 + JSON body
90
+ * `{"code":"per_user_quota_exhausted","user_id":"...","cap":N}`.
91
+ *
92
+ * The `userId` and `cap` fields are populated from the JSON body when
93
+ * the response is parseable; null when the server didn't include them
94
+ * (very old loomcycle binaries or non-JSON 429 responses).
95
+ *
96
+ * Typical handling:
97
+ *
98
+ * try { await client.runStreaming(...); }
99
+ * catch (e) {
100
+ * if (e instanceof PerUserQuotaExhaustedError) {
101
+ * // Wait the server-suggested window, then retry.
102
+ * await sleep(e.retryAfterMs ?? 5000);
103
+ * return client.runStreaming(...);
104
+ * }
105
+ * if (e instanceof BackpressureError) {
106
+ * // Operator-wide load — jittered backoff.
107
+ * await sleep(jittered(2000, 30000));
108
+ * return client.runStreaming(...);
109
+ * }
110
+ * throw e;
111
+ * }
112
+ */
113
+ class PerUserQuotaExhaustedError extends LoomcycleError {
114
+ /** Server-side user identifier the cap applies to. Null when the
115
+ * server didn't include it in the JSON body. */
116
+ userId;
117
+ /** Per-user cap value as configured on the server (active+queued).
118
+ * Null when the server didn't include it. */
119
+ cap;
120
+ /** Server-suggested retry window in milliseconds, from the
121
+ * Retry-After header. Null when absent. */
122
+ retryAfterMs;
123
+ constructor(message, opts) {
124
+ super(message, opts);
125
+ this.name = "PerUserQuotaExhaustedError";
126
+ this.userId = opts?.userId ?? null;
127
+ this.cap = opts?.cap ?? null;
128
+ this.retryAfterMs = opts?.retryAfterMs ?? null;
129
+ }
130
+ }
131
+ exports.PerUserQuotaExhaustedError = PerUserQuotaExhaustedError;
132
+ class AuthError extends LoomcycleError {
133
+ constructor(message, opts) {
134
+ super(message, opts);
135
+ this.name = "AuthError";
136
+ }
137
+ }
138
+ exports.AuthError = AuthError;
139
+ class UnavailableError extends LoomcycleError {
140
+ constructor(message, opts) {
141
+ super(message, opts);
142
+ this.name = "UnavailableError";
143
+ }
144
+ }
145
+ exports.UnavailableError = UnavailableError;
146
+ class InvalidArgumentError extends LoomcycleError {
147
+ constructor(message, opts) {
148
+ super(message, opts);
149
+ this.name = "InvalidArgumentError";
150
+ }
151
+ }
152
+ exports.InvalidArgumentError = InvalidArgumentError;
153
+ // ---- v0.8.18 — Pause/Snapshot typed errors ----
154
+ /** Subclasses UnavailableError for back-compat: code that broadly
155
+ * catches UnavailableError keeps working when this more-specific
156
+ * variant fires. */
157
+ class PauseNotConfiguredError extends UnavailableError {
158
+ constructor(message, opts) {
159
+ super(message, opts);
160
+ this.name = "PauseNotConfiguredError";
161
+ }
162
+ }
163
+ exports.PauseNotConfiguredError = PauseNotConfiguredError;
164
+ class AlreadyPausingError extends LoomcycleError {
165
+ constructor(message, opts) {
166
+ super(message, opts);
167
+ this.name = "AlreadyPausingError";
168
+ }
169
+ }
170
+ exports.AlreadyPausingError = AlreadyPausingError;
171
+ class NotPausedError extends LoomcycleError {
172
+ constructor(message, opts) {
173
+ super(message, opts);
174
+ this.name = "NotPausedError";
175
+ }
176
+ }
177
+ exports.NotPausedError = NotPausedError;
178
+ class SnapshotNotFoundError extends NotFoundError {
179
+ constructor(message, opts) {
180
+ super(message, opts);
181
+ this.name = "SnapshotNotFoundError";
182
+ }
183
+ }
184
+ exports.SnapshotNotFoundError = SnapshotNotFoundError;
185
+ class SnapshotTooLargeError extends LoomcycleError {
186
+ constructor(message, opts) {
187
+ super(message, opts);
188
+ this.name = "SnapshotTooLargeError";
189
+ }
190
+ }
191
+ exports.SnapshotTooLargeError = SnapshotTooLargeError;
192
+ class SnapshotVersionError extends LoomcycleError {
193
+ constructor(message, opts) {
194
+ super(message, opts);
195
+ this.name = "SnapshotVersionError";
196
+ }
197
+ }
198
+ exports.SnapshotVersionError = SnapshotVersionError;
199
+ /** HookNotFoundError — raised by deleteHook when no hook has the
200
+ * supplied id (HTTP 404 with "hook" in the body). Extends
201
+ * NotFoundError so consumers catching the broader category get this
202
+ * one too. */
203
+ class HookNotFoundError extends NotFoundError {
204
+ constructor(message, opts) {
205
+ super(message, opts);
206
+ this.name = "HookNotFoundError";
207
+ }
208
+ }
209
+ exports.HookNotFoundError = HookNotFoundError;
210
+ /** ChannelCursorRegressionError — raised by `client.ackChannel()`
211
+ * when the caller-supplied cursor is older than the currently-
212
+ * committed cursor for the (channel, scope, scope_id) tuple. HTTP
213
+ * 409 with `{code: "channel_cursor_regression", ...}` body.
214
+ *
215
+ * Mirrors `store.ErrChannelCursorRegression` on the loomcycle
216
+ * side. Distinct from `SessionBusyError` etc. (which also map to
217
+ * 409) so the n8n adapter can distinguish "this cursor is stale,
218
+ * re-fetch and retry from the new committed position" from other
219
+ * 409 conditions. */
220
+ class ChannelCursorRegressionError extends LoomcycleError {
221
+ constructor(message, opts) {
222
+ super(message, opts);
223
+ this.name = "ChannelCursorRegressionError";
224
+ }
225
+ }
226
+ exports.ChannelCursorRegressionError = ChannelCursorRegressionError;
227
+ /** SubstrateToolRefusedError — raised by `client.agentDef()` /
228
+ * `client.skillDef()` when the in-process tool refused the call
229
+ * (scope deny, empty body, allowed-tools widening, etc.). HTTP
230
+ * status 422 with `{code: "tool_refused", error, tool}` body.
231
+ *
232
+ * Distinct from transport failures: the request reached the
233
+ * server, the substrate tool ran, and the tool itself returned
234
+ * IsError=true. Operators catching this error should surface the
235
+ * reason in `message` to the calling agent / user rather than
236
+ * retrying. */
237
+ class SubstrateToolRefusedError extends LoomcycleError {
238
+ /** Which substrate tool refused — "AgentDef" or "SkillDef". */
239
+ tool;
240
+ constructor(message, opts) {
241
+ super(message, opts);
242
+ this.name = "SubstrateToolRefusedError";
243
+ this.tool = opts?.tool ?? "";
244
+ }
245
+ }
246
+ exports.SubstrateToolRefusedError = SubstrateToolRefusedError;
@@ -0,0 +1,253 @@
1
+ "use strict";
2
+ /**
3
+ * Shared HTTP plumbing for LoomcycleClient. Three responsibilities:
4
+ *
5
+ * 1. Build the Authorization header from the client's bearer token.
6
+ * 2. JSON encode/decode the request + response.
7
+ * 3. Map non-2xx responses to typed errors from errors.ts (single
8
+ * source of truth — mirrors Python's _raise_from_grpc).
9
+ *
10
+ * Method-level code in client.ts stays focused on URL + body shape;
11
+ * the boring fetch + error-translation machinery lives here.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.authHeaders = authHeaders;
15
+ exports.jsonFetch = jsonFetch;
16
+ exports.postJSON = postJSON;
17
+ exports.deleteRequest = deleteRequest;
18
+ exports.raiseFromResponse = raiseFromResponse;
19
+ const errors_js_1 = require("./errors.js");
20
+ /** authHeaders builds the standard request header set: JSON Accept
21
+ * + Bearer token when the client was constructed with one. The
22
+ * caller adds Content-Type when posting a body. */
23
+ function authHeaders(ctx) {
24
+ const h = { Accept: "application/json" };
25
+ if (ctx.authToken)
26
+ h.Authorization = `Bearer ${ctx.authToken}`;
27
+ return h;
28
+ }
29
+ /** jsonFetch performs a GET and unwraps the JSON body. Non-2xx
30
+ * status maps to a typed error via raiseFromResponse. */
31
+ async function jsonFetch(ctx, path, opts) {
32
+ const resp = await ctx.fetchImpl(ctx.baseUrl + path, {
33
+ method: "GET",
34
+ headers: authHeaders(ctx),
35
+ signal: opts?.signal,
36
+ });
37
+ if (!resp.ok) {
38
+ await raiseFromResponse(resp);
39
+ }
40
+ return (await resp.json());
41
+ }
42
+ /** postJSON sends a JSON-encoded body and unwraps the response.
43
+ * When `body` is undefined, no body is sent (Content-Type
44
+ * omitted). */
45
+ async function postJSON(ctx, path, body, opts) {
46
+ const headers = authHeaders(ctx);
47
+ let bodyStr;
48
+ if (body !== undefined) {
49
+ headers["Content-Type"] = "application/json";
50
+ bodyStr = JSON.stringify(body);
51
+ }
52
+ const resp = await ctx.fetchImpl(ctx.baseUrl + path, {
53
+ method: "POST",
54
+ headers,
55
+ body: bodyStr,
56
+ signal: opts?.signal,
57
+ });
58
+ if (!resp.ok) {
59
+ await raiseFromResponse(resp);
60
+ }
61
+ // Some endpoints return 204 No Content; tolerate that with a
62
+ // null cast — typed methods that know they return 204 use a
63
+ // void wrapper instead.
64
+ if (resp.status === 204)
65
+ return null;
66
+ return (await resp.json());
67
+ }
68
+ /** deleteRequest sends a DELETE and tolerates 204/200/404-with-
69
+ * idempotent-semantics per the loomcycle wire contract. */
70
+ async function deleteRequest(ctx, path, opts) {
71
+ const resp = await ctx.fetchImpl(ctx.baseUrl + path, {
72
+ method: "DELETE",
73
+ headers: authHeaders(ctx),
74
+ signal: opts?.signal,
75
+ });
76
+ if (!resp.ok) {
77
+ await raiseFromResponse(resp);
78
+ }
79
+ }
80
+ /**
81
+ * raiseFromResponse — the single point where HTTP status + body
82
+ * text get mapped to typed errors. Always throws; the function
83
+ * signature returns `never` only because TypeScript needs the
84
+ * return type for control-flow narrowing.
85
+ *
86
+ * Mapping table:
87
+ *
88
+ * 400 → InvalidArgumentError
89
+ * 401 → AuthError
90
+ * 404 + "snapshot" → SnapshotNotFoundError ────────┐
91
+ * 404 + "session" → SessionNotFoundError │ All extend
92
+ * 404 + "hook" → HookNotFoundError │ NotFoundError —
93
+ * 404 + "agent" → AgentNotFoundError │ callers can
94
+ * 404 + (other) → NotFoundError (base) │ catch any 404
95
+ * │ with one
96
+ * │ instanceof.
97
+ * 409 + "already_pausing" / "already paused" → AlreadyPausingError
98
+ * 409 + "not_paused" / "not paused" → NotPausedError
99
+ * 409 + "session" → SessionBusyError
100
+ * 409 + "agent_id" → AgentIDInUseError
101
+ * 409 + (other) → LoomcycleError (base)
102
+ * 413 → SnapshotTooLargeError
103
+ * 422 → SnapshotVersionError (snapshot-version-too-new/unknown)
104
+ * 429 → BackpressureError
105
+ * 503 + "pause manager not configured" → PauseNotConfiguredError
106
+ * (subclass of UnavailableError)
107
+ * 503 + (other) → UnavailableError
108
+ * 500-599 (other) → LoomcycleError (base)
109
+ * default → LoomcycleError (base)
110
+ *
111
+ * Priority within a status group is most-specific-first; an unknown
112
+ * 409 falls through to base LoomcycleError so callers see a
113
+ * meaningful message + status. For 404, the catch-all is NotFoundError
114
+ * (base) so the v0.8.18-added memory + interrupt routes don't
115
+ * misclassify into AgentNotFoundError when the 404 body doesn't
116
+ * mention "agent".
117
+ */
118
+ async function raiseFromResponse(resp) {
119
+ const status = resp.status;
120
+ // Read body with a cap; many error bodies are JSON {error, message}
121
+ // shape but raw text is fine for matching keywords.
122
+ let bodyText = "";
123
+ try {
124
+ bodyText = await resp.text();
125
+ }
126
+ catch {
127
+ // network-level body read failure — fall through with empty body
128
+ }
129
+ const bodyLower = bodyText.toLowerCase();
130
+ // HTTP/2 strips reason phrases — Node's undici fetch returns "" for
131
+ // resp.statusText on HTTP/2 responses. Fall back to a stock phrase
132
+ // for the common status codes so the error message reads cleanly
133
+ // ("401 Unauthorized" not "401 " with a trailing space).
134
+ const statusPhrase = resp.statusText || stockStatusPhrase(status);
135
+ const msg = bodyText.trim() ? bodyText.slice(0, 1024) : `${status} ${statusPhrase}`;
136
+ const opts = { status, bodyText: bodyText.slice(0, 1024) };
137
+ switch (status) {
138
+ case 400:
139
+ throw new errors_js_1.InvalidArgumentError(msg, opts);
140
+ case 401:
141
+ throw new errors_js_1.AuthError(msg, opts);
142
+ case 404:
143
+ // Priority: most-specific keyword wins.
144
+ // - "snapshot" → SnapshotNotFoundError
145
+ // - "session" → SessionNotFoundError
146
+ // - "hook" → HookNotFoundError (must precede "agent" — the
147
+ // hooks 404 body is `no hook with id "..."`,
148
+ // doesn't mention "agent")
149
+ // - "agent" or "agent_id" → AgentNotFoundError
150
+ // - otherwise → NotFoundError (base) — e.g. memory rows, interrupts,
151
+ // or any future 404-returning endpoint that doesn't fit the
152
+ // existing keyword set.
153
+ if (bodyLower.includes("snapshot"))
154
+ throw new errors_js_1.SnapshotNotFoundError(msg, opts);
155
+ if (bodyLower.includes("session"))
156
+ throw new errors_js_1.SessionNotFoundError(msg, opts);
157
+ if (bodyLower.includes("hook"))
158
+ throw new errors_js_1.HookNotFoundError(msg, opts);
159
+ if (bodyLower.includes("agent"))
160
+ throw new errors_js_1.AgentNotFoundError(msg, opts);
161
+ throw new errors_js_1.NotFoundError(msg, opts);
162
+ case 409:
163
+ if (bodyLower.includes("already_pausing") || bodyLower.includes("already paused"))
164
+ throw new errors_js_1.AlreadyPausingError(msg, opts);
165
+ if (bodyLower.includes("not_paused") || bodyLower.includes("not paused"))
166
+ throw new errors_js_1.NotPausedError(msg, opts);
167
+ // v0.9.x — Channel CRUD ack with a stale cursor. Distinct so
168
+ // the n8n adapter / consumer can branch on `instanceof`.
169
+ if (bodyLower.includes("channel_cursor_regression"))
170
+ throw new errors_js_1.ChannelCursorRegressionError(msg, opts);
171
+ if (bodyLower.includes("session"))
172
+ throw new errors_js_1.SessionBusyError(msg, opts);
173
+ if (bodyLower.includes("agent_id"))
174
+ throw new errors_js_1.AgentIDInUseError(msg, opts);
175
+ throw new errors_js_1.LoomcycleError(msg, opts);
176
+ case 413:
177
+ throw new errors_js_1.SnapshotTooLargeError(msg, opts);
178
+ case 422: {
179
+ // 422 is shared between snapshot version errors (existing)
180
+ // and v0.8.22 substrate tool refusals. Discriminate by body:
181
+ // the substrate path returns `{code: "tool_refused", tool,
182
+ // error}` JSON; the snapshot path returns a free-form text.
183
+ try {
184
+ const parsed = JSON.parse(bodyText);
185
+ if (parsed.code === "tool_refused") {
186
+ throw new errors_js_1.SubstrateToolRefusedError(parsed.error ?? msg, { status, bodyText, tool: parsed.tool });
187
+ }
188
+ }
189
+ catch (e) {
190
+ // Re-throw our typed error if we matched; fall through to
191
+ // SnapshotVersionError on any JSON-parse failure.
192
+ if (e instanceof errors_js_1.SubstrateToolRefusedError)
193
+ throw e;
194
+ }
195
+ throw new errors_js_1.SnapshotVersionError(msg, opts);
196
+ }
197
+ case 429: {
198
+ // v0.10.1: distinguish per-user quota exhaustion from
199
+ // operator-wide backpressure. The shapes share the 429 status
200
+ // but the JSON body's `code` field discriminates. Consumers
201
+ // branch retry strategies on the typed error.
202
+ try {
203
+ const parsed = JSON.parse(bodyText);
204
+ if (parsed.code === "per_user_quota_exhausted") {
205
+ // Retry-After is `<seconds>` per RFC; convert to ms.
206
+ const retryAfterRaw = resp.headers.get("retry-after");
207
+ const retryAfterMs = retryAfterRaw
208
+ ? Number.parseInt(retryAfterRaw, 10) * 1000
209
+ : undefined;
210
+ throw new errors_js_1.PerUserQuotaExhaustedError(parsed.error ?? msg, {
211
+ status,
212
+ bodyText,
213
+ userId: parsed.user_id,
214
+ cap: parsed.cap,
215
+ retryAfterMs: Number.isFinite(retryAfterMs) ? retryAfterMs : undefined,
216
+ });
217
+ }
218
+ }
219
+ catch (e) {
220
+ // Re-throw the typed error; fall through on JSON-parse fail.
221
+ if (e instanceof errors_js_1.PerUserQuotaExhaustedError)
222
+ throw e;
223
+ }
224
+ throw new errors_js_1.BackpressureError(msg, opts);
225
+ }
226
+ case 503:
227
+ if (bodyLower.includes("pause") && bodyLower.includes("not configured"))
228
+ throw new errors_js_1.PauseNotConfiguredError(msg, opts);
229
+ throw new errors_js_1.UnavailableError(msg, opts);
230
+ default:
231
+ throw new errors_js_1.LoomcycleError(msg, opts);
232
+ }
233
+ }
234
+ /** stockStatusPhrase returns a stock reason phrase for the common
235
+ * HTTP statuses raiseFromResponse handles. Used as a fallback when
236
+ * Response.statusText is empty (HTTP/2 strips reason phrases). */
237
+ function stockStatusPhrase(status) {
238
+ switch (status) {
239
+ case 400: return "Bad Request";
240
+ case 401: return "Unauthorized";
241
+ case 403: return "Forbidden";
242
+ case 404: return "Not Found";
243
+ case 409: return "Conflict";
244
+ case 413: return "Payload Too Large";
245
+ case 422: return "Unprocessable Entity";
246
+ case 429: return "Too Many Requests";
247
+ case 500: return "Internal Server Error";
248
+ case 502: return "Bad Gateway";
249
+ case 503: return "Service Unavailable";
250
+ case 504: return "Gateway Timeout";
251
+ default: return "HTTP " + status;
252
+ }
253
+ }
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ /**
3
+ * @loomcycle/client — TypeScript client for the loomcycle sidecar.
4
+ *
5
+ * Public surface (v0.8.18 — Python-adapter parity):
6
+ *
7
+ * class LoomcycleClient
8
+ * constructor(opts: ClientOptions)
9
+ *
10
+ * // Run lifecycle (SSE streams)
11
+ * runStreaming(opts: RunOptions): AsyncIterable<AgentEvent>
12
+ * continueSession(opts: ContinueOptions): AsyncIterable<AgentEvent>
13
+ *
14
+ * // Agent metadata
15
+ * getAgent(agentId): Promise<Agent>
16
+ * cancelAgent(agentId, opts?): Promise<CancelAgentResult>
17
+ * listUserAgents(userId, opts?): Promise<Agent[]>
18
+ * getTranscript(sessionId): Promise<TranscriptResponse>
19
+ * health(): Promise<HealthResponse>
20
+ * listUsers(): Promise<ListUsersResponse>
21
+ *
22
+ * // Pause / Resume / State (v0.8.17/8.18)
23
+ * pauseRuntime(opts?): Promise<PauseResult>
24
+ * resumeRuntime(): Promise<ResumeResult>
25
+ * getRuntimeState(): Promise<RuntimeStateResponse>
26
+ *
27
+ * // Snapshot lifecycle (v0.8.17/8.18)
28
+ * createSnapshot(opts?): Promise<SnapshotCreateResponse>
29
+ * listSnapshots(opts?): Promise<SnapshotDescriptor[]>
30
+ * getSnapshot(id): Promise<SnapshotEnvelope>
31
+ * exportSnapshotURL(id): string (synchronous; returns a URL)
32
+ * restoreSnapshot(opts): Promise<SnapshotRestoreResponse>
33
+ * deleteSnapshot(id): Promise<void>
34
+ *
35
+ * // Memory admin
36
+ * listMemoryScopes(): Promise<MemoryScopesResponse>
37
+ * listMemoryScopeIDs(scope): Promise<MemoryScopeIDsResponse>
38
+ * listMemoryEntries(scope, scopeID, opts?): Promise<MemoryEntriesResponse>
39
+ * getMemoryEntry(scope, scopeID, key): Promise<MemoryEntryResponse>
40
+ *
41
+ * // Interruption (v0.8.16)
42
+ * listUserInterrupts(userId, opts?): Promise<InterruptListResponse>
43
+ * listRunInterrupts(runId, opts?): Promise<InterruptListResponse>
44
+ * resolveInterrupt(runId, interruptId, opts): Promise<unknown>
45
+ *
46
+ * // Substrate admin (v0.8.22)
47
+ * agentDef(input): Promise<SubstrateToolResponse>
48
+ * skillDef(input): Promise<SubstrateToolResponse>
49
+ *
50
+ * Errors (typed subclasses of LoomcycleError; see README for the
51
+ * full HTTP-status → typed-error mapping table):
52
+ * LoomcycleError, AgentNotFoundError, SessionNotFoundError,
53
+ * SessionBusyError, AgentIDInUseError, BackpressureError,
54
+ * AuthError, UnavailableError, InvalidArgumentError,
55
+ * PauseNotConfiguredError (subclass of UnavailableError),
56
+ * AlreadyPausingError, NotPausedError, SnapshotNotFoundError,
57
+ * SnapshotTooLargeError, SnapshotVersionError,
58
+ * SubstrateToolRefusedError (v0.8.22)
59
+ *
60
+ * Transport: HTTP+SSE. Auth: Bearer token via the Authorization
61
+ * header. Designed for Node ≥18 (engines pinned); Bun/Deno likely
62
+ * work but untested. Browser support is not a target (use the
63
+ * Web UI for browser-side operator control).
64
+ *
65
+ * See `adapters/ts/README.md` for usage examples.
66
+ */
67
+ Object.defineProperty(exports, "__esModule", { value: true });
68
+ exports.UnavailableError = exports.SubstrateToolRefusedError = exports.SnapshotVersionError = exports.SnapshotTooLargeError = exports.SnapshotNotFoundError = exports.SessionNotFoundError = exports.SessionBusyError = exports.PerUserQuotaExhaustedError = exports.PauseNotConfiguredError = exports.NotPausedError = exports.LoomcycleError = exports.ChannelCursorRegressionError = exports.InvalidArgumentError = exports.NotFoundError = exports.HookNotFoundError = exports.BackpressureError = exports.AuthError = exports.AlreadyPausingError = exports.AgentNotFoundError = exports.AgentIDInUseError = exports.LoomcycleClient = void 0;
69
+ var client_js_1 = require("./client.js");
70
+ Object.defineProperty(exports, "LoomcycleClient", { enumerable: true, get: function () { return client_js_1.LoomcycleClient; } });
71
+ var errors_js_1 = require("./errors.js");
72
+ Object.defineProperty(exports, "AgentIDInUseError", { enumerable: true, get: function () { return errors_js_1.AgentIDInUseError; } });
73
+ Object.defineProperty(exports, "AgentNotFoundError", { enumerable: true, get: function () { return errors_js_1.AgentNotFoundError; } });
74
+ Object.defineProperty(exports, "AlreadyPausingError", { enumerable: true, get: function () { return errors_js_1.AlreadyPausingError; } });
75
+ Object.defineProperty(exports, "AuthError", { enumerable: true, get: function () { return errors_js_1.AuthError; } });
76
+ Object.defineProperty(exports, "BackpressureError", { enumerable: true, get: function () { return errors_js_1.BackpressureError; } });
77
+ Object.defineProperty(exports, "HookNotFoundError", { enumerable: true, get: function () { return errors_js_1.HookNotFoundError; } });
78
+ Object.defineProperty(exports, "NotFoundError", { enumerable: true, get: function () { return errors_js_1.NotFoundError; } });
79
+ Object.defineProperty(exports, "InvalidArgumentError", { enumerable: true, get: function () { return errors_js_1.InvalidArgumentError; } });
80
+ Object.defineProperty(exports, "ChannelCursorRegressionError", { enumerable: true, get: function () { return errors_js_1.ChannelCursorRegressionError; } });
81
+ Object.defineProperty(exports, "LoomcycleError", { enumerable: true, get: function () { return errors_js_1.LoomcycleError; } });
82
+ Object.defineProperty(exports, "NotPausedError", { enumerable: true, get: function () { return errors_js_1.NotPausedError; } });
83
+ Object.defineProperty(exports, "PauseNotConfiguredError", { enumerable: true, get: function () { return errors_js_1.PauseNotConfiguredError; } });
84
+ Object.defineProperty(exports, "PerUserQuotaExhaustedError", { enumerable: true, get: function () { return errors_js_1.PerUserQuotaExhaustedError; } });
85
+ Object.defineProperty(exports, "SessionBusyError", { enumerable: true, get: function () { return errors_js_1.SessionBusyError; } });
86
+ Object.defineProperty(exports, "SessionNotFoundError", { enumerable: true, get: function () { return errors_js_1.SessionNotFoundError; } });
87
+ Object.defineProperty(exports, "SnapshotNotFoundError", { enumerable: true, get: function () { return errors_js_1.SnapshotNotFoundError; } });
88
+ Object.defineProperty(exports, "SnapshotTooLargeError", { enumerable: true, get: function () { return errors_js_1.SnapshotTooLargeError; } });
89
+ Object.defineProperty(exports, "SnapshotVersionError", { enumerable: true, get: function () { return errors_js_1.SnapshotVersionError; } });
90
+ Object.defineProperty(exports, "SubstrateToolRefusedError", { enumerable: true, get: function () { return errors_js_1.SubstrateToolRefusedError; } });
91
+ Object.defineProperty(exports, "UnavailableError", { enumerable: true, get: function () { return errors_js_1.UnavailableError; } });
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseSSE = parseSSE;
4
+ /**
5
+ * parseSSE turns a chunked byte stream into typed AgentEvents.
6
+ *
7
+ * SSE framing (subset): "event: <name>\ndata: <json>\n\n". We only emit a
8
+ * frame when both event + data have been seen since the last blank line.
9
+ *
10
+ * Used by `runStreaming` and `continueSession` — both POST endpoints
11
+ * return the same SSE wire shape and the parser doesn't differentiate.
12
+ *
13
+ * Side-channel frames: the v0.4 `event: agent` SSE frame (and any future
14
+ * sse.sendRaw user) emits a JSON payload that does NOT carry the `type`
15
+ * field — the SSE event name is the only discriminator. parseSSE backfills
16
+ * `type` from the event name in that case so consumers see a well-formed
17
+ * AgentEvent and switch on `ev.type` uniformly.
18
+ */
19
+ async function* parseSSE(reader) {
20
+ const decoder = new TextDecoder("utf-8");
21
+ let buf = "";
22
+ let event = "";
23
+ let data = "";
24
+ const flush = () => {
25
+ if (!event && !data)
26
+ return null;
27
+ if (!data) {
28
+ event = "";
29
+ return null;
30
+ }
31
+ try {
32
+ const parsed = JSON.parse(data);
33
+ // Side-channel sendRaw frames omit `type` in the JSON payload — the
34
+ // SSE event name is the only discriminator. Backfill it so the
35
+ // consumer's switch on ev.type doesn't miss these.
36
+ if (!parsed.type && event) {
37
+ parsed.type = event;
38
+ }
39
+ event = "";
40
+ data = "";
41
+ return parsed;
42
+ }
43
+ catch {
44
+ event = "";
45
+ data = "";
46
+ return null;
47
+ }
48
+ };
49
+ while (true) {
50
+ const { value, done } = await reader.read();
51
+ if (done)
52
+ break;
53
+ buf += decoder.decode(value, { stream: true });
54
+ let idx;
55
+ while ((idx = buf.indexOf("\n")) !== -1) {
56
+ const line = buf.slice(0, idx).replace(/\r$/, "");
57
+ buf = buf.slice(idx + 1);
58
+ if (line === "") {
59
+ const ev = flush();
60
+ if (ev)
61
+ yield ev;
62
+ continue;
63
+ }
64
+ if (line.startsWith("event:"))
65
+ event = line.slice("event:".length).trim();
66
+ else if (line.startsWith("data:"))
67
+ data = line.slice("data:".length).trim();
68
+ }
69
+ }
70
+ // Stream ended. Drain any unterminated final line still in `buf` — a
71
+ // connection drop can land here mid-frame, and without this step the
72
+ // last frame whose `\n` never arrived would be silently lost. Then
73
+ // flush any pending event + data.
74
+ if (buf.length > 0) {
75
+ const line = buf.replace(/\r$/, "");
76
+ if (line.startsWith("event:"))
77
+ event = line.slice("event:".length).trim();
78
+ else if (line.startsWith("data:"))
79
+ data = line.slice("data:".length).trim();
80
+ }
81
+ const ev = flush();
82
+ if (ev)
83
+ yield ev;
84
+ }