@loomcycle/client 0.33.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,6 +12,8 @@ TypeScript client for the [loomcycle](https://github.com/denn-gubsky/loomcycle)
12
12
 
13
13
  ### What's new since v0.8.18
14
14
 
15
+ - **`interactiveSession` / `sendRunInput` / `streamRunByID` + the `interactive` flag** (v1.1.1, RFC AI) — the interactive agentic session, the adapter port of the Web UI's run terminal. Pass `interactive: true` to `runStreaming` / `continueSession` to start a **persistent** run that parks at end_turn (an `awaiting_input` frame) instead of ending; **`sendRunInput(runId, text)`** steers it (the response arrives on the same stream); **`streamRunByID(runId, {fromSeq})`** re-attaches by run_id (the operator's prior turns replay as `steer` events, `user_input.source === "replay"`, so a cold client — e.g. another device — reconstructs the whole conversation). The high-level **`client.interactiveSession({agent, segments})`** returns an `InteractiveSession` with `events()` / `send()` / `cancel()`; **`attachInteractiveSession(runId)`** resumes one. The `AgentEvent` union gains `awaiting_input` / `steer` / `context_compaction`.
16
+ - **`volumeDef` / `listVolumes` / `listEphemeralVolumes`** (v0.35.0, RFC AH) — the dynamic filesystem-volume surface. `volumeDef` is the op-discriminated substrate tool (`create` / `get` / `list` / `delete` / `purge`); a Volume is **flat** (a pointer to mutable on-disk state, not a versioned def), so `delete` unmaps + leaves files while `purge` removes the row **and** the directory tree — there is no retire/promote/fork. Tenant-confined (`ScopeTenant`): the runtime derives the path inside an operator-blessed `dynamic_root`, so you pass `{name, mode}`, never a host path. `listVolumes()` / `listEphemeralVolumes()` return the tenant's persistent + live run-scoped volumes; host paths are redacted (`""`) for a non-operator caller.
15
17
  - **`ensureMcpServer` / `mcpServerDefVerify`** (v0.18.0) — typed ergonomics for the dynamic-MCP dedup flow. `ensureMcpServer({name, url, headers?, rediscover?})` registers a callback MCP server **idempotently**: it runs `create` (a no-op in loomcycle ≥ v0.18.0 when the active def already carries identical content) plus an optional `rediscover` (a no-op on unchanged tools), and returns `{defId, version, changed, discoveredToolCount?}` — so a consumer re-registering on every startup gets `changed: false` once its registration content is stable. Keep `${run.*}` / `${LOOMCYCLE_*}` header placeholders **literal** (don't bake a per-restart token) or the content varies each boot and dedup can't engage. `mcpServerDefVerify(name, sha)` is the typed `op: verify` wrapper (`matches: true` = no-op signal).
16
18
  - **`operatorTokenDef` / `whoami` + tenant-scoped reads** (v0.17.0, RFC L) — the OSS multi-tenant authorization surface. `operatorTokenDef` is the op-discriminated admin tool over the `OperatorTokenDef` substrate (create / rotate / retire per-principal bearer tokens); `whoami()` returns the authoritative `(tenant, subject, scopes, is_admin)` resolved from the calling bearer; `listUsers({ tenant })` / `listUserAgents(userId, { tenant })` accept a super-admin tenant-focus (ignored server-side for a tenant principal — its own tenant is forced).
17
19
 
@@ -104,8 +106,31 @@ All methods are async / return `Promise<T>` unless noted; streaming methods retu
104
106
 
105
107
  | Method | Returns | Notes |
106
108
  |---|---|---|
107
- | `runStreaming(opts: RunOptions)` | `AsyncIterable<AgentEvent>` | Server-streams provider events for a fresh run. |
109
+ | `runStreaming(opts: RunOptions)` | `AsyncIterable<AgentEvent>` | Server-streams provider events for a fresh run. `interactive: true` parks at end_turn for steering (RFC AI). |
108
110
  | `continueSession(opts: ContinueOptions)` | `AsyncIterable<AgentEvent>` | Continues an existing session. |
111
+ | `sendRunInput(runId, text)` | `{run_id, delivered}` | RFC AI — steer a live interactive run (`POST /v1/runs/{id}/input`). |
112
+ | `streamRunByID(runId, {fromSeq})` | `AsyncIterable<AgentEvent>` | RFC AI — re-attach by run_id (`GET /v1/runs/{id}/stream`); replays operator turns as `steer` events. |
113
+ | `interactiveSession(opts)` / `attachInteractiveSession(runId)` | `InteractiveSession` | RFC AI — high-level driver: `events()` / `send()` / `cancel()`. |
114
+
115
+ ### Interactive sessions (RFC AI)
116
+
117
+ ```ts
118
+ const sess = client.interactiveSession({
119
+ agent: "assistant",
120
+ segments: [{ role: "user", content: [{ type: "trusted-text", text: "help me debug" }] }],
121
+ });
122
+ for await (const ev of sess.events()) {
123
+ if (ev.type === "text") process.stdout.write(ev.text ?? "");
124
+ if (ev.type === "awaiting_input") {
125
+ await sess.send(await prompt("you> ")); // steers; response arrives on this same loop
126
+ }
127
+ if (ev.type === "done") break;
128
+ }
129
+ // later, from anywhere (another process / device): resume the same run
130
+ const resumed = client.attachInteractiveSession(sess.runId);
131
+ ```
132
+
133
+ The low-level primitives (`runStreaming({interactive:true})` + `sendRunInput` + `streamRunByID`) are the escape hatch if you'd rather drive the stream yourself.
109
134
 
110
135
  ### Agent metadata
111
136
 
@@ -274,6 +299,32 @@ Operations on AgentDef: `create` / `fork` / `get` / `list` / `promote` / `retire
274
299
 
275
300
  Refusals throw `SubstrateToolRefusedError` (a scope deny / empty body / allowed-tools widening); transport failures throw the usual typed errors (`AuthError`, `UnavailableError`, etc.).
276
301
 
302
+ ### Dynamic filesystem volumes (v0.35.0 — RFC AH)
303
+
304
+ Per-tenant, ro/rw filesystem roots an agent can be bound to. `volumeDef` provisions and manages them at runtime; the two list methods render the volume universe. Tenant-confined (`ScopeTenant`).
305
+
306
+ | Method | Returns | Notes |
307
+ |---|---|---|
308
+ | `volumeDef(input)` | `Promise<SubstrateToolResponse>` | Op-discriminated (`create` / `get` / `list` / `delete` / `purge`). Mirrors `POST /v1/_volumedef`. |
309
+ | `listVolumes()` | `Promise<PersistentVolumesResponse>` | Static (read-only floor) + the tenant's dynamic volumes. `GET /v1/_volumes`. |
310
+ | `listEphemeralVolumes()` | `Promise<EphemeralVolumesResponse>` | Live, run-scoped volumes (auto-purged at run completion). `GET /v1/_volumes/ephemeral`. |
311
+
312
+ A Volume is **flat** — a pointer to mutable on-disk state, not a versioned definition — so the op set is `create` / `get` / `list` / `delete` / `purge` (no retire/promote/fork). The runtime DERIVES the path inside an operator-blessed `dynamic_root` (`<root>/<tenant>/<name>`), so you pass `{name, mode}` and never a host path:
313
+
314
+ ```ts
315
+ // Provision a writable per-tenant volume (the runtime mkdir's it).
316
+ await client.volumeDef({ op: "create", name: "repo-a", mode: "rw" });
317
+
318
+ // Unmap (keeps files) vs. destroy (RemoveAll's the tree).
319
+ await client.volumeDef({ op: "delete", name: "repo-a" }); // non-destructive
320
+ await client.volumeDef({ op: "purge", name: "repo-a" }); // destructive
321
+
322
+ const { entries } = await client.listVolumes();
323
+ // entries[].path is "" (redacted) unless the caller is operator-equivalent.
324
+ ```
325
+
326
+ Refusals throw `SubstrateToolRefusedError` (collision with a static volume name, no `dynamic_root` configured, cross-tenant); transport failures throw the usual typed errors.
327
+
277
328
  ### Channels + run-state stream (v0.9.x n8n Phase 0)
278
329
 
279
330
  Two substrate-side surfaces added in the n8n integration's Phase 0 wire-API work. Useful for any orchestrator (not just n8n) that needs to see channel state or subscribe to run-state transitions.
@@ -28,6 +28,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
28
28
  exports.LoomcycleClient = void 0;
29
29
  const fetch_helpers_js_1 = require("./fetch-helpers.js");
30
30
  const stream_js_1 = require("./stream.js");
31
+ const interactive_js_1 = require("./interactive.js");
31
32
  /** samplingToWire maps the camelCase SamplingOptions to the snake_case wire
32
33
  * object, omitting unset fields so they inherit the agent's value. */
33
34
  function samplingToWire(s) {
@@ -107,6 +108,8 @@ function runBody(opts) {
107
108
  body.sampling = samplingToWire(opts.sampling);
108
109
  if (opts.compaction !== undefined)
109
110
  body.compaction = compactionToWire(opts.compaction);
111
+ if (opts.interactive !== undefined)
112
+ body.interactive = opts.interactive;
110
113
  return body;
111
114
  }
112
115
  class LoomcycleClient {
@@ -189,8 +192,65 @@ class LoomcycleClient {
189
192
  body.sampling = samplingToWire(opts.sampling);
190
193
  if (opts.compaction !== undefined)
191
194
  body.compaction = compactionToWire(opts.compaction);
195
+ if (opts.interactive !== undefined)
196
+ body.interactive = opts.interactive;
192
197
  yield* this.streamSSE(`/v1/sessions/${encodeURIComponent(opts.sessionId)}/messages`, body, opts.signal, opts.debug);
193
198
  }
199
+ /** Push an operator steering message into a LIVE interactive run (RFC AI).
200
+ * Mirrors `POST /v1/runs/{run_id}/input`. The run must be in-flight —
201
+ * parked at end_turn awaiting input, or mid-turn (the message is drained
202
+ * at the next iteration boundary). Does NOT open a stream; the operator's
203
+ * turn + the model's response arrive on the run's OWN event stream
204
+ * (the open `runStreaming` iterator, or a `streamRunByID` re-attach).
205
+ * Returns `{ run_id, delivered }`.
206
+ *
207
+ * Raises {@link UnavailableError} (503, steering off / no run),
208
+ * {@link AuthError} (401). A full steer queue surfaces as a 429. */
209
+ async sendRunInput(runId, text, opts) {
210
+ return (0, fetch_helpers_js_1.postJSON)(this.ctx, `/v1/runs/${encodeURIComponent(runId)}/input`, { text }, opts);
211
+ }
212
+ /** Re-attach to a run's event stream by `run_id` (RFC AI), replaying from
213
+ * `fromSeq` (default 0) then live-tailing — the gRPC-free twin of the Web
214
+ * UI's resume-in-terminal. Mirrors `GET /v1/runs/{run_id}/stream`. The
215
+ * operator's own prior turns are replayed as `steer` events
216
+ * (`user_input.source === "replay"`), so a cold client — e.g. resuming on
217
+ * another device — reconstructs the whole conversation. A PARKED
218
+ * interactive run keeps streaming until it ends or `signal` aborts; a
219
+ * disconnect does NOT stop the run.
220
+ *
221
+ * The first frame is an `agent` side-channel carrying the run's
222
+ * agent_id / session_id (for `sendRunInput` / `cancelAgent`). */
223
+ async *streamRunByID(runId, opts) {
224
+ const q = opts?.fromSeq ? `?from_seq=${opts.fromSeq}` : "";
225
+ yield* this.streamSSEGet(`/v1/runs/${encodeURIComponent(runId)}/stream${q}`, opts?.signal);
226
+ }
227
+ /** Start a high-level INTERACTIVE session (RFC AI) — the ergonomic driver
228
+ * for a persistent run you converse with turn by turn. Starts an
229
+ * `interactive` run and returns an {@link InteractiveSession} whose
230
+ * `events()` you iterate and whose `send()` / `cancel()` steer it. See
231
+ * {@link InteractiveSession}. For the raw primitives, use
232
+ * {@link LoomcycleClient.runStreaming} with `interactive: true` +
233
+ * {@link LoomcycleClient.sendRunInput}. */
234
+ interactiveSession(opts) {
235
+ const source = this.runStreaming({ ...opts, interactive: true });
236
+ return new interactive_js_1.InteractiveSession(source, {
237
+ sendRunInput: (rid, t) => this.sendRunInput(rid, t),
238
+ cancelAgent: (aid) => this.cancelAgent(aid),
239
+ });
240
+ }
241
+ /** Re-attach to a detached interactive run by `run_id` as a high-level
242
+ * {@link InteractiveSession} — e.g. resuming on another device. The run_id
243
+ * is known up front, so `send()` works immediately. Replays the
244
+ * conversation (incl. the operator's prior turns, RFC AI) then live-tails. */
245
+ attachInteractiveSession(runId, opts) {
246
+ const source = this.streamRunByID(runId, opts);
247
+ const session = new interactive_js_1.InteractiveSession(source, {
248
+ sendRunInput: (rid, t) => this.sendRunInput(rid, t),
249
+ cancelAgent: (aid) => this.cancelAgent(aid),
250
+ });
251
+ session.runId = runId;
252
+ return session;
253
+ }
194
254
  /**
195
255
  * Spawn N fresh runs concurrently in ONE call (RFC Y external fan-out) and
196
256
  * resolve once they ALL settle, returning the combined index-aligned
@@ -744,6 +804,48 @@ class LoomcycleClient {
744
804
  async operatorTokenDef(input, opts) {
745
805
  return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_operatortokendef", input, opts);
746
806
  }
807
+ // ---- RFC AH (v0.35.0) dynamic filesystem volumes ----
808
+ /** Invoke the RFC AH VolumeDef substrate tool over HTTP. Provision +
809
+ * manage CONFINED, per-tenant filesystem volumes at runtime.
810
+ * Tenant-confined (ScopeTenant, not operator-admin-only): the runtime
811
+ * stamps the caller's authoritative tenant and opaque-404s cross-tenant.
812
+ *
813
+ * Op-discriminated input. A VolumeDef is FLAT (a pointer to mutable
814
+ * on-disk state, not a versioned definition), so the op set is
815
+ * `{op: "create" | "get" | "list" | "delete" | "purge", ...}` — NOT the
816
+ * content-addressed retire/promote/fork of the other Def families:
817
+ * - `create` — `{op, name, mode}`. The runtime DERIVES the path inside the
818
+ * operator-blessed `dynamic_root` (`<root>/<tenant>/<name>`) and
819
+ * `MkdirAll`s it; you never supply a host path.
820
+ * - `delete` — non-destructive: drops the mapping, leaves files on disk.
821
+ * - `purge` — destructive: drops the mapping AND `RemoveAll`s the tree.
822
+ *
823
+ * Raises {@link SubstrateToolRefusedError} on tool-level refusals
824
+ * (collision with a static volume name, no `dynamic_root` configured,
825
+ * cross-tenant); {@link InvalidArgumentError} on 400; {@link AuthError}
826
+ * on 401. */
827
+ async volumeDef(input, opts) {
828
+ return (0, fetch_helpers_js_1.postJSON)(this.ctx, "/v1/_volumedef", input, opts);
829
+ }
830
+ /** List the PERSISTENT volume universe for the caller's tenant — every
831
+ * static volume (the shared bind floor, `source: "static"`, read-only)
832
+ * plus the tenant's own dynamic VolumeDefs (`source: "dynamic"`). Host
833
+ * paths are redacted (`""`) for a non-operator (tenant) caller.
834
+ *
835
+ * Wraps `GET /v1/_volumes` (RFC AH Phase 4). Bearer-authed; tenant-scoped
836
+ * from the authoritative principal. */
837
+ async listVolumes(opts) {
838
+ return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, "/v1/_volumes", opts);
839
+ }
840
+ /** List the LIVE ephemeral (run-scoped) volumes for the caller's tenant —
841
+ * created mid-run, inherited by sub-agents, auto-purged when the run
842
+ * completes. Host paths are redacted (`""`) for a non-operator caller.
843
+ *
844
+ * Wraps `GET /v1/_volumes/ephemeral` (RFC AH Phase 4). Bearer-authed;
845
+ * tenant-scoped from the authoritative principal. */
846
+ async listEphemeralVolumes(opts) {
847
+ return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, "/v1/_volumes/ephemeral", opts);
848
+ }
747
849
  // ---- v0.10.3 Library v2 enumeration (read-only, merged yaml+substrate) ----
748
850
  /** List every agent the runtime knows about — yaml-static + dynamic
749
851
  * AgentDefs merged into one envelope per name. Each entry carries
@@ -875,6 +977,29 @@ class LoomcycleClient {
875
977
  * events AND a `{ type: "_meta", meta_subtype: "stream_close",
876
978
  * meta_reason }` on EOF / abort / error. The default is silent
877
979
  * (matches pre-v0.9.x behaviour). */
980
+ /** GET-based SSE stream (RFC AI re-attach). The interactive re-attach
981
+ * endpoint is a GET with a query string, not a POST body, so it can't reuse
982
+ * streamSSE; the parse + error handling are identical otherwise. No debug
983
+ * shape — re-attach is a plain tail. */
984
+ async *streamSSEGet(path, signal) {
985
+ const headers = {
986
+ Accept: "text/event-stream, application/json",
987
+ };
988
+ if (this.ctx.authToken)
989
+ headers.Authorization = `Bearer ${this.ctx.authToken}`;
990
+ const resp = await this.ctx.fetchImpl(this.ctx.baseUrl + path, {
991
+ method: "GET",
992
+ headers,
993
+ signal,
994
+ });
995
+ if (!resp.ok) {
996
+ await (0, fetch_helpers_js_1.raiseFromResponse)(resp);
997
+ }
998
+ if (!resp.body) {
999
+ throw new Error("loomcycle: response has no body");
1000
+ }
1001
+ yield* (0, stream_js_1.parseSSE)(resp.body.getReader());
1002
+ }
878
1003
  async *streamSSE(path, body, signal, debug) {
879
1004
  const headers = {
880
1005
  "Content-Type": "application/json",
package/dist/cjs/index.js CHANGED
@@ -53,6 +53,11 @@
53
53
  * ensureMcpServer(opts): Promise<EnsureMcpServerResult> // v0.18.0 — idempotent register-if-changed
54
54
  * scheduleDef(input): Promise<SubstrateToolResponse>
55
55
  *
56
+ * // Dynamic filesystem volumes (v0.35.0 — RFC AH; tenant-confined)
57
+ * volumeDef(input): Promise<SubstrateToolResponse> // create/get/list/delete/purge
58
+ * listVolumes(): Promise<PersistentVolumesResponse>
59
+ * listEphemeralVolumes(): Promise<EphemeralVolumesResponse>
60
+ *
56
61
  * // Library v2 enumeration (v0.10.3 — yaml+substrate merged)
57
62
  * listLibraryAgents(): Promise<LibraryListResponse<LibraryAgentDefinition>>
58
63
  * listLibrarySkills(): Promise<LibraryListResponse<LibrarySkillDefinition>>
@@ -83,9 +88,11 @@
83
88
  * See `adapters/ts/README.md` for usage examples.
84
89
  */
85
90
  Object.defineProperty(exports, "__esModule", { value: true });
86
- exports.UnavailableError = exports.SubstrateToolRefusedError = exports.SnapshotVersionError = exports.SnapshotTooLargeError = exports.SnapshotNotFoundError = exports.SessionNotFoundError = exports.SessionBusyError = exports.PerUserQuotaExhaustedError = exports.PauseNotConfiguredError = exports.NotPausedError = exports.LoomcycleError = exports.ChannelCursorRegressionError = exports.InvalidArgumentError = exports.NotFoundError = exports.HookNotFoundError = exports.BackpressureError = exports.AuthError = exports.AlreadyPausingError = exports.AgentNotFoundError = exports.AgentIDInUseError = exports.LoomcycleClient = void 0;
91
+ exports.UnavailableError = exports.SubstrateToolRefusedError = exports.SnapshotVersionError = exports.SnapshotTooLargeError = exports.SnapshotNotFoundError = exports.SessionNotFoundError = exports.SessionBusyError = exports.PerUserQuotaExhaustedError = exports.PauseNotConfiguredError = exports.NotPausedError = exports.LoomcycleError = exports.ChannelCursorRegressionError = exports.InvalidArgumentError = exports.NotFoundError = exports.HookNotFoundError = exports.BackpressureError = exports.AuthError = exports.AlreadyPausingError = exports.AgentNotFoundError = exports.AgentIDInUseError = exports.InteractiveSession = exports.LoomcycleClient = void 0;
87
92
  var client_js_1 = require("./client.js");
88
93
  Object.defineProperty(exports, "LoomcycleClient", { enumerable: true, get: function () { return client_js_1.LoomcycleClient; } });
94
+ var interactive_js_1 = require("./interactive.js");
95
+ Object.defineProperty(exports, "InteractiveSession", { enumerable: true, get: function () { return interactive_js_1.InteractiveSession; } });
89
96
  var errors_js_1 = require("./errors.js");
90
97
  Object.defineProperty(exports, "AgentIDInUseError", { enumerable: true, get: function () { return errors_js_1.AgentIDInUseError; } });
91
98
  Object.defineProperty(exports, "AgentNotFoundError", { enumerable: true, get: function () { return errors_js_1.AgentNotFoundError; } });
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InteractiveSession = void 0;
4
+ /** A high-level driver for an interactive agentic session (RFC AI) — the
5
+ * adapter port of the Web UI's run terminal. It wraps the underlying event
6
+ * stream (a fresh `interactive` run, or a `streamRunByID` re-attach), taps
7
+ * each frame to track the run's tracking IDs + parked state, and routes
8
+ * operator input + cancel through the client.
9
+ *
10
+ * Typical loop:
11
+ * ```ts
12
+ * const sess = client.interactiveSession({ agent: "chat", segments: [...] });
13
+ * for await (const ev of sess.events()) {
14
+ * if (ev.type === "text") process.stdout.write(ev.text ?? "");
15
+ * if (ev.type === "awaiting_input") await sess.send(await prompt("you> "));
16
+ * }
17
+ * ```
18
+ * `send()` does NOT open a new stream — the operator's turn and the model's
19
+ * response arrive on the same `events()` iterator. */
20
+ class InteractiveSession {
21
+ source;
22
+ ops;
23
+ /** The run's id, from the first `agent` frame (set up-front on re-attach). */
24
+ runId = "";
25
+ /** The run's agent_id (for cancel), from the first `agent` frame. */
26
+ agentId = "";
27
+ /** The run's session_id, from the `session` / `agent` frames. */
28
+ sessionId = "";
29
+ /** True after an `awaiting_input` frame; cleared on the next activity. */
30
+ awaitingInput = false;
31
+ constructor(source, ops) {
32
+ this.source = source;
33
+ this.ops = ops;
34
+ }
35
+ /** The merged event stream. Consume with `for await`. Each frame updates the
36
+ * session's runId / agentId / sessionId / awaitingInput BEFORE it is
37
+ * yielded, so a consumer that reacts to `awaiting_input` can immediately
38
+ * `send()`. */
39
+ async *events() {
40
+ for await (const ev of this.source) {
41
+ if (ev.type === "agent") {
42
+ if (ev.run_id)
43
+ this.runId = ev.run_id;
44
+ if (ev.agent_id)
45
+ this.agentId = ev.agent_id;
46
+ if (ev.session_id)
47
+ this.sessionId = ev.session_id;
48
+ }
49
+ else if (ev.type === "session" && ev.session_id) {
50
+ this.sessionId = ev.session_id;
51
+ }
52
+ this.awaitingInput = ev.type === "awaiting_input";
53
+ yield ev;
54
+ }
55
+ }
56
+ /** Steer the live run with an operator message. The response arrives on the
57
+ * `events()` stream (NOT a new stream). Returns the server's `delivered`
58
+ * flag. Throws if the run_id isn't known yet — for a fresh session, consume
59
+ * `events()` until the `agent` frame (or the first `awaiting_input`) first;
60
+ * a re-attached session has the run_id up front. */
61
+ async send(text) {
62
+ if (!this.runId) {
63
+ throw new Error("loomcycle: run_id not known yet — consume events() until the `agent` frame before send()");
64
+ }
65
+ const { delivered } = await this.ops.sendRunInput(this.runId, text);
66
+ this.awaitingInput = false;
67
+ return delivered;
68
+ }
69
+ /** Cancel the run (and its sub-agents). No-op if the agent_id isn't known
70
+ * yet (nothing to cancel). */
71
+ async cancel() {
72
+ if (this.agentId)
73
+ await this.ops.cancelAgent(this.agentId);
74
+ }
75
+ }
76
+ exports.InteractiveSession = InteractiveSession;
package/dist/client.d.ts CHANGED
@@ -23,7 +23,8 @@
23
23
  * via fetch-helpers.ts:raiseFromResponse — see README.md for the
24
24
  * full mapping table.
25
25
  */
26
- import type { Agent, AgentEvent, AgentStatus, CancelAgentResult, ClientOptions, ContinueOptions, CreateSnapshotOptions, EnsureCodeAgentOptions, EnsureCodeAgentResult, EnsureMcpServerOptions, EnsureMcpServerResult, HealthResponse, Hook, InterruptListResponse, InterruptStatus, AckChannelOptions, AwaitChannelsOptions, BroadcastChannelsOptions, ChannelAckResult, ChannelAwaitResult, ChannelBroadcastResult, ChannelDescriptor, ChannelPeekResult, ChannelPublishResult, ChannelPurgeResult, ChannelSubscribeResult, CreateChannelOptions, ListChannelsResponse, PeekChannelOptions, PublishChannelOptions, SetMemoryEntryOptions, SetMemoryEntryResponse, SubscribeChannelOptions, UpdateChannelOptions, LibraryAgentDefinition, LibraryListResponse, LibraryMcpServerDefinition, LibrarySkillDefinition, ListUsersResponse, LLMChatOptions, LLMChatResponse, LLMChatStreamItem, LLMEmbeddingsOptions, LLMEmbeddingsResponse, MCPServerDefVerifyResult, MemoryEntriesResponse, MemoryEntryResponse, MemoryScopeIDsResponse, MemoryScopesResponse, PauseResult, RegisterHookOptions, RegisterHookResponse, ResolveInterruptOptions, ResumeResult, ResolverMatrix, CompactRunResult, RunBatchOptions, RunBatchResult, RunOptions, RunStateStreamItem, RuntimeStateResponse, SnapshotCreateResponse, SnapshotDescriptor, SnapshotEnvelope, SnapshotRestoreResponse, StreamUserRunStatesOptions, SubstrateToolInput, SubstrateToolResponse, TranscriptResponse, WhoamiResponse } from "./types.js";
26
+ import { InteractiveSession } from "./interactive.js";
27
+ import type { Agent, AgentEvent, AgentStatus, CancelAgentResult, ClientOptions, ContinueOptions, CreateSnapshotOptions, EnsureCodeAgentOptions, EnsureCodeAgentResult, EnsureMcpServerOptions, EnsureMcpServerResult, HealthResponse, Hook, InterruptListResponse, InterruptStatus, AckChannelOptions, AwaitChannelsOptions, BroadcastChannelsOptions, ChannelAckResult, ChannelAwaitResult, ChannelBroadcastResult, ChannelDescriptor, ChannelPeekResult, ChannelPublishResult, ChannelPurgeResult, ChannelSubscribeResult, CreateChannelOptions, ListChannelsResponse, PeekChannelOptions, PublishChannelOptions, SetMemoryEntryOptions, SetMemoryEntryResponse, SubscribeChannelOptions, UpdateChannelOptions, LibraryAgentDefinition, LibraryListResponse, LibraryMcpServerDefinition, LibrarySkillDefinition, ListUsersResponse, LLMChatOptions, LLMChatResponse, LLMChatStreamItem, LLMEmbeddingsOptions, LLMEmbeddingsResponse, MCPServerDefVerifyResult, MemoryEntriesResponse, MemoryEntryResponse, MemoryScopeIDsResponse, MemoryScopesResponse, PauseResult, PersistentVolumesResponse, EphemeralVolumesResponse, RegisterHookOptions, RegisterHookResponse, ResolveInterruptOptions, ResumeResult, ResolverMatrix, CompactRunResult, RunBatchOptions, RunBatchResult, RunOptions, RunStateStreamItem, RuntimeStateResponse, SnapshotCreateResponse, SnapshotDescriptor, SnapshotEnvelope, SnapshotRestoreResponse, StreamUserRunStatesOptions, SubstrateToolInput, SubstrateToolResponse, TranscriptResponse, WhoamiResponse } from "./types.js";
27
28
  export declare class LoomcycleClient {
28
29
  private ctx;
29
30
  constructor(opts?: ClientOptions);
@@ -67,6 +68,53 @@ export declare class LoomcycleClient {
67
68
  * `_meta` open/close events.
68
69
  */
69
70
  continueSession(opts: ContinueOptions): AsyncIterable<AgentEvent>;
71
+ /** Push an operator steering message into a LIVE interactive run (RFC AI).
72
+ * Mirrors `POST /v1/runs/{run_id}/input`. The run must be in-flight —
73
+ * parked at end_turn awaiting input, or mid-turn (the message is drained
74
+ * at the next iteration boundary). Does NOT open a stream; the operator's
75
+ * turn + the model's response arrive on the run's OWN event stream
76
+ * (the open `runStreaming` iterator, or a `streamRunByID` re-attach).
77
+ * Returns `{ run_id, delivered }`.
78
+ *
79
+ * Raises {@link UnavailableError} (503, steering off / no run),
80
+ * {@link AuthError} (401). A full steer queue surfaces as a 429. */
81
+ sendRunInput(runId: string, text: string, opts?: {
82
+ signal?: AbortSignal;
83
+ }): Promise<{
84
+ run_id: string;
85
+ delivered: boolean;
86
+ }>;
87
+ /** Re-attach to a run's event stream by `run_id` (RFC AI), replaying from
88
+ * `fromSeq` (default 0) then live-tailing — the gRPC-free twin of the Web
89
+ * UI's resume-in-terminal. Mirrors `GET /v1/runs/{run_id}/stream`. The
90
+ * operator's own prior turns are replayed as `steer` events
91
+ * (`user_input.source === "replay"`), so a cold client — e.g. resuming on
92
+ * another device — reconstructs the whole conversation. A PARKED
93
+ * interactive run keeps streaming until it ends or `signal` aborts; a
94
+ * disconnect does NOT stop the run.
95
+ *
96
+ * The first frame is an `agent` side-channel carrying the run's
97
+ * agent_id / session_id (for `sendRunInput` / `cancelAgent`). */
98
+ streamRunByID(runId: string, opts?: {
99
+ fromSeq?: number;
100
+ signal?: AbortSignal;
101
+ }): AsyncIterable<AgentEvent>;
102
+ /** Start a high-level INTERACTIVE session (RFC AI) — the ergonomic driver
103
+ * for a persistent run you converse with turn by turn. Starts an
104
+ * `interactive` run and returns an {@link InteractiveSession} whose
105
+ * `events()` you iterate and whose `send()` / `cancel()` steer it. See
106
+ * {@link InteractiveSession}. For the raw primitives, use
107
+ * {@link LoomcycleClient.runStreaming} with `interactive: true` +
108
+ * {@link LoomcycleClient.sendRunInput}. */
109
+ interactiveSession(opts: Omit<RunOptions, "interactive">): InteractiveSession;
110
+ /** Re-attach to a detached interactive run by `run_id` as a high-level
111
+ * {@link InteractiveSession} — e.g. resuming on another device. The run_id
112
+ * is known up front, so `send()` works immediately. Replays the
113
+ * conversation (incl. the operator's prior turns, RFC AI) then live-tails. */
114
+ attachInteractiveSession(runId: string, opts?: {
115
+ fromSeq?: number;
116
+ signal?: AbortSignal;
117
+ }): InteractiveSession;
70
118
  /**
71
119
  * Spawn N fresh runs concurrently in ONE call (RFC Y external fan-out) and
72
120
  * resolve once they ALL settle, returning the combined index-aligned
@@ -486,6 +534,47 @@ export declare class LoomcycleClient {
486
534
  operatorTokenDef(input: SubstrateToolInput, opts?: {
487
535
  signal?: AbortSignal;
488
536
  }): Promise<SubstrateToolResponse>;
537
+ /** Invoke the RFC AH VolumeDef substrate tool over HTTP. Provision +
538
+ * manage CONFINED, per-tenant filesystem volumes at runtime.
539
+ * Tenant-confined (ScopeTenant, not operator-admin-only): the runtime
540
+ * stamps the caller's authoritative tenant and opaque-404s cross-tenant.
541
+ *
542
+ * Op-discriminated input. A VolumeDef is FLAT (a pointer to mutable
543
+ * on-disk state, not a versioned definition), so the op set is
544
+ * `{op: "create" | "get" | "list" | "delete" | "purge", ...}` — NOT the
545
+ * content-addressed retire/promote/fork of the other Def families:
546
+ * - `create` — `{op, name, mode}`. The runtime DERIVES the path inside the
547
+ * operator-blessed `dynamic_root` (`<root>/<tenant>/<name>`) and
548
+ * `MkdirAll`s it; you never supply a host path.
549
+ * - `delete` — non-destructive: drops the mapping, leaves files on disk.
550
+ * - `purge` — destructive: drops the mapping AND `RemoveAll`s the tree.
551
+ *
552
+ * Raises {@link SubstrateToolRefusedError} on tool-level refusals
553
+ * (collision with a static volume name, no `dynamic_root` configured,
554
+ * cross-tenant); {@link InvalidArgumentError} on 400; {@link AuthError}
555
+ * on 401. */
556
+ volumeDef(input: SubstrateToolInput, opts?: {
557
+ signal?: AbortSignal;
558
+ }): Promise<SubstrateToolResponse>;
559
+ /** List the PERSISTENT volume universe for the caller's tenant — every
560
+ * static volume (the shared bind floor, `source: "static"`, read-only)
561
+ * plus the tenant's own dynamic VolumeDefs (`source: "dynamic"`). Host
562
+ * paths are redacted (`""`) for a non-operator (tenant) caller.
563
+ *
564
+ * Wraps `GET /v1/_volumes` (RFC AH Phase 4). Bearer-authed; tenant-scoped
565
+ * from the authoritative principal. */
566
+ listVolumes(opts?: {
567
+ signal?: AbortSignal;
568
+ }): Promise<PersistentVolumesResponse>;
569
+ /** List the LIVE ephemeral (run-scoped) volumes for the caller's tenant —
570
+ * created mid-run, inherited by sub-agents, auto-purged when the run
571
+ * completes. Host paths are redacted (`""`) for a non-operator caller.
572
+ *
573
+ * Wraps `GET /v1/_volumes/ephemeral` (RFC AH Phase 4). Bearer-authed;
574
+ * tenant-scoped from the authoritative principal. */
575
+ listEphemeralVolumes(opts?: {
576
+ signal?: AbortSignal;
577
+ }): Promise<EphemeralVolumesResponse>;
489
578
  /** List every agent the runtime knows about — yaml-static + dynamic
490
579
  * AgentDefs merged into one envelope per name. Each entry carries
491
580
  * `source: "static-only" | "dynamic-only" | "both"` so callers can
@@ -583,6 +672,11 @@ export declare class LoomcycleClient {
583
672
  * events AND a `{ type: "_meta", meta_subtype: "stream_close",
584
673
  * meta_reason }` on EOF / abort / error. The default is silent
585
674
  * (matches pre-v0.9.x behaviour). */
675
+ /** GET-based SSE stream (RFC AI re-attach). The interactive re-attach
676
+ * endpoint is a GET with a query string, not a POST body, so it can't reuse
677
+ * streamSSE; the parse + error handling are identical otherwise. No debug
678
+ * shape — re-attach is a plain tail. */
679
+ private streamSSEGet;
586
680
  private streamSSE;
587
681
  /** List every operator-declared channel with aggregate stats
588
682
  * (message_count, oldest_visible_at, newest_visible_at).
package/dist/client.js CHANGED
@@ -25,6 +25,7 @@
25
25
  */
26
26
  import { authHeaders, deleteRequest, jsonFetch, patchJSON, postJSON, putJSON, raiseFromResponse, } from "./fetch-helpers.js";
27
27
  import { parseSSE } from "./stream.js";
28
+ import { InteractiveSession } from "./interactive.js";
28
29
  /** samplingToWire maps the camelCase SamplingOptions to the snake_case wire
29
30
  * object, omitting unset fields so they inherit the agent's value. */
30
31
  function samplingToWire(s) {
@@ -104,6 +105,8 @@ function runBody(opts) {
104
105
  body.sampling = samplingToWire(opts.sampling);
105
106
  if (opts.compaction !== undefined)
106
107
  body.compaction = compactionToWire(opts.compaction);
108
+ if (opts.interactive !== undefined)
109
+ body.interactive = opts.interactive;
107
110
  return body;
108
111
  }
109
112
  export class LoomcycleClient {
@@ -186,8 +189,65 @@ export class LoomcycleClient {
186
189
  body.sampling = samplingToWire(opts.sampling);
187
190
  if (opts.compaction !== undefined)
188
191
  body.compaction = compactionToWire(opts.compaction);
192
+ if (opts.interactive !== undefined)
193
+ body.interactive = opts.interactive;
189
194
  yield* this.streamSSE(`/v1/sessions/${encodeURIComponent(opts.sessionId)}/messages`, body, opts.signal, opts.debug);
190
195
  }
196
+ /** Push an operator steering message into a LIVE interactive run (RFC AI).
197
+ * Mirrors `POST /v1/runs/{run_id}/input`. The run must be in-flight —
198
+ * parked at end_turn awaiting input, or mid-turn (the message is drained
199
+ * at the next iteration boundary). Does NOT open a stream; the operator's
200
+ * turn + the model's response arrive on the run's OWN event stream
201
+ * (the open `runStreaming` iterator, or a `streamRunByID` re-attach).
202
+ * Returns `{ run_id, delivered }`.
203
+ *
204
+ * Raises {@link UnavailableError} (503, steering off / no run),
205
+ * {@link AuthError} (401). A full steer queue surfaces as a 429. */
206
+ async sendRunInput(runId, text, opts) {
207
+ return postJSON(this.ctx, `/v1/runs/${encodeURIComponent(runId)}/input`, { text }, opts);
208
+ }
209
+ /** Re-attach to a run's event stream by `run_id` (RFC AI), replaying from
210
+ * `fromSeq` (default 0) then live-tailing — the gRPC-free twin of the Web
211
+ * UI's resume-in-terminal. Mirrors `GET /v1/runs/{run_id}/stream`. The
212
+ * operator's own prior turns are replayed as `steer` events
213
+ * (`user_input.source === "replay"`), so a cold client — e.g. resuming on
214
+ * another device — reconstructs the whole conversation. A PARKED
215
+ * interactive run keeps streaming until it ends or `signal` aborts; a
216
+ * disconnect does NOT stop the run.
217
+ *
218
+ * The first frame is an `agent` side-channel carrying the run's
219
+ * agent_id / session_id (for `sendRunInput` / `cancelAgent`). */
220
+ async *streamRunByID(runId, opts) {
221
+ const q = opts?.fromSeq ? `?from_seq=${opts.fromSeq}` : "";
222
+ yield* this.streamSSEGet(`/v1/runs/${encodeURIComponent(runId)}/stream${q}`, opts?.signal);
223
+ }
224
+ /** Start a high-level INTERACTIVE session (RFC AI) — the ergonomic driver
225
+ * for a persistent run you converse with turn by turn. Starts an
226
+ * `interactive` run and returns an {@link InteractiveSession} whose
227
+ * `events()` you iterate and whose `send()` / `cancel()` steer it. See
228
+ * {@link InteractiveSession}. For the raw primitives, use
229
+ * {@link LoomcycleClient.runStreaming} with `interactive: true` +
230
+ * {@link LoomcycleClient.sendRunInput}. */
231
+ interactiveSession(opts) {
232
+ const source = this.runStreaming({ ...opts, interactive: true });
233
+ return new InteractiveSession(source, {
234
+ sendRunInput: (rid, t) => this.sendRunInput(rid, t),
235
+ cancelAgent: (aid) => this.cancelAgent(aid),
236
+ });
237
+ }
238
+ /** Re-attach to a detached interactive run by `run_id` as a high-level
239
+ * {@link InteractiveSession} — e.g. resuming on another device. The run_id
240
+ * is known up front, so `send()` works immediately. Replays the
241
+ * conversation (incl. the operator's prior turns, RFC AI) then live-tails. */
242
+ attachInteractiveSession(runId, opts) {
243
+ const source = this.streamRunByID(runId, opts);
244
+ const session = new InteractiveSession(source, {
245
+ sendRunInput: (rid, t) => this.sendRunInput(rid, t),
246
+ cancelAgent: (aid) => this.cancelAgent(aid),
247
+ });
248
+ session.runId = runId;
249
+ return session;
250
+ }
191
251
  /**
192
252
  * Spawn N fresh runs concurrently in ONE call (RFC Y external fan-out) and
193
253
  * resolve once they ALL settle, returning the combined index-aligned
@@ -741,6 +801,48 @@ export class LoomcycleClient {
741
801
  async operatorTokenDef(input, opts) {
742
802
  return postJSON(this.ctx, "/v1/_operatortokendef", input, opts);
743
803
  }
804
+ // ---- RFC AH (v0.35.0) dynamic filesystem volumes ----
805
+ /** Invoke the RFC AH VolumeDef substrate tool over HTTP. Provision +
806
+ * manage CONFINED, per-tenant filesystem volumes at runtime.
807
+ * Tenant-confined (ScopeTenant, not operator-admin-only): the runtime
808
+ * stamps the caller's authoritative tenant and opaque-404s cross-tenant.
809
+ *
810
+ * Op-discriminated input. A VolumeDef is FLAT (a pointer to mutable
811
+ * on-disk state, not a versioned definition), so the op set is
812
+ * `{op: "create" | "get" | "list" | "delete" | "purge", ...}` — NOT the
813
+ * content-addressed retire/promote/fork of the other Def families:
814
+ * - `create` — `{op, name, mode}`. The runtime DERIVES the path inside the
815
+ * operator-blessed `dynamic_root` (`<root>/<tenant>/<name>`) and
816
+ * `MkdirAll`s it; you never supply a host path.
817
+ * - `delete` — non-destructive: drops the mapping, leaves files on disk.
818
+ * - `purge` — destructive: drops the mapping AND `RemoveAll`s the tree.
819
+ *
820
+ * Raises {@link SubstrateToolRefusedError} on tool-level refusals
821
+ * (collision with a static volume name, no `dynamic_root` configured,
822
+ * cross-tenant); {@link InvalidArgumentError} on 400; {@link AuthError}
823
+ * on 401. */
824
+ async volumeDef(input, opts) {
825
+ return postJSON(this.ctx, "/v1/_volumedef", input, opts);
826
+ }
827
+ /** List the PERSISTENT volume universe for the caller's tenant — every
828
+ * static volume (the shared bind floor, `source: "static"`, read-only)
829
+ * plus the tenant's own dynamic VolumeDefs (`source: "dynamic"`). Host
830
+ * paths are redacted (`""`) for a non-operator (tenant) caller.
831
+ *
832
+ * Wraps `GET /v1/_volumes` (RFC AH Phase 4). Bearer-authed; tenant-scoped
833
+ * from the authoritative principal. */
834
+ async listVolumes(opts) {
835
+ return jsonFetch(this.ctx, "/v1/_volumes", opts);
836
+ }
837
+ /** List the LIVE ephemeral (run-scoped) volumes for the caller's tenant —
838
+ * created mid-run, inherited by sub-agents, auto-purged when the run
839
+ * completes. Host paths are redacted (`""`) for a non-operator caller.
840
+ *
841
+ * Wraps `GET /v1/_volumes/ephemeral` (RFC AH Phase 4). Bearer-authed;
842
+ * tenant-scoped from the authoritative principal. */
843
+ async listEphemeralVolumes(opts) {
844
+ return jsonFetch(this.ctx, "/v1/_volumes/ephemeral", opts);
845
+ }
744
846
  // ---- v0.10.3 Library v2 enumeration (read-only, merged yaml+substrate) ----
745
847
  /** List every agent the runtime knows about — yaml-static + dynamic
746
848
  * AgentDefs merged into one envelope per name. Each entry carries
@@ -872,6 +974,29 @@ export class LoomcycleClient {
872
974
  * events AND a `{ type: "_meta", meta_subtype: "stream_close",
873
975
  * meta_reason }` on EOF / abort / error. The default is silent
874
976
  * (matches pre-v0.9.x behaviour). */
977
+ /** GET-based SSE stream (RFC AI re-attach). The interactive re-attach
978
+ * endpoint is a GET with a query string, not a POST body, so it can't reuse
979
+ * streamSSE; the parse + error handling are identical otherwise. No debug
980
+ * shape — re-attach is a plain tail. */
981
+ async *streamSSEGet(path, signal) {
982
+ const headers = {
983
+ Accept: "text/event-stream, application/json",
984
+ };
985
+ if (this.ctx.authToken)
986
+ headers.Authorization = `Bearer ${this.ctx.authToken}`;
987
+ const resp = await this.ctx.fetchImpl(this.ctx.baseUrl + path, {
988
+ method: "GET",
989
+ headers,
990
+ signal,
991
+ });
992
+ if (!resp.ok) {
993
+ await raiseFromResponse(resp);
994
+ }
995
+ if (!resp.body) {
996
+ throw new Error("loomcycle: response has no body");
997
+ }
998
+ yield* parseSSE(resp.body.getReader());
999
+ }
875
1000
  async *streamSSE(path, body, signal, debug) {
876
1001
  const headers = {
877
1002
  "Content-Type": "application/json",
package/dist/index.d.ts CHANGED
@@ -52,6 +52,11 @@
52
52
  * ensureMcpServer(opts): Promise<EnsureMcpServerResult> // v0.18.0 — idempotent register-if-changed
53
53
  * scheduleDef(input): Promise<SubstrateToolResponse>
54
54
  *
55
+ * // Dynamic filesystem volumes (v0.35.0 — RFC AH; tenant-confined)
56
+ * volumeDef(input): Promise<SubstrateToolResponse> // create/get/list/delete/purge
57
+ * listVolumes(): Promise<PersistentVolumesResponse>
58
+ * listEphemeralVolumes(): Promise<EphemeralVolumesResponse>
59
+ *
55
60
  * // Library v2 enumeration (v0.10.3 — yaml+substrate merged)
56
61
  * listLibraryAgents(): Promise<LibraryListResponse<LibraryAgentDefinition>>
57
62
  * listLibrarySkills(): Promise<LibraryListResponse<LibrarySkillDefinition>>
@@ -82,5 +87,7 @@
82
87
  * See `adapters/ts/README.md` for usage examples.
83
88
  */
84
89
  export { LoomcycleClient } from "./client.js";
85
- export type { AgentEvent, ClientOptions, ContinueOptions, EventType, HostWidening, ParentContext, PromptContent, PromptSegment, RetryInfo, RunOptions, SamplingOptions, CompactionOptions, ToolUse, Usage, Agent, AgentStatus, AgentUsage, CancelAgentResult, ListAgentsResponse, RunBatchOptions, RunBatchResult, SpawnRunResult, CompactRunResult, TranscriptEvent, TranscriptResponse, HealthResponse, ListUsersResponse, UserSummary, WhoamiResponse, PauseResult, ResumeResult, RuntimeStateResponse, RuntimeStateStatus, ResolverMatrix, ResolverModelStatus, ResolverProviderAvailability, CreateSnapshotOptions, SnapshotCreateResponse, SnapshotDescriptor, SnapshotEnvelope, SnapshotListResponse, SnapshotRestoreResponse, MemoryEntriesResponse, MemoryEntry, MemoryEntryResponse, MemoryScopeIDsResponse, MemoryScopeIDSummary, MemoryScopeKind, MemoryScopesResponse, InterruptListResponse, InterruptRow, InterruptStatus, ResolveInterruptOptions, Hook, HookFailMode, HookPhase, HookToolCall, HookToolResult, ListHooksResponse, PostHookCall, PostHookResult, PreHookCall, PreHookResult, RegisterHookOptions, RegisterHookResponse, SubstrateToolInput, SubstrateToolResponse, SystemPromptPayload, UserInputPayload, ChannelDescriptor, ListChannelsResponse, RunStateEvent, RunStateStreamClose, RunStateStreamItem, RunStateStreamOpen, StreamUserRunStatesOptions, AckChannelOptions, ChannelAckResult, ChannelMessageItem, ChannelPeekResult, ChannelPublishResult, ChannelPurgeResult, ChannelScope, ChannelSubscribeResult, PeekChannelOptions, PublishChannelOptions, SubscribeChannelOptions, AwaitChannelsOptions, BroadcastChannelsOptions, ChannelAwaitEntry, ChannelAwaitMode, ChannelAwaitResult, ChannelBroadcastEntry, ChannelBroadcastResult, CreateChannelOptions, UpdateChannelOptions, SetMemoryEntryOptions, SetMemoryEntryResponse, AgentDefRowResponse, AgentDefVerifyResult, SkillDefVerifyResult, EnsureMcpServerOptions, EnsureMcpServerResult, MCPServerDefRowResponse, MCPServerDefVerifyResult, AgentDefOverlay, EnsureCodeAgentOptions, EnsureCodeAgentResult, LibraryAgentDefinition, LibraryEntry, LibraryListResponse, LibraryMcpServerDefinition, LibrarySkillDefinition, LLMChatContent, LLMChatMessage, LLMChatOptions, LLMChatResponse, LLMChatStreamDelta, LLMChatStreamItem, LLMChatToolCall, LLMChatUsage, LLMTool, LLMEmbeddingItem, LLMEmbeddingsOptions, LLMEmbeddingsResponse, LLMEmbeddingsUsage, } from "./types.js";
90
+ export { InteractiveSession } from "./interactive.js";
91
+ export type { InteractiveSessionOps } from "./interactive.js";
92
+ export type { AgentEvent, ClientOptions, ContinueOptions, EventType, HostWidening, ParentContext, PromptContent, PromptSegment, RetryInfo, RunOptions, SamplingOptions, CompactionOptions, ToolUse, Usage, Agent, AgentStatus, AgentUsage, CancelAgentResult, ListAgentsResponse, RunBatchOptions, RunBatchResult, SpawnRunResult, CompactRunResult, TranscriptEvent, TranscriptResponse, HealthResponse, ListUsersResponse, UserSummary, WhoamiResponse, PauseResult, ResumeResult, RuntimeStateResponse, RuntimeStateStatus, ResolverMatrix, ResolverModelStatus, ResolverProviderAvailability, CreateSnapshotOptions, SnapshotCreateResponse, SnapshotDescriptor, SnapshotEnvelope, SnapshotListResponse, SnapshotRestoreResponse, MemoryEntriesResponse, MemoryEntry, MemoryEntryResponse, MemoryScopeIDsResponse, MemoryScopeIDSummary, MemoryScopeKind, MemoryScopesResponse, InterruptListResponse, InterruptRow, InterruptStatus, ResolveInterruptOptions, Hook, HookFailMode, HookPhase, HookToolCall, HookToolResult, ListHooksResponse, PostHookCall, PostHookResult, PreHookCall, PreHookResult, RegisterHookOptions, RegisterHookResponse, SubstrateToolInput, SubstrateToolResponse, VolumeMode, PersistentVolumeEntry, PersistentVolumesResponse, EphemeralVolumeEntry, EphemeralVolumesResponse, SystemPromptPayload, UserInputPayload, ChannelDescriptor, ListChannelsResponse, RunStateEvent, RunStateStreamClose, RunStateStreamItem, RunStateStreamOpen, StreamUserRunStatesOptions, AckChannelOptions, ChannelAckResult, ChannelMessageItem, ChannelPeekResult, ChannelPublishResult, ChannelPurgeResult, ChannelScope, ChannelSubscribeResult, PeekChannelOptions, PublishChannelOptions, SubscribeChannelOptions, AwaitChannelsOptions, BroadcastChannelsOptions, ChannelAwaitEntry, ChannelAwaitMode, ChannelAwaitResult, ChannelBroadcastEntry, ChannelBroadcastResult, CreateChannelOptions, UpdateChannelOptions, SetMemoryEntryOptions, SetMemoryEntryResponse, AgentDefRowResponse, AgentDefVerifyResult, SkillDefVerifyResult, EnsureMcpServerOptions, EnsureMcpServerResult, MCPServerDefRowResponse, MCPServerDefVerifyResult, AgentDefOverlay, EnsureCodeAgentOptions, EnsureCodeAgentResult, LibraryAgentDefinition, LibraryEntry, LibraryListResponse, LibraryMcpServerDefinition, LibrarySkillDefinition, LLMChatContent, LLMChatMessage, LLMChatOptions, LLMChatResponse, LLMChatStreamDelta, LLMChatStreamItem, LLMChatToolCall, LLMChatUsage, LLMTool, LLMEmbeddingItem, LLMEmbeddingsOptions, LLMEmbeddingsResponse, LLMEmbeddingsUsage, } from "./types.js";
86
93
  export { AgentIDInUseError, AgentNotFoundError, AlreadyPausingError, AuthError, BackpressureError, HookNotFoundError, NotFoundError, InvalidArgumentError, ChannelCursorRegressionError, LoomcycleError, NotPausedError, PauseNotConfiguredError, PerUserQuotaExhaustedError, SessionBusyError, SessionNotFoundError, SnapshotNotFoundError, SnapshotTooLargeError, SnapshotVersionError, SubstrateToolRefusedError, UnavailableError, } from "./errors.js";
package/dist/index.js CHANGED
@@ -52,6 +52,11 @@
52
52
  * ensureMcpServer(opts): Promise<EnsureMcpServerResult> // v0.18.0 — idempotent register-if-changed
53
53
  * scheduleDef(input): Promise<SubstrateToolResponse>
54
54
  *
55
+ * // Dynamic filesystem volumes (v0.35.0 — RFC AH; tenant-confined)
56
+ * volumeDef(input): Promise<SubstrateToolResponse> // create/get/list/delete/purge
57
+ * listVolumes(): Promise<PersistentVolumesResponse>
58
+ * listEphemeralVolumes(): Promise<EphemeralVolumesResponse>
59
+ *
55
60
  * // Library v2 enumeration (v0.10.3 — yaml+substrate merged)
56
61
  * listLibraryAgents(): Promise<LibraryListResponse<LibraryAgentDefinition>>
57
62
  * listLibrarySkills(): Promise<LibraryListResponse<LibrarySkillDefinition>>
@@ -82,4 +87,5 @@
82
87
  * See `adapters/ts/README.md` for usage examples.
83
88
  */
84
89
  export { LoomcycleClient } from "./client.js";
90
+ export { InteractiveSession } from "./interactive.js";
85
91
  export { AgentIDInUseError, AgentNotFoundError, AlreadyPausingError, AuthError, BackpressureError, HookNotFoundError, NotFoundError, InvalidArgumentError, ChannelCursorRegressionError, LoomcycleError, NotPausedError, PauseNotConfiguredError, PerUserQuotaExhaustedError, SessionBusyError, SessionNotFoundError, SnapshotNotFoundError, SnapshotTooLargeError, SnapshotVersionError, SubstrateToolRefusedError, UnavailableError, } from "./errors.js";
@@ -0,0 +1,53 @@
1
+ import type { AgentEvent } from "./types.js";
2
+ /** The client-side operations an {@link InteractiveSession} routes through.
3
+ * Supplied by LoomcycleClient.interactiveSession / attachInteractiveSession —
4
+ * the session itself holds no transport logic. */
5
+ export interface InteractiveSessionOps {
6
+ sendRunInput: (runId: string, text: string) => Promise<{
7
+ delivered: boolean;
8
+ }>;
9
+ cancelAgent: (agentId: string) => Promise<unknown>;
10
+ }
11
+ /** A high-level driver for an interactive agentic session (RFC AI) — the
12
+ * adapter port of the Web UI's run terminal. It wraps the underlying event
13
+ * stream (a fresh `interactive` run, or a `streamRunByID` re-attach), taps
14
+ * each frame to track the run's tracking IDs + parked state, and routes
15
+ * operator input + cancel through the client.
16
+ *
17
+ * Typical loop:
18
+ * ```ts
19
+ * const sess = client.interactiveSession({ agent: "chat", segments: [...] });
20
+ * for await (const ev of sess.events()) {
21
+ * if (ev.type === "text") process.stdout.write(ev.text ?? "");
22
+ * if (ev.type === "awaiting_input") await sess.send(await prompt("you> "));
23
+ * }
24
+ * ```
25
+ * `send()` does NOT open a new stream — the operator's turn and the model's
26
+ * response arrive on the same `events()` iterator. */
27
+ export declare class InteractiveSession {
28
+ private readonly source;
29
+ private readonly ops;
30
+ /** The run's id, from the first `agent` frame (set up-front on re-attach). */
31
+ runId: string;
32
+ /** The run's agent_id (for cancel), from the first `agent` frame. */
33
+ agentId: string;
34
+ /** The run's session_id, from the `session` / `agent` frames. */
35
+ sessionId: string;
36
+ /** True after an `awaiting_input` frame; cleared on the next activity. */
37
+ awaitingInput: boolean;
38
+ constructor(source: AsyncIterable<AgentEvent>, ops: InteractiveSessionOps);
39
+ /** The merged event stream. Consume with `for await`. Each frame updates the
40
+ * session's runId / agentId / sessionId / awaitingInput BEFORE it is
41
+ * yielded, so a consumer that reacts to `awaiting_input` can immediately
42
+ * `send()`. */
43
+ events(): AsyncIterable<AgentEvent>;
44
+ /** Steer the live run with an operator message. The response arrives on the
45
+ * `events()` stream (NOT a new stream). Returns the server's `delivered`
46
+ * flag. Throws if the run_id isn't known yet — for a fresh session, consume
47
+ * `events()` until the `agent` frame (or the first `awaiting_input`) first;
48
+ * a re-attached session has the run_id up front. */
49
+ send(text: string): Promise<boolean>;
50
+ /** Cancel the run (and its sub-agents). No-op if the agent_id isn't known
51
+ * yet (nothing to cancel). */
52
+ cancel(): Promise<void>;
53
+ }
@@ -0,0 +1,72 @@
1
+ /** A high-level driver for an interactive agentic session (RFC AI) — the
2
+ * adapter port of the Web UI's run terminal. It wraps the underlying event
3
+ * stream (a fresh `interactive` run, or a `streamRunByID` re-attach), taps
4
+ * each frame to track the run's tracking IDs + parked state, and routes
5
+ * operator input + cancel through the client.
6
+ *
7
+ * Typical loop:
8
+ * ```ts
9
+ * const sess = client.interactiveSession({ agent: "chat", segments: [...] });
10
+ * for await (const ev of sess.events()) {
11
+ * if (ev.type === "text") process.stdout.write(ev.text ?? "");
12
+ * if (ev.type === "awaiting_input") await sess.send(await prompt("you> "));
13
+ * }
14
+ * ```
15
+ * `send()` does NOT open a new stream — the operator's turn and the model's
16
+ * response arrive on the same `events()` iterator. */
17
+ export class InteractiveSession {
18
+ source;
19
+ ops;
20
+ /** The run's id, from the first `agent` frame (set up-front on re-attach). */
21
+ runId = "";
22
+ /** The run's agent_id (for cancel), from the first `agent` frame. */
23
+ agentId = "";
24
+ /** The run's session_id, from the `session` / `agent` frames. */
25
+ sessionId = "";
26
+ /** True after an `awaiting_input` frame; cleared on the next activity. */
27
+ awaitingInput = false;
28
+ constructor(source, ops) {
29
+ this.source = source;
30
+ this.ops = ops;
31
+ }
32
+ /** The merged event stream. Consume with `for await`. Each frame updates the
33
+ * session's runId / agentId / sessionId / awaitingInput BEFORE it is
34
+ * yielded, so a consumer that reacts to `awaiting_input` can immediately
35
+ * `send()`. */
36
+ async *events() {
37
+ for await (const ev of this.source) {
38
+ if (ev.type === "agent") {
39
+ if (ev.run_id)
40
+ this.runId = ev.run_id;
41
+ if (ev.agent_id)
42
+ this.agentId = ev.agent_id;
43
+ if (ev.session_id)
44
+ this.sessionId = ev.session_id;
45
+ }
46
+ else if (ev.type === "session" && ev.session_id) {
47
+ this.sessionId = ev.session_id;
48
+ }
49
+ this.awaitingInput = ev.type === "awaiting_input";
50
+ yield ev;
51
+ }
52
+ }
53
+ /** Steer the live run with an operator message. The response arrives on the
54
+ * `events()` stream (NOT a new stream). Returns the server's `delivered`
55
+ * flag. Throws if the run_id isn't known yet — for a fresh session, consume
56
+ * `events()` until the `agent` frame (or the first `awaiting_input`) first;
57
+ * a re-attached session has the run_id up front. */
58
+ async send(text) {
59
+ if (!this.runId) {
60
+ throw new Error("loomcycle: run_id not known yet — consume events() until the `agent` frame before send()");
61
+ }
62
+ const { delivered } = await this.ops.sendRunInput(this.runId, text);
63
+ this.awaitingInput = false;
64
+ return delivered;
65
+ }
66
+ /** Cancel the run (and its sub-agents). No-op if the agent_id isn't known
67
+ * yet (nothing to cancel). */
68
+ async cancel() {
69
+ if (this.agentId)
70
+ await this.ops.cancelAgent(this.agentId);
71
+ }
72
+ }
package/dist/types.d.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * `client.ts` for the input shapes (RunOptions, CreateSnapshotOptions,
8
8
  * etc.) — those are translated to snake_case in the request body.
9
9
  */
10
- export type EventType = "started" | "text" | "tool_call" | "tool_result" | "usage" | "done" | "error" | "retry" | "host_widened" | "session" | "agent" | "_meta";
10
+ export type EventType = "started" | "text" | "tool_call" | "tool_result" | "usage" | "done" | "error" | "retry" | "host_widened" | "session" | "agent" | "awaiting_input" | "steer" | "context_compaction" | "_meta";
11
11
  export interface ToolUse {
12
12
  id: string;
13
13
  name: string;
@@ -70,6 +70,18 @@ export interface AgentEvent {
70
70
  /** Host-widening payload on `event: host_widened`. Nil on all other
71
71
  * event types. */
72
72
  host_widening?: HostWidening;
73
+ /** Payload on `event: awaiting_input` (RFC AI) — a persistent interactive
74
+ * run parked at end_turn. `since_turn` is the iteration it parked after. */
75
+ awaiting_input?: {
76
+ since_turn?: number;
77
+ };
78
+ /** Payload on `event: steer` (RFC AI) — the operator's drained turn. On a
79
+ * re-attach replay, `source` is `"replay"`. Nil on all other event types. */
80
+ user_input?: {
81
+ text?: string;
82
+ source?: string;
83
+ seen_at?: string;
84
+ };
73
85
  agent_id?: string;
74
86
  run_id?: string;
75
87
  session_id?: string;
@@ -177,6 +189,13 @@ export interface RunOptions {
177
189
  * Omitted = inherit entirely. Trigger compaction mid-run with
178
190
  * {@link LoomcycleClient.compactRun}. */
179
191
  compaction?: CompactionOptions;
192
+ /** RFC AI — start a PERSISTENT interactive run that parks at end_turn
193
+ * awaiting operator steering instead of terminating. The stream emits an
194
+ * `awaiting_input` frame when it parks; drive it with
195
+ * {@link LoomcycleClient.sendRunInput}, re-attach with
196
+ * {@link LoomcycleClient.streamRunByID}, and `cancelAgent` to end it.
197
+ * Higher-level: {@link LoomcycleClient.interactiveSession}. */
198
+ interactive?: boolean;
180
199
  /** Opt-in observability: when true, the iterator emits client-
181
200
  * synthesized `{ type: "_meta", meta_subtype: "stream_open" | "stream_close" }`
182
201
  * events around the real event stream. `meta_reason` carries the
@@ -279,6 +298,9 @@ export interface ContinueOptions {
279
298
  sampling?: SamplingOptions;
280
299
  /** Per-continuation context-compaction override — see {@link RunOptions.compaction}. */
281
300
  compaction?: CompactionOptions;
301
+ /** RFC AI — park this continuation at end_turn for operator steering. See
302
+ * {@link RunOptions.interactive}. */
303
+ interactive?: boolean;
282
304
  /** Opt-in observability: see {@link RunOptions.debug}. Same shape. */
283
305
  debug?: boolean;
284
306
  signal?: AbortSignal;
@@ -725,7 +747,7 @@ export interface PostHookCall {
725
747
  * the adapter doesn't re-validate. Use the optional `extra` index
726
748
  * signature for forward-compat fields. */
727
749
  export type SubstrateToolInput = {
728
- op: "create" | "fork" | "get" | "list" | "promote" | "retire" | "rediscover" | "verify";
750
+ op: "create" | "fork" | "get" | "list" | "promote" | "retire" | "rediscover" | "verify" | "delete" | "purge";
729
751
  name?: string;
730
752
  def_id?: string;
731
753
  parent_def_id?: string;
@@ -733,6 +755,7 @@ export type SubstrateToolInput = {
733
755
  description?: string;
734
756
  promote?: boolean;
735
757
  retired?: boolean;
758
+ mode?: VolumeMode;
736
759
  [extra: string]: unknown;
737
760
  };
738
761
  /** Response shape for {@link LoomcycleClient.agentDef} and
@@ -741,6 +764,42 @@ export type SubstrateToolInput = {
741
764
  * `{name, versions: [...]}`, promote/retire return summary shapes.
742
765
  * Callers narrow as needed. */
743
766
  export type SubstrateToolResponse = unknown;
767
+ /** Volume access mode — read-only or read-write (RFC AH). */
768
+ export type VolumeMode = "ro" | "rw";
769
+ /** One row of {@link LoomcycleClient.listVolumes} (`GET /v1/_volumes`). */
770
+ export interface PersistentVolumeEntry {
771
+ name: string;
772
+ /** "static" (operator yaml, read-only) or "dynamic" (a tenant VolumeDef). */
773
+ source: "static" | "dynamic";
774
+ /** Host path. Redacted to "" for a non-operator (tenant) caller — the
775
+ * volume universe is visible to a tenant operator, the host location is
776
+ * not. Operator-equivalent callers see the real path. */
777
+ path: string;
778
+ mode: VolumeMode;
779
+ /** True for the static volume flagged `default: true` (never for dynamic). */
780
+ default: boolean;
781
+ /** True for the static volume dynamic VolumeDefs are provisioned inside. */
782
+ dynamic_root: boolean;
783
+ /** Set for dynamic rows (the substrate stamps it); absent for static. */
784
+ created_at?: string;
785
+ }
786
+ export interface PersistentVolumesResponse {
787
+ entries: PersistentVolumeEntry[];
788
+ }
789
+ /** One row of {@link LoomcycleClient.listEphemeralVolumes}
790
+ * (`GET /v1/_volumes/ephemeral`). */
791
+ export interface EphemeralVolumeEntry {
792
+ name: string;
793
+ root_run_id: string;
794
+ /** Host path — redacted to "" for a non-operator caller (see
795
+ * {@link PersistentVolumeEntry.path}). */
796
+ path: string;
797
+ mode: VolumeMode;
798
+ created_at: string;
799
+ }
800
+ export interface EphemeralVolumesResponse {
801
+ entries: EphemeralVolumeEntry[];
802
+ }
744
803
  /** Response a Post webhook returns. When result is omitted the tool
745
804
  * result passes through unchanged. */
746
805
  export interface PostHookResult {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@loomcycle/client",
3
- "version": "0.33.0",
4
- "description": "TypeScript client for the loomcycle sidecar (HTTP+SSE). 54 methods covering run streaming, agent metadata, pause/resume/state, resolver re-probe (resolveProbe — issue #88 operator escape hatch), operator-token admin (operatorTokenDef — RFC L OSS multi-tenant auth) + whoami (RFC L authoritative principal, v0.17.0) + tenant-scoped listUsers / listUserAgents, dynamic MCP registration (mcpServerDef + v0.18.0 typed mcpServerDefVerify + ensureMcpServer idempotent register-if-changed), snapshot lifecycle, memory admin (incl. v0.9.0 Vector Memory embed_stats + reembed AND v0.11.5 setMemoryEntry + deleteMemoryEntry), interruption resolve, hook management, v0.8.22 substrate admin (agentDef + skillDef), v0.9.x n8n Phase 0 (listChannels + streamUserRunStates), v0.9.x Channel CRUD (publishChannel + subscribeChannel + peekChannel + ackChannel), v0.11.5 Channel admin CRUD (createChannel + updateChannel + deleteChannel — runtime substrate; yaml channels refuse mutation with HTTP 409), v0.9.x content_sha256 verify, v0.9.1 transcript first-cycle, v0.9.x dynamic MCP server registration (mcpServerDef + MCPServerDefVerifyResult), v0.10.3 Library v2 enumeration (listLibraryAgents + listLibrarySkills + listLibraryMcpServers), and v0.11.0 LLM Gateway (llmChat + llmStream — direct provider routing without agent overhead; primary target is n8n's LoomCycleChatModel AI Agent sub-node and any LangChain-compatible consumer). v0.10.1 — dual ESM + CommonJS distribution (additive — ESM consumers unchanged; CJS consumers like n8n's community-node loader now work). v0.19.0 — typed inline code-js agent ingestion (AgentDefOverlay.code_body + ensureCodeAgent — register a deterministic code agent through the substrate with no host filesystem bind; RFC J). v0.20.0 — ensureMcpServer surfaces discoveredToolCount straight from create (loomcycle now auto-discovers MCP tools at ingestion); rediscover is now an explicit force-refresh. v0.21.0 — run/continue accept optional non-secret `metadata` (repo name, review policy, …) passed to the agent, symmetric with the WebHook/Schedule trigger paths. v0.22.0 — version-aligned lockstep release; no client-surface change (RFC N tenant isolation of the agent/skill/MCP/Schedule/Webhook definition plane + real op-schemas on the builtin MCP meta-tools are both server-side). v0.23.0 — version-aligned lockstep release; no client-surface change (RFCs O/P/R MCP-server hardening + thin client and the RFC Q DeepSeek tool-content fix are all server-side / MCP-transport-side). v0.24.0 — purgeChannel clears a channel's buffered messages without deleting its definition (allowed on yaml-declared channels too, unlike deleteChannel — F20); AgentDefOverlay gains channels / evaluation_scopes / interruption so a COMPLETE interactive/multi-agent agent round-trips over the substrate (F14). (Consolidates the interim 0.24/0.25 package bumps, which were never tag-published, back into lockstep with the loomcycle v0.24.0 tag.) v0.25.0 — adds the RFC S channel fan-in/fan-out client twins: awaitChannels() (wait for any/all/at_least N messages across channels, or a timeout — non-committing) + broadcastChannels() (publish one payload to N channels in one atomic-pre-flight call), the client-facing counterparts of the in-band Channel.await / Channel.broadcast tool ops (the rest of v0.25.0 — the manual-management Web UI console + Context op=time + max_fires self-retiring schedules — is server-side / in-band, no client-surface change). v0.29.1 — Usage gains optional `max_context_tokens` (the serving model's context-window ceiling, stamped by the loop from Provider.Capabilities() on each usage event) so a consumer can render a 'context used / max' gauge without a hard-coded per-model table; additive + optional, no behavior change. (Lockstep catch-up: the field landed in loomcycle v0.29.0 but the adapter publish skipped on a version mismatch; v0.29.1 realigns the package version with the release tag so it publishes.) v0.33.0 — gRPC + TS client parity for the RFC Y external fan-out and the compaction surface: spawnRunBatch() (POST /v1/runs:batch — spawn up to 32 fresh runs concurrently in one call, combined index-aligned envelope, per-child failures in-envelope) + compactRun(runId) (POST /v1/runs/{run_id}/compact — summarize a parked run's context); plus per-run sampling + compaction overrides now accepted on runStreaming / continueSession (an explicit temperature 0 is preserved as deterministic, not dropped as falsy). (The gRPC half adds the matching SpawnRunBatch / CompactRun RPCs + the sampling/compaction fields on RunRequest/ContinueRequest — server-side.)",
3
+ "version": "1.1.1",
4
+ "description": "TypeScript client for the loomcycle sidecar (HTTP+SSE). 61 methods covering run streaming, agent metadata, pause/resume/state, resolver re-probe (resolveProbe — issue #88 operator escape hatch), operator-token admin (operatorTokenDef — RFC L OSS multi-tenant auth) + whoami (RFC L authoritative principal, v0.17.0) + tenant-scoped listUsers / listUserAgents, dynamic MCP registration (mcpServerDef + v0.18.0 typed mcpServerDefVerify + ensureMcpServer idempotent register-if-changed), snapshot lifecycle, memory admin (incl. v0.9.0 Vector Memory embed_stats + reembed AND v0.11.5 setMemoryEntry + deleteMemoryEntry), interruption resolve, hook management, v0.8.22 substrate admin (agentDef + skillDef), v0.9.x n8n Phase 0 (listChannels + streamUserRunStates), v0.9.x Channel CRUD (publishChannel + subscribeChannel + peekChannel + ackChannel), v0.11.5 Channel admin CRUD (createChannel + updateChannel + deleteChannel — runtime substrate; yaml channels refuse mutation with HTTP 409), v0.9.x content_sha256 verify, v0.9.1 transcript first-cycle, v0.9.x dynamic MCP server registration (mcpServerDef + MCPServerDefVerifyResult), v0.10.3 Library v2 enumeration (listLibraryAgents + listLibrarySkills + listLibraryMcpServers), and v0.11.0 LLM Gateway (llmChat + llmStream — direct provider routing without agent overhead; primary target is n8n's LoomCycleChatModel AI Agent sub-node and any LangChain-compatible consumer). v0.10.1 — dual ESM + CommonJS distribution (additive — ESM consumers unchanged; CJS consumers like n8n's community-node loader now work). v0.19.0 — typed inline code-js agent ingestion (AgentDefOverlay.code_body + ensureCodeAgent — register a deterministic code agent through the substrate with no host filesystem bind; RFC J). v0.20.0 — ensureMcpServer surfaces discoveredToolCount straight from create (loomcycle now auto-discovers MCP tools at ingestion); rediscover is now an explicit force-refresh. v0.21.0 — run/continue accept optional non-secret `metadata` (repo name, review policy, …) passed to the agent, symmetric with the WebHook/Schedule trigger paths. v0.22.0 — version-aligned lockstep release; no client-surface change (RFC N tenant isolation of the agent/skill/MCP/Schedule/Webhook definition plane + real op-schemas on the builtin MCP meta-tools are both server-side). v0.23.0 — version-aligned lockstep release; no client-surface change (RFCs O/P/R MCP-server hardening + thin client and the RFC Q DeepSeek tool-content fix are all server-side / MCP-transport-side). v0.24.0 — purgeChannel clears a channel's buffered messages without deleting its definition (allowed on yaml-declared channels too, unlike deleteChannel — F20); AgentDefOverlay gains channels / evaluation_scopes / interruption so a COMPLETE interactive/multi-agent agent round-trips over the substrate (F14). (Consolidates the interim 0.24/0.25 package bumps, which were never tag-published, back into lockstep with the loomcycle v0.24.0 tag.) v0.25.0 — adds the RFC S channel fan-in/fan-out client twins: awaitChannels() (wait for any/all/at_least N messages across channels, or a timeout — non-committing) + broadcastChannels() (publish one payload to N channels in one atomic-pre-flight call), the client-facing counterparts of the in-band Channel.await / Channel.broadcast tool ops (the rest of v0.25.0 — the manual-management Web UI console + Context op=time + max_fires self-retiring schedules — is server-side / in-band, no client-surface change). v0.29.1 — Usage gains optional `max_context_tokens` (the serving model's context-window ceiling, stamped by the loop from Provider.Capabilities() on each usage event) so a consumer can render a 'context used / max' gauge without a hard-coded per-model table; additive + optional, no behavior change. (Lockstep catch-up: the field landed in loomcycle v0.29.0 but the adapter publish skipped on a version mismatch; v0.29.1 realigns the package version with the release tag so it publishes.) v0.33.0 — gRPC + TS client parity for the RFC Y external fan-out and the compaction surface: spawnRunBatch() (POST /v1/runs:batch — spawn up to 32 fresh runs concurrently in one call, combined index-aligned envelope, per-child failures in-envelope) + compactRun(runId) (POST /v1/runs/{run_id}/compact — summarize a parked run's context); plus per-run sampling + compaction overrides now accepted on runStreaming / continueSession (an explicit temperature 0 is preserved as deterministic, not dropped as falsy). (The gRPC half adds the matching SpawnRunBatch / CompactRun RPCs + the sampling/compaction fields on RunRequest/ContinueRequest — server-side.) v0.34.0 — version-aligned lockstep release; no client-surface change (context-transform plugins / RFC Z Phase 1a are server-side config; the exp7 hardening pass is server-side; the R2 cross-provider thinking-model downgrade surfaces a new `model_downgraded` SSE event the generic stream passes through unchanged). v0.35.0 — RFC AH dynamic filesystem volumes: volumeDef() (POST /v1/_volumedef — op-discriminated create/get/list/delete/purge; a Volume is flat, so delete unmaps + purge RemoveAll's, no retire/promote/fork) + listVolumes() / listEphemeralVolumes() (GET /v1/_volumes[/ephemeral] — tenant-scoped; host paths redacted for non-operator callers). Tenant-confined; the runtime derives the path inside an operator-blessed dynamic_root, so callers pass name + mode, never a host path. v1.1.1 — RFC AI interactive agentic sessions: an `interactive: true` flag on runStreaming/continueSession (a run that parks at end_turn for steering) + sendRunInput(runId, text) (POST /v1/runs/{id}/input — steer a live run) + streamRunByID(runId, {fromSeq}) (GET /v1/runs/{id}/stream — re-attach by run_id; the operator's prior turns replay as `steer` events so a cold client reconstructs the whole conversation) + a high-level InteractiveSession driver (client.interactiveSession / attachInteractiveSession — events()/send()/cancel(), the adapter port of the Web UI run terminal). The AgentEvent union gains awaiting_input/steer/context_compaction frames. Version-aligned with the loomcycle v1.1.x line so the v1.1.1 tag publishes it (also carrying the previously-unpublished v0.35.0 volume surface).",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "repository": {