@demicodes/agent 0.15.0 → 0.17.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.
@@ -1,4 +1,4 @@
1
- import { b as AgentMetadata, d as TranscriptPatch, l as ServerFrame, n as AgentServerTransport, o as ClientFrame, r as AgentTransport, s as ClientSessionEvent, t as AgentClientTransport, u as ShellCommandStatusLike } from "./transport-gkyOzXn-.mjs";
2
- import { a as AgentClient, i as AgentActionOptions, n as createWebSocketClientTransport, o as AgentClientListener, r as createWebSocketServerTransport, t as JsonWebSocket } from "./websocket-transport-C6bu3Ufc.mjs";
1
+ import { b as AgentMetadata, d as TranscriptPatch, l as ServerFrame, n as AgentServerTransport, o as ClientFrame, r as AgentTransport, s as ClientSessionEvent, t as AgentClientTransport, u as ShellCommandStatusLike } from "./transport-DtBCCS_A.mjs";
2
+ import { a as AgentClient, i as AgentActionOptions, n as createWebSocketClientTransport, o as AgentClientListener, r as createWebSocketServerTransport, t as JsonWebSocket } from "./websocket-transport-DdfGgBjO.mjs";
3
3
  import { ProviderSelection } from "@demicodes/provider";
4
4
  export { type AgentActionOptions, AgentClient, type AgentClientListener, type AgentClientTransport, type AgentMetadata, type AgentServerTransport, type AgentTransport, type ClientFrame, type ClientSessionEvent, type JsonWebSocket, type ProviderSelection, type ServerFrame, type ShellCommandStatusLike, type TranscriptPatch, createWebSocketClientTransport, createWebSocketServerTransport };
@@ -1,2 +1,2 @@
1
- import { n as createWebSocketServerTransport, r as AgentClient, t as createWebSocketClientTransport } from "./websocket-transport-BjSj3U0I.mjs";
1
+ import { n as createWebSocketServerTransport, r as AgentClient, t as createWebSocketClientTransport } from "./websocket-transport-c6P3NBuf.mjs";
2
2
  export { AgentClient, createWebSocketClientTransport, createWebSocketServerTransport };
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { A as AgentTool, B as TranscriptOptions, C as AgentSessionCheckpoint, D as AgentSessionRestoreParams, E as AgentSessionParams, F as SessionEvent, G as resolveRetryPolicy, H as DEFAULT_TURN_RETRY_POLICY, I as SessionEventListener, K as retryDelayMs, L as CompactionWindow, M as AgentToolInvokeContext, N as AgentToolInvokeResult, O as AgentSessionStore, P as ExternalMutationReservation, R as DrainedTranscriptPatches, S as AgentReferenceResolveContext, T as AgentSessionOptions, U as TurnRetryPolicy, V as estimateTranscriptBlockTokens, W as isRetryableCode, _ as AgentHarnessRuntime, a as createInProcessTransportPair, b as AgentMetadata, c as ConversationSummary, d as TranscriptPatch, f as AbortResult, g as AgentHarnessContext, h as AgentHarness, i as InProcessTransportPair, j as AgentToolContext, k as AgentSystemPromptContext, l as ServerFrame, m as AgentDisposeContext, n as AgentServerTransport, o as ClientFrame, p as AbortTarget, r as AgentTransport, s as ClientSessionEvent, t as AgentClientTransport, u as ShellCommandStatusLike, v as AgentHostContext, w as AgentSessionCloneParams, x as AgentPromptContext, y as AgentLifecycleEvent, z as TranscriptLog } from "./transport-gkyOzXn-.mjs";
2
- import { a as AgentClient, i as AgentActionOptions, n as createWebSocketClientTransport, o as AgentClientListener, r as createWebSocketServerTransport, t as JsonWebSocket } from "./websocket-transport-C6bu3Ufc.mjs";
1
+ import { A as AgentTool, B as TranscriptLog, C as AgentSessionCheckpoint, D as AgentSessionRestoreParams, E as AgentSessionParams, F as ModelSwitchApply, G as isRetryableCode, H as estimateTranscriptBlockTokens, I as SessionEvent, K as resolveRetryPolicy, L as SessionEventListener, M as AgentToolInvokeContext, N as AgentToolInvokeResult, O as AgentSessionStore, P as ExternalMutationReservation, R as CompactionWindow, S as AgentReferenceResolveContext, T as AgentSessionOptions, U as DEFAULT_TURN_RETRY_POLICY, V as TranscriptOptions, W as TurnRetryPolicy, _ as AgentHarnessRuntime, a as createInProcessTransportPair, b as AgentMetadata, c as ConversationSummary, d as TranscriptPatch, f as AbortResult, g as AgentHarnessContext, h as AgentHarness, i as InProcessTransportPair, j as AgentToolContext, k as AgentSystemPromptContext, l as ServerFrame, m as AgentDisposeContext, n as AgentServerTransport, o as ClientFrame, p as AbortTarget, q as retryDelayMs, r as AgentTransport, s as ClientSessionEvent, t as AgentClientTransport, u as ShellCommandStatusLike, v as AgentHostContext, w as AgentSessionCloneParams, x as AgentPromptContext, y as AgentLifecycleEvent, z as DrainedTranscriptPatches } from "./transport-DtBCCS_A.mjs";
2
+ import { a as AgentClient, i as AgentActionOptions, n as createWebSocketClientTransport, o as AgentClientListener, r as createWebSocketServerTransport, t as JsonWebSocket } from "./websocket-transport-DdfGgBjO.mjs";
3
3
  import { BashAuditEvent, BashEnvironment, BashEnvironmentOptions, CommandMetadataRecord, Host, ShellCommandStatus, ShellOutputChunk } from "@demicodes/shell";
