@loomcycle/client 1.112.0 → 1.114.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/dist/client.js CHANGED
@@ -135,6 +135,8 @@ function runBody(opts) {
135
135
  body.idempotency_key = opts.idempotencyKey;
136
136
  if (opts.maxWallSeconds !== undefined)
137
137
  body.max_wall_seconds = opts.maxWallSeconds;
138
+ if (opts.admission !== undefined)
139
+ body.admission = opts.admission;
138
140
  if (opts.retention !== undefined)
139
141
  body.retention = opts.retention;
140
142
  if (opts.metadata !== undefined)
@@ -209,6 +211,9 @@ function applyOverridesToWire(body, opts) {
209
211
  body.tool_choice = opts.toolChoice;
210
212
  if (opts.outputFormat !== undefined)
211
213
  body.output_format = opts.outputFormat;
214
+ // Same keys on the wire as in the option (queue_aware / switch_after_ms).
215
+ if (opts.routing !== undefined)
216
+ body.routing = opts.routing;
212
217
  }
213
218
  export class LoomcycleClient {
214
219
  ctx;
@@ -438,6 +443,23 @@ export class LoomcycleClient {
438
443
  async getEffectiveConfig(runId, opts) {
439
444
  return jsonFetch(this.ctx, `/v1/runs/${encodeURIComponent(runId)}/effective-config`, opts);
440
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 jsonFetch(this.ctx, `/v1/_agentdef/${encodeURIComponent(name)}/overridable${q}`, opts);
462
+ }
441
463
  /** Re-attach to a run's event stream by `run_id` (RFC AI), replaying from
442
464
  * `fromSeq` (default 0) then live-tailing — the gRPC-free twin of the Web
443
465
  * UI's resume-in-terminal. Mirrors `GET /v1/runs/{run_id}/stream`. The
@@ -706,8 +728,14 @@ export class LoomcycleClient {
706
728
  const resp = await postJSON(this.ctx, `/v1/agents/${encodeURIComponent(agentId)}/cancel`, { reason: opts?.reason ?? "" }, opts);
707
729
  return { cancelledCount: resp.cancelled_count };
708
730
  }
709
- /** List a user's recent agent runs, optionally filtered by status. With no
710
- * `status` only running runs are returned; `"all"` returns every status.
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.
711
739
  *
712
740
  * v0.9.x — `parentAgentId` narrows the result CLIENT-SIDE to runs
713
741
  * whose `parent_agent_id` matches. The server still returns the
@@ -716,17 +744,21 @@ export class LoomcycleClient {
716
744
  * n8n trigger pattern "show me all sub-runs spawned by parent X." */
717
745
  async listUserAgents(userId, opts) {
718
746
  const params = new URLSearchParams();
719
- if (opts?.status)
720
- params.set("status", opts.status);
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);
721
751
  if (opts?.tenant)
722
752
  params.set("tenant", opts.tenant);
723
753
  const q = params.toString() ? `?${params.toString()}` : "";
724
754
  const resp = await jsonFetch(this.ctx, `/v1/users/${encodeURIComponent(userId)}/agents${q}`, opts);
725
- const all = resp.agents ?? [];
755
+ let out = resp.agents ?? [];
726
756
  if (opts?.parentAgentId !== undefined && opts.parentAgentId !== "") {
727
- return all.filter((a) => a.parent_agent_id === opts.parentAgentId);
757
+ out = out.filter((a) => a.parent_agent_id === opts.parentAgentId);
728
758
  }
729
- return all;
759
+ if (resp.truncated)
760
+ out.truncated = true;
761
+ return out;
730
762
  }
731
763
  /** One page of a team walk's runs — the walk's own run (its run id IS the
732
764
  * walk id) and every member it spawned, oldest first. The walk's own row
@@ -1031,6 +1063,38 @@ export class LoomcycleClient {
1031
1063
  async resolveProbe(opts) {
1032
1064
  return postJSON(this.ctx, "/v1/_resolve/probe", undefined, opts);
1033
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 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 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 jsonFetch(this.ctx, `/v1/_providers/${encodeURIComponent(providerId)}/models`, opts);
1097
+ }
1034
1098
  // ---- Snapshot lifecycle ----
1035
1099
  /** Capture running-state into a per-section-semver JSON envelope.
1036
1100
  * Raises SnapshotTooLargeError on 413 when the envelope exceeds
@@ -1484,10 +1548,33 @@ export class LoomcycleClient {
1484
1548
  * Raises {@link SubstrateToolRefusedError} on tool-level refusals
1485
1549
  * (static-name collision, missing required credential, invalid
1486
1550
  * cron, etc.); {@link InvalidArgumentError} on 400 (malformed
1487
- * 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. */
1488
1558
  async scheduleDef(input, opts) {
1489
1559
  return postJSON(this.ctx, "/v1/_scheduledef", input, opts);
1490
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
+ }
1491
1578
  /** Invoke the v1.x RFC G A2AServerCardDef substrate tool over HTTP.
1492
1579
  * Author + fork + retire A2A server-card definitions at runtime.
1493
1580
  * Mirror of {@link LoomcycleClient.scheduleDef} for A2A server cards.
@@ -1529,10 +1616,33 @@ export class LoomcycleClient {
1529
1616
  *
1530
1617
  * Raises {@link SubstrateToolRefusedError} on tool-level refusals;
1531
1618
  * {@link InvalidArgumentError} on 400 (malformed JSON);
1532
- * {@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. */
1533
1625
  async webhookDef(input, opts) {
1534
1626
  return postJSON(this.ctx, "/v1/_webhookdef", input, opts);
1535
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
+ }
1536
1646
  /** Invoke the RFC I MR-3a MemoryBackendDef substrate tool over HTTP.
1537
1647
  * Author + fork + retire named memory backend definitions at runtime.
1538
1648
  * Mirror of {@link LoomcycleClient.webhookDef} for memory backends.
@@ -1561,10 +1671,71 @@ export class LoomcycleClient {
1561
1671
  * matches, its body (code-js or a webhook URL), fail mode and timeout.
1562
1672
  * Tenant-confined like agentDef. A HookDef fires on nothing by itself; an
1563
1673
  * agent definition, a team definition or a run names it. Mirrors
1564
- * 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}. */
1565
1683
  async hookDef(input, opts) {
1566
1684
  return postJSON(this.ctx, "/v1/_hookdef", input, opts);
1567
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
+ }
1568
1739
  /** OperatorTokenDef substrate ops (create/rotate/retire/get/list) —
1569
1740
  * RFC L OSS multi-tenant authorization. Operator-admin only. The
1570
1741
  * token plaintext is returned ONCE in the create/rotate response.
@@ -1595,6 +1766,109 @@ export class LoomcycleClient {
1595
1766
  async volumeDef(input, opts) {
1596
1767
  return postJSON(this.ctx, "/v1/_volumedef", input, opts);
1597
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 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 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 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 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 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 postJSON(this.ctx, `/v1/_schedules/${encodeURIComponent(defId)}/resume`, undefined, opts);
1871
+ }
1598
1872
  // ---- RFC AP Agent Teams (TeamDef substrate) ----
1599
1873
  //
1600
1874
  // TeamDefs are agent-team workflow graphs (states + transitions + a per-state
@@ -1623,15 +1897,37 @@ export class LoomcycleClient {
1623
1897
  }
1624
1898
  /** Author a new TeamDef (op=create). `overlay` is the workflow graph
1625
1899
  * (`{entry, states, transitions, max_iterations?, colors?}`), validated
1626
- * 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. */
1627
1906
  async createTeam(name, overlay, opts) {
1628
- return postJSON(this.ctx, "/v1/_teamdef", { op: "create", name, overlay }, opts);
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 postJSON(this.ctx, "/v1/_teamdef", body, { signal: opts?.signal });
1629
1913
  }
1630
1914
  /** Save an edited graph as a NEW version of an existing team (op=fork). With
1631
- * no parent the server forks the name's active def; `overlay` (a full graph)
1632
- * 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. */
1633
1922
  async forkTeam(name, overlay, opts) {
1634
- return postJSON(this.ctx, "/v1/_teamdef", { op: "fork", name, overlay }, opts);
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 postJSON(this.ctx, "/v1/_teamdef", body, { signal: opts?.signal });
1635
1931
  }
1636
1932
  /** List every version of ONE team, newest first (op=list) — the lineage
1637
1933
  * behind {@link LoomcycleClient.listTeams}' roll-up. Tenant-scoped
@@ -1703,6 +1999,8 @@ export class LoomcycleClient {
1703
1999
  body.review_ttl_seconds = target.reviewTtlSeconds;
1704
2000
  if (target.idempotencyKey !== undefined)
1705
2001
  body.idempotency_key = target.idempotencyKey;
2002
+ if (target.interruptOnCap !== undefined)
2003
+ body.interrupt_on_cap = target.interruptOnCap;
1706
2004
  return postJSON(this.ctx, "/v1/_teamdef", body, opts);
1707
2005
  }
1708
2006
  /** Read the debug breakpoints armed on a live team walk
@@ -1854,6 +2152,48 @@ export class LoomcycleClient {
1854
2152
  async history(input, opts) {
1855
2153
  return postJSON(this.ctx, "/v1/_history", input, opts);
1856
2154
  }
2155
+ async evaluation(input, opts) {
2156
+ return 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
+ }
1857
2197
  /** List the PERSISTENT volume universe for the caller's tenant — every
1858
2198
  * static volume (the shared bind floor, `source: "static"`, read-only)
1859
2199
  * plus the tenant's own dynamic VolumeDefs (`source: "dynamic"`). Host
@@ -2410,11 +2750,11 @@ export class LoomcycleClient {
2410
2750
  *
2411
2751
  * v1.78.0 — `walkId` is a SERVER-side filter: only the runs one team
2412
2752
  * walk spawned. A walk's own run_id IS its walk id, so a caller that
2413
- * started a team with `detach: true` passes back the handle it already
2753
+ * started a team with `mode: "detach"` passes back the handle it already
2414
2754
  * holds and gets a live view of that workflow's agents:
2415
2755
  *
2416
2756
  * ```ts
2417
- * const { run_id } = await client.runTeam({ team: "triage", detach: true });
2757
+ * const { run_id } = await client.runTeam({ name: "triage", mode: "detach" });
2418
2758
  * for await (const item of client.streamUserRunStates(userId, { walkId: run_id })) {
2419
2759
  * if (item.kind === "event") place(item.payload.parent_context?.wave_index);
2420
2760
  * }
@@ -2496,6 +2836,61 @@ export class LoomcycleClient {
2496
2836
  yield { kind: "close", payload: { reason: closeReason } };
2497
2837
  }
2498
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
+ }
2499
2894
  /** List credential METADATA for a scope (RFC AR) — never a secret value.
2500
2895
  * `user` = your own subject's tokens (per-user, e.g. a personal Telegram bot
2501
2896
  * token); `tenant` = shared across the tenant (operator authority). RFC CN: a
@@ -2506,10 +2901,35 @@ export class LoomcycleClient {
2506
2901
  }
2507
2902
  /** Store or rotate a credential (RFC AR). The plaintext `value` is write-only:
2508
2903
  * the server encrypts it, masks it from the transcript, and never returns it.
2509
- * 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. */
2510
2922
  async createCredential(input, opts) {
2511
2923
  return postJSON(this.ctx, "/v1/_credentialdef", { op: "create", ...input }, opts);
2512
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 postJSON(this.ctx, "/v1/_credentialdef", { op: "get", ...input }, opts);
2932
+ }
2513
2933
  /** Delete a credential (RFC AR). Anything referencing `$cred:<name>` at that
2514
2934
  * scope stops resolving. Mirrors `POST /v1/_credentialdef {op:delete}`. */
2515
2935
  async deleteCredential(input, opts) {
@@ -2565,6 +2985,113 @@ async function* parseRunStateSSE(reader) {
2565
2985
  }
2566
2986
  }
2567
2987
  }
2988
+ /** The wire input of a typed create / fork. `promote` goes out only when the
2989
+ * caller set it — false included — so the server's per-def default applies
2990
+ * otherwise. */
2991
+ function defWriteInput(op, name, overlay, opts) {
2992
+ const input = { op, name, overlay };
2993
+ if (opts?.parentDefId)
2994
+ input.parent_def_id = opts.parentDefId;
2995
+ if (opts?.description)
2996
+ input.description = opts.description;
2997
+ if (opts?.promote !== undefined)
2998
+ input.promote = opts.promote;
2999
+ return input;
3000
+ }
3001
+ function signalOpts(opts) {
3002
+ return opts?.signal ? { signal: opts.signal } : undefined;
3003
+ }
3004
+ /** The form a HookDef definition is stored in: hooks.Def.Normalize followed
3005
+ * by Go's omitempty encoding. Applied to both sides of ensureHookDef's
3006
+ * comparison; the stored side is already in this form, so what matters is
3007
+ * that the overlay lands exactly where the server would put it. */
3008
+ function normalizeHookDef(d) {
3009
+ const out = { event: d.event };
3010
+ const description = goTrimSpace(d.description ?? "");
3011
+ if (description)
3012
+ out.description = description;
3013
+ if (d.match?.tools && d.match.tools.length > 0)
3014
+ out.match = { tools: d.match.tools };
3015
+ const body = { kind: d.body.kind };
3016
+ if (d.body.kind === "code-js") {
3017
+ if (d.body.code)
3018
+ body.code = d.body.code;
3019
+ }
3020
+ else {
3021
+ if (d.body.url)
3022
+ body.url = d.body.url;
3023
+ if (d.body.headers && Object.keys(d.body.headers).length > 0)
3024
+ body.headers = d.body.headers;
3025
+ if (d.body.signing_secret)
3026
+ body.signing_secret = d.body.signing_secret;
3027
+ }
3028
+ out.body = body;
3029
+ out.fail_mode = d.fail_mode || "open";
3030
+ if (d.timeout_ms)
3031
+ out.timeout_ms = d.timeout_ms;
3032
+ return out;
3033
+ }
3034
+ /** strings.TrimSpace: Go's Unicode White_Space set, which is not
3035
+ * String.prototype.trim's (Go trims U+0085 and keeps U+FEFF; JavaScript does
3036
+ * the reverse). Trimming differently could make two descriptions the server
3037
+ * stores differently compare equal, and skip a real change. */
3038
+ 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;
3039
+ function goTrimSpace(s) {
3040
+ return s.replace(goTrimRe, "");
3041
+ }
3042
+ /** Structural JSON equality, ignoring object key order (the server's
3043
+ * encoding orders keys by struct field, a caller's literal by whatever it
3044
+ * wrote). Arrays compare in order. */
3045
+ function sameJSON(a, b) {
3046
+ if (a === b)
3047
+ return true;
3048
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
3049
+ return false;
3050
+ if (Array.isArray(a) !== Array.isArray(b))
3051
+ return false;
3052
+ if (Array.isArray(a) && Array.isArray(b)) {
3053
+ return a.length === b.length && a.every((v, i) => sameJSON(v, b[i]));
3054
+ }
3055
+ const ao = a;
3056
+ const bo = b;
3057
+ const keys = Object.keys(ao);
3058
+ if (keys.length !== Object.keys(bo).length)
3059
+ return false;
3060
+ return keys.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && sameJSON(ao[k], bo[k]));
3061
+ }
3062
+ /** toChangeFeedFrame types one frame of a change feed, or returns null for a
3063
+ * frame it does not recognise.
3064
+ *
3065
+ * The frames arrive through parseSSE, which backfills `type` from the SSE
3066
+ * event name ONLY when the payload has none. The `feed` and `error` frames
3067
+ * deliberately carry no `type`, so they surface as `type: "feed"` /
3068
+ * `type: "error"`; a `change` frame
3069
+ * carries its own change type (`memory.set`, …), so it is recognised by its
3070
+ * numeric `seq` instead. Anything else — an event name a newer server adds —
3071
+ * is skipped, the adapter's usual ignore-what-you-don't-know posture. */
3072
+ function toChangeFeedFrame(raw) {
3073
+ if (raw.type === "feed") {
3074
+ // An unreadable status must not default to enabled: the frame exists so a
3075
+ // disabled feed is not mistaken for a quiet one.
3076
+ if (typeof raw.enabled !== "boolean")
3077
+ return null;
3078
+ const { type: _type, ...status } = raw;
3079
+ return { kind: "feed", payload: status };
3080
+ }
3081
+ if (raw.type === "error") {
3082
+ // Like `feed`, the error frame carries no `type` of its own, so this one
3083
+ // was backfilled from the event name. Both fields are required: a
3084
+ // half-read error is skipped rather than surfaced as a blank one.
3085
+ if (typeof raw.code !== "string" || typeof raw.error !== "string")
3086
+ return null;
3087
+ const { type: _type, ...failure } = raw;
3088
+ return { kind: "error", payload: failure };
3089
+ }
3090
+ if (typeof raw.seq === "number" && typeof raw.type === "string") {
3091
+ return { kind: "change", payload: raw };
3092
+ }
3093
+ return null;
3094
+ }
2568
3095
  // channelOpPath builds the v0.9.x Channel CRUD URL. Two families:
2569
3096
  // - scope === "global" → /v1/_channels/{channel}/{op}
2570
3097
  // - scope === "user" → /v1/users/{userId}/channels/{channel}/{op}