@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.
@@ -138,6 +138,8 @@ 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;
141
143
  if (opts.retention !== undefined)
142
144
  body.retention = opts.retention;
143
145
  if (opts.metadata !== undefined)
@@ -212,6 +214,9 @@ function applyOverridesToWire(body, opts) {
212
214
  body.tool_choice = opts.toolChoice;
213
215
  if (opts.outputFormat !== undefined)
214
216
  body.output_format = opts.outputFormat;
217
+ // Same keys on the wire as in the option (queue_aware / switch_after_ms).
218
+ if (opts.routing !== undefined)
219
+ body.routing = opts.routing;
215
220
  }
216
221
  class LoomcycleClient {
217
222
  ctx;
@@ -441,6 +446,23 @@ class LoomcycleClient {
441
446
  async getEffectiveConfig(runId, opts) {
442
447
  return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, `/v1/runs/${encodeURIComponent(runId)}/effective-config`, opts);
443
448
  }
449
+ /** Which per-run overrides a run of `name` may set, for this caller — what a
450
+ * run form needs before it sends anything. Mirrors
451
+ * `GET /v1/_agentdef/{name}/overridable`.
452
+ *
453
+ * Folds the agent's `allow_overrides` with the caller's token overrides
454
+ * policy, through the same checks the run path refuses with; a refused field
455
+ * names which (`reason`). Overridable fields carry what the run path then
456
+ * accepts: `narrow_only`, `max`, and `values` for a closed set.
457
+ *
458
+ * `substrate:tenant` (a non-isolated member too). A token limited to certain
459
+ * agents may ask about those agents only. Raises {@link AgentNotFoundError}
460
+ * (404) for an unknown or another tenant's agent, and a 403
461
+ * `agent_not_allowed` for an agent outside the token's allowlist. */
462
+ async getAgentOverridable(name, opts) {
463
+ const q = opts?.tenant ? `?tenant=${encodeURIComponent(opts.tenant)}` : "";
464
+ return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, `/v1/_agentdef/${encodeURIComponent(name)}/overridable${q}`, opts);
465
+ }
444
466
  /** Re-attach to a run's event stream by `run_id` (RFC AI), replaying from
445
467
  * `fromSeq` (default 0) then live-tailing — the gRPC-free twin of the Web
446
468
  * UI's resume-in-terminal. Mirrors `GET /v1/runs/{run_id}/stream`. The
@@ -709,8 +731,14 @@ class LoomcycleClient {
709
731
  const resp = await (0, fetch_helpers_js_1.postJSON)(this.ctx, `/v1/agents/${encodeURIComponent(agentId)}/cancel`, { reason: opts?.reason ?? "" }, opts);
710
732
  return { cancelledCount: resp.cancelled_count };
711
733
  }
712
- /** List a user's recent agent runs, optionally filtered by status. With no
713
- * `status` only running runs are returned; `"all"` returns every status.
734
+ /** List a user's recent agent runs (at most 100), optionally filtered by
735
+ * status. With no `status` only running runs are returned; `"all"` returns
736
+ * every status; several statuses list a run in any of them.
737
+ *
738
+ * `awaitedState` lists only running runs blocked on one of the named waits
739
+ * (`"review"`, `"input"`, …). The state is derived per run, so the server
740
+ * reads at most 1,000 running runs; the returned array carries
741
+ * `truncated: true` when it stopped with some unread.
714
742
  *
715
743
  * v0.9.x — `parentAgentId` narrows the result CLIENT-SIDE to runs
716
744
  * whose `parent_agent_id` matches. The server still returns the
@@ -719,17 +747,21 @@ class LoomcycleClient {
719
747
  * n8n trigger pattern "show me all sub-runs spawned by parent X." */
720
748
  async listUserAgents(userId, opts) {
721
749
  const params = new URLSearchParams();
722
- if (opts?.status)
723
- params.set("status", opts.status);
750
+ for (const s of [].concat(opts?.status ?? []))
751
+ params.append("status", s);
752
+ for (const s of [].concat(opts?.awaitedState ?? []))
753
+ params.append("awaited_state", s);
724
754
  if (opts?.tenant)
725
755
  params.set("tenant", opts.tenant);
726
756
  const q = params.toString() ? `?${params.toString()}` : "";
727
757
  const resp = await (0, fetch_helpers_js_1.jsonFetch)(this.ctx, `/v1/users/${encodeURIComponent(userId)}/agents${q}`, opts);
728
- const all = resp.agents ?? [];
758
+ let out = resp.agents ?? [];
729
759
  if (opts?.parentAgentId !== undefined && opts.parentAgentId !== "") {
730
- return all.filter((a) => a.parent_agent_id === opts.parentAgentId);
760
+ out = out.filter((a) => a.parent_agent_id === opts.parentAgentId);
731
761
  }
732
- return all;
762
+ if (resp.truncated)
763
+ out.truncated = true;
764
+ return out;
733
765
  }
