@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.
- package/README.md +18 -0
- package/dist/cjs/client.js +734 -0
- package/dist/cjs/errors.js +246 -0
- package/dist/cjs/fetch-helpers.js +253 -0
- package/dist/cjs/index.js +91 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/stream.js +84 -0
- package/dist/cjs/types.js +11 -0
- package/dist/errors.d.ts +50 -0
- package/dist/errors.js +50 -0
- package/dist/fetch-helpers.js +29 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +9 -5
|
@@ -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,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
|
+
}
|