@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 +6 -0
- package/dist/types/agent.d.ts +5 -4
- package/dist/types/types.d.ts +3 -3
- package/package.json +8 -8
- package/src/agent-loop.ts +5 -5
- package/src/agent.ts +148 -55
- package/src/compaction/errors.ts +2 -2
- package/src/types.ts +3 -3
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
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/types/types.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
39
|
-
"@oh-my-pi/pi-catalog": "17.2.
|
|
40
|
-
"@oh-my-pi/pi-natives": "17.2.
|
|
41
|
-
"@oh-my-pi/pi-utils": "17.2.
|
|
42
|
-
"@oh-my-pi/pi-wire": "17.2.
|
|
43
|
-
"@oh-my-pi/snapcompact": "17.2.
|
|
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.
|
|
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
|
-
|
|
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
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
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.#
|
|
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:
|
|
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,
|
|
1408
|
-
: agentLoopContinue(context, config,
|
|
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 (
|
|
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 =
|
|
1572
|
+
const stoppedForAbort = loopSignal.aborted;
|
|
1482
1573
|
const errorMessage = stoppedForAbort
|
|
1483
|
-
? abortReasonText(
|
|
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
|
-
|
|
1586
|
-
this.#
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
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
|
|
package/src/compaction/errors.ts
CHANGED
|
@@ -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
|