734
766
  /** One page of a team walk's runs — the walk's own run (its run id IS the
735
767
  * walk id) and every member it spawned, oldest first. The walk's own row
@@ -1034,6 +1066,38 @@ class LoomcycleClient {
1034
1066
  async resolveProbe(opts) {
1035
1067
  return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_resolve/probe", undefined, opts);
1036
1068
  }
1069
+ /** The configured model aliases — the operator's top-level `models:` map,
1070
+ * alias name → `{provider, model, kind?}` (`GET /v1/_models`). Non-secret and
1071
+ * global (the same set for every tenant); tenant-readable. Store an alias on
1072
+ * an agent or fork rather than the concrete model it names, and the agent
1073
+ * follows when the operator retargets the alias. */
1074
+ async listModels(opts) {
1075
+ return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, "/v1/_models", opts);
1076
+ }
1077
+ /** For each user tier × tier, the ordered provider/model cascade a run
1078
+ * resolves to now, top first, with live availability and the candidate
1079
+ * `selected` (`GET /v1/_routing`). Tenant-readable. A non-admin caller gets
1080
+ * `admin: false`, no raw `last_error`, and — when the operator-key gate
1081
+ * restricts it — only the providers it can key itself
1082
+ * (`operator_key_restricted: true`). */
1083
+ async getRouting(opts) {
1084
+ return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, "/v1/_routing", opts);
1085
+ }
1086
+ /** List the models a provider serves right now, by a live round-trip to it
1087
+ * (`GET /v1/_providers/{id}/models`) rather than the cached probe matrix.
1088
+ *
1089
+ * **Requires an admin token.** A `substrate:tenant` token is refused with a
1090
+ * {@link LoomcycleError} whose `status` is 403 ("insufficient scope"); a
1091
+ * model picker built on a tenant token should fall back to
1092
+ * {@link LoomcycleClient.listModels} plus free-text model entry.
1093
+ *
1094
+ * Errors carry the server's code on `e.code`: 404 `provider_unknown` (no such
1095
+ * driver), 503 `provider_unavailable` (known, but not configured — e.g. no
1096
+ * key), 502 `provider_list_failed` (the round-trip failed; bounded at 10 s
1097
+ * server-side). */
1098
+ async listProviderModels(providerId, opts) {
1099
+ return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, `/v1/_providers/${encodeURIComponent(providerId)}/models`, opts);
1100
+ }
1037
1101
  // ---- Snapshot lifecycle ----
1038
1102
  /** Capture running-state into a per-section-semver JSON envelope.
1039
1103
  * Raises SnapshotTooLargeError on 413 when the envelope exceeds
@@ -1487,10 +1551,33 @@ class LoomcycleClient {
1487
1551
  * Raises {@link SubstrateToolRefusedError} on tool-level refusals
1488
1552
  * (static-name collision, missing required credential, invalid
1489
1553
  * cron, etc.); {@link InvalidArgumentError} on 400 (malformed
1490
- * JSON); {@link AuthError} on 401. */
1554
+ * JSON); {@link AuthError} on 401.
1555
+ *
1556
+ * Annotate the input as {@link ScheduleDefInput} to have the overlay and
1557
+ * each op's fields checked (including `add_hook` / `remove_hook`, which the
1558
+ * loose {@link SubstrateToolInput} does not list); a loose input is sent
1559
+ * as it is. For create / fork, {@link LoomcycleClient.createScheduleDef} /
1560
+ * {@link LoomcycleClient.forkScheduleDef} check a bare literal too. */
1491
1561
  async scheduleDef(input, opts) {
1492
1562
  return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_scheduledef", input, opts);
1493
1563
  }
1564
+ /** `scheduleDef({op: "create"})` with the overlay typed and no loose
1565
+ * fallback. {@link ScheduleDefOverlay} stays OPEN (the server drops keys it
1566
+ * does not know), so a misspelt key still compiles here; what fails is a
1567
+ * wrong type or value on a key it declares (`concurrency_policy: "skip"`,
1568
+ * an `on_complete` entry missing its `tool`). New versions are promoted
1569
+ * unless `promote: false`. */
1570
+ async createScheduleDef(name, overlay, opts) {
1571
+ return (await this.scheduleDef(defWriteInput("create", name, overlay, opts), signalOpts(opts)));
1572
+ }
1573
+ /** `scheduleDef({op: "fork"})`, typed like
1574
+ * {@link LoomcycleClient.createScheduleDef}. A field the overlay leaves out
1575
+ * keeps the parent's; the credential maps merge key by key. Forks the
1576
+ * active version of `name` (or its yaml template) unless `parentDefId` is
1577
+ * given; promoted unless `promote: false`. */
1578
+ async forkScheduleDef(name, overlay, opts) {
1579
+ return (await this.scheduleDef(defWriteInput("fork", name, overlay, opts), signalOpts(opts)));
1580
+ }
1494
1581
  /** Invoke the v1.x RFC G A2AServerCardDef substrate tool over HTTP.
1495
1582
  * Author + fork + retire A2A server-card definitions at runtime.
1496
1583
  * Mirror of {@link LoomcycleClient.scheduleDef} for A2A server cards.
@@ -1532,10 +1619,33 @@ class LoomcycleClient {
1532
1619
  *
1533
1620
  * Raises {@link SubstrateToolRefusedError} on tool-level refusals;
1534
1621
  * {@link InvalidArgumentError} on 400 (malformed JSON);
1535
- * {@link AuthError} on 401. */
1622
+ * {@link AuthError} on 401.
1623
+ *
1624
+ * Annotate the input as {@link WebhookDefInput} to have the overlay and
1625
+ * each op's fields checked; a loose input is sent as it is. For create /
1626
+ * fork, {@link LoomcycleClient.createWebhookDef} /
1627
+ * {@link LoomcycleClient.forkWebhookDef} check a bare literal too. */
1536
1628
  async webhookDef(input, opts) {
1537
1629
  return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_webhookdef", input, opts);
1538
1630
  }
