@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/client.js
CHANGED
|
@@ -138,6 +138,10 @@ function runBody(opts) {
|
|
|
138
138
|
body.idempotency_key = opts.idempotencyKey;
|
|
139
139
|
if (opts.maxWallSeconds !== undefined)
|
|
140
140
|
body.max_wall_seconds = opts.maxWallSeconds;
|
|
141
|
+
if (opts.admission !== undefined)
|
|
142
|
+
body.admission = opts.admission;
|
|
143
|
+
if (opts.retention !== undefined)
|
|
144
|
+
body.retention = opts.retention;
|
|
141
145
|
if (opts.metadata !== undefined)
|
|
142
146
|
body.metadata = opts.metadata;
|
|
143
147
|
if (opts.runTimeoutSeconds !== undefined)
|
|
@@ -439,6 +443,23 @@ class LoomcycleClient {
|
|
|
439
443
|
async getEffectiveConfig(runId, opts) {
|
|
440
444
|
return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, `/v1/runs/${encodeURIComponent(runId)}/effective-config`, opts);
|
|
441
445
|
}
|
|
446
|
+
/** Which per-run overrides a run of `name` may set, for this caller — what a
|
|
447
|
+
* run form needs before it sends anything. Mirrors
|
|
448
|
+
* `GET /v1/_agentdef/{name}/overridable`.
|
|
449
|
+
*
|
|
450
|
+
* Folds the agent's `allow_overrides` with the caller's token overrides
|
|
451
|
+
* policy, through the same checks the run path refuses with; a refused field
|
|
452
|
+
* names which (`reason`). Overridable fields carry what the run path then
|
|
453
|
+
* accepts: `narrow_only`, `max`, and `values` for a closed set.
|
|
454
|
+
*
|
|
455
|
+
* `substrate:tenant` (a non-isolated member too). A token limited to certain
|
|
456
|
+
* agents may ask about those agents only. Raises {@link AgentNotFoundError}
|
|
457
|
+
* (404) for an unknown or another tenant's agent, and a 403
|
|
458
|
+
* `agent_not_allowed` for an agent outside the token's allowlist. */
|
|
459
|
+
async getAgentOverridable(name, opts) {
|
|
460
|
+
const q = opts?.tenant ? `?tenant=${encodeURIComponent(opts.tenant)}` : "";
|
|
461
|
+
return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, `/v1/_agentdef/${encodeURIComponent(name)}/overridable${q}`, opts);
|
|
462
|
+
}
|
|
442
463
|
/** Re-attach to a run's event stream by `run_id` (RFC AI), replaying from
|
|
443
464
|
* `fromSeq` (default 0) then live-tailing — the gRPC-free twin of the Web
|
|
444
465
|
* UI's resume-in-terminal. Mirrors `GET /v1/runs/{run_id}/stream`. The
|
|
@@ -601,6 +622,35 @@ class LoomcycleClient {
|
|
|
601
622
|
body.reason = opts.reason;
|
|
602
623
|
return (0, fetch_helpers_js_1.postJSON)(this.ctx, `/v1/runs/${encodeURIComponent(runId)}/compact`, body, opts);
|
|
603
624
|
}
|
|
625
|
+
/** Permanently delete a run and every run it spawned, with what is stored
|
|
626
|
+
* about them: transcripts, their sessions when no other run is left in
|
|
627
|
+
* them, interrupts, evaluations of them, queued memory rows and indexed
|
|
628
|
+
* turns. The usage ledger (tokens, cost) is kept. Mirrors
|
|
629
|
+
* `DELETE /v1/runs/{run_id}/tree`.
|
|
630
|
+
*
|
|
631
|
+
* Needs the `runs:delete` scope (an admin and a tenant operator have it;
|
|
632
|
+
* any other token only if it was granted at mint) — a token without it
|
|
633
|
+
* raises `LoomcycleError` with status 403. A run that does not exist, was
|
|
634
|
+
* already deleted, or is not yours to read resolves with `deleted_runs: 0`
|
|
635
|
+
* rather than raising, so repeating the call is safe. A tree holding a run
|
|
636
|
+
* that is still running, awaiting input or paused deletes nothing and
|
|
637
|
+
* raises `LoomcycleError` with `code: "run_tree_live"` (status 409): cancel
|
|
638
|
+
* it first. `not_reached` names where a copy may remain that this call
|
|
639
|
+
* cannot remove. To discard a configured run that never started, use
|
|
640
|
+
* {@link LoomcycleClient.deleteConfiguredRun}. */
|
|
641
|
+
async deleteRunTree(runId, opts) {
|
|
642
|
+
return (0, fetch_helpers_js_1.deleteJSON)(this.ctx, `/v1/runs/${encodeURIComponent(runId)}/tree`, opts);
|
|
643
|
+
}
|
|
644
|
+
/** Permanently delete every run of a session, each with the runs it
|
|
645
|
+
* spawned — {@link LoomcycleClient.deleteRunTree} for all of them at once.
|
|
646
|
+
* `cascade: "children"` is required and is the only mode: there is no
|
|
647
|
+
* delete that leaves sub-agent runs behind. Mirrors
|
|
648
|
+
* `DELETE /v1/sessions/{session_id}?cascade=children`. Same scope, same
|
|
649
|
+
* zero-count answer for a session you cannot read, same `run_tree_live`
|
|
650
|
+
* refusal. */
|
|
651
|
+
async deleteSession(sessionId, opts) {
|
|
652
|
+
return (0, fetch_helpers_js_1.deleteJSON)(this.ctx, `/v1/sessions/${encodeURIComponent(sessionId)}?cascade=${encodeURIComponent(opts.cascade)}`, opts);
|
|
653
|
+
}
|
|
604
654
|
/**
|
|
605
655
|
* Replay a session's transcript into a NEW session bound to a (possibly
|
|
606
656
|
* different) target agent, so that agent continues from the same context
|
|
@@ -678,8 +728,14 @@ class LoomcycleClient {
|
|
|
678
728
|
const resp = await (0, fetch_helpers_js_1.postJSON)(this.ctx, `/v1/agents/${encodeURIComponent(agentId)}/cancel`, { reason: opts?.reason ?? "" }, opts);
|
|
679
729
|
return { cancelledCount: resp.cancelled_count };
|
|
680
730
|
}
|
|
681
|
-
/** List a user's recent agent runs, optionally filtered by
|
|
682
|
-
* `status` only running runs are returned; `"all"` returns
|
|
731
|
+
/** List a user's recent agent runs (at most 100), optionally filtered by
|
|
732
|
+
* status. With no `status` only running runs are returned; `"all"` returns
|
|
733
|
+
* every status; several statuses list a run in any of them.
|
|
734
|
+
*
|
|
735
|
+
* `awaitedState` lists only running runs blocked on one of the named waits
|
|
736
|
+
* (`"review"`, `"input"`, …). The state is derived per run, so the server
|
|
737
|
+
* reads at most 1,000 running runs; the returned array carries
|
|
738
|
+
* `truncated: true` when it stopped with some unread.
|
|
683
739
|
*
|
|
684
740
|
* v0.9.x — `parentAgentId` narrows the result CLIENT-SIDE to runs
|
|
685
741
|
* whose `parent_agent_id` matches. The server still returns the
|
|
@@ -688,17 +744,21 @@ class LoomcycleClient {
|
|
|
688
744
|
* n8n trigger pattern "show me all sub-runs spawned by parent X." */
|
|
689
745
|
async listUserAgents(userId, opts) {
|
|
690
746
|
const params = new URLSearchParams();
|
|
691
|
-
|
|
692
|
-
params.
|
|
747
|
+
for (const s of [].concat(opts?.status ?? []))
|
|
748
|
+
params.append("status", s);
|
|
749
|
+
for (const s of [].concat(opts?.awaitedState ?? []))
|
|
750
|
+
params.append("awaited_state", s);
|
|
693
751
|
if (opts?.tenant)
|
|
694
752
|
params.set("tenant", opts.tenant);
|
|
695
753
|
const q = params.toString() ? `?${params.toString()}` : "";
|
|
696
754
|
const resp = await (0, fetch_helpers_js_1.jsonFetch)(this.ctx, `/v1/users/${encodeURIComponent(userId)}/agents${q}`, opts);
|
|
697
|
-
|
|
755
|
+
let out = resp.agents ?? [];
|
|
698
756
|
if (opts?.parentAgentId !== undefined && opts.parentAgentId !== "") {
|
|
699
|
-
|
|
757
|
+
out = out.filter((a) => a.parent_agent_id === opts.parentAgentId);
|
|
700
758
|
}
|
|
701
|
-
|
|
759
|
+
if (resp.truncated)
|
|
760
|
+
out.truncated = true;
|
|
761
|
+
return out;
|
|
702
762
|
}
|
|
703
763
|
/** One page of a team walk's runs — the walk's own run (its run id IS the
|
|
704
764
|
* walk id) and every member it spawned, oldest first. The walk's own row
|
|
@@ -790,6 +850,36 @@ class LoomcycleClient {
|
|
|
790
850
|
params.set("tenant", opts.tenant);
|
|
791
851
|
return (0, fetch_helpers_js_1.deleteRequest)(this.ctx, `/v1/_limits?${params.toString()}`, opts);
|
|
792
852
|
}
|
|
853
|
+
/** The timed no-trace overrides in force: agents whose runs are being
|
|
854
|
+
* stored although they normally store none. An admin sees every tenant's
|
|
855
|
+
* (or one with `tenant`); a tenant operator sees its own. Mirrors
|
|
856
|
+
* `GET /v1/_retention/overrides`. */
|
|
857
|
+
async listRetentionOverrides(opts) {
|
|
858
|
+
const q = opts?.tenant ? `?tenant=${encodeURIComponent(opts.tenant)}` : "";
|
|
859
|
+
return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, `/v1/_retention/overrides${q}`, opts);
|
|
860
|
+
}
|
|
861
|
+
/** Store the runs of one no-trace agent until `until`, to debug it. At
|
|
862
|
+
* most 24 hours ahead; setting one replaces the last. It lifts a mode
|
|
863
|
+
* that came from the agent's definition or the run request, never one a
|
|
864
|
+
* token requires. Audited. `tenant` is admin-only: a tenant operator's
|
|
865
|
+
* override is always for its own tenant. Mirrors
|
|
866
|
+
* `PUT /v1/_retention/overrides`. */
|
|
867
|
+
async setRetentionOverride(body, opts) {
|
|
868
|
+
const until = body.until instanceof Date ? body.until.toISOString() : body.until;
|
|
869
|
+
const wire = { agent: body.agent, until };
|
|
870
|
+
if (body.tenant !== undefined)
|
|
871
|
+
wire.tenant = body.tenant;
|
|
872
|
+
return (0, fetch_helpers_js_1.putJSON)(this.ctx, "/v1/_retention/overrides", wire, opts);
|
|
873
|
+
}
|
|
874
|
+
/** End an agent's no-trace override before it runs out. Clearing one that
|
|
875
|
+
* is not there is not an error. Mirrors
|
|
876
|
+
* `DELETE /v1/_retention/overrides`. */
|
|
877
|
+
async clearRetentionOverride(agent, opts) {
|
|
878
|
+
const params = new URLSearchParams({ agent });
|
|
879
|
+
if (opts?.tenant)
|
|
880
|
+
params.set("tenant", opts.tenant);
|
|
881
|
+
return (0, fetch_helpers_js_1.deleteRequest)(this.ctx, `/v1/_retention/overrides?${params.toString()}`, opts);
|
|
882
|
+
}
|
|
793
883
|
/** Ask a decision model typed questions (`choice` / `noul` / `score`) about
|
|
794
884
|
* `state` and get each answer with its probabilities. A decision model does
|
|
795
885
|
* not write: there is no reply to parse and the call costs a few output
|
|
@@ -869,9 +959,51 @@ class LoomcycleClient {
|
|
|
869
959
|
* scopes are DERIVED server-side from the member's access_mode (never sent),
|
|
870
960
|
* and the plaintext `token` is returned exactly ONCE — store it now, it is
|
|
871
961
|
* never retrievable again. 404 for an unknown/cross-tenant subject; 409 for
|
|
872
|
-
* a disabled user.
|
|
962
|
+
* a disabled user.
|
|
963
|
+
*
|
|
964
|
+
* `ttlSeconds` gives the token a fixed lifetime: it stops authenticating
|
|
965
|
+
* when that runs out, and the answer carries `expires_at`. It cannot run
|
|
966
|
+
* past the end of the token making the call. `name` labels it (stored as
|
|
967
|
+
* `u-<name>`).
|
|
968
|
+
* `maxActive` keeps at most that many of the user's tokens alive — the
|
|
969
|
+
* oldest beyond it are retired at once and listed in `retired`.
|
|
970
|
+
*
|
|
971
|
+
* `agents` limits the token to starting those agents as top-level runs, and
|
|
972
|
+
* to the run endpoints: it is refused everywhere else (403
|
|
973
|
+
* `restricted_token`), and refused any other agent (403
|
|
974
|
+
* `agent_not_allowed`). An agent that a listed one spawns is not limited.
|
|
975
|
+
* An empty list is refused; omit the option for no limit.
|
|
976
|
+
*
|
|
977
|
+
* `overrides` locks the per-run settings for runs the token starts, and
|
|
978
|
+
* for retunes it makes: `"none"` locks them all (model, provider, tier,
|
|
979
|
+
* effort, sampling, limits, output format and the rest), a list names the
|
|
980
|
+
* ones it may still set. A request that sets a locked one is refused with
|
|
981
|
+
* 403 `override_not_allowed`.
|
|
982
|
+
*
|
|
983
|
+
* `rate` bounds how often the token may start runs: `perMinute` runs a
|
|
984
|
+
* minute, with `burst` allowed at once from idle (default: `perMinute`).
|
|
985
|
+
* A start past the rate is refused with 429 `rate_limited` and a
|
|
986
|
+
* `Retry-After` header.
|
|
987
|
+
*
|
|
988
|
+
* With none of the options the request has no body, as before. */
|
|
873
989
|
async mintUserToken(subject, opts) {
|
|
874
|
-
|
|
990
|
+
const body = {};
|
|
991
|
+
if (opts?.ttlSeconds !== undefined)
|
|
992
|
+
body.ttl_seconds = opts.ttlSeconds;
|
|
993
|
+
if (opts?.name !== undefined)
|
|
994
|
+
body.name = opts.name;
|
|
995
|
+
if (opts?.maxActive !== undefined)
|
|
996
|
+
body.max_active = opts.maxActive;
|
|
997
|
+
if (opts?.agents !== undefined)
|
|
998
|
+
body.agents = opts.agents;
|
|
999
|
+
if (opts?.overrides !== undefined)
|
|
1000
|
+
body.overrides = opts.overrides;
|
|
1001
|
+
if (opts?.rate !== undefined) {
|
|
1002
|
+
body.rate = { per_minute: opts.rate.perMinute, burst: opts.rate.burst };
|
|
1003
|
+
}
|
|
1004
|
+
if (opts?.retention !== undefined)
|
|
1005
|
+
body.retention = opts.retention;
|
|
1006
|
+
return (0, fetch_helpers_js_1.postJSON)(this.ctx, `/v1/_users/${encodeURIComponent(subject)}/tokens`, Object.keys(body).length > 0 ? body : undefined, { signal: opts?.signal });
|
|
875
1007
|
}
|
|
876
1008
|
/** List the caller's own user's delegated tokens — METADATA ONLY (never the
|
|
877
1009
|
* plaintext or hash). `active` applies the auth-layer validity rule so a
|
|
@@ -931,6 +1063,38 @@ class LoomcycleClient {
|
|
|
931
1063
|
async resolveProbe(opts) {
|
|
932
1064
|
return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_resolve/probe", undefined, opts);
|
|
933
1065
|
}
|
|
1066
|
+
/** The configured model aliases — the operator's top-level `models:` map,
|
|
1067
|
+
* alias name → `{provider, model, kind?}` (`GET /v1/_models`). Non-secret and
|
|
1068
|
+
* global (the same set for every tenant); tenant-readable. Store an alias on
|
|
1069
|
+
* an agent or fork rather than the concrete model it names, and the agent
|
|
1070
|
+
* follows when the operator retargets the alias. */
|
|
1071
|
+
async listModels(opts) {
|
|
1072
|
+
return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, "/v1/_models", opts);
|
|
1073
|
+
}
|
|
1074
|
+
/** For each user tier × tier, the ordered provider/model cascade a run
|
|
1075
|
+
* resolves to now, top first, with live availability and the candidate
|
|
1076
|
+
* `selected` (`GET /v1/_routing`). Tenant-readable. A non-admin caller gets
|
|
1077
|
+
* `admin: false`, no raw `last_error`, and — when the operator-key gate
|
|
1078
|
+
* restricts it — only the providers it can key itself
|
|
1079
|
+
* (`operator_key_restricted: true`). */
|
|
1080
|
+
async getRouting(opts) {
|
|
1081
|
+
return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, "/v1/_routing", opts);
|
|
1082
|
+
}
|
|
1083
|
+
/** List the models a provider serves right now, by a live round-trip to it
|
|
1084
|
+
* (`GET /v1/_providers/{id}/models`) rather than the cached probe matrix.
|
|
1085
|
+
*
|
|
1086
|
+
* **Requires an admin token.** A `substrate:tenant` token is refused with a
|
|
1087
|
+
* {@link LoomcycleError} whose `status` is 403 ("insufficient scope"); a
|
|
1088
|
+
* model picker built on a tenant token should fall back to
|
|
1089
|
+
* {@link LoomcycleClient.listModels} plus free-text model entry.
|
|
1090
|
+
*
|
|
1091
|
+
* Errors carry the server's code on `e.code`: 404 `provider_unknown` (no such
|
|
1092
|
+
* driver), 503 `provider_unavailable` (known, but not configured — e.g. no
|
|
1093
|
+
* key), 502 `provider_list_failed` (the round-trip failed; bounded at 10 s
|
|
1094
|
+
* server-side). */
|
|
1095
|
+
async listProviderModels(providerId, opts) {
|
|
1096
|
+
return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, `/v1/_providers/${encodeURIComponent(providerId)}/models`, opts);
|
|
1097
|
+
}
|
|
934
1098
|
// ---- Snapshot lifecycle ----
|
|
935
1099
|
/** Capture running-state into a per-section-semver JSON envelope.
|
|
936
1100
|
* Raises SnapshotTooLargeError on 413 when the envelope exceeds
|
|
@@ -1384,10 +1548,33 @@ class LoomcycleClient {
|
|
|
1384
1548
|
* Raises {@link SubstrateToolRefusedError} on tool-level refusals
|
|
1385
1549
|
* (static-name collision, missing required credential, invalid
|
|
1386
1550
|
* cron, etc.); {@link InvalidArgumentError} on 400 (malformed
|
|
1387
|
-
* JSON); {@link AuthError} on 401.
|
|
1551
|
+
* JSON); {@link AuthError} on 401.
|
|
1552
|
+
*
|
|
1553
|
+
* Annotate the input as {@link ScheduleDefInput} to have the overlay and
|
|
1554
|
+
* each op's fields checked (including `add_hook` / `remove_hook`, which the
|
|
1555
|
+
* loose {@link SubstrateToolInput} does not list); a loose input is sent
|
|
1556
|
+
* as it is. For create / fork, {@link LoomcycleClient.createScheduleDef} /
|
|
1557
|
+
* {@link LoomcycleClient.forkScheduleDef} check a bare literal too. */
|
|
1388
1558
|
async scheduleDef(input, opts) {
|
|
1389
1559
|
return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_scheduledef", input, opts);
|
|
1390
1560
|
}
|
|
1561
|
+
/** `scheduleDef({op: "create"})` with the overlay typed and no loose
|
|
1562
|
+
* fallback. {@link ScheduleDefOverlay} stays OPEN (the server drops keys it
|
|
1563
|
+
* does not know), so a misspelt key still compiles here; what fails is a
|
|
1564
|
+
* wrong type or value on a key it declares (`concurrency_policy: "skip"`,
|
|
1565
|
+
* an `on_complete` entry missing its `tool`). New versions are promoted
|
|
1566
|
+
* unless `promote: false`. */
|
|
1567
|
+
async createScheduleDef(name, overlay, opts) {
|
|
1568
|
+
return (await this.scheduleDef(defWriteInput("create", name, overlay, opts), signalOpts(opts)));
|
|
1569
|
+
}
|
|
1570
|
+
/** `scheduleDef({op: "fork"})`, typed like
|
|
1571
|
+
* {@link LoomcycleClient.createScheduleDef}. A field the overlay leaves out
|
|
1572
|
+
* keeps the parent's; the credential maps merge key by key. Forks the
|
|
1573
|
+
* active version of `name` (or its yaml template) unless `parentDefId` is
|
|
1574
|
+
* given; promoted unless `promote: false`. */
|
|
1575
|
+
async forkScheduleDef(name, overlay, opts) {
|
|
1576
|
+
return (await this.scheduleDef(defWriteInput("fork", name, overlay, opts), signalOpts(opts)));
|
|
1577
|
+
}
|
|
1391
1578
|
/** Invoke the v1.x RFC G A2AServerCardDef substrate tool over HTTP.
|
|
1392
1579
|
* Author + fork + retire A2A server-card definitions at runtime.
|
|
1393
1580
|
* Mirror of {@link LoomcycleClient.scheduleDef} for A2A server cards.
|
|
@@ -1429,10 +1616,33 @@ class LoomcycleClient {
|
|
|
1429
1616
|
*
|
|
1430
1617
|
* Raises {@link SubstrateToolRefusedError} on tool-level refusals;
|
|
1431
1618
|
* {@link InvalidArgumentError} on 400 (malformed JSON);
|
|
1432
|
-
* {@link AuthError} on 401.
|
|
1619
|
+
* {@link AuthError} on 401.
|
|
1620
|
+
*
|
|
1621
|
+
* Annotate the input as {@link WebhookDefInput} to have the overlay and
|
|
1622
|
+
* each op's fields checked; a loose input is sent as it is. For create /
|
|
1623
|
+
* fork, {@link LoomcycleClient.createWebhookDef} /
|
|
1624
|
+
* {@link LoomcycleClient.forkWebhookDef} check a bare literal too. */
|
|
1433
1625
|
async webhookDef(input, opts) {
|
|
1434
1626
|
return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_webhookdef", input, opts);
|
|
1435
1627
|
}
|
|
1628
|
+
/** `webhookDef({op: "create"})` with the overlay typed and no loose
|
|
1629
|
+
* fallback. {@link WebhookDefOverlay} stays OPEN for now (the server drops
|
|
1630
|
+
* keys it does not know), so a misspelt key still compiles here; what
|
|
1631
|
+
* fails is a wrong type or value on a key it declares (`delivery: "run"`,
|
|
1632
|
+
* `auth.kind: "basic"`, a misspelt key inside `rate_limit`). Promoted
|
|
1633
|
+
* unless `promote: false`. */
|
|
1634
|
+
async createWebhookDef(name, overlay, opts) {
|
|
1635
|
+
return (await this.webhookDef(defWriteInput("create", name, overlay, opts), signalOpts(opts)));
|
|
1636
|
+
}
|
|
1637
|
+
/** `webhookDef({op: "fork"})`, typed like
|
|
1638
|
+
* {@link LoomcycleClient.createWebhookDef}. A field the overlay leaves out
|
|
1639
|
+
* keeps the parent's; naming exactly one of `agent` / `channel` / `team`
|
|
1640
|
+
* clears the other two. Forks the active version of `name` (or its yaml
|
|
1641
|
+
* template) unless `parentDefId` is given; promoted unless
|
|
1642
|
+
* `promote: false`. */
|
|
1643
|
+
async forkWebhookDef(name, overlay, opts) {
|
|
1644
|
+
return (await this.webhookDef(defWriteInput("fork", name, overlay, opts), signalOpts(opts)));
|
|
1645
|
+
}
|
|
1436
1646
|
/** Invoke the RFC I MR-3a MemoryBackendDef substrate tool over HTTP.
|
|
1437
1647
|
* Author + fork + retire named memory backend definitions at runtime.
|
|
1438
1648
|
* Mirror of {@link LoomcycleClient.webhookDef} for memory backends.
|
|
@@ -1461,10 +1671,71 @@ class LoomcycleClient {
|
|
|
1461
1671
|
* matches, its body (code-js or a webhook URL), fail mode and timeout.
|
|
1462
1672
|
* Tenant-confined like agentDef. A HookDef fires on nothing by itself; an
|
|
1463
1673
|
* agent definition, a team definition or a run names it. Mirrors
|
|
1464
|
-
* POST /v1/_hookdef.
|
|
1674
|
+
* POST /v1/_hookdef.
|
|
1675
|
+
*
|
|
1676
|
+
* Annotate the input as {@link HookDefInput} to have the overlay checked
|
|
1677
|
+
* against the definition the server decodes STRICTLY — an unknown overlay
|
|
1678
|
+
* key is then a compile error instead of a refusal. A loose
|
|
1679
|
+
* {@link SubstrateToolInput} is sent as it is — including a bare literal
|
|
1680
|
+
* with a misspelt key, which falls through to that loose shape. To have a
|
|
1681
|
+
* bare literal checked, use {@link LoomcycleClient.createHookDef} /
|
|
1682
|
+
* {@link LoomcycleClient.forkHookDef}. */
|
|
1465
1683
|
async hookDef(input, opts) {
|
|
1466
1684
|
return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_hookdef", input, opts);
|
|
1467
1685
|
}
|
|
1686
|
+
/** `hookDef({op: "create"})` with no loose fallback: `overlay` is the
|
|
1687
|
+
* CLOSED {@link HookDefOverlay}, so any key the server would refuse is a
|
|
1688
|
+
* compile error, even in a bare literal. Every create mints a new version
|
|
1689
|
+
* (use {@link LoomcycleClient.ensureHookDef} to register only on change);
|
|
1690
|
+
* promoted unless `promote: false`. */
|
|
1691
|
+
async createHookDef(name, overlay, opts) {
|
|
1692
|
+
return (await this.hookDef(defWriteInput("create", name, overlay, opts), signalOpts(opts)));
|
|
1693
|
+
}
|
|
1694
|
+
/** `hookDef({op: "fork"})`, closed like
|
|
1695
|
+
* {@link LoomcycleClient.createHookDef}. Each top-level field given replaces
|
|
1696
|
+
* the parent's; `null` clears one. Forks the active version of `name` in
|
|
1697
|
+
* your tenant unless `parentDefId` is given. NOT promoted unless
|
|
1698
|
+
* `promote: true` — authoring and putting in force are two steps. */
|
|
1699
|
+
async forkHookDef(name, overlay, opts) {
|
|
1700
|
+
return (await this.hookDef(defWriteInput("fork", name, overlay, opts), signalOpts(opts)));
|
|
1701
|
+
}
|
|
1702
|
+
/** Register a HookDef idempotently — the register-on-every-boot
|
|
1703
|
+
* convenience, sibling of {@link LoomcycleClient.ensureMcpServer}.
|
|
1704
|
+
*
|
|
1705
|
+
* Unlike MCPServerDef and AgentDef, a HookDef `create` is NOT
|
|
1706
|
+
* content-addressed: every create mints a new version. And the hash
|
|
1707
|
+
* `verify` compares is computed over the server's own JSON encoding of the
|
|
1708
|
+
* definition, which this client cannot reproduce byte for byte. So instead
|
|
1709
|
+
* this reads the active version (`verify` for its def_id — it answers
|
|
1710
|
+
* `deployed: false` rather than raising — then `get`) and compares its
|
|
1711
|
+
* stored definition with `overlay`, both normalized the way the server
|
|
1712
|
+
* normalizes (`fail_mode` defaults to `"open"`, an empty `match.tools`,
|
|
1713
|
+
* description, header map or timeout is dropped). Equal → no write,
|
|
1714
|
+
* `changed: false`. Otherwise `create` with the default promote, which
|
|
1715
|
+
* makes the new version active.
|
|
1716
|
+
*
|
|
1717
|
+
* The normalization only collapses what the server collapses, so a real
|
|
1718
|
+
* change is never skipped; should the server come to normalize something
|
|
1719
|
+
* more, the cost is a redundant version, not a lost one. The rationale
|
|
1720
|
+
* in {@link EnsureHookDefOptions.description} is not part of the
|
|
1721
|
+
* definition and does not count as a change. */
|
|
1722
|
+
async ensureHookDef(name, overlay, opts) {
|
|
1723
|
+
const callOpts = signalOpts(opts);
|
|
1724
|
+
const verified = (await this.hookDef({ op: "verify", name }, callOpts));
|
|
1725
|
+
if (verified.deployed && verified.current_def_id) {
|
|
1726
|
+
// By def_id, not name: the version verify reported, even if another
|
|
1727
|
+
// writer promotes a different one in between.
|
|
1728
|
+
const active = (await this.hookDef({ op: "get", def_id: verified.current_def_id }, callOpts));
|
|
1729
|
+
if (sameJSON(normalizeHookDef(active.definition), normalizeHookDef(overlay))) {
|
|
1730
|
+
return { name, defId: active.def_id, version: active.version, changed: false };
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
const input = { op: "create", name, overlay };
|
|
1734
|
+
if (opts?.description)
|
|
1735
|
+
input.description = opts.description;
|
|
1736
|
+
const row = (await this.hookDef(input, callOpts));
|
|
1737
|
+
return { name, defId: row.def_id, version: row.version, changed: true };
|
|
1738
|
+
}
|
|
1468
1739
|
/** OperatorTokenDef substrate ops (create/rotate/retire/get/list) —
|
|
1469
1740
|
* RFC L OSS multi-tenant authorization. Operator-admin only. The
|
|
1470
1741
|
* token plaintext is returned ONCE in the create/rotate response.
|
|
@@ -1495,6 +1766,109 @@ class LoomcycleClient {
|
|
|
1495
1766
|
async volumeDef(input, opts) {
|
|
1496
1767
|
return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_volumedef", input, opts);
|
|
1497
1768
|
}
|
|
1769
|
+
// ---- Def name enumeration (GET /v1/_<family>def/names) ----
|
|
1770
|
+
//
|
|
1771
|
+
// Every name declared in one substrate family, each with its version
|
|
1772
|
+
// roll-up — what the op-dispatched `list` op cannot give (it lists the
|
|
1773
|
+
// versions of ONE name). Tenant-scoped server-side: a substrate:tenant token
|
|
1774
|
+
// sees only its own tenant's names; an admin sees all, or focuses one tenant
|
|
1775
|
+
// with `tenant` (ignored for a tenant token). The TeamDef family is
|
|
1776
|
+
// {@link LoomcycleClient.listTeams}.
|
|
1777
|
+
listDefNames(family, opts) {
|
|
1778
|
+
const q = opts?.tenant ? `?tenant=${encodeURIComponent(opts.tenant)}` : "";
|
|
1779
|
+
return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, `/v1/_${family}/names${q}`, opts);
|
|
1780
|
+
}
|
|
1781
|
+
/** Every AgentDef name (`GET /v1/_agentdef/names`). */
|
|
1782
|
+
async listAgentDefNames(opts) {
|
|
1783
|
+
return this.listDefNames("agentdef", opts);
|
|
1784
|
+
}
|
|
1785
|
+
/** Every SkillDef name (`GET /v1/_skilldef/names`). */
|
|
1786
|
+
async listSkillDefNames(opts) {
|
|
1787
|
+
return this.listDefNames("skilldef", opts);
|
|
1788
|
+
}
|
|
1789
|
+
/** Every HookDef name (`GET /v1/_hookdef/names`). */
|
|
1790
|
+
async listHookDefNames(opts) {
|
|
1791
|
+
return this.listDefNames("hookdef", opts);
|
|
1792
|
+
}
|
|
1793
|
+
/** Every MCPServerDef name (`GET /v1/_mcpserverdef/names`). */
|
|
1794
|
+
async listMcpServerDefNames(opts) {
|
|
1795
|
+
return this.listDefNames("mcpserverdef", opts);
|
|
1796
|
+
}
|
|
1797
|
+
/** Every ScheduleDef name (`GET /v1/_scheduledef/names`). Substrate only —
|
|
1798
|
+
* {@link LoomcycleClient.listAllSchedules} merges in the static yaml ones. */
|
|
1799
|
+
async listScheduleDefNames(opts) {
|
|
1800
|
+
return this.listDefNames("scheduledef", opts);
|
|
1801
|
+
}
|
|
1802
|
+
/** Every A2AServerCardDef name (`GET /v1/_a2aservercarddef/names`). */
|
|
1803
|
+
async listA2AServerCardDefNames(opts) {
|
|
1804
|
+
return this.listDefNames("a2aservercarddef", opts);
|
|
1805
|
+
}
|
|
1806
|
+
/** Every A2AAgentDef name (`GET /v1/_a2aagentdef/names`). */
|
|
1807
|
+
async listA2AAgentDefNames(opts) {
|
|
1808
|
+
return this.listDefNames("a2aagentdef", opts);
|
|
1809
|
+
}
|
|
1810
|
+
/** Every WebhookDef name (`GET /v1/_webhookdef/names`). */
|
|
1811
|
+
async listWebhookDefNames(opts) {
|
|
1812
|
+
return this.listDefNames("webhookdef", opts);
|
|
1813
|
+
}
|
|
1814
|
+
/** Every MemoryBackendDef name (`GET /v1/_memorybackenddef/names`). */
|
|
1815
|
+
async listMemoryBackendDefNames(opts) {
|
|
1816
|
+
return this.listDefNames("memorybackenddef", opts);
|
|
1817
|
+
}
|
|
1818
|
+
/** Every DocumentSourceDef name (`GET /v1/_documentsourcedef/names`). */
|
|
1819
|
+
async listDocumentSourceDefNames(opts) {
|
|
1820
|
+
return this.listDefNames("documentsourcedef", opts);
|
|
1821
|
+
}
|
|
1822
|
+
/** Every OperatorTokenDef name (`GET /v1/_operatortokendef/names`), with its
|
|
1823
|
+
* subject and token counts — never a token. **Admin token only**: token
|
|
1824
|
+
* minting stays operator-only, so a `substrate:tenant` token is refused
|
|
1825
|
+
* with 403. */
|
|
1826
|
+
async listOperatorTokenDefNames(opts) {
|
|
1827
|
+
return this.listDefNames("operatortokendef", opts);
|
|
1828
|
+
}
|
|
1829
|
+
// ---- Schedules admin (GET/POST /v1/_schedules/...) ----
|
|
1830
|
+
//
|
|
1831
|
+
// What the Web UI's schedules page drives: the merged list, one schedule's
|
|
1832
|
+
// runtime state, and run-now / pause / resume. Authoring stays on
|
|
1833
|
+
// {@link LoomcycleClient.scheduleDef}. The per-schedule methods take the
|
|
1834
|
+
// ScheduleDef's `active_def_id`; a static yaml schedule has none to give, and
|
|
1835
|
+
// a tenant token reaching another tenant's def gets the same 404 as an
|
|
1836
|
+
// unknown one ({@link NotFoundError}).
|
|
1837
|
+
/** Every schedule: static yaml `scheduled_runs` and ScheduleDefs, merged by
|
|
1838
|
+
* name (`GET /v1/_schedules/list-all`), sorted by name. A tenant token sees
|
|
1839
|
+
* only its own tenant's ScheduleDefs — static schedules are operator
|
|
1840
|
+
* automation and appear only for an admin, who may focus one tenant with
|
|
1841
|
+
* `tenant`. */
|
|
1842
|
+
async listAllSchedules(opts) {
|
|
1843
|
+
const q = opts?.tenant ? `?tenant=${encodeURIComponent(opts.tenant)}` : "";
|
|
1844
|
+
return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, `/v1/_schedules/list-all${q}`, opts);
|
|
1845
|
+
}
|
|
1846
|
+
/** One schedule's runtime state (`GET /v1/_schedules/{def_id}/state`): last
|
|
1847
|
+
* and next fire, pause, the last slot claim, the catch-up backlog and the
|
|
1848
|
+
* runs it has in flight. 404 when the def has no state row (never promoted,
|
|
1849
|
+
* retired, or not yours). */
|
|
1850
|
+
async getScheduleState(defId, opts) {
|
|
1851
|
+
return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, `/v1/_schedules/${encodeURIComponent(defId)}/state`, opts);
|
|
1852
|
+
}
|
|
1853
|
+
/** Fire a schedule now (`POST /v1/_schedules/{def_id}/run-now`). The run
|
|
1854
|
+
* starts on the scheduler's next tick, not during this call, and the regular
|
|
1855
|
+
* cadence continues after it. A run already in flight is handled by the
|
|
1856
|
+
* schedule's `concurrency_policy`. 404 for an unknown def. */
|
|
1857
|
+
async runScheduleNow(defId, opts) {
|
|
1858
|
+
return (0, fetch_helpers_js_1.postJSON)(this.ctx, `/v1/_schedules/${encodeURIComponent(defId)}/run-now`, undefined, opts);
|
|
1859
|
+
}
|
|
1860
|
+
/** Pause a schedule indefinitely (`POST /v1/_schedules/{def_id}/pause`) until
|
|
1861
|
+
* {@link LoomcycleClient.resumeSchedule}. Takes no duration: the server sets
|
|
1862
|
+
* `paused_until` about 100 years ahead. Distinct from
|
|
1863
|
+
* {@link LoomcycleClient.pauseRuntime}, which stops the whole runtime. */
|
|
1864
|
+
async pauseSchedule(defId, opts) {
|
|
1865
|
+
return (0, fetch_helpers_js_1.postJSON)(this.ctx, `/v1/_schedules/${encodeURIComponent(defId)}/pause`, undefined, opts);
|
|
1866
|
+
}
|
|
1867
|
+
/** Clear a schedule's pause (`POST /v1/_schedules/{def_id}/resume`), so the
|
|
1868
|
+
* scheduler considers it due again; `next_run_at` is left as it was. */
|
|
1869
|
+
async resumeSchedule(defId, opts) {
|
|
1870
|
+
return (0, fetch_helpers_js_1.postJSON)(this.ctx, `/v1/_schedules/${encodeURIComponent(defId)}/resume`, undefined, opts);
|
|
1871
|
+
}
|
|
1498
1872
|
// ---- RFC AP Agent Teams (TeamDef substrate) ----
|
|
1499
1873
|
//
|
|
1500
1874
|
// TeamDefs are agent-team workflow graphs (states + transitions + a per-state
|
|
@@ -1523,15 +1897,37 @@ class LoomcycleClient {
|
|
|
1523
1897
|
}
|
|
1524
1898
|
/** Author a new TeamDef (op=create). `overlay` is the workflow graph
|
|
1525
1899
|
* (`{entry, states, transitions, max_iterations?, colors?}`), validated
|
|
1526
|
-
* server-side — an invalid graph raises {@link SubstrateToolRefusedError}.
|
|
1900
|
+
* server-side — an invalid graph raises {@link SubstrateToolRefusedError}.
|
|
1901
|
+
*
|
|
1902
|
+
* `opts.promote` and `opts.description` ride beside `signal`, as on
|
|
1903
|
+
* {@link LoomcycleClient.forkTeam}; each is sent only when set. Omitted,
|
|
1904
|
+
* the server promotes a create (unlike a fork); `promote: false` authors the
|
|
1905
|
+
* version without putting it in force. */
|
|
1527
1906
|
async createTeam(name, overlay, opts) {
|
|
1528
|
-
|
|
1907
|
+
const body = { op: "create", name, overlay };
|
|
1908
|
+
if (opts?.promote !== undefined)
|
|
1909
|
+
body.promote = opts.promote;
|
|
1910
|
+
if (opts?.description !== undefined)
|
|
1911
|
+
body.description = opts.description;
|
|
1912
|
+
return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_teamdef", body, { signal: opts?.signal });
|
|
1529
1913
|
}
|
|
1530
1914
|
/** Save an edited graph as a NEW version of an existing team (op=fork). With
|
|
1531
|
-
* no
|
|
1532
|
-
* replaces it. Validated server-side ({@link SubstrateToolRefusedError}).
|
|
1915
|
+
* no `parentDefId` the server forks the name's active def; `overlay` (a full
|
|
1916
|
+
* graph) replaces it. Validated server-side ({@link SubstrateToolRefusedError}).
|
|
1917
|
+
*
|
|
1918
|
+
* The fork's own fields ride in `opts` beside `signal` — the pattern
|
|
1919
|
+
* {@link LoomcycleClient.setRunBreakpoints} uses — so existing two- and
|
|
1920
|
+
* three-argument calls are unchanged. `promote: true` puts the new version
|
|
1921
|
+
* in force in the same call; omitted, it is authored but not active. */
|
|
1533
1922
|
async forkTeam(name, overlay, opts) {
|
|
1534
|
-
|
|
1923
|
+
const body = { op: "fork", name, overlay };
|
|
1924
|
+
if (opts?.promote !== undefined)
|
|
1925
|
+
body.promote = opts.promote;
|
|
1926
|
+
if (opts?.parentDefId !== undefined)
|
|
1927
|
+
body.parent_def_id = opts.parentDefId;
|
|
1928
|
+
if (opts?.description !== undefined)
|
|
1929
|
+
body.description = opts.description;
|
|
1930
|
+
return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_teamdef", body, { signal: opts?.signal });
|
|
1535
1931
|
}
|
|
1536
1932
|
/** List every version of ONE team, newest first (op=list) — the lineage
|
|
1537
1933
|
* behind {@link LoomcycleClient.listTeams}' roll-up. Tenant-scoped
|
|
@@ -1603,6 +1999,8 @@ class LoomcycleClient {
|
|
|
1603
1999
|
body.review_ttl_seconds = target.reviewTtlSeconds;
|
|
1604
2000
|
if (target.idempotencyKey !== undefined)
|
|
1605
2001
|
body.idempotency_key = target.idempotencyKey;
|
|
2002
|
+
if (target.interruptOnCap !== undefined)
|
|
2003
|
+
body.interrupt_on_cap = target.interruptOnCap;
|
|
1606
2004
|
return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_teamdef", body, opts);
|
|
1607
2005
|
}
|
|
1608
2006
|
/** Read the debug breakpoints armed on a live team walk
|
|
@@ -1754,6 +2152,48 @@ class LoomcycleClient {
|
|
|
1754
2152
|
async history(input, opts) {
|
|
1755
2153
|
return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_history", input, opts);
|
|
1756
2154
|
}
|
|
2155
|
+
async evaluation(input, opts) {
|
|
2156
|
+
return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_evaluation", input, opts);
|
|
2157
|
+
}
|
|
2158
|
+
/** Record a score against a run — `evaluation({op:"submit"})`. Scores are
|
|
2159
|
+
* additive: a second submit records another evaluation. */
|
|
2160
|
+
async submitEvaluation(runId, score, opts) {
|
|
2161
|
+
const input = { op: "submit", run_id: runId, score };
|
|
2162
|
+
if (opts?.dimensions !== undefined)
|
|
2163
|
+
input.dimensions = opts.dimensions;
|
|
2164
|
+
if (opts?.judgement !== undefined)
|
|
2165
|
+
input.judgement = opts.judgement;
|
|
2166
|
+
if (opts?.rationale !== undefined)
|
|
2167
|
+
input.rationale = opts.rationale;
|
|
2168
|
+
return this.evaluation(input, { signal: opts?.signal });
|
|
2169
|
+
}
|
|
2170
|
+
/** One evaluation by id — `evaluation({op:"get"})`. */
|
|
2171
|
+
async getEvaluation(evalId, opts) {
|
|
2172
|
+
return this.evaluation({ op: "get", eval_id: evalId }, opts);
|
|
2173
|
+
}
|
|
2174
|
+
/** Every evaluation of one run — `evaluation({op:"list_for_run"})`. */
|
|
2175
|
+
async listEvaluationsForRun(runId, opts) {
|
|
2176
|
+
const input = { op: "list_for_run", run_id: runId };
|
|
2177
|
+
if (opts?.limit !== undefined)
|
|
2178
|
+
input.limit = opts.limit;
|
|
2179
|
+
return this.evaluation(input, { signal: opts?.signal });
|
|
2180
|
+
}
|
|
2181
|
+
/** Every evaluation of one definition version, in your tenant —
|
|
2182
|
+
* `evaluation({op:"list_for_def"})`. */
|
|
2183
|
+
async listEvaluationsForDef(defId, opts) {
|
|
2184
|
+
const input = { op: "list_for_def", def_id: defId };
|
|
2185
|
+
if (opts?.limit !== undefined)
|
|
2186
|
+
input.limit = opts.limit;
|
|
2187
|
+
return this.evaluation(input, { signal: opts?.signal });
|
|
2188
|
+
}
|
|
2189
|
+
/** A definition's scores rolled up — `evaluation({op:"aggregate"})`.
|
|
2190
|
+
* `includeLineage` adds the ancestors' evaluations along parent_def_id. */
|
|
2191
|
+
async aggregateEvaluations(defId, opts) {
|
|
2192
|
+
const input = { op: "aggregate", def_id: defId };
|
|
2193
|
+
if (opts?.includeLineage !== undefined)
|
|
2194
|
+
input.include_lineage = opts.includeLineage;
|
|
2195
|
+
return this.evaluation(input, { signal: opts?.signal });
|
|
2196
|
+
}
|
|
1757
2197
|
/** List the PERSISTENT volume universe for the caller's tenant — every
|
|
1758
2198
|
* static volume (the shared bind floor, `source: "static"`, read-only)
|
|
1759
2199
|
* plus the tenant's own dynamic VolumeDefs (`source: "dynamic"`). Host
|
|
@@ -2310,11 +2750,11 @@ class LoomcycleClient {
|
|
|
2310
2750
|
*
|
|
2311
2751
|
* v1.78.0 — `walkId` is a SERVER-side filter: only the runs one team
|
|
2312
2752
|
* walk spawned. A walk's own run_id IS its walk id, so a caller that
|
|
2313
|
-
* started a team with `
|
|
2753
|
+
* started a team with `mode: "detach"` passes back the handle it already
|
|
2314
2754
|
* holds and gets a live view of that workflow's agents:
|
|
2315
2755
|
*
|
|
2316
2756
|
* ```ts
|
|
2317
|
-
* const { run_id } = await client.runTeam({
|
|
2757
|
+
* const { run_id } = await client.runTeam({ name: "triage", mode: "detach" });
|
|
2318
2758
|
* for await (const item of client.streamUserRunStates(userId, { walkId: run_id })) {
|
|
2319
2759
|
* if (item.kind === "event") place(item.payload.parent_context?.wave_index);
|
|
2320
2760
|
* }
|
|
@@ -2396,6 +2836,61 @@ class LoomcycleClient {
|
|
|
2396
2836
|
yield { kind: "close", payload: { reason: closeReason } };
|
|
2397
2837
|
}
|
|
2398
2838
|
}
|
|
2839
|
+
/** Tail the caller's tenant's MEMORY change feed (RFC CD Part C) — one
|
|
2840
|
+
* value-free coordinate per `memory.set` / `memory.delete` /
|
|
2841
|
+
* `memory.scope_deleted`. Mirrors `GET /v1/_memory/changes?since=<seq>`.
|
|
2842
|
+
*
|
|
2843
|
+
* The first item is always `{ kind: "feed", payload: ChangeFeedStatus }`,
|
|
2844
|
+
* then one `{ kind: "change", payload: MemoryChange }` per change, oldest
|
|
2845
|
+
* first. Pull the current value through the data API; the feed never
|
|
2846
|
+
* carries it.
|
|
2847
|
+
*
|
|
2848
|
+
* - The feed is EMPTY unless the server runs with
|
|
2849
|
+
* `LOOMCYCLE_MEMORY_CHANGES_ENABLED`. The stream still opens and idles,
|
|
2850
|
+
* so read `payload.enabled` on the feed frame instead of waiting.
|
|
2851
|
+
* - Scope is the caller's OWN tenant; there is no cross-tenant feed.
|
|
2852
|
+
* - Needs `substrate:tenant` (or admin). A tenant MEMBER's token is refused
|
|
2853
|
+
* — the feed exposes the tenant's write pattern, which is operator
|
|
2854
|
+
* observability, not member data.
|
|
2855
|
+
* - To resume after a disconnect, persist the last `payload.seq` you
|
|
2856
|
+
* processed and pass it back as `since`. A cursor older than the
|
|
2857
|
+
* server's change retention resumes past the pruned gap without saying
|
|
2858
|
+
* so.
|
|
2859
|
+
* - A `{ kind: "error", payload: ChangeFeedError }` item means the server
|
|
2860
|
+
* could not read its change log. It is NOT fatal and iteration
|
|
2861
|
+
* continues: the server retries with backoff without advancing its
|
|
2862
|
+
* cursor, so changes resume after recovery with no gap. One item per
|
|
2863
|
+
* failure streak.
|
|
2864
|
+
* - The stream has no end of its own: iteration ends when the server
|
|
2865
|
+
* closes it (shutdown — reconnect with `since`) or throws an
|
|
2866
|
+
* `AbortError` when `signal` aborts. */
|
|
2867
|
+
async *streamMemoryChanges(opts) {
|
|
2868
|
+
yield* this.streamChangeFeed("/v1/_memory/changes", opts);
|
|
2869
|
+
}
|
|
2870
|
+
/** Tail the caller's tenant's DOCUMENT change feed (RFC CD Part C) — one
|
|
2871
|
+
* coordinate per `document.chunk.updated` / `document.chunk.deleted`,
|
|
2872
|
+
* naming the `chunk_id`. Mirrors `GET /v1/_document/changes?since=<seq>`.
|
|
2873
|
+
* Same frames, scope, auth and resume contract as
|
|
2874
|
+
* {@link LoomcycleClient.streamMemoryChanges}; a reader wanting both
|
|
2875
|
+
* families opens both. */
|
|
2876
|
+
async *streamDocumentChanges(opts) {
|
|
2877
|
+
yield* this.streamChangeFeed("/v1/_document/changes", opts);
|
|
2878
|
+
}
|
|
2879
|
+
async *streamChangeFeed(path, opts) {
|
|
2880
|
+
const since = opts?.since;
|
|
2881
|
+
// The server reads an unparseable or negative `since` as 0 rather than
|
|
2882
|
+
// refusing it, which replays every retained change — a resume bug that
|
|
2883
|
+
// would look like a burst of activity. Refuse it here instead.
|
|
2884
|
+
if (since !== undefined && !(Number.isSafeInteger(since) && since >= 0)) {
|
|
2885
|
+
throw new Error(`loomcycle: change feed "since" must be a non-negative integer, got ${String(since)}`);
|
|
2886
|
+
}
|
|
2887
|
+
const q = since ? `?since=${since}` : "";
|
|
2888
|
+
for await (const ev of this.streamSSEGet(path + q, opts?.signal)) {
|
|
2889
|
+
const frame = toChangeFeedFrame(ev);
|
|
2890
|
+
if (frame)
|
|
2891
|
+
yield frame;
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2399
2894
|
/** List credential METADATA for a scope (RFC AR) — never a secret value.
|
|
2400
2895
|
* `user` = your own subject's tokens (per-user, e.g. a personal Telegram bot
|
|
2401
2896
|
* token); `tenant` = shared across the tenant (operator authority). RFC CN: a
|
|
@@ -2406,10 +2901,35 @@ class LoomcycleClient {
|
|
|
2406
2901
|
}
|
|
2407
2902
|
/** Store or rotate a credential (RFC AR). The plaintext `value` is write-only:
|
|
2408
2903
|
* the server encrypts it, masks it from the transcript, and never returns it.
|
|
2409
|
-
* Returns the stored row's metadata. Mirrors `POST /v1/_credentialdef {op:create}`.
|
|
2904
|
+
* Returns the stored row's metadata. Mirrors `POST /v1/_credentialdef {op:create}`.
|
|
2905
|
+
*
|
|
2906
|
+
* `allowed_urls` restricts where the value may be sent: https URLs, each
|
|
2907
|
+
* covering itself and everything under it. `[]` stores a value that is
|
|
2908
|
+
* sent nowhere. Leave it out for no restriction, or, when the credential
|
|
2909
|
+
* already exists, to keep the list it has. A restricted credential is used
|
|
2910
|
+
* only where the destination is known and covered (an MCP server's or a
|
|
2911
|
+
* hook's header, a remote peer's key, a git source), never as a provider
|
|
2912
|
+
* key.
|
|
2913
|
+
*
|
|
2914
|
+
* `username` is the user name sent with the value where a destination
|
|
2915
|
+
* takes one (HTTP basic auth for a git source). Leave it out to keep the
|
|
2916
|
+
* stored one; `""` removes it.
|
|
2917
|
+
*
|
|
2918
|
+
* `expires_at` is an RFC3339 timestamp (`"2026-12-31T00:00:00Z"`) stored
|
|
2919
|
+
* with the credential and reported back in its metadata; any other format
|
|
2920
|
+
* is refused. `backend` is where the value is kept: `"inline"` (encrypted
|
|
2921
|
+
* in loomcycle's own store, the default) is the only one accepted. */
|
|
2410
2922
|
async createCredential(input, opts) {
|
|
2411
2923
|
return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_credentialdef", { op: "create", ...input }, opts);
|
|
2412
2924
|
}
|
|
2925
|
+
/** Read ONE credential's metadata (RFC AR) — never its value. Answers
|
|
2926
|
+
* whether a name is stored, and its `allowed_urls` / `username` /
|
|
2927
|
+
* `expires_at`, without listing the whole scope. An absent name raises
|
|
2928
|
+
* {@link SubstrateToolRefusedError}. Mirrors
|
|
2929
|
+
* `POST /v1/_credentialdef {op:get}`. */
|
|
2930
|
+
async getCredential(input, opts) {
|
|
2931
|
+
return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_credentialdef", { op: "get", ...input }, opts);
|
|
2932
|
+
}
|
|
2413
2933
|
/** Delete a credential (RFC AR). Anything referencing `$cred:<name>` at that
|
|
2414
2934
|
* scope stops resolving. Mirrors `POST /v1/_credentialdef {op:delete}`. */
|
|
2415
2935
|
async deleteCredential(input, opts) {
|
|
@@ -2466,6 +2986,113 @@ async function* parseRunStateSSE(reader) {
|
|
|
2466
2986
|
}
|
|
2467
2987
|
}
|
|
2468
2988
|
}
|
|
2989
|
+
/** The wire input of a typed create / fork. `promote` goes out only when the
|
|
2990
|
+
* caller set it — false included — so the server's per-def default applies
|
|
2991
|
+
* otherwise. */
|
|
2992
|
+
function defWriteInput(op, name, overlay, opts) {
|
|
2993
|
+
const input = { op, name, overlay };
|
|
2994
|
+
if (opts?.parentDefId)
|
|
2995
|
+
input.parent_def_id = opts.parentDefId;
|
|
2996
|
+
if (opts?.description)
|
|
2997
|
+
input.description = opts.description;
|
|
2998
|
+
if (opts?.promote !== undefined)
|
|
2999
|
+
input.promote = opts.promote;
|
|
3000
|
+
return input;
|
|
3001
|
+
}
|
|
3002
|
+
function signalOpts(opts) {
|
|
3003
|
+
return opts?.signal ? { signal: opts.signal } : undefined;
|
|
3004
|
+
}
|
|
3005
|
+
/** The form a HookDef definition is stored in: hooks.Def.Normalize followed
|
|
3006
|
+
* by Go's omitempty encoding. Applied to both sides of ensureHookDef's
|
|
3007
|
+
* comparison; the stored side is already in this form, so what matters is
|
|
3008
|
+
* that the overlay lands exactly where the server would put it. */
|
|
3009
|
+
function normalizeHookDef(d) {
|
|
3010
|
+
const out = { event: d.event };
|
|
3011
|
+
const description = goTrimSpace(d.description ?? "");
|
|
3012
|
+
if (description)
|
|
3013
|
+
out.description = description;
|
|
3014
|
+
if (d.match?.tools && d.match.tools.length > 0)
|
|
3015
|
+
out.match = { tools: d.match.tools };
|
|
3016
|
+
const body = { kind: d.body.kind };
|
|
3017
|
+
if (d.body.kind === "code-js") {
|
|
3018
|
+
if (d.body.code)
|
|
3019
|
+
body.code = d.body.code;
|
|
3020
|
+
}
|
|
3021
|
+
else {
|
|
3022
|
+
if (d.body.url)
|
|
3023
|
+
body.url = d.body.url;
|
|
3024
|
+
if (d.body.headers && Object.keys(d.body.headers).length > 0)
|
|
3025
|
+
body.headers = d.body.headers;
|
|
3026
|
+
if (d.body.signing_secret)
|
|
3027
|
+
body.signing_secret = d.body.signing_secret;
|
|
3028
|
+
}
|
|
3029
|
+
out.body = body;
|
|
3030
|
+
out.fail_mode = d.fail_mode || "open";
|
|
3031
|
+
if (d.timeout_ms)
|
|
3032
|
+
out.timeout_ms = d.timeout_ms;
|
|
3033
|
+
return out;
|
|
3034
|
+
}
|
|
3035
|
+
/** strings.TrimSpace: Go's Unicode White_Space set, which is not
|
|
3036
|
+
* String.prototype.trim's (Go trims U+0085 and keeps U+FEFF; JavaScript does
|
|
3037
|
+
* the reverse). Trimming differently could make two descriptions the server
|
|
3038
|
+
* stores differently compare equal, and skip a real change. */
|
|
3039
|
+
const goTrimRe = /^[\t\n\v\f\r \u0085\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]+|[\t\n\v\f\r \u0085\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]+$/g;
|
|
3040
|
+
function goTrimSpace(s) {
|
|
3041
|
+
return s.replace(goTrimRe, "");
|
|
3042
|
+
}
|
|
3043
|
+
/** Structural JSON equality, ignoring object key order (the server's
|
|
3044
|
+
* encoding orders keys by struct field, a caller's literal by whatever it
|
|
3045
|
+
* wrote). Arrays compare in order. */
|
|
3046
|
+
function sameJSON(a, b) {
|
|
3047
|
+
if (a === b)
|
|
3048
|
+
return true;
|
|
3049
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
|
|
3050
|
+
return false;
|
|
3051
|
+
if (Array.isArray(a) !== Array.isArray(b))
|
|
3052
|
+
return false;
|
|
3053
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
3054
|
+
return a.length === b.length && a.every((v, i) => sameJSON(v, b[i]));
|
|
3055
|
+
}
|
|
3056
|
+
const ao = a;
|
|
3057
|
+
const bo = b;
|
|
3058
|
+
const keys = Object.keys(ao);
|
|
3059
|
+
if (keys.length !== Object.keys(bo).length)
|
|
3060
|
+
return false;
|
|
3061
|
+
return keys.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && sameJSON(ao[k], bo[k]));
|
|
3062
|
+
}
|
|
3063
|
+
/** toChangeFeedFrame types one frame of a change feed, or returns null for a
|
|
3064
|
+
* frame it does not recognise.
|
|
3065
|
+
*
|
|
3066
|
+
* The frames arrive through parseSSE, which backfills `type` from the SSE
|
|
3067
|
+
* event name ONLY when the payload has none. The `feed` and `error` frames
|
|
3068
|
+
* deliberately carry no `type`, so they surface as `type: "feed"` /
|
|
3069
|
+
* `type: "error"`; a `change` frame
|
|
3070
|
+
* carries its own change type (`memory.set`, …), so it is recognised by its
|
|
3071
|
+
* numeric `seq` instead. Anything else — an event name a newer server adds —
|
|
3072
|
+
* is skipped, the adapter's usual ignore-what-you-don't-know posture. */
|
|
3073
|
+
function toChangeFeedFrame(raw) {
|
|
3074
|
+
if (raw.type === "feed") {
|
|
3075
|
+
// An unreadable status must not default to enabled: the frame exists so a
|
|
3076
|
+
// disabled feed is not mistaken for a quiet one.
|
|
3077
|
+
if (typeof raw.enabled !== "boolean")
|
|
3078
|
+
return null;
|
|
3079
|
+
const { type: _type, ...status } = raw;
|
|
3080
|
+
return { kind: "feed", payload: status };
|
|
3081
|
+
}
|
|
3082
|
+
if (raw.type === "error") {
|
|
3083
|
+
// Like `feed`, the error frame carries no `type` of its own, so this one
|
|
3084
|
+
// was backfilled from the event name. Both fields are required: a
|
|
3085
|
+
// half-read error is skipped rather than surfaced as a blank one.
|
|
3086
|
+
if (typeof raw.code !== "string" || typeof raw.error !== "string")
|
|
3087
|
+
return null;
|
|
3088
|
+
const { type: _type, ...failure } = raw;
|
|
3089
|
+
return { kind: "error", payload: failure };
|
|
3090
|
+
}
|
|
3091
|
+
if (typeof raw.seq === "number" && typeof raw.type === "string") {
|
|
3092
|
+
return { kind: "change", payload: raw };
|
|
3093
|
+
}
|
|
3094
|
+
return null;
|
|
3095
|
+
}
|
|
2469
3096
|
// channelOpPath builds the v0.9.x Channel CRUD URL. Two families:
|
|
2470
3097
|
// - scope === "global" → /v1/_channels/{channel}/{op}
|
|
2471
3098
|
// - scope === "user" → /v1/users/{userId}/channels/{channel}/{op}
|