@averyyy/pi-coding-agent 0.80.3-piclient.5 → 0.80.3-piclient.7
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 +2 -0
- package/dist/core/agent-session.d.ts +17 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +344 -18
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +4 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +27 -2
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/pi-server-client.d.ts +4 -1
- package/dist/core/pi-server-client.d.ts.map +1 -1
- package/dist/core/pi-server-client.js +87 -21
- package/dist/core/pi-server-client.js.map +1 -1
- package/dist/core/pi-server-protocol.d.ts +13 -0
- package/dist/core/pi-server-protocol.d.ts.map +1 -0
- package/dist/core/pi-server-protocol.js +32 -0
- package/dist/core/pi-server-protocol.js.map +1 -0
- package/dist/core/pi-server-request.d.ts.map +1 -1
- package/dist/core/pi-server-request.js +62 -116
- package/dist/core/pi-server-request.js.map +1 -1
- package/dist/core/sdk.d.ts +8 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +17 -24
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +1 -0
- package/dist/core/tools/bash.js.map +1 -1
- package/package.json +4 -4
|
@@ -62,12 +62,84 @@ function estimateMessagesTokens(messages) {
|
|
|
62
62
|
// ============================================================================
|
|
63
63
|
/** Standard thinking levels */
|
|
64
64
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
|
|
65
|
+
const INTRA_TURN_COMPACTION_MARKER_TYPE = "pi:intra-turn-compaction";
|
|
66
|
+
const INTRA_TURN_COMPACTION_MARKER_TEXT = "Continue from the compaction summary above. The previous tool loop was compacted before the next provider request; use the summary's Operational State, file lists, failures, last command, and next steps.";
|
|
67
|
+
const VALIDATION_HINT_MESSAGE_TYPE = "pi:validation-hint";
|
|
68
|
+
const VALIDATION_HINT_FILE_LIMIT = 12;
|
|
69
|
+
const VALIDATION_HINT_FAILURE_CHAR_LIMIT = 1200;
|
|
70
|
+
const AUTO_SESSION_NAME_INPUT_CHAR_LIMIT = 4000;
|
|
71
|
+
const AUTO_SESSION_NAME_MAX_CHARS = 80;
|
|
72
|
+
const AUTO_SESSION_NAME_PROMPT = "Name this coding session from the transcript below. Return only a concise title, in the user's language when clear. No quotes, no markdown, no explanation. Do not call tools.";
|
|
65
73
|
function isPiServerMode() {
|
|
66
74
|
return process.env.PI_SERVER_MODE === "true";
|
|
67
75
|
}
|
|
68
76
|
function toProviderReasoning(thinkingLevel) {
|
|
69
77
|
return thinkingLevel === "off" ? undefined : thinkingLevel;
|
|
70
78
|
}
|
|
79
|
+
function getStringField(input, key) {
|
|
80
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
81
|
+
return undefined;
|
|
82
|
+
const value = input[key];
|
|
83
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
84
|
+
}
|
|
85
|
+
function getPiServerFailurePhase(message) {
|
|
86
|
+
for (const diagnostic of message.diagnostics ?? []) {
|
|
87
|
+
if (diagnostic.type !== "pi_server_failure")
|
|
88
|
+
continue;
|
|
89
|
+
const phase = diagnostic.details?.phase;
|
|
90
|
+
if (typeof phase === "string")
|
|
91
|
+
return phase;
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
function toolTextContent(content) {
|
|
96
|
+
return content
|
|
97
|
+
.filter((part) => part.type === "text")
|
|
98
|
+
.map((part) => part.text)
|
|
99
|
+
.join("\n")
|
|
100
|
+
.trim();
|
|
101
|
+
}
|
|
102
|
+
function compactValidationSummary(text) {
|
|
103
|
+
const trimmed = text.trim();
|
|
104
|
+
if (trimmed.length <= VALIDATION_HINT_FAILURE_CHAR_LIMIT)
|
|
105
|
+
return trimmed;
|
|
106
|
+
return `...${trimmed.slice(trimmed.length - VALIDATION_HINT_FAILURE_CHAR_LIMIT + 3)}`;
|
|
107
|
+
}
|
|
108
|
+
function messageTextContent(message) {
|
|
109
|
+
if (!("content" in message))
|
|
110
|
+
return "";
|
|
111
|
+
const content = message.content;
|
|
112
|
+
if (typeof content === "string")
|
|
113
|
+
return content;
|
|
114
|
+
if (!Array.isArray(content))
|
|
115
|
+
return "";
|
|
116
|
+
return content
|
|
117
|
+
.filter((part) => part.type === "text")
|
|
118
|
+
.map((part) => part.text)
|
|
119
|
+
.join("\n")
|
|
120
|
+
.trim();
|
|
121
|
+
}
|
|
122
|
+
function truncateForSessionNameInput(text) {
|
|
123
|
+
if (text.length <= AUTO_SESSION_NAME_INPUT_CHAR_LIMIT)
|
|
124
|
+
return text;
|
|
125
|
+
return text.slice(0, AUTO_SESSION_NAME_INPUT_CHAR_LIMIT).trim();
|
|
126
|
+
}
|
|
127
|
+
function sanitizeGeneratedSessionName(text) {
|
|
128
|
+
const firstLine = text
|
|
129
|
+
.split(/\r?\n/u)
|
|
130
|
+
.map((line) => line.trim())
|
|
131
|
+
.find((line) => line.length > 0);
|
|
132
|
+
if (!firstLine)
|
|
133
|
+
return undefined;
|
|
134
|
+
const title = firstLine
|
|
135
|
+
.replace(/^title:\s*/iu, "")
|
|
136
|
+
.replace(/^["'`]+|["'`]+$/g, "")
|
|
137
|
+
.replace(/\s+/g, " ")
|
|
138
|
+
.trim();
|
|
139
|
+
if (!title)
|
|
140
|
+
return undefined;
|
|
141
|
+
return title.length > AUTO_SESSION_NAME_MAX_CHARS ? title.slice(0, AUTO_SESSION_NAME_MAX_CHARS).trim() : title;
|
|
142
|
+
}
|
|
71
143
|
// ============================================================================
|
|
72
144
|
// AgentSession Class
|
|
73
145
|
// ============================================================================
|
|
@@ -98,6 +170,8 @@ export class AgentSession {
|
|
|
98
170
|
// Bash execution state
|
|
99
171
|
_bashAbortController = undefined;
|
|
100
172
|
_pendingBashMessages = [];
|
|
173
|
+
_validationModifiedFiles = new Set();
|
|
174
|
+
_validationFailure = undefined;
|
|
101
175
|
// Extension system
|
|
102
176
|
_extensionRunner;
|
|
103
177
|
_turnIndex = 0;
|
|
@@ -118,6 +192,7 @@ export class AgentSession {
|
|
|
118
192
|
_extensionShutdownHandler;
|
|
119
193
|
_extensionErrorListener;
|
|
120
194
|
_extensionErrorUnsubscriber;
|
|
195
|
+
_autoSessionNameAttempted = false;
|
|
121
196
|
// Model registry for API key resolution
|
|
122
197
|
_modelRegistry;
|
|
123
198
|
// Tool registry for extension getTools/setTools
|
|
@@ -197,6 +272,71 @@ export class AgentSession {
|
|
|
197
272
|
const result = await this._modelRegistry.getApiKeyAndHeaders(model);
|
|
198
273
|
return result.ok ? { apiKey: result.apiKey, headers: result.headers, env: result.env } : {};
|
|
199
274
|
}
|
|
275
|
+
_resetValidationHintState() {
|
|
276
|
+
this._validationModifiedFiles.clear();
|
|
277
|
+
this._validationFailure = undefined;
|
|
278
|
+
}
|
|
279
|
+
_recordValidationToolResult(toolName, input, content, isError) {
|
|
280
|
+
if ((toolName === "edit" || toolName === "write") && !isError) {
|
|
281
|
+
const path = getStringField(input, "path") ?? getStringField(input, "file_path");
|
|
282
|
+
if (path) {
|
|
283
|
+
this._validationModifiedFiles.add(path);
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (toolName !== "bash") {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const command = getStringField(input, "command");
|
|
291
|
+
if (!command) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (isError) {
|
|
295
|
+
this._validationFailure = {
|
|
296
|
+
command,
|
|
297
|
+
summary: compactValidationSummary(toolTextContent(content)),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
else if (this._validationFailure?.command === command) {
|
|
301
|
+
this._resetValidationHintState();
|
|
302
|
+
}
|
|
303
|
+
else if (!this._validationFailure) {
|
|
304
|
+
this._validationModifiedFiles.clear();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
_createValidationHintMessage() {
|
|
308
|
+
const modifiedFiles = Array.from(this._validationModifiedFiles).slice(-VALIDATION_HINT_FILE_LIMIT);
|
|
309
|
+
if (modifiedFiles.length === 0 && !this._validationFailure) {
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
const lines = ["<validation-hint>"];
|
|
313
|
+
if (modifiedFiles.length > 0) {
|
|
314
|
+
lines.push("Recent modified files:", ...modifiedFiles.map((file) => `- ${file}`));
|
|
315
|
+
}
|
|
316
|
+
if (this._validationFailure) {
|
|
317
|
+
lines.push("Recent failed command:", this._validationFailure.command, "Failure summary:", this._validationFailure.summary || "(no output)", "Suggested next validation command:", this._validationFailure.command);
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
lines.push("Suggested next step:", "Run the smallest project validation that covers the modified files.");
|
|
321
|
+
}
|
|
322
|
+
lines.push("Inspect failures, fix the root cause, then rerun validation before finalizing.", "</validation-hint>");
|
|
323
|
+
return {
|
|
324
|
+
role: "custom",
|
|
325
|
+
customType: VALIDATION_HINT_MESSAGE_TYPE,
|
|
326
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
327
|
+
display: false,
|
|
328
|
+
details: { modifiedFiles, failedCommand: this._validationFailure?.command },
|
|
329
|
+
timestamp: Date.now(),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
_withValidationHint(context) {
|
|
333
|
+
const messages = context.messages.filter((message) => !(message.role === "custom" && message.customType === VALIDATION_HINT_MESSAGE_TYPE));
|
|
334
|
+
const hint = this._createValidationHintMessage();
|
|
335
|
+
return {
|
|
336
|
+
...context,
|
|
337
|
+
messages: hint ? [...messages, hint] : messages,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
200
340
|
/**
|
|
201
341
|
* Install tool hooks once on the Agent instance.
|
|
202
342
|
*
|
|
@@ -228,18 +368,20 @@ export class AgentSession {
|
|
|
228
368
|
};
|
|
229
369
|
this.agent.afterToolCall = async ({ toolCall, args, result, isError }) => {
|
|
230
370
|
const runner = this._extensionRunner;
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
371
|
+
const hookResult = runner.hasHandlers("tool_result")
|
|
372
|
+
? await runner.emitToolResult({
|
|
373
|
+
type: "tool_result",
|
|
374
|
+
toolName: toolCall.name,
|
|
375
|
+
toolCallId: toolCall.id,
|
|
376
|
+
input: args,
|
|
377
|
+
content: result.content,
|
|
378
|
+
details: result.details,
|
|
379
|
+
isError,
|
|
380
|
+
})
|
|
381
|
+
: undefined;
|
|
382
|
+
const finalContent = hookResult?.content ?? result.content;
|
|
383
|
+
const finalIsError = hookResult?.isError ?? isError;
|
|
384
|
+
this._recordValidationToolResult(toolCall.name, args, finalContent, finalIsError);
|
|
243
385
|
if (!hookResult) {
|
|
244
386
|
return undefined;
|
|
245
387
|
}
|
|
@@ -256,15 +398,17 @@ export class AgentSession {
|
|
|
256
398
|
? async (_turn, signal) => await this.agent.prepareNextTurn?.(signal)
|
|
257
399
|
: undefined);
|
|
258
400
|
this.agent.prepareNextTurnWithContext = async (turn, signal) => {
|
|
259
|
-
const
|
|
260
|
-
const
|
|
401
|
+
const compactedContext = await this._compactBeforeNextProviderRequest(turn, signal);
|
|
402
|
+
const nextTurn = compactedContext ? { ...turn, context: compactedContext } : turn;
|
|
403
|
+
const previousSnapshot = await previousPrepareNextTurnWithContext?.(nextTurn, signal);
|
|
404
|
+
const previousContext = previousSnapshot?.context ?? nextTurn.context;
|
|
261
405
|
return {
|
|
262
406
|
...previousSnapshot,
|
|
263
|
-
context: {
|
|
407
|
+
context: this._withValidationHint({
|
|
264
408
|
...previousContext,
|
|
265
409
|
systemPrompt: this._systemPromptOverride ?? this._baseSystemPrompt,
|
|
266
410
|
tools: this.agent.state.tools.slice(),
|
|
267
|
-
},
|
|
411
|
+
}),
|
|
268
412
|
model: this.agent.state.model,
|
|
269
413
|
thinkingLevel: this.agent.state.thinkingLevel,
|
|
270
414
|
};
|
|
@@ -394,6 +538,20 @@ export class AgentSession {
|
|
|
394
538
|
_isTerminalAssistantFailure(message) {
|
|
395
539
|
return message.stopReason === "error" || message.stopReason === "aborted" || message.stopReason === "length";
|
|
396
540
|
}
|
|
541
|
+
_isPostAssistantPiServerSyncFailure(message) {
|
|
542
|
+
if (!isPiServerMode() || !message.errorMessage) {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
if (!/^(Session init|Session tree (append|sync|switch)|Session history reconciliation) failed/.test(message.errorMessage)) {
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
const messages = this.agent.state.messages;
|
|
549
|
+
if (messages[messages.length - 1] !== message) {
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
const previous = messages[messages.length - 2];
|
|
553
|
+
return previous?.role === "assistant" && !this._isTerminalAssistantFailure(previous);
|
|
554
|
+
}
|
|
397
555
|
_detachTerminalAssistantFailure() {
|
|
398
556
|
const branch = this.sessionManager.getBranch();
|
|
399
557
|
let terminalMessageEntry;
|
|
@@ -423,6 +581,16 @@ export class AgentSession {
|
|
|
423
581
|
_detachPiServerTerminalAssistantFailure() {
|
|
424
582
|
this._detachTerminalAssistantFailure();
|
|
425
583
|
}
|
|
584
|
+
_restoreSessionLeaf(leafId) {
|
|
585
|
+
if (leafId) {
|
|
586
|
+
this.sessionManager.branch(leafId);
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
this.sessionManager.resetLeaf();
|
|
590
|
+
}
|
|
591
|
+
const sessionContext = this.sessionManager.buildSessionContext();
|
|
592
|
+
this.agent.state.messages = sessionContext.messages;
|
|
593
|
+
}
|
|
426
594
|
_replaceMessageInPlace(target, replacement) {
|
|
427
595
|
// Agent-core stores the finalized message object in its state before emitting message_end.
|
|
428
596
|
// SessionManager persistence happens later in _handleAgentEvent() with event.message.
|
|
@@ -743,17 +911,100 @@ export class AgentSession {
|
|
|
743
911
|
// Prompting
|
|
744
912
|
// =========================================================================
|
|
745
913
|
async _runAgentPrompt(messages) {
|
|
914
|
+
this._resetValidationHintState();
|
|
746
915
|
try {
|
|
747
916
|
await this.agent.prompt(messages);
|
|
748
917
|
while (await this._handlePostAgentRun()) {
|
|
749
|
-
|
|
918
|
+
try {
|
|
919
|
+
await this.agent.continue();
|
|
920
|
+
}
|
|
921
|
+
catch (error) {
|
|
922
|
+
if (this._retryAttempt > 0) {
|
|
923
|
+
const attempt = this._retryAttempt;
|
|
924
|
+
this._retryAttempt = 0;
|
|
925
|
+
this._emit({
|
|
926
|
+
type: "auto_retry_end",
|
|
927
|
+
success: false,
|
|
928
|
+
attempt,
|
|
929
|
+
finalError: error instanceof Error ? error.message : String(error),
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
throw error;
|
|
933
|
+
}
|
|
750
934
|
}
|
|
935
|
+
await this._maybeGenerateSessionName();
|
|
751
936
|
}
|
|
752
937
|
finally {
|
|
753
938
|
this._systemPromptOverride = undefined;
|
|
754
939
|
this._flushPendingBashMessages();
|
|
755
940
|
}
|
|
756
941
|
}
|
|
942
|
+
_sessionHasInfoEntry() {
|
|
943
|
+
return this.sessionManager.getEntries().some((entry) => entry.type === "session_info");
|
|
944
|
+
}
|
|
945
|
+
_buildSessionNameTranscript() {
|
|
946
|
+
const lines = [];
|
|
947
|
+
for (const message of this.agent.state.messages) {
|
|
948
|
+
if (message.role !== "user" && message.role !== "assistant")
|
|
949
|
+
continue;
|
|
950
|
+
const text = messageTextContent(message);
|
|
951
|
+
if (!text)
|
|
952
|
+
continue;
|
|
953
|
+
lines.push(`${message.role === "user" ? "User" : "Assistant"}: ${text}`);
|
|
954
|
+
}
|
|
955
|
+
const transcript = lines.join("\n\n").trim();
|
|
956
|
+
return transcript ? truncateForSessionNameInput(transcript) : undefined;
|
|
957
|
+
}
|
|
958
|
+
_shouldGenerateSessionName() {
|
|
959
|
+
if (this._autoSessionNameAttempted || this._sessionHasInfoEntry())
|
|
960
|
+
return false;
|
|
961
|
+
const userMessages = this.agent.state.messages.filter((message) => message.role === "user");
|
|
962
|
+
if (userMessages.length !== 1)
|
|
963
|
+
return false;
|
|
964
|
+
const lastAssistant = this._findLastAssistantMessage();
|
|
965
|
+
return lastAssistant !== undefined && !this._isTerminalAssistantFailure(lastAssistant);
|
|
966
|
+
}
|
|
967
|
+
async _maybeGenerateSessionName() {
|
|
968
|
+
if (!this._shouldGenerateSessionName())
|
|
969
|
+
return;
|
|
970
|
+
this._autoSessionNameAttempted = true;
|
|
971
|
+
const model = this.model;
|
|
972
|
+
const transcript = this._buildSessionNameTranscript();
|
|
973
|
+
if (!model || !transcript)
|
|
974
|
+
return;
|
|
975
|
+
try {
|
|
976
|
+
const stream = await this.agent.streamFn(model, {
|
|
977
|
+
systemPrompt: this.systemPrompt,
|
|
978
|
+
messages: [
|
|
979
|
+
{
|
|
980
|
+
role: "user",
|
|
981
|
+
content: [{ type: "text", text: `${AUTO_SESSION_NAME_PROMPT}\n\n${transcript}` }],
|
|
982
|
+
timestamp: Date.now(),
|
|
983
|
+
},
|
|
984
|
+
],
|
|
985
|
+
tools: this.agent.state.tools,
|
|
986
|
+
}, {
|
|
987
|
+
maxTokens: 32,
|
|
988
|
+
reasoning: undefined,
|
|
989
|
+
sessionId: this.sessionId,
|
|
990
|
+
transport: this.agent.transport,
|
|
991
|
+
thinkingBudgets: this.agent.thinkingBudgets,
|
|
992
|
+
maxRetryDelayMs: this.agent.maxRetryDelayMs,
|
|
993
|
+
});
|
|
994
|
+
for await (const _event of stream) {
|
|
995
|
+
// Drain the title stream without surfacing it as a chat message.
|
|
996
|
+
}
|
|
997
|
+
const response = await stream.result();
|
|
998
|
+
if (response.stopReason === "error" || response.stopReason === "aborted")
|
|
999
|
+
return;
|
|
1000
|
+
const title = sanitizeGeneratedSessionName(messageTextContent(response));
|
|
1001
|
+
if (title)
|
|
1002
|
+
this.setSessionName(title);
|
|
1003
|
+
}
|
|
1004
|
+
catch {
|
|
1005
|
+
// Session naming is a post-turn convenience; the completed assistant turn stays authoritative.
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
757
1008
|
async _handlePostAgentRun() {
|
|
758
1009
|
const msg = this._lastAssistantMessage;
|
|
759
1010
|
this._lastAssistantMessage = undefined;
|
|
@@ -1636,7 +1887,7 @@ export class AgentSession {
|
|
|
1636
1887
|
/**
|
|
1637
1888
|
* Internal: Run auto-compaction with events.
|
|
1638
1889
|
*/
|
|
1639
|
-
async _runAutoCompaction(reason, willRetry, preparationOverride) {
|
|
1890
|
+
async _runAutoCompaction(reason, willRetry, preparationOverride, preparationOptions) {
|
|
1640
1891
|
const settings = this.settingsManager.getCompactionSettings();
|
|
1641
1892
|
let started = false;
|
|
1642
1893
|
try {
|
|
@@ -1672,6 +1923,7 @@ export class AgentSession {
|
|
|
1672
1923
|
apiKey,
|
|
1673
1924
|
headers,
|
|
1674
1925
|
settings,
|
|
1926
|
+
preparation: preparationOptions,
|
|
1675
1927
|
sessionTree: { entries: this.sessionManager.getEntries(), leafId: this.sessionManager.getLeafId() },
|
|
1676
1928
|
signal,
|
|
1677
1929
|
reasoning: toProviderReasoning(this.thinkingLevel),
|
|
@@ -1812,6 +2064,63 @@ export class AgentSession {
|
|
|
1812
2064
|
get autoCompactionEnabled() {
|
|
1813
2065
|
return this.settingsManager.getCompactionEnabled();
|
|
1814
2066
|
}
|
|
2067
|
+
_shouldCompactBeforeNextProviderRequest(turn) {
|
|
2068
|
+
const settings = this.settingsManager.getCompactionSettings();
|
|
2069
|
+
if (!settings.enabled || turn.toolResults.length === 0)
|
|
2070
|
+
return false;
|
|
2071
|
+
const contextWindow = this.model?.contextWindow ?? 0;
|
|
2072
|
+
if (contextWindow <= 0)
|
|
2073
|
+
return false;
|
|
2074
|
+
const projectedTokens = estimateContextTokens(turn.context.messages).tokens;
|
|
2075
|
+
if (projectedTokens >= contextWindow - settings.reserveTokens)
|
|
2076
|
+
return true;
|
|
2077
|
+
const toolResultTokens = estimateMessagesTokens(turn.toolResults);
|
|
2078
|
+
return toolResultTokens >= settings.keepRecentTokens;
|
|
2079
|
+
}
|
|
2080
|
+
_appendIntraTurnCompactionMarker() {
|
|
2081
|
+
const marker = {
|
|
2082
|
+
role: "custom",
|
|
2083
|
+
customType: INTRA_TURN_COMPACTION_MARKER_TYPE,
|
|
2084
|
+
content: [{ type: "text", text: INTRA_TURN_COMPACTION_MARKER_TEXT }],
|
|
2085
|
+
display: false,
|
|
2086
|
+
details: { reason: "intra-turn-compaction" },
|
|
2087
|
+
timestamp: Date.now(),
|
|
2088
|
+
};
|
|
2089
|
+
const markerId = this.sessionManager.appendCustomMessageEntry(marker.customType, marker.content, marker.display, marker.details);
|
|
2090
|
+
this.agent.state.messages = [...this.agent.state.messages, marker];
|
|
2091
|
+
return markerId;
|
|
2092
|
+
}
|
|
2093
|
+
_currentAgentContext(baseContext) {
|
|
2094
|
+
return {
|
|
2095
|
+
...baseContext,
|
|
2096
|
+
messages: this.agent.state.messages.slice(),
|
|
2097
|
+
systemPrompt: this._systemPromptOverride ?? this._baseSystemPrompt,
|
|
2098
|
+
tools: this.agent.state.tools.slice(),
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
2101
|
+
async _compactBeforeNextProviderRequest(turn, signal) {
|
|
2102
|
+
if (signal?.aborted || !this._shouldCompactBeforeNextProviderRequest(turn)) {
|
|
2103
|
+
return undefined;
|
|
2104
|
+
}
|
|
2105
|
+
const settings = this.settingsManager.getCompactionSettings();
|
|
2106
|
+
const markerParentId = this.sessionManager.getLeafId();
|
|
2107
|
+
const markerId = this._appendIntraTurnCompactionMarker();
|
|
2108
|
+
const preparationOptions = { firstKeptEntryId: markerId };
|
|
2109
|
+
const branchWithMarker = this.sessionManager.getBranch();
|
|
2110
|
+
const previousCompactionId = getLatestCompactionEntry(branchWithMarker)?.id;
|
|
2111
|
+
const preparation = prepareCompaction(branchWithMarker, settings, preparationOptions);
|
|
2112
|
+
if (!preparation) {
|
|
2113
|
+
this._restoreSessionLeaf(markerParentId);
|
|
2114
|
+
return undefined;
|
|
2115
|
+
}
|
|
2116
|
+
await this._runAutoCompaction("threshold", false, preparation, preparationOptions);
|
|
2117
|
+
const currentCompactionId = getLatestCompactionEntry(this.sessionManager.getBranch())?.id;
|
|
2118
|
+
if (currentCompactionId === previousCompactionId) {
|
|
2119
|
+
this._restoreSessionLeaf(markerParentId);
|
|
2120
|
+
return undefined;
|
|
2121
|
+
}
|
|
2122
|
+
return this._currentAgentContext(turn.context);
|
|
2123
|
+
}
|
|
1815
2124
|
async bindExtensions(bindings) {
|
|
1816
2125
|
if (bindings.uiContext !== undefined) {
|
|
1817
2126
|
this._extensionUIContext = bindings.uiContext;
|
|
@@ -2150,6 +2459,11 @@ export class AgentSession {
|
|
|
2150
2459
|
// Context overflow is handled by compaction, not retry.
|
|
2151
2460
|
if (isContextOverflow(message, this.model?.contextWindow ?? 0))
|
|
2152
2461
|
return false;
|
|
2462
|
+
if (this._isPostAssistantPiServerSyncFailure(message))
|
|
2463
|
+
return false;
|
|
2464
|
+
const piServerPhase = getPiServerFailurePhase(message);
|
|
2465
|
+
if (piServerPhase && piServerPhase !== "provider_stream")
|
|
2466
|
+
return false;
|
|
2153
2467
|
if (message.errorMessage && this._isNonRetryableProviderLimitError(message.errorMessage))
|
|
2154
2468
|
return false;
|
|
2155
2469
|
return isRetryableAssistantError(message);
|
|
@@ -2187,6 +2501,18 @@ export class AgentSession {
|
|
|
2187
2501
|
this.agent.state.messages = messages.slice(0, -1);
|
|
2188
2502
|
}
|
|
2189
2503
|
}
|
|
2504
|
+
const lastMessage = this.agent.state.messages[this.agent.state.messages.length - 1];
|
|
2505
|
+
if (!lastMessage || lastMessage.role === "assistant") {
|
|
2506
|
+
const attempt = this._retryAttempt;
|
|
2507
|
+
this._retryAttempt = 0;
|
|
2508
|
+
this._emit({
|
|
2509
|
+
type: "auto_retry_end",
|
|
2510
|
+
success: false,
|
|
2511
|
+
attempt,
|
|
2512
|
+
finalError: lastMessage ? "Cannot continue from message role: assistant" : "No messages to continue from",
|
|
2513
|
+
});
|
|
2514
|
+
return false;
|
|
2515
|
+
}
|
|
2190
2516
|
// Wait with exponential backoff (abortable)
|
|
2191
2517
|
this._retryAbortController = new AbortController();
|
|
2192
2518
|
try {
|