1631
+ /** `webhookDef({op: "create"})` with the overlay typed and no loose
1632
+ * fallback. {@link WebhookDefOverlay} stays OPEN for now (the server drops
1633
+ * keys it does not know), so a misspelt key still compiles here; what
1634
+ * fails is a wrong type or value on a key it declares (`delivery: "run"`,
1635
+ * `auth.kind: "basic"`, a misspelt key inside `rate_limit`). Promoted
1636
+ * unless `promote: false`. */
1637
+ async createWebhookDef(name, overlay, opts) {
1638
+ return (await this.webhookDef(defWriteInput("create", name, overlay, opts), signalOpts(opts)));
1639
+ }
1640
+ /** `webhookDef({op: "fork"})`, typed like
1641
+ * {@link LoomcycleClient.createWebhookDef}. A field the overlay leaves out
1642
+ * keeps the parent's; naming exactly one of `agent` / `channel` / `team`
1643
+ * clears the other two. Forks the active version of `name` (or its yaml
1644
+ * template) unless `parentDefId` is given; promoted unless
1645
+ * `promote: false`. */
1646
+ async forkWebhookDef(name, overlay, opts) {
1647
+ return (await this.webhookDef(defWriteInput("fork", name, overlay, opts), signalOpts(opts)));
1648
+ }
1539
1649
  /** Invoke the RFC I MR-3a MemoryBackendDef substrate tool over HTTP.
1540
1650
  * Author + fork + retire named memory backend definitions at runtime.
1541
1651
  * Mirror of {@link LoomcycleClient.webhookDef} for memory backends.
@@ -1564,10 +1674,71 @@ class LoomcycleClient {
1564
1674
  * matches, its body (code-js or a webhook URL), fail mode and timeout.
1565
1675
  * Tenant-confined like agentDef. A HookDef fires on nothing by itself; an
1566
1676
  * agent definition, a team definition or a run names it. Mirrors
1567
- * POST /v1/_hookdef. */
1677
+ * POST /v1/_hookdef.
1678
+ *
1679
+ * Annotate the input as {@link HookDefInput} to have the overlay checked
1680
+ * against the definition the server decodes STRICTLY — an unknown overlay
1681
+ * key is then a compile error instead of a refusal. A loose
1682
+ * {@link SubstrateToolInput} is sent as it is — including a bare literal
1683
+ * with a misspelt key, which falls through to that loose shape. To have a
1684
+ * bare literal checked, use {@link LoomcycleClient.createHookDef} /
1685
+ * {@link LoomcycleClient.forkHookDef}. */
1568
1686
  async hookDef(input, opts) {
1569
1687
  return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_hookdef", input, opts);
1570
1688
  }