4
4
  import { AgentProvider, Provider } from "@demicodes/provider";
5
5
  import { Block, Model, ModelMediaKind, ModelSelection, ProviderErrorDiagnostics, QueuedMessage, SessionPhase, UserContentBlock } from "@demicodes/core";
@@ -48,6 +48,7 @@ declare class AgentSession<State> {
48
48
  private readonly store?;
49
49
  private readonly compactionKeepRecentTokens;
50
50
  private readonly compactionThresholdRatio;
51
+ private readonly compactionThresholdTokens;
51
52
  private readonly listeners;
52
53
  private readonly pendingActions;
53
54
  private readonly queued;
@@ -122,18 +123,19 @@ declare class AgentSession<State> {
122
123
  }): Promise<void>;
123
124
  cancelPendingSteer(id: string): boolean;
124
125
  /**
125
- * Switches the model (and optionally the provider) for the rest of the session. Queued as an
126
- * action so it lands at a turn boundary, never mid-tool-continuation; the next turn uses it.
127
- * A non-null `provider` replaces the current one (its predecessor is disposed); pass null to
128
- * keep the same provider instance and only change the model.
129
- */
130
- /**
131
- * Records a model/provider switch. It is applied at the next turn's preflight (see
132
- * applyPendingModelSwitch): if the new model can't hold the history, compaction runs there
133
- * with the current (pre-switch) model first, then the swap happens. Recording is cheap and
134
- * non-blocking so it never holds up an in-flight turn or other frames.
126
+ * Records a model/provider switch for the rest of the session. A non-null `provider`
127
+ * replaces the current one (its predecessor is disposed); pass null to keep the same
128
+ * provider instance and only change the model.
129
+ *
130
+ * `apply` picks the boundary the switch lands on: 'next_turn' (default) waits for the
131
+ * start of the next queued action, so a running turn finishes entirely on the old model;
132
+ * 'immediate' also applies mid-turn at the next sampling/tool continuation, so the very
133
+ * next request already runs on the new model (the same "as soon as possible" semantics
134
+ * steering has). Either way, if the new model can't hold the history, compaction runs
135
+ * first with the current (pre-switch) model, then the swap happens. Recording is cheap
136
+ * and non-blocking so it never holds up an in-flight turn or other frames.
135
137
  */
136
- updateModel(provider: AgentProvider | null, model: ModelSelection): void;
138
+ updateModel(provider: AgentProvider | null, model: ModelSelection, apply?: ModelSwitchApply): void;
137
139
  abort(): Promise<AbortResult>;
138
140
  scheduleYieldWakeup(durationMs: number, metadata?: AgentMetadata | null): AgentToolInvokeResult;
139
141
  waitUntilDone(): Promise<void>;
