@loomcycle/client 1.111.0 → 1.113.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +287 -29
- package/dist/cjs/client.js +647 -20
- package/dist/cjs/errors.js +119 -15
- package/dist/cjs/fetch-helpers.js +39 -8
- package/dist/cjs/index.js +55 -8
- package/dist/cjs/interactive.js +5 -1
- package/dist/cjs/signature.js +119 -0
- package/dist/client.d.ts +494 -30
- package/dist/client.js +647 -20
- package/dist/errors.d.ts +92 -14
- package/dist/errors.js +116 -14
- package/dist/fetch-helpers.d.ts +3 -0
- package/dist/fetch-helpers.js +40 -9
- package/dist/index.d.ts +53 -9
- package/dist/index.js +51 -8
- package/dist/interactive.js +5 -1
- package/dist/signature.d.ts +73 -0
- package/dist/signature.js +115 -0
- package/dist/types.d.ts +1315 -17
- package/package.json +1 -1
package/dist/cjs/errors.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* to raise typed errors via `raiseFromResponse`.
|
|
20
20
|
*/
|
|
21
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
exports.SubstrateToolRefusedError = exports.ChannelCursorRegressionError = exports.SnapshotVersionError = exports.PromptTooLargeError = exports.RequestTooLargeError = exports.SnapshotTooLargeError = exports.SnapshotNotFoundError = exports.NotPausedError = exports.AlreadyPausingError = exports.PauseNotConfiguredError = exports.InvalidArgumentError = exports.UnavailableError = exports.AuthError = exports.PerUserQuotaExhaustedError = exports.TokenLimitExceededError = exports.BackpressureError = exports.AgentIDInUseError = exports.SessionBusyError = exports.SessionNotFoundError = exports.AgentNotFoundError = exports.NotFoundError = exports.LoomcycleError = void 0;
|
|
22
|
+
exports.SubstrateToolRefusedError = exports.ChannelCursorRegressionError = exports.SnapshotVersionError = exports.PromptTooLargeError = exports.RequestTooLargeError = exports.SnapshotTooLargeError = exports.SnapshotNotFoundError = exports.NotPausedError = exports.AlreadyPausingError = exports.PauseNotConfiguredError = exports.InvalidArgumentError = exports.UnavailableError = exports.InsufficientScopeError = exports.AuthError = exports.PerUserQuotaExhaustedError = exports.TokenLimitExceededError = exports.RateLimitedError = exports.BackpressureError = exports.AgentIDInUseError = exports.SessionBusyError = exports.SessionNotFoundError = exports.AgentNotFoundError = exports.NotFoundError = exports.LoomcycleError = void 0;
|
|
23
23
|
class LoomcycleError extends Error {
|
|
24
24
|
status;
|
|
25
25
|
bodyText;
|
|
@@ -36,8 +36,43 @@ class LoomcycleError extends Error {
|
|
|
36
36
|
this.bodyText = opts?.bodyText;
|
|
37
37
|
this.code = opts?.code;
|
|
38
38
|
}
|
|
39
|
+
/** Whether the same call, sent again unchanged, can succeed once time has
|
|
40
|
+
* passed: true for a refusal that clears on its own (backpressure, a rate
|
|
41
|
+
* limit, a per-user run quota, a paused runtime, a decision-model call
|
|
42
|
+
* that failed or timed out), false for everything else, including a hard
|
|
43
|
+
* token budget, a cap the caller must make room under, a bad request and
|
|
44
|
+
* a missing resource. A retry loop can test this instead of a list of
|
|
45
|
+
* classes.
|
|
46
|
+
*
|
|
47
|
+
* It is a getter rather than a constructor field so that a subclass states
|
|
48
|
+
* its answer by overriding it, without a constructor option any caller
|
|
49
|
+
* could set. False is the default because a wrong `true` turns a refusal
|
|
50
|
+
* that never clears into a loop that never ends, while a wrong `false`
|
|
51
|
+
* only surfaces an error the caller could have waited out. The client
|
|
52
|
+
* itself never retries; bound your own retries even when this is true. */
|
|
53
|
+
get retryable() {
|
|
54
|
+
return this.code !== undefined && TRANSIENT_CODES.has(this.code);
|
|
55
|
+
}
|
|
39
56
|
}
|
|
40
57
|
exports.LoomcycleError = LoomcycleError;
|
|
58
|
+
/** Codes that clear on their own although the class their status maps to
|
|
59
|
+
* does not: a 503 is mostly a feature the deployment lacks, and a 502 / 504
|
|
60
|
+
* is any upstream failure, so the class alone says false. The server
|
|
61
|
+
* classifies each of these as transient:
|
|
62
|
+
* - `runtime_paused` (503): new runs are refused until the operator resumes.
|
|
63
|
+
* - `call_failed` (502): a decision-model call did not complete.
|
|
64
|
+
* - `timeout` (504): a decision-model call did not answer in time. */
|
|
65
|
+
const TRANSIENT_CODES = new Set([
|
|
66
|
+
"runtime_paused",
|
|
67
|
+
"call_failed",
|
|
68
|
+
"timeout",
|
|
69
|
+
]);
|
|
70
|
+
/** 429 codes that do NOT clear by waiting: the caller has to act first, so
|
|
71
|
+
* a retry loop on them never ends. `configured_run_cap`: the user already
|
|
72
|
+
* holds as many configured (not yet started) runs as the operator allows,
|
|
73
|
+
* and only starting or discarding one makes room. (A hard token budget,
|
|
74
|
+
* `token_limit_exceeded`, has a class of its own outside BackpressureError.) */
|
|
75
|
+
const CALLER_ACTION_429_CODES = new Set(["configured_run_cap"]);
|
|
41
76
|
/** Base class for every HTTP 404 the client surfaces. Lets callers
|
|
42
77
|
* catch any not-found case with a single `instanceof NotFoundError`
|
|
43
78
|
* check, regardless of which specific resource was missing
|
|
@@ -78,17 +113,48 @@ class AgentIDInUseError extends LoomcycleError {
|
|
|
78
113
|
}
|
|
79
114
|
}
|
|
80
115
|
exports.AgentIDInUseError = AgentIDInUseError;
|
|
81
|
-
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
116
|
+
/** An HTTP 429 that clears on its own: the server is busy (`backpressure`),
|
|
117
|
+
* a provider's queue is full (`provider_concurrency_exhausted`), or the
|
|
118
|
+
* caller is starting runs too fast (`rate_limited`, raised as the
|
|
119
|
+
* RateLimitedError subclass). Back off and retry; `retryable` is true.
|
|
120
|
+
*
|
|
121
|
+
* One code under this class is the exception: `configured_run_cap` (the user
|
|
122
|
+
* holds as many configured runs as allowed; start or discard one first)
|
|
123
|
+
* needs the caller to act, so its `retryable` is false. It stays a
|
|
124
|
+
* BackpressureError, the class every other 429 lands in.
|
|
125
|
+
*
|
|
126
|
+
* Two 429s are not this class: a per-user run quota
|
|
127
|
+
* (PerUserQuotaExhaustedError, also retryable) and a hard token budget
|
|
128
|
+
* (TokenLimitExceededError, not retryable). */
|
|
85
129
|
class BackpressureError extends LoomcycleError {
|
|
86
130
|
constructor(message, opts) {
|
|
87
131
|
super(message, opts);
|
|
88
132
|
this.name = "BackpressureError";
|
|
89
133
|
}
|
|
134
|
+
get retryable() {
|
|
135
|
+
return this.code === undefined || !CALLER_ACTION_429_CODES.has(this.code);
|
|
136
|
+
}
|
|
90
137
|
}
|
|
91
138
|
exports.BackpressureError = BackpressureError;
|
|
139
|
+
/**
|
|
140
|
+
* RateLimitedError signals that the caller is starting runs faster than its
|
|
141
|
+
* per-caller run-start rate allows: HTTP 429 with body code `rate_limited`.
|
|
142
|
+
* The server says when the next start will be admitted in `Retry-After`,
|
|
143
|
+
* surfaced here as `retryAfterMs`; wait that long rather than backing off on
|
|
144
|
+
* a guess. Retryable, like every BackpressureError.
|
|
145
|
+
*/
|
|
146
|
+
class RateLimitedError extends BackpressureError {
|
|
147
|
+
/** Server-suggested retry window in milliseconds, from the Retry-After
|
|
148
|
+
* header. Null when the header is absent or not a whole number of
|
|
149
|
+
* seconds. */
|
|
150
|
+
retryAfterMs;
|
|
151
|
+
constructor(message, opts) {
|
|
152
|
+
super(message, opts);
|
|
153
|
+
this.name = "RateLimitedError";
|
|
154
|
+
this.retryAfterMs = opts?.retryAfterMs ?? null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
exports.RateLimitedError = RateLimitedError;
|
|
92
158
|
/**
|
|
93
159
|
* TokenLimitExceededError signals that the caller is at a hard token budget:
|
|
94
160
|
* HTTP 429 with body code `token_limit_exceeded`, from run creation and from
|
|
@@ -98,20 +164,18 @@ exports.BackpressureError = BackpressureError;
|
|
|
98
164
|
* month's tokens for an operator, tenant or user; the same call is refused
|
|
99
165
|
* until an operator raises the budget or the window rolls over.
|
|
100
166
|
*
|
|
101
|
-
* It
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* // transient: back off and retry
|
|
109
|
-
* }
|
|
167
|
+
* It is not a BackpressureError, and `retryable` is false. It used to
|
|
168
|
+
* extend BackpressureError so that an older `instanceof BackpressureError`
|
|
169
|
+
* check kept catching it, but such a check is almost always a retry loop, and
|
|
170
|
+
* that loop then retried a refusal that cannot clear for the rest of the
|
|
171
|
+
* month. The relation was dropped on purpose: a retry loop keyed on
|
|
172
|
+
* BackpressureError or on `retryable` now stops here, and code that wants to
|
|
173
|
+
* report the budget tests for this class.
|
|
110
174
|
*
|
|
111
175
|
* The fields come from the response body and are null when the server did
|
|
112
176
|
* not send them.
|
|
113
177
|
*/
|
|
114
|
-
class TokenLimitExceededError extends
|
|
178
|
+
class TokenLimitExceededError extends LoomcycleError {
|
|
115
179
|
/** Whose budget was hit: "operator", "tenant" or "user". */
|
|
116
180
|
scope;
|
|
117
181
|
/** The id of that scope: the tenant id or the user subject. Null for the
|
|
@@ -183,6 +247,11 @@ class PerUserQuotaExhaustedError extends LoomcycleError {
|
|
|
183
247
|
this.cap = opts?.cap ?? null;
|
|
184
248
|
this.retryAfterMs = opts?.retryAfterMs ?? null;
|
|
185
249
|
}
|
|
250
|
+
/** The quota counts the caller's own in-flight runs, so it frees up as
|
|
251
|
+
* they finish. */
|
|
252
|
+
get retryable() {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
186
255
|
}
|
|
187
256
|
exports.PerUserQuotaExhaustedError = PerUserQuotaExhaustedError;
|
|
188
257
|
class AuthError extends LoomcycleError {
|
|
@@ -192,6 +261,37 @@ class AuthError extends LoomcycleError {
|
|
|
192
261
|
}
|
|
193
262
|
}
|
|
194
263
|
exports.AuthError = AuthError;
|
|
264
|
+
/** An HTTP 403 because the bearer token lacks a scope the call needs: body code
|
|
265
|
+
* `insufficient_scope`. The token is valid; it was minted without that
|
|
266
|
+
* grant. Ask an operator for a token that carries `requiredScope`; sending
|
|
267
|
+
* the same call again never succeeds, so `retryable` is false.
|
|
268
|
+
*
|
|
269
|
+
* Any other 403 (`restricted_token`, `operator_key_restricted`, …) stays a
|
|
270
|
+
* plain LoomcycleError: those refuse the caller for something a new scope
|
|
271
|
+
* does not fix. */
|
|
272
|
+
class InsufficientScopeError extends LoomcycleError {
|
|
273
|
+
/** The scope the token lacks, e.g. `runs:create` or `substrate:tenant`.
|
|
274
|
+
* Read from the body's `required_scope`, or, from a server that predates
|
|
275
|
+
* that field, from the `WWW-Authenticate: Bearer scope="…"` header. Null
|
|
276
|
+
* when neither names one (a browser does not see that header on a
|
|
277
|
+
* cross-origin response unless the server exposes it). */
|
|
278
|
+
requiredScope;
|
|
279
|
+
constructor(message, opts) {
|
|
280
|
+
super(message, opts);
|
|
281
|
+
this.name = "InsufficientScopeError";
|
|
282
|
+
this.requiredScope = opts?.requiredScope ?? null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
exports.InsufficientScopeError = InsufficientScopeError;
|
|
286
|
+
/** Every HTTP 503. The status is shared between a condition that clears on
|
|
287
|
+
* its own and features this deployment does not have
|
|
288
|
+
* (`decision_not_configured`, `store_unavailable`, `vector_unsupported`, …),
|
|
289
|
+
* which never will, and many 503 bodies carry no code at all. So
|
|
290
|
+
* `retryable` is true only for `runtime_paused` (new runs are refused while
|
|
291
|
+
* the operator has the runtime paused) and false otherwise. A code-less 503
|
|
292
|
+
* from run creation can also be a model tier with no provider available
|
|
293
|
+
* right now; retry that one on your own judgement, with a bound. (The
|
|
294
|
+
* inherited `retryable` reads `runtime_paused` from TRANSIENT_CODES.) */
|
|
195
295
|
class UnavailableError extends LoomcycleError {
|
|
196
296
|
constructor(message, opts) {
|
|
197
297
|
super(message, opts);
|
|
@@ -215,6 +315,10 @@ class PauseNotConfiguredError extends UnavailableError {
|
|
|
215
315
|
super(message, opts);
|
|
216
316
|
this.name = "PauseNotConfiguredError";
|
|
217
317
|
}
|
|
318
|
+
/** The server has no pause manager; asking again does not give it one. */
|
|
319
|
+
get retryable() {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
218
322
|
}
|
|
219
323
|
exports.PauseNotConfiguredError = PauseNotConfiguredError;
|
|
220
324
|
class AlreadyPausingError extends LoomcycleError {
|
|
@@ -167,6 +167,9 @@ async function deleteJSON(ctx, path, opts) {
|
|
|
167
167
|
*
|
|
168
168
|
* 400 → InvalidArgumentError
|
|
169
169
|
* 401 → AuthError
|
|
170
|
+
* 403 + code "insufficient_scope" (or an older server's
|
|
171
|
+
* WWW-Authenticate scope hint) → InsufficientScopeError
|
|
172
|
+
* 403 (other) → LoomcycleError (base)
|
|
170
173
|
* 404 + "snapshot" → SnapshotNotFoundError ────────┐
|
|
171
174
|
* 404 + "session" → SessionNotFoundError │ All extend
|
|
172
175
|
* 404 + "agent" → AgentNotFoundError │ callers can
|
|
@@ -226,6 +229,19 @@ async function raiseFromResponse(resp) {
|
|
|
226
229
|
throw new errors_js_1.InvalidArgumentError(msg, opts);
|
|
227
230
|
case 401:
|
|
228
231
|
throw new errors_js_1.AuthError(msg, opts);
|
|
232
|
+
case 403: {
|
|
233
|
+
// A missing scope says so in its code. A server from before the JSON
|
|
234
|
+
// body sent "insufficient scope" as plain text; the scope hint in
|
|
235
|
+
// WWW-Authenticate, which no other 403 carries, still identifies it.
|
|
236
|
+
const hinted = scopeFromWWWAuthenticate(resp);
|
|
237
|
+
if (opts.code === "insufficient_scope" || (opts.code === undefined && hinted !== undefined)) {
|
|
238
|
+
throw new errors_js_1.InsufficientScopeError(msg, {
|
|
239
|
+
...opts,
|
|
240
|
+
requiredScope: stringField(bodyObject(bodyText)?.required_scope) ?? hinted,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
throw new errors_js_1.LoomcycleError(msg, opts);
|
|
244
|
+
}
|
|
229
245
|
case 404:
|
|
230
246
|
// Priority: most-specific keyword wins.
|
|
231
247
|
// - "snapshot" → SnapshotNotFoundError
|
|
@@ -285,8 +301,8 @@ async function raiseFromResponse(resp) {
|
|
|
285
301
|
}
|
|
286
302
|
case 429: {
|
|
287
303
|
// A hard token budget is not transient the way backpressure is, so
|
|
288
|
-
// it gets its own class
|
|
289
|
-
// that
|
|
304
|
+
// it gets its own class, deliberately outside BackpressureError: a
|
|
305
|
+
// retry loop keyed on that class must not spin on it all month.
|
|
290
306
|
if (opts.code === "token_limit_exceeded") {
|
|
291
307
|
const body = bodyObject(bodyText) ?? {};
|
|
292
308
|
throw new errors_js_1.TokenLimitExceededError(msg, {
|
|
@@ -298,6 +314,9 @@ async function raiseFromResponse(resp) {
|
|
|
298
314
|
window: stringField(body.window),
|
|
299
315
|
});
|
|
300
316
|
}
|
|
317
|
+
if (opts.code === "rate_limited") {
|
|
318
|
+
throw new errors_js_1.RateLimitedError(msg, { ...opts, retryAfterMs: retryAfterMs(resp) });
|
|
319
|
+
}
|
|
301
320
|
// v0.10.1: distinguish per-user quota exhaustion from
|
|
302
321
|
// operator-wide backpressure. The shapes share the 429 status
|
|
303
322
|
// but the JSON body's `code` field discriminates. Consumers
|
|
@@ -305,18 +324,13 @@ async function raiseFromResponse(resp) {
|
|
|
305
324
|
try {
|
|
306
325
|
const parsed = JSON.parse(bodyText);
|
|
307
326
|
if (parsed.code === "per_user_quota_exhausted") {
|
|
308
|
-
// Retry-After is `<seconds>` per RFC; convert to ms.
|
|
309
|
-
const retryAfterRaw = resp.headers.get("retry-after");
|
|
310
|
-
const retryAfterMs = retryAfterRaw
|
|
311
|
-
? Number.parseInt(retryAfterRaw, 10) * 1000
|
|
312
|
-
: undefined;
|
|
313
327
|
throw new errors_js_1.PerUserQuotaExhaustedError(parsed.error ?? msg, {
|
|
314
328
|
status,
|
|
315
329
|
bodyText,
|
|
316
330
|
code: parsed.code,
|
|
317
331
|
userId: parsed.user_id,
|
|
318
332
|
cap: parsed.cap,
|
|
319
|
-
retryAfterMs:
|
|
333
|
+
retryAfterMs: retryAfterMs(resp),
|
|
320
334
|
});
|
|
321
335
|
}
|
|
322
336
|
}
|
|
@@ -335,6 +349,23 @@ async function raiseFromResponse(resp) {
|
|
|
335
349
|
throw new errors_js_1.LoomcycleError(msg, opts);
|
|
336
350
|
}
|
|
337
351
|
}
|
|
352
|
+
/** retryAfterMs reads a `Retry-After` header given in seconds, as the server
|
|
353
|
+
* always sends it, and returns milliseconds; undefined when the header is
|
|
354
|
+
* absent or not a number (an HTTP-date form is not parsed). */
|
|
355
|
+
function retryAfterMs(resp) {
|
|
356
|
+
const raw = resp.headers.get("retry-after");
|
|
357
|
+
if (!raw)
|
|
358
|
+
return undefined;
|
|
359
|
+
const ms = Number.parseInt(raw, 10) * 1000;
|
|
360
|
+
return Number.isFinite(ms) ? ms : undefined;
|
|
361
|
+
}
|
|
362
|
+
/** scopeFromWWWAuthenticate reads the scope from the RFC 6750 hint the server
|
|
363
|
+
* sends with a scope refusal (`Bearer scope="runs:create"`); undefined when
|
|
364
|
+
* the header is absent or names no scope. */
|
|
365
|
+
function scopeFromWWWAuthenticate(resp) {
|
|
366
|
+
const m = /\bscope="([^"]*)"/.exec(resp.headers.get("www-authenticate") ?? "");
|
|
367
|
+
return stringField(m?.[1]);
|
|
368
|
+
}
|
|
338
369
|
/** bodyCode returns the string `code` of a JSON error body, or undefined when
|
|
339
370
|
* the body is not a JSON object or has none. Statuses are shared between
|
|
340
371
|
* unrelated conditions (a 400 is any bad request; a 429 is backpressure, a
|
package/dist/cjs/index.js
CHANGED
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
* getAgent(agentId): Promise<Agent>
|
|
16
16
|
* getRun(runId): Promise<Agent>
|
|
17
17
|
* cancelAgent(agentId, opts?): Promise<CancelAgentResult>
|
|
18
|
-
* listUserAgents(userId, opts?): Promise<
|
|
18
|
+
* listUserAgents(userId, opts?): Promise<AgentList>
|
|
19
19
|
* listWalkRuns(walkId, opts?): Promise<WalkRunsPage>
|
|
20
|
+
* deleteRunTree(runId, opts?): Promise<RunTreeDeletion> // needs runs:delete
|
|
21
|
+
* deleteSession(sessionId, {cascade: "children"}): Promise<RunTreeDeletion>
|
|
20
22
|
* getTranscript(sessionId): Promise<TranscriptResponse>
|
|
21
23
|
* health(): Promise<HealthResponse>
|
|
22
24
|
* listUsers(opts?): Promise<ListUsersResponse> // tenant-scoped (RFC L)
|
|
@@ -34,6 +36,9 @@
|
|
|
34
36
|
* resumeRuntime(): Promise<ResumeResult>
|
|
35
37
|
* getRuntimeState(): Promise<RuntimeStateResponse>
|
|
36
38
|
* resolveProbe(opts?): Promise<ResolverMatrix>
|
|
39
|
+
* listModels(): Promise<ModelAliasesResponse> // configured model aliases
|
|
40
|
+
* getRouting(): Promise<RoutingResponse> // per user_tier × tier cascade
|
|
41
|
+
* listProviderModels(providerId): Promise<ProviderModelsResponse> // admin token only
|
|
37
42
|
*
|
|
38
43
|
* // Snapshot lifecycle (v0.8.17/8.18)
|
|
39
44
|
* createSnapshot(opts?): Promise<SnapshotCreateResponse>
|
|
@@ -56,6 +61,10 @@
|
|
|
56
61
|
* backfillEmbeddings(scope, scopeId, opts?): Promise<MemoryBackfillResponse> // embeds rows that have none
|
|
57
62
|
* purgeStaleEmbeddings(scope, scopeId, opts?): Promise<MemoryPurgeResponse> // DELETES; dry_run defaults true
|
|
58
63
|
*
|
|
64
|
+
* // Memory / document change feeds (RFC CD Part C; SSE; substrate:tenant)
|
|
65
|
+
* streamMemoryChanges(opts?): AsyncIterable<ChangeFeedFrame> // feed | change | error (non-fatal); resume with opts.since = last seq
|
|
66
|
+
* streamDocumentChanges(opts?): AsyncIterable<ChangeFeedFrame>
|
|
67
|
+
*
|
|
59
68
|
* // Interruption (v0.8.16; decline RFC BH)
|
|
60
69
|
* listUserInterrupts(userId, opts?): Promise<InterruptListResponse>
|
|
61
70
|
* listRunInterrupts(runId, opts?): Promise<InterruptListResponse>
|
|
@@ -71,7 +80,27 @@
|
|
|
71
80
|
* mcpServerDef(input): Promise<SubstrateToolResponse>
|
|
72
81
|
* mcpServerDefVerify(name, sha): Promise<MCPServerDefVerifyResult> // v0.18.0
|
|
73
82
|
* ensureMcpServer(opts): Promise<EnsureMcpServerResult> // v0.18.0 — idempotent register-if-changed
|
|
74
|
-
* scheduleDef(input): Promise<SubstrateToolResponse>
|
|
83
|
+
* scheduleDef(input): Promise<SubstrateToolResponse> // input: ScheduleDefInput | SubstrateToolInput
|
|
84
|
+
* webhookDef(input): Promise<SubstrateToolResponse> // input: WebhookDefInput | SubstrateToolInput
|
|
85
|
+
* hookDef(input): Promise<SubstrateToolResponse> // input: HookDefInput | SubstrateToolInput
|
|
86
|
+
* ensureHookDef(name, overlay, opts?): Promise<EnsureHookDefResult> // idempotent register-if-changed
|
|
87
|
+
* createHookDef / forkHookDef(name, overlay, opts?) // overlay strictly typed, no loose fallback
|
|
88
|
+
* createScheduleDef / forkScheduleDef(name, overlay, opts?) // ditto (overlay open: types of known keys checked)
|
|
89
|
+
* createWebhookDef / forkWebhookDef(name, overlay, opts?) // ditto
|
|
90
|
+
*
|
|
91
|
+
* // Def name enumeration (tenant-scoped; opts.tenant = admin focus)
|
|
92
|
+
* listAgentDefNames / listSkillDefNames / listHookDefNames / listMcpServerDefNames /
|
|
93
|
+
* listScheduleDefNames / listA2AServerCardDefNames / listA2AAgentDefNames /
|
|
94
|
+
* listWebhookDefNames / listMemoryBackendDefNames / listDocumentSourceDefNames
|
|
95
|
+
* (opts?): Promise<DefNamesResponse>
|
|
96
|
+
* listOperatorTokenDefNames(opts?): Promise<OperatorTokenDefNamesResponse> // admin token only
|
|
97
|
+
*
|
|
98
|
+
* // Schedules admin
|
|
99
|
+
* listAllSchedules(opts?): Promise<ScheduleListResponse> // static yaml + ScheduleDefs, merged
|
|
100
|
+
* getScheduleState(defId): Promise<ScheduleStateView>
|
|
101
|
+
* runScheduleNow(defId): Promise<ScheduleRunNowResult> // fires on the next tick
|
|
102
|
+
* pauseSchedule(defId): Promise<SchedulePauseResult> // indefinite
|
|
103
|
+
* resumeSchedule(defId): Promise<SchedulePauseResult>
|
|
75
104
|
*
|
|
76
105
|
* // Dynamic filesystem volumes (v0.35.0 — RFC AH; tenant-confined)
|
|
77
106
|
* volumeDef(input): Promise<SubstrateToolResponse> // create/get/list/delete/purge
|
|
@@ -82,8 +111,8 @@
|
|
|
82
111
|
* listTeams(): Promise<ListTeamsResponse>
|
|
83
112
|
* renderTeamDiagram(name, opts?): Promise<TeamDiagram>
|
|
84
113
|
* getTeamDef(defId): Promise<TeamDefDetail>
|
|
85
|
-
* createTeam(name, overlay): Promise<CreatedTeam>
|
|
86
|
-
* forkTeam(name, overlay): Promise<CreatedTeam>
|
|
114
|
+
* createTeam(name, overlay, opts?): Promise<CreatedTeam> // opts: promote (default true) / description
|
|
115
|
+
* forkTeam(name, overlay, opts?): Promise<CreatedTeam> // opts: promote / parentDefId / description
|
|
87
116
|
* deleteTeam(name): Promise<{name, deleted}>
|
|
88
117
|
* runTeam({name|defId, input}): Promise<TeamRunResult>
|
|
89
118
|
* runTeam({..., mode:"detach"}): Promise<TeamRunDetached> // the handle, now — the walk runs on
|
|
@@ -97,6 +126,14 @@
|
|
|
97
126
|
* // History — browse/search/annotate past chats (RFC BE; tenant-confined)
|
|
98
127
|
* history(input): Promise<HistoryToolResponse> // list/get/search/rename/annotate/pin/archive/recap/resume
|
|
99
128
|
*
|
|
129
|
+
* // Evaluation — score runs and read the scores back, outside a run
|
|
130
|
+
* evaluation(input): Promise<…> // submit/get/list_for_run/list_for_def/aggregate
|
|
131
|
+
* submitEvaluation(runId, score, opts?): Promise<EvaluationSubmitResult>
|
|
132
|
+
* getEvaluation(evalId): Promise<EvaluationRecord>
|
|
133
|
+
* listEvaluationsForRun(runId, opts?): Promise<EvaluationListForRunResult>
|
|
134
|
+
* listEvaluationsForDef(defId, opts?): Promise<EvaluationListForDefResult>
|
|
135
|
+
* aggregateEvaluations(defId, opts?): Promise<EvaluationAggregate>
|
|
136
|
+
*
|
|
100
137
|
* // Library v2 enumeration (v0.10.3 — yaml+substrate merged)
|
|
101
138
|
* listLibraryAgents(): Promise<LibraryListResponse<LibraryAgentDefinition>>
|
|
102
139
|
* listLibrarySkills(): Promise<LibraryListResponse<LibrarySkillDefinition>>
|
|
@@ -113,13 +150,18 @@
|
|
|
113
150
|
* full HTTP-status → typed-error mapping table):
|
|
114
151
|
* LoomcycleError, AgentNotFoundError, SessionNotFoundError,
|
|
115
152
|
* SessionBusyError, AgentIDInUseError, BackpressureError,
|
|
116
|
-
*
|
|
153
|
+
* RateLimitedError (subclass of BackpressureError),
|
|
154
|
+
* PerUserQuotaExhaustedError, AuthError, InsufficientScopeError (a 403
|
|
155
|
+
* naming the scope the token lacks), UnavailableError,
|
|
156
|
+
* InvalidArgumentError,
|
|
117
157
|
* PauseNotConfiguredError (subclass of UnavailableError),
|
|
118
158
|
* AlreadyPausingError, NotPausedError, SnapshotNotFoundError,
|
|
119
159
|
* SnapshotTooLargeError, RequestTooLargeError + PromptTooLargeError
|
|
120
160
|
* (both subclasses of SnapshotTooLargeError), TokenLimitExceededError
|
|
121
|
-
* (
|
|
122
|
-
* SubstrateToolRefusedError (v0.8.22)
|
|
161
|
+
* (NOT a BackpressureError: a hard budget, not retryable),
|
|
162
|
+
* SnapshotVersionError, SubstrateToolRefusedError (v0.8.22).
|
|
163
|
+
* Every one carries `retryable` (true only for a refusal that clears
|
|
164
|
+
* on its own).
|
|
123
165
|
*
|
|
124
166
|
* Transport: HTTP+SSE. Auth: Bearer token via the Authorization
|
|
125
167
|
* header. Designed for Node ≥18 (engines pinned); Bun/Deno likely
|
|
@@ -129,11 +171,14 @@
|
|
|
129
171
|
* See `adapters/ts/README.md` for usage examples.
|
|
130
172
|
*/
|
|
131
173
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
132
|
-
exports.UnavailableError = exports.TokenLimitExceededError = exports.SubstrateToolRefusedError = exports.SnapshotVersionError = exports.SnapshotTooLargeError = exports.SnapshotNotFoundError = exports.SessionNotFoundError = exports.SessionBusyError = exports.RequestTooLargeError = exports.PromptTooLargeError = exports.PerUserQuotaExhaustedError = exports.PauseNotConfiguredError = exports.NotPausedError = exports.LoomcycleError = exports.ChannelCursorRegressionError = exports.InvalidArgumentError = exports.NotFoundError = exports.BackpressureError = exports.AuthError = exports.AlreadyPausingError = exports.AgentNotFoundError = exports.AgentIDInUseError = exports.CLIENT_TOOL_SUBPROTOCOL = exports.clientToolsURL = exports.ClientToolHost = exports.InteractiveSession = exports.LoomcycleClient = void 0;
|
|
174
|
+
exports.UnavailableError = exports.TokenLimitExceededError = exports.SubstrateToolRefusedError = exports.SnapshotVersionError = exports.SnapshotTooLargeError = exports.SnapshotNotFoundError = exports.SessionNotFoundError = exports.SessionBusyError = exports.RequestTooLargeError = exports.RateLimitedError = exports.PromptTooLargeError = exports.PerUserQuotaExhaustedError = exports.PauseNotConfiguredError = exports.NotPausedError = exports.LoomcycleError = exports.ChannelCursorRegressionError = exports.InvalidArgumentError = exports.InsufficientScopeError = exports.NotFoundError = exports.BackpressureError = exports.AuthError = exports.AlreadyPausingError = exports.AgentNotFoundError = exports.AgentIDInUseError = exports.CLIENT_TOOL_SUBPROTOCOL = exports.clientToolsURL = exports.ClientToolHost = exports.verifyLoomcycleDelivery = exports.verifyLoomcycleSignature = exports.InteractiveSession = exports.LoomcycleClient = void 0;
|
|
133
175
|
var client_js_1 = require("./client.js");
|
|
134
176
|
Object.defineProperty(exports, "LoomcycleClient", { enumerable: true, get: function () { return client_js_1.LoomcycleClient; } });
|
|
135
177
|
var interactive_js_1 = require("./interactive.js");
|
|
136
178
|
Object.defineProperty(exports, "InteractiveSession", { enumerable: true, get: function () { return interactive_js_1.InteractiveSession; } });
|
|
179
|
+
var signature_js_1 = require("./signature.js");
|
|
180
|
+
Object.defineProperty(exports, "verifyLoomcycleSignature", { enumerable: true, get: function () { return signature_js_1.verifyLoomcycleSignature; } });
|
|
181
|
+
Object.defineProperty(exports, "verifyLoomcycleDelivery", { enumerable: true, get: function () { return signature_js_1.verifyLoomcycleDelivery; } });
|
|
137
182
|
var client_tools_js_1 = require("./client-tools.js");
|
|
138
183
|
Object.defineProperty(exports, "ClientToolHost", { enumerable: true, get: function () { return client_tools_js_1.ClientToolHost; } });
|
|
139
184
|
Object.defineProperty(exports, "clientToolsURL", { enumerable: true, get: function () { return client_tools_js_1.clientToolsURL; } });
|
|
@@ -145,6 +190,7 @@ Object.defineProperty(exports, "AlreadyPausingError", { enumerable: true, get: f
|
|
|
145
190
|
Object.defineProperty(exports, "AuthError", { enumerable: true, get: function () { return errors_js_1.AuthError; } });
|
|
146
191
|
Object.defineProperty(exports, "BackpressureError", { enumerable: true, get: function () { return errors_js_1.BackpressureError; } });
|
|
147
192
|
Object.defineProperty(exports, "NotFoundError", { enumerable: true, get: function () { return errors_js_1.NotFoundError; } });
|
|
193
|
+
Object.defineProperty(exports, "InsufficientScopeError", { enumerable: true, get: function () { return errors_js_1.InsufficientScopeError; } });
|
|
148
194
|
Object.defineProperty(exports, "InvalidArgumentError", { enumerable: true, get: function () { return errors_js_1.InvalidArgumentError; } });
|
|
149
195
|
Object.defineProperty(exports, "ChannelCursorRegressionError", { enumerable: true, get: function () { return errors_js_1.ChannelCursorRegressionError; } });
|
|
150
196
|
Object.defineProperty(exports, "LoomcycleError", { enumerable: true, get: function () { return errors_js_1.LoomcycleError; } });
|
|
@@ -152,6 +198,7 @@ Object.defineProperty(exports, "NotPausedError", { enumerable: true, get: functi
|
|
|
152
198
|
Object.defineProperty(exports, "PauseNotConfiguredError", { enumerable: true, get: function () { return errors_js_1.PauseNotConfiguredError; } });
|
|
153
199
|
Object.defineProperty(exports, "PerUserQuotaExhaustedError", { enumerable: true, get: function () { return errors_js_1.PerUserQuotaExhaustedError; } });
|
|
154
200
|
Object.defineProperty(exports, "PromptTooLargeError", { enumerable: true, get: function () { return errors_js_1.PromptTooLargeError; } });
|
|
201
|
+
Object.defineProperty(exports, "RateLimitedError", { enumerable: true, get: function () { return errors_js_1.RateLimitedError; } });
|
|
155
202
|
Object.defineProperty(exports, "RequestTooLargeError", { enumerable: true, get: function () { return errors_js_1.RequestTooLargeError; } });
|
|
156
203
|
Object.defineProperty(exports, "SessionBusyError", { enumerable: true, get: function () { return errors_js_1.SessionBusyError; } });
|
|
157
204
|
Object.defineProperty(exports, "SessionNotFoundError", { enumerable: true, get: function () { return errors_js_1.SessionNotFoundError; } });
|
package/dist/cjs/interactive.js
CHANGED
|
@@ -49,7 +49,11 @@ class InteractiveSession {
|
|
|
49
49
|
else if (ev.type === "session" && ev.session_id) {
|
|
50
50
|
this.sessionId = ev.session_id;
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
// A queued/admitted frame reports the run's admission, not activity in
|
|
53
|
+
// its conversation, so it leaves awaitingInput as it was.
|
|
54
|
+
if (ev.type !== "queued" && ev.type !== "admitted") {
|
|
55
|
+
this.awaitingInput = ev.type === "awaiting_input";
|
|
56
|
+
}
|
|
53
57
|
yield ev;
|
|
54
58
|
}
|
|
55
59
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Verifying a signed loomcycle hook delivery.
|
|
4
|
+
*
|
|
5
|
+
* A HookDef whose webhook body names a `signing_secret` signs every call it
|
|
6
|
+
* makes. The call carries
|
|
7
|
+
*
|
|
8
|
+
* X-Loomcycle-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256>
|
|
9
|
+
*
|
|
10
|
+
* where the MAC is keyed by the secret and computed over `<t>.<raw body>` —
|
|
11
|
+
* the exact bytes of the request body. Only the body is signed: the body's
|
|
12
|
+
* `delivery_id` (a retry keeps it) is what a receiver dedupes on, and its
|
|
13
|
+
* `hook_name` / `owner` say which hook sent it. `X-Loomcycle-Delivery` and
|
|
14
|
+
* `X-Loomcycle-Hook` are UNSIGNED copies of those two fields — a replayer can
|
|
15
|
+
* change them — so never dedupe or authorize on them.
|
|
16
|
+
* {@link verifyLoomcycleDelivery} verifies and returns the body's identity.
|
|
17
|
+
*
|
|
18
|
+
* Web Crypto only (`crypto.subtle`), so the same function runs in Node and in
|
|
19
|
+
* a browser or edge runtime. It is async because Web Crypto is.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.verifyLoomcycleSignature = verifyLoomcycleSignature;
|
|
23
|
+
exports.verifyLoomcycleDelivery = verifyLoomcycleDelivery;
|
|
24
|
+
const DEFAULT_TOLERANCE_SEC = 300;
|
|
25
|
+
const HEX_RE = /^(?:[0-9a-fA-F]{2})+$/;
|
|
26
|
+
// What Go's strconv.ParseInt accepts, so a header loomcycle's own receiver
|
|
27
|
+
// would accept is accepted here too.
|
|
28
|
+
const UNIX_RE = /^[+-]?\d+$/;
|
|
29
|
+
/**
|
|
30
|
+
* Reports whether `header` (the `X-Loomcycle-Signature` value) is a valid
|
|
31
|
+
* signature of `rawBody` under `secret`, made within the tolerance of now.
|
|
32
|
+
*
|
|
33
|
+
* `rawBody` must be the request body exactly as received — the string or the
|
|
34
|
+
* bytes, before any JSON parsing. A body parsed and re-serialized will not
|
|
35
|
+
* match: key order, spacing and escapes all change the bytes. A string is
|
|
36
|
+
* hashed as its UTF-8 encoding.
|
|
37
|
+
*
|
|
38
|
+
* Fails closed: a missing or repeated-garbage header, a `t` that is not an
|
|
39
|
+
* integer, a stale or future `t`, a `v1` that is not hex, or a wrong MAC all
|
|
40
|
+
* return `false`, with no reason given. The MAC is compared by Web Crypto's
|
|
41
|
+
* HMAC verify, which compares in constant time. Throws only when no Web Crypto
|
|
42
|
+
* implementation is available (a configuration problem, not a bad request).
|
|
43
|
+
*/
|
|
44
|
+
async function verifyLoomcycleSignature(rawBody, header, secret, opts = {}) {
|
|
45
|
+
const subtle = opts.subtle ?? globalThis.crypto?.subtle;
|
|
46
|
+
if (!subtle) {
|
|
47
|
+
throw new TypeError("verifyLoomcycleSignature: no Web Crypto implementation; pass opts.subtle (on Node 18: require('node:crypto').webcrypto.subtle)");
|
|
48
|
+
}
|
|
49
|
+
if (typeof header !== "string" || typeof secret !== "string" || secret === "") {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
let ts = "";
|
|
53
|
+
let macHex = "";
|
|
54
|
+
for (const raw of header.split(",")) {
|
|
55
|
+
const part = raw.trim();
|
|
56
|
+
if (part.startsWith("t="))
|
|
57
|
+
ts = part.slice(2);
|
|
58
|
+
else if (part.startsWith("v1="))
|
|
59
|
+
macHex = part.slice(3);
|
|
60
|
+
}
|
|
61
|
+
if (!UNIX_RE.test(ts) || !HEX_RE.test(macHex)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
const t = Number(ts);
|
|
65
|
+
if (!Number.isSafeInteger(t)) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const tolerance = opts.toleranceSec ?? DEFAULT_TOLERANCE_SEC;
|
|
69
|
+
const nowSec = (opts.now ?? Date.now()) / 1000;
|
|
70
|
+
if (!(tolerance >= 0) || Math.abs(nowSec - t) > tolerance) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const enc = new TextEncoder();
|
|
74
|
+
const body = typeof rawBody === "string" ? enc.encode(rawBody) : rawBody;
|
|
75
|
+
// The MAC covers t exactly as written in the header, then ".", then the body.
|
|
76
|
+
const prefix = enc.encode(ts + ".");
|
|
77
|
+
const signed = new Uint8Array(prefix.length + body.length);
|
|
78
|
+
signed.set(prefix, 0);
|
|
79
|
+
signed.set(body, prefix.length);
|
|
80
|
+
const mac = new Uint8Array(macHex.length / 2);
|
|
81
|
+
for (let i = 0; i < mac.length; i++) {
|
|
82
|
+
mac[i] = parseInt(macHex.slice(i * 2, i * 2 + 2), 16);
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const key = await subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
|
|
86
|
+
return await subtle.verify("HMAC", key, mac, signed);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* {@link verifyLoomcycleSignature}, then parse the verified body and return
|
|
94
|
+
* the identity a receiver should act on: its `delivery_id`, `hook_name` and
|
|
95
|
+
* `owner`. Resolves `null` when the signature does not verify, or the body is
|
|
96
|
+
* not a JSON object carrying a non-empty string `delivery_id` and `hook_name`
|
|
97
|
+
* — a signed delivery always has both. Deduplicating on `deliveryId` stays the
|
|
98
|
+
* caller's job.
|
|
99
|
+
*/
|
|
100
|
+
async function verifyLoomcycleDelivery(rawBody, header, secret, opts = {}) {
|
|
101
|
+
if (!(await verifyLoomcycleSignature(rawBody, header, secret, opts))) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
let body;
|
|
105
|
+
try {
|
|
106
|
+
body = JSON.parse(typeof rawBody === "string" ? rawBody : new TextDecoder().decode(rawBody));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const { delivery_id: deliveryId, hook_name: hookName, owner } = body;
|
|
115
|
+
if (typeof deliveryId !== "string" || deliveryId === "" || typeof hookName !== "string" || hookName === "") {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
return { deliveryId, hookName, owner: typeof owner === "string" ? owner : "", body: body };
|
|
119
|
+
}
|