1689
+ /** `hookDef({op: "create"})` with no loose fallback: `overlay` is the
1690
+ * CLOSED {@link HookDefOverlay}, so any key the server would refuse is a
1691
+ * compile error, even in a bare literal. Every create mints a new version
1692
+ * (use {@link LoomcycleClient.ensureHookDef} to register only on change);
1693
+ * promoted unless `promote: false`. */
1694
+ async createHookDef(name, overlay, opts) {
1695
+ return (await this.hookDef(defWriteInput("create", name, overlay, opts), signalOpts(opts)));
1696
+ }
1697
+ /** `hookDef({op: "fork"})`, closed like
1698
+ * {@link LoomcycleClient.createHookDef}. Each top-level field given replaces
1699
+ * the parent's; `null` clears one. Forks the active version of `name` in
1700
+ * your tenant unless `parentDefId` is given. NOT promoted unless
1701
+ * `promote: true` — authoring and putting in force are two steps. */
1702
+ async forkHookDef(name, overlay, opts) {
1703
+ return (await this.hookDef(defWriteInput("fork", name, overlay, opts), signalOpts(opts)));
1704
+ }
1705
+ /** Register a HookDef idempotently — the register-on-every-boot
1706
+ * convenience, sibling of {@link LoomcycleClient.ensureMcpServer}.
1707
+ *
1708
+ * Unlike MCPServerDef and AgentDef, a HookDef `create` is NOT
1709
+ * content-addressed: every create mints a new version. And the hash
1710
+ * `verify` compares is computed over the server's own JSON encoding of the
1711
+ * definition, which this client cannot reproduce byte for byte. So instead
1712
+ * this reads the active version (`verify` for its def_id — it answers
1713
+ * `deployed: false` rather than raising — then `get`) and compares its
1714
+ * stored definition with `overlay`, both normalized the way the server
1715
+ * normalizes (`fail_mode` defaults to `"open"`, an empty `match.tools`,
1716
+ * description, header map or timeout is dropped). Equal → no write,
1717
+ * `changed: false`. Otherwise `create` with the default promote, which
1718
+ * makes the new version active.
1719
+ *
1720
+ * The normalization only collapses what the server collapses, so a real
1721
+ * change is never skipped; should the server come to normalize something
1722
+ * more, the cost is a redundant version, not a lost one. The rationale
1723
+ * in {@link EnsureHookDefOptions.description} is not part of the
1724
+ * definition and does not count as a change. */
1725
+ async ensureHookDef(name, overlay, opts) {
1726
+ const callOpts = signalOpts(opts);
1727
+ const verified = (await this.hookDef({ op: "verify", name }, callOpts));
1728
+ if (verified.deployed && verified.current_def_id) {
1729
+ // By def_id, not name: the version verify reported, even if another
1730
+ // writer promotes a different one in between.
1731
+ const active = (await this.hookDef({ op: "get", def_id: verified.current_def_id }, callOpts));
1732
+ if (sameJSON(normalizeHookDef(active.definition), normalizeHookDef(overlay))) {
1733
+ return { name, defId: active.def_id, version: active.version, changed: false };
1734
+ }
1735
+ }
1736
+ const input = { op: "create", name, overlay };
1737
+ if (opts?.description)
1738
+ input.description = opts.description;
1739
+ const row = (await this.hookDef(input, callOpts));
1740
+ return { name, defId: row.def_id, version: row.version, changed: true };
1741
+ }
1571
1742
  /** OperatorTokenDef substrate ops (create/rotate/retire/get/list) —
1572
1743
  * RFC L OSS multi-tenant authorization. Operator-admin only. The
1573
1744
  * token plaintext is returned ONCE in the create/rotate response.
@@ -1598,6 +1769,109 @@ class LoomcycleClient {
1598
1769
  async volumeDef(input, opts) {
1599
1770
  return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_volumedef", input, opts);
1600
1771
  }
1772
+ // ---- Def name enumeration (GET /v1/_<family>def/names) ----
1773
+ //
1774
+ // Every name declared in one substrate family, each with its version
1775
+ // roll-up — what the op-dispatched `list` op cannot give (it lists the
1776
+ // versions of ONE name). Tenant-scoped server-side: a substrate:tenant token
1777
+ // sees only its own tenant's names; an admin sees all, or focuses one tenant
1778
+ // with `tenant` (ignored for a tenant token). The TeamDef family is
1779
+ // {@link LoomcycleClient.listTeams}.
1780
+ listDefNames(family, opts) {
1781
+ const q = opts?.tenant ? `?tenant=${encodeURIComponent(opts.tenant)}` : "";
1782
+ return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, `/v1/_${family}/names${q}`, opts);
1783
+ }
1784
+ /** Every AgentDef name (`GET /v1/_agentdef/names`). */
1785
+ async listAgentDefNames(opts) {
1786
+ return this.listDefNames("agentdef", opts);
1787
+ }
1788
+ /** Every SkillDef name (`GET /v1/_skilldef/names`). */
1789
+ async listSkillDefNames(opts) {
1790
+ return this.listDefNames("skilldef", opts);
1791
+ }
1792
+ /** Every HookDef name (`GET /v1/_hookdef/names`). */
1793
+ async listHookDefNames(opts) {
1794
+ return this.listDefNames("hookdef", opts);
1795
+ }
1796
+ /** Every MCPServerDef name (`GET /v1/_mcpserverdef/names`). */
1797
+ async listMcpServerDefNames(opts) {
1798
+ return this.listDefNames("mcpserverdef", opts);
1799
+ }
1800
+ /** Every ScheduleDef name (`GET /v1/_scheduledef/names`). Substrate only —
1801
+ * {@link LoomcycleClient.listAllSchedules} merges in the static yaml ones. */
1802
+ async listScheduleDefNames(opts) {
1803
+ return this.listDefNames("scheduledef", opts);
1804
+ }
1805
+ /** Every A2AServerCardDef name (`GET /v1/_a2aservercarddef/names`). */
1806
+ async listA2AServerCardDefNames(opts) {
1807
+ return this.listDefNames("a2aservercarddef", opts);
1808
+ }
1809
+ /** Every A2AAgentDef name (`GET /v1/_a2aagentdef/names`). */
1810
+ async listA2AAgentDefNames(opts) {
1811
+ return this.listDefNames("a2aagentdef", opts);
1812
+ }
1813
+ /** Every WebhookDef name (`GET /v1/_webhookdef/names`). */
1814
+ async listWebhookDefNames(opts) {
1815
+ return this.listDefNames("webhookdef", opts);
1816
+ }
1817
+ /** Every MemoryBackendDef name (`GET /v1/_memorybackenddef/names`). */
1818
+ async listMemoryBackendDefNames(opts) {
1819
+ return this.listDefNames("memorybackenddef", opts);
1820
+ }
1821
+ /** Every DocumentSourceDef name (`GET /v1/_documentsourcedef/names`). */
1822
+ async listDocumentSourceDefNames(opts) {
1823
+ return this.listDefNames("documentsourcedef", opts);
1824
+ }
1825
+ /** Every OperatorTokenDef name (`GET /v1/_operatortokendef/names`), with its
1826
+ * subject and token counts — never a token. **Admin token only**: token
1827
+ * minting stays operator-only, so a `substrate:tenant` token is refused
1828
+ * with 403. */
1829
+ async listOperatorTokenDefNames(opts) {
1830
+ return this.listDefNames("operatortokendef", opts);
1831
+ }
1832
+ // ---- Schedules admin (GET/POST /v1/_schedules/...) ----
1833
+ //
1834
+ // What the Web UI's schedules page drives: the merged list, one schedule's
1835
+ // runtime state, and run-now / pause / resume. Authoring stays on
1836
+ // {@link LoomcycleClient.scheduleDef}. The per-schedule methods take the
1837
+ // ScheduleDef's `active_def_id`; a static yaml schedule has none to give, and
1838
+ // a tenant token reaching another tenant's def gets the same 404 as an
1839
+ // unknown one ({@link NotFoundError}).
1840
+ /** Every schedule: static yaml `scheduled_runs` and ScheduleDefs, merged by
1841
+ * name (`GET /v1/_schedules/list-all`), sorted by name. A tenant token sees
1842
+ * only its own tenant's ScheduleDefs — static schedules are operator
1843
+ * automation and appear only for an admin, who may focus one tenant with
1844
+ * `tenant`. */
1845
+ async listAllSchedules(opts) {
1846
+ const q = opts?.tenant ? `?tenant=${encodeURIComponent(opts.tenant)}` : "";
1847
+ return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, `/v1/_schedules/list-all${q}`, opts);
1848
+ }
1849
+ /** One schedule's runtime state (`GET /v1/_schedules/{def_id}/state`): last
1850
+ * and next fire, pause, the last slot claim, the catch-up backlog and the
1851
+ * runs it has in flight. 404 when the def has no state row (never promoted,
1852
+ * retired, or not yours). */
1853
+ async getScheduleState(defId, opts) {
1854
+ return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, `/v1/_schedules/${encodeURIComponent(defId)}/state`, opts);
1855
+ }
1856
+ /** Fire a schedule now (`POST /v1/_schedules/{def_id}/run-now`). The run
1857
+ * starts on the scheduler's next tick, not during this call, and the regular
1858
+ * cadence continues after it. A run already in flight is handled by the
1859
+ * schedule's `concurrency_policy`. 404 for an unknown def. */
1860
+ async runScheduleNow(defId, opts) {
1861
+ return (0, fetch_helpers_js_1.postJSON)(this.ctx, `/v1/_schedules/${encodeURIComponent(defId)}/run-now`, undefined, opts);
1862
+ }
1863
+ /** Pause a schedule indefinitely (`POST /v1/_schedules/{def_id}/pause`) until
1864
+ * {@link LoomcycleClient.resumeSchedule}. Takes no duration: the server sets
1865
+ * `paused_until` about 100 years ahead. Distinct from
1866
+ * {@link LoomcycleClient.pauseRuntime}, which stops the whole runtime. */
1867
+ async pauseSchedule(defId, opts) {
1868
+ return (0, fetch_helpers_js_1.postJSON)(this.ctx, `/v1/_schedules/${encodeURIComponent(defId)}/pause`, undefined, opts);
1869
+ }
1870
+ /** Clear a schedule's pause (`POST /v1/_schedules/{def_id}/resume`), so the
1871
+ * scheduler considers it due again; `next_run_at` is left as it was. */
1872
+ async resumeSchedule(defId, opts) {
1873
+ return (0, fetch_helpers_js_1.postJSON)(this.ctx, `/v1/_schedules/${encodeURIComponent(defId)}/resume`, undefined, opts);
1874
+ }
1601
1875
  // ---- RFC AP Agent Teams (TeamDef substrate) ----
