@linxiraos/pi-agent-core 1.1.15 → 1.1.17
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 +194 -2
- package/THIRD-PARTY-NOTICES.txt +54 -25
- package/dist/types/agent.d.ts +5 -2
- package/dist/types/append-only-context.d.ts +0 -4
- package/dist/types/compaction/compaction.d.ts +26 -17
- package/dist/types/compaction/entries.d.ts +2 -0
- package/dist/types/compaction/openai.d.ts +2 -1
- package/dist/types/thinking.d.ts +1 -1
- package/dist/types/types.d.ts +9 -1
- package/package.json +10 -10
- package/src/agent-loop.ts +127 -10
- package/src/agent.ts +142 -17
- package/src/append-only-context.ts +122 -1
- package/src/compaction/compaction-v2-streaming.ts +2 -1
- package/src/compaction/compaction.ts +98 -66
- package/src/compaction/entries.ts +2 -0
- package/src/compaction/openai.ts +10 -3
- package/src/proxy.ts +49 -4
- package/src/telemetry.ts +1 -0
- package/src/thinking.ts +1 -1
- package/src/tokenizer.ts +1 -0
- package/src/types.ts +14 -1
package/src/agent-loop.ts
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
getStreamingPartialJson,
|
|
38
38
|
kCursorExecResolved,
|
|
39
39
|
} from "@linxiraos/pi-ai/utils/block-symbols";
|
|
40
|
+
import { stamp } from "@linxiraos/pi-ai/utils/schema/stamps";
|
|
40
41
|
import {
|
|
41
42
|
createHarmonyAuditEvent,
|
|
42
43
|
detectHarmonyLeakInAssistantMessage,
|
|
@@ -395,6 +396,64 @@ function snapshotAssistantMessage(message: AssistantMessage): AssistantMessage {
|
|
|
395
396
|
};
|
|
396
397
|
}
|
|
397
398
|
|
|
399
|
+
/**
|
|
400
|
+
* Incremental variant of `snapshotAssistantMessage` for per-delta
|
|
401
|
+
* `message_update` events.
|
|
402
|
+
*
|
|
403
|
+
* The stream contract guarantees that every content-block mutation a provider
|
|
404
|
+
* makes is paired with an event carrying that block's `contentIndex` (every
|
|
405
|
+
* provider mutates then pushes), and that `output.content` is append-only
|
|
406
|
+
* within a turn. A fresh snapshot therefore only needs to re-clone:
|
|
407
|
+
*
|
|
408
|
+
* - the block the current event targets (`changedIndex`),
|
|
409
|
+
* - blocks still open (started but not ended) — Cursor's edit block merges
|
|
410
|
+
* `path`/`stream_content` into a live block without an event, so open blocks
|
|
411
|
+
* are re-cloned on every delta,
|
|
412
|
+
* - blocks appended since the previous snapshot.
|
|
413
|
+
*
|
|
414
|
+
* Every other (finalized) block is carried over from the previous snapshot by
|
|
415
|
+
* reference: finalized blocks are never mutated after their end event, so
|
|
416
|
+
* sharing them is exact and turns the per-delta cost from O(turn content) into
|
|
417
|
+
* O(open blocks) — the difference between quadratic and linear streaming cost
|
|
418
|
+
* on long turns (issue #10605).
|
|
419
|
+
*/
|
|
420
|
+
function snapshotAssistantMessageIncremental(
|
|
421
|
+
live: AssistantMessage,
|
|
422
|
+
prev: AssistantMessage,
|
|
423
|
+
changedIndex: number,
|
|
424
|
+
openBlocks: ReadonlySet<number>,
|
|
425
|
+
): AssistantMessage {
|
|
426
|
+
const liveContent = live.content;
|
|
427
|
+
const prevContent = prev.content;
|
|
428
|
+
const prevLen = prevContent.length;
|
|
429
|
+
// Reference-copy the previous snapshot's block array (native-speed), then
|
|
430
|
+
// patch in fresh clones only where the live state moved: the block this
|
|
431
|
+
// delta targeted, blocks still streaming, and blocks appended since the
|
|
432
|
+
// previous snapshot. Finalized blocks keep their existing snapshot clone.
|
|
433
|
+
const content = prevContent.slice();
|
|
434
|
+
for (let i = prevLen; i < liveContent.length; i++) {
|
|
435
|
+
content.push(snapshotAssistantContentBlock(liveContent[i]!));
|
|
436
|
+
}
|
|
437
|
+
if (changedIndex >= 0 && changedIndex < prevLen && changedIndex < liveContent.length) {
|
|
438
|
+
content[changedIndex] = snapshotAssistantContentBlock(liveContent[changedIndex]!);
|
|
439
|
+
}
|
|
440
|
+
for (const openIndex of openBlocks) {
|
|
441
|
+
if (openIndex !== changedIndex && openIndex >= 0 && openIndex < prevLen && openIndex < liveContent.length) {
|
|
442
|
+
content[openIndex] = snapshotAssistantContentBlock(liveContent[openIndex]!);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
...live,
|
|
447
|
+
content,
|
|
448
|
+
usage: {
|
|
449
|
+
...live.usage,
|
|
450
|
+
cost: { ...live.usage.cost },
|
|
451
|
+
},
|
|
452
|
+
disabledFeatures: live.disabledFeatures ? [...live.disabledFeatures] : undefined,
|
|
453
|
+
toolCallAbortMessages: live.toolCallAbortMessages ? { ...live.toolCallAbortMessages } : undefined,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
398
457
|
/**
|
|
399
458
|
* Deep-clone an assistant streaming event so subscribers get an immutable view.
|
|
400
459
|
* Pass `partialSnapshot` when the caller has already snapshotted `event.partial`
|
|
@@ -828,6 +887,29 @@ export function normalizeMessagesForProvider(
|
|
|
828
887
|
const INTENT_FIELD_DESCRIPTION = "concise intent";
|
|
829
888
|
const INTENT_SCHEMA_UNION_KEYS = ["anyOf", "oneOf"] as const;
|
|
830
889
|
|
|
890
|
+
// Memoize injection per input schema identity: normalizeTools runs on every
|
|
891
|
+
// model call and injectIntentIntoSchema mints a fresh root object each time,
|
|
892
|
+
// which defeats the stamp-keyed schema memos downstream (toolWireSchema,
|
|
893
|
+
// stripSchemaDescriptions, tryEnforceStrictSchema each deep-clone + re-walk
|
|
894
|
+
// the whole catalog per request). The injected object is shared across
|
|
895
|
+
// requests — the same profile as the intent-off path, where parameters IS the
|
|
896
|
+
// shared memoized wire schema (see schema-immutability.test.ts). One stamp
|
|
897
|
+
// key per (mode, describeIntent) variant; index 0 = bare, 1 = described.
|
|
898
|
+
const INTENT_STAMPS = {
|
|
899
|
+
require: [Symbol("intent:require"), Symbol("intent:require:described")],
|
|
900
|
+
optional: [Symbol("intent:optional"), Symbol("intent:optional:described")],
|
|
901
|
+
} as const;
|
|
902
|
+
|
|
903
|
+
function memoizedInjectIntentIntoSchema(
|
|
904
|
+
schema: Record<string, unknown>,
|
|
905
|
+
mode: "require" | "optional",
|
|
906
|
+
describeIntent: boolean,
|
|
907
|
+
): unknown {
|
|
908
|
+
return stamp(schema, INTENT_STAMPS[mode][describeIntent ? 1 : 0], host =>
|
|
909
|
+
injectIntentIntoSchema(host, mode, describeIntent),
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
|
|
831
913
|
function injectIntentIntoSchema(
|
|
832
914
|
schema: unknown,
|
|
833
915
|
mode: "require" | "optional" = "require",
|
|
@@ -907,12 +989,14 @@ export function normalizeTools(tools: AgentContext["tools"], options: NormalizeT
|
|
|
907
989
|
// re-inject `i` (without its hint, which `describeIntent: false` omits) so
|
|
908
990
|
// intent tracing keeps the field while no descriptions ride the wire.
|
|
909
991
|
if (pruneDescriptions) {
|
|
910
|
-
|
|
911
|
-
|
|
992
|
+
const stripped = stripSchemaDescriptions(toolWireSchema(t));
|
|
993
|
+
const parameters = (
|
|
994
|
+
doInjectIntent ? memoizedInjectIntentIntoSchema(stripped, intentMode, false) : stripped
|
|
995
|
+
) as TSchema;
|
|
912
996
|
return { ...t, parameters, description: "" };
|
|
913
997
|
}
|
|
914
|
-
|
|
915
|
-
|
|
998
|
+
const wire = toolWireSchema(t);
|
|
999
|
+
const parameters = (doInjectIntent ? memoizedInjectIntentIntoSchema(wire, intentMode, true) : wire) as TSchema;
|
|
916
1000
|
const description = t.description ?? "";
|
|
917
1001
|
const examplesBlock = renderToolExamples({ ...t, parameters }, doInjectIntent ? INTENT_FIELD : undefined);
|
|
918
1002
|
const finalDescription = examplesBlock ? `${description}\n\n${examplesBlock}` : description;
|
|
@@ -1813,6 +1897,13 @@ async function streamAssistantResponse(
|
|
|
1813
1897
|
|
|
1814
1898
|
let partialMessage: AssistantMessage | null = null;
|
|
1815
1899
|
let addedPartial = false;
|
|
1900
|
+
// Previous `message_update` snapshot for the incremental rebuild below;
|
|
1901
|
+
// null until the turn's `start` event seeds it.
|
|
1902
|
+
let turnSnapshot: AssistantMessage | null = null;
|
|
1903
|
+
// Content indices of blocks that started streaming but have not ended
|
|
1904
|
+
// yet — re-cloned on every delta because live blocks may be patched
|
|
1905
|
+
// without a paired event (Cursor's silent edit-block merge).
|
|
1906
|
+
const openBlocks = new Set<number>();
|
|
1816
1907
|
const completedToolCallIds = new Set<string>();
|
|
1817
1908
|
const argStreams = new Map<number, { id: string; stream: AgentToolArgStream }>();
|
|
1818
1909
|
const cancelArgStreams = (): void => {
|
|
@@ -2037,6 +2128,8 @@ async function streamAssistantResponse(
|
|
|
2037
2128
|
// consumer treats both as read-only, so cloning the identical partial
|
|
2038
2129
|
// twice per delta was pure waste.
|
|
2039
2130
|
const messageSnapshot = snapshotAssistantMessage(partialMessage);
|
|
2131
|
+
turnSnapshot = messageSnapshot;
|
|
2132
|
+
openBlocks.clear();
|
|
2040
2133
|
stream.push({
|
|
2041
2134
|
type: "message_update",
|
|
2042
2135
|
assistantMessageEvent: snapshotAssistantMessageEvent(event, messageSnapshot),
|
|
@@ -2045,7 +2138,8 @@ async function streamAssistantResponse(
|
|
|
2045
2138
|
} else {
|
|
2046
2139
|
context.messages.push(partialMessage);
|
|
2047
2140
|
addedPartial = true;
|
|
2048
|
-
|
|
2141
|
+
turnSnapshot = snapshotAssistantMessage(partialMessage);
|
|
2142
|
+
stream.push({ type: "message_start", message: turnSnapshot });
|
|
2049
2143
|
}
|
|
2050
2144
|
break;
|
|
2051
2145
|
|
|
@@ -2127,11 +2221,34 @@ async function streamAssistantResponse(
|
|
|
2127
2221
|
partialMessage = event.partial;
|
|
2128
2222
|
context.messages[context.messages.length - 1] = partialMessage;
|
|
2129
2223
|
config.onAssistantMessageEvent?.(partialMessage, event);
|
|
2130
|
-
//
|
|
2131
|
-
//
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2224
|
+
// Track which blocks are still streaming: open blocks are
|
|
2225
|
+
// re-cloned on every delta, finalized blocks are shared.
|
|
2226
|
+
const contentIndex = (event as { contentIndex?: number }).contentIndex;
|
|
2227
|
+
if (contentIndex !== undefined) {
|
|
2228
|
+
if (event.type.endsWith("_start")) openBlocks.add(contentIndex);
|
|
2229
|
+
else if (event.type.endsWith("_end")) openBlocks.delete(contentIndex);
|
|
2230
|
+
}
|
|
2231
|
+
// READ-ONLY-CONSUMER INVARIANT: `message` and
|
|
2232
|
+
// `assistantMessageEvent.partial` intentionally share one snapshot,
|
|
2233
|
+
// and the snapshot is rebuilt incrementally — only the delta's
|
|
2234
|
+
// block, open blocks, and newly appended blocks are deep-cloned;
|
|
2235
|
+
// finalized blocks are carried over from the previous snapshot by
|
|
2236
|
+
// reference (see `snapshotAssistantMessageIncremental`), so they are
|
|
2237
|
+
// shared across ALL message_update snapshots of the turn.
|
|
2238
|
+
// Consumers MUST treat both fields — and every content block inside
|
|
2239
|
+
// them — as read-only: mutating a snapshot would corrupt every
|
|
2240
|
+
// earlier and later snapshot of the turn, not just this one. In
|
|
2241
|
+
// exchange, per-delta work is proportional to the live stream
|
|
2242
|
+
// instead of the whole turn (issue #10605).
|
|
2243
|
+
const messageSnapshot: AssistantMessage = turnSnapshot
|
|
2244
|
+
? snapshotAssistantMessageIncremental(
|
|
2245
|
+
partialMessage,
|
|
2246
|
+
turnSnapshot,
|
|
2247
|
+
contentIndex ?? -1,
|
|
2248
|
+
openBlocks,
|
|
2249
|
+
)
|
|
2250
|
+
: snapshotAssistantMessage(partialMessage);
|
|
2251
|
+
turnSnapshot = messageSnapshot;
|
|
2135
2252
|
stream.push({
|
|
2136
2253
|
type: "message_update",
|
|
2137
2254
|
assistantMessageEvent: snapshotAssistantMessageEvent(event, messageSnapshot),
|
package/src/agent.ts
CHANGED
|
@@ -50,6 +50,7 @@ import type {
|
|
|
50
50
|
AgentToolContext,
|
|
51
51
|
AgentTurnEndContext,
|
|
52
52
|
AsideMessage,
|
|
53
|
+
PrepareQueuedMessages,
|
|
53
54
|
SpeculativeToolExecutionConfig,
|
|
54
55
|
StreamFn,
|
|
55
56
|
ToolCallContext,
|
|
@@ -362,6 +363,13 @@ interface CursorToolResultEntry {
|
|
|
362
363
|
pending?: Promise<void>;
|
|
363
364
|
}
|
|
364
365
|
|
|
366
|
+
type QueuedMessageQueue = "steering" | "followUp";
|
|
367
|
+
|
|
368
|
+
interface QueuedMessageClaim {
|
|
369
|
+
messages: AgentMessage[];
|
|
370
|
+
controller: AbortController;
|
|
371
|
+
}
|
|
372
|
+
|
|
365
373
|
export class Agent {
|
|
366
374
|
#state: AgentState = {
|
|
367
375
|
systemPrompt: [],
|
|
@@ -383,6 +391,14 @@ export class Agent {
|
|
|
383
391
|
#transformProviderContext?: (context: Context, model: Model) => Context | Promise<Context>;
|
|
384
392
|
#steeringQueue: AgentMessage[] = [];
|
|
385
393
|
#followUpQueue: AgentMessage[] = [];
|
|
394
|
+
#queuedMessageClaims: Partial<Record<QueuedMessageQueue, QueuedMessageClaim>> = {};
|
|
395
|
+
/** Dequeued originals remain recoverable until their transcript events arrive. */
|
|
396
|
+
#queuedMessageDeliveries = new Set<{
|
|
397
|
+
queue: QueuedMessageQueue;
|
|
398
|
+
controller: AbortController | undefined;
|
|
399
|
+
messages: AgentMessage[];
|
|
400
|
+
next: number;
|
|
401
|
+
}>();
|
|
386
402
|
#steeringWaiters = new Set<() => void>();
|
|
387
403
|
|
|
388
404
|
#steeringMode: "all" | "one-at-a-time";
|
|
@@ -449,6 +465,8 @@ export class Agent {
|
|
|
449
465
|
|
|
450
466
|
streamFn: StreamFn;
|
|
451
467
|
getApiKey?: (model: Model) => Promise<ApiKey | undefined> | ApiKey | undefined;
|
|
468
|
+
/** Prepare actual queue deliveries after dequeue gates; commit runs only while ownership remains valid. */
|
|
469
|
+
prepareQueuedMessages?: PrepareQueuedMessages;
|
|
452
470
|
/**
|
|
453
471
|
* Hook invoked after tool arguments are validated and before execution.
|
|
454
472
|
* Reassign at any time to swap the implementation (e.g. on extension reload).
|
|
@@ -844,16 +862,90 @@ export class Agent {
|
|
|
844
862
|
for (const hook of this.#beforeQueuedMessageDequeueHooks) await hook(signal);
|
|
845
863
|
}
|
|
846
864
|
|
|
847
|
-
async #dequeueSteeringMessagesAfterHooks(signal
|
|
848
|
-
if (signal
|
|
865
|
+
async #dequeueSteeringMessagesAfterHooks(signal: AbortSignal): Promise<AgentMessage[]> {
|
|
866
|
+
if (signal.aborted || this.#steeringQueue.length === 0) return [];
|
|
849
867
|
await this.#runBeforeQueuedMessageDequeueHooks(signal);
|
|
850
|
-
return signal
|
|
868
|
+
return signal.aborted ? [] : this.#prepareQueuedMessageBatch("steering", signal);
|
|
851
869
|
}
|
|
852
870
|
|
|
853
|
-
async #dequeueFollowUpMessagesAfterHooks(signal
|
|
854
|
-
if (signal
|
|
871
|
+
async #dequeueFollowUpMessagesAfterHooks(signal: AbortSignal): Promise<AgentMessage[]> {
|
|
872
|
+
if (signal.aborted || this.#followUpQueue.length === 0) return [];
|
|
855
873
|
await this.#runBeforeQueuedMessageDequeueHooks(signal);
|
|
856
|
-
return signal
|
|
874
|
+
return signal.aborted ? [] : this.#prepareQueuedMessageBatch("followUp", signal);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
async #prepareQueuedMessageBatch(queue: QueuedMessageQueue, signal: AbortSignal): Promise<AgentMessage[]> {
|
|
878
|
+
if (this.#queuedMessageClaims[queue]) return [];
|
|
879
|
+
const messages = queue === "steering" ? this.#dequeueSteeringMessages() : this.#dequeueFollowUpMessages();
|
|
880
|
+
const prepare = this.prepareQueuedMessages;
|
|
881
|
+
if (messages.length === 0) return messages;
|
|
882
|
+
const runController = this.#abortController;
|
|
883
|
+
if (!prepare) {
|
|
884
|
+
this.#queuedMessageDeliveries.add({ queue, controller: runController, messages, next: 0 });
|
|
885
|
+
return messages;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const claim: QueuedMessageClaim = { messages, controller: new AbortController() };
|
|
889
|
+
this.#queuedMessageClaims[queue] = claim;
|
|
890
|
+
const preparationSignal = AbortSignal.any([signal, claim.controller.signal]);
|
|
891
|
+
try {
|
|
892
|
+
const preparation = await prepare(messages, preparationSignal);
|
|
893
|
+
signal.throwIfAborted();
|
|
894
|
+
if (preparationSignal.aborted || this.#queuedMessageClaims[queue] !== claim) return [];
|
|
895
|
+
const additional = preparation?.commit();
|
|
896
|
+
if (preparationSignal.aborted || this.#queuedMessageClaims[queue] !== claim) return [];
|
|
897
|
+
if (preparation && additional === undefined) {
|
|
898
|
+
// Stop this attempt before the loop can immediately reclaim the restored batch.
|
|
899
|
+
runController?.abort();
|
|
900
|
+
throw new DOMException("Queued message preparation cancelled", "AbortError");
|
|
901
|
+
}
|
|
902
|
+
delete this.#queuedMessageClaims[queue];
|
|
903
|
+
this.#queuedMessageDeliveries.add({ queue, controller: runController, messages, next: 0 });
|
|
904
|
+
return additional?.length ? [...messages, ...additional] : messages;
|
|
905
|
+
} catch (error) {
|
|
906
|
+
if (signal.aborted) throw error;
|
|
907
|
+
if (preparationSignal.aborted || this.#queuedMessageClaims[queue] !== claim) return [];
|
|
908
|
+
throw error;
|
|
909
|
+
} finally {
|
|
910
|
+
if (this.#queuedMessageClaims[queue] === claim) this.#cancelQueuedMessagePreparation(queue, true);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
#cancelQueuedMessagePreparation(queue: QueuedMessageQueue, restore = false): void {
|
|
915
|
+
if (!restore) {
|
|
916
|
+
for (const delivery of this.#queuedMessageDeliveries) {
|
|
917
|
+
if (delivery.queue === queue) this.#queuedMessageDeliveries.delete(delivery);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
const claim = this.#queuedMessageClaims[queue];
|
|
921
|
+
if (!claim) return;
|
|
922
|
+
delete this.#queuedMessageClaims[queue];
|
|
923
|
+
if (restore) {
|
|
924
|
+
if (queue === "steering") {
|
|
925
|
+
this.#steeringQueue = [...claim.messages, ...this.#steeringQueue];
|
|
926
|
+
this.#notifySteeringWaiters();
|
|
927
|
+
} else {
|
|
928
|
+
this.#followUpQueue = [...claim.messages, ...this.#followUpQueue];
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
claim.controller.abort();
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
#restoreUndeliveredQueuedMessages(controller: AbortController): void {
|
|
935
|
+
if (this.#queuedMessageDeliveries.size === 0) return;
|
|
936
|
+
const restored: Record<QueuedMessageQueue, AgentMessage[]> = { steering: [], followUp: [] };
|
|
937
|
+
for (const delivery of this.#queuedMessageDeliveries) {
|
|
938
|
+
if (delivery.controller !== controller) continue;
|
|
939
|
+
this.#queuedMessageDeliveries.delete(delivery);
|
|
940
|
+
for (let i = delivery.next; i < delivery.messages.length; i++) {
|
|
941
|
+
restored[delivery.queue].push(delivery.messages[i]);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
if (restored.steering.length > 0) {
|
|
945
|
+
this.#steeringQueue = [...restored.steering, ...this.#steeringQueue];
|
|
946
|
+
this.#notifySteeringWaiters();
|
|
947
|
+
}
|
|
948
|
+
if (restored.followUp.length > 0) this.#followUpQueue = [...restored.followUp, ...this.#followUpQueue];
|
|
857
949
|
}
|
|
858
950
|
|
|
859
951
|
setProviderResponseInterceptor(fn: SimpleStreamOptions["onResponse"] | undefined): void {
|
|
@@ -988,11 +1080,18 @@ export class Agent {
|
|
|
988
1080
|
replaceQueues(steering: AgentMessage[], followUp: AgentMessage[]) {
|
|
989
1081
|
this.#steeringQueue = steering.slice();
|
|
990
1082
|
this.#followUpQueue = followUp.slice();
|
|
1083
|
+
this.#cancelQueuedMessagePreparation("steering");
|
|
1084
|
+
this.#cancelQueuedMessagePreparation("followUp");
|
|
991
1085
|
this.#notifySteeringWaiters();
|
|
992
1086
|
}
|
|
993
1087
|
|
|
994
1088
|
appendMessage(m: AgentMessage) {
|
|
995
1089
|
this.#state.messages.push(m);
|
|
1090
|
+
for (const delivery of this.#queuedMessageDeliveries) {
|
|
1091
|
+
if (delivery.messages[delivery.next] !== m) continue;
|
|
1092
|
+
if (++delivery.next === delivery.messages.length) this.#queuedMessageDeliveries.delete(delivery);
|
|
1093
|
+
break;
|
|
1094
|
+
}
|
|
996
1095
|
}
|
|
997
1096
|
|
|
998
1097
|
popMessage(): AgentMessage | undefined {
|
|
@@ -1022,11 +1121,13 @@ export class Agent {
|
|
|
1022
1121
|
|
|
1023
1122
|
clearSteeringQueue() {
|
|
1024
1123
|
this.#steeringQueue = [];
|
|
1124
|
+
this.#cancelQueuedMessagePreparation("steering");
|
|
1025
1125
|
this.#notifySteeringWaiters();
|
|
1026
1126
|
}
|
|
1027
1127
|
|
|
1028
1128
|
clearFollowUpQueue() {
|
|
1029
1129
|
this.#followUpQueue = [];
|
|
1130
|
+
this.#cancelQueuedMessagePreparation("followUp");
|
|
1030
1131
|
}
|
|
1031
1132
|
|
|
1032
1133
|
/**
|
|
@@ -1041,26 +1142,36 @@ export class Agent {
|
|
|
1041
1142
|
clearAllQueues() {
|
|
1042
1143
|
this.#steeringQueue = [];
|
|
1043
1144
|
this.#followUpQueue = [];
|
|
1145
|
+
this.#cancelQueuedMessagePreparation("steering");
|
|
1146
|
+
this.#cancelQueuedMessagePreparation("followUp");
|
|
1044
1147
|
this.#notifySteeringWaiters();
|
|
1045
1148
|
this.clearDeferredToolDirectives();
|
|
1046
1149
|
}
|
|
1047
1150
|
|
|
1048
1151
|
hasQueuedMessages(): boolean {
|
|
1049
|
-
return
|
|
1152
|
+
return (
|
|
1153
|
+
this.#steeringQueue.length > 0 ||
|
|
1154
|
+
this.#followUpQueue.length > 0 ||
|
|
1155
|
+
this.#queuedMessageClaims.steering !== undefined ||
|
|
1156
|
+
this.#queuedMessageClaims.followUp !== undefined
|
|
1157
|
+
);
|
|
1050
1158
|
}
|
|
1051
1159
|
|
|
1052
1160
|
/** Non-consuming view of the pending steering queue (insertion order, newest
|
|
1053
1161
|
* last). The session layer derives its queued-message display/count from
|
|
1054
1162
|
* this live view instead of a mirror, so the agent-core queue stays the
|
|
1055
|
-
* single source of truth.
|
|
1163
|
+
* single source of truth. Includes exclusively claimed originals while
|
|
1164
|
+
* preparation is pending, so editor restoration can cancel their delivery. */
|
|
1056
1165
|
peekSteeringQueue(): readonly AgentMessage[] {
|
|
1057
|
-
|
|
1166
|
+
const claim = this.#queuedMessageClaims.steering;
|
|
1167
|
+
return claim ? [...claim.messages, ...this.#steeringQueue] : this.#steeringQueue;
|
|
1058
1168
|
}
|
|
1059
1169
|
|
|
1060
1170
|
/** Non-consuming view of the pending follow-up queue. See
|
|
1061
1171
|
* {@link peekSteeringQueue}. */
|
|
1062
1172
|
peekFollowUpQueue(): readonly AgentMessage[] {
|
|
1063
|
-
|
|
1173
|
+
const claim = this.#queuedMessageClaims.followUp;
|
|
1174
|
+
return claim ? [...claim.messages, ...this.#followUpQueue] : this.#followUpQueue;
|
|
1064
1175
|
}
|
|
1065
1176
|
|
|
1066
1177
|
/** Nonblocking snapshot of the latest results, including provisional payloads while transforms are pending. */
|
|
@@ -1107,6 +1218,7 @@ export class Agent {
|
|
|
1107
1218
|
* Used by dequeue keybinding.
|
|
1108
1219
|
*/
|
|
1109
1220
|
popLastSteer(): AgentMessage | undefined {
|
|
1221
|
+
if (this.#steeringQueue.length === 0) this.#cancelQueuedMessagePreparation("steering", true);
|
|
1110
1222
|
return this.#steeringQueue.pop();
|
|
1111
1223
|
}
|
|
1112
1224
|
|
|
@@ -1115,6 +1227,7 @@ export class Agent {
|
|
|
1115
1227
|
* Used by dequeue keybinding.
|
|
1116
1228
|
*/
|
|
1117
1229
|
popLastFollowUp(): AgentMessage | undefined {
|
|
1230
|
+
if (this.#followUpQueue.length === 0) this.#cancelQueuedMessagePreparation("followUp", true);
|
|
1118
1231
|
return this.#followUpQueue.pop();
|
|
1119
1232
|
}
|
|
1120
1233
|
|
|
@@ -1154,15 +1267,19 @@ export class Agent {
|
|
|
1154
1267
|
}
|
|
1155
1268
|
|
|
1156
1269
|
reset() {
|
|
1270
|
+
if (this.#queuedMessageClaims.steering || this.#queuedMessageClaims.followUp) {
|
|
1271
|
+
this.#abortController?.abort();
|
|
1272
|
+
this.#abortController = undefined;
|
|
1273
|
+
this.#resolveRunningPrompt?.();
|
|
1274
|
+
this.#runningPrompt = undefined;
|
|
1275
|
+
this.#resolveRunningPrompt = undefined;
|
|
1276
|
+
}
|
|
1157
1277
|
this.#state.messages.length = 0;
|
|
1158
1278
|
this.#state.isStreaming = false;
|
|
1159
1279
|
this.#state.streamMessage = null;
|
|
1160
1280
|
this.#state.pendingToolCalls.clear();
|
|
1161
1281
|
this.#state.error = undefined;
|
|
1162
|
-
this
|
|
1163
|
-
this.#followUpQueue = [];
|
|
1164
|
-
this.#notifySteeringWaiters();
|
|
1165
|
-
this.clearDeferredToolDirectives();
|
|
1282
|
+
this.clearAllQueues();
|
|
1166
1283
|
}
|
|
1167
1284
|
|
|
1168
1285
|
/** Send a prompt with an AgentMessage */
|
|
@@ -1250,7 +1367,7 @@ export class Agent {
|
|
|
1250
1367
|
this.#state.error = undefined;
|
|
1251
1368
|
|
|
1252
1369
|
try {
|
|
1253
|
-
const dequeueSignal = this.#continuationDequeueSignal(signal);
|
|
1370
|
+
const dequeueSignal = this.#continuationDequeueSignal(signal) ?? continuationAbortController.signal;
|
|
1254
1371
|
const messages = this.#state.messages;
|
|
1255
1372
|
if (messages.length === 0) {
|
|
1256
1373
|
// An empty transcript has nothing to resume, but a queued steer/follow-up
|
|
@@ -1260,11 +1377,13 @@ export class Agent {
|
|
|
1260
1377
|
// microtask because hasQueuedMessages() never clears, spinning an unbounded
|
|
1261
1378
|
// allocation loop until OOM (issue #6344).
|
|
1262
1379
|
const queuedSteering = await this.#dequeueSteeringMessagesAfterHooks(dequeueSignal);
|
|
1380
|
+
if (this.#abortController !== continuationAbortController) return;
|
|
1263
1381
|
if (queuedSteering.length > 0) {
|
|
1264
1382
|
await this.#runLoop(queuedSteering, { skipInitialSteeringPoll: true }, signal, true);
|
|
1265
1383
|
return;
|
|
1266
1384
|
}
|
|
1267
1385
|
const queuedFollowUp = await this.#dequeueFollowUpMessagesAfterHooks(dequeueSignal);
|
|
1386
|
+
if (this.#abortController !== continuationAbortController) return;
|
|
1268
1387
|
if (queuedFollowUp.length > 0) {
|
|
1269
1388
|
await this.#runLoop(queuedFollowUp, undefined, signal, true);
|
|
1270
1389
|
return;
|
|
@@ -1282,12 +1401,14 @@ export class Agent {
|
|
|
1282
1401
|
return;
|
|
1283
1402
|
}
|
|
1284
1403
|
const queuedSteering = await this.#dequeueSteeringMessagesAfterHooks(dequeueSignal);
|
|
1404
|
+
if (this.#abortController !== continuationAbortController) return;
|
|
1285
1405
|
if (queuedSteering.length > 0) {
|
|
1286
1406
|
await this.#runLoop(queuedSteering, { skipInitialSteeringPoll: true }, signal, true);
|
|
1287
1407
|
return;
|
|
1288
1408
|
}
|
|
1289
1409
|
|
|
1290
1410
|
const queuedFollowUp = await this.#dequeueFollowUpMessagesAfterHooks(dequeueSignal);
|
|
1411
|
+
if (this.#abortController !== continuationAbortController) return;
|
|
1291
1412
|
if (queuedFollowUp.length > 0) {
|
|
1292
1413
|
await this.#runLoop(queuedFollowUp, undefined, signal, true);
|
|
1293
1414
|
return;
|
|
@@ -1298,6 +1419,7 @@ export class Agent {
|
|
|
1298
1419
|
|
|
1299
1420
|
await this.#runLoop(undefined, undefined, signal, true);
|
|
1300
1421
|
} finally {
|
|
1422
|
+
this.#restoreUndeliveredQueuedMessages(continuationAbortController);
|
|
1301
1423
|
resolve();
|
|
1302
1424
|
if (this.#abortController === continuationAbortController) {
|
|
1303
1425
|
this.#state.isStreaming = false;
|
|
@@ -1513,7 +1635,7 @@ export class Agent {
|
|
|
1513
1635
|
skipInitialSteeringPoll = false;
|
|
1514
1636
|
return [];
|
|
1515
1637
|
}
|
|
1516
|
-
return this.#dequeueSteeringMessagesAfterHooks(signal);
|
|
1638
|
+
return this.#dequeueSteeringMessagesAfterHooks(signal ?? loopSignal);
|
|
1517
1639
|
},
|
|
1518
1640
|
hasSteeringMessages: () => {
|
|
1519
1641
|
if (this.#steeringQueue.length === 0) {
|
|
@@ -1538,7 +1660,7 @@ export class Agent {
|
|
|
1538
1660
|
},
|
|
1539
1661
|
waitForSteeringMessages: signal => this.#waitForSteeringMessages(signal),
|
|
1540
1662
|
hasIrcInterrupts: this.hasIrcInterrupts,
|
|
1541
|
-
getFollowUpMessages: signal => this.#dequeueFollowUpMessagesAfterHooks(signal),
|
|
1663
|
+
getFollowUpMessages: signal => this.#dequeueFollowUpMessagesAfterHooks(signal ?? loopSignal),
|
|
1542
1664
|
getAsideMessages: async () => (await this.#asideMessageProvider?.()) ?? [],
|
|
1543
1665
|
onBeforeYield: () => this.#onBeforeYield?.(),
|
|
1544
1666
|
telemetry: this.#telemetry,
|
|
@@ -1554,6 +1676,7 @@ export class Agent {
|
|
|
1554
1676
|
: agentLoopContinue(context, config, loopSignal, this.streamFn);
|
|
1555
1677
|
|
|
1556
1678
|
for await (const event of stream) {
|
|
1679
|
+
if (this.#abortController !== loopAbortController) return;
|
|
1557
1680
|
if (event.type === "turn_start") turnOpen = true;
|
|
1558
1681
|
if (event.type === "turn_end") turnOpen = false;
|
|
1559
1682
|
// Update internal state based on events
|
|
@@ -1624,6 +1747,7 @@ export class Agent {
|
|
|
1624
1747
|
}
|
|
1625
1748
|
}
|
|
1626
1749
|
} catch (err) {
|
|
1750
|
+
if (this.#abortController !== loopAbortController) return;
|
|
1627
1751
|
const stoppedForAbort = loopSignal.aborted;
|
|
1628
1752
|
const errorMessage = stoppedForAbort
|
|
1629
1753
|
? abortReasonText(loopSignal)
|
|
@@ -1728,6 +1852,7 @@ export class Agent {
|
|
|
1728
1852
|
this.#emit({ type: "agent_end", messages: [errorMsg] });
|
|
1729
1853
|
}
|
|
1730
1854
|
} finally {
|
|
1855
|
+
this.#restoreUndeliveredQueuedMessages(loopAbortController);
|
|
1731
1856
|
resolveRun?.();
|
|
1732
1857
|
if (this.#abortController === loopAbortController) {
|
|
1733
1858
|
this.#state.isStreaming = false;
|