@linxiraos/pi-agent-core 1.1.20 → 1.1.23
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 +243 -2
- package/README.md +4 -3
- package/THIRD-PARTY-NOTICES.txt +3 -40
- package/dist/types/agent-loop.d.ts +14 -1
- package/dist/types/agent.d.ts +36 -0
- package/dist/types/compaction/anthropic.d.ts +22 -52
- package/dist/types/compaction/branch-summarization.d.ts +1 -1
- package/dist/types/compaction/compaction.d.ts +4 -2
- package/dist/types/compaction/messages.d.ts +9 -0
- package/dist/types/compaction/openai.d.ts +11 -0
- package/dist/types/compaction/pruning.d.ts +9 -0
- package/dist/types/compaction/transcript-tokens.d.ts +25 -2
- package/dist/types/index.d.ts +3 -0
- package/dist/types/live-steering.d.ts +34 -0
- package/dist/types/output-budget.d.ts +54 -0
- package/dist/types/sent-tool-definitions.d.ts +17 -0
- package/dist/types/speculative-execution.d.ts +2 -1
- package/dist/types/telemetry.d.ts +103 -60
- package/dist/types/tool-context.d.ts +32 -0
- package/dist/types/types.d.ts +105 -7
- package/package.json +10 -10
- package/src/agent-loop.ts +386 -90
- package/src/agent.ts +175 -26
- package/src/compaction/anthropic.ts +62 -105
- package/src/compaction/branch-summarization.ts +1 -1
- package/src/compaction/compaction-v2-streaming.ts +13 -4
- package/src/compaction/compaction.ts +63 -51
- package/src/compaction/messages.ts +12 -2
- package/src/compaction/openai.ts +37 -9
- package/src/compaction/prompts/anthropic-compaction-instructions.md +2 -6
- package/src/compaction/pruning.ts +1 -1
- package/src/compaction/transcript-tokens.ts +64 -3
- package/src/index.ts +6 -0
- package/src/live-steering.ts +89 -0
- package/src/output-budget.ts +142 -0
- package/src/sent-tool-definitions.ts +40 -0
- package/src/speculative-execution.ts +12 -0
- package/src/telemetry.ts +240 -117
- package/src/tokenizer.ts +45 -6
- package/src/tool-context.ts +55 -0
- package/src/types.ts +109 -7
package/src/agent.ts
CHANGED
|
@@ -34,11 +34,19 @@ import {
|
|
|
34
34
|
normalizeMessagesForProvider,
|
|
35
35
|
normalizeTools,
|
|
36
36
|
resolveOwnedDialectFromEnv,
|
|
37
|
+
steeringQueueState,
|
|
37
38
|
unpairedToolCallTail,
|
|
38
39
|
} from "./agent-loop";
|
|
39
40
|
import type { AppendOnlyContextManager } from "./append-only-context";
|
|
40
41
|
import { isProviderRefusalMessage } from "./replay-policy";
|
|
42
|
+
import { SentToolDefinitions } from "./sent-tool-definitions";
|
|
41
43
|
import { Tokenizer, tokenizerEncodingForModel } from "./tokenizer";
|
|
44
|
+
import {
|
|
45
|
+
createAdditionalContextMessage,
|
|
46
|
+
joinAdditionalContext,
|
|
47
|
+
TOOL_RESULT_ADDITIONAL_CONTEXT,
|
|
48
|
+
type ToolResultWithAdditionalContext,
|
|
49
|
+
} from "./tool-context";
|
|
42
50
|
import type {
|
|
43
51
|
AgentBeforeModelCall,
|
|
44
52
|
AgentContext,
|
|
@@ -65,7 +73,7 @@ import { EventLoopKeepalive } from "./utils/yield";
|
|
|
65
73
|
function defaultConvertToLlm(messages: AgentMessage[]): Message[] {
|
|
66
74
|
return messages.filter((m): m is Message => {
|
|
67
75
|
if (m.role === "assistant") return !isProviderRefusalMessage(m);
|
|
68
|
-
return m.role === "user" || m.role === "toolResult";
|
|
76
|
+
return m.role === "user" || m.role === "developer" || m.role === "toolResult";
|
|
69
77
|
});
|
|
70
78
|
}
|
|
71
79
|
|
|
@@ -271,6 +279,11 @@ export interface AgentOptions {
|
|
|
271
279
|
pruneToolDescriptions?: boolean;
|
|
272
280
|
/** Owned tool-calling dialect. Undefined keeps provider-native tool calling. */
|
|
273
281
|
dialect?: Dialect;
|
|
282
|
+
/**
|
|
283
|
+
* Per-request owned-dialect resolver, consulted with the model being requested.
|
|
284
|
+
* Authoritative when set (like {@link serviceTierResolver}): replaces {@link dialect}.
|
|
285
|
+
*/
|
|
286
|
+
dialectResolver?: (model: Model) => Dialect | undefined;
|
|
274
287
|
/**
|
|
275
288
|
* When owned tool calling is active and the model fabricates a tool result
|
|
276
289
|
* mid-turn: `true` (default) aborts the provider request immediately; `false`
|
|
@@ -333,6 +346,8 @@ export interface AgentOptions {
|
|
|
333
346
|
* tool-call arguments). See {@link AgentLoopConfig.transformAssistantMessage}.
|
|
334
347
|
*/
|
|
335
348
|
transformAssistantMessage?: AgentLoopConfig["transformAssistantMessage"];
|
|
349
|
+
/** See {@link AgentLoopConfig.transformAssistantMessagePreservesToolCalls}. */
|
|
350
|
+
transformAssistantMessagePreservesToolCalls?: boolean;
|
|
336
351
|
|
|
337
352
|
/**
|
|
338
353
|
* Opt-in OpenTelemetry instrumentation. Passing `{}` enables the loop's
|
|
@@ -361,6 +376,12 @@ interface CursorToolResultEntry {
|
|
|
361
376
|
* `message_end` lands in the same chunk as the tool result.
|
|
362
377
|
*/
|
|
363
378
|
pending?: Promise<void>;
|
|
379
|
+
/**
|
|
380
|
+
* Passive context the executor attached via
|
|
381
|
+
* {@link TOOL_RESULT_ADDITIONAL_CONTEXT}, captured before any transformer
|
|
382
|
+
* can replace the message. Injected after the buffered results.
|
|
383
|
+
*/
|
|
384
|
+
additionalContext?: string;
|
|
364
385
|
}
|
|
365
386
|
|
|
366
387
|
type QueuedMessageQueue = "steering" | "followUp";
|
|
@@ -389,9 +410,18 @@ export class Agent {
|
|
|
389
410
|
#convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
390
411
|
#transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;
|
|
391
412
|
#transformProviderContext?: (context: Context, model: Model) => Context | Promise<Context>;
|
|
413
|
+
#sentToolDefinitions = new SentToolDefinitions();
|
|
392
414
|
#steeringQueue: AgentMessage[] = [];
|
|
393
415
|
#followUpQueue: AgentMessage[] = [];
|
|
394
416
|
#queuedMessageClaims: Partial<Record<QueuedMessageQueue, QueuedMessageClaim>> = {};
|
|
417
|
+
/**
|
|
418
|
+
* Steering live steering took for the in-flight response (`onLiveSteeringTaken`) that the
|
|
419
|
+
* transcript has not recorded yet, whether or not the provider accepted it. Kept apart from
|
|
420
|
+
* {@link #queuedMessageDeliveries}: the loop drops it on abort instead of recording it, so queue
|
|
421
|
+
* replacement must not drop it too; the run's end requeues whatever it did not record, and
|
|
422
|
+
* {@link withdrawLiveSteering} takes it back ahead of an abort.
|
|
423
|
+
*/
|
|
424
|
+
#liveSteered: { message: AgentMessage; controller: AbortController | undefined }[] = [];
|
|
395
425
|
/** Dequeued originals remain recoverable until their transcript events arrive. */
|
|
396
426
|
#queuedMessageDeliveries = new Set<{
|
|
397
427
|
queue: QueuedMessageQueue;
|
|
@@ -439,6 +469,7 @@ export class Agent {
|
|
|
439
469
|
#intentTracing: boolean;
|
|
440
470
|
#pruneToolDescriptions: boolean;
|
|
441
471
|
#dialect?: Dialect;
|
|
472
|
+
#dialectResolver?: (model: Model) => Dialect | undefined;
|
|
442
473
|
#abortOnFabricatedToolResult?: boolean;
|
|
443
474
|
#getToolChoice?: () => ToolChoiceDirective | undefined;
|
|
444
475
|
#onToolChoiceUnavailable?: () => void;
|
|
@@ -482,10 +513,17 @@ export class Agent {
|
|
|
482
513
|
* UI emission, and tool dispatch. Reassign at any time to swap the implementation.
|
|
483
514
|
*/
|
|
484
515
|
transformAssistantMessage?: AgentLoopConfig["transformAssistantMessage"];
|
|
516
|
+
/** Declares {@link transformAssistantMessage} never rewrites streamed tool calls; reassign alongside it. */
|
|
517
|
+
transformAssistantMessagePreservesToolCalls?: boolean;
|
|
485
518
|
/**
|
|
486
519
|
* Hook that peeks whether interrupting IRC asides are queued for the next boundary.
|
|
487
520
|
*/
|
|
488
521
|
hasIrcInterrupts?: AgentLoopConfig["hasIrcInterrupts"];
|
|
522
|
+
/**
|
|
523
|
+
* Hook that peeks whether background completions (jobs, supervised processes)
|
|
524
|
+
* are queued for the next boundary.
|
|
525
|
+
*/
|
|
526
|
+
hasBackgroundCompletions?: AgentLoopConfig["hasBackgroundCompletions"];
|
|
489
527
|
|
|
490
528
|
constructor(opts: AgentOptions = {}) {
|
|
491
529
|
this.#state = { ...this.#state, ...opts.initialState };
|
|
@@ -533,6 +571,7 @@ export class Agent {
|
|
|
533
571
|
this.#intentTracing = opts.intentTracing === true;
|
|
534
572
|
this.#pruneToolDescriptions = opts.pruneToolDescriptions === true;
|
|
535
573
|
this.#dialect = opts.dialect;
|
|
574
|
+
this.#dialectResolver = opts.dialectResolver;
|
|
536
575
|
this.#abortOnFabricatedToolResult = opts.abortOnFabricatedToolResult;
|
|
537
576
|
this.#getToolChoice = opts.getToolChoice;
|
|
538
577
|
this.#onToolChoiceUnavailable = opts.onToolChoiceUnavailable;
|
|
@@ -541,6 +580,7 @@ export class Agent {
|
|
|
541
580
|
this.beforeToolCall = opts.beforeToolCall;
|
|
542
581
|
this.afterToolCall = opts.afterToolCall;
|
|
543
582
|
this.transformAssistantMessage = opts.transformAssistantMessage;
|
|
583
|
+
this.transformAssistantMessagePreservesToolCalls = opts.transformAssistantMessagePreservesToolCalls;
|
|
544
584
|
this.#telemetry = opts.telemetry;
|
|
545
585
|
this.#appendOnlyContext = opts.appendOnlyContext;
|
|
546
586
|
this.#transformProviderContext = opts.transformProviderContext;
|
|
@@ -742,6 +782,33 @@ export class Agent {
|
|
|
742
782
|
this.#hideThinkingSummary = value;
|
|
743
783
|
}
|
|
744
784
|
|
|
785
|
+
/** Strip tool descriptions from provider-bound specs; read per request. */
|
|
786
|
+
get pruneToolDescriptions(): boolean {
|
|
787
|
+
return this.#pruneToolDescriptions;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
set pruneToolDescriptions(value: boolean) {
|
|
791
|
+
this.#pruneToolDescriptions = value;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/** Inject/strip the intent field on tool calls; applies from the next prompt run. */
|
|
795
|
+
get intentTracing(): boolean {
|
|
796
|
+
return this.#intentTracing;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
set intentTracing(value: boolean) {
|
|
800
|
+
this.#intentTracing = value;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/** Abort the provider request on a fabricated tool result; applies from the next prompt run. */
|
|
804
|
+
get abortOnFabricatedToolResult(): boolean | undefined {
|
|
805
|
+
return this.#abortOnFabricatedToolResult;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
set abortOnFabricatedToolResult(value: boolean | undefined) {
|
|
809
|
+
this.#abortOnFabricatedToolResult = value;
|
|
810
|
+
}
|
|
811
|
+
|
|
745
812
|
/**
|
|
746
813
|
* Get the current max retry delay in milliseconds.
|
|
747
814
|
*/
|
|
@@ -822,7 +889,9 @@ export class Agent {
|
|
|
822
889
|
): Promise<Context> {
|
|
823
890
|
const model = this.#state.model;
|
|
824
891
|
if (!model) throw new Error("No active model on agent");
|
|
825
|
-
const ownedDialect =
|
|
892
|
+
const ownedDialect =
|
|
893
|
+
(this.#dialectResolver ? this.#dialectResolver(model) : this.#dialect) ??
|
|
894
|
+
resolveOwnedDialectFromEnv(Bun.env.PI_DIALECT);
|
|
826
895
|
const messages = normalizeMessagesForProvider(llmMessages, model);
|
|
827
896
|
const tools = ownedDialect
|
|
828
897
|
? []
|
|
@@ -832,6 +901,11 @@ export class Agent {
|
|
|
832
901
|
}) ?? []);
|
|
833
902
|
let context: Context = { systemPrompt, messages, tools };
|
|
834
903
|
if (this.#transformProviderContext) context = await this.#transformProviderContext(context, model);
|
|
904
|
+
// Side requests reuse the main loop's sent definitions without recording their own.
|
|
905
|
+
if (context.tools?.length) {
|
|
906
|
+
const inactiveTools = this.#sentToolDefinitions.inactiveFor(context.messages, context.tools);
|
|
907
|
+
if (inactiveTools) context = { ...context, inactiveTools };
|
|
908
|
+
}
|
|
835
909
|
return context;
|
|
836
910
|
}
|
|
837
911
|
|
|
@@ -932,8 +1006,14 @@ export class Agent {
|
|
|
932
1006
|
}
|
|
933
1007
|
|
|
934
1008
|
#restoreUndeliveredQueuedMessages(controller: AbortController): void {
|
|
935
|
-
if (this.#queuedMessageDeliveries.size === 0) return;
|
|
1009
|
+
if (this.#queuedMessageDeliveries.size === 0 && this.#liveSteered.length === 0) return;
|
|
936
1010
|
const restored: Record<QueuedMessageQueue, AgentMessage[]> = { steering: [], followUp: [] };
|
|
1011
|
+
// Live steering was taken before anything the run still holds undelivered.
|
|
1012
|
+
this.#liveSteered = this.#liveSteered.filter(entry => {
|
|
1013
|
+
if (entry.controller !== controller) return true;
|
|
1014
|
+
restored.steering.push(entry.message);
|
|
1015
|
+
return false;
|
|
1016
|
+
});
|
|
937
1017
|
for (const delivery of this.#queuedMessageDeliveries) {
|
|
938
1018
|
if (delivery.controller !== controller) continue;
|
|
939
1019
|
this.#queuedMessageDeliveries.delete(delivery);
|
|
@@ -948,6 +1028,40 @@ export class Agent {
|
|
|
948
1028
|
if (restored.followUp.length > 0) this.#followUpQueue = [...restored.followUp, ...this.#followUpQueue];
|
|
949
1029
|
}
|
|
950
1030
|
|
|
1031
|
+
/** Move steering live steering took out of the queue-delivery records into {@link #liveSteered}. */
|
|
1032
|
+
#adoptLiveSteering(taken: readonly AgentMessage[]): void {
|
|
1033
|
+
for (const delivery of this.#queuedMessageDeliveries) {
|
|
1034
|
+
const pending = delivery.messages.slice(delivery.next);
|
|
1035
|
+
const kept = pending.filter(message => !taken.includes(message));
|
|
1036
|
+
if (kept.length === pending.length) continue;
|
|
1037
|
+
for (const message of pending) {
|
|
1038
|
+
if (taken.includes(message)) this.#liveSteered.push({ message, controller: delivery.controller });
|
|
1039
|
+
}
|
|
1040
|
+
if (kept.length === 0) {
|
|
1041
|
+
this.#queuedMessageDeliveries.delete(delivery);
|
|
1042
|
+
} else {
|
|
1043
|
+
delivery.messages = kept;
|
|
1044
|
+
delivery.next = 0;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* Take back live-steered messages ahead of an abort (Esc restores them to the editor):
|
|
1051
|
+
* the aborted run then neither records nor requeues them.
|
|
1052
|
+
*/
|
|
1053
|
+
withdrawLiveSteering(): AgentMessage[] {
|
|
1054
|
+
const messages = this.peekLiveSteeredMessages();
|
|
1055
|
+
this.#liveSteered = [];
|
|
1056
|
+
return messages;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/** Steering live steering took for the streaming response; the transcript records it once
|
|
1060
|
+
* that response (or its tool batch) ends, which is when the model switches to it. */
|
|
1061
|
+
peekLiveSteeredMessages(): AgentMessage[] {
|
|
1062
|
+
return this.#liveSteered.map(entry => entry.message);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
951
1065
|
setProviderResponseInterceptor(fn: SimpleStreamOptions["onResponse"] | undefined): void {
|
|
952
1066
|
this.#onResponse = fn;
|
|
953
1067
|
}
|
|
@@ -1087,6 +1201,11 @@ export class Agent {
|
|
|
1087
1201
|
|
|
1088
1202
|
appendMessage(m: AgentMessage) {
|
|
1089
1203
|
this.#state.messages.push(m);
|
|
1204
|
+
const live = this.#liveSteered.findIndex(entry => entry.message === m);
|
|
1205
|
+
if (live >= 0) {
|
|
1206
|
+
this.#liveSteered.splice(live, 1);
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1090
1209
|
for (const delivery of this.#queuedMessageDeliveries) {
|
|
1091
1210
|
if (delivery.messages[delivery.next] !== m) continue;
|
|
1092
1211
|
if (++delivery.next === delivery.messages.length) this.#queuedMessageDeliveries.delete(delivery);
|
|
@@ -1142,6 +1261,7 @@ export class Agent {
|
|
|
1142
1261
|
clearAllQueues() {
|
|
1143
1262
|
this.#steeringQueue = [];
|
|
1144
1263
|
this.#followUpQueue = [];
|
|
1264
|
+
this.#liveSteered = [];
|
|
1145
1265
|
this.#cancelQueuedMessagePreparation("steering");
|
|
1146
1266
|
this.#cancelQueuedMessagePreparation("followUp");
|
|
1147
1267
|
this.#notifySteeringWaiters();
|
|
@@ -1167,6 +1287,18 @@ export class Agent {
|
|
|
1167
1287
|
return claim ? [...claim.messages, ...this.#steeringQueue] : this.#steeringQueue;
|
|
1168
1288
|
}
|
|
1169
1289
|
|
|
1290
|
+
/** Dequeued messages not yet in the transcript, e.g. steering a provider
|
|
1291
|
+
* took into its in-flight response via live steering. Aborting the run
|
|
1292
|
+
* requeues them, so the session's empty-submit interrupt counts them as
|
|
1293
|
+
* pending input even though {@link peekSteeringQueue} no longer does. */
|
|
1294
|
+
peekUndeliveredQueuedMessages(): AgentMessage[] {
|
|
1295
|
+
const messages = this.peekLiveSteeredMessages();
|
|
1296
|
+
for (const delivery of this.#queuedMessageDeliveries) {
|
|
1297
|
+
for (let i = delivery.next; i < delivery.messages.length; i++) messages.push(delivery.messages[i]);
|
|
1298
|
+
}
|
|
1299
|
+
return messages;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1170
1302
|
/** Non-consuming view of the pending follow-up queue. See
|
|
1171
1303
|
* {@link peekSteeringQueue}. */
|
|
1172
1304
|
peekFollowUpQueue(): readonly AgentMessage[] {
|
|
@@ -1506,7 +1638,10 @@ export class Agent {
|
|
|
1506
1638
|
// that, a transformer resolving after the swap would patch a detached
|
|
1507
1639
|
// object while the persisted result kept the original payload — the
|
|
1508
1640
|
// rewrite silently lost.
|
|
1509
|
-
const entry: CursorToolResultEntry = {
|
|
1641
|
+
const entry: CursorToolResultEntry = {
|
|
1642
|
+
toolResult: message,
|
|
1643
|
+
additionalContext: (message as ToolResultWithAdditionalContext)[TOOL_RESULT_ADDITIONAL_CONTEXT],
|
|
1644
|
+
};
|
|
1510
1645
|
this.#cursorToolResultBuffer.push(entry);
|
|
1511
1646
|
const transform = this.#cursorOnToolResult;
|
|
1512
1647
|
if (transform) {
|
|
@@ -1574,6 +1709,7 @@ export class Agent {
|
|
|
1574
1709
|
preferWebsockets: this.#preferWebsockets,
|
|
1575
1710
|
convertToLlm: this.#convertToLlm,
|
|
1576
1711
|
transformProviderContext: this.#transformProviderContext,
|
|
1712
|
+
sentToolDefinitions: this.#sentToolDefinitions,
|
|
1577
1713
|
transformContext: this.#transformContext,
|
|
1578
1714
|
onPayload: this.#onPayload,
|
|
1579
1715
|
onResponse: this.#onResponse,
|
|
@@ -1611,6 +1747,7 @@ export class Agent {
|
|
|
1611
1747
|
intentTracing: this.#intentTracing,
|
|
1612
1748
|
pruneToolDescriptions: this.#pruneToolDescriptions,
|
|
1613
1749
|
dialect: this.#dialect,
|
|
1750
|
+
getDialect: this.#dialectResolver,
|
|
1614
1751
|
abortOnFabricatedToolResult: this.#abortOnFabricatedToolResult,
|
|
1615
1752
|
appendOnlyContext: this.#appendOnlyContext,
|
|
1616
1753
|
beforeToolCall: this.beforeToolCall ? (ctx, signal) => this.beforeToolCall?.(ctx, signal) : undefined,
|
|
@@ -1618,6 +1755,7 @@ export class Agent {
|
|
|
1618
1755
|
transformAssistantMessage: this.transformAssistantMessage
|
|
1619
1756
|
? (message, signal) => this.transformAssistantMessage?.(message, signal)
|
|
1620
1757
|
: undefined,
|
|
1758
|
+
transformAssistantMessagePreservesToolCalls: this.transformAssistantMessagePreservesToolCalls,
|
|
1621
1759
|
onAssistantMessageEvent: this.#onAssistantMessageEvent,
|
|
1622
1760
|
onHarmonyLeak: this.#onHarmonyLeak,
|
|
1623
1761
|
onTurnEnd: (messages, signal, context) => this.#onTurnEnd?.(messages, signal, context),
|
|
@@ -1637,29 +1775,17 @@ export class Agent {
|
|
|
1637
1775
|
}
|
|
1638
1776
|
return this.#dequeueSteeringMessagesAfterHooks(signal ?? loopSignal);
|
|
1639
1777
|
},
|
|
1640
|
-
hasSteeringMessages: () =>
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
const message = this.#steeringQueue[i];
|
|
1648
|
-
const role = "role" in message ? message.role : undefined;
|
|
1649
|
-
const attribution = "attribution" in message ? message.attribution : undefined;
|
|
1650
|
-
if (attribution === "user") {
|
|
1651
|
-
return { queued: true, source: "user" };
|
|
1652
|
-
}
|
|
1653
|
-
if (role !== "user") continue;
|
|
1654
|
-
if (attribution !== "agent") {
|
|
1655
|
-
return { queued: true, source: "user" };
|
|
1656
|
-
}
|
|
1657
|
-
hasAgentSteering = true;
|
|
1658
|
-
}
|
|
1659
|
-
return { queued: true, source: hasAgentSteering ? "agent" : "system" };
|
|
1660
|
-
},
|
|
1778
|
+
hasSteeringMessages: () =>
|
|
1779
|
+
steeringQueueState(
|
|
1780
|
+
this.#steeringQueue,
|
|
1781
|
+
this.#steeringMode === "one-at-a-time"
|
|
1782
|
+
? Math.min(1, this.#steeringQueue.length)
|
|
1783
|
+
: this.#steeringQueue.length,
|
|
1784
|
+
),
|
|
1661
1785
|
waitForSteeringMessages: signal => this.#waitForSteeringMessages(signal),
|
|
1786
|
+
onLiveSteeringTaken: messages => this.#adoptLiveSteering(messages),
|
|
1662
1787
|
hasIrcInterrupts: this.hasIrcInterrupts,
|
|
1788
|
+
hasBackgroundCompletions: this.hasBackgroundCompletions,
|
|
1663
1789
|
getFollowUpMessages: signal => this.#dequeueFollowUpMessagesAfterHooks(signal ?? loopSignal),
|
|
1664
1790
|
getAsideMessages: async () => (await this.#asideMessageProvider?.()) ?? [],
|
|
1665
1791
|
onBeforeYield: () => this.#onBeforeYield?.(),
|
|
@@ -1767,6 +1893,9 @@ export class Agent {
|
|
|
1767
1893
|
.map(entry => entry.pending);
|
|
1768
1894
|
if (pendingTransforms.length > 0) await Promise.all(pendingTransforms);
|
|
1769
1895
|
const bufferedCursorResults = this.#cursorToolResultBuffer.map(({ toolResult }) => toolResult);
|
|
1896
|
+
const bufferedCursorContext = joinAdditionalContext(
|
|
1897
|
+
this.#cursorToolResultBuffer.map(({ additionalContext }) => additionalContext),
|
|
1898
|
+
);
|
|
1770
1899
|
const retainedToolCallIds = new Set(completedToolCallIds);
|
|
1771
1900
|
for (const { toolCallId } of bufferedCursorResults) retainedToolCallIds.add(toolCallId);
|
|
1772
1901
|
const errorMsg: AssistantMessage =
|
|
@@ -1843,9 +1972,13 @@ export class Agent {
|
|
|
1843
1972
|
this.#emit({ type: "message_end", message: toolResult });
|
|
1844
1973
|
toolResults.push(toolResult);
|
|
1845
1974
|
}
|
|
1975
|
+
const agentEndMessages: AgentMessage[] = [errorMsg, ...toolResults];
|
|
1976
|
+
if (bufferedCursorContext !== undefined) {
|
|
1977
|
+
agentEndMessages.push(this.#emitCursorAdditionalContext(bufferedCursorContext));
|
|
1978
|
+
}
|
|
1846
1979
|
this.#emit({ type: "turn_end", message: errorMsg, toolResults });
|
|
1847
1980
|
turnOpen = false;
|
|
1848
|
-
this.#emit({ type: "agent_end", messages:
|
|
1981
|
+
this.#emit({ type: "agent_end", messages: agentEndMessages });
|
|
1849
1982
|
} else {
|
|
1850
1983
|
this.appendMessage(errorMsg);
|
|
1851
1984
|
this.#state.error = errorMessage;
|
|
@@ -1917,8 +2050,24 @@ export class Agent {
|
|
|
1917
2050
|
this.appendMessage(toolResult);
|
|
1918
2051
|
this.#emit({ type: "message_end", message: toolResult });
|
|
1919
2052
|
}
|
|
2053
|
+
const additionalContext = joinAdditionalContext(buffer.map(entry => entry.additionalContext));
|
|
2054
|
+
if (additionalContext !== undefined) this.#emitCursorAdditionalContext(additionalContext);
|
|
1920
2055
|
} finally {
|
|
1921
2056
|
this.#cursorToolResultDrain = undefined;
|
|
1922
2057
|
}
|
|
1923
2058
|
}
|
|
2059
|
+
|
|
2060
|
+
/**
|
|
2061
|
+
* Append passive context reported by Cursor exec-channel tools after their
|
|
2062
|
+
* results, mirroring the loop's post-batch developer message. Cursor runs
|
|
2063
|
+
* those tools server-side mid-stream, so the context reaches the next
|
|
2064
|
+
* provider request instead of the current one.
|
|
2065
|
+
*/
|
|
2066
|
+
#emitCursorAdditionalContext(text: string): AgentMessage {
|
|
2067
|
+
const message = createAdditionalContextMessage(text);
|
|
2068
|
+
this.#emit({ type: "message_start", message });
|
|
2069
|
+
this.appendMessage(message);
|
|
2070
|
+
this.#emit({ type: "message_end", message });
|
|
2071
|
+
return message;
|
|
2072
|
+
}
|
|
1924
2073
|
}
|
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Anthropic
|
|
2
|
+
* Anthropic on-demand compaction (`compact-2026-09-04` beta).
|
|
3
3
|
*
|
|
4
|
-
* The
|
|
5
|
-
* prompt, tools
|
|
6
|
-
*
|
|
7
|
-
* cached prefix and stops; the summary arrives as a `compaction` block that
|
|
8
|
-
* the provider surfaces as an `anthropicCompaction` payload. The summary is
|
|
9
|
-
* plain text, so it doubles as the compaction entry's readable summary for
|
|
10
|
-
* every other provider, while the Anthropic provider replays it as a native
|
|
11
|
-
* block (the API drops everything that precedes it). The retained tail after
|
|
12
|
-
* the cut point is replayed from session entries exactly like a local summary.
|
|
4
|
+
* The request sends only the prefix to summarize, with the live conversation's
|
|
5
|
+
* system prompt, tools and thinking settings. The returned signed block
|
|
6
|
+
* replaces that prefix; the retained tail is replayed from session entries.
|
|
13
7
|
*/
|
|
14
8
|
|
|
15
9
|
import type {
|
|
16
10
|
AnthropicCompactionPayload,
|
|
17
11
|
ApiKey,
|
|
18
|
-
AssistantMessage,
|
|
19
12
|
Effort,
|
|
20
13
|
Message,
|
|
21
14
|
Model,
|
|
@@ -24,29 +17,21 @@ import type {
|
|
|
24
17
|
Usage,
|
|
25
18
|
} from "@linxiraos/pi-ai";
|
|
26
19
|
import * as AIError from "@linxiraos/pi-ai/error";
|
|
27
|
-
import { supportsAnthropicCompaction } from "@linxiraos/pi-ai/providers/anthropic";
|
|
20
|
+
import { supportsAnthropicCompaction } from "@linxiraos/pi-ai/providers/anthropic-compaction";
|
|
28
21
|
import { isRecord, prompt } from "@linxiraos/pi-utils";
|
|
29
22
|
import { type InstrumentedChatSpanOptions, instrumentedCompleteSimple } from "../telemetry";
|
|
23
|
+
import type { AgentMessage } from "../types";
|
|
30
24
|
import anthropicCompactionInstructionsPrompt from "./prompts/anthropic-compaction-instructions.md" with { type: "text" };
|
|
31
25
|
|
|
32
26
|
export const ANTHROPIC_COMPACTION_PRESERVE_KEY = "anthropicCompaction";
|
|
33
27
|
|
|
34
|
-
/** The API rejects a `compact_20260112` trigger below this many input tokens. */
|
|
35
|
-
export const ANTHROPIC_COMPACTION_MIN_TRIGGER_TOKENS = 50_000;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Smallest context the native lane accepts. The trigger sits at the API
|
|
39
|
-
* floor, so a prompt that lands below it is answered instead of compacted;
|
|
40
|
-
* the margin over the floor absorbs the difference between the last reported
|
|
41
|
-
* context size and the compaction request's own input.
|
|
42
|
-
*/
|
|
43
|
-
export const ANTHROPIC_COMPACTION_MIN_CONTEXT_TOKENS = 55_000;
|
|
44
|
-
|
|
45
28
|
/** Summary persisted under {@link ANTHROPIC_COMPACTION_PRESERVE_KEY}. */
|
|
46
29
|
export interface AnthropicCompactionPreserveData {
|
|
47
30
|
provider: string;
|
|
48
31
|
content: string;
|
|
49
|
-
/**
|
|
32
|
+
/** Signature attached to an on-demand block; replayed verbatim. */
|
|
33
|
+
signature?: string;
|
|
34
|
+
/** Legacy threshold block state; replay-only. */
|
|
50
35
|
encryptedContent?: string;
|
|
51
36
|
/** Harness file metadata (`<files>` section) replayed after the native block. */
|
|
52
37
|
filesText?: string;
|
|
@@ -82,6 +67,9 @@ export function getPreservedAnthropicCompactionData(
|
|
|
82
67
|
return {
|
|
83
68
|
provider: candidate.provider,
|
|
84
69
|
content: candidate.content,
|
|
70
|
+
...(typeof candidate.signature === "string" && candidate.signature.length > 0
|
|
71
|
+
? { signature: candidate.signature }
|
|
72
|
+
: {}),
|
|
85
73
|
...(typeof candidate.encryptedContent === "string" && candidate.encryptedContent.length > 0
|
|
86
74
|
? { encryptedContent: candidate.encryptedContent }
|
|
87
75
|
: {}),
|
|
@@ -118,93 +106,63 @@ export function getAnthropicCompactionPayload(
|
|
|
118
106
|
type: "anthropicCompaction",
|
|
119
107
|
provider: preserved.provider,
|
|
120
108
|
content: preserved.content,
|
|
109
|
+
...(preserved.signature ? { signature: preserved.signature } : {}),
|
|
121
110
|
...(preserved.encryptedContent ? { encryptedContent: preserved.encryptedContent } : {}),
|
|
122
111
|
...(preserved.filesText ? { filesText: preserved.filesText } : {}),
|
|
123
112
|
};
|
|
124
113
|
}
|
|
125
114
|
|
|
126
115
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* the whole conversation so the prompt cache the live turn wrote is read, but
|
|
131
|
-
* the summary must cover only the history before that tail — the local
|
|
132
|
-
* summarizer never sees the tail, and the rebuilt context replays it after the
|
|
133
|
-
* summary. Counting mirrors the provider's message conversion (consecutive
|
|
134
|
-
* tool results collapse into one user message; developer messages are user
|
|
135
|
-
* messages). Structured for the prompt template, which renders the
|
|
136
|
-
* singular/plural wording; the description quotes no content: quoting the
|
|
137
|
-
* tail would hand the summarizer the very facts it must leave to the tail.
|
|
116
|
+
* Move the existing keep-tail boundary forward until the summary/tail boundary
|
|
117
|
+
* alternates wire roles and no tool call is separated from its result. If no
|
|
118
|
+
* boundary is safe, the request summarizes the entire snapshot (empty tail).
|
|
138
119
|
*/
|
|
139
|
-
export
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const isToolResult = message.role === "toolResult";
|
|
151
|
-
if (!(isToolResult && previousWasToolResult)) count += 1;
|
|
152
|
-
previousWasToolResult = isToolResult;
|
|
120
|
+
export function findAnthropicCompactionCut(
|
|
121
|
+
messages: readonly (AgentMessage | { role: "system"; content: string; timestamp: number })[],
|
|
122
|
+
initialCut: number,
|
|
123
|
+
): number {
|
|
124
|
+
let calls: Map<string, number> | undefined;
|
|
125
|
+
for (let i = 0; i < messages.length; i++) {
|
|
126
|
+
const message = messages[i];
|
|
127
|
+
if (message.role !== "assistant") continue;
|
|
128
|
+
for (const block of message.content) {
|
|
129
|
+
if (block.type === "toolCall") (calls ??= new Map()).set(block.id, i);
|
|
130
|
+
}
|
|
153
131
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
count += 1;
|
|
132
|
+
let lastResult: Int32Array | undefined;
|
|
133
|
+
if (calls) {
|
|
134
|
+
lastResult = new Int32Array(messages.length);
|
|
135
|
+
for (let i = 0; i < messages.length; i++) {
|
|
136
|
+
const message = messages[i];
|
|
137
|
+
if (message.role !== "toolResult") continue;
|
|
138
|
+
const callIndex = calls.get(message.toolCallId);
|
|
139
|
+
if (callIndex !== undefined) lastResult[callIndex] = i;
|
|
140
|
+
}
|
|
164
141
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
switch (block.type) {
|
|
176
|
-
case "text":
|
|
177
|
-
return block.text.trim().length > 0;
|
|
178
|
-
case "toolCall":
|
|
179
|
-
case "anthropicServerTool":
|
|
180
|
-
return true;
|
|
181
|
-
case "thinking":
|
|
182
|
-
return block.thinking.trim().length > 0 || (block.thinkingSignature ?? "").trim().length > 0;
|
|
183
|
-
default:
|
|
184
|
-
return false;
|
|
142
|
+
let protectedThrough = -1;
|
|
143
|
+
for (let i = 0; i < initialCut; i++) protectedThrough = Math.max(protectedThrough, lastResult?.[i] ?? -1);
|
|
144
|
+
for (let cut = initialCut; cut < messages.length; cut++) {
|
|
145
|
+
protectedThrough = Math.max(protectedThrough, lastResult?.[cut - 1] ?? -1);
|
|
146
|
+
if (cut <= protectedThrough) continue;
|
|
147
|
+
const first = messages[cut];
|
|
148
|
+
if (first.role === "system" || first.role === "developer") continue;
|
|
149
|
+
const previous = messages[cut - 1];
|
|
150
|
+
if (!previous) continue;
|
|
151
|
+
if ((previous.role === "assistant") !== (first.role === "assistant")) return cut;
|
|
185
152
|
}
|
|
153
|
+
return messages.length;
|
|
186
154
|
}
|
|
187
155
|
|
|
188
|
-
/**
|
|
189
|
-
* Summarization prompt sent as the edit's `instructions`, which replace the
|
|
190
|
-
* API default entirely. The template lays out the retained-tail boundary
|
|
191
|
-
* first, so the summary covers only the history the rebuilt context drops,
|
|
192
|
-
* then the caller's extra context, the same structure prompt as the local
|
|
193
|
-
* summarizer, the caller's focus, and the tool-abstention clause the API
|
|
194
|
-
* recommends when tools are defined (a summarization pass that calls a tool
|
|
195
|
-
* yields no summary).
|
|
196
|
-
*/
|
|
156
|
+
/** Instructions replace the API default; the request contains only summarized messages. */
|
|
197
157
|
export function buildAnthropicCompactionInstructions(
|
|
198
158
|
basePrompt: string,
|
|
199
159
|
customInstructions: string | undefined,
|
|
200
160
|
extraContext: string | undefined,
|
|
201
|
-
retainedTail: RetainedTailScope | undefined,
|
|
202
161
|
): string {
|
|
203
162
|
return prompt.render(anthropicCompactionInstructionsPrompt, {
|
|
204
163
|
basePrompt,
|
|
205
164
|
customInstructions,
|
|
206
165
|
extraContext,
|
|
207
|
-
retainedTail,
|
|
208
166
|
});
|
|
209
167
|
}
|
|
210
168
|
|
|
@@ -219,7 +177,7 @@ export interface AnthropicNativeCompactionRequest {
|
|
|
219
177
|
|
|
220
178
|
export interface AnthropicNativeCompactionResponse {
|
|
221
179
|
content: string;
|
|
222
|
-
|
|
180
|
+
signature: string;
|
|
223
181
|
usage: Usage;
|
|
224
182
|
model: string;
|
|
225
183
|
}
|
|
@@ -239,15 +197,13 @@ export interface AnthropicNativeCompactionOptions
|
|
|
239
197
|
Pick<InstrumentedChatSpanOptions, "completeImpl" | "telemetry" | "retry"> {}
|
|
240
198
|
|
|
241
199
|
/**
|
|
242
|
-
* Run one compaction request and return the summary the API wrote
|
|
243
|
-
* opaque `encrypted_content` the API attached for the replay. `completeSimple`
|
|
200
|
+
* Run one compaction request and return the summary and signature the API wrote. `completeSimple`
|
|
244
201
|
* resolves terminal failures as messages, so their classification is restored
|
|
245
202
|
* here: an aborted response is an `AbortError` (a cancellation, never a native
|
|
246
203
|
* failure) and an error response keeps its HTTP status, so auth and timeout
|
|
247
204
|
* handling downstream classify it the same way as the OpenAI lanes. A response
|
|
248
|
-
* without a summary is a native failure
|
|
249
|
-
*
|
|
250
|
-
* the model called a tool during summarization.
|
|
205
|
+
* without a summary is a native failure, including tool use, refusals and
|
|
206
|
+
* output limits; the configured method order can then choose a fallback.
|
|
251
207
|
*/
|
|
252
208
|
export async function requestAnthropicNativeCompaction(
|
|
253
209
|
model: Model<"anthropic-messages">,
|
|
@@ -271,11 +227,7 @@ export async function requestAnthropicNativeCompaction(
|
|
|
271
227
|
promptCacheKey: options.promptCacheKey,
|
|
272
228
|
providerSessionState: options.providerSessionState,
|
|
273
229
|
maxInFlightRequests: options.maxInFlightRequests,
|
|
274
|
-
anthropicCompaction: {
|
|
275
|
-
triggerInputTokens: ANTHROPIC_COMPACTION_MIN_TRIGGER_TOKENS,
|
|
276
|
-
pauseAfterCompaction: true,
|
|
277
|
-
instructions: request.instructions,
|
|
278
|
-
},
|
|
230
|
+
anthropicCompaction: { instructions: request.instructions },
|
|
279
231
|
},
|
|
280
232
|
{
|
|
281
233
|
telemetry: options.telemetry,
|
|
@@ -294,16 +246,21 @@ export async function requestAnthropicNativeCompaction(
|
|
|
294
246
|
: new AIError.ProviderHttpError(message, response.errorStatus);
|
|
295
247
|
}
|
|
296
248
|
const payload = response.providerPayload;
|
|
297
|
-
if (
|
|
249
|
+
if (
|
|
250
|
+
response.stopDetails?.type !== "compaction" ||
|
|
251
|
+
payload?.type !== "anthropicCompaction" ||
|
|
252
|
+
!payload.content ||
|
|
253
|
+
!payload.signature
|
|
254
|
+
) {
|
|
298
255
|
throw new Error(
|
|
299
256
|
response.stopDetails?.type === "compaction"
|
|
300
|
-
? "Anthropic compaction returned no summary"
|
|
301
|
-
:
|
|
257
|
+
? "Anthropic compaction returned no signed summary"
|
|
258
|
+
: `Anthropic compaction response carried no compaction block (stop reason: ${response.stopDetails?.type ?? response.stopReason})`,
|
|
302
259
|
);
|
|
303
260
|
}
|
|
304
261
|
return {
|
|
305
262
|
content: payload.content,
|
|
306
|
-
|
|
263
|
+
signature: payload.signature,
|
|
307
264
|
usage: response.usage,
|
|
308
265
|
model: response.model,
|
|
309
266
|
};
|
|
@@ -86,7 +86,7 @@ export interface GenerateBranchSummaryOptions {
|
|
|
86
86
|
convertToLlm?: ConvertToLlm;
|
|
87
87
|
/**
|
|
88
88
|
* Optional telemetry handle. When provided, the branch summary LLM call is
|
|
89
|
-
* wrapped in an OTEL chat span tagged with `
|
|
89
|
+
* wrapped in an OTEL chat span tagged with `omp.gen_ai.oneshot.kind = "branch_summary"`.
|
|
90
90
|
*/
|
|
91
91
|
telemetry?: AgentTelemetry;
|
|
92
92
|
/**
|