1602
1876
  //
1603
1877
  // TeamDefs are agent-team workflow graphs (states + transitions + a per-state
@@ -1626,15 +1900,37 @@ class LoomcycleClient {
1626
1900
  }
1627
1901
  /** Author a new TeamDef (op=create). `overlay` is the workflow graph
1628
1902
  * (`{entry, states, transitions, max_iterations?, colors?}`), validated
1629
- * server-side — an invalid graph raises {@link SubstrateToolRefusedError}. */
1903
+ * server-side — an invalid graph raises {@link SubstrateToolRefusedError}.
1904
+ *
1905
+ * `opts.promote` and `opts.description` ride beside `signal`, as on
1906
+ * {@link LoomcycleClient.forkTeam}; each is sent only when set. Omitted,
1907
+ * the server promotes a create (unlike a fork); `promote: false` authors the
1908
+ * version without putting it in force. */
1630
1909
  async createTeam(name, overlay, opts) {
1631
- return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_teamdef", { op: "create", name, overlay }, opts);
1910
+ const body = { op: "create", name, overlay };
1911
+ if (opts?.promote !== undefined)
1912
+ body.promote = opts.promote;
1913
+ if (opts?.description !== undefined)
1914
+ body.description = opts.description;
1915
+ return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_teamdef", body, { signal: opts?.signal });
1632
1916
  }
1633
1917
  /** Save an edited graph as a NEW version of an existing team (op=fork). With
1634
- * no parent the server forks the name's active def; `overlay` (a full graph)
1635
- * replaces it. Validated server-side ({@link SubstrateToolRefusedError}). */
1918
+ * no `parentDefId` the server forks the name's active def; `overlay` (a full
1919
+ * graph) replaces it. Validated server-side ({@link SubstrateToolRefusedError}).
1920
+ *
1921
+ * The fork's own fields ride in `opts` beside `signal` — the pattern
1922
+ * {@link LoomcycleClient.setRunBreakpoints} uses — so existing two- and
1923
+ * three-argument calls are unchanged. `promote: true` puts the new version
1924
+ * in force in the same call; omitted, it is authored but not active. */
1636
1925
  async forkTeam(name, overlay, opts) {
1637
- return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_teamdef", { op: "fork", name, overlay }, opts);
1926
+ const body = { op: "fork", name, overlay };
1927
+ if (opts?.promote !== undefined)
1928
+ body.promote = opts.promote;
1929
+ if (opts?.parentDefId !== undefined)
1930
+ body.parent_def_id = opts.parentDefId;
1931
+ if (opts?.description !== undefined)
1932
+ body.description = opts.description;
1933
+ return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_teamdef", body, { signal: opts?.signal });
1638
1934
  }
