@linxiraos/pi-agent-core 1.1.14 → 1.1.16
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 +169 -26
- package/THIRD-PARTY-NOTICES.txt +54 -25
- package/dist/types/agent.d.ts +17 -4
- package/dist/types/append-only-context.d.ts +0 -4
- package/dist/types/compaction/anthropic.d.ts +108 -0
- package/dist/types/compaction/compaction-v2-streaming.d.ts +2 -2
- package/dist/types/compaction/compaction.d.ts +33 -18
- package/dist/types/compaction/entries.d.ts +2 -0
- package/dist/types/compaction/index.d.ts +1 -0
- package/dist/types/compaction/openai.d.ts +9 -8
- package/dist/types/index.d.ts +1 -0
- package/dist/types/speculative-execution.d.ts +63 -0
- package/dist/types/thinking.d.ts +1 -1
- package/dist/types/types.d.ts +212 -3
- package/package.json +10 -10
- package/src/agent-loop.ts +582 -86
- package/src/agent.ts +183 -38
- package/src/append-only-context.ts +122 -1
- package/src/compaction/anthropic.ts +310 -0
- package/src/compaction/compaction-v2-streaming.ts +4 -3
- package/src/compaction/compaction.ts +299 -131
- package/src/compaction/entries.ts +2 -0
- package/src/compaction/index.ts +1 -0
- package/src/compaction/openai.ts +17 -10
- package/src/compaction/prompts/anthropic-compaction-instructions.md +17 -0
- package/src/index.ts +2 -0
- package/src/proxy.ts +49 -8
- package/src/speculative-execution.ts +953 -0
- package/src/telemetry.ts +1 -0
- package/src/thinking.ts +1 -1
- package/src/tokenizer.ts +11 -2
- package/src/types.ts +232 -4
package/src/agent.ts
CHANGED
|
@@ -50,6 +50,8 @@ import type {
|
|
|
50
50
|
AgentToolContext,
|
|
51
51
|
AgentTurnEndContext,
|
|
52
52
|
AsideMessage,
|
|
53
|
+
PrepareQueuedMessages,
|
|
54
|
+
SpeculativeToolExecutionConfig,
|
|
53
55
|
StreamFn,
|
|
54
56
|
ToolCallContext,
|
|
55
57
|
ToolChoiceDirective,
|
|
@@ -242,13 +244,22 @@ export interface AgentOptions {
|
|
|
242
244
|
* Use for deobfuscating secrets or rewriting arguments.
|
|
243
245
|
*/
|
|
244
246
|
transformToolCallArguments?: (args: Record<string, unknown>, toolName: string) => Record<string, unknown>;
|
|
247
|
+
/** Host authorization and telemetry for opt-in speculative tool execution. */
|
|
248
|
+
speculativeToolExecution?: SpeculativeToolExecutionConfig;
|
|
245
249
|
|
|
246
250
|
/**
|
|
247
251
|
* Resolve a tool call whose name matched no advertised tool. Lets hosts
|
|
248
252
|
* route calls to tools exposed through side transports (e.g. `xd://`
|
|
249
253
|
* device mounts) instead of failing with "Tool not found".
|
|
250
254
|
*/
|
|
251
|
-
resolveFallbackTool?: (name: string) => AgentTool<any> | undefined;
|
|
255
|
+
resolveFallbackTool?: (name: string, advertised: readonly AgentTool<any>[]) => AgentTool<any> | undefined;
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Names routable by {@link resolveFallbackTool} that the advertised set
|
|
259
|
+
* omits (e.g. `xd://` device mounts), used only to suggest a target when a
|
|
260
|
+
* call misses.
|
|
261
|
+
*/
|
|
262
|
+
suggestFallbackToolNames?: () => Iterable<string>;
|
|
252
263
|
|
|
253
264
|
/** Enable intent tracing schema injection/stripping in the harness. */
|
|
254
265
|
intentTracing?: boolean;
|
|
@@ -352,6 +363,13 @@ interface CursorToolResultEntry {
|
|
|
352
363
|
pending?: Promise<void>;
|
|
353
364
|
}
|
|
354
365
|
|
|
366
|
+
type QueuedMessageQueue = "steering" | "followUp";
|
|
367
|
+
|
|
368
|
+
interface QueuedMessageClaim {
|
|
369
|
+
messages: AgentMessage[];
|
|
370
|
+
controller: AbortController;
|
|
371
|
+
}
|
|
372
|
+
|
|
355
373
|
export class Agent {
|
|
356
374
|
#state: AgentState = {
|
|
357
375
|
systemPrompt: [],
|
|
@@ -373,6 +391,14 @@ export class Agent {
|
|
|
373
391
|
#transformProviderContext?: (context: Context, model: Model) => Context | Promise<Context>;
|
|
374
392
|
#steeringQueue: AgentMessage[] = [];
|
|
375
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
|
+
}>();
|
|
376
402
|
#steeringWaiters = new Set<() => void>();
|
|
377
403
|
|
|
378
404
|
#steeringMode: "all" | "one-at-a-time";
|
|
@@ -407,7 +433,9 @@ export class Agent {
|
|
|
407
433
|
#kimiApiFormat?: "openai" | "anthropic";
|
|
408
434
|
#preferWebsockets?: boolean;
|
|
409
435
|
#transformToolCallArguments?: (args: Record<string, unknown>, toolName: string) => Record<string, unknown>;
|
|
410
|
-
#
|
|
436
|
+
#speculativeToolExecution?: SpeculativeToolExecutionConfig;
|
|
437
|
+
#resolveFallbackTool?: (name: string, advertised: readonly AgentTool<any>[]) => AgentTool<any> | undefined;
|
|
438
|
+
#suggestFallbackToolNames?: () => Iterable<string>;
|
|
411
439
|
#intentTracing: boolean;
|
|
412
440
|
#pruneToolDescriptions: boolean;
|
|
413
441
|
#dialect?: Dialect;
|
|
@@ -433,9 +461,12 @@ export class Agent {
|
|
|
433
461
|
|
|
434
462
|
/** Buffered Cursor tool results with text length at time of call (for correct ordering) */
|
|
435
463
|
#cursorToolResultBuffer: CursorToolResultEntry[] = [];
|
|
464
|
+
#cursorToolResultDrain: CursorToolResultEntry[] | undefined;
|
|
436
465
|
|
|
437
466
|
streamFn: StreamFn;
|
|
438
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;
|
|
439
470
|
/**
|
|
440
471
|
* Hook invoked after tool arguments are validated and before execution.
|
|
441
472
|
* Reassign at any time to swap the implementation (e.g. on extension reload).
|
|
@@ -496,7 +527,9 @@ export class Agent {
|
|
|
496
527
|
this.#kimiApiFormat = opts.kimiApiFormat;
|
|
497
528
|
this.#preferWebsockets = opts.preferWebsockets;
|
|
498
529
|
this.#transformToolCallArguments = opts.transformToolCallArguments;
|
|
530
|
+
this.#speculativeToolExecution = opts.speculativeToolExecution;
|
|
499
531
|
this.#resolveFallbackTool = opts.resolveFallbackTool;
|
|
532
|
+
this.#suggestFallbackToolNames = opts.suggestFallbackToolNames;
|
|
500
533
|
this.#intentTracing = opts.intentTracing === true;
|
|
501
534
|
this.#pruneToolDescriptions = opts.pruneToolDescriptions === true;
|
|
502
535
|
this.#dialect = opts.dialect;
|
|
@@ -829,16 +862,90 @@ export class Agent {
|
|
|
829
862
|
for (const hook of this.#beforeQueuedMessageDequeueHooks) await hook(signal);
|
|
830
863
|
}
|
|
831
864
|
|
|
832
|
-
async #dequeueSteeringMessagesAfterHooks(signal
|
|
833
|
-
if (signal
|
|
865
|
+
async #dequeueSteeringMessagesAfterHooks(signal: AbortSignal): Promise<AgentMessage[]> {
|
|
866
|
+
if (signal.aborted || this.#steeringQueue.length === 0) return [];
|
|
834
867
|
await this.#runBeforeQueuedMessageDequeueHooks(signal);
|
|
835
|
-
return signal
|
|
868
|
+
return signal.aborted ? [] : this.#prepareQueuedMessageBatch("steering", signal);
|
|
836
869
|
}
|
|
837
870
|
|
|
838
|
-
async #dequeueFollowUpMessagesAfterHooks(signal
|
|
839
|
-
if (signal
|
|
871
|
+
async #dequeueFollowUpMessagesAfterHooks(signal: AbortSignal): Promise<AgentMessage[]> {
|
|
872
|
+
if (signal.aborted || this.#followUpQueue.length === 0) return [];
|
|
840
873
|
await this.#runBeforeQueuedMessageDequeueHooks(signal);
|
|
841
|
-
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];
|
|
842
949
|
}
|
|
843
950
|
|
|
844
951
|
setProviderResponseInterceptor(fn: SimpleStreamOptions["onResponse"] | undefined): void {
|
|
@@ -973,11 +1080,18 @@ export class Agent {
|
|
|
973
1080
|
replaceQueues(steering: AgentMessage[], followUp: AgentMessage[]) {
|
|
974
1081
|
this.#steeringQueue = steering.slice();
|
|
975
1082
|
this.#followUpQueue = followUp.slice();
|
|
1083
|
+
this.#cancelQueuedMessagePreparation("steering");
|
|
1084
|
+
this.#cancelQueuedMessagePreparation("followUp");
|
|
976
1085
|
this.#notifySteeringWaiters();
|
|
977
1086
|
}
|
|
978
1087
|
|
|
979
1088
|
appendMessage(m: AgentMessage) {
|
|
980
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
|
+
}
|
|
981
1095
|
}
|
|
982
1096
|
|
|
983
1097
|
popMessage(): AgentMessage | undefined {
|
|
@@ -1007,11 +1121,13 @@ export class Agent {
|
|
|
1007
1121
|
|
|
1008
1122
|
clearSteeringQueue() {
|
|
1009
1123
|
this.#steeringQueue = [];
|
|
1124
|
+
this.#cancelQueuedMessagePreparation("steering");
|
|
1010
1125
|
this.#notifySteeringWaiters();
|
|
1011
1126
|
}
|
|
1012
1127
|
|
|
1013
1128
|
clearFollowUpQueue() {
|
|
1014
1129
|
this.#followUpQueue = [];
|
|
1130
|
+
this.#cancelQueuedMessagePreparation("followUp");
|
|
1015
1131
|
}
|
|
1016
1132
|
|
|
1017
1133
|
/**
|
|
@@ -1026,26 +1142,43 @@ export class Agent {
|
|
|
1026
1142
|
clearAllQueues() {
|
|
1027
1143
|
this.#steeringQueue = [];
|
|
1028
1144
|
this.#followUpQueue = [];
|
|
1145
|
+
this.#cancelQueuedMessagePreparation("steering");
|
|
1146
|
+
this.#cancelQueuedMessagePreparation("followUp");
|
|
1029
1147
|
this.#notifySteeringWaiters();
|
|
1030
1148
|
this.clearDeferredToolDirectives();
|
|
1031
1149
|
}
|
|
1032
1150
|
|
|
1033
1151
|
hasQueuedMessages(): boolean {
|
|
1034
|
-
return
|
|
1152
|
+
return (
|
|
1153
|
+
this.#steeringQueue.length > 0 ||
|
|
1154
|
+
this.#followUpQueue.length > 0 ||
|
|
1155
|
+
this.#queuedMessageClaims.steering !== undefined ||
|
|
1156
|
+
this.#queuedMessageClaims.followUp !== undefined
|
|
1157
|
+
);
|
|
1035
1158
|
}
|
|
1036
1159
|
|
|
1037
1160
|
/** Non-consuming view of the pending steering queue (insertion order, newest
|
|
1038
1161
|
* last). The session layer derives its queued-message display/count from
|
|
1039
1162
|
* this live view instead of a mirror, so the agent-core queue stays the
|
|
1040
|
-
* 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. */
|
|
1041
1165
|
peekSteeringQueue(): readonly AgentMessage[] {
|
|
1042
|
-
|
|
1166
|
+
const claim = this.#queuedMessageClaims.steering;
|
|
1167
|
+
return claim ? [...claim.messages, ...this.#steeringQueue] : this.#steeringQueue;
|
|
1043
1168
|
}
|
|
1044
1169
|
|
|
1045
1170
|
/** Non-consuming view of the pending follow-up queue. See
|
|
1046
1171
|
* {@link peekSteeringQueue}. */
|
|
1047
1172
|
peekFollowUpQueue(): readonly AgentMessage[] {
|
|
1048
|
-
|
|
1173
|
+
const claim = this.#queuedMessageClaims.followUp;
|
|
1174
|
+
return claim ? [...claim.messages, ...this.#followUpQueue] : this.#followUpQueue;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/** Nonblocking snapshot of the latest results, including provisional payloads while transforms are pending. */
|
|
1178
|
+
getPendingToolResults(): readonly ToolResultMessage[] {
|
|
1179
|
+
const results = this.#cursorToolResultDrain?.map(({ toolResult }) => toolResult) ?? [];
|
|
1180
|
+
for (const { toolResult } of this.#cursorToolResultBuffer) results.push(toolResult);
|
|
1181
|
+
return results;
|
|
1049
1182
|
}
|
|
1050
1183
|
|
|
1051
1184
|
get isAborting(): boolean {
|
|
@@ -1085,6 +1218,7 @@ export class Agent {
|
|
|
1085
1218
|
* Used by dequeue keybinding.
|
|
1086
1219
|
*/
|
|
1087
1220
|
popLastSteer(): AgentMessage | undefined {
|
|
1221
|
+
if (this.#steeringQueue.length === 0) this.#cancelQueuedMessagePreparation("steering", true);
|
|
1088
1222
|
return this.#steeringQueue.pop();
|
|
1089
1223
|
}
|
|
1090
1224
|
|
|
@@ -1093,6 +1227,7 @@ export class Agent {
|
|
|
1093
1227
|
* Used by dequeue keybinding.
|
|
1094
1228
|
*/
|
|
1095
1229
|
popLastFollowUp(): AgentMessage | undefined {
|
|
1230
|
+
if (this.#followUpQueue.length === 0) this.#cancelQueuedMessagePreparation("followUp", true);
|
|
1096
1231
|
return this.#followUpQueue.pop();
|
|
1097
1232
|
}
|
|
1098
1233
|
|
|
@@ -1132,15 +1267,19 @@ export class Agent {
|
|
|
1132
1267
|
}
|
|
1133
1268
|
|
|
1134
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
|
+
}
|
|
1135
1277
|
this.#state.messages.length = 0;
|
|
1136
1278
|
this.#state.isStreaming = false;
|
|
1137
1279
|
this.#state.streamMessage = null;
|
|
1138
1280
|
this.#state.pendingToolCalls.clear();
|
|
1139
1281
|
this.#state.error = undefined;
|
|
1140
|
-
this
|
|
1141
|
-
this.#followUpQueue = [];
|
|
1142
|
-
this.#notifySteeringWaiters();
|
|
1143
|
-
this.clearDeferredToolDirectives();
|
|
1282
|
+
this.clearAllQueues();
|
|
1144
1283
|
}
|
|
1145
1284
|
|
|
1146
1285
|
/** Send a prompt with an AgentMessage */
|
|
@@ -1228,7 +1367,7 @@ export class Agent {
|
|
|
1228
1367
|
this.#state.error = undefined;
|
|
1229
1368
|
|
|
1230
1369
|
try {
|
|
1231
|
-
const dequeueSignal = this.#continuationDequeueSignal(signal);
|
|
1370
|
+
const dequeueSignal = this.#continuationDequeueSignal(signal) ?? continuationAbortController.signal;
|
|
1232
1371
|
const messages = this.#state.messages;
|
|
1233
1372
|
if (messages.length === 0) {
|
|
1234
1373
|
// An empty transcript has nothing to resume, but a queued steer/follow-up
|
|
@@ -1238,11 +1377,13 @@ export class Agent {
|
|
|
1238
1377
|
// microtask because hasQueuedMessages() never clears, spinning an unbounded
|
|
1239
1378
|
// allocation loop until OOM (issue #6344).
|
|
1240
1379
|
const queuedSteering = await this.#dequeueSteeringMessagesAfterHooks(dequeueSignal);
|
|
1380
|
+
if (this.#abortController !== continuationAbortController) return;
|
|
1241
1381
|
if (queuedSteering.length > 0) {
|
|
1242
1382
|
await this.#runLoop(queuedSteering, { skipInitialSteeringPoll: true }, signal, true);
|
|
1243
1383
|
return;
|
|
1244
1384
|
}
|
|
1245
1385
|
const queuedFollowUp = await this.#dequeueFollowUpMessagesAfterHooks(dequeueSignal);
|
|
1386
|
+
if (this.#abortController !== continuationAbortController) return;
|
|
1246
1387
|
if (queuedFollowUp.length > 0) {
|
|
1247
1388
|
await this.#runLoop(queuedFollowUp, undefined, signal, true);
|
|
1248
1389
|
return;
|
|
@@ -1260,12 +1401,14 @@ export class Agent {
|
|
|
1260
1401
|
return;
|
|
1261
1402
|
}
|
|
1262
1403
|
const queuedSteering = await this.#dequeueSteeringMessagesAfterHooks(dequeueSignal);
|
|
1404
|
+
if (this.#abortController !== continuationAbortController) return;
|
|
1263
1405
|
if (queuedSteering.length > 0) {
|
|
1264
1406
|
await this.#runLoop(queuedSteering, { skipInitialSteeringPoll: true }, signal, true);
|
|
1265
1407
|
return;
|
|
1266
1408
|
}
|
|
1267
1409
|
|
|
1268
1410
|
const queuedFollowUp = await this.#dequeueFollowUpMessagesAfterHooks(dequeueSignal);
|
|
1411
|
+
if (this.#abortController !== continuationAbortController) return;
|
|
1269
1412
|
if (queuedFollowUp.length > 0) {
|
|
1270
1413
|
await this.#runLoop(queuedFollowUp, undefined, signal, true);
|
|
1271
1414
|
return;
|
|
@@ -1276,6 +1419,7 @@ export class Agent {
|
|
|
1276
1419
|
|
|
1277
1420
|
await this.#runLoop(undefined, undefined, signal, true);
|
|
1278
1421
|
} finally {
|
|
1422
|
+
this.#restoreUndeliveredQueuedMessages(continuationAbortController);
|
|
1279
1423
|
resolve();
|
|
1280
1424
|
if (this.#abortController === continuationAbortController) {
|
|
1281
1425
|
this.#state.isStreaming = false;
|
|
@@ -1461,7 +1605,9 @@ export class Agent {
|
|
|
1461
1605
|
cwd: this.#cwd,
|
|
1462
1606
|
getCwd: this.#cwdResolver,
|
|
1463
1607
|
transformToolCallArguments: this.#transformToolCallArguments,
|
|
1608
|
+
speculativeToolExecution: this.#speculativeToolExecution,
|
|
1464
1609
|
resolveFallbackTool: this.#resolveFallbackTool,
|
|
1610
|
+
suggestFallbackToolNames: this.#suggestFallbackToolNames,
|
|
1465
1611
|
intentTracing: this.#intentTracing,
|
|
1466
1612
|
pruneToolDescriptions: this.#pruneToolDescriptions,
|
|
1467
1613
|
dialect: this.#dialect,
|
|
@@ -1489,7 +1635,7 @@ export class Agent {
|
|
|
1489
1635
|
skipInitialSteeringPoll = false;
|
|
1490
1636
|
return [];
|
|
1491
1637
|
}
|
|
1492
|
-
return this.#dequeueSteeringMessagesAfterHooks(signal);
|
|
1638
|
+
return this.#dequeueSteeringMessagesAfterHooks(signal ?? loopSignal);
|
|
1493
1639
|
},
|
|
1494
1640
|
hasSteeringMessages: () => {
|
|
1495
1641
|
if (this.#steeringQueue.length === 0) {
|
|
@@ -1514,7 +1660,7 @@ export class Agent {
|
|
|
1514
1660
|
},
|
|
1515
1661
|
waitForSteeringMessages: signal => this.#waitForSteeringMessages(signal),
|
|
1516
1662
|
hasIrcInterrupts: this.hasIrcInterrupts,
|
|
1517
|
-
getFollowUpMessages: signal => this.#dequeueFollowUpMessagesAfterHooks(signal),
|
|
1663
|
+
getFollowUpMessages: signal => this.#dequeueFollowUpMessagesAfterHooks(signal ?? loopSignal),
|
|
1518
1664
|
getAsideMessages: async () => (await this.#asideMessageProvider?.()) ?? [],
|
|
1519
1665
|
onBeforeYield: () => this.#onBeforeYield?.(),
|
|
1520
1666
|
telemetry: this.#telemetry,
|
|
@@ -1530,6 +1676,7 @@ export class Agent {
|
|
|
1530
1676
|
: agentLoopContinue(context, config, loopSignal, this.streamFn);
|
|
1531
1677
|
|
|
1532
1678
|
for await (const event of stream) {
|
|
1679
|
+
if (this.#abortController !== loopAbortController) return;
|
|
1533
1680
|
if (event.type === "turn_start") turnOpen = true;
|
|
1534
1681
|
if (event.type === "turn_end") turnOpen = false;
|
|
1535
1682
|
// Update internal state based on events
|
|
@@ -1600,6 +1747,7 @@ export class Agent {
|
|
|
1600
1747
|
}
|
|
1601
1748
|
}
|
|
1602
1749
|
} catch (err) {
|
|
1750
|
+
if (this.#abortController !== loopAbortController) return;
|
|
1603
1751
|
const stoppedForAbort = loopSignal.aborted;
|
|
1604
1752
|
const errorMessage = stoppedForAbort
|
|
1605
1753
|
? abortReasonText(loopSignal)
|
|
@@ -1704,6 +1852,7 @@ export class Agent {
|
|
|
1704
1852
|
this.#emit({ type: "agent_end", messages: [errorMsg] });
|
|
1705
1853
|
}
|
|
1706
1854
|
} finally {
|
|
1855
|
+
this.#restoreUndeliveredQueuedMessages(loopAbortController);
|
|
1707
1856
|
resolveRun?.();
|
|
1708
1857
|
if (this.#abortController === loopAbortController) {
|
|
1709
1858
|
this.#state.isStreaming = false;
|
|
@@ -1751,29 +1900,25 @@ export class Agent {
|
|
|
1751
1900
|
* multi-text turns, producing duplicated text on replay.
|
|
1752
1901
|
*/
|
|
1753
1902
|
async #emitCursorSplitAssistantMessage(assistantMessage: AssistantMessage): Promise<void> {
|
|
1754
|
-
// Snapshot and detach immediately so a still-pending `cursorOnToolResult`
|
|
1755
|
-
// cannot push into a drained buffer. Entries already reserved stay paired
|
|
1756
|
-
// with their toolCall.
|
|
1757
1903
|
const buffer = this.#cursorToolResultBuffer;
|
|
1758
1904
|
this.#cursorToolResultBuffer = [];
|
|
1905
|
+
this.#cursorToolResultDrain = buffer;
|
|
1906
|
+
try {
|
|
1907
|
+
// Keep the detached batch observable while its transforms finish.
|
|
1908
|
+
const pending = buffer.filter(entry => entry.pending !== undefined).map(entry => entry.pending);
|
|
1909
|
+
if (pending.length > 0) await Promise.all(pending);
|
|
1759
1910
|
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
// transformer is mid-flight; without the await its rewrite would land on
|
|
1764
|
-
// the detached entry after the original was already appended and emitted.
|
|
1765
|
-
// Each `pending` swallows its own rejection, so this cannot throw.
|
|
1766
|
-
const pending = buffer.filter(entry => entry.pending !== undefined).map(entry => entry.pending);
|
|
1767
|
-
if (pending.length > 0) await Promise.all(pending);
|
|
1768
|
-
|
|
1769
|
-
this.#state.streamMessage = null;
|
|
1770
|
-
this.appendMessage(assistantMessage);
|
|
1771
|
-
this.#emit({ type: "message_end", message: assistantMessage });
|
|
1911
|
+
this.#state.streamMessage = null;
|
|
1912
|
+
this.appendMessage(assistantMessage);
|
|
1913
|
+
this.#emit({ type: "message_end", message: assistantMessage });
|
|
1772
1914
|
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1915
|
+
for (const { toolResult } of buffer) {
|
|
1916
|
+
this.#emit({ type: "message_start", message: toolResult });
|
|
1917
|
+
this.appendMessage(toolResult);
|
|
1918
|
+
this.#emit({ type: "message_end", message: toolResult });
|
|
1919
|
+
}
|
|
1920
|
+
} finally {
|
|
1921
|
+
this.#cursorToolResultDrain = undefined;
|
|
1777
1922
|
}
|
|
1778
1923
|
}
|
|
1779
1924
|
}
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
* message delta is a cache miss each turn.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import type { AgentTool } from "./types";
|
|
17
18
|
import type { Context, Message, Tool } from "@linxiraos/pi-ai";
|
|
19
|
+
import { toolWireSchema } from "@linxiraos/pi-ai";
|
|
18
20
|
import { normalizeTools } from "./agent-loop";
|
|
19
21
|
import { messageEstimateVersion } from "./compaction/message-cache";
|
|
20
22
|
import type { AgentContext, AgentMessage } from "./types";
|
|
@@ -23,6 +25,52 @@ import type { AgentContext, AgentMessage } from "./types";
|
|
|
23
25
|
// StablePrefix (formerly ImmutablePrefix)
|
|
24
26
|
// ---------------------------------------------------------------------------
|
|
25
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Cheap per-tool wire-identity key: every field normalizeTools and
|
|
30
|
+
* computeFingerprint read. The resolved parameters contribute by object
|
|
31
|
+
* identity — the wire-schema memo is stamp-keyed on the parameters object,
|
|
32
|
+
* so identity equals normalized bytes (modulo intent injection, a pure
|
|
33
|
+
* function of the mode flags compared separately). `customFormat` and
|
|
34
|
+
* `examples` contribute by reference: both are treated as immutable config
|
|
35
|
+
* (a registry swap replaces the object), matching how the snapshot consumes
|
|
36
|
+
* them. `intent` functions are compared by reference — same closure means
|
|
37
|
+
* same mode resolution.
|
|
38
|
+
*/
|
|
39
|
+
function toolKeyForPrefix(tool: AgentTool): string {
|
|
40
|
+
const params = toolWireSchema(tool);
|
|
41
|
+
const customFormat = tool.customFormat;
|
|
42
|
+
const examples = (tool as { examples?: unknown }).examples;
|
|
43
|
+
// Identity strings for objects (Map-assigned ids, no string-build cost
|
|
44
|
+
// on the hot path beyond the first sighting per object).
|
|
45
|
+
return [
|
|
46
|
+
tool.name ?? "",
|
|
47
|
+
tool.description ?? "",
|
|
48
|
+
String(tool.strict ?? ""),
|
|
49
|
+
tool.customWireName ?? "",
|
|
50
|
+
typeof tool.intent === "function" ? `fn:${objectId(tool.intent)}` : `mode:${tool.intent ?? "require"}`,
|
|
51
|
+
objectId(params),
|
|
52
|
+
objectId(customFormat),
|
|
53
|
+
objectId(examples),
|
|
54
|
+
].join("\u0000");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const objectIds = new WeakMap<object, number>();
|
|
58
|
+
let nextObjectId = 1;
|
|
59
|
+
|
|
60
|
+
function objectId(obj: unknown): number {
|
|
61
|
+
if (obj === null || obj === undefined || (typeof obj !== "object" && typeof obj !== "function")) {
|
|
62
|
+
// Primitives contribute by value below via String(); the id path is
|
|
63
|
+
// only for reference-identity comparison of config objects.
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
let id = objectIds.get(obj);
|
|
67
|
+
if (id === undefined) {
|
|
68
|
+
id = nextObjectId++;
|
|
69
|
+
objectIds.set(obj, id);
|
|
70
|
+
}
|
|
71
|
+
return id;
|
|
72
|
+
}
|
|
73
|
+
|
|
26
74
|
/** Frozen system prompt + tool spec snapshot. */
|
|
27
75
|
export interface StablePrefixSnapshot {
|
|
28
76
|
systemPrompt: string[];
|
|
@@ -63,10 +111,42 @@ export class StablePrefix {
|
|
|
63
111
|
/**
|
|
64
112
|
* Build or rebuild from live context.
|
|
65
113
|
* Returns `true` if the prefix actually changed (cache miss imminent).
|
|
114
|
+
*
|
|
115
|
+
* Steady-state fast path: when the live prompt reference is unchanged
|
|
116
|
+
* AND every tool resolves to the same normalized parameters identity as
|
|
117
|
+
* last build, the fingerprint cannot have changed, so the full snapshot
|
|
118
|
+
* + stringify is skipped. Comparing resolved parameters (not the tool
|
|
119
|
+
* container) is load-bearing: tools like ReadTool expose `parameters` as
|
|
120
|
+
* a getter over live settings (`skillful`, `memory.backend`), so the
|
|
121
|
+
* schema can swap under stable tool references when a setting toggles.
|
|
122
|
+
* Any other in-place mutation must go through `invalidate()`.
|
|
66
123
|
*/
|
|
124
|
+
#lastPrompt: readonly string[] | undefined;
|
|
125
|
+
// Joined prompt bytes snapshot: the prompt array is caller-owned and
|
|
126
|
+
// mutable in place (Agent.setSystemPrompt stores the caller's array;
|
|
127
|
+
// anyone holding it can push/splice), so reference equality alone cannot
|
|
128
|
+
// prove the bytes are unchanged. Compared by value on the fast path.
|
|
129
|
+
#lastPromptText: string | undefined;
|
|
130
|
+
// Per-tool wire-identity snapshot: every field normalizeTools and
|
|
131
|
+
// computeFingerprint read (name, description, resolved parameters,
|
|
132
|
+
// strict, customFormat, customWireName, intent mode, examples
|
|
133
|
+
// reference). A registry swap that keeps name/description/parameters
|
|
134
|
+
// but changes any of these must miss the fast path.
|
|
135
|
+
#lastToolKey: readonly string[] | undefined;
|
|
136
|
+
#lastIntentTracing: boolean | undefined;
|
|
137
|
+
#lastPruneToolDescriptions: boolean | undefined;
|
|
138
|
+
|
|
67
139
|
build(context: AgentContext, options: BuildOptions): boolean {
|
|
140
|
+
const prev = this.#snapshot;
|
|
141
|
+
if (prev !== null && this.#fastPathHit(context, options)) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
68
144
|
const snapshot = takeSnapshot(context, options);
|
|
69
|
-
|
|
145
|
+
this.#recordFastPathKey(context, options, snapshot.tools);
|
|
146
|
+
if (prev && prev.fingerprint === snapshot.fingerprint) {
|
|
147
|
+
// Identity changed but bytes did not (e.g. equivalent rebuild):
|
|
148
|
+
// keep serving the cached snapshot so downstream memo identity
|
|
149
|
+
// (stamp-keyed schema caches) stays stable.
|
|
70
150
|
return false;
|
|
71
151
|
}
|
|
72
152
|
this.#snapshot = snapshot;
|
|
@@ -74,9 +154,50 @@ export class StablePrefix {
|
|
|
74
154
|
return true;
|
|
75
155
|
}
|
|
76
156
|
|
|
157
|
+
/** True when the cheap key matches: prompt + per-tool resolved identity. */
|
|
158
|
+
#fastPathHit(context: AgentContext, options: BuildOptions): boolean {
|
|
159
|
+
if (
|
|
160
|
+
this.#lastIntentTracing !== options.intentTracing ||
|
|
161
|
+
this.#lastPruneToolDescriptions !== options.pruneToolDescriptions
|
|
162
|
+
) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
// Prompt by reference first (steady state), then by joined bytes so
|
|
166
|
+
// an in-place push/splice of the same array still misses.
|
|
167
|
+
if (this.#lastPrompt !== context.systemPrompt) return false;
|
|
168
|
+
if (this.#lastPromptText !== undefined) {
|
|
169
|
+
const text = context.systemPrompt.join("\u0000");
|
|
170
|
+
if (text !== this.#lastPromptText) return false;
|
|
171
|
+
}
|
|
172
|
+
const tools = context.tools ?? [];
|
|
173
|
+
if (this.#lastToolKey === undefined || this.#lastToolKey.length !== tools.length) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
for (let i = 0; i < tools.length; i++) {
|
|
177
|
+
if (this.#lastToolKey[i] !== toolKeyForPrefix(tools[i]!)) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#recordFastPathKey(context: AgentContext, options: BuildOptions, normalized: Tool[]): void {
|
|
185
|
+
this.#lastPrompt = context.systemPrompt;
|
|
186
|
+
this.#lastPromptText = context.systemPrompt.join("\u0000");
|
|
187
|
+
this.#lastIntentTracing = options.intentTracing;
|
|
188
|
+
this.#lastPruneToolDescriptions = options.pruneToolDescriptions;
|
|
189
|
+
this.#lastToolKey = (context.tools ?? []).map(tool => toolKeyForPrefix(tool));
|
|
190
|
+
void normalized;
|
|
191
|
+
}
|
|
192
|
+
|
|
77
193
|
/** Force rebuild on the next `build()` call. */
|
|
78
194
|
invalidate(): void {
|
|
79
195
|
this.#snapshot = null;
|
|
196
|
+
this.#lastPrompt = undefined;
|
|
197
|
+
this.#lastPromptText = undefined;
|
|
198
|
+
this.#lastToolKey = undefined;
|
|
199
|
+
this.#lastIntentTracing = undefined;
|
|
200
|
+
this.#lastPruneToolDescriptions = undefined;
|
|
80
201
|
}
|
|
81
202
|
|
|
82
203
|
/**
|