@astralyn/pi 0.86.0 → 0.87.0
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 +45 -0
- package/dist/bundle/chunks/chunk-INDBRWNK.js +2 -0
- package/dist/bundle/chunks/{chunk-MSHHFVMD.js → chunk-RKLIHRPH.js} +58 -51
- package/dist/bundle/chunks/github-copilot.js +1 -1
- package/dist/bundle/chunks/{openai-completions-WJIA7NEB.js → openai-completions-JZCEEJ43.js} +1 -1
- package/dist/bundle/chunks/{virtual-modules-B4BILEXT.js → virtual-modules-CG7NJ5WO.js} +1 -1
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/file-processor.d.ts +1 -1
- package/dist/cli/file-processor.d.ts.map +1 -1
- package/dist/cli/file-processor.js.map +1 -1
- package/dist/core/agent-session-runtime.js +1 -1
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session.d.ts +31 -4
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +395 -126
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/cache-warmer.d.ts +1 -0
- package/dist/core/cache-warmer.d.ts.map +1 -1
- package/dist/core/cache-warmer.js +15 -1
- package/dist/core/cache-warmer.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +3 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +146 -48
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/crash-log.d.ts +5 -0
- package/dist/core/crash-log.d.ts.map +1 -1
- package/dist/core/crash-log.js +68 -0
- package/dist/core/crash-log.js.map +1 -1
- package/dist/core/export-html/template.js +6 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +16 -3
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +110 -5
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +77 -6
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/model-config.d.ts +52 -0
- package/dist/core/model-config.d.ts.map +1 -1
- package/dist/core/model-config.js +16 -0
- package/dist/core/model-config.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +1 -0
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/prompt-templates.d.ts +6 -1
- package/dist/core/prompt-templates.d.ts.map +1 -1
- package/dist/core/prompt-templates.js +61 -35
- package/dist/core/prompt-templates.js.map +1 -1
- package/dist/core/provider-composer.d.ts +1 -0
- package/dist/core/provider-composer.d.ts.map +1 -1
- package/dist/core/provider-composer.js +19 -0
- package/dist/core/provider-composer.js.map +1 -1
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +6 -2
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +3 -4
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +36 -9
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +97 -7
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/tools/read.d.ts +4 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +5 -1
- package/dist/core/tools/read.js.map +1 -1
- package/dist/extensions/btw/agent.d.ts.map +1 -1
- package/dist/extensions/btw/agent.js +8 -5
- package/dist/extensions/btw/agent.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +4 -3
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/bug-report.d.ts +2 -0
- package/dist/modes/interactive/bug-report.d.ts.map +1 -1
- package/dist/modes/interactive/bug-report.js +6 -5
- package/dist/modes/interactive/bug-report.js.map +1 -1
- package/dist/modes/interactive/components/extension-editor.d.ts +4 -1
- package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-editor.js +7 -2
- package/dist/modes/interactive/components/extension-editor.js.map +1 -1
- package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/tree-selector.js +7 -0
- package/dist/modes/interactive/components/tree-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +5 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +74 -5
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/clipboard-image.d.ts.map +1 -1
- package/dist/utils/clipboard-image.js +1 -12
- package/dist/utils/clipboard-image.js.map +1 -1
- package/dist/utils/clipboard.d.ts.map +1 -1
- package/dist/utils/clipboard.js +74 -20
- package/dist/utils/clipboard.js.map +1 -1
- package/dist/utils/mime.js +1 -1
- package/dist/utils/mime.js.map +1 -1
- package/dist/utils/tool-result-images.d.ts +3 -1
- package/dist/utils/tool-result-images.d.ts.map +1 -1
- package/dist/utils/tool-result-images.js +4 -1
- package/dist/utils/tool-result-images.js.map +1 -1
- package/dist/utils/wsl.d.ts +3 -0
- package/dist/utils/wsl.d.ts.map +1 -0
- package/dist/utils/wsl.js +15 -0
- package/dist/utils/wsl.js.map +1 -0
- package/docs/compaction.md +23 -4
- package/docs/extensions.md +72 -8
- package/docs/models.md +30 -1
- package/docs/providers.md +8 -0
- package/docs/sdk.md +14 -3
- package/docs/session-format.md +25 -4
- package/examples/plugins/pi-example-plugin/src/session.ts +3 -2
- package/npm-shrinkwrap.json +43 -43
- package/package.json +8 -8
- package/dist/bundle/chunks/chunk-M2YWW37K.js +0 -2
|
@@ -18,23 +18,25 @@ import { contentText, getCurrentSystemMessage, getSystemMessageText, retryDelayM
|
|
|
18
18
|
import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, isRecoverableLength, isRetryableAssistantError, modelsAreEqual, resetApiProviders, streamSimple, } from "@earendil-works/pi-ai/compat";
|
|
19
19
|
import { getThemeByName, theme } from "../modes/interactive/theme/theme.js";
|
|
20
20
|
import { stripFrontmatter } from "../utils/frontmatter.js";
|
|
21
|
+
import { processImage } from "../utils/image-process.js";
|
|
21
22
|
import { sleep } from "../utils/sleep.js";
|
|
22
23
|
import { normalizeToolResultImages } from "../utils/tool-result-images.js";
|
|
23
24
|
import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
|
|
24
25
|
import { BackgroundSession } from "./background/session.js";
|
|
25
26
|
import { executeBashWithOperations } from "./bash-executor.js";
|
|
26
27
|
import { generateBugReportSummary } from "./bug-report.js";
|
|
27
|
-
import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
|
|
28
|
+
import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateProjectedContextTokens, estimateTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
|
|
28
29
|
import { DEFAULT_THINKING_LEVEL, THINKING_LEVEL_OPTIONS } from "./defaults.js";
|
|
29
30
|
import { exportSessionToHtml } from "./export-html/index.js";
|
|
30
31
|
import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
|
|
31
32
|
import { ExtensionRunner, STALE_EXTENSION_CONTEXT_MESSAGE, wrapRegisteredTools, } from "./extensions/index.js";
|
|
32
33
|
import { emitSessionShutdownEvent } from "./extensions/runner.js";
|
|
34
|
+
import { convertToLlm } from "./messages.js";
|
|
33
35
|
import { ModelRegistry } from "./model-registry.js";
|
|
34
36
|
import { resolveModelStreamOptions } from "./model-stream-options.js";
|
|
35
37
|
import { expandPromptTemplate } from "./prompt-templates.js";
|
|
36
38
|
import { exportSessionToJsonl } from "./session-export.js";
|
|
37
|
-
import { getLatestCompactionEntry } from "./session-manager.js";
|
|
39
|
+
import { getLatestCompactionEntry, SessionManager, } from "./session-manager.js";
|
|
38
40
|
import { createSyntheticSourceInfo } from "./source-info.js";
|
|
39
41
|
import { buildSystemPrompt, buildSystemPromptSections, diffSystemPromptSections, normalizeBuildSystemPromptOptions, } from "./system-prompt.js";
|
|
40
42
|
import { createLocalBashOperations } from "./tools/bash.js";
|
|
@@ -100,6 +102,14 @@ export class AgentSession {
|
|
|
100
102
|
this._bashAbortControllers = new Set();
|
|
101
103
|
this._pendingBashMessages = [];
|
|
102
104
|
this._turnIndex = 0;
|
|
105
|
+
this._entryIdsByMessage = new WeakMap();
|
|
106
|
+
this._boundaryDispatchedMessages = new WeakSet();
|
|
107
|
+
this._lastAssistantToolResults = [];
|
|
108
|
+
this._lastActivityOutcome = "completed";
|
|
109
|
+
this._isBeforeSettle = false;
|
|
110
|
+
this._abortDuringBeforeSettle = false;
|
|
111
|
+
this._isEmittingAgentSettled = false;
|
|
112
|
+
this._deferredSettledActions = [];
|
|
103
113
|
this._baseToolDefinitions = new Map();
|
|
104
114
|
this._extensionMode = "print";
|
|
105
115
|
// Tool registry for extension getTools/setTools
|
|
@@ -107,8 +117,6 @@ export class AgentSession {
|
|
|
107
117
|
this._toolDefinitions = new Map();
|
|
108
118
|
this._toolPromptSnippets = new Map();
|
|
109
119
|
this._toolPromptGuidelines = new Map();
|
|
110
|
-
// Track last assistant message for auto-compaction check
|
|
111
|
-
this._lastAssistantMessage = undefined;
|
|
112
120
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
113
121
|
this._handleAgentEvent = async (event) => {
|
|
114
122
|
// Preserve host delivery identity before extension message_end transforms.
|
|
@@ -136,18 +144,16 @@ export class AgentSession {
|
|
|
136
144
|
}
|
|
137
145
|
}
|
|
138
146
|
}
|
|
139
|
-
// Emit to extensions first
|
|
147
|
+
// Emit to extensions first, then notify public listeners.
|
|
140
148
|
await this._emitExtensionEvent(event);
|
|
141
|
-
// Notify all listeners
|
|
142
149
|
this._emit(event.type === "agent_end" ? { ...event, willRetry: this._willRetryAfterAgentEnd(event) } : event);
|
|
143
150
|
// Handle session persistence
|
|
144
151
|
if (event.type === "message_end") {
|
|
145
152
|
if (this._persistMessage(event.message))
|
|
146
153
|
acknowledgeBackground?.();
|
|
147
|
-
// Track assistant message for auto-compaction (checked on agent_end)
|
|
148
154
|
if (event.message.role === "assistant") {
|
|
149
|
-
this._lastAssistantMessage = event.message;
|
|
150
155
|
const assistantMsg = event.message;
|
|
156
|
+
this._lastAssistantMessage = assistantMsg;
|
|
151
157
|
if (assistantMsg.stopReason !== "error" && assistantMsg.stopReason !== "length") {
|
|
152
158
|
this._overflowRecoveryAttempted = false;
|
|
153
159
|
}
|
|
@@ -169,6 +175,7 @@ export class AgentSession {
|
|
|
169
175
|
// extension and listener dispatch above also picks up messages that turn_end
|
|
170
176
|
// handlers queued.
|
|
171
177
|
if (event.type === "turn_end") {
|
|
178
|
+
this._lastAssistantToolResults = event.toolResults;
|
|
172
179
|
this._flushPendingCustomMessages();
|
|
173
180
|
}
|
|
174
181
|
};
|
|
@@ -212,6 +219,8 @@ export class AgentSession {
|
|
|
212
219
|
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
|
213
220
|
this._installAgentToolHooks();
|
|
214
221
|
this._installAgentNextTurnRefresh();
|
|
222
|
+
this._installAgentRequestProjection();
|
|
223
|
+
this._installAgentBoundaryHooks();
|
|
215
224
|
this._installAgentForcedPromptProjection();
|
|
216
225
|
this._buildRuntime({
|
|
217
226
|
activeToolNames: this._initialActiveToolNames,
|
|
@@ -264,7 +273,8 @@ export class AgentSession {
|
|
|
264
273
|
}),
|
|
265
274
|
};
|
|
266
275
|
const currentSystemPrompt = this.systemPrompt;
|
|
267
|
-
|
|
276
|
+
// The canonical projection is what the next request converts; the inspection cache may lag it.
|
|
277
|
+
const messages = structuredClone(this.sessionManager.buildSessionProjection().messages);
|
|
268
278
|
const convert = this.agent.convertToLlm;
|
|
269
279
|
const transform = this.agent.transformContext;
|
|
270
280
|
const prepared = this._captureModelContext
|
|
@@ -375,8 +385,10 @@ export class AgentSession {
|
|
|
375
385
|
: undefined;
|
|
376
386
|
const content = hookResult?.content ?? result.content ?? [];
|
|
377
387
|
// Runs after the extension hook so images injected or replaced by extensions are normalized too.
|
|
388
|
+
const resizeOptions = this.model?.inputLimits?.images?.resize;
|
|
378
389
|
const normalizedContent = await normalizeToolResultImages(content, {
|
|
379
390
|
autoResizeImages: this.settingsManager.getImageAutoResize(),
|
|
391
|
+
...(resizeOptions ? { resizeOptions } : {}),
|
|
380
392
|
});
|
|
381
393
|
if (!hookResult && normalizedContent === content) {
|
|
382
394
|
return undefined;
|
|
@@ -392,15 +404,83 @@ export class AgentSession {
|
|
|
392
404
|
async _compactBeforeNextAssistantResponse(context) {
|
|
393
405
|
const model = this.model;
|
|
394
406
|
const settings = this.settingsManager.getCompactionSettings(model);
|
|
407
|
+
const projection = this.sessionManager.buildSessionProjection();
|
|
395
408
|
if (!model ||
|
|
396
409
|
model.contextWindow <= 0 ||
|
|
397
|
-
!shouldCompact(
|
|
398
|
-
return context;
|
|
410
|
+
!shouldCompact(estimateProjectedContextTokens(projection, this.sessionManager.getBranch()).tokens, model.contextWindow, settings)) {
|
|
411
|
+
return { ...context, messages: projection.messages };
|
|
399
412
|
}
|
|
400
413
|
await this._runAutoCompaction("threshold", false);
|
|
401
|
-
return {
|
|
402
|
-
|
|
403
|
-
|
|
414
|
+
return { ...context, messages: this.sessionManager.buildSessionProjection().messages };
|
|
415
|
+
}
|
|
416
|
+
_installAgentRequestProjection() {
|
|
417
|
+
const previousPrepareRequest = this.agent.prepareRequest;
|
|
418
|
+
this.agent.prepareRequest = async (request, signal) => {
|
|
419
|
+
const canonicalContext = {
|
|
420
|
+
...request.context,
|
|
421
|
+
messages: this.sessionManager.buildSessionProjection().messages,
|
|
422
|
+
// Messages declare the provider-visible loadout; context.tools keeps executable implementations.
|
|
423
|
+
tools: this.agent.state.tools.slice(),
|
|
424
|
+
};
|
|
425
|
+
const previous = await previousPrepareRequest?.({
|
|
426
|
+
...request,
|
|
427
|
+
context: canonicalContext,
|
|
428
|
+
model: this.agent.state.model,
|
|
429
|
+
thinkingLevel: this.agent.state.thinkingLevel,
|
|
430
|
+
}, signal);
|
|
431
|
+
return {
|
|
432
|
+
...previous,
|
|
433
|
+
context: previous?.context ?? canonicalContext,
|
|
434
|
+
model: previous?.model ?? this.agent.state.model,
|
|
435
|
+
thinkingLevel: previous?.thinkingLevel ?? this.agent.state.thinkingLevel,
|
|
436
|
+
};
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
async _dispatchTurnEndBoundary(message, toolResults) {
|
|
440
|
+
this._lastActivityOutcome =
|
|
441
|
+
message.stopReason === "aborted" ? "aborted" : message.stopReason === "error" ? "error" : "completed";
|
|
442
|
+
const messageEntryId = this._findPersistedMessageEntryId(message);
|
|
443
|
+
if (!this._extensionRunner.hasHandlers("turn_end"))
|
|
444
|
+
return false;
|
|
445
|
+
if (!messageEntryId) {
|
|
446
|
+
this._extensionRunner.emitError({
|
|
447
|
+
extensionPath: "<boundary>",
|
|
448
|
+
event: "turn_end",
|
|
449
|
+
error: "turn_end could not resolve the persisted assistant entry ID",
|
|
450
|
+
});
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
const toolResultEntryIds = toolResults.flatMap((result) => {
|
|
454
|
+
const entryId = this._findPersistedMessageEntryId(result);
|
|
455
|
+
return entryId ? [entryId] : [];
|
|
456
|
+
});
|
|
457
|
+
const boundary = await this._extensionRunner.emitBoundary({
|
|
458
|
+
type: "turn_end",
|
|
459
|
+
turnIndex: this._turnIndex,
|
|
460
|
+
message,
|
|
461
|
+
toolResults,
|
|
462
|
+
messageEntryId,
|
|
463
|
+
toolResultEntryIds,
|
|
464
|
+
outcome: this._lastActivityOutcome,
|
|
465
|
+
}, (entries) => this._buildBoundaryContext(entries, "turn_end"));
|
|
466
|
+
this._commitBoundaryDrafts(boundary.entries);
|
|
467
|
+
if (boundary.continue && !this._buildBoundaryContext([], "turn_end").canContinue) {
|
|
468
|
+
this._reportInvalidBoundaryContinuation("turn_end");
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
return boundary.continue;
|
|
472
|
+
}
|
|
473
|
+
_installAgentBoundaryHooks() {
|
|
474
|
+
const previousFinishTurn = this.agent.finishTurn;
|
|
475
|
+
this.agent.finishTurn = async (turn, signal) => {
|
|
476
|
+
this._boundaryDispatchedMessages.add(turn.message);
|
|
477
|
+
const extensionContinue = await this._dispatchTurnEndBoundary(turn.message, turn.toolResults);
|
|
478
|
+
const previousDecision = await previousFinishTurn?.(turn, signal);
|
|
479
|
+
if (previousDecision?.action === "end")
|
|
480
|
+
return previousDecision;
|
|
481
|
+
if (extensionContinue || previousDecision?.action === "continue")
|
|
482
|
+
return { action: "continue" };
|
|
483
|
+
return undefined;
|
|
404
484
|
};
|
|
405
485
|
}
|
|
406
486
|
_installAgentNextTurnRefresh() {
|
|
@@ -409,7 +489,10 @@ export class AgentSession {
|
|
|
409
489
|
? async (_turn, signal) => await this.agent.prepareNextTurn?.(signal)
|
|
410
490
|
: undefined);
|
|
411
491
|
this.agent.prepareNextTurnWithContext = async (turn, signal) => {
|
|
412
|
-
const context = await this._compactBeforeNextAssistantResponse(
|
|
492
|
+
const context = await this._compactBeforeNextAssistantResponse({
|
|
493
|
+
...turn.context,
|
|
494
|
+
messages: this.sessionManager.buildSessionProjection().messages,
|
|
495
|
+
});
|
|
413
496
|
const previousSnapshot = await previousPrepareNextTurnWithContext?.({ ...turn, context }, signal);
|
|
414
497
|
const nextContext = previousSnapshot?.context ?? context;
|
|
415
498
|
const runOptions = this._runSystemPromptOptions ?? this._baseSystemPromptOptions;
|
|
@@ -439,6 +522,84 @@ export class AgentSession {
|
|
|
439
522
|
// =========================================================================
|
|
440
523
|
// Event Subscription
|
|
441
524
|
// =========================================================================
|
|
525
|
+
_refreshFinalizedContext() {
|
|
526
|
+
const projection = this.sessionManager.buildSessionProjection();
|
|
527
|
+
for (const entry of projection.entries) {
|
|
528
|
+
for (const message of entry.messages)
|
|
529
|
+
this._entryIdsByMessage.set(message, entry.sourceEntry.id);
|
|
530
|
+
}
|
|
531
|
+
this.agent.state.messages = projection.messages;
|
|
532
|
+
}
|
|
533
|
+
_applyBoundaryDrafts(manager, drafts) {
|
|
534
|
+
const appended = [];
|
|
535
|
+
for (const draft of drafts) {
|
|
536
|
+
let entryId;
|
|
537
|
+
switch (draft.type) {
|
|
538
|
+
case "custom":
|
|
539
|
+
entryId = manager.appendCustomEntry(draft.customType, draft.data);
|
|
540
|
+
break;
|
|
541
|
+
case "custom_message":
|
|
542
|
+
entryId = manager.appendCustomMessageEntry(draft.customType, draft.content, draft.display, draft.details);
|
|
543
|
+
break;
|
|
544
|
+
case "context_edit":
|
|
545
|
+
entryId = manager.appendContextEdit(draft.targetId, draft.replacement);
|
|
546
|
+
break;
|
|
547
|
+
case "compaction": {
|
|
548
|
+
const tokensBefore = estimateProjectedContextTokens(manager.buildSessionProjection(), manager.getBranch()).tokens;
|
|
549
|
+
entryId = manager.appendCompaction(draft.summary, draft.firstKeptEntryId, tokensBefore, draft.details, true, draft.usage);
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
const entry = manager.getEntry(entryId);
|
|
554
|
+
if (entry)
|
|
555
|
+
appended.push(entry);
|
|
556
|
+
}
|
|
557
|
+
return appended;
|
|
558
|
+
}
|
|
559
|
+
_createBoundaryPreviewManager(drafts) {
|
|
560
|
+
const header = this.sessionManager.getHeader();
|
|
561
|
+
if (!header)
|
|
562
|
+
throw new Error("Session header is missing");
|
|
563
|
+
const manager = SessionManager.inMemory(this._cwd, undefined, [header, ...this.sessionManager.getBranch()]);
|
|
564
|
+
this._applyBoundaryDrafts(manager, drafts);
|
|
565
|
+
return manager;
|
|
566
|
+
}
|
|
567
|
+
_getPendingBoundaryMessages() {
|
|
568
|
+
return [...this.agent.peekQueuedMessages(), ...this._pendingCustomMessages];
|
|
569
|
+
}
|
|
570
|
+
_buildBoundaryContext(drafts, boundary) {
|
|
571
|
+
const projection = this._createBoundaryPreviewManager(drafts).buildSessionProjection();
|
|
572
|
+
const pendingMessages = this._getPendingBoundaryMessages();
|
|
573
|
+
const llmMessages = convertToLlm(projection.messages);
|
|
574
|
+
const finalRole = llmMessages[llmMessages.length - 1]?.role;
|
|
575
|
+
const hasNonSystemContext = llmMessages.some((message) => message.role !== "system");
|
|
576
|
+
const contextCanContinue = hasNonSystemContext && finalRole !== "assistant";
|
|
577
|
+
const pendingCustomContext = this._pendingCustomMessages.length > 0;
|
|
578
|
+
return {
|
|
579
|
+
contextEntries: projection.entries,
|
|
580
|
+
contextMessages: projection.messages,
|
|
581
|
+
llmMessages,
|
|
582
|
+
pendingMessages,
|
|
583
|
+
canContinue: contextCanContinue ||
|
|
584
|
+
pendingCustomContext ||
|
|
585
|
+
(boundary === "turn_end"
|
|
586
|
+
? this.agent.hasQueuedMessages()
|
|
587
|
+
: finalRole === "assistant" && this.agent.hasQueuedMessages()),
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
_commitBoundaryDrafts(drafts) {
|
|
591
|
+
const appended = this._applyBoundaryDrafts(this.sessionManager, drafts);
|
|
592
|
+
this._refreshFinalizedContext();
|
|
593
|
+
for (const entry of appended)
|
|
594
|
+
this._emit({ type: "entry_appended", entry });
|
|
595
|
+
}
|
|
596
|
+
_reportInvalidBoundaryContinuation(event) {
|
|
597
|
+
this._extensionRunner.emitError({
|
|
598
|
+
extensionPath: "<boundary>",
|
|
599
|
+
event,
|
|
600
|
+
error: `${event} requested continuation without runnable model context`,
|
|
601
|
+
});
|
|
602
|
+
}
|
|
442
603
|
/** Emit an event to all listeners */
|
|
443
604
|
_emit(event) {
|
|
444
605
|
for (const l of this._eventListeners) {
|
|
@@ -476,21 +637,35 @@ export class AgentSession {
|
|
|
476
637
|
}
|
|
477
638
|
async _emitAgentSettled() {
|
|
478
639
|
this._cacheWarmer?.onAgentSettled();
|
|
640
|
+
// Background completions stay paused until runs requested by settled handlers have started,
|
|
641
|
+
// so a deferred prompt never races a delivery for the same idle gap.
|
|
479
642
|
const resumeBackground = this.pauseBackgroundNotifications();
|
|
480
643
|
this._isAgentRunActive = false;
|
|
481
644
|
try {
|
|
482
|
-
|
|
483
|
-
|
|
645
|
+
this._isEmittingAgentSettled = true;
|
|
646
|
+
try {
|
|
647
|
+
await this._extensionRunner.emit({ type: "agent_settled" });
|
|
648
|
+
this._emit({ type: "agent_settled" });
|
|
649
|
+
}
|
|
650
|
+
finally {
|
|
651
|
+
this._isEmittingAgentSettled = false;
|
|
652
|
+
}
|
|
653
|
+
for (const action of this._deferredSettledActions.splice(0))
|
|
654
|
+
await action();
|
|
484
655
|
}
|
|
485
656
|
finally {
|
|
486
657
|
this._resolveIdleWaitIfIdle();
|
|
487
658
|
resumeBackground();
|
|
488
659
|
}
|
|
489
660
|
}
|
|
490
|
-
/**
|
|
661
|
+
/**
|
|
662
|
+
* Persist model/custom messages and consume queued context only after the append succeeds.
|
|
663
|
+
* Returns the persisted entry ID, which boundary events and recovery omissions resolve later.
|
|
664
|
+
*/
|
|
491
665
|
_persistMessage(message) {
|
|
666
|
+
let entryId;
|
|
492
667
|
if (message.role === "custom") {
|
|
493
|
-
this.sessionManager.appendCustomMessageEntry(message.customType, message.content, message.display, message.details);
|
|
668
|
+
entryId = this.sessionManager.appendCustomMessageEntry(message.customType, message.content, message.display, message.details);
|
|
494
669
|
const pendingIndex = this._pendingNextTurnMessages.indexOf(message);
|
|
495
670
|
if (pendingIndex !== -1)
|
|
496
671
|
this._pendingNextTurnMessages.splice(pendingIndex, 1);
|
|
@@ -499,15 +674,16 @@ export class AgentSession {
|
|
|
499
674
|
message.role === "user" ||
|
|
500
675
|
message.role === "assistant" ||
|
|
501
676
|
message.role === "toolResult") {
|
|
502
|
-
this.sessionManager.appendMessage(message);
|
|
677
|
+
entryId = this.sessionManager.appendMessage(message);
|
|
503
678
|
}
|
|
504
679
|
else {
|
|
505
680
|
// Bash execution, compaction and branch summaries are persisted by their owners.
|
|
506
|
-
return
|
|
681
|
+
return undefined;
|
|
507
682
|
}
|
|
683
|
+
this._entryIdsByMessage.set(message, entryId);
|
|
508
684
|
if (message.role === "toolResult")
|
|
509
685
|
this._backgroundHost.acknowledgeWaitResult(message.details);
|
|
510
|
-
return
|
|
686
|
+
return entryId;
|
|
511
687
|
}
|
|
512
688
|
_willRetryAfterAgentEnd(event) {
|
|
513
689
|
if (this._agentRunAbortRequested)
|
|
@@ -524,6 +700,47 @@ export class AgentSession {
|
|
|
524
700
|
}
|
|
525
701
|
return false;
|
|
526
702
|
}
|
|
703
|
+
_findPersistedMessageEntryId(message) {
|
|
704
|
+
const mapped = this._entryIdsByMessage.get(message);
|
|
705
|
+
if (mapped)
|
|
706
|
+
return mapped;
|
|
707
|
+
for (const entry of [...this.sessionManager.getBranch()].reverse()) {
|
|
708
|
+
if (entry.type === "message" && entry.message === message)
|
|
709
|
+
return entry.id;
|
|
710
|
+
}
|
|
711
|
+
const messageIndex = this.agent.state.messages.indexOf(message);
|
|
712
|
+
if (messageIndex < 0)
|
|
713
|
+
return undefined;
|
|
714
|
+
const projection = this.sessionManager.buildSessionProjection();
|
|
715
|
+
let projectedIndex = 0;
|
|
716
|
+
for (const entry of projection.entries) {
|
|
717
|
+
for (let i = 0; i < entry.messages.length; i++) {
|
|
718
|
+
if (projectedIndex === messageIndex) {
|
|
719
|
+
this._entryIdsByMessage.set(message, entry.sourceEntry.id);
|
|
720
|
+
return entry.sourceEntry.id;
|
|
721
|
+
}
|
|
722
|
+
projectedIndex++;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return undefined;
|
|
726
|
+
}
|
|
727
|
+
_omitRecoveryAttempt(message, toolResults = []) {
|
|
728
|
+
const targets = [message, ...toolResults];
|
|
729
|
+
const targetIds = targets.map((target) => this._findPersistedMessageEntryId(target));
|
|
730
|
+
const unresolvedProjectedTarget = targets.some((target, index) => targetIds[index] === undefined && this.agent.state.messages.includes(target));
|
|
731
|
+
if (unresolvedProjectedTarget) {
|
|
732
|
+
throw new Error("Cannot persist recovery omission because a projected message has no source entry");
|
|
733
|
+
}
|
|
734
|
+
for (const targetId of targetIds) {
|
|
735
|
+
if (!targetId)
|
|
736
|
+
continue;
|
|
737
|
+
const editId = this.sessionManager.appendContextEdit(targetId, null);
|
|
738
|
+
const entry = this.sessionManager.getEntry(editId);
|
|
739
|
+
if (entry)
|
|
740
|
+
this._emit({ type: "entry_appended", entry });
|
|
741
|
+
}
|
|
742
|
+
this._refreshFinalizedContext();
|
|
743
|
+
}
|
|
527
744
|
/** Find the last assistant message in agent state (including aborted ones) */
|
|
528
745
|
_findLastAssistantMessage() {
|
|
529
746
|
const messages = this.agent.state.messages;
|
|
@@ -567,13 +784,9 @@ export class AgentSession {
|
|
|
567
784
|
await this._extensionRunner.emit(extensionEvent);
|
|
568
785
|
}
|
|
569
786
|
else if (event.type === "turn_end") {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
message: event.message,
|
|
574
|
-
toolResults: event.toolResults,
|
|
575
|
-
};
|
|
576
|
-
await this._extensionRunner.emit(extensionEvent);
|
|
787
|
+
if (event.message.role === "assistant" && !this._boundaryDispatchedMessages.delete(event.message)) {
|
|
788
|
+
await this._dispatchTurnEndBoundary(event.message, event.toolResults);
|
|
789
|
+
}
|
|
577
790
|
this._turnIndex++;
|
|
578
791
|
}
|
|
579
792
|
else if (event.type === "message_start") {
|
|
@@ -690,6 +903,10 @@ export class AgentSession {
|
|
|
690
903
|
// =========================================================================
|
|
691
904
|
// Read-only State Access
|
|
692
905
|
// =========================================================================
|
|
906
|
+
/** Refresh the public finalized transcript from the canonical session projection. */
|
|
907
|
+
refreshContext() {
|
|
908
|
+
this._refreshFinalizedContext();
|
|
909
|
+
}
|
|
693
910
|
/** Full agent state */
|
|
694
911
|
get state() {
|
|
695
912
|
return this.agent.state;
|
|
@@ -924,7 +1141,15 @@ export class AgentSession {
|
|
|
924
1141
|
this._isAgentRunActive = true;
|
|
925
1142
|
try {
|
|
926
1143
|
await this.agent.prompt(messages);
|
|
927
|
-
while (
|
|
1144
|
+
while (!this._agentRunAbortRequested) {
|
|
1145
|
+
if (await this._handlePostAgentRun()) {
|
|
1146
|
+
if (this._agentRunAbortRequested)
|
|
1147
|
+
break;
|
|
1148
|
+
await this.agent.continue();
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
if (this._agentRunAbortRequested || !(await this._runBeforeSettleBoundary()))
|
|
1152
|
+
break;
|
|
928
1153
|
if (this._agentRunAbortRequested)
|
|
929
1154
|
break;
|
|
930
1155
|
await this.agent.continue();
|
|
@@ -940,16 +1165,17 @@ export class AgentSession {
|
|
|
940
1165
|
}
|
|
941
1166
|
}
|
|
942
1167
|
async _handlePostAgentRun() {
|
|
943
|
-
const
|
|
1168
|
+
const message = this._lastAssistantMessage;
|
|
1169
|
+
const toolResults = this._lastAssistantToolResults;
|
|
944
1170
|
this._lastAssistantMessage = undefined;
|
|
1171
|
+
this._lastAssistantToolResults = [];
|
|
945
1172
|
if (this._agentRunAbortRequested) {
|
|
946
1173
|
this._finishCancelledRetry();
|
|
947
1174
|
return false;
|
|
948
1175
|
}
|
|
949
|
-
if (!
|
|
950
|
-
return
|
|
951
|
-
|
|
952
|
-
if (this._isRetryableError(msg) && (await this._prepareRetry(msg))) {
|
|
1176
|
+
if (!message)
|
|
1177
|
+
return this.agent.hasQueuedMessages();
|
|
1178
|
+
if (this._isRetryableError(message) && (await this._prepareRetry(message))) {
|
|
953
1179
|
if (this._agentRunAbortRequested)
|
|
954
1180
|
this._finishCancelledRetry();
|
|
955
1181
|
return !this._agentRunAbortRequested;
|
|
@@ -958,22 +1184,46 @@ export class AgentSession {
|
|
|
958
1184
|
this._finishCancelledRetry();
|
|
959
1185
|
return false;
|
|
960
1186
|
}
|
|
961
|
-
if (
|
|
1187
|
+
if (message.stopReason === "error" && this._retryAttempt > 0) {
|
|
962
1188
|
this._emit({
|
|
963
1189
|
type: "auto_retry_end",
|
|
964
1190
|
success: false,
|
|
965
1191
|
attempt: this._retryAttempt,
|
|
966
|
-
finalError:
|
|
1192
|
+
finalError: message.errorMessage,
|
|
967
1193
|
});
|
|
968
1194
|
this._retryAttempt = 0;
|
|
969
1195
|
}
|
|
970
|
-
if (await this._checkCompaction(
|
|
1196
|
+
if (await this._checkCompaction(message, true, toolResults)) {
|
|
971
1197
|
return !this._agentRunAbortRequested;
|
|
972
1198
|
}
|
|
973
|
-
// The
|
|
974
|
-
//
|
|
1199
|
+
// The low-level loop drains both queues before agent_end. Messages queued by
|
|
1200
|
+
// agent_end handlers require a fresh run before pre-settlement handlers fire.
|
|
975
1201
|
return !this._agentRunAbortRequested && this.agent.hasQueuedMessages();
|
|
976
1202
|
}
|
|
1203
|
+
async _runBeforeSettleBoundary() {
|
|
1204
|
+
if (!this._extensionRunner.hasHandlers("agent_before_settle"))
|
|
1205
|
+
return this.agent.hasQueuedMessages();
|
|
1206
|
+
this._isBeforeSettle = true;
|
|
1207
|
+
this._abortDuringBeforeSettle = false;
|
|
1208
|
+
try {
|
|
1209
|
+
const result = await this._extensionRunner.emitBoundary({ type: "agent_before_settle", outcome: this._lastActivityOutcome }, (entries) => this._buildBoundaryContext(entries, "agent_before_settle"));
|
|
1210
|
+
this._commitBoundaryDrafts(result.entries);
|
|
1211
|
+
this._flushPendingCustomMessages();
|
|
1212
|
+
const finalContext = this._buildBoundaryContext([], "agent_before_settle");
|
|
1213
|
+
if (this._abortDuringBeforeSettle)
|
|
1214
|
+
return false;
|
|
1215
|
+
const shouldContinue = result.continue || this.agent.hasQueuedMessages();
|
|
1216
|
+
if (shouldContinue && !finalContext.canContinue) {
|
|
1217
|
+
if (result.continue)
|
|
1218
|
+
this._reportInvalidBoundaryContinuation("agent_before_settle");
|
|
1219
|
+
return false;
|
|
1220
|
+
}
|
|
1221
|
+
return shouldContinue;
|
|
1222
|
+
}
|
|
1223
|
+
finally {
|
|
1224
|
+
this._isBeforeSettle = false;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
977
1227
|
async _runInputHandlers(text, images, source, streamingBehavior) {
|
|
978
1228
|
if (!this._extensionRunner.hasHandlers("input")) {
|
|
979
1229
|
return { text, images };
|
|
@@ -987,6 +1237,25 @@ export class AgentSession {
|
|
|
987
1237
|
}
|
|
988
1238
|
return { text, images };
|
|
989
1239
|
}
|
|
1240
|
+
async _normalizePromptImages(images) {
|
|
1241
|
+
if (!images)
|
|
1242
|
+
return { images: [], hints: [] };
|
|
1243
|
+
const normalizedImages = [];
|
|
1244
|
+
const hints = [];
|
|
1245
|
+
for (const image of images) {
|
|
1246
|
+
const processed = await processImage(Buffer.from(image.data, "base64"), image.mimeType, {
|
|
1247
|
+
autoResizeImages: this.settingsManager.getImageAutoResize(),
|
|
1248
|
+
resizeOptions: this.model?.inputLimits?.images?.resize,
|
|
1249
|
+
});
|
|
1250
|
+
if (!processed.ok) {
|
|
1251
|
+
hints.push(processed.message);
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
normalizedImages.push({ type: "image", data: processed.data, mimeType: processed.mimeType });
|
|
1255
|
+
hints.push(...processed.hints);
|
|
1256
|
+
}
|
|
1257
|
+
return { images: normalizedImages, hints };
|
|
1258
|
+
}
|
|
990
1259
|
/**
|
|
991
1260
|
* Send a prompt to the agent.
|
|
992
1261
|
* - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming
|
|
@@ -997,6 +1266,10 @@ export class AgentSession {
|
|
|
997
1266
|
* @throws Error if no model selected or no API key available (when not streaming)
|
|
998
1267
|
*/
|
|
999
1268
|
async prompt(text, options) {
|
|
1269
|
+
if (this._isEmittingAgentSettled) {
|
|
1270
|
+
this._deferredSettledActions.push(async () => await this.prompt(text, options));
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1000
1273
|
// A user-initiated prompt is the recovery point for failed completion deliveries.
|
|
1001
1274
|
this.retryBackgroundNotifications();
|
|
1002
1275
|
const expandPromptTemplates = options?.expandPromptTemplates ?? true;
|
|
@@ -1073,23 +1346,8 @@ export class AgentSession {
|
|
|
1073
1346
|
if (lastAssistant) {
|
|
1074
1347
|
await this._checkCompaction(lastAssistant, false);
|
|
1075
1348
|
}
|
|
1076
|
-
//
|
|
1077
|
-
|
|
1078
|
-
// Add user message
|
|
1079
|
-
const userContent = [{ type: "text", text: expandedText }];
|
|
1080
|
-
if (currentImages) {
|
|
1081
|
-
userContent.push(...currentImages);
|
|
1082
|
-
}
|
|
1083
|
-
messages.push({
|
|
1084
|
-
role: "user",
|
|
1085
|
-
content: userContent,
|
|
1086
|
-
timestamp: Date.now(),
|
|
1087
|
-
});
|
|
1088
|
-
// Inject any pending "nextTurn" messages as context alongside the user message
|
|
1089
|
-
for (const msg of this._pendingNextTurnMessages) {
|
|
1090
|
-
messages.push(msg);
|
|
1091
|
-
}
|
|
1092
|
-
// Emit before_agent_start extension event
|
|
1349
|
+
// Emit before_agent_start before normalizing images so extension-driven model
|
|
1350
|
+
// selection determines the resize profile used for the request and history.
|
|
1093
1351
|
const selectedToolsBefore = this._baseSystemPromptOptions.selectedTools;
|
|
1094
1352
|
const result = await this._extensionRunner.emitBeforeAgentStart(expandedText, currentImages, this._baseSystemPromptOptions);
|
|
1095
1353
|
// Handlers may edit event.systemPromptOptions.selectedTools or call setActiveTools(),
|
|
@@ -1099,6 +1357,22 @@ export class AgentSession {
|
|
|
1099
1357
|
result.systemPromptOptions.selectedTools.some((name, index) => name !== selectedToolsBefore[index]);
|
|
1100
1358
|
if (!handlerEditedTools)
|
|
1101
1359
|
result.systemPromptOptions.selectedTools = this.getActiveToolNames();
|
|
1360
|
+
const normalized = await this._normalizePromptImages(currentImages);
|
|
1361
|
+
const userText = normalized.hints.length > 0 ? `${expandedText}\n\n${normalized.hints.join("\n")}` : expandedText;
|
|
1362
|
+
// Build messages only after hooks and image normalization have completed.
|
|
1363
|
+
messages = [];
|
|
1364
|
+
const userContent = [{ type: "text", text: userText }];
|
|
1365
|
+
userContent.push(...normalized.images);
|
|
1366
|
+
messages.push({
|
|
1367
|
+
role: "user",
|
|
1368
|
+
content: userContent,
|
|
1369
|
+
timestamp: Date.now(),
|
|
1370
|
+
});
|
|
1371
|
+
// Inject any pending "nextTurn" messages as context alongside the user message.
|
|
1372
|
+
// They stay queued until _persistMessage() confirms the append.
|
|
1373
|
+
for (const msg of this._pendingNextTurnMessages) {
|
|
1374
|
+
messages.push(msg);
|
|
1375
|
+
}
|
|
1102
1376
|
for (const msg of result.messages) {
|
|
1103
1377
|
messages.push({
|
|
1104
1378
|
role: "custom",
|
|
@@ -1251,11 +1525,7 @@ export class AgentSession {
|
|
|
1251
1525
|
if (images) {
|
|
1252
1526
|
content.push(...images);
|
|
1253
1527
|
}
|
|
1254
|
-
this.agent.followUp({
|
|
1255
|
-
role: "user",
|
|
1256
|
-
content,
|
|
1257
|
-
timestamp: Date.now(),
|
|
1258
|
-
});
|
|
1528
|
+
this.agent.followUp({ role: "user", content, timestamp: Date.now() });
|
|
1259
1529
|
}
|
|
1260
1530
|
/**
|
|
1261
1531
|
* Throw an error if the text is an extension command.
|
|
@@ -1303,6 +1573,10 @@ export class AgentSession {
|
|
|
1303
1573
|
}
|
|
1304
1574
|
}
|
|
1305
1575
|
else if (options?.triggerTurn) {
|
|
1576
|
+
if (this._isEmittingAgentSettled) {
|
|
1577
|
+
this._deferredSettledActions.push(async () => await this._runAgentPrompt(appMessage));
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1306
1580
|
await this._runAgentPrompt(appMessage);
|
|
1307
1581
|
}
|
|
1308
1582
|
else if (this.isStreaming) {
|
|
@@ -1317,8 +1591,8 @@ export class AgentSession {
|
|
|
1317
1591
|
}
|
|
1318
1592
|
}
|
|
1319
1593
|
_appendCustomMessage(appMessage) {
|
|
1320
|
-
this.agent.state.messages.push(appMessage);
|
|
1321
1594
|
this._persistMessage(appMessage);
|
|
1595
|
+
this._refreshFinalizedContext();
|
|
1322
1596
|
this._backgroundHost.messageAcknowledgement(appMessage)?.();
|
|
1323
1597
|
this._emit({ type: "message_start", message: appMessage });
|
|
1324
1598
|
this._emit({ type: "message_end", message: appMessage });
|
|
@@ -1412,6 +1686,8 @@ export class AgentSession {
|
|
|
1412
1686
|
this.abortRetry();
|
|
1413
1687
|
this.abortCompaction();
|
|
1414
1688
|
this.abortBranchSummary();
|
|
1689
|
+
if (this._isBeforeSettle)
|
|
1690
|
+
this._abortDuringBeforeSettle = true;
|
|
1415
1691
|
this.agent.abort();
|
|
1416
1692
|
await this.waitForIdle();
|
|
1417
1693
|
}
|
|
@@ -1732,9 +2008,8 @@ export class AgentSession {
|
|
|
1732
2008
|
}
|
|
1733
2009
|
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension, usage);
|
|
1734
2010
|
const newEntries = this.sessionManager.getEntries();
|
|
1735
|
-
|
|
1736
|
-
this.
|
|
1737
|
-
const estimatedTokensAfter = estimateMessagesTokens(sessionContext.messages);
|
|
2011
|
+
this._refreshFinalizedContext();
|
|
2012
|
+
const estimatedTokensAfter = estimateMessagesTokens(this.sessionManager.buildSessionProjection().messages);
|
|
1738
2013
|
// Get the saved compaction entry for the extension event
|
|
1739
2014
|
const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
|
|
1740
2015
|
if (this._extensionRunner && savedCompactionEntry) {
|
|
@@ -1829,7 +2104,7 @@ export class AgentSession {
|
|
|
1829
2104
|
* @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
|
|
1830
2105
|
* @returns Whether the post-run loop should call `agent.continue()` for overflow recovery or queued messages
|
|
1831
2106
|
*/
|
|
1832
|
-
async _checkCompaction(assistantMessage, skipAbortedCheck = true) {
|
|
2107
|
+
async _checkCompaction(assistantMessage, skipAbortedCheck = true, toolResults = []) {
|
|
1833
2108
|
const settings = this.settingsManager.getCompactionSettings(this.model);
|
|
1834
2109
|
if (!settings.enabled)
|
|
1835
2110
|
return false;
|
|
@@ -1853,8 +2128,27 @@ export class AgentSession {
|
|
|
1853
2128
|
// Automatic cases 1 and 2: context overflow.
|
|
1854
2129
|
// A length stop is recoverable when output ended below the model's original desired limit,
|
|
1855
2130
|
// independent of the configured context size or any context-clamped provider request limit.
|
|
1856
|
-
const
|
|
1857
|
-
const
|
|
2131
|
+
const currentProjection = this.sessionManager.buildSessionProjection();
|
|
2132
|
+
const assistantEntryId = this._findPersistedMessageEntryId(assistantMessage);
|
|
2133
|
+
const assistantIsProjected = assistantEntryId === undefined ||
|
|
2134
|
+
currentProjection.entries.some((entry) => entry.sourceEntry.id === assistantEntryId &&
|
|
2135
|
+
entry.messages.some((message) => message.role === "assistant"));
|
|
2136
|
+
const branch = this.sessionManager.getBranch();
|
|
2137
|
+
const assistantIndex = assistantEntryId ? branch.findIndex((entry) => entry.id === assistantEntryId) : -1;
|
|
2138
|
+
const entriesAfterAssistant = assistantIndex >= 0 ? branch.slice(assistantIndex + 1) : [];
|
|
2139
|
+
const hasPostAssistantContextEdit = entriesAfterAssistant.some((entry) => entry.type === "context_edit");
|
|
2140
|
+
const latestAssistantEdit = entriesAfterAssistant
|
|
2141
|
+
.filter((entry) => entry.type === "context_edit" && entry.targetId === assistantEntryId)
|
|
2142
|
+
.at(-1);
|
|
2143
|
+
const assistantRetainedForExplicitRecovery = assistantEntryId === undefined ||
|
|
2144
|
+
(!entriesAfterAssistant.some((entry) => entry.type === "compaction") &&
|
|
2145
|
+
latestAssistantEdit?.replacement !== null);
|
|
2146
|
+
const assistantUsageMatchesProjection = assistantIsProjected && !hasPostAssistantContextEdit;
|
|
2147
|
+
const explicitOverflow = assistantMessage.stopReason === "error" && isContextOverflow(assistantMessage);
|
|
2148
|
+
const contextOverflow = sameModel &&
|
|
2149
|
+
((explicitOverflow && assistantRetainedForExplicitRecovery) ||
|
|
2150
|
+
(assistantUsageMatchesProjection && isContextOverflow(assistantMessage, contextWindow)));
|
|
2151
|
+
const recoverableLength = sameModel && assistantIsProjected && isRecoverableLength(assistantMessage, this.model?.maxTokens ?? 0);
|
|
1858
2152
|
if (contextOverflow || recoverableLength) {
|
|
1859
2153
|
const willRetry = assistantMessage.stopReason !== "stop";
|
|
1860
2154
|
// Case 2: the response completed successfully. Compact, but do not retry because
|
|
@@ -1883,13 +2177,9 @@ export class AgentSession {
|
|
|
1883
2177
|
});
|
|
1884
2178
|
return false;
|
|
1885
2179
|
}
|
|
1886
|
-
//
|
|
1887
|
-
// retry once. The message remains in session history but is excluded from retry context.
|
|
2180
|
+
// Persistently omit the selected final attempt before post-run recovery compaction.
|
|
1888
2181
|
this._overflowRecoveryAttempted = true;
|
|
1889
|
-
|
|
1890
|
-
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
1891
|
-
this.agent.state.messages = messages.slice(0, -1);
|
|
1892
|
-
}
|
|
2182
|
+
this._omitRecoveryAttempt(assistantMessage, toolResults);
|
|
1893
2183
|
return await this._runAutoCompaction("overflow", willRetry);
|
|
1894
2184
|
}
|
|
1895
2185
|
// Case 3: threshold compaction without retry.
|
|
@@ -1897,8 +2187,13 @@ export class AgentSession {
|
|
|
1897
2187
|
// This ensures sessions that hit persistent API errors (e.g. 529) or malformed zero-usage
|
|
1898
2188
|
// responses can still compact and do not reset context accounting.
|
|
1899
2189
|
let contextTokens;
|
|
2190
|
+
const projection = currentProjection;
|
|
2191
|
+
const hasContextEdits = projection.entries.some((entry) => entry.sourceEntry.type === "context_edit");
|
|
1900
2192
|
const directContextTokens = assistantMessage.usage ? calculateContextTokens(assistantMessage.usage) : 0;
|
|
1901
|
-
if (
|
|
2193
|
+
if (hasContextEdits) {
|
|
2194
|
+
contextTokens = estimateProjectedContextTokens(projection, branch).tokens;
|
|
2195
|
+
}
|
|
2196
|
+
else if (assistantMessage.stopReason === "error" || directContextTokens === 0) {
|
|
1902
2197
|
const messages = this.agent.state.messages;
|
|
1903
2198
|
const estimate = estimateContextTokens(messages);
|
|
1904
2199
|
// Without provider usage, estimate.tokens is the pure message-size estimate.
|
|
@@ -2005,9 +2300,8 @@ export class AgentSession {
|
|
|
2005
2300
|
abortController.signal.throwIfAborted();
|
|
2006
2301
|
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension, usage);
|
|
2007
2302
|
const newEntries = this.sessionManager.getEntries();
|
|
2008
|
-
|
|
2009
|
-
this.
|
|
2010
|
-
const estimatedTokensAfter = estimateMessagesTokens(sessionContext.messages);
|
|
2303
|
+
this._refreshFinalizedContext();
|
|
2304
|
+
const estimatedTokensAfter = estimateMessagesTokens(this.sessionManager.buildSessionProjection().messages);
|
|
2011
2305
|
// Get the saved compaction entry for the extension event
|
|
2012
2306
|
const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
|
|
2013
2307
|
if (this._extensionRunner && savedCompactionEntry) {
|
|
@@ -2028,19 +2322,8 @@ export class AgentSession {
|
|
|
2028
2322
|
details,
|
|
2029
2323
|
};
|
|
2030
2324
|
this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry });
|
|
2031
|
-
if (willRetry)
|
|
2032
|
-
const messages = this.agent.state.messages;
|
|
2033
|
-
const lastMsg = messages[messages.length - 1];
|
|
2034
|
-
// The overflow response was persisted on message_end before _checkCompaction() removed it
|
|
2035
|
-
// from agent state. Rebuilding state from the new compaction can restore that kept entry,
|
|
2036
|
-
// leaving an assistant as the final message. agent.continue() rejects that state, so remove
|
|
2037
|
-
// the retriable error or truncated-length response again before continuing the interrupted turn.
|
|
2038
|
-
if (lastMsg?.role === "assistant" &&
|
|
2039
|
-
(lastMsg.stopReason === "error" || lastMsg.stopReason === "length")) {
|
|
2040
|
-
this.agent.state.messages = messages.slice(0, -1);
|
|
2041
|
-
}
|
|
2325
|
+
if (willRetry)
|
|
2042
2326
|
return true;
|
|
2043
|
-
}
|
|
2044
2327
|
// Auto-compaction can complete while follow-up/steering/custom messages are waiting.
|
|
2045
2328
|
// Continue once so queued messages are delivered.
|
|
2046
2329
|
return this.agent.hasQueuedMessages();
|
|
@@ -2524,11 +2807,8 @@ export class AgentSession {
|
|
|
2524
2807
|
delayMs,
|
|
2525
2808
|
errorMessage: message.errorMessage || "Unknown error",
|
|
2526
2809
|
});
|
|
2527
|
-
//
|
|
2528
|
-
|
|
2529
|
-
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
2530
|
-
this.agent.state.messages = messages.slice(0, -1);
|
|
2531
|
-
}
|
|
2810
|
+
// Keep the failed attempt in raw history while durably omitting it from model projection.
|
|
2811
|
+
this._omitRecoveryAttempt(message);
|
|
2532
2812
|
// Wait with exponential backoff (abortable)
|
|
2533
2813
|
this._retryAbortController = new AbortController();
|
|
2534
2814
|
try {
|
|
@@ -2620,10 +2900,8 @@ export class AgentSession {
|
|
|
2620
2900
|
this._pendingBashMessages.push(bashMessage);
|
|
2621
2901
|
}
|
|
2622
2902
|
else {
|
|
2623
|
-
// Add to agent state immediately
|
|
2624
|
-
this.agent.state.messages.push(bashMessage);
|
|
2625
|
-
// Save to session
|
|
2626
2903
|
this.sessionManager.appendMessage(bashMessage);
|
|
2904
|
+
this._refreshFinalizedContext();
|
|
2627
2905
|
}
|
|
2628
2906
|
}
|
|
2629
2907
|
/**
|
|
@@ -2650,12 +2928,10 @@ export class AgentSession {
|
|
|
2650
2928
|
if (this._pendingBashMessages.length === 0)
|
|
2651
2929
|
return;
|
|
2652
2930
|
for (const bashMessage of this._pendingBashMessages) {
|
|
2653
|
-
// Add to agent state
|
|
2654
|
-
this.agent.state.messages.push(bashMessage);
|
|
2655
|
-
// Save to session
|
|
2656
2931
|
this.sessionManager.appendMessage(bashMessage);
|
|
2657
2932
|
}
|
|
2658
2933
|
this._pendingBashMessages = [];
|
|
2934
|
+
this._refreshFinalizedContext();
|
|
2659
2935
|
}
|
|
2660
2936
|
// =========================================================================
|
|
2661
2937
|
// Session Management
|
|
@@ -2832,9 +3108,8 @@ export class AgentSession {
|
|
|
2832
3108
|
this.sessionManager.appendLabelChange(targetId, label);
|
|
2833
3109
|
}
|
|
2834
3110
|
this._backgroundHost.restoreHistory();
|
|
2835
|
-
// Update
|
|
2836
|
-
|
|
2837
|
-
this.agent.state.messages = sessionContext.messages;
|
|
3111
|
+
// Update finalized context from the canonical session projection.
|
|
3112
|
+
this._refreshFinalizedContext();
|
|
2838
3113
|
this._restoreToolsFromTranscript();
|
|
2839
3114
|
// Emit session_tree event
|
|
2840
3115
|
await this._extensionRunner.emit({
|
|
@@ -2934,30 +3209,24 @@ export class AgentSession {
|
|
|
2934
3209
|
// After compaction, the last assistant usage reflects pre-compaction context size.
|
|
2935
3210
|
// We can only trust usage from an assistant that responded after the latest compaction.
|
|
2936
3211
|
// If no such assistant exists, context token count is unknown until the next LLM response.
|
|
2937
|
-
const
|
|
2938
|
-
const
|
|
3212
|
+
const projection = this.sessionManager.buildSessionProjection();
|
|
3213
|
+
const branch = this.sessionManager.getBranch();
|
|
3214
|
+
const latestCompaction = getLatestCompactionEntry(branch);
|
|
2939
3215
|
if (latestCompaction) {
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
break;
|
|
2952
|
-
}
|
|
2953
|
-
}
|
|
2954
|
-
}
|
|
2955
|
-
}
|
|
2956
|
-
if (!hasPostCompactionUsage) {
|
|
3216
|
+
const projectedAssistants = new Set(projection.entries.flatMap((entry) => entry.messages.some((message) => message.role === "assistant" &&
|
|
3217
|
+
message.stopReason !== "aborted" &&
|
|
3218
|
+
message.stopReason !== "error" &&
|
|
3219
|
+
calculateContextTokens(message.usage) > 0)
|
|
3220
|
+
? [entry.sourceEntry.id]
|
|
3221
|
+
: []));
|
|
3222
|
+
const compactionIndex = branch.findIndex((entry) => entry.id === latestCompaction.id);
|
|
3223
|
+
const hasPostCompactionUsage = branch
|
|
3224
|
+
.slice(compactionIndex + 1)
|
|
3225
|
+
.some((entry) => projectedAssistants.has(entry.id));
|
|
3226
|
+
if (!hasPostCompactionUsage)
|
|
2957
3227
|
return { tokens: null, contextWindow, percent: null };
|
|
2958
|
-
}
|
|
2959
3228
|
}
|
|
2960
|
-
const estimate =
|
|
3229
|
+
const estimate = estimateProjectedContextTokens(projection, branch);
|
|
2961
3230
|
const percent = (estimate.tokens / contextWindow) * 100;
|
|
2962
3231
|
return {
|
|
2963
3232
|
tokens: estimate.tokens,
|