1639
1935
  /** List every version of ONE team, newest first (op=list) — the lineage
1640
1936
  * behind {@link LoomcycleClient.listTeams}' roll-up. Tenant-scoped
@@ -1706,6 +2002,8 @@ class LoomcycleClient {
1706
2002
  body.review_ttl_seconds = target.reviewTtlSeconds;
1707
2003
  if (target.idempotencyKey !== undefined)
1708
2004
  body.idempotency_key = target.idempotencyKey;
2005
+ if (target.interruptOnCap !== undefined)
2006
+ body.interrupt_on_cap = target.interruptOnCap;
1709
2007
  return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_teamdef", body, opts);
1710
2008
  }
1711
2009
  /** Read the debug breakpoints armed on a live team walk
@@ -1857,6 +2155,48 @@ class LoomcycleClient {
1857
2155
  async history(input, opts) {
1858
2156
  return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_history", input, opts);
1859
2157
  }
2158
+ async evaluation(input, opts) {
2159
+ return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_evaluation", input, opts);
2160
+ }
2161
+ /** Record a score against a run — `evaluation({op:"submit"})`. Scores are
2162
+ * additive: a second submit records another evaluation. */
2163
+ async submitEvaluation(runId, score, opts) {
2164
+ const input = { op: "submit", run_id: runId, score };
2165
+ if (opts?.dimensions !== undefined)
2166
+ input.dimensions = opts.dimensions;
2167
+ if (opts?.judgement !== undefined)
2168
+ input.judgement = opts.judgement;
2169
+ if (opts?.rationale !== undefined)
2170
+ input.rationale = opts.rationale;
2171
+ return this.evaluation(input, { signal: opts?.signal });
2172
+ }
2173
+ /** One evaluation by id — `evaluation({op:"get"})`. */
2174
+ async getEvaluation(evalId, opts) {
2175
+ return this.evaluation({ op: "get", eval_id: evalId }, opts);
2176
+ }
2177
+ /** Every evaluation of one run — `evaluation({op:"list_for_run"})`. */
2178
+ async listEvaluationsForRun(runId, opts) {
2179
+ const input = { op: "list_for_run", run_id: runId };
2180
+ if (opts?.limit !== undefined)
2181
+ input.limit = opts.limit;
2182
+ return this.evaluation(input, { signal: opts?.signal });
2183
+ }
2184
+ /** Every evaluation of one definition version, in your tenant —
2185
+ * `evaluation({op:"list_for_def"})`. */
2186
+ async listEvaluationsForDef(defId, opts) {
2187
+ const input = { op: "list_for_def", def_id: defId };
2188
+ if (opts?.limit !== undefined)
2189
+ input.limit = opts.limit;
2190
+ return this.evaluation(input, { signal: opts?.signal });
2191
+ }
2192
+ /** A definition's scores rolled up — `evaluation({op:"aggregate"})`.
2193
+ * `includeLineage` adds the ancestors' evaluations along parent_def_id. */
2194
+ async aggregateEvaluations(defId, opts) {
2195
+ const input = { op: "aggregate", def_id: defId };
2196
+ if (opts?.includeLineage !== undefined)
2197
+ input.include_lineage = opts.includeLineage;
2198
+ return this.evaluation(input, { signal: opts?.signal });
2199
+ }
1860
2200
  /** List the PERSISTENT volume universe for the caller's tenant — every
1861
2201
  * static volume (the shared bind floor, `source: "static"`, read-only)
1862
2202
  * plus the tenant's own dynamic VolumeDefs (`source: "dynamic"`). Host
@@ -2413,11 +2753,11 @@ class LoomcycleClient {
2413
2753
  *
2414
2754
  * v1.78.0 — `walkId` is a SERVER-side filter: only the runs one team
2415
2755
  * walk spawned. A walk's own run_id IS its walk id, so a caller that
2416
- * started a team with `detach: true` passes back the handle it already
2756
+ * started a team with `mode: "detach"` passes back the handle it already
2417
2757
  * holds and gets a live view of that workflow's agents:
2418
2758
  *
2419
2759
  * ```ts
2420
- * const { run_id } = await client.runTeam({ team: "triage", detach: true });
2760
+ * const { run_id } = await client.runTeam({ name: "triage", mode: "detach" });
2421
2761
  * for await (const item of client.streamUserRunStates(userId, { walkId: run_id })) {
2422
2762
  * if (item.kind === "event") place(item.payload.parent_context?.wave_index);
2423
2763
  * }
@@ -2499,6 +2839,61 @@ class LoomcycleClient {
2499
2839
  yield { kind: "close", payload: { reason: closeReason } };
2500
2840
  }
2501
2841
  }
2842
+ /** Tail the caller's tenant's MEMORY change feed (RFC CD Part C) — one
2843
+ * value-free coordinate per `memory.set` / `memory.delete` /
2844
+ * `memory.scope_deleted`. Mirrors `GET /v1/_memory/changes?since=<seq>`.
2845
+ *
2846
+ * The first item is always `{ kind: "feed", payload: ChangeFeedStatus }`,
2847
+ * then one `{ kind: "change", payload: MemoryChange }` per change, oldest
2848
+ * first. Pull the current value through the data API; the feed never
2849
+ * carries it.
2850
+ *
2851
+ * - The feed is EMPTY unless the server runs with
2852
+ * `LOOMCYCLE_MEMORY_CHANGES_ENABLED`. The stream still opens and idles,
2853
+ * so read `payload.enabled` on the feed frame instead of waiting.
2854
+ * - Scope is the caller's OWN tenant; there is no cross-tenant feed.
2855
+ * - Needs `substrate:tenant` (or admin). A tenant MEMBER's token is refused
2856
+ * — the feed exposes the tenant's write pattern, which is operator
2857
+ * observability, not member data.
2858
+ * - To resume after a disconnect, persist the last `payload.seq` you
2859
+ * processed and pass it back as `since`. A cursor older than the
2860
+ * server's change retention resumes past the pruned gap without saying
2861
+ * so.
2862
+ * - A `{ kind: "error", payload: ChangeFeedError }` item means the server
2863
+ * could not read its change log. It is NOT fatal and iteration
2864
+ * continues: the server retries with backoff without advancing its
2865
+ * cursor, so changes resume after recovery with no gap. One item per
2866
+ * failure streak.
2867
+ * - The stream has no end of its own: iteration ends when the server
2868
+ * closes it (shutdown — reconnect with `since`) or throws an
2869
+ * `AbortError` when `signal` aborts. */
2870
+ async *streamMemoryChanges(opts) {
2871
+ yield* this.streamChangeFeed("/v1/_memory/changes", opts);
2872
+ }
2873
+ /** Tail the caller's tenant's DOCUMENT change feed (RFC CD Part C) — one
2874
+ * coordinate per `document.chunk.updated` / `document.chunk.deleted`,
2875
+ * naming the `chunk_id`. Mirrors `GET /v1/_document/changes?since=<seq>`.
2876
+ * Same frames, scope, auth and resume contract as
2877
+ * {@link LoomcycleClient.streamMemoryChanges}; a reader wanting both
2878
+ * families opens both. */
2879
+ async *streamDocumentChanges(opts) {
2880
+ yield* this.streamChangeFeed("/v1/_document/changes", opts);
2881
+ }
2882
+ async *streamChangeFeed(path, opts) {
2883
+ const since = opts?.since;
2884
+ // The server reads an unparseable or negative `since` as 0 rather than
2885
+ // refusing it, which replays every retained change — a resume bug that
2886
+ // would look like a burst of activity. Refuse it here instead.
2887
+ if (since !== undefined && !(Number.isSafeInteger(since) && since >= 0)) {
2888
+ throw new Error(`loomcycle: change feed "since" must be a non-negative integer, got ${String(since)}`);
2889
+ }
2890
+ const q = since ? `?since=${since}` : "";
2891
+ for await (const ev of this.streamSSEGet(path + q, opts?.signal)) {
2892
+ const frame = toChangeFeedFrame(ev);
2893
+ if (frame)
2894
+ yield frame;
2895
+ }
2896
+ }
2502
2897
  /** List credential METADATA for a scope (RFC AR) — never a secret value.
2503
2898
  * `user` = your own subject's tokens (per-user, e.g. a personal Telegram bot
2504
2899
  * token); `tenant` = shared across the tenant (operator authority). RFC CN: a
@@ -2509,10 +2904,35 @@ class LoomcycleClient {
2509
2904
  }
2510
2905
  /** Store or rotate a credential (RFC AR). The plaintext `value` is write-only:
2511
2906
  * the server encrypts it, masks it from the transcript, and never returns it.
2512
- * Returns the stored row's metadata. Mirrors `POST /v1/_credentialdef {op:create}`. */
2907
+ * Returns the stored row's metadata. Mirrors `POST /v1/_credentialdef {op:create}`.
2908
+ *
2909
+ * `allowed_urls` restricts where the value may be sent: https URLs, each
2910
+ * covering itself and everything under it. `[]` stores a value that is
2911
+ * sent nowhere. Leave it out for no restriction, or, when the credential
2912
+ * already exists, to keep the list it has. A restricted credential is used
2913
+ * only where the destination is known and covered (an MCP server's or a
2914
+ * hook's header, a remote peer's key, a git source), never as a provider
2915
+ * key.
2916
+ *
2917
+ * `username` is the user name sent with the value where a destination
2918
+ * takes one (HTTP basic auth for a git source). Leave it out to keep the
2919
+ * stored one; `""` removes it.
2920
+ *
2921
+ * `expires_at` is an RFC3339 timestamp (`"2026-12-31T00:00:00Z"`) stored
2922
+ * with the credential and reported back in its metadata; any other format
2923
+ * is refused. `backend` is where the value is kept: `"inline"` (encrypted
2924
+ * in loomcycle's own store, the default) is the only one accepted. */
2513
2925
  async createCredential(input, opts) {
2514
2926
  return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_credentialdef", { op: "create", ...input }, opts);
2515
2927
  }
2928
+ /** Read ONE credential's metadata (RFC AR) — never its value. Answers
2929
+ * whether a name is stored, and its `allowed_urls` / `username` /
2930
+ * `expires_at`, without listing the whole scope. An absent name raises
2931
+ * {@link SubstrateToolRefusedError}. Mirrors
2932
+ * `POST /v1/_credentialdef {op:get}`. */
2933
+ async getCredential(input, opts) {
2934
+ return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_credentialdef", { op: "get", ...input }, opts);
2935
+ }
2516
2936
  /** Delete a credential (RFC AR). Anything referencing `$cred:<name>` at that
2517
2937
  * scope stops resolving. Mirrors `POST /v1/_credentialdef {op:delete}`. */
2518
2938
  async deleteCredential(input, opts) {
@@ -2569,6 +2989,113 @@ async function* parseRunStateSSE(reader) {
2569
2989
  }
2570
2990
  }
2571
2991
  }
