@oh-my-pi/pi-agent-core 17.2.8 → 17.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.2.9] - 2026-08-05
6
+
7
+ ### Fixed
8
+
9
+ - Preserved queued steering and follow-up messages when a continuation is cancelled before or during pre-dequeue hooks, and propagated the caller's cancellation signal through every continuation model-call loop.
10
+
5
11
  ## [17.2.6] - 2026-08-03
6
12
 
7
13
  ### Fixed
@@ -371,6 +371,10 @@ export declare class Agent {
371
371
  */
372
372
  buildSideRequestContext(llmMessages: Message[], systemPrompt?: string[]): Promise<Context>;
373
373
  subscribe(fn: (e: AgentEvent) => void): () => void;
374
+ /** Register an independently removable hook that runs before queued messages are consumed. */
375
+ addBeforeQueuedMessageDequeueHook(hook: (signal?: AbortSignal) => Promise<void> | void): () => void;
376
+ /** Register an independently removable hook that runs immediately before each model call. */
377
+ addBeforeModelCallHook(hook: (signal?: AbortSignal) => Promise<void> | void): () => void;
374
378
  setProviderResponseInterceptor(fn: SimpleStreamOptions["onResponse"] | undefined): void;
375
379
  setRawSseEventInterceptor(fn: SimpleStreamOptions["onSseEvent"] | undefined): void;
376
380
  setAssistantMessageEventInterceptor(fn: ((message: AssistantMessage, event: AssistantMessageEvent) => void) | undefined): void;
@@ -457,8 +461,5 @@ export declare class Agent {
457
461
  prompt(message: AgentMessage | AgentMessage[], options?: AgentPromptOptions): Promise<void>;
458
462
  prompt(input: string, options?: AgentPromptOptions): Promise<void>;
459
463
  prompt(input: string, images?: ImageContent[], options?: AgentPromptOptions): Promise<void>;
460
- /**
461
- * Continue from current context (used for retries and resuming queued messages).
462
- */
463
- continue(): Promise<void>;
464
+ continue(signal?: AbortSignal): Promise<void>;
464
465
  }
@@ -191,7 +191,7 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
191
191
  * mid-batch interrupt poll uses {@link hasSteeringMessages} instead and
192
192
  * never consumes the queue.
193
193
  */
194
- getSteeringMessages?: () => Promise<AgentMessage[]>;
194
+ getSteeringMessages?: (signal?: AbortSignal) => Promise<AgentMessage[]>;
195
195
  /**
196
196
  * Peeks whether steering messages are queued, without consuming them.
197
197
  *
@@ -232,7 +232,7 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
232
232
  * If messages are returned, they're added to the context and the agent
233
233
  * continues with another turn.
234
234
  */
235
- getFollowUpMessages?: () => Promise<AgentMessage[]>;
235
+ getFollowUpMessages?: (signal?: AbortSignal) => Promise<AgentMessage[]>;
236
236
  /**
237
237
  * Returns non-interrupting "aside" messages to inject at a step boundary.
238
238
  *
@@ -264,7 +264,7 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
264
264
  * Mutate the agent context here; use `beforeModelCall` to inspect the
265
265
  * provider-bound context.
266
266
  */
267
- syncContextBeforeModelCall?: (context: AgentContext) => void | Promise<void>;
267
+ syncContextBeforeModelCall?: (context: AgentContext, signal?: AbortSignal) => void | Promise<void>;
268
268
  /**
269
269
  * Asked after the complete provider context has been built, including
270
270
  * message conversion, provider transforms, normalized tools, and owned
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-agent-core",
4
- "version": "17.2.8",
4
+ "version": "17.2.9",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -35,16 +35,16 @@
35
35
  "fmt": "biome format --write ."
36
36
  },
37
37
  "dependencies": {
38
- "@oh-my-pi/pi-ai": "17.2.8",
39
- "@oh-my-pi/pi-catalog": "17.2.8",
40
- "@oh-my-pi/pi-natives": "17.2.8",
41
- "@oh-my-pi/pi-utils": "17.2.8",
42
- "@oh-my-pi/pi-wire": "17.2.8",
43
- "@oh-my-pi/snapcompact": "17.2.8",
38
+ "@oh-my-pi/pi-ai": "17.2.9",
39
+ "@oh-my-pi/pi-catalog": "17.2.9",
40
+ "@oh-my-pi/pi-natives": "17.2.9",
41
+ "@oh-my-pi/pi-utils": "17.2.9",
42
+ "@oh-my-pi/pi-wire": "17.2.9",
43
+ "@oh-my-pi/snapcompact": "17.2.9",
44
44
  "@opentelemetry/api": "^1.9.1"
45
45
  },
46
46
  "devDependencies": {
47
- "@oh-my-pi/omptype": "17.2.8",
47
+ "@oh-my-pi/omptype": "17.2.9",
48
48
  "@opentelemetry/context-async-hooks": "^2.9.0",
49
49
  "@opentelemetry/sdk-trace-base": "^2.9.0",
50
50
  "@types/bun": "^1.3.14"
package/src/agent-loop.ts CHANGED
@@ -1016,7 +1016,7 @@ async function runLoopBody(
1016
1016
  // Skip when the run is already externally aborted — dequeuing would strand
1017
1017
  // the messages in a run that is about to die.
1018
1018
  try {
1019
- pendingMessages = signal?.aborted ? [] : (await config.getSteeringMessages?.()) || [];
1019
+ pendingMessages = signal?.aborted ? [] : (await config.getSteeringMessages?.(signal)) || [];
1020
1020
  } catch (error) {
1021
1021
  stream.push({ type: "turn_start" });
1022
1022
  emitInputMessages(stream, messagesToEmit);
@@ -1075,7 +1075,7 @@ async function runLoopBody(
1075
1075
  let gateResult: AgentPreModelCallResult;
1076
1076
  try {
1077
1077
  if (config.syncContextBeforeModelCall) {
1078
- await config.syncContextBeforeModelCall(currentContext);
1078
+ await config.syncContextBeforeModelCall(currentContext, signal);
1079
1079
  }
1080
1080
 
1081
1081
  if (!directiveResolvedForTurn) {
@@ -1421,7 +1421,7 @@ async function runLoopBody(
1421
1421
  // instantly aborts — message lands in history, agent never responds. The
1422
1422
  // mid-batch interrupt poll only peeks (hasSteeringMessages), so the queue
1423
1423
  // still owns every message until this dequeue.
1424
- const steering = signal?.aborted ? [] : (await config.getSteeringMessages?.()) || [];
1424
+ const steering = signal?.aborted ? [] : (await config.getSteeringMessages?.(signal)) || [];
1425
1425
  if (hasMoreToolCalls) {
1426
1426
  // Mid-work: fold any non-interrupting asides into the next turn alongside steering.
1427
1427
  const asides = signal?.aborted ? [] : resolveAsides(await config.getAsideMessages?.());
@@ -1450,9 +1450,9 @@ async function runLoopBody(
1450
1450
  // Re-poll steering too: a steer can land between the stop-boundary dequeue
1451
1451
  // above and this yield point (e.g. queued while onBeforeYield ran). Without
1452
1452
  // this poll it would strand in the queue until the next manual prompt.
1453
- const lateSteering = signal?.aborted ? [] : (await config.getSteeringMessages?.()) || [];
1453
+ const lateSteering = signal?.aborted ? [] : (await config.getSteeringMessages?.(signal)) || [];
1454
1454
  const asideMessages = signal?.aborted ? [] : resolveAsides(await config.getAsideMessages?.());
1455
- const followUpMessages = signal?.aborted ? [] : (await config.getFollowUpMessages?.()) || [];
1455
+ const followUpMessages = signal?.aborted ? [] : (await config.getFollowUpMessages?.(signal)) || [];
1456
1456
  if (lateSteering.length > 0 || asideMessages.length > 0 || followUpMessages.length > 0) {
1457
1457
  // Set as pending so the inner loop processes them before stopping.
1458
1458
  pendingMessages = [...lateSteering, ...asideMessages, ...followUpMessages];
package/src/agent.ts CHANGED
@@ -426,6 +426,8 @@ export class Agent {
426
426
  #asideMessageProvider?: () => AsideMessage[] | Promise<AsideMessage[]>;
427
427
  #telemetry?: AgentLoopConfig["telemetry"];
428
428
  #appendOnlyContext?: AppendOnlyContextManager;
429
+ #beforeQueuedMessageDequeueHooks = new Set<(signal?: AbortSignal) => Promise<void> | void>();
430
+ #beforeModelCallHooks = new Set<(signal?: AbortSignal) => Promise<void> | void>();
429
431
 
430
432
  /** Buffered Cursor tool results with text length at time of call (for correct ordering) */
431
433
  #cursorToolResultBuffer: CursorToolResultEntry[] = [];
@@ -784,6 +786,40 @@ export class Agent {
784
786
  return () => this.#listeners.delete(fn);
785
787
  }
786
788
 
789
+ /** Register an independently removable hook that runs before queued messages are consumed. */
790
+ addBeforeQueuedMessageDequeueHook(hook: (signal?: AbortSignal) => Promise<void> | void): () => void {
791
+ const registration = (signal?: AbortSignal) => hook(signal);
792
+ this.#beforeQueuedMessageDequeueHooks.add(registration);
793
+ return () => this.#beforeQueuedMessageDequeueHooks.delete(registration);
794
+ }
795
+
796
+ /** Register an independently removable hook that runs immediately before each model call. */
797
+ addBeforeModelCallHook(hook: (signal?: AbortSignal) => Promise<void> | void): () => void {
798
+ const registration = (signal?: AbortSignal) => hook(signal);
799
+ this.#beforeModelCallHooks.add(registration);
800
+ return () => this.#beforeModelCallHooks.delete(registration);
801
+ }
802
+
803
+ async #runBeforeModelCallHooks(signal?: AbortSignal): Promise<void> {
804
+ for (const hook of this.#beforeModelCallHooks) await hook(signal);
805
+ }
806
+
807
+ async #runBeforeQueuedMessageDequeueHooks(signal?: AbortSignal): Promise<void> {
808
+ for (const hook of this.#beforeQueuedMessageDequeueHooks) await hook(signal);
809
+ }
810
+
811
+ async #dequeueSteeringMessagesAfterHooks(signal?: AbortSignal): Promise<AgentMessage[]> {
812
+ if (signal?.aborted || this.#steeringQueue.length === 0) return [];
813
+ await this.#runBeforeQueuedMessageDequeueHooks(signal);
814
+ return signal?.aborted ? [] : this.#dequeueSteeringMessages();
815
+ }
816
+
817
+ async #dequeueFollowUpMessagesAfterHooks(signal?: AbortSignal): Promise<AgentMessage[]> {
818
+ if (signal?.aborted || this.#followUpQueue.length === 0) return [];
819
+ await this.#runBeforeQueuedMessageDequeueHooks(signal);
820
+ return signal?.aborted ? [] : this.#dequeueFollowUpMessages();
821
+ }
822
+
787
823
  setProviderResponseInterceptor(fn: SimpleStreamOptions["onResponse"] | undefined): void {
788
824
  this.#onResponse = fn;
789
825
  }
@@ -1137,48 +1173,90 @@ export class Agent {
1137
1173
  /**
1138
1174
  * Continue from current context (used for retries and resuming queued messages).
1139
1175
  */
1140
- async continue() {
1176
+ #continuationDequeueSignal(signal?: AbortSignal): AbortSignal | undefined {
1177
+ const signals: AbortSignal[] = [];
1178
+ if (this.#abortController) signals.push(this.#abortController.signal);
1179
+ if (signal) signals.push(signal);
1180
+ if (this.#deadline !== undefined) {
1181
+ const delay = this.#deadline - Date.now();
1182
+ if (delay <= 0) {
1183
+ const controller = new AbortController();
1184
+ controller.abort(new DOMException("Deadline exceeded", "TimeoutError"));
1185
+ signals.push(controller.signal);
1186
+ } else {
1187
+ signals.push(AbortSignal.timeout(delay));
1188
+ }
1189
+ }
1190
+ if (signals.length === 0) return undefined;
1191
+ return signals.length === 1 ? signals[0] : AbortSignal.any(signals);
1192
+ }
1193
+
1194
+ async continue(signal?: AbortSignal) {
1141
1195
  if (this.#state.isStreaming) {
1142
1196
  throw new AgentBusyError();
1143
1197
  }
1144
1198
 
1145
- const messages = this.#state.messages;
1146
- if (messages.length === 0) {
1147
- // An empty transcript has nothing to resume, but a queued steer/follow-up
1148
- // must still be delivered as the opening turn — mirroring the assistant-tail
1149
- // branch below. Throwing here leaves the message undeliverable, and idle-drain
1150
- // callers (AgentSession#scheduleQueuedMessageDrain) re-arm continue() on every
1151
- // microtask because hasQueuedMessages() never clears, spinning an unbounded
1152
- // allocation loop until OOM (issue #6344).
1153
- const queuedSteering = this.#dequeueSteeringMessages();
1154
- if (queuedSteering.length > 0) {
1155
- await this.#runLoop(queuedSteering, { skipInitialSteeringPoll: true });
1156
- return;
1157
- }
1158
- const queuedFollowUp = this.#dequeueFollowUpMessages();
1159
- if (queuedFollowUp.length > 0) {
1160
- await this.#runLoop(queuedFollowUp);
1161
- return;
1162
- }
1163
- throw new Error("No messages to continue from");
1164
- }
1165
- if (messages[messages.length - 1].role === "assistant") {
1166
- const queuedSteering = this.#dequeueSteeringMessages();
1167
- if (queuedSteering.length > 0) {
1168
- await this.#runLoop(queuedSteering, { skipInitialSteeringPoll: true });
1169
- return;
1199
+ const { promise, resolve } = Promise.withResolvers<void>();
1200
+ this.#runningPrompt = promise;
1201
+ this.#resolveRunningPrompt = resolve;
1202
+ const continuationAbortController = new AbortController();
1203
+ this.#abortController = continuationAbortController;
1204
+ this.#state.isStreaming = true;
1205
+ this.#state.streamMessage = null;
1206
+ this.#state.error = undefined;
1207
+
1208
+ try {
1209
+ const dequeueSignal = this.#continuationDequeueSignal(signal);
1210
+ const messages = this.#state.messages;
1211
+ if (messages.length === 0) {
1212
+ // An empty transcript has nothing to resume, but a queued steer/follow-up
1213
+ // must still be delivered as the opening turn — mirroring the assistant-tail
1214
+ // branch below. Throwing here leaves the message undeliverable, and idle-drain
1215
+ // callers (AgentSession#scheduleQueuedMessageDrain) re-arm continue() on every
1216
+ // microtask because hasQueuedMessages() never clears, spinning an unbounded
1217
+ // allocation loop until OOM (issue #6344).
1218
+ const queuedSteering = await this.#dequeueSteeringMessagesAfterHooks(dequeueSignal);
1219
+ if (queuedSteering.length > 0) {
1220
+ await this.#runLoop(queuedSteering, { skipInitialSteeringPoll: true }, signal, true);
1221
+ return;
1222
+ }
1223
+ const queuedFollowUp = await this.#dequeueFollowUpMessagesAfterHooks(dequeueSignal);
1224
+ if (queuedFollowUp.length > 0) {
1225
+ await this.#runLoop(queuedFollowUp, undefined, signal, true);
1226
+ return;
1227
+ }
1228
+ throw new Error("No messages to continue from");
1170
1229
  }
1230
+ if (messages[messages.length - 1].role === "assistant") {
1231
+ const queuedSteering = await this.#dequeueSteeringMessagesAfterHooks(dequeueSignal);
1232
+ if (queuedSteering.length > 0) {
1233
+ await this.#runLoop(queuedSteering, { skipInitialSteeringPoll: true }, signal, true);
1234
+ return;
1235
+ }
1236
+
1237
+ const queuedFollowUp = await this.#dequeueFollowUpMessagesAfterHooks(dequeueSignal);
1238
+ if (queuedFollowUp.length > 0) {
1239
+ await this.#runLoop(queuedFollowUp, undefined, signal, true);
1240
+ return;
1241
+ }
1171
1242
 
1172
- const queuedFollowUp = this.#dequeueFollowUpMessages();
1173
- if (queuedFollowUp.length > 0) {
1174
- await this.#runLoop(queuedFollowUp);
1175
- return;
1243
+ throw new Error("Cannot continue from message role: assistant");
1176
1244
  }
1177
1245
 
1178
- throw new Error("Cannot continue from message role: assistant");
1246
+ await this.#runLoop(undefined, undefined, signal, true);
1247
+ } finally {
1248
+ resolve();
1249
+ if (this.#abortController === continuationAbortController) {
1250
+ this.#state.isStreaming = false;
1251
+ this.#state.streamMessage = null;
1252
+ this.#state.pendingToolCalls.clear();
1253
+ this.#abortController = undefined;
1254
+ if (this.#runningPrompt === promise) {
1255
+ this.#runningPrompt = undefined;
1256
+ this.#resolveRunningPrompt = undefined;
1257
+ }
1258
+ }
1179
1259
  }
1180
-
1181
- await this.#runLoop(undefined);
1182
1260
  }
1183
1261
 
1184
1262
  /**
@@ -1186,17 +1264,29 @@ export class Agent {
1186
1264
  * If messages are provided, starts a new conversation turn with those messages.
1187
1265
  * Otherwise, continues from existing context.
1188
1266
  */
1189
- async #runLoop(messages?: AgentMessage[], options?: AgentPromptOptions & { skipInitialSteeringPoll?: boolean }) {
1267
+ async #runLoop(
1268
+ messages?: AgentMessage[],
1269
+ options?: AgentPromptOptions & { skipInitialSteeringPoll?: boolean },
1270
+ continuationSignal?: AbortSignal,
1271
+ runStateClaimed = false,
1272
+ ) {
1190
1273
  const model = this.#state.model;
1191
1274
  if (!model) throw new Error("No model configured");
1192
1275
 
1193
1276
  let skipInitialSteeringPoll = options?.skipInitialSteeringPoll === true;
1194
1277
  using _ = new EventLoopKeepalive();
1195
- const { promise, resolve } = Promise.withResolvers<void>();
1196
- this.#runningPrompt = promise;
1197
- this.#resolveRunningPrompt = resolve;
1198
-
1199
- this.#abortController = new AbortController();
1278
+ if (!runStateClaimed) {
1279
+ const { promise, resolve } = Promise.withResolvers<void>();
1280
+ this.#runningPrompt = promise;
1281
+ this.#resolveRunningPrompt = resolve;
1282
+ this.#abortController = new AbortController();
1283
+ }
1284
+ const resolveRun = this.#resolveRunningPrompt;
1285
+ const loopAbortController = this.#abortController;
1286
+ if (!loopAbortController) throw new Error("Agent run state was not initialized");
1287
+ const loopSignal = continuationSignal
1288
+ ? AbortSignal.any([loopAbortController.signal, continuationSignal])
1289
+ : loopAbortController.signal;
1200
1290
  this.#state.isStreaming = true;
1201
1291
  this.#state.streamMessage = null;
1202
1292
  this.#state.error = undefined;
@@ -1315,7 +1405,8 @@ export class Agent {
1315
1405
  onSseEvent: this.#onSseEvent,
1316
1406
  getApiKey: this.getApiKey,
1317
1407
  getToolContext: this.#getToolContext,
1318
- syncContextBeforeModelCall: async context => {
1408
+ syncContextBeforeModelCall: async (context, signal) => {
1409
+ await this.#runBeforeModelCallHooks(signal);
1319
1410
  if (this.#listeners.size > 0) {
1320
1411
  await Bun.sleep(0);
1321
1412
  }
@@ -1362,12 +1453,12 @@ export class Agent {
1362
1453
  getReasoning: () => this.#state.thinkingLevel,
1363
1454
  getDisableReasoning: () => this.#state.disableReasoning,
1364
1455
  getServiceTier: this.#serviceTierResolver,
1365
- getSteeringMessages: async () => {
1456
+ getSteeringMessages: async signal => {
1366
1457
  if (skipInitialSteeringPoll) {
1367
1458
  skipInitialSteeringPoll = false;
1368
1459
  return [];
1369
1460
  }
1370
- return this.#dequeueSteeringMessages();
1461
+ return this.#dequeueSteeringMessagesAfterHooks(signal);
1371
1462
  },
1372
1463
  hasSteeringMessages: () => {
1373
1464
  if (this.#steeringQueue.length === 0) {
@@ -1392,7 +1483,7 @@ export class Agent {
1392
1483
  },
1393
1484
  waitForSteeringMessages: signal => this.#waitForSteeringMessages(signal),
1394
1485
  hasIrcInterrupts: this.hasIrcInterrupts,
1395
- getFollowUpMessages: async () => this.#dequeueFollowUpMessages(),
1486
+ getFollowUpMessages: signal => this.#dequeueFollowUpMessagesAfterHooks(signal),
1396
1487
  getAsideMessages: async () => (await this.#asideMessageProvider?.()) ?? [],
1397
1488
  onBeforeYield: () => this.#onBeforeYield?.(),
1398
1489
  telemetry: this.#telemetry,
@@ -1404,8 +1495,8 @@ export class Agent {
1404
1495
 
1405
1496
  try {
1406
1497
  const stream = messages
1407
- ? agentLoop(messages, context, config, this.#abortController.signal, this.streamFn)
1408
- : agentLoopContinue(context, config, this.#abortController.signal, this.streamFn);
1498
+ ? agentLoop(messages, context, config, loopSignal, this.streamFn)
1499
+ : agentLoopContinue(context, config, loopSignal, this.streamFn);
1409
1500
 
1410
1501
  for await (const event of stream) {
1411
1502
  if (event.type === "turn_start") turnOpen = true;
@@ -1472,15 +1563,15 @@ export class Agent {
1472
1563
  if (!onlyEmpty) {
1473
1564
  this.appendMessage(partial);
1474
1565
  } else {
1475
- if (this.#abortController?.signal.aborted) {
1566
+ if (loopSignal.aborted) {
1476
1567
  throw new Error("Request was aborted");
1477
1568
  }
1478
1569
  }
1479
1570
  }
1480
1571
  } catch (err) {
1481
- const stoppedForAbort = this.#abortController?.signal.aborted === true;
1572
+ const stoppedForAbort = loopSignal.aborted;
1482
1573
  const errorMessage = stoppedForAbort
1483
- ? abortReasonText(this.#abortController?.signal)
1574
+ ? abortReasonText(loopSignal)
1484
1575
  : err instanceof Error
1485
1576
  ? err.message
1486
1577
  : String(err);
@@ -1582,13 +1673,15 @@ export class Agent {
1582
1673
  this.#emit({ type: "agent_end", messages: [errorMsg] });
1583
1674
  }
1584
1675
  } finally {
1585
- this.#state.isStreaming = false;
1586
- this.#state.streamMessage = null;
1587
- this.#state.pendingToolCalls.clear();
1588
- this.#abortController = undefined;
1589
- this.#resolveRunningPrompt?.();
1590
- this.#runningPrompt = undefined;
1591
- this.#resolveRunningPrompt = undefined;
1676
+ resolveRun?.();
1677
+ if (this.#abortController === loopAbortController) {
1678
+ this.#state.isStreaming = false;
1679
+ this.#state.streamMessage = null;
1680
+ this.#state.pendingToolCalls.clear();
1681
+ this.#abortController = undefined;
1682
+ this.#runningPrompt = undefined;
1683
+ this.#resolveRunningPrompt = undefined;
1684
+ }
1592
1685
  }
1593
1686
  }
1594
1687
 
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  export class CompactionCancelledError extends Error {
14
- readonly name = "CompactionCancelledError" as const;
14
+ override readonly name = "CompactionCancelledError" as const;
15
15
 
16
16
  constructor(message = "Compaction cancelled") {
17
17
  super(message);
@@ -27,7 +27,7 @@ export class CompactionCancelledError extends Error {
27
27
  * ordinary summarization errors and must not fall through to another provider.
28
28
  */
29
29
  export class NativeCompactionError extends Error {
30
- readonly name = "NativeCompactionError" as const;
30
+ override readonly name = "NativeCompactionError" as const;
31
31
 
32
32
  constructor(cause: unknown) {
33
33
  super(cause instanceof Error ? cause.message : String(cause), { cause });
package/src/types.ts CHANGED
@@ -240,7 +240,7 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
240
240
  * mid-batch interrupt poll uses {@link hasSteeringMessages} instead and
241
241
  * never consumes the queue.
242
242
  */
243
- getSteeringMessages?: () => Promise<AgentMessage[]>;
243
+ getSteeringMessages?: (signal?: AbortSignal) => Promise<AgentMessage[]>;
244
244
 
245
245
  /**
246
246
  * Peeks whether steering messages are queued, without consuming them.
@@ -285,7 +285,7 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
285
285
  * If messages are returned, they're added to the context and the agent
286
286
  * continues with another turn.
287
287
  */
288
- getFollowUpMessages?: () => Promise<AgentMessage[]>;
288
+ getFollowUpMessages?: (signal?: AbortSignal) => Promise<AgentMessage[]>;
289
289
  /**
290
290
  * Returns non-interrupting "aside" messages to inject at a step boundary.
291
291
  *
@@ -319,7 +319,7 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
319
319
  * Mutate the agent context here; use `beforeModelCall` to inspect the
320
320
  * provider-bound context.
321
321
  */
322
- syncContextBeforeModelCall?: (context: AgentContext) => void | Promise<void>;
322
+ syncContextBeforeModelCall?: (context: AgentContext, signal?: AbortSignal) => void | Promise<void>;
323
323
 
324
324
  /**
325
325
  * Asked after the complete provider context has been built, including