@@ -169,13 +171,20 @@ declare class AgentSession<State> {
169
171
  private runWorker;
170
172
  private executeAction;
171
173
  /**
172
- * Applies a queued model/provider switch at a turn boundary (preflight). If the new model's
173
- * context window can't hold the current history, compaction runs FIRST with the current
174
- * (pre-switch) model + provider — which can still load it to summarize — and only then do we
175
- * swap. Doing it the other way would ask the smaller model to summarize a history it may not
176
- * be able to load.
174
+ * Applies a queued model/provider switch at an action boundary (turn preflight). If the
175
+ * new model's context window can't hold the current history, compaction runs FIRST with
176
+ * the current (pre-switch) model + provider — which can still load it to summarize — and
177
+ * only then do we swap. Doing it the other way would ask the smaller model to summarize
178
+ * a history it may not be able to load. Returns whether that compaction actually ran
179
+ * (false when no switch was pending).
177
180
  */
178
181
  private applyPendingModelSwitch;
182
+ /**
183
+ * Turn-loop variant for mid-turn continuation boundaries: only switches recorded with
184
+ * apply 'immediate' land here; 'next_turn' switches stay pending until the running turn
185
+ * finishes and the next action starts.
186
+ */
187
+ private applyImmediateModelSwitch;
179
188
  private runWithCompactingPhase;
180
189
  private executeSend;
181
190
  private resolveReferences;
@@ -231,6 +240,7 @@ interface AgentServerSessionOptions {
231
240
  compaction?: {
232
241
  keepRecentTokens?: number;
233
242
  preflightThresholdRatio?: number;
243
+ preflightThresholdTokens?: number;
234
244
  };
235
245
  persistIntervalMs?: number;
236
246
  }
@@ -375,4 +385,4 @@ interface ShellToolView {
375
385
  declare function finishShellToolResult<State>(environment: BashEnvironment, result: ShellCommandStatus, ctx: AgentToolInvokeContext<State>): Promise<AgentToolInvokeResult>;
376
386
  declare function shellCommandHandleRequired(result: ShellCommandStatus, budgetTokens: number): boolean;
377
387
  //#endregion
378
- export { AbortResult, AbortTarget, ActiveTurnPhase, AgentActionOptions, AgentClient, AgentClientListener, AgentClientTransport, AgentDisposeContext, AgentHarness, AgentHarnessContext, AgentHarnessRuntime, AgentHostContext, AgentLifecycleEvent, AgentMetadata, AgentPromptContext, AgentReferenceResolveContext, AgentServer, AgentServerOptions, AgentServerSessionOptions, AgentServerTransport, AgentSession, AgentSessionCheckpoint, AgentSessionCloneParams, AgentSessionOptions, AgentSessionParams, AgentSessionRestoreParams, AgentSessionStore, AgentSystemPromptContext, AgentTool, AgentToolContext, AgentToolInvokeContext, AgentToolInvokeResult, AgentTransport, AgentTransportBinding, ClientFrame, ClientSessionEvent, CompactionWindow, ConversationSummary, DEFAULT_MAX_MEDIA_BYTES, DEFAULT_TURN_RETRY_POLICY, DrainedTranscriptPatches, ExternalMutationReservation, InProcessTransportPair, JsonWebSocket, PrepareShell, PrepareShellContext, ProviderStreamError, ResumePoint, RunCommandLineCommandNotRegisteredError, RunCommandLineOptions, RunCommandLineResult, RunCommandLineShellNotFoundError, RunCommandLineTimeoutError, SHELL_VIEW_MAX_CHARS, ServerFrame, SessionEvent, SessionEventListener, ShellCommandStatusLike, ShellToolResultOptions, ShellToolView, StandardAgentToolOptions, TranscriptLog, TranscriptOptions, TranscriptPatch, TurnRetryPolicy, applyTranscriptPatches, cloneBlocks, createInProcessTransportPair, createStandardAgentTools, createWebSocketClientTransport, createWebSocketServerTransport, estimateTranscriptBlockTokens, findResumePoint, finishShellToolResult, isContextLengthExceeded, isRetryableCode, resolveRetryPolicy, retryDelayMs, shellCommandHandleRequired, shellPreviewBudgetTokens, toShellToolResult };
388
+ export { AbortResult, AbortTarget, ActiveTurnPhase, AgentActionOptions, AgentClient, AgentClientListener, AgentClientTransport, AgentDisposeContext, AgentHarness, AgentHarnessContext, AgentHarnessRuntime, AgentHostContext, AgentLifecycleEvent, AgentMetadata, AgentPromptContext, AgentReferenceResolveContext, AgentServer, AgentServerOptions, AgentServerSessionOptions, AgentServerTransport, AgentSession, AgentSessionCheckpoint, AgentSessionCloneParams, AgentSessionOptions, AgentSessionParams, AgentSessionRestoreParams, AgentSessionStore, AgentSystemPromptContext, AgentTool, AgentToolContext, AgentToolInvokeContext, AgentToolInvokeResult, AgentTransport, AgentTransportBinding, ClientFrame, ClientSessionEvent, CompactionWindow, ConversationSummary, DEFAULT_MAX_MEDIA_BYTES, DEFAULT_TURN_RETRY_POLICY, DrainedTranscriptPatches, ExternalMutationReservation, InProcessTransportPair, JsonWebSocket, ModelSwitchApply, PrepareShell, PrepareShellContext, ProviderStreamError, ResumePoint, RunCommandLineCommandNotRegisteredError, RunCommandLineOptions, RunCommandLineResult, RunCommandLineShellNotFoundError, RunCommandLineTimeoutError, SHELL_VIEW_MAX_CHARS, ServerFrame, SessionEvent, SessionEventListener, ShellCommandStatusLike, ShellToolResultOptions, ShellToolView, StandardAgentToolOptions, TranscriptLog, TranscriptOptions, TranscriptPatch, TurnRetryPolicy, applyTranscriptPatches, cloneBlocks, createInProcessTransportPair, createStandardAgentTools, createWebSocketClientTransport, createWebSocketServerTransport, estimateTranscriptBlockTokens, findResumePoint, finishShellToolResult, isContextLengthExceeded, isRetryableCode, resolveRetryPolicy, retryDelayMs, shellCommandHandleRequired, shellPreviewBudgetTokens, toShellToolResult };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as cloneBlocks, i as applyTranscriptPatches, n as createWebSocketServerTransport, o as ProviderStreamError, r as AgentClient, s as isContextLengthExceeded, t as createWebSocketClientTransport } from "./websocket-transport-BjSj3U0I.mjs";
1
+ import { a as cloneBlocks, i as applyTranscriptPatches, n as createWebSocketServerTransport, o as ProviderStreamError, r as AgentClient, s as isContextLengthExceeded, t as createWebSocketClientTransport } from "./websocket-transport-c6P3NBuf.mjs";
2
2
  import { AbortError, abortable, asError, asRecord, asString, bytesToBase64, createId, delay, isAbortError, isRecord, noop, parseJsonOrString, safeJsonStringify, sliceHead, sliceTail, throwIfAborted, toWellFormedText, truncate } from "@demicodes/utils";
3
3
  import { BashEnvironment, CommandRegistry, MAX_TIMEOUT_MS, heredocDelimiter, shellQuote } from "@demicodes/shell";
4
4
  import { providerRuntime } from "@demicodes/provider";
@@ -793,6 +793,20 @@ var PendingSteerQueue = class {
793
793
  function estimateTokens(text) {
794
794
  return Math.ceil(text.length / 4);
795
795
  }
796
+ /**
797
+ * Resolves the token count at which auto-compaction fires.
798
+ *
799
+ * - A non-finite `thresholdRatio` disables auto-compaction (used by summary clones).
800
+ * - When `thresholdTokens` is a finite number, it is used as an absolute threshold,
801
+ * clamped to `[0, contextWindow]`.
802
+ * - Otherwise the threshold is `floor(contextWindow * thresholdRatio)`.
803
+ */
804
+ function resolveCompactionThreshold(contextWindow, thresholdRatio, thresholdTokens) {
805
+ if (contextWindow <= 0) return 0;
806
+ if (!Number.isFinite(thresholdRatio)) return Number.POSITIVE_INFINITY;
807
+ if (thresholdTokens != null && Number.isFinite(thresholdTokens)) return Math.min(Math.max(0, Math.floor(thresholdTokens)), contextWindow);
808
+ return Math.floor(contextWindow * thresholdRatio);
809
+ }
796
810
  /** The next (smaller) cut point to retry compaction with, or null when nothing more can be compacted. */
797
811
  function nextSmallerCompactionCutPoint(startIndex, cutPoint) {
798
812
  const compactedBlockCount = cutPoint - startIndex;
@@ -821,21 +835,29 @@ var CompactionController = class {
821
835
  constructor(host) {
822
836
  this.host = host;
823
837
  }
824
- /** Compacts (up to 8 passes) until the history fits `targetModel`'s context, if over threshold. */
838
+ /**
839
+ * Compacts (up to 8 passes) until the history fits `targetModel`'s context, if over
840
+ * threshold. Returns whether anything was compacted.
841
+ */
825
842
  async compactToFit(targetModel) {
826
843
  const contextWindow = targetModel.model.contextWindow;
827
- if (contextWindow <= 0) return;
828
- const threshold = Math.floor(contextWindow * this.host.thresholdRatio);
829
- if (this.host.transcript.estimateContextTokens(contextWindow) < threshold) return;
830
- await this.host.runWithCompactingPhase(async () => {
831
- for (let attempt = 0; attempt < 8 && this.host.transcript.estimateContextTokens(contextWindow) >= threshold; attempt += 1) if (!await this.run()) break;
844
+ if (contextWindow <= 0) return false;
845
+ const threshold = resolveCompactionThreshold(contextWindow, this.host.thresholdRatio, this.host.thresholdTokens);
846
+ if (this.host.transcript.estimateContextTokens(contextWindow) < threshold) return false;
847
+ return this.host.runWithCompactingPhase(async () => {
848
+ let compacted = false;
849
+ for (let attempt = 0; attempt < 8 && this.host.transcript.estimateContextTokens(contextWindow) >= threshold; attempt += 1) {
850
+ if (!await this.run()) break;
851
+ compacted = true;
852
+ }
853
+ return compacted;
832
854
  });
833
855
  }
834
856
  /** Runs one compaction pass before a turn when the current model is over threshold. */
835
857
  async preflight() {
836
858
  const contextWindow = this.host.model.model.contextWindow;
837
859
  if (contextWindow <= 0) return;
838
- const threshold = Math.floor(contextWindow * this.host.thresholdRatio);
860
+ const threshold = resolveCompactionThreshold(contextWindow, this.host.thresholdRatio, this.host.thresholdTokens);
839
861
  if (this.host.transcript.estimateContextTokens(contextWindow) < threshold) return;
840
862
  await this.host.runWithCompactingPhase(() => this.run());
841
863
  }
@@ -922,6 +944,10 @@ var ProviderTurnLoop = class {
922
944
  let autoCompactions = 0;
923
945
  while (true) {
924
946
  throwIfAborted(this.host.currentSignal());
947
+ if (await this.host.applyImmediateModelSwitch()) {
948
+ this.host.transcript.pushResumeTurn(this.host.currentTurnId(), this.host.model);
949
+ await this.host.commitTranscript();
950
+ }
925
951
  await this.host.materializePendingSteers();
926
952
  const steerContinuationBeforeStream = this.host.steerContinuationCount;
927
953
  const shouldAutoRecover = await this.streamProviderOnce();
@@ -1146,7 +1172,7 @@ var ProviderTurnLoop = class {
1146
1172
  isUsageNearLimit(usage) {
1147
1173
  const contextWindow = this.host.model.model.contextWindow;
1148
1174
  if (contextWindow <= 0) return false;
1149
- return usage.inputTokens + usage.outputTokens + usage.cacheReadTokens + usage.cacheWriteTokens >= Math.floor(contextWindow * this.host.thresholdRatio);
1175
+ return usage.inputTokens + usage.outputTokens + usage.cacheReadTokens + usage.cacheWriteTokens >= resolveCompactionThreshold(contextWindow, this.host.thresholdRatio, this.host.thresholdTokens);
1150
1176
  }
1151
1177
  };
1152
1178
  function toToolDefinition(tool) {
@@ -1172,6 +1198,7 @@ var AgentSession = class AgentSession {
1172
1198
  store;
1173
1199
  compactionKeepRecentTokens;
1174
1200
  compactionThresholdRatio;
1201
+ compactionThresholdTokens;
1175
1202
  listeners = /* @__PURE__ */ new Set();
1176
1203
  pendingActions = [];
1177
1204
  queued = [];
@@ -1237,6 +1264,7 @@ var AgentSession = class AgentSession {
1237
1264
  this.retryPolicy = resolveRetryPolicy(options.retry);
1238
1265
  this.compactionKeepRecentTokens = options.compaction?.keepRecentTokens ?? DEFAULT_KEEP_RECENT_TOKENS;
1239
1266
  this.compactionThresholdRatio = options.compaction?.preflightThresholdRatio ?? DEFAULT_PREFLIGHT_THRESHOLD_RATIO;
1267
+ this.compactionThresholdTokens = options.compaction?.preflightThresholdTokens === void 0 ? null : options.compaction.preflightThresholdTokens;
1240
1268
  const transcriptOptions = {
1241
1269
  idFactory: this.idFactory,
1242
1270
  now: options.now
@@ -1256,6 +1284,9 @@ var AgentSession = class AgentSession {
1256
1284
  get thresholdRatio() {
1257
1285
  return self.compactionThresholdRatio;
1258
1286
  },
1287
+ get thresholdTokens() {
1288
+ return self.compactionThresholdTokens;
1289
+ },
1259
1290
  currentSignal: () => self.currentSignal(),
1260
1291
  clone: (transcript) => self.clone({
1261
1292
  transcript,
@@ -1294,6 +1325,9 @@ var AgentSession = class AgentSession {
1294
1325
  get thresholdRatio() {
1295
1326
  return self.compactionThresholdRatio;
1296
1327
  },
1328
+ get thresholdTokens() {
1329
+ return self.compactionThresholdTokens;
1330
+ },
1297
1331
  get steerContinuationCount() {
1298
1332
  return self.steerQueue.continuationCount;
1299
1333
  },
@@ -1316,6 +1350,7 @@ var AgentSession = class AgentSession {
1316
1350
  self.activeProviderRun = run;
1317
1351
  },
1318
1352
  streamProvider: (request, run) => self.providerEvents(request, run),
1353
+ applyImmediateModelSwitch: () => self.applyImmediateModelSwitch(),
1319
1354
  runCompaction: () => self.compaction.run(),
1320
1355
  runWithCompactingPhase: (fn) => self.runWithCompactingPhase(fn),
1321
1356
  commitTranscript: () => self.commitTranscript(),
@@ -1449,21 +1484,23 @@ var AgentSession = class AgentSession {
1449
1484
  return false;
1450
1485
  }
1451
1486
  /**
1452
- * Switches the model (and optionally the provider) for the rest of the session. Queued as an
1453
- * action so it lands at a turn boundary, never mid-tool-continuation; the next turn uses it.
1454
- * A non-null `provider` replaces the current one (its predecessor is disposed); pass null to
1455
- * keep the same provider instance and only change the model.
1456
- */
1457
- /**
1458
- * Records a model/provider switch. It is applied at the next turn's preflight (see
1459
- * applyPendingModelSwitch): if the new model can't hold the history, compaction runs there
1460
- * with the current (pre-switch) model first, then the swap happens. Recording is cheap and
1461
- * non-blocking so it never holds up an in-flight turn or other frames.
1487
+ * Records a model/provider switch for the rest of the session. A non-null `provider`
1488
+ * replaces the current one (its predecessor is disposed); pass null to keep the same
1489
+ * provider instance and only change the model.
1490
+ *
1491
+ * `apply` picks the boundary the switch lands on: 'next_turn' (default) waits for the
1492
+ * start of the next queued action, so a running turn finishes entirely on the old model;
1493
+ * 'immediate' also applies mid-turn at the next sampling/tool continuation, so the very
1494
+ * next request already runs on the new model (the same "as soon as possible" semantics
1495
+ * steering has). Either way, if the new model can't hold the history, compaction runs
1496
+ * first with the current (pre-switch) model, then the swap happens. Recording is cheap
1497
+ * and non-blocking so it never holds up an in-flight turn or other frames.
1462
1498
  */
1463
- updateModel(provider, model) {
1499
+ updateModel(provider, model, apply = "next_turn") {
1464
1500
  this.pendingModelSwitch = {
1465
1501
  provider,
1466
- model
1502
+ model,
1503
+ apply
1467
1504
  };
1468
1505
  }
1469
1506
  async abort() {
@@ -1835,23 +1872,34 @@ var AgentSession = class AgentSession {
1835
1872
  }
1836
1873
  }
1837
1874
  /**
1838
- * Applies a queued model/provider switch at a turn boundary (preflight). If the new model's
1839
- * context window can't hold the current history, compaction runs FIRST with the current
1840
- * (pre-switch) model + provider — which can still load it to summarize — and only then do we
1841
- * swap. Doing it the other way would ask the smaller model to summarize a history it may not
1842
- * be able to load.
1875
+ * Applies a queued model/provider switch at an action boundary (turn preflight). If the
1876
+ * new model's context window can't hold the current history, compaction runs FIRST with
1877
+ * the current (pre-switch) model + provider — which can still load it to summarize — and
1878
+ * only then do we swap. Doing it the other way would ask the smaller model to summarize
1879
+ * a history it may not be able to load. Returns whether that compaction actually ran
1880
+ * (false when no switch was pending).
1843
1881
  */
1844
1882
  async applyPendingModelSwitch() {
1845
1883
  const pending = this.pendingModelSwitch;
1846
- if (!pending) return;
1884
+ if (!pending) return false;
1847
1885
  this.pendingModelSwitch = null;
1848
- await this.compaction.compactToFit(pending.model);
1886
+ const compacted = await this.compaction.compactToFit(pending.model);
1849
1887
  if (pending.provider && pending.provider !== this.provider) {
1850
1888
  const previous = this.provider;
1851
1889
  this.provider = pending.provider;
1852
1890
  await previous.dispose?.();
1853
1891
  }
1854
1892
  this.model = pending.model;
1893
+ return compacted;
1894
+ }
1895
+ /**
1896
+ * Turn-loop variant for mid-turn continuation boundaries: only switches recorded with
1897
+ * apply 'immediate' land here; 'next_turn' switches stay pending until the running turn
1898
+ * finishes and the next action starts.
1899
+ */
1900
+ async applyImmediateModelSwitch() {
1901
+ if (this.pendingModelSwitch?.apply !== "immediate") return false;
1902
+ return this.applyPendingModelSwitch();
1855
1903
  }
1856
1904
  async runWithCompactingPhase(fn) {
1857
1905
  const previousPhase = this.currentPhase;
@@ -2834,7 +2882,7 @@ var AgentTransportBindingImpl = class {
2834
2882
  this.session?.cancelPendingSteer(frame.steerId);
2835
2883
  return;
2836
2884
  case "set_provider":
2837
- await this.setProvider(frame.provider);
2885
+ await this.setProvider(frame.provider, frame.apply);
2838
2886
  return;
2839
2887
  case "retry": {
2840
2888
  const session = this.sessionFor("retry");
@@ -3024,7 +3072,7 @@ var AgentTransportBindingImpl = class {
3024
3072
  return;
3025
3073
  }
3026
3074
  }
3027
- async setProvider(provider) {
3075
+ async setProvider(provider, apply) {
3028
3076
  const session = this.session;
3029
3077
  if (!session) {
3030
3078
  this.send({
@@ -3035,11 +3083,11 @@ var AgentTransportBindingImpl = class {
3035
3083
  return;
3036
3084
  }
3037
3085
  if (provider.providerId === this.currentProviderId) {
3038
- await session.updateModel(null, provider.model);
3086
+ session.updateModel(null, provider.model, apply);
3039
3087
  return;
3040
3088
  }
3041
3089
  const next = await this.createRuntime(provider);
3042
- await session.updateModel(next, provider.model);
3090
+ session.updateModel(next, provider.model, apply);
3043
3091
  this.currentProviderId = provider.providerId;
3044
3092
  }
3045
3093
  async createRuntime(selection) {
@@ -1,4 +1,4 @@
1
- import { n as AgentServerTransport, t as AgentClientTransport } from "./transport-gkyOzXn-.mjs";
1
+ import { n as AgentServerTransport, t as AgentClientTransport } from "./transport-DtBCCS_A.mjs";
2
2
  import { Readable, Writable } from "node:stream";
3
3
  //#region src/stdio-transport.d.ts
4
4
  declare function createStdioClientTransport(readable: Readable, writable: Writable): AgentClientTransport;
@@ -209,6 +209,14 @@ interface AgentToolInvokeResult {
209
209
  view?: unknown | null;
210
210
  stopAfterToolResult?: boolean;
211
211
  }
212
+ /**
213
+ * When a recorded model/provider switch is applied:
214
+ * - 'next_turn' (default): the start of the next queued action — a running turn, if any,
215
+ * finishes entirely on the old model.
216
+ * - 'immediate': the next inference boundary — mid-turn that is the next sampling/tool
217
+ * continuation, so the very next request already runs on the new model.
218
+ */
219
+ type ModelSwitchApply = 'immediate' | 'next_turn';
212
220
  type AbortTarget = 'active_provider_stream' | 'active_tool' | 'active_compaction' | 'active_turn' | 'queued_action' | 'queued_message' | 'pending_yield_wakeup';
213
221
  interface AbortResult {
214
222
  aborted: boolean;
@@ -308,6 +316,12 @@ interface AgentSessionOptions<State = unknown> {
308
316
  compaction?: {
309
317
  keepRecentTokens?: number;
310
318
  preflightThresholdRatio?: number;
319
+ /**
320
+ * Absolute token threshold for auto-compaction. When set, replaces the
321
+ * ratio-derived threshold (still clamped to the model context window).
322
+ * A non-finite `preflightThresholdRatio` still disables auto-compaction.
323
+ */
324
+ preflightThresholdTokens?: number;
311
325
  };
312
326
  /**
313
327
  * Maximum interval between checkpoint writes while a turn is streaming. Writes
@@ -388,6 +402,7 @@ type ClientFrame = {
388
402
  } | {
389
403
  type: 'set_provider';
390
404
  provider: ProviderSelection;
405
+ apply?: ModelSwitchApply;
391
406
  } | {
392
407
  type: 'abort';
393
408
  } | {
@@ -580,4 +595,4 @@ interface InProcessTransportPair {
580
595
  }
581
596
  declare function createInProcessTransportPair(): InProcessTransportPair;
582
597
  //#endregion
583
- export { AgentTool as A, TranscriptOptions as B, AgentSessionCheckpoint as C, AgentSessionRestoreParams as D, AgentSessionParams as E, SessionEvent as F, resolveRetryPolicy as G, DEFAULT_TURN_RETRY_POLICY as H, SessionEventListener as I, retryDelayMs as K, CompactionWindow as L, AgentToolInvokeContext as M, AgentToolInvokeResult as N, AgentSessionStore as O, ExternalMutationReservation as P, DrainedTranscriptPatches as R, AgentReferenceResolveContext as S, AgentSessionOptions as T, TurnRetryPolicy as U, estimateTranscriptBlockTokens as V, isRetryableCode as W, AgentHarnessRuntime as _, createInProcessTransportPair as a, AgentMetadata as b, ConversationSummary as c, TranscriptPatch as d, AbortResult as f, AgentHarnessContext as g, AgentHarness as h, InProcessTransportPair as i, AgentToolContext as j, AgentSystemPromptContext as k, ServerFrame as l, AgentDisposeContext as m, AgentServerTransport as n, ClientFrame as o, AbortTarget as p, AgentTransport as r, ClientSessionEvent as s, AgentClientTransport as t, ShellCommandStatusLike as u, AgentHostContext as v, AgentSessionCloneParams as w, AgentPromptContext as x, AgentLifecycleEvent as y, TranscriptLog as z };
598
+ export { AgentTool as A, TranscriptLog as B, AgentSessionCheckpoint as C, AgentSessionRestoreParams as D, AgentSessionParams as E, ModelSwitchApply as F, isRetryableCode as G, estimateTranscriptBlockTokens as H, SessionEvent as I, resolveRetryPolicy as K, SessionEventListener as L, AgentToolInvokeContext as M, AgentToolInvokeResult as N, AgentSessionStore as O, ExternalMutationReservation as P, CompactionWindow as R, AgentReferenceResolveContext as S, AgentSessionOptions as T, DEFAULT_TURN_RETRY_POLICY as U, TranscriptOptions as V, TurnRetryPolicy as W, AgentHarnessRuntime as _, createInProcessTransportPair as a, AgentMetadata as b, ConversationSummary as c, TranscriptPatch as d, AbortResult as f, AgentHarnessContext as g, AgentHarness as h, InProcessTransportPair as i, AgentToolContext as j, AgentSystemPromptContext as k, ServerFrame as l, AgentDisposeContext as m, AgentServerTransport as n, ClientFrame as o, AbortTarget as p, retryDelayMs as q, AgentTransport as r, ClientSessionEvent as s, AgentClientTransport as t, ShellCommandStatusLike as u, AgentHostContext as v, AgentSessionCloneParams as w, AgentPromptContext as x, AgentLifecycleEvent as y, DrainedTranscriptPatches as z };
@@ -1,4 +1,4 @@
1
- import { b as AgentMetadata, c as ConversationSummary, f as AbortResult, n as AgentServerTransport, s as ClientSessionEvent, t as AgentClientTransport } from "./transport-gkyOzXn-.mjs";
1
+ import { F as ModelSwitchApply, b as AgentMetadata, c as ConversationSummary, f as AbortResult, n as AgentServerTransport, s as ClientSessionEvent, t as AgentClientTransport } from "./transport-DtBCCS_A.mjs";
2
2
  import { ProviderSelection } from "@demicodes/provider";
3
3
  import { Block, UserContentBlock } from "@demicodes/core";
4
4
  //#region src/client.d.ts
@@ -35,10 +35,15 @@ declare class AgentClient {
35
35
  }): Promise<void>;
36
36
  cancelPendingSteer(steerId: string): void;
37
37
  /**
38
- * Switches the provider/model for an open session. The change takes effect on the next
39
- * turn (the server applies it at a turn boundary), so this is fire-and-forget.
38
+ * Switches the provider/model for an open session. Fire-and-forget: the server records
39
+ * the switch and applies it per `options.apply` 'next_turn' (default) waits for the
40
+ * next queued action, so a running turn finishes on the old model; 'immediate' applies
41
+ * at the next inference boundary, so mid-turn the very next sampling/tool continuation
42
+ * already runs on the new model.
40
43
  */
41
- setProvider(provider: ProviderSelection): void;
44
+ setProvider(provider: ProviderSelection, options?: {
45
+ apply?: ModelSwitchApply;
46
+ }): void;
42
47
  /**
43
48
  * Discards the whole latest turn and runs it again from the user's input —
44
49
  * "regenerate". NOT a recovery path: it drops text the turn already emitted and
@@ -156,13 +156,17 @@ var AgentClient = class {
156
156
  });
157
157
  }
158
158
  /**
159
- * Switches the provider/model for an open session. The change takes effect on the next
160
- * turn (the server applies it at a turn boundary), so this is fire-and-forget.
159
+ * Switches the provider/model for an open session. Fire-and-forget: the server records
160
+ * the switch and applies it per `options.apply` 'next_turn' (default) waits for the
161
+ * next queued action, so a running turn finishes on the old model; 'immediate' applies
162
+ * at the next inference boundary, so mid-turn the very next sampling/tool continuation
163
+ * already runs on the new model.
161
164
  */
162
- setProvider(provider) {
165
+ setProvider(provider, options = {}) {
163
166
  this.sendFrame({
164
167
  type: "set_provider",
165
- provider
168
+ provider,
169
+ apply: options.apply
166
170
  });
167
171
  }
168
172
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@demicodes/agent",
3
3
  "description": "Session runtime and transport-neutral client and server protocol for Demi.",
4
- "version": "0.15.0",
4
+ "version": "0.17.0",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "exports": {
@@ -19,10 +19,10 @@
19
19
  }
20
20
  },
21
21
  "dependencies": {
22
- "@demicodes/core": "^0.15.0",
23
- "@demicodes/provider": "^0.15.0",
24
- "@demicodes/shell": "^0.15.0",
25
- "@demicodes/utils": "^0.15.0"
22
+ "@demicodes/core": "^0.17.0",
23
+ "@demicodes/provider": "^0.17.0",
24
+ "@demicodes/shell": "^0.17.0",
25
+ "@demicodes/utils": "^0.17.0"
26
26
  },
27
27
  "license": "Apache-2.0",
28
28
  "main": "./dist/index.mjs",