2992
+ /** The wire input of a typed create / fork. `promote` goes out only when the
2993
+ * caller set it — false included — so the server's per-def default applies
2994
+ * otherwise. */
2995
+ function defWriteInput(op, name, overlay, opts) {
2996
+ const input = { op, name, overlay };
2997
+ if (opts?.parentDefId)
2998
+ input.parent_def_id = opts.parentDefId;
2999
+ if (opts?.description)
3000
+ input.description = opts.description;
3001
+ if (opts?.promote !== undefined)
3002
+ input.promote = opts.promote;
3003
+ return input;
3004
+ }
3005
+ function signalOpts(opts) {
3006
+ return opts?.signal ? { signal: opts.signal } : undefined;
3007
+ }
3008
+ /** The form a HookDef definition is stored in: hooks.Def.Normalize followed
3009
+ * by Go's omitempty encoding. Applied to both sides of ensureHookDef's
3010
+ * comparison; the stored side is already in this form, so what matters is
3011
+ * that the overlay lands exactly where the server would put it. */
3012
+ function normalizeHookDef(d) {
3013
+ const out = { event: d.event };
3014
+ const description = goTrimSpace(d.description ?? "");
3015
+ if (description)
3016
+ out.description = description;
3017
+ if (d.match?.tools && d.match.tools.length > 0)
3018
+ out.match = { tools: d.match.tools };
3019
+ const body = { kind: d.body.kind };
3020
+ if (d.body.kind === "code-js") {
3021
+ if (d.body.code)
3022
+ body.code = d.body.code;
3023
+ }
3024
+ else {
3025
+ if (d.body.url)
3026
+ body.url = d.body.url;
3027
+ if (d.body.headers && Object.keys(d.body.headers).length > 0)
3028
+ body.headers = d.body.headers;
3029
+ if (d.body.signing_secret)
3030
+ body.signing_secret = d.body.signing_secret;
3031
+ }
3032
+ out.body = body;
3033
+ out.fail_mode = d.fail_mode || "open";
3034
+ if (d.timeout_ms)
3035
+ out.timeout_ms = d.timeout_ms;
3036
+ return out;
3037
+ }
3038
+ /** strings.TrimSpace: Go's Unicode White_Space set, which is not
3039
+ * String.prototype.trim's (Go trims U+0085 and keeps U+FEFF; JavaScript does
3040
+ * the reverse). Trimming differently could make two descriptions the server
3041
+ * stores differently compare equal, and skip a real change. */
3042
+ 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;
3043
+ function goTrimSpace(s) {
3044
+ return s.replace(goTrimRe, "");
3045
+ }
3046
+ /** Structural JSON equality, ignoring object key order (the server's
3047
+ * encoding orders keys by struct field, a caller's literal by whatever it
3048
+ * wrote). Arrays compare in order. */
3049
+ function sameJSON(a, b) {
3050
+ if (a === b)
3051
+ return true;
3052
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
3053
+ return false;
3054
+ if (Array.isArray(a) !== Array.isArray(b))
3055
+ return false;
3056
+ if (Array.isArray(a) && Array.isArray(b)) {
3057
+ return a.length === b.length && a.every((v, i) => sameJSON(v, b[i]));
3058
+ }
3059
+ const ao = a;
3060
+ const bo = b;
3061
+ const keys = Object.keys(ao);
3062
+ if (keys.length !== Object.keys(bo).length)
3063
+ return false;
3064
+ return keys.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && sameJSON(ao[k], bo[k]));
3065
+ }
3066
+ /** toChangeFeedFrame types one frame of a change feed, or returns null for a
3067
+ * frame it does not recognise.
3068
+ *
3069
+ * The frames arrive through parseSSE, which backfills `type` from the SSE
3070
+ * event name ONLY when the payload has none. The `feed` and `error` frames
3071
+ * deliberately carry no `type`, so they surface as `type: "feed"` /
3072
+ * `type: "error"`; a `change` frame
3073
+ * carries its own change type (`memory.set`, …), so it is recognised by its
3074
+ * numeric `seq` instead. Anything else — an event name a newer server adds —
3075
+ * is skipped, the adapter's usual ignore-what-you-don't-know posture. */
3076
+ function toChangeFeedFrame(raw) {
3077
+ if (raw.type === "feed") {
3078
+ // An unreadable status must not default to enabled: the frame exists so a
3079
+ // disabled feed is not mistaken for a quiet one.
3080
+ if (typeof raw.enabled !== "boolean")
3081
+ return null;
3082
+ const { type: _type, ...status } = raw;
3083
+ return { kind: "feed", payload: status };
3084
+ }
3085
+ if (raw.type === "error") {
3086
+ // Like `feed`, the error frame carries no `type` of its own, so this one
3087
+ // was backfilled from the event name. Both fields are required: a
3088
+ // half-read error is skipped rather than surfaced as a blank one.
3089
+ if (typeof raw.code !== "string" || typeof raw.error !== "string")
3090
+ return null;
3091
+ const { type: _type, ...failure } = raw;
3092
+ return { kind: "error", payload: failure };
3093
+ }
3094
+ if (typeof raw.seq === "number" && typeof raw.type === "string") {
3095
+ return { kind: "change", payload: raw };
3096
+ }
3097
+ return null;
3098
+ }
2572
3099
  // channelOpPath builds the v0.9.x Channel CRUD URL. Two families:
2573
3100
  // - scope === "global" → /v1/_channels/{channel}/{op}
2574
3101
  // - scope === "user" → /v1/users/{userId}/channels/{channel}/{op}