@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/README.md +276 -0
- package/dist/client.d.ts +213 -0
- package/dist/client.js +366 -0
- package/dist/errors.d.ts +135 -0
- package/dist/errors.js +138 -0
- package/dist/fetch-helpers.d.ts +89 -0
- package/dist/fetch-helpers.js +198 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +62 -0
- package/dist/stream.d.ts +17 -0
- package/dist/stream.js +81 -0
- package/dist/types.d.ts +479 -0
- package/dist/types.js +10 -0
- package/package.json +32 -0
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
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -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
|
+
}
|