@loomcycle/client 1.81.0 → 1.83.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.
@@ -113,8 +113,52 @@ function runBody(opts) {
113
113
  body.max_context_tokens = opts.maxContextTokens;
114
114
  if (opts.interactive !== undefined)
115
115
  body.interactive = opts.interactive;
116
+ applyOverridesToWire(body, opts);
116
117
  return body;
117
118
  }
119
+ /** Serializes the RFC DC per-run overrides onto a request body.
120
+ *
121
+ * One function rather than two copies of the list, because this file already
122
+ * serializes RunOptions field-by-field at two call sites — and a field added
123
+ * to the TYPE but to only one of those lists is invisible on the wire from one
124
+ * of them, silently. That is the shape of the bug RFC DA shipped.
125
+ */
126
+ // The parameter is RunOverrideOptions itself, not a structural copy of its
127
+ // fields. It WAS such a copy — a second enumeration of the same list sitting in
128
+ // the signature of the function whose whole job is to be the single one — and
129
+ // adding a field to the shared interface then failed to compile here, which is
130
+ // the good outcome only because someone was looking. Typing it as the interface
131
+ // makes the list unduplicated rather than merely checked.
132
+ function applyOverridesToWire(body, opts) {
133
+ if (opts.model !== undefined)
134
+ body.model = opts.model;
135
+ if (opts.provider !== undefined)
136
+ body.provider = opts.provider;
137
+ if (opts.tier !== undefined)
138
+ body.tier = opts.tier;
139
+ if (opts.effort !== undefined)
140
+ body.effort = opts.effort;
141
+ if (opts.maxTokens !== undefined)
142
+ body.max_tokens = opts.maxTokens;
143
+ if (opts.maxIterations !== undefined)
144
+ body.max_iterations = opts.maxIterations;
145
+ if (opts.unboundedIterations !== undefined)
146
+ body.unbounded_iterations = opts.unboundedIterations;
147
+ if (opts.maxConcurrentChildren !== undefined)
148
+ body.max_concurrent_children = opts.maxConcurrentChildren;
149
+ if (opts.retryAttempts !== undefined)
150
+ body.retry_attempts = opts.retryAttempts;
151
+ if (opts.memoryInjectMaxTokens !== undefined)
152
+ body.memory_inject_max_tokens = opts.memoryInjectMaxTokens;
153
+ if (opts.memoryIndexMaxBytes !== undefined)
154
+ body.memory_index_max_bytes = opts.memoryIndexMaxBytes;
155
+ if (opts.injectToolGuide !== undefined)
156
+ body.inject_tool_guide = opts.injectToolGuide;
157
+ if (opts.interactive !== undefined)
158
+ body.interactive = opts.interactive;
159
+ if (opts.interruption !== undefined)
160
+ body.interruption = opts.interruption;
161
+ }
118
162
  class LoomcycleClient {
119
163
  ctx;
120
164
  constructor(opts = {}) {
@@ -199,6 +243,7 @@ class LoomcycleClient {
199
243
  body.max_context_tokens = opts.maxContextTokens;
200
244
  if (opts.interactive !== undefined)
201
245
  body.interactive = opts.interactive;
246
+ applyOverridesToWire(body, opts);
202
247
  yield* this.streamSSE(`/v1/sessions/${encodeURIComponent(opts.sessionId)}/messages`, body, opts.signal, opts.debug);
203
248
  }
204
249
  /** Push an operator steering message into a LIVE interactive run (RFC AI).
@@ -212,7 +257,69 @@ class LoomcycleClient {
212
257
  * Raises {@link UnavailableError} (503, steering off / no run),
213
258
  * {@link AuthError} (401). A full steer queue surfaces as a 429. */
214
259
  async sendRunInput(runId, text, opts) {
215
- return (0, fetch_helpers_js_1.postJSON)(this.ctx, `/v1/runs/${encodeURIComponent(runId)}/input`, { text }, opts);
260
+ const body = { text };
261
+ if (opts?.overrides) {
262
+ // The wire nests these under `overrides` on THIS endpoint (unlike the flat
263
+ // fields on /v1/runs), because the request's other job is sending a turn
264
+ // and the group needs a name.
265
+ const o = {};
266
+ applyOverridesToWire(o, opts.overrides);
267
+ if (Object.keys(o).length > 0)
268
+ body.overrides = o;
269
+ }
270
+ return (0, fetch_helpers_js_1.postJSON)(this.ctx, `/v1/runs/${encodeURIComponent(runId)}/input`, body, opts);
271
+ }
272
+ /** Change a run's settings WITHOUT sending it a turn. Mirrors
273
+ * `POST /v1/runs/{run_id}/retune`.
274
+ *
275
+ * Use this to retune a PARKED chat — switch it to a different model, raise
276
+ * its iteration bound — when you do not also want a message in the
277
+ * transcript. `sendRunInput(runId, text, { overrides })` is the other half:
278
+ * retune and speak in one atomic call.
279
+ *
280
+ * The overrides select WITHIN what the agent's definition already allows and
281
+ * cannot widen it; an override the definition forbids is REFUSED here rather
282
+ * than silently dropped, so a rejected retune is visible at the call.
283
+ *
284
+ * Raises {@link NotFoundError} (404, no in-flight run — which is also what a
285
+ * run belonging to another tenant returns, deliberately), and 422 when no
286
+ * override is supplied at all. */
287
+ async retuneRun(runId, overrides, opts) {
288
+ const body = {};
289
+ applyOverridesToWire(body, overrides);
290
+ return (0, fetch_helpers_js_1.postJSON)(this.ctx, `/v1/runs/${encodeURIComponent(runId)}/retune`, body, opts);
291
+ }
292
+ /** Read a run's own stored overrides. Mirrors `GET /v1/runs/{run_id}/config`.
293
+ *
294
+ * This is what the RUN overrides, not what it will effectively use: a field
295
+ * absent from `config` means "this run does not override it", and the
296
+ * definition, the tier or a driver still decides. {@link getEffectiveConfig}
297
+ * is the resolved view.
298
+ *
299
+ * A run that was never retuned answers with an empty `config` — "this run
300
+ * overrides nothing" is an answer, not a 404.
301
+ *
302
+ * Raises {@link NotFoundError} (404), which is also what a run belonging to
303
+ * another tenant returns — deliberately, so the gate is not an existence
304
+ * oracle for run ids that are not secrets. */
305
+ async getRunConfig(runId, opts) {
306
+ return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, `/v1/runs/${encodeURIComponent(runId)}/config`, opts);
307
+ }
308
+ /** Read what a run will ACTUALLY use, field by field, with the layer that
309
+ * decided each. Mirrors `GET /v1/runs/{run_id}/effective-config`.
310
+ *
311
+ * Nothing else assembles this: a definition is a sparse overlay, the tier and
312
+ * driver defaults are published nowhere else, and a field no layer set
313
+ * resolves later somewhere the caller cannot see. The `source` on each field
314
+ * is the point — `max_iterations: 16` cannot tell a deliberate setting from a
315
+ * default nobody chose, and those call for opposite actions.
316
+ *
317
+ * Keys are wire names (`max_tokens`, not `maxTokens`), so the result joins
318
+ * directly against a definition from the library endpoints.
319
+ *
320
+ * Raises {@link NotFoundError} (404) for an unknown or cross-tenant run. */
321
+ async getEffectiveConfig(runId, opts) {
322
+ return (0, fetch_helpers_js_1.jsonFetch)(this.ctx, `/v1/runs/${encodeURIComponent(runId)}/effective-config`, opts);
216
323
  }
217
324
  /** Re-attach to a run's event stream by `run_id` (RFC AI), replaying from
218
325
  * `fromSeq` (default 0) then live-tailing — the gRPC-free twin of the Web
@@ -239,7 +346,8 @@ class LoomcycleClient {
239
346
  interactiveSession(opts) {
240
347
  const source = this.runStreaming({ ...opts, interactive: true });
241
348
  return new interactive_js_1.InteractiveSession(source, {
242
- sendRunInput: (rid, t) => this.sendRunInput(rid, t),
349
+ sendRunInput: (rid, t, o) => this.sendRunInput(rid, t, o),
350
+ retuneRun: (rid, ov) => this.retuneRun(rid, ov),
243
351
  cancelAgent: (aid) => this.cancelAgent(aid),
244
352
  });
245
353
  }
@@ -250,7 +358,8 @@ class LoomcycleClient {
250
358
  attachInteractiveSession(runId, opts) {
251
359
  const source = this.streamRunByID(runId, opts);
252
360
  const session = new interactive_js_1.InteractiveSession(source, {
253
- sendRunInput: (rid, t) => this.sendRunInput(rid, t),
361
+ sendRunInput: (rid, t, o) => this.sendRunInput(rid, t, o),
362
+ retuneRun: (rid, ov) => this.retuneRun(rid, ov),
254
363
  cancelAgent: (aid) => this.cancelAgent(aid),
255
364
  });
256
365
  session.runId = runId;
@@ -58,14 +58,30 @@ class InteractiveSession {
58
58
  * flag. Throws if the run_id isn't known yet — for a fresh session, consume
59
59
  * `events()` until the `agent` frame (or the first `awaiting_input`) first;
60
60
  * a re-attached session has the run_id up front. */
61
- async send(text) {
61
+ async send(text, opts) {
62
62
  if (!this.runId) {
63
63
  throw new Error("loomcycle: run_id not known yet — consume events() until the `agent` frame before send()");
64
64
  }
65
- const { delivered } = await this.ops.sendRunInput(this.runId, text);
65
+ const { delivered } = await this.ops.sendRunInput(this.runId, text, opts);
66
66
  this.awaitingInput = false;
67
67
  return delivered;
68
68
  }
69
+ /** Change this run's settings WITHOUT sending it a turn — switch a parked
70
+ * chat to another model, raise its iteration bound.
71
+ *
72
+ * `send(text, { overrides })` is the other half: retune and speak in one
73
+ * atomic call. Use THIS one when a message in the transcript would be an
74
+ * artefact of changing a setting rather than something the operator said.
75
+ *
76
+ * Does not clear `awaitingInput`: a parked run is still parked after a
77
+ * retune, because nothing was delivered for it to answer. */
78
+ async retune(overrides) {
79
+ if (!this.runId) {
80
+ throw new Error("loomcycle: run_id not known yet — consume events() until the `agent` frame before retune()");
81
+ }
82
+ const { retuned } = await this.ops.retuneRun(this.runId, overrides);
83
+ return retuned;
84
+ }
69
85
  /** Cancel the run (and its sub-agents). No-op if the agent_id isn't known
70
86
  * yet (nothing to cancel). */
71
87
  async cancel() {
package/dist/client.d.ts CHANGED
@@ -25,7 +25,7 @@
25
25
  */
26
26
  import { InteractiveSession } from "./interactive.js";
27
27
  import { ClientToolHost, type ConnectClientToolsOptions } from "./client-tools.js";
28
- import type { CredentialListResponse, CredentialMeta, CredentialScope, DirectoryInspection, DirectoryTenant, DirectoryUser, ErasureReport, ErasureResult, 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, ChannelReleaseResult, ChannelSubscribeResult, CreateChannelOptions, ListChannelsResponse, PeekChannelOptions, PublishChannelOptions, ReleaseChannelOptions, SetMemoryEntryOptions, SetMemoryEntryResponse, SubscribeChannelOptions, UpdateChannelOptions, LibraryAgentDefinition, LibraryListResponse, LibraryMcpServerDefinition, LibrarySkillDefinition, ListUsersResponse, UserRecord, CreateUserBody, UpdateUserBody, MintedUserToken, ListUserTokensResponse, RunnableAgentsResponse, LLMChatOptions, LLMChatResponse, LLMChatStreamItem, LLMEmbeddingsOptions, LLMEmbeddingsResponse, MCPServerDefVerifyResult, MemoryEmbedStatsResponse, MemoryEntriesResponse, MemoryEntryResponse, MemoryBackfillResponse, MemoryPurgeResponse, MemoryReembedResponse, MemoryScopeIDsResponse, MemoryScopesResponse, MemorySearchInput, MemorySearchResponse, PauseResult, PersistentVolumesResponse, EphemeralVolumesResponse, RegisterHookOptions, RegisterHookResponse, ResolveInterruptOptions, ResumeResult, ResolverMatrix, CompactRunResult, ReplaySessionResult, CancelTurnResult, RunBatchOptions, RunBatchResult, RunOptions, RunStateStreamItem, RuntimeStateResponse, SnapshotCreateResponse, SnapshotDescriptor, SnapshotEnvelope, SnapshotRestoreResponse, StreamUserRunStatesOptions, SubstrateToolInput, SubstrateToolResponse, CreatedTeam, ListTeamsResponse, PromotedTeam, RetiredTeam, TeamBreakpoints, TeamDefDetail, TeamRunDetached, TeamRunTarget, TeamVerification, TeamVersionList, TeamDiagram, TeamRunResult, PathToolInput, PathToolResponse, DocumentToolInput, DocumentToolResponse, HistoryToolInput, HistoryToolResponse, TranscriptResponse, WhoamiResponse, UsageDimension, UsageReportResponse, TokenLimit, TokenLimitsResponse, ConfigResponse, SetTokenLimitRequest } from "./types.js";
28
+ import type { CredentialListResponse, CredentialMeta, CredentialScope, DirectoryInspection, DirectoryTenant, DirectoryUser, ErasureReport, ErasureResult, Agent, AgentEvent, AgentStatus, CancelAgentResult, ClientOptions, ContinueOptions, RunOverrideOptions, EffectiveConfigResponse, RetuneRunResponse, RunConfigResponse, CreateSnapshotOptions, EnsureCodeAgentOptions, EnsureCodeAgentResult, EnsureMcpServerOptions, EnsureMcpServerResult, HealthResponse, Hook, InterruptListResponse, InterruptStatus, AckChannelOptions, AwaitChannelsOptions, BroadcastChannelsOptions, ChannelAckResult, ChannelAwaitResult, ChannelBroadcastResult, ChannelDescriptor, ChannelPeekResult, ChannelPublishResult, ChannelPurgeResult, ChannelReleaseResult, ChannelSubscribeResult, CreateChannelOptions, ListChannelsResponse, PeekChannelOptions, PublishChannelOptions, ReleaseChannelOptions, SetMemoryEntryOptions, SetMemoryEntryResponse, SubscribeChannelOptions, UpdateChannelOptions, LibraryAgentDefinition, LibraryListResponse, LibraryMcpServerDefinition, LibrarySkillDefinition, ListUsersResponse, UserRecord, CreateUserBody, UpdateUserBody, MintedUserToken, ListUserTokensResponse, RunnableAgentsResponse, LLMChatOptions, LLMChatResponse, LLMChatStreamItem, LLMEmbeddingsOptions, LLMEmbeddingsResponse, MCPServerDefVerifyResult, MemoryEmbedStatsResponse, MemoryEntriesResponse, MemoryEntryResponse, MemoryBackfillResponse, MemoryPurgeResponse, MemoryReembedResponse, MemoryScopeIDsResponse, MemoryScopesResponse, MemorySearchInput, MemorySearchResponse, PauseResult, PersistentVolumesResponse, EphemeralVolumesResponse, RegisterHookOptions, RegisterHookResponse, ResolveInterruptOptions, ResumeResult, ResolverMatrix, CompactRunResult, ReplaySessionResult, CancelTurnResult, RunBatchOptions, RunBatchResult, RunOptions, RunStateStreamItem, RuntimeStateResponse, SnapshotCreateResponse, SnapshotDescriptor, SnapshotEnvelope, SnapshotRestoreResponse, StreamUserRunStatesOptions, SubstrateToolInput, SubstrateToolResponse, CreatedTeam, ListTeamsResponse, PromotedTeam, RetiredTeam, TeamBreakpoints, TeamDefDetail, TeamRunDetached, TeamRunTarget, TeamVerification, TeamVersionList, TeamDiagram, TeamRunResult, PathToolInput, PathToolResponse, DocumentToolInput, DocumentToolResponse, HistoryToolInput, HistoryToolResponse, TranscriptResponse, WhoamiResponse, UsageDimension, UsageReportResponse, TokenLimit, TokenLimitsResponse, ConfigResponse, SetTokenLimitRequest } from "./types.js";
29
29
  export declare class LoomcycleClient {
30
30
  private ctx;
31
31
  constructor(opts?: ClientOptions);
@@ -81,10 +81,61 @@ export declare class LoomcycleClient {
81
81
  * {@link AuthError} (401). A full steer queue surfaces as a 429. */
82
82
  sendRunInput(runId: string, text: string, opts?: {
83
83
  signal?: AbortSignal;
84
+ overrides?: RunOverrideOptions;
84
85
  }): Promise<{
85
86
  run_id: string;
86
87
  delivered: boolean;
87
88
  }>;
89
+ /** Change a run's settings WITHOUT sending it a turn. Mirrors
90
+ * `POST /v1/runs/{run_id}/retune`.
91
+ *
92
+ * Use this to retune a PARKED chat — switch it to a different model, raise
93
+ * its iteration bound — when you do not also want a message in the
94
+ * transcript. `sendRunInput(runId, text, { overrides })` is the other half:
95
+ * retune and speak in one atomic call.
96
+ *
97
+ * The overrides select WITHIN what the agent's definition already allows and
98
+ * cannot widen it; an override the definition forbids is REFUSED here rather
99
+ * than silently dropped, so a rejected retune is visible at the call.
100
+ *
101
+ * Raises {@link NotFoundError} (404, no in-flight run — which is also what a
102
+ * run belonging to another tenant returns, deliberately), and 422 when no
103
+ * override is supplied at all. */
104
+ retuneRun(runId: string, overrides: RunOverrideOptions, opts?: {
105
+ signal?: AbortSignal;
106
+ }): Promise<RetuneRunResponse>;
107
+ /** Read a run's own stored overrides. Mirrors `GET /v1/runs/{run_id}/config`.
108
+ *
109
+ * This is what the RUN overrides, not what it will effectively use: a field
110
+ * absent from `config` means "this run does not override it", and the
111
+ * definition, the tier or a driver still decides. {@link getEffectiveConfig}
112
+ * is the resolved view.
113
+ *
114
+ * A run that was never retuned answers with an empty `config` — "this run
115
+ * overrides nothing" is an answer, not a 404.
116
+ *
117
+ * Raises {@link NotFoundError} (404), which is also what a run belonging to
118
+ * another tenant returns — deliberately, so the gate is not an existence
119
+ * oracle for run ids that are not secrets. */
120
+ getRunConfig(runId: string, opts?: {
121
+ signal?: AbortSignal;
122
+ }): Promise<RunConfigResponse>;
123
+ /** Read what a run will ACTUALLY use, field by field, with the layer that
124
+ * decided each. Mirrors `GET /v1/runs/{run_id}/effective-config`.
125
+ *
126
+ * Nothing else assembles this: a definition is a sparse overlay, the tier and
127
+ * driver defaults are published nowhere else, and a field no layer set
128
+ * resolves later somewhere the caller cannot see. The `source` on each field
129
+ * is the point — `max_iterations: 16` cannot tell a deliberate setting from a
130
+ * default nobody chose, and those call for opposite actions.
131
+ *
132
+ * Keys are wire names (`max_tokens`, not `maxTokens`), so the result joins
133
+ * directly against a definition from the library endpoints.
134
+ *
135
+ * Raises {@link NotFoundError} (404) for an unknown or cross-tenant run. */
136
+ getEffectiveConfig(runId: string, opts?: {
137
+ signal?: AbortSignal;
138
+ }): Promise<EffectiveConfigResponse>;
88
139
  /** Re-attach to a run's event stream by `run_id` (RFC AI), replaying from
89
140
  * `fromSeq` (default 0) then live-tailing — the gRPC-free twin of the Web
90
141
  * UI's resume-in-terminal. Mirrors `GET /v1/runs/{run_id}/stream`. The
package/dist/client.js CHANGED
@@ -110,8 +110,52 @@ function runBody(opts) {
110
110
  body.max_context_tokens = opts.maxContextTokens;
111
111
  if (opts.interactive !== undefined)
112
112
  body.interactive = opts.interactive;
113
+ applyOverridesToWire(body, opts);
113
114
  return body;
114
115
  }
116
+ /** Serializes the RFC DC per-run overrides onto a request body.
117
+ *
118
+ * One function rather than two copies of the list, because this file already
119
+ * serializes RunOptions field-by-field at two call sites — and a field added
120
+ * to the TYPE but to only one of those lists is invisible on the wire from one
121
+ * of them, silently. That is the shape of the bug RFC DA shipped.
122
+ */
123
+ // The parameter is RunOverrideOptions itself, not a structural copy of its
124
+ // fields. It WAS such a copy — a second enumeration of the same list sitting in
125
+ // the signature of the function whose whole job is to be the single one — and
126
+ // adding a field to the shared interface then failed to compile here, which is
127
+ // the good outcome only because someone was looking. Typing it as the interface
128
+ // makes the list unduplicated rather than merely checked.
129
+ function applyOverridesToWire(body, opts) {
130
+ if (opts.model !== undefined)
131
+ body.model = opts.model;
132
+ if (opts.provider !== undefined)
133
+ body.provider = opts.provider;
134
+ if (opts.tier !== undefined)
135
+ body.tier = opts.tier;
136
+ if (opts.effort !== undefined)
137
+ body.effort = opts.effort;
138
+ if (opts.maxTokens !== undefined)
139
+ body.max_tokens = opts.maxTokens;
140
+ if (opts.maxIterations !== undefined)
141
+ body.max_iterations = opts.maxIterations;
142
+ if (opts.unboundedIterations !== undefined)
143
+ body.unbounded_iterations = opts.unboundedIterations;
144
+ if (opts.maxConcurrentChildren !== undefined)
145
+ body.max_concurrent_children = opts.maxConcurrentChildren;
146
+ if (opts.retryAttempts !== undefined)
147
+ body.retry_attempts = opts.retryAttempts;
148
+ if (opts.memoryInjectMaxTokens !== undefined)
149
+ body.memory_inject_max_tokens = opts.memoryInjectMaxTokens;
150
+ if (opts.memoryIndexMaxBytes !== undefined)
151
+ body.memory_index_max_bytes = opts.memoryIndexMaxBytes;
152
+ if (opts.injectToolGuide !== undefined)
153
+ body.inject_tool_guide = opts.injectToolGuide;
154
+ if (opts.interactive !== undefined)
155
+ body.interactive = opts.interactive;
156
+ if (opts.interruption !== undefined)
157
+ body.interruption = opts.interruption;
158
+ }
115
159
  export class LoomcycleClient {
116
160
  ctx;
117
161
  constructor(opts = {}) {
@@ -196,6 +240,7 @@ export class LoomcycleClient {
196
240
  body.max_context_tokens = opts.maxContextTokens;
197
241
  if (opts.interactive !== undefined)
198
242
  body.interactive = opts.interactive;
243
+ applyOverridesToWire(body, opts);
199
244
  yield* this.streamSSE(`/v1/sessions/${encodeURIComponent(opts.sessionId)}/messages`, body, opts.signal, opts.debug);
200
245
  }
201
246
  /** Push an operator steering message into a LIVE interactive run (RFC AI).
@@ -209,7 +254,69 @@ export class LoomcycleClient {
209
254
  * Raises {@link UnavailableError} (503, steering off / no run),
210
255
  * {@link AuthError} (401). A full steer queue surfaces as a 429. */
211
256
  async sendRunInput(runId, text, opts) {
212
- return postJSON(this.ctx, `/v1/runs/${encodeURIComponent(runId)}/input`, { text }, opts);
257
+ const body = { text };
258
+ if (opts?.overrides) {
259
+ // The wire nests these under `overrides` on THIS endpoint (unlike the flat
260
+ // fields on /v1/runs), because the request's other job is sending a turn
261
+ // and the group needs a name.
262
+ const o = {};
263
+ applyOverridesToWire(o, opts.overrides);
264
+ if (Object.keys(o).length > 0)
265
+ body.overrides = o;
266
+ }
267
+ return postJSON(this.ctx, `/v1/runs/${encodeURIComponent(runId)}/input`, body, opts);
268
+ }
269
+ /** Change a run's settings WITHOUT sending it a turn. Mirrors
270
+ * `POST /v1/runs/{run_id}/retune`.
271
+ *
272
+ * Use this to retune a PARKED chat — switch it to a different model, raise
273
+ * its iteration bound — when you do not also want a message in the
274
+ * transcript. `sendRunInput(runId, text, { overrides })` is the other half:
275
+ * retune and speak in one atomic call.
276
+ *
277
+ * The overrides select WITHIN what the agent's definition already allows and
278
+ * cannot widen it; an override the definition forbids is REFUSED here rather
279
+ * than silently dropped, so a rejected retune is visible at the call.
280
+ *
281
+ * Raises {@link NotFoundError} (404, no in-flight run — which is also what a
282
+ * run belonging to another tenant returns, deliberately), and 422 when no
283
+ * override is supplied at all. */
284
+ async retuneRun(runId, overrides, opts) {
285
+ const body = {};
286
+ applyOverridesToWire(body, overrides);
287
+ return postJSON(this.ctx, `/v1/runs/${encodeURIComponent(runId)}/retune`, body, opts);
288
+ }
289
+ /** Read a run's own stored overrides. Mirrors `GET /v1/runs/{run_id}/config`.
290
+ *
291
+ * This is what the RUN overrides, not what it will effectively use: a field
292
+ * absent from `config` means "this run does not override it", and the
293
+ * definition, the tier or a driver still decides. {@link getEffectiveConfig}
294
+ * is the resolved view.
295
+ *
296
+ * A run that was never retuned answers with an empty `config` — "this run
297
+ * overrides nothing" is an answer, not a 404.
298
+ *
299
+ * Raises {@link NotFoundError} (404), which is also what a run belonging to
300
+ * another tenant returns — deliberately, so the gate is not an existence
301
+ * oracle for run ids that are not secrets. */
302
+ async getRunConfig(runId, opts) {
303
+ return jsonFetch(this.ctx, `/v1/runs/${encodeURIComponent(runId)}/config`, opts);
304
+ }
305
+ /** Read what a run will ACTUALLY use, field by field, with the layer that
306
+ * decided each. Mirrors `GET /v1/runs/{run_id}/effective-config`.
307
+ *
308
+ * Nothing else assembles this: a definition is a sparse overlay, the tier and
309
+ * driver defaults are published nowhere else, and a field no layer set
310
+ * resolves later somewhere the caller cannot see. The `source` on each field
311
+ * is the point — `max_iterations: 16` cannot tell a deliberate setting from a
312
+ * default nobody chose, and those call for opposite actions.
313
+ *
314
+ * Keys are wire names (`max_tokens`, not `maxTokens`), so the result joins
315
+ * directly against a definition from the library endpoints.
316
+ *
317
+ * Raises {@link NotFoundError} (404) for an unknown or cross-tenant run. */
318
+ async getEffectiveConfig(runId, opts) {
319
+ return jsonFetch(this.ctx, `/v1/runs/${encodeURIComponent(runId)}/effective-config`, opts);
213
320
  }
214
321
  /** Re-attach to a run's event stream by `run_id` (RFC AI), replaying from
215
322
  * `fromSeq` (default 0) then live-tailing — the gRPC-free twin of the Web
@@ -236,7 +343,8 @@ export class LoomcycleClient {
236
343
  interactiveSession(opts) {
237
344
  const source = this.runStreaming({ ...opts, interactive: true });
238
345
  return new InteractiveSession(source, {
239
- sendRunInput: (rid, t) => this.sendRunInput(rid, t),
346
+ sendRunInput: (rid, t, o) => this.sendRunInput(rid, t, o),
347
+ retuneRun: (rid, ov) => this.retuneRun(rid, ov),
240
348
  cancelAgent: (aid) => this.cancelAgent(aid),
241
349
  });
242
350
  }
@@ -247,7 +355,8 @@ export class LoomcycleClient {
247
355
  attachInteractiveSession(runId, opts) {
248
356
  const source = this.streamRunByID(runId, opts);
249
357
  const session = new InteractiveSession(source, {
250
- sendRunInput: (rid, t) => this.sendRunInput(rid, t),
358
+ sendRunInput: (rid, t, o) => this.sendRunInput(rid, t, o),
359
+ retuneRun: (rid, ov) => this.retuneRun(rid, ov),
251
360
  cancelAgent: (aid) => this.cancelAgent(aid),
252
361
  });
253
362
  session.runId = runId;
package/dist/index.d.ts CHANGED
@@ -128,5 +128,5 @@ export { InteractiveSession } from "./interactive.js";
128
128
  export type { InteractiveSessionOps } from "./interactive.js";
129
129
  export { ClientToolHost, clientToolsURL, CLIENT_TOOL_SUBPROTOCOL } from "./client-tools.js";
130
130
  export type { ClientToolSchema, ClientToolInvocation, ConnectClientToolsOptions, WebSocketLike, WebSocketCtor, } from "./client-tools.js";
131
- export type { AgentEvent, ClientOptions, ContinueOptions, EventType, HostWidening, ImageMediaType, ParentContext, PromptContent, PromptSegment, RetryInfo, RunOptions, SamplingOptions, CompactionOptions, ToolUse, Usage, Agent, AgentStatus, AgentUsage, CancelAgentResult, ListAgentsResponse, RunBatchOptions, RunBatchResult, SpawnRunResult, CompactRunResult, DirectoryBudget, DirectoryInspection, DirectoryTenant, DirectoryUser, ErasureReport, ErasureResidue, ErasureResult, ErasureTier, CancelTurnResult, TranscriptEvent, TranscriptResponse, HealthResponse, ListUsersResponse, UserSummary, UserRecord, CreateUserBody, UpdateUserBody, MintedUserToken, UserTokenMeta, ListUserTokensResponse, RunnableAgent, RunnableAgentsResponse, WhoamiResponse, PauseResult, ResumeResult, RuntimeStateResponse, RuntimeStateStatus, ResolverMatrix, ResolverModelStatus, ResolverProviderAvailability, CreateSnapshotOptions, SnapshotCreateResponse, SnapshotDescriptor, SnapshotEnvelope, SnapshotListResponse, SnapshotRestoreResponse, MemoryEntriesResponse, MemoryEntry, MemoryEntryResponse, MemoryScopeIDsResponse, MemoryScopeIDSummary, MemoryScopeKind, MemoryScopesResponse, MemorySearchInput, MemorySource, MemoryWhen, MemoryTimeFilter, MemorySearchEntry, MemorySearchResponse, MemoryEmbedModelStats, MemoryEmbedStatsResponse, MemoryReembedConfigured, MemoryReembedResponse, MemoryBackfillResponse, MemoryPurgeResponse, InterruptListResponse, InterruptRow, InterruptStatus, ResolveInterruptOptions, Hook, HookFailMode, HookPhase, HookToolCall, HookToolResult, ListHooksResponse, PostHookCall, PostHookResult, PreHookCall, PreHookResult, RegisterHookOptions, RegisterHookResponse, SubstrateToolInput, SubstrateToolResponse, TeamNameSummary, ListTeamsResponse, TeamDiagram, CreatedTeam, PromotedTeam, RetiredTeam, TeamDefDetail, TeamVerification, TeamVersion, TeamVersionList, TeamBreakpoints, TeamRunDetached, TeamRunResult, TeamRunTarget, PathToolInput, PathToolResponse, DocumentToolInput, DocumentToolResponse, HistoryToolInput, HistoryToolResponse, VolumeMode, PersistentVolumeEntry, PersistentVolumesResponse, EphemeralVolumeEntry, EphemeralVolumesResponse, SystemPromptPayload, UserInputPayload, ChannelDescriptor, ListChannelsResponse, RunStateEvent, RunStateStreamClose, RunStateStreamItem, RunStateStreamOpen, StreamUserRunStatesOptions, AckChannelOptions, ChannelAckResult, ChannelMessageItem, ChannelPeekResult, ChannelPublishResult, ChannelPurgeResult, ChannelReleaseResult, ChannelScope, ChannelSubscribeResult, PeekChannelOptions, PublishChannelOptions, SubscribeChannelOptions, AwaitChannelsOptions, BroadcastChannelsOptions, ChannelAwaitEntry, ChannelAwaitMode, ChannelAwaitResult, ChannelBroadcastEntry, ChannelBroadcastResult, CreateChannelOptions, ReleaseChannelOptions, 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, UsageDimension, UsageAggregate, UsageReportResponse, LimitInfo, TokenLimit, TokenLimitsResponse, ConfigResponse, ConfigProvider, ConfigModel, ConfigSearch, ConfigInstance, SetTokenLimitRequest, CredentialScope, CredentialMeta, CredentialListResponse, } from "./types.js";
131
+ export type { AgentEvent, ClientOptions, ContinueOptions, EventType, HostWidening, ImageMediaType, ParentContext, PromptContent, PromptSegment, RetryInfo, RunOptions, SamplingOptions, CompactionOptions, ToolUse, Usage, Agent, AgentStatus, AgentUsage, CancelAgentResult, ListAgentsResponse, RunBatchOptions, RunBatchResult, SpawnRunResult, CompactRunResult, DirectoryBudget, DirectoryInspection, DirectoryTenant, DirectoryUser, ErasureReport, ErasureResidue, ErasureResult, ErasureTier, CancelTurnResult, TranscriptEvent, TranscriptResponse, HealthResponse, ListUsersResponse, UserSummary, UserRecord, CreateUserBody, UpdateUserBody, MintedUserToken, UserTokenMeta, ListUserTokensResponse, RunnableAgent, RunnableAgentsResponse, WhoamiResponse, PauseResult, ResumeResult, RuntimeStateResponse, RuntimeStateStatus, ResolverMatrix, ResolverModelStatus, ResolverProviderAvailability, CreateSnapshotOptions, SnapshotCreateResponse, SnapshotDescriptor, SnapshotEnvelope, SnapshotListResponse, SnapshotRestoreResponse, MemoryEntriesResponse, MemoryEntry, MemoryEntryResponse, MemoryScopeIDsResponse, MemoryScopeIDSummary, MemoryScopeKind, MemoryScopesResponse, MemorySearchInput, MemorySource, MemoryWhen, MemoryTimeFilter, MemorySearchEntry, MemorySearchResponse, MemoryEmbedModelStats, MemoryEmbedStatsResponse, MemoryReembedConfigured, MemoryReembedResponse, MemoryBackfillResponse, MemoryPurgeResponse, InterruptListResponse, InterruptRow, InterruptStatus, ResolveInterruptOptions, Hook, HookFailMode, HookPhase, HookToolCall, HookToolResult, ListHooksResponse, PostHookCall, PostHookResult, PreHookCall, PreHookResult, RegisterHookOptions, RegisterHookResponse, SubstrateToolInput, SubstrateToolResponse, TeamNameSummary, ListTeamsResponse, TeamDiagram, CreatedTeam, PromotedTeam, RetiredTeam, TeamDefDetail, TeamVerification, TeamVersion, TeamVersionList, TeamBreakpoints, TeamRunDetached, TeamRunResult, TeamRunTarget, PathToolInput, PathToolResponse, DocumentToolInput, DocumentToolResponse, HistoryToolInput, HistoryToolResponse, VolumeMode, PersistentVolumeEntry, PersistentVolumesResponse, EphemeralVolumeEntry, EphemeralVolumesResponse, SystemPromptPayload, UserInputPayload, ChannelDescriptor, ListChannelsResponse, RunStateEvent, RunStateStreamClose, RunStateStreamItem, RunStateStreamOpen, StreamUserRunStatesOptions, AckChannelOptions, ChannelAckResult, ChannelMessageItem, ChannelPeekResult, ChannelPublishResult, ChannelPurgeResult, ChannelReleaseResult, ChannelScope, ChannelSubscribeResult, PeekChannelOptions, PublishChannelOptions, SubscribeChannelOptions, AwaitChannelsOptions, BroadcastChannelsOptions, ChannelAwaitEntry, ChannelAwaitMode, ChannelAwaitResult, ChannelBroadcastEntry, ChannelBroadcastResult, CreateChannelOptions, ReleaseChannelOptions, 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, UsageDimension, UsageAggregate, UsageReportResponse, CapabilityInertInfo, LimitInfo, EffectiveConfigResponse, EffectiveConfigSource, EffectiveValue, RetuneRunResponse, RunConfigRecord, RunConfigResponse, TokenLimit, TokenLimitsResponse, ConfigResponse, ConfigProvider, ConfigModel, ConfigSearch, ConfigInstance, SetTokenLimitRequest, CredentialScope, CredentialMeta, CredentialListResponse, } from "./types.js";
132
132
  export { AgentIDInUseError, AgentNotFoundError, AlreadyPausingError, AuthError, BackpressureError, HookNotFoundError, NotFoundError, InvalidArgumentError, ChannelCursorRegressionError, LoomcycleError, NotPausedError, PauseNotConfiguredError, PerUserQuotaExhaustedError, SessionBusyError, SessionNotFoundError, SnapshotNotFoundError, SnapshotTooLargeError, SnapshotVersionError, SubstrateToolRefusedError, UnavailableError, } from "./errors.js";
@@ -1,11 +1,16 @@
1
- import type { AgentEvent } from "./types.js";
1
+ import type { AgentEvent, RunOverrideOptions } from "./types.js";
2
2
  /** The client-side operations an {@link InteractiveSession} routes through.
3
3
  * Supplied by LoomcycleClient.interactiveSession / attachInteractiveSession —
4
4
  * the session itself holds no transport logic. */
5
5
  export interface InteractiveSessionOps {
6
- sendRunInput: (runId: string, text: string) => Promise<{
6
+ sendRunInput: (runId: string, text: string, opts?: {
7
+ overrides?: RunOverrideOptions;
8
+ }) => Promise<{
7
9
  delivered: boolean;
8
10
  }>;
11
+ retuneRun: (runId: string, overrides: RunOverrideOptions) => Promise<{
12
+ retuned: boolean;
13
+ }>;
9
14
  cancelAgent: (agentId: string) => Promise<unknown>;
10
15
  }
11
16
  /** A high-level driver for an interactive agentic session (RFC AI) — the
@@ -46,7 +51,19 @@ export declare class InteractiveSession {
46
51
  * flag. Throws if the run_id isn't known yet — for a fresh session, consume
47
52
  * `events()` until the `agent` frame (or the first `awaiting_input`) first;
48
53
  * a re-attached session has the run_id up front. */
49
- send(text: string): Promise<boolean>;
54
+ send(text: string, opts?: {
55
+ overrides?: RunOverrideOptions;
56
+ }): Promise<boolean>;
57
+ /** Change this run's settings WITHOUT sending it a turn — switch a parked
58
+ * chat to another model, raise its iteration bound.
59
+ *
60
+ * `send(text, { overrides })` is the other half: retune and speak in one
61
+ * atomic call. Use THIS one when a message in the transcript would be an
62
+ * artefact of changing a setting rather than something the operator said.
63
+ *
64
+ * Does not clear `awaitingInput`: a parked run is still parked after a
65
+ * retune, because nothing was delivered for it to answer. */
66
+ retune(overrides: RunOverrideOptions): Promise<boolean>;
50
67
  /** Cancel the run (and its sub-agents). No-op if the agent_id isn't known
51
68
  * yet (nothing to cancel). */
52
69
  cancel(): Promise<void>;
@@ -55,14 +55,30 @@ export class InteractiveSession {
55
55
  * flag. Throws if the run_id isn't known yet — for a fresh session, consume
56
56
  * `events()` until the `agent` frame (or the first `awaiting_input`) first;
57
57
  * a re-attached session has the run_id up front. */
58
- async send(text) {
58
+ async send(text, opts) {
59
59
  if (!this.runId) {
60
60
  throw new Error("loomcycle: run_id not known yet — consume events() until the `agent` frame before send()");
61
61
  }
62
- const { delivered } = await this.ops.sendRunInput(this.runId, text);
62
+ const { delivered } = await this.ops.sendRunInput(this.runId, text, opts);
63
63
  this.awaitingInput = false;
64
64
  return delivered;
65
65
  }
66
+ /** Change this run's settings WITHOUT sending it a turn — switch a parked
67
+ * chat to another model, raise its iteration bound.
68
+ *
69
+ * `send(text, { overrides })` is the other half: retune and speak in one
70
+ * atomic call. Use THIS one when a message in the transcript would be an
71
+ * artefact of changing a setting rather than something the operator said.
72
+ *
73
+ * Does not clear `awaitingInput`: a parked run is still parked after a
74
+ * retune, because nothing was delivered for it to answer. */
75
+ async retune(overrides) {
76
+ if (!this.runId) {
77
+ throw new Error("loomcycle: run_id not known yet — consume events() until the `agent` frame before retune()");
78
+ }
79
+ const { retuned } = await this.ops.retuneRun(this.runId, overrides);
80
+ return retuned;
81
+ }
66
82
  /** Cancel the run (and its sub-agents). No-op if the agent_id isn't known
67
83
  * yet (nothing to cancel). */
68
84
  async cancel() {
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" | "awaiting_input" | "steer" | "context_compaction" | "limit" | "_meta";
10
+ export type EventType = "started" | "text" | "tool_call" | "tool_result" | "usage" | "done" | "error" | "retry" | "host_widened" | "session" | "agent" | "awaiting_input" | "steer" | "context_compaction" | "capability_inert" | "limit" | "override" | "_meta";
11
11
  export interface ToolUse {
12
12
  id: string;
13
13
  name: string;
@@ -60,6 +60,131 @@ export interface HostWidening {
60
60
  * scope stands against its ceiling — so a UI can render "tenant acme at 1.2M /
61
61
  * 1M tokens this month" without a follow-up fetch. Wire-stable; mirrors
62
62
  * providers.LimitInfo. */
63
+ /** A run's own stored overrides — what the RUN set, not what it will
64
+ * effectively use. Mirrors the `config` object on `GET /v1/runs/{id}/config`
65
+ * and on the `retuneRun` reply.
66
+ *
67
+ * A field absent here means "this run does not override it", which is a
68
+ * different and more useful answer at this layer than a resolved value would
69
+ * be: it says the definition, the tier or a driver still decides. Use
70
+ * {@link LoomcycleClient.getEffectiveConfig} for the resolved view. */
71
+ export interface RunConfigRecord {
72
+ sampling?: Record<string, unknown>;
73
+ compaction?: Record<string, unknown>;
74
+ context?: Record<string, unknown>;
75
+ max_context_tokens?: number;
76
+ run_timeout_seconds?: number;
77
+ routing?: {
78
+ provider?: string;
79
+ model?: string;
80
+ tier?: string;
81
+ effort?: string;
82
+ };
83
+ resources?: {
84
+ max_tokens?: number;
85
+ max_iterations?: number;
86
+ unbounded_iterations?: boolean;
87
+ max_concurrent_children?: number;
88
+ };
89
+ tuning?: {
90
+ retry_attempts?: number;
91
+ memory_inject_max_tokens?: number;
92
+ memory_index_max_bytes?: number;
93
+ inject_tool_guide?: boolean;
94
+ };
95
+ interactive?: boolean;
96
+ interruption?: {
97
+ enabled?: boolean;
98
+ kinds?: string[];
99
+ max_pending?: number;
100
+ };
101
+ hosts?: Record<string, unknown>;
102
+ }
103
+ /** Which layer decided an effective value.
104
+ *
105
+ * This is the half that makes the report worth fetching. `max_iterations: 16`
106
+ * cannot distinguish a deliberate setting from a default nobody chose, and
107
+ * those call for opposite actions — so every field carries where it came from.
108
+ *
109
+ * - `run` — a per-run override set it
110
+ * - `definition` — the agent definition set it
111
+ * - `user_tier` — operator tier policy (e.g. retry_attempts)
112
+ * - `operator` — operator env / global configuration
113
+ * - `resolved` — decided at runtime: the tier cascade, the driver, the model.
114
+ * A `resolved` field with a null value means the runtime
115
+ * settles it somewhere this report cannot see, which is a
116
+ * more honest answer than omitting the field.
117
+ * - `default` — a fixed constant in the runtime */
118
+ export type EffectiveConfigSource = "run" | "definition" | "user_tier" | "operator" | "resolved" | "default";
119
+ /** One field's effective value plus the layer that decided it. */
120
+ export interface EffectiveValue {
121
+ value: unknown;
122
+ source: EffectiveConfigSource;
123
+ }
124
+ /** The reply from `GET /v1/runs/{run_id}/config` — what the RUN overrides. */
125
+ export interface RunConfigResponse {
126
+ run_id: string;
127
+ agent: string;
128
+ /** The model the run last resolved to, as recorded on the run row. */
129
+ model: string;
130
+ config: RunConfigRecord;
131
+ }
132
+ /** The reply from `GET /v1/runs/{run_id}/effective-config` — for every
133
+ * overridable field, the value this run will actually use and which layer
134
+ * decided it.
135
+ *
136
+ * Keyed by the wire name the rest of the API uses (`max_tokens`, not
137
+ * `maxTokens`), so it joins directly against a definition from
138
+ * `/v1/_library/agents`. */
139
+ export interface EffectiveConfigResponse {
140
+ run_id: string;
141
+ agent: string;
142
+ fields: Record<string, EffectiveValue>;
143
+ }
144
+ /** The reply from `retuneRun` — the run's MERGED configuration, not an echo of
145
+ * the request.
146
+ *
147
+ * A caller cannot recompute it: the merge is not a field-wise union. Naming a
148
+ * `model` clears the `provider` so a previous choice cannot contradict the new
149
+ * pin, and naming a `tier` clears the `model`. */
150
+ export interface RetuneRunResponse {
151
+ run_id: string;
152
+ retuned: boolean;
153
+ config: RunConfigRecord;
154
+ }
155
+ /** OverrideInfo accompanies an `event: override` frame (RFC DC per-run
156
+ * overrides): a run's own configuration changed mid-run because an operator
157
+ * retuned it.
158
+ *
159
+ * TWO EVENTS CARRY THIS TYPE, and a consumer needs to tell them apart because
160
+ * one retune can produce both. The server emits one when the operator acts,
161
+ * listing in `fields` the keys the REQUEST set and carrying NO from/to pair —
162
+ * nothing has been re-resolved yet, so there is no honest "to" to report. The
163
+ * runtime emits one when the run ADOPTS a routing change, and that one always
164
+ * carries both halves of the pair.
165
+ *
166
+ * So: a pair present means "the run is now using this"; a pair absent means
167
+ * "an operator asked for these fields". Filter on `from_model === undefined`
168
+ * for the second kind.
169
+ *
170
+ * `fields` used to be documented as the request's keys unconditionally while
171
+ * the only site filling it in was the runtime's, which cannot see a request —
172
+ * so a branch written for "max_tokens changed" could never run.
173
+ *
174
+ * Carries only operator-chosen configuration whose effects are already visible
175
+ * — a model name, a budget. No credential, no operator host. */
176
+ export interface OverrideInfo {
177
+ /** Who changed it. "operator" today; present so a later automatic retune is
178
+ * distinguishable rather than indistinguishable. */
179
+ source: string;
180
+ /** "provider/model" before the change. Absent when routing did not move. */
181
+ from_model?: string;
182
+ /** "provider/model" after the change. */
183
+ to_model?: string;
184
+ /** The override keys the request actually set — so a budget or tuning change
185
+ * that moved no model is still legible. */
186
+ fields?: string[];
187
+ }
63
188
  /** The machine-readable half of a terminal run failure, carried on
64
189
  * `event: error` frames.
65
190
  *
@@ -80,6 +205,21 @@ export interface ErrorInfo {
80
205
  * instructions. */
81
206
  retry_after_ms?: number;
82
207
  }
208
+ /** The payload on a `capability_inert` event: one tool the agent holds and
209
+ * cannot use, because the capability gate that tool reads grants nothing.
210
+ *
211
+ * Server-generated, emitted ONCE at run start — the condition is a property of
212
+ * the definition, not of any one call. It carries the FIX as well as the fact:
213
+ * the governing yaml key is not guessable from the tool name, which is most of
214
+ * why this failure was hard to act on. */
215
+ export interface CapabilityInertInfo {
216
+ /** The granted tool, as named in the agent's `tools` list. */
217
+ tool: string;
218
+ /** The yaml key that governs it, e.g. `agent_def_scopes`. */
219
+ gate: string;
220
+ /** A line naming the tool, the gate, and what to set. */
221
+ message?: string;
222
+ }
83
223
  export interface LimitInfo {
84
224
  /** Which axis tripped: "operator" | "tenant" | "user". */
85
225
  scope: string;
@@ -128,6 +268,11 @@ export interface AgentEvent {
128
268
  /** Payload on `event: limit` (RFC AW) — a per-scope token-budget crossing.
129
269
  * Nil on all other event types. */
130
270
  limit?: LimitInfo;
271
+ /** Set on `capability_inert` events: a tool the agent holds and cannot use. */
272
+ capability_inert?: CapabilityInertInfo;
273
+ /** Set on `event: override` frames — what the operator changed, and from
274
+ * what to what. */
275
+ override?: OverrideInfo;
131
276
  /** Payload on `event: error` — the classification of a TERMINAL run failure.
132
277
  * Absent on every other event type, and absent for a failure the runtime
133
278
  * cannot categorise: there is deliberately no "unknown" category, so a
@@ -161,7 +306,80 @@ export interface PromptSegment {
161
306
  role: "system" | "user";
162
307
  content: PromptContent[];
163
308
  }
164
- export interface RunOptions {
309
+ /** The per-run overrides (RFC DC) — a run's own answer to how it should run,
310
+ * instead of its agent definition's. Persisted with the run, so they survive a
311
+ * pause. A parked run is retuned with `retuneRun()` — or with
312
+ * `sendRunInput(id, text, { overrides })` when you also want to say something.
313
+ *
314
+ * This line used to say "can also be retuned via `sendRunInput`", which was
315
+ * true of the WIRE and not of the method: the adapter sent a bare `{text}`, so
316
+ * the sentence described a capability no caller could reach through it. A
317
+ * comment that names the transport instead of the API is how a gap reads as
318
+ * closed.
319
+ *
320
+ * Declared ONCE and extended by both {@link RunOptions} and
321
+ * {@link ContinueOptions}, because the serializer that writes them is shared
322
+ * by both paths — a copy on one interface and not the other compiles as a type
323
+ * error at best and drops the caller's value at worst.
324
+ */
325
+ export interface RunOverrideOptions {
326
+ /** Run on a specific model. Must be one the agent's definition already
327
+ * allows — an override selects WITHIN that set and cannot widen it, so
328
+ * which vendor sees the conversation stays an operator decision. Naming a
329
+ * model PINS it: the tier stops choosing and stops falling back. */
330
+ model?: string;
331
+ /** Run on a specific provider. Must be one the definition already allows.
332
+ * Unlike `model` this NARROWS rather than pins — the tier still chooses the
333
+ * model within that vendor and still falls back within it. */
334
+ provider?: string;
335
+ /** Route through a different configured tier. */
336
+ tier?: string;
337
+ /** Reasoning-effort hint. Any value outside low|medium|high is REFUSED
338
+ * rather than ignored: "effort was dropped" and "effort was applied" look
339
+ * identical from the outside. */
340
+ effort?: "low" | "medium" | "high";
341
+ /** Per-reply output cap. May be RAISED above the agent's own. */
342
+ maxTokens?: number;
343
+ /** Loop bound. May be RAISED above the agent's own. */
344
+ maxIterations?: number;
345
+ /** Lift or restore the loop bound. `false` bounds an otherwise-unbounded
346
+ * agent for this run only — which is why it is a boolean you can set rather
347
+ * than a flag you can only turn on. */
348
+ unboundedIterations?: boolean;
349
+ /** How wide this run may fan out into sub-agents. May only be LOWERED below
350
+ * what the definition allows; raising it is refused, because a child takes
351
+ * no admission slot and is not budget-checked at spawn, so this is the only
352
+ * bound on fan-out that exists. */
353
+ maxConcurrentChildren?: number;
354
+ /** How many times to retry the same provider before falling back. 0 disables
355
+ * retrying for this run. */
356
+ retryAttempts?: number;
357
+ /** Token budget for memory injected into the prompt. 0 injects none. */
358
+ memoryInjectMaxTokens?: number;
359
+ /** Byte budget for the memory index. 0 omits it. */
360
+ memoryIndexMaxBytes?: number;
361
+ /** Whether to inject the generated tool guide into the prompt. */
362
+ injectToolGuide?: boolean;
363
+ /** Park this run at its turn boundaries instead of finishing, so an operator
364
+ * can steer it — settable while the run is ALREADY GOING, which is the point:
365
+ * nobody knows at start that they will need to correct it.
366
+ *
367
+ * `false` releases a run that was started interactive. Omit to keep whatever
368
+ * the run has; a retune of anything else must not disturb this. */
369
+ interactive?: boolean;
370
+ /** Let this run's agent ASK a human a question, overriding what its
371
+ * definition allows.
372
+ *
373
+ * Overridable because an interruption touches no data and no host — it blocks
374
+ * and waits for a person — so the exposure is liveness, bounded by the run's
375
+ * timeout and the interruption's own. */
376
+ interruption?: {
377
+ enabled?: boolean;
378
+ kinds?: string[];
379
+ max_pending?: number;
380
+ };
381
+ }
382
+ export interface RunOptions extends RunOverrideOptions {
165
383
  agent: string;
166
384
  segments: PromptSegment[];
167
385
  tools?: string[];
@@ -338,7 +556,7 @@ export interface ParentContext {
338
556
  * position and not an absent one. */
339
557
  wave_index?: number;
340
558
  }
341
- export interface ContinueOptions {
559
+ export interface ContinueOptions extends RunOverrideOptions {
342
560
  /** Required — the session to continue. */
343
561
  sessionId: string;
344
562
  segments: PromptSegment[];
@@ -2171,6 +2389,12 @@ export interface AgentDefOverlay {
2171
2389
  tools?: string[];
2172
2390
  skills?: string[];
2173
2391
  memory_scopes?: string[];
2392
+ /** RFC DF: recall attaches the conversation TURN each fact was distilled from,
2393
+ * for this agent, WITHOUT the model asking for it. Operator-set on purpose — a
2394
+ * tool parameter is a decision the model makes, and measured across three local
2395
+ * models they do not make it. Also gated by `history_scope`: this decides whether
2396
+ * turns are OFFERED, history_scope whether they may be READ. */
2397
+ recall_include_turns?: boolean;
2174
2398
  memory_quota_bytes?: number;
2175
2399
  memory_backend?: string;
2176
2400
  retry_attempts?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loomcycle/client",
3
- "version": "1.81.0",
3
+ "version": "1.83.0",
4
4
  "description": "TypeScript client for the loomcycle sidecar (HTTP+SSE). 71 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). v1.4.0 — RFC AL Path VFS + RFC AK Document on the wire: path(input) (POST /v1/_path — a Unix-like filesystem over Memory/Volumes/Documents; resolve/ls/stat/mkdir/mv/rm) + document(input) (POST /v1/_document — chunked-graph documents; 13 ops, needs SQL Memory on the sidecar). Scope (agent/user/tenant) + tenant are resolved server-side from the authenticated principal, never the wire; an off-run scope:'user' op keys on the principal subject so it interoperates with that user's agent runs. New PathToolInput / DocumentToolInput types; responses are op-varying (unknown — narrow as needed). v1.7.0 — RFC AT image/vision input: the PromptContent union gains an `image` variant ({ type:'image'; media_type: ImageMediaType; data: base64-no-prefix }) accepted in a user segment by runStreaming / continueSession (segments pass through unchanged — no method change). The model must be vision-capable or the run errors before the call. New ImageMediaType type (image/png|jpeg|gif|webp). v1.12.1 — Path/Document browse-by-subject + the full Document op set (RFC AS/AK): path(input, opts) / document(input, opts) accept optional scopeId / tenant browse overrides sent as ?scope_id= / ?tenant= query params (server reads them from the URL, re-checks authorization; omit both to browse your own subject — byte-identical to the pre-RFC-AS request), and DocumentToolInput.op now covers all 16 backend ops (adds set_path, export_md, import_md) with the matching include_metadata / markdown fields. Additive — existing path() / document() callers are unchanged. v1.16.0 — RFC BC client-executed tools (local tool host): connectClientTools({tools,onInvoke}) opens a persistent WebSocket to /v1/client-tools, registers tools this client runs on the user's machine (browser DOM / files / shell), and answers the invoke frames loomcycle routes when an agent of your principal calls a client:<tool> — returning your reply as an ordinary tool result. Returns a ClientToolHost driver (.close() to stop; auto-reconnect); dependency-free (global WebSocket in browsers / Node 22+, or an injected WebSocketImpl like the ws package on older Node); the bearer rides the Sec-WebSocket-Protocol subprotocol (browsers can't set an Authorization header on a WebSocket). The runtime's first WebSocket surface. v1.45.0 — RFC BL P5 subject erasure: erasureReport(subject, {tenant}) (GET /v1/_erasure — what this deployment holds about one subject, in three tiers: deletable with existing primitives, subject-keyed but not deletable, and facts ABOUT the subject in scopes they do not own) + erasureExecute(subject, {dryRun, confirm, tenant}) (POST /v1/_erasure — removes tiers 1 and 2). DEFAULTS TO A DRY RUN: dryRun defaults true and a live run also requires confirm === subject. The tier-3 residue is traceable only through the subject's chats, which a live run deletes, so a report afterwards shows rows: 0 while those facts remain — the returned object is the only durable record of what was not reached; persist it. New ErasureReport / ErasureResult / ErasureTier / ErasureResidue types. v1.47.0 — RFC BV memory-view SDK: memorySearch() (POST /v1/_memory/search — off-run unified semantic search spanning k/v entries AND document-chunk bodies in one ranked list, each hit tagged kind memory|document with chunk_id on document hits) + memoryEmbedStats(scope) + reembedMemory(scope, scopeId, {dryRun,limit}) (the Vector Memory embed-admin reads the memory-view console needs; dry_run defaults true). Fact reads (list_facts + get_chunk's entity block) ride the existing document() passthrough. Additive — existing callers unchanged. v1.61.0 — RFC CJ per-run context-window override: runStreaming / continueSession accept an optional maxContextTokens (integer tokens) that wins over the agent's own max_context_tokens; omitted inherits it (which defers to the provider/driver default). Distinct from a model's output cap; primarily for local inference (Ollama num_ctx). Serialized as the snake_case max_context_tokens field on POST /v1/runs + the continuation body; additive, existing callers unchanged. v1.72.1 — the TeamDef version lifecycle: listTeamVersions(name) (op=list — every version of one team, newest first), promoteTeam(defId) (op=promote — point the active pointer, which is what a run BY NAME executes; forkTeam defaults to promote:false, so authoring and putting in force stay two steps), retireTeam(defId, retired) (op=retire — reversible and version-scoped, unlike deleteTeam) and verifyTeam(name, contentSha256) (op=verify — the drift check for a workflow kept in source control and pushed to several deployments; an absent team answers deployed:false rather than raising). The ops existed on the substrate and over HTTP; a client that could author a team could not put one in force.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",