@earendil-works/pi-coding-agent 0.86.1 → 0.87.1
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 +62 -0
- package/README.md +25 -675
- package/dist/bundle/chunks/{anthropic-messages-MYU5ZMRF.js → anthropic-messages-J5WXPPPC.js} +1 -1
- package/dist/bundle/chunks/chunk-65HAU2C5.js +2 -0
- package/dist/bundle/chunks/{chunk-CMRUVXTE.js → chunk-OJP47DM6.js} +48 -42
- package/dist/bundle/chunks/github-copilot.js +1 -1
- package/dist/bundle/chunks/{openai-completions-CYGM3XXP.js → openai-completions-OBX42CLD.js} +2 -2
- package/dist/bundle/chunks/{virtual-modules-MGTKWDID.js → virtual-modules-VHMJYYWQ.js} +1 -1
- package/dist/bundle/cli-runtime.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 +14 -4
- 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.d.ts.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 +27 -3
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +389 -119
- 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 +155 -57
- 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 -1
- 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/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.map +1 -1
- package/dist/modes/interactive/bug-report.js +4 -0
- package/dist/modes/interactive/bug-report.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 +3 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +64 -2
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/mime.d.ts.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/docs/cli-integration.md +106 -0
- package/docs/cli.md +268 -0
- package/docs/compaction.md +45 -26
- package/docs/configuration.md +45 -0
- package/docs/containerization.md +109 -82
- package/docs/custom-provider.md +132 -784
- package/docs/docs.json +139 -99
- package/docs/environment-variables.md +3 -5
- package/docs/extensions.md +134 -2956
- package/docs/how-pi-works.md +49 -0
- package/docs/images/interactive-mode.png +0 -0
- package/docs/index.md +24 -69
- package/docs/json.md +193 -65
- package/docs/keybindings.md +57 -102
- package/docs/llama-cpp.md +3 -3
- package/docs/message-types.md +261 -0
- package/docs/models.md +64 -546
- package/docs/packages.md +66 -167
- package/docs/prompt-templates.md +31 -68
- package/docs/providers.md +102 -240
- package/docs/quickstart.md +61 -106
- package/docs/rpc-commands.md +854 -0
- package/docs/rpc-extension-ui.md +200 -0
- package/docs/rpc.md +129 -1556
- package/docs/sdk.md +76 -1160
- package/docs/security.md +70 -32
- package/docs/session-format.md +25 -216
- package/docs/sessions.md +35 -141
- package/docs/settings.md +109 -387
- package/docs/shell-aliases.md +85 -5
- package/docs/skills.md +51 -190
- package/docs/slash-commands.md +60 -0
- package/docs/terminal-setup.md +105 -78
- package/docs/termux.md +74 -83
- package/docs/themes.md +68 -280
- package/docs/tmux.md +31 -39
- package/docs/tui.md +69 -923
- package/docs/usage.md +54 -272
- package/docs/windows.md +43 -17
- package/examples/README.md +13 -2
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/plugins/pi-example-plugin/src/session.ts +3 -2
- package/examples/rpc-client.ts +35 -0
- package/examples/rpc-extension-ui.ts +25 -5
- package/examples/sdk/README.md +1 -1
- package/npm-shrinkwrap.json +20 -20
- package/package.json +8 -8
- package/dist/bundle/chunks/chunk-HTEQD2HM.js +0 -2
- package/docs/development.md +0 -90
|
@@ -18,21 +18,23 @@ import { contentText, getCurrentSystemMessage, retryDelayMs } from "@earendil-wo
|
|
|
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 { executeBashWithOperations } from "./bash-executor.js";
|
|
25
26
|
import { generateBugReportSummary } from "./bug-report.js";
|
|
26
|
-
import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
|
|
27
|
+
import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, estimateProjectedContextTokens, estimateTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
|
|
27
28
|
import { DEFAULT_THINKING_LEVEL, THINKING_LEVEL_OPTIONS } from "./defaults.js";
|
|
28
29
|
import { exportSessionToHtml } from "./export-html/index.js";
|
|
29
30
|
import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
|
|
30
31
|
import { ExtensionRunner, wrapRegisteredTools, } from "./extensions/index.js";
|
|
31
32
|
import { emitSessionShutdownEvent } from "./extensions/runner.js";
|
|
33
|
+
import { convertToLlm } from "./messages.js";
|
|
32
34
|
import { ModelRegistry } from "./model-registry.js";
|
|
33
35
|
import { expandPromptTemplate } from "./prompt-templates.js";
|
|
34
36
|
import { exportSessionToJsonl } from "./session-export.js";
|
|
35
|
-
import { getLatestCompactionEntry } from "./session-manager.js";
|
|
37
|
+
import { getLatestCompactionEntry, SessionManager, } from "./session-manager.js";
|
|
36
38
|
import { createSyntheticSourceInfo } from "./source-info.js";
|
|
37
39
|
import { buildSystemPrompt, buildSystemPromptSections, diffSystemPromptSections, normalizeBuildSystemPromptOptions, } from "./system-prompt.js";
|
|
38
40
|
import { createLocalBashOperations } from "./tools/bash.js";
|
|
@@ -107,6 +109,15 @@ export class AgentSession {
|
|
|
107
109
|
// Extension system
|
|
108
110
|
_extensionRunner;
|
|
109
111
|
_turnIndex = 0;
|
|
112
|
+
_entryIdsByMessage = new WeakMap();
|
|
113
|
+
_boundaryDispatchedMessages = new WeakSet();
|
|
114
|
+
_lastAssistantMessage;
|
|
115
|
+
_lastAssistantToolResults = [];
|
|
116
|
+
_lastActivityOutcome = "completed";
|
|
117
|
+
_isBeforeSettle = false;
|
|
118
|
+
_abortDuringBeforeSettle = false;
|
|
119
|
+
_isEmittingAgentSettled = false;
|
|
120
|
+
_deferredSettledActions = [];
|
|
110
121
|
_resourceLoader;
|
|
111
122
|
_customTools;
|
|
112
123
|
_baseToolDefinitions = new Map();
|
|
@@ -158,6 +169,8 @@ export class AgentSession {
|
|
|
158
169
|
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
|
159
170
|
this._installAgentToolHooks();
|
|
160
171
|
this._installAgentNextTurnRefresh();
|
|
172
|
+
this._installAgentRequestProjection();
|
|
173
|
+
this._installAgentBoundaryHooks();
|
|
161
174
|
this._installAgentForcedPromptProjection();
|
|
162
175
|
this._buildRuntime({
|
|
163
176
|
activeToolNames: this._initialActiveToolNames,
|
|
@@ -265,8 +278,10 @@ export class AgentSession {
|
|
|
265
278
|
: undefined;
|
|
266
279
|
const content = hookResult?.content ?? result.content ?? [];
|
|
267
280
|
// Runs after the extension hook so images injected or replaced by extensions are normalized too.
|
|
281
|
+
const resizeOptions = this.model?.inputLimits?.images?.resize;
|
|
268
282
|
const normalizedContent = await normalizeToolResultImages(content, {
|
|
269
283
|
autoResizeImages: this.settingsManager.getImageAutoResize(),
|
|
284
|
+
...(resizeOptions ? { resizeOptions } : {}),
|
|
270
285
|
});
|
|
271
286
|
if (!hookResult && normalizedContent === content) {
|
|
272
287
|
return undefined;
|
|
@@ -282,15 +297,83 @@ export class AgentSession {
|
|
|
282
297
|
async _compactBeforeNextAssistantResponse(context) {
|
|
283
298
|
const model = this.model;
|
|
284
299
|
const settings = this.settingsManager.getCompactionSettings(model);
|
|
300
|
+
const projection = this.sessionManager.buildSessionProjection();
|
|
285
301
|
if (!model ||
|
|
286
302
|
model.contextWindow <= 0 ||
|
|
287
|
-
!shouldCompact(
|
|
288
|
-
return context;
|
|
303
|
+
!shouldCompact(estimateProjectedContextTokens(projection, this.sessionManager.getBranch()).tokens, model.contextWindow, settings)) {
|
|
304
|
+
return { ...context, messages: projection.messages };
|
|
289
305
|
}
|
|
290
306
|
await this._runAutoCompaction("threshold", false);
|
|
291
|
-
return {
|
|
292
|
-
|
|
293
|
-
|
|
307
|
+
return { ...context, messages: this.sessionManager.buildSessionProjection().messages };
|
|
308
|
+
}
|
|
309
|
+
_installAgentRequestProjection() {
|
|
310
|
+
const previousPrepareRequest = this.agent.prepareRequest;
|
|
311
|
+
this.agent.prepareRequest = async (request, signal) => {
|
|
312
|
+
const canonicalContext = {
|
|
313
|
+
...request.context,
|
|
314
|
+
messages: this.sessionManager.buildSessionProjection().messages,
|
|
315
|
+
// Messages declare the provider-visible loadout; context.tools keeps executable implementations.
|
|
316
|
+
tools: this.agent.state.tools.slice(),
|
|
317
|
+
};
|
|
318
|
+
const previous = await previousPrepareRequest?.({
|
|
319
|
+
...request,
|
|
320
|
+
context: canonicalContext,
|
|
321
|
+
model: this.agent.state.model,
|
|
322
|
+
thinkingLevel: this.agent.state.thinkingLevel,
|
|
323
|
+
}, signal);
|
|
324
|
+
return {
|
|
325
|
+
...previous,
|
|
326
|
+
context: previous?.context ?? canonicalContext,
|
|
327
|
+
model: previous?.model ?? this.agent.state.model,
|
|
328
|
+
thinkingLevel: previous?.thinkingLevel ?? this.agent.state.thinkingLevel,
|
|
329
|
+
};
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
async _dispatchTurnEndBoundary(message, toolResults) {
|
|
333
|
+
this._lastActivityOutcome =
|
|
334
|
+
message.stopReason === "aborted" ? "aborted" : message.stopReason === "error" ? "error" : "completed";
|
|
335
|
+
const messageEntryId = this._findPersistedMessageEntryId(message);
|
|
336
|
+
if (!this._extensionRunner.hasHandlers("turn_end"))
|
|
337
|
+
return false;
|
|
338
|
+
if (!messageEntryId) {
|
|
339
|
+
this._extensionRunner.emitError({
|
|
340
|
+
extensionPath: "<boundary>",
|
|
341
|
+
event: "turn_end",
|
|
342
|
+
error: "turn_end could not resolve the persisted assistant entry ID",
|
|
343
|
+
});
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
const toolResultEntryIds = toolResults.flatMap((result) => {
|
|
347
|
+
const entryId = this._findPersistedMessageEntryId(result);
|
|
348
|
+
return entryId ? [entryId] : [];
|
|
349
|
+
});
|
|
350
|
+
const boundary = await this._extensionRunner.emitBoundary({
|
|
351
|
+
type: "turn_end",
|
|
352
|
+
turnIndex: this._turnIndex,
|
|
353
|
+
message,
|
|
354
|
+
toolResults,
|
|
355
|
+
messageEntryId,
|
|
356
|
+
toolResultEntryIds,
|
|
357
|
+
outcome: this._lastActivityOutcome,
|
|
358
|
+
}, (entries) => this._buildBoundaryContext(entries, "turn_end"));
|
|
359
|
+
this._commitBoundaryDrafts(boundary.entries);
|
|
360
|
+
if (boundary.continue && !this._buildBoundaryContext([], "turn_end").canContinue) {
|
|
361
|
+
this._reportInvalidBoundaryContinuation("turn_end");
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
return boundary.continue;
|
|
365
|
+
}
|
|
366
|
+
_installAgentBoundaryHooks() {
|
|
367
|
+
const previousFinishTurn = this.agent.finishTurn;
|
|
368
|
+
this.agent.finishTurn = async (turn, signal) => {
|
|
369
|
+
this._boundaryDispatchedMessages.add(turn.message);
|
|
370
|
+
const extensionContinue = await this._dispatchTurnEndBoundary(turn.message, turn.toolResults);
|
|
371
|
+
const previousDecision = await previousFinishTurn?.(turn, signal);
|
|
372
|
+
if (previousDecision?.action === "end")
|
|
373
|
+
return previousDecision;
|
|
374
|
+
if (extensionContinue || previousDecision?.action === "continue")
|
|
375
|
+
return { action: "continue" };
|
|
376
|
+
return undefined;
|
|
294
377
|
};
|
|
295
378
|
}
|
|
296
379
|
_installAgentNextTurnRefresh() {
|
|
@@ -299,7 +382,10 @@ export class AgentSession {
|
|
|
299
382
|
? async (_turn, signal) => await this.agent.prepareNextTurn?.(signal)
|
|
300
383
|
: undefined);
|
|
301
384
|
this.agent.prepareNextTurnWithContext = async (turn, signal) => {
|
|
302
|
-
const context = await this._compactBeforeNextAssistantResponse(
|
|
385
|
+
const context = await this._compactBeforeNextAssistantResponse({
|
|
386
|
+
...turn.context,
|
|
387
|
+
messages: this.sessionManager.buildSessionProjection().messages,
|
|
388
|
+
});
|
|
303
389
|
const previousSnapshot = await previousPrepareNextTurnWithContext?.({ ...turn, context }, signal);
|
|
304
390
|
const nextContext = previousSnapshot?.context ?? context;
|
|
305
391
|
const runOptions = this._runSystemPromptOptions ?? this._baseSystemPromptOptions;
|
|
@@ -329,6 +415,84 @@ export class AgentSession {
|
|
|
329
415
|
// =========================================================================
|
|
330
416
|
// Event Subscription
|
|
331
417
|
// =========================================================================
|
|
418
|
+
_refreshFinalizedContext() {
|
|
419
|
+
const projection = this.sessionManager.buildSessionProjection();
|
|
420
|
+
for (const entry of projection.entries) {
|
|
421
|
+
for (const message of entry.messages)
|
|
422
|
+
this._entryIdsByMessage.set(message, entry.sourceEntry.id);
|
|
423
|
+
}
|
|
424
|
+
this.agent.state.messages = projection.messages;
|
|
425
|
+
}
|
|
426
|
+
_applyBoundaryDrafts(manager, drafts) {
|
|
427
|
+
const appended = [];
|
|
428
|
+
for (const draft of drafts) {
|
|
429
|
+
let entryId;
|
|
430
|
+
switch (draft.type) {
|
|
431
|
+
case "custom":
|
|
432
|
+
entryId = manager.appendCustomEntry(draft.customType, draft.data);
|
|
433
|
+
break;
|
|
434
|
+
case "custom_message":
|
|
435
|
+
entryId = manager.appendCustomMessageEntry(draft.customType, draft.content, draft.display, draft.details);
|
|
436
|
+
break;
|
|
437
|
+
case "context_edit":
|
|
438
|
+
entryId = manager.appendContextEdit(draft.targetId, draft.replacement);
|
|
439
|
+
break;
|
|
440
|
+
case "compaction": {
|
|
441
|
+
const tokensBefore = estimateProjectedContextTokens(manager.buildSessionProjection(), manager.getBranch()).tokens;
|
|
442
|
+
entryId = manager.appendCompaction(draft.summary, draft.firstKeptEntryId, tokensBefore, draft.details, true, draft.usage);
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const entry = manager.getEntry(entryId);
|
|
447
|
+
if (entry)
|
|
448
|
+
appended.push(entry);
|
|
449
|
+
}
|
|
450
|
+
return appended;
|
|
451
|
+
}
|
|
452
|
+
_createBoundaryPreviewManager(drafts) {
|
|
453
|
+
const header = this.sessionManager.getHeader();
|
|
454
|
+
if (!header)
|
|
455
|
+
throw new Error("Session header is missing");
|
|
456
|
+
const manager = SessionManager.inMemory(this._cwd, undefined, [header, ...this.sessionManager.getBranch()]);
|
|
457
|
+
this._applyBoundaryDrafts(manager, drafts);
|
|
458
|
+
return manager;
|
|
459
|
+
}
|
|
460
|
+
_getPendingBoundaryMessages() {
|
|
461
|
+
return [...this.agent.peekQueuedMessages(), ...this._pendingCustomMessages];
|
|
462
|
+
}
|
|
463
|
+
_buildBoundaryContext(drafts, boundary) {
|
|
464
|
+
const projection = this._createBoundaryPreviewManager(drafts).buildSessionProjection();
|
|
465
|
+
const pendingMessages = this._getPendingBoundaryMessages();
|
|
466
|
+
const llmMessages = convertToLlm(projection.messages);
|
|
467
|
+
const finalRole = llmMessages[llmMessages.length - 1]?.role;
|
|
468
|
+
const hasNonSystemContext = llmMessages.some((message) => message.role !== "system");
|
|
469
|
+
const contextCanContinue = hasNonSystemContext && finalRole !== "assistant";
|
|
470
|
+
const pendingCustomContext = this._pendingCustomMessages.length > 0;
|
|
471
|
+
return {
|
|
472
|
+
contextEntries: projection.entries,
|
|
473
|
+
contextMessages: projection.messages,
|
|
474
|
+
llmMessages,
|
|
475
|
+
pendingMessages,
|
|
476
|
+
canContinue: contextCanContinue ||
|
|
477
|
+
pendingCustomContext ||
|
|
478
|
+
(boundary === "turn_end"
|
|
479
|
+
? this.agent.hasQueuedMessages()
|
|
480
|
+
: finalRole === "assistant" && this.agent.hasQueuedMessages()),
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
_commitBoundaryDrafts(drafts) {
|
|
484
|
+
const appended = this._applyBoundaryDrafts(this.sessionManager, drafts);
|
|
485
|
+
this._refreshFinalizedContext();
|
|
486
|
+
for (const entry of appended)
|
|
487
|
+
this._emit({ type: "entry_appended", entry });
|
|
488
|
+
}
|
|
489
|
+
_reportInvalidBoundaryContinuation(event) {
|
|
490
|
+
this._extensionRunner.emitError({
|
|
491
|
+
extensionPath: "<boundary>",
|
|
492
|
+
event,
|
|
493
|
+
error: `${event} requested continuation without runnable model context`,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
332
496
|
/** Emit an event to all listeners */
|
|
333
497
|
_emit(event) {
|
|
334
498
|
for (const l of this._eventListeners) {
|
|
@@ -367,16 +531,27 @@ export class AgentSession {
|
|
|
367
531
|
async _emitAgentSettled() {
|
|
368
532
|
this._cacheWarmer?.onAgentSettled();
|
|
369
533
|
this._isAgentRunActive = false;
|
|
534
|
+
this._isEmittingAgentSettled = true;
|
|
370
535
|
try {
|
|
371
536
|
await this._extensionRunner.emit({ type: "agent_settled" });
|
|
372
537
|
this._emit({ type: "agent_settled" });
|
|
373
538
|
}
|
|
374
539
|
finally {
|
|
375
|
-
this.
|
|
540
|
+
this._isEmittingAgentSettled = false;
|
|
541
|
+
}
|
|
542
|
+
const deferred = this._deferredSettledActions.splice(0);
|
|
543
|
+
if (deferred.length > 0) {
|
|
544
|
+
try {
|
|
545
|
+
for (const action of deferred)
|
|
546
|
+
await action();
|
|
547
|
+
}
|
|
548
|
+
finally {
|
|
549
|
+
this._resolveIdleWaitIfIdle();
|
|
550
|
+
}
|
|
551
|
+
return;
|
|
376
552
|
}
|
|
553
|
+
this._resolveIdleWaitIfIdle();
|
|
377
554
|
}
|
|
378
|
-
// Track last assistant message for auto-compaction check
|
|
379
|
-
_lastAssistantMessage = undefined;
|
|
380
555
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
381
556
|
_handleAgentEvent = async (event) => {
|
|
382
557
|
// When a user message starts, check if it's from either queue and remove it BEFORE emitting
|
|
@@ -401,29 +576,30 @@ export class AgentSession {
|
|
|
401
576
|
}
|
|
402
577
|
}
|
|
403
578
|
}
|
|
404
|
-
// Emit to extensions first
|
|
579
|
+
// Emit to extensions first, then notify public listeners.
|
|
405
580
|
await this._emitExtensionEvent(event);
|
|
406
|
-
// Notify all listeners
|
|
407
581
|
this._emit(event.type === "agent_end" ? { ...event, willRetry: this._willRetryAfterAgentEnd(event) } : event);
|
|
408
582
|
// Handle session persistence
|
|
409
583
|
if (event.type === "message_end") {
|
|
584
|
+
let entryId;
|
|
410
585
|
// Check if this is a custom message from extensions
|
|
411
586
|
if (event.message.role === "custom") {
|
|
412
587
|
// Persist as CustomMessageEntry
|
|
413
|
-
this.sessionManager.appendCustomMessageEntry(event.message.customType, event.message.content, event.message.display, event.message.details);
|
|
588
|
+
entryId = this.sessionManager.appendCustomMessageEntry(event.message.customType, event.message.content, event.message.display, event.message.details);
|
|
414
589
|
}
|
|
415
590
|
else if (event.message.role === "system" ||
|
|
416
591
|
event.message.role === "user" ||
|
|
417
592
|
event.message.role === "assistant" ||
|
|
418
593
|
event.message.role === "toolResult") {
|
|
419
594
|
// Regular LLM message - persist as SessionMessageEntry
|
|
420
|
-
this.sessionManager.appendMessage(event.message);
|
|
595
|
+
entryId = this.sessionManager.appendMessage(event.message);
|
|
421
596
|
}
|
|
597
|
+
if (entryId)
|
|
598
|
+
this._entryIdsByMessage.set(event.message, entryId);
|
|
422
599
|
// Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
|
|
423
|
-
// Track assistant message for auto-compaction (checked on agent_end)
|
|
424
600
|
if (event.message.role === "assistant") {
|
|
425
|
-
this._lastAssistantMessage = event.message;
|
|
426
601
|
const assistantMsg = event.message;
|
|
602
|
+
this._lastAssistantMessage = assistantMsg;
|
|
427
603
|
if (assistantMsg.stopReason !== "error" && assistantMsg.stopReason !== "length") {
|
|
428
604
|
this._overflowRecoveryAttempted = false;
|
|
429
605
|
}
|
|
@@ -445,6 +621,7 @@ export class AgentSession {
|
|
|
445
621
|
// extension and listener dispatch above also picks up messages that turn_end
|
|
446
622
|
// handlers queued.
|
|
447
623
|
if (event.type === "turn_end") {
|
|
624
|
+
this._lastAssistantToolResults = event.toolResults;
|
|
448
625
|
this._flushPendingCustomMessages();
|
|
449
626
|
}
|
|
450
627
|
};
|
|
@@ -463,6 +640,47 @@ export class AgentSession {
|
|
|
463
640
|
}
|
|
464
641
|
return false;
|
|
465
642
|
}
|
|
643
|
+
_findPersistedMessageEntryId(message) {
|
|
644
|
+
const mapped = this._entryIdsByMessage.get(message);
|
|
645
|
+
if (mapped)
|
|
646
|
+
return mapped;
|
|
647
|
+
for (const entry of [...this.sessionManager.getBranch()].reverse()) {
|
|
648
|
+
if (entry.type === "message" && entry.message === message)
|
|
649
|
+
return entry.id;
|
|
650
|
+
}
|
|
651
|
+
const messageIndex = this.agent.state.messages.indexOf(message);
|
|
652
|
+
if (messageIndex < 0)
|
|
653
|
+
return undefined;
|
|
654
|
+
const projection = this.sessionManager.buildSessionProjection();
|
|
655
|
+
let projectedIndex = 0;
|
|
656
|
+
for (const entry of projection.entries) {
|
|
657
|
+
for (let i = 0; i < entry.messages.length; i++) {
|
|
658
|
+
if (projectedIndex === messageIndex) {
|
|
659
|
+
this._entryIdsByMessage.set(message, entry.sourceEntry.id);
|
|
660
|
+
return entry.sourceEntry.id;
|
|
661
|
+
}
|
|
662
|
+
projectedIndex++;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return undefined;
|
|
666
|
+
}
|
|
667
|
+
_omitRecoveryAttempt(message, toolResults = []) {
|
|
668
|
+
const targets = [message, ...toolResults];
|
|
669
|
+
const targetIds = targets.map((target) => this._findPersistedMessageEntryId(target));
|
|
670
|
+
const unresolvedProjectedTarget = targets.some((target, index) => targetIds[index] === undefined && this.agent.state.messages.includes(target));
|
|
671
|
+
if (unresolvedProjectedTarget) {
|
|
672
|
+
throw new Error("Cannot persist recovery omission because a projected message has no source entry");
|
|
673
|
+
}
|
|
674
|
+
for (const targetId of targetIds) {
|
|
675
|
+
if (!targetId)
|
|
676
|
+
continue;
|
|
677
|
+
const editId = this.sessionManager.appendContextEdit(targetId, null);
|
|
678
|
+
const entry = this.sessionManager.getEntry(editId);
|
|
679
|
+
if (entry)
|
|
680
|
+
this._emit({ type: "entry_appended", entry });
|
|
681
|
+
}
|
|
682
|
+
this._refreshFinalizedContext();
|
|
683
|
+
}
|
|
466
684
|
/** Find the last assistant message in agent state (including aborted ones) */
|
|
467
685
|
_findLastAssistantMessage() {
|
|
468
686
|
const messages = this.agent.state.messages;
|
|
@@ -506,13 +724,9 @@ export class AgentSession {
|
|
|
506
724
|
await this._extensionRunner.emit(extensionEvent);
|
|
507
725
|
}
|
|
508
726
|
else if (event.type === "turn_end") {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
message: event.message,
|
|
513
|
-
toolResults: event.toolResults,
|
|
514
|
-
};
|
|
515
|
-
await this._extensionRunner.emit(extensionEvent);
|
|
727
|
+
if (event.message.role === "assistant" && !this._boundaryDispatchedMessages.delete(event.message)) {
|
|
728
|
+
await this._dispatchTurnEndBoundary(event.message, event.toolResults);
|
|
729
|
+
}
|
|
516
730
|
this._turnIndex++;
|
|
517
731
|
}
|
|
518
732
|
else if (event.type === "message_start") {
|
|
@@ -628,6 +842,10 @@ export class AgentSession {
|
|
|
628
842
|
// =========================================================================
|
|
629
843
|
// Read-only State Access
|
|
630
844
|
// =========================================================================
|
|
845
|
+
/** Refresh the public finalized transcript from the canonical session projection. */
|
|
846
|
+
refreshContext() {
|
|
847
|
+
this._refreshFinalizedContext();
|
|
848
|
+
}
|
|
631
849
|
/** Full agent state */
|
|
632
850
|
get state() {
|
|
633
851
|
return this.agent.state;
|
|
@@ -862,7 +1080,15 @@ export class AgentSession {
|
|
|
862
1080
|
this._isAgentRunActive = true;
|
|
863
1081
|
try {
|
|
864
1082
|
await this.agent.prompt(messages);
|
|
865
|
-
while (
|
|
1083
|
+
while (!this._agentRunAbortRequested) {
|
|
1084
|
+
if (await this._handlePostAgentRun()) {
|
|
1085
|
+
if (this._agentRunAbortRequested)
|
|
1086
|
+
break;
|
|
1087
|
+
await this.agent.continue();
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
if (this._agentRunAbortRequested || !(await this._runBeforeSettleBoundary()))
|
|
1091
|
+
break;
|
|
866
1092
|
if (this._agentRunAbortRequested)
|
|
867
1093
|
break;
|
|
868
1094
|
await this.agent.continue();
|
|
@@ -878,16 +1104,17 @@ export class AgentSession {
|
|
|
878
1104
|
}
|
|
879
1105
|
}
|
|
880
1106
|
async _handlePostAgentRun() {
|
|
881
|
-
const
|
|
1107
|
+
const message = this._lastAssistantMessage;
|
|
1108
|
+
const toolResults = this._lastAssistantToolResults;
|
|
882
1109
|
this._lastAssistantMessage = undefined;
|
|
1110
|
+
this._lastAssistantToolResults = [];
|
|
883
1111
|
if (this._agentRunAbortRequested) {
|
|
884
1112
|
this._finishCancelledRetry();
|
|
885
1113
|
return false;
|
|
886
1114
|
}
|
|
887
|
-
if (!
|
|
888
|
-
return
|
|
889
|
-
|
|
890
|
-
if (this._isRetryableError(msg) && (await this._prepareRetry(msg))) {
|
|
1115
|
+
if (!message)
|
|
1116
|
+
return this.agent.hasQueuedMessages();
|
|
1117
|
+
if (this._isRetryableError(message) && (await this._prepareRetry(message))) {
|
|
891
1118
|
if (this._agentRunAbortRequested)
|
|
892
1119
|
this._finishCancelledRetry();
|
|
893
1120
|
return !this._agentRunAbortRequested;
|
|
@@ -896,22 +1123,46 @@ export class AgentSession {
|
|
|
896
1123
|
this._finishCancelledRetry();
|
|
897
1124
|
return false;
|
|
898
1125
|
}
|
|
899
|
-
if (
|
|
1126
|
+
if (message.stopReason === "error" && this._retryAttempt > 0) {
|
|
900
1127
|
this._emit({
|
|
901
1128
|
type: "auto_retry_end",
|
|
902
1129
|
success: false,
|
|
903
1130
|
attempt: this._retryAttempt,
|
|
904
|
-
finalError:
|
|
1131
|
+
finalError: message.errorMessage,
|
|
905
1132
|
});
|
|
906
1133
|
this._retryAttempt = 0;
|
|
907
1134
|
}
|
|
908
|
-
if (await this._checkCompaction(
|
|
1135
|
+
if (await this._checkCompaction(message, true, toolResults)) {
|
|
909
1136
|
return !this._agentRunAbortRequested;
|
|
910
1137
|
}
|
|
911
|
-
// The
|
|
912
|
-
//
|
|
1138
|
+
// The low-level loop drains both queues before agent_end. Messages queued by
|
|
1139
|
+
// agent_end handlers require a fresh run before pre-settlement handlers fire.
|
|
913
1140
|
return !this._agentRunAbortRequested && this.agent.hasQueuedMessages();
|
|
914
1141
|
}
|
|
1142
|
+
async _runBeforeSettleBoundary() {
|
|
1143
|
+
if (!this._extensionRunner.hasHandlers("agent_before_settle"))
|
|
1144
|
+
return this.agent.hasQueuedMessages();
|
|
1145
|
+
this._isBeforeSettle = true;
|
|
1146
|
+
this._abortDuringBeforeSettle = false;
|
|
1147
|
+
try {
|
|
1148
|
+
const result = await this._extensionRunner.emitBoundary({ type: "agent_before_settle", outcome: this._lastActivityOutcome }, (entries) => this._buildBoundaryContext(entries, "agent_before_settle"));
|
|
1149
|
+
this._commitBoundaryDrafts(result.entries);
|
|
1150
|
+
this._flushPendingCustomMessages();
|
|
1151
|
+
const finalContext = this._buildBoundaryContext([], "agent_before_settle");
|
|
1152
|
+
if (this._abortDuringBeforeSettle)
|
|
1153
|
+
return false;
|
|
1154
|
+
const shouldContinue = result.continue || this.agent.hasQueuedMessages();
|
|
1155
|
+
if (shouldContinue && !finalContext.canContinue) {
|
|
1156
|
+
if (result.continue)
|
|
1157
|
+
this._reportInvalidBoundaryContinuation("agent_before_settle");
|
|
1158
|
+
return false;
|
|
1159
|
+
}
|
|
1160
|
+
return shouldContinue;
|
|
1161
|
+
}
|
|
1162
|
+
finally {
|
|
1163
|
+
this._isBeforeSettle = false;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
915
1166
|
async _runInputHandlers(text, images, source, streamingBehavior) {
|
|
916
1167
|
if (!this._extensionRunner.hasHandlers("input")) {
|
|
917
1168
|
return { text, images };
|
|
@@ -925,6 +1176,25 @@ export class AgentSession {
|
|
|
925
1176
|
}
|
|
926
1177
|
return { text, images };
|
|
927
1178
|
}
|
|
1179
|
+
async _normalizePromptImages(images) {
|
|
1180
|
+
if (!images)
|
|
1181
|
+
return { images: [], hints: [] };
|
|
1182
|
+
const normalizedImages = [];
|
|
1183
|
+
const hints = [];
|
|
1184
|
+
for (const image of images) {
|
|
1185
|
+
const processed = await processImage(Buffer.from(image.data, "base64"), image.mimeType, {
|
|
1186
|
+
autoResizeImages: this.settingsManager.getImageAutoResize(),
|
|
1187
|
+
resizeOptions: this.model?.inputLimits?.images?.resize,
|
|
1188
|
+
});
|
|
1189
|
+
if (!processed.ok) {
|
|
1190
|
+
hints.push(processed.message);
|
|
1191
|
+
continue;
|
|
1192
|
+
}
|
|
1193
|
+
normalizedImages.push({ type: "image", data: processed.data, mimeType: processed.mimeType });
|
|
1194
|
+
hints.push(...processed.hints);
|
|
1195
|
+
}
|
|
1196
|
+
return { images: normalizedImages, hints };
|
|
1197
|
+
}
|
|
928
1198
|
/**
|
|
929
1199
|
* Send a prompt to the agent.
|
|
930
1200
|
* - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming
|
|
@@ -935,6 +1205,10 @@ export class AgentSession {
|
|
|
935
1205
|
* @throws Error if no model selected or no API key available (when not streaming)
|
|
936
1206
|
*/
|
|
937
1207
|
async prompt(text, options) {
|
|
1208
|
+
if (this._isEmittingAgentSettled) {
|
|
1209
|
+
this._deferredSettledActions.push(async () => await this.prompt(text, options));
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
938
1212
|
const expandPromptTemplates = options?.expandPromptTemplates ?? true;
|
|
939
1213
|
const preflightResult = options?.preflightResult;
|
|
940
1214
|
let messages;
|
|
@@ -1003,13 +1277,23 @@ export class AgentSession {
|
|
|
1003
1277
|
if (lastAssistant) {
|
|
1004
1278
|
await this._checkCompaction(lastAssistant, false);
|
|
1005
1279
|
}
|
|
1006
|
-
//
|
|
1280
|
+
// Emit before_agent_start before normalizing images so extension-driven model
|
|
1281
|
+
// selection determines the resize profile used for the request and history.
|
|
1282
|
+
const selectedToolsBefore = this._baseSystemPromptOptions.selectedTools;
|
|
1283
|
+
const result = await this._extensionRunner.emitBeforeAgentStart(expandedText, currentImages, this._baseSystemPromptOptions);
|
|
1284
|
+
// Handlers may edit event.systemPromptOptions.selectedTools or call setActiveTools(),
|
|
1285
|
+
// which updates the live loadout instead. An explicit edit wins; otherwise the live
|
|
1286
|
+
// loadout is authoritative, so a setActiveTools() call is not undone here.
|
|
1287
|
+
const handlerEditedTools = result.systemPromptOptions.selectedTools.length !== selectedToolsBefore.length ||
|
|
1288
|
+
result.systemPromptOptions.selectedTools.some((name, index) => name !== selectedToolsBefore[index]);
|
|
1289
|
+
if (!handlerEditedTools)
|
|
1290
|
+
result.systemPromptOptions.selectedTools = this.getActiveToolNames();
|
|
1291
|
+
const normalized = await this._normalizePromptImages(currentImages);
|
|
1292
|
+
const userText = normalized.hints.length > 0 ? `${expandedText}\n\n${normalized.hints.join("\n")}` : expandedText;
|
|
1293
|
+
// Build messages only after hooks and image normalization have completed.
|
|
1007
1294
|
messages = [];
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
if (currentImages) {
|
|
1011
|
-
userContent.push(...currentImages);
|
|
1012
|
-
}
|
|
1295
|
+
const userContent = [{ type: "text", text: userText }];
|
|
1296
|
+
userContent.push(...normalized.images);
|
|
1013
1297
|
messages.push({
|
|
1014
1298
|
role: "user",
|
|
1015
1299
|
content: userContent,
|
|
@@ -1020,16 +1304,6 @@ export class AgentSession {
|
|
|
1020
1304
|
messages.push(msg);
|
|
1021
1305
|
}
|
|
1022
1306
|
this._pendingNextTurnMessages = [];
|
|
1023
|
-
// Emit before_agent_start extension event
|
|
1024
|
-
const selectedToolsBefore = this._baseSystemPromptOptions.selectedTools;
|
|
1025
|
-
const result = await this._extensionRunner.emitBeforeAgentStart(expandedText, currentImages, this._baseSystemPromptOptions);
|
|
1026
|
-
// Handlers may edit event.systemPromptOptions.selectedTools or call setActiveTools(),
|
|
1027
|
-
// which updates the live loadout instead. An explicit edit wins; otherwise the live
|
|
1028
|
-
// loadout is authoritative, so a setActiveTools() call is not undone here.
|
|
1029
|
-
const handlerEditedTools = result.systemPromptOptions.selectedTools.length !== selectedToolsBefore.length ||
|
|
1030
|
-
result.systemPromptOptions.selectedTools.some((name, index) => name !== selectedToolsBefore[index]);
|
|
1031
|
-
if (!handlerEditedTools)
|
|
1032
|
-
result.systemPromptOptions.selectedTools = this.getActiveToolNames();
|
|
1033
1307
|
for (const msg of result.messages) {
|
|
1034
1308
|
messages.push({
|
|
1035
1309
|
role: "custom",
|
|
@@ -1178,11 +1452,7 @@ export class AgentSession {
|
|
|
1178
1452
|
if (images) {
|
|
1179
1453
|
content.push(...images);
|
|
1180
1454
|
}
|
|
1181
|
-
this.agent.followUp({
|
|
1182
|
-
role: "user",
|
|
1183
|
-
content,
|
|
1184
|
-
timestamp: Date.now(),
|
|
1185
|
-
});
|
|
1455
|
+
this.agent.followUp({ role: "user", content, timestamp: Date.now() });
|
|
1186
1456
|
}
|
|
1187
1457
|
/**
|
|
1188
1458
|
* Throw an error if the text is an extension command.
|
|
@@ -1230,6 +1500,10 @@ export class AgentSession {
|
|
|
1230
1500
|
}
|
|
1231
1501
|
}
|
|
1232
1502
|
else if (options?.triggerTurn) {
|
|
1503
|
+
if (this._isEmittingAgentSettled) {
|
|
1504
|
+
this._deferredSettledActions.push(async () => await this._runAgentPrompt(appMessage));
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1233
1507
|
await this._runAgentPrompt(appMessage);
|
|
1234
1508
|
}
|
|
1235
1509
|
else if (this.isStreaming) {
|
|
@@ -1244,8 +1518,8 @@ export class AgentSession {
|
|
|
1244
1518
|
}
|
|
1245
1519
|
}
|
|
1246
1520
|
_appendCustomMessage(appMessage) {
|
|
1247
|
-
this.agent.state.messages.push(appMessage);
|
|
1248
1521
|
this.sessionManager.appendCustomMessageEntry(appMessage.customType, appMessage.content, appMessage.display, appMessage.details);
|
|
1522
|
+
this._refreshFinalizedContext();
|
|
1249
1523
|
this._emit({ type: "message_start", message: appMessage });
|
|
1250
1524
|
this._emit({ type: "message_end", message: appMessage });
|
|
1251
1525
|
}
|
|
@@ -1338,6 +1612,8 @@ export class AgentSession {
|
|
|
1338
1612
|
this.abortRetry();
|
|
1339
1613
|
this.abortCompaction();
|
|
1340
1614
|
this.abortBranchSummary();
|
|
1615
|
+
if (this._isBeforeSettle)
|
|
1616
|
+
this._abortDuringBeforeSettle = true;
|
|
1341
1617
|
this.agent.abort();
|
|
1342
1618
|
await this.waitForIdle();
|
|
1343
1619
|
}
|
|
@@ -1656,9 +1932,8 @@ export class AgentSession {
|
|
|
1656
1932
|
}
|
|
1657
1933
|
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension, usage);
|
|
1658
1934
|
const newEntries = this.sessionManager.getEntries();
|
|
1659
|
-
|
|
1660
|
-
this.
|
|
1661
|
-
const estimatedTokensAfter = estimateMessagesTokens(sessionContext.messages);
|
|
1935
|
+
this._refreshFinalizedContext();
|
|
1936
|
+
const estimatedTokensAfter = estimateMessagesTokens(this.sessionManager.buildSessionProjection().messages);
|
|
1662
1937
|
// Get the saved compaction entry for the extension event
|
|
1663
1938
|
const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
|
|
1664
1939
|
if (this._extensionRunner && savedCompactionEntry) {
|
|
@@ -1749,7 +2024,7 @@ export class AgentSession {
|
|
|
1749
2024
|
* @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
|
|
1750
2025
|
* @returns Whether the post-run loop should call `agent.continue()` for overflow recovery or queued messages
|
|
1751
2026
|
*/
|
|
1752
|
-
async _checkCompaction(assistantMessage, skipAbortedCheck = true) {
|
|
2027
|
+
async _checkCompaction(assistantMessage, skipAbortedCheck = true, toolResults = []) {
|
|
1753
2028
|
const settings = this.settingsManager.getCompactionSettings(this.model);
|
|
1754
2029
|
if (!settings.enabled)
|
|
1755
2030
|
return false;
|
|
@@ -1773,8 +2048,27 @@ export class AgentSession {
|
|
|
1773
2048
|
// Automatic cases 1 and 2: context overflow.
|
|
1774
2049
|
// A length stop is recoverable when output ended below the model's original desired limit,
|
|
1775
2050
|
// independent of the configured context size or any context-clamped provider request limit.
|
|
1776
|
-
const
|
|
1777
|
-
const
|
|
2051
|
+
const currentProjection = this.sessionManager.buildSessionProjection();
|
|
2052
|
+
const assistantEntryId = this._findPersistedMessageEntryId(assistantMessage);
|
|
2053
|
+
const assistantIsProjected = assistantEntryId === undefined ||
|
|
2054
|
+
currentProjection.entries.some((entry) => entry.sourceEntry.id === assistantEntryId &&
|
|
2055
|
+
entry.messages.some((message) => message.role === "assistant"));
|
|
2056
|
+
const branch = this.sessionManager.getBranch();
|
|
2057
|
+
const assistantIndex = assistantEntryId ? branch.findIndex((entry) => entry.id === assistantEntryId) : -1;
|
|
2058
|
+
const entriesAfterAssistant = assistantIndex >= 0 ? branch.slice(assistantIndex + 1) : [];
|
|
2059
|
+
const hasPostAssistantContextEdit = entriesAfterAssistant.some((entry) => entry.type === "context_edit");
|
|
2060
|
+
const latestAssistantEdit = entriesAfterAssistant
|
|
2061
|
+
.filter((entry) => entry.type === "context_edit" && entry.targetId === assistantEntryId)
|
|
2062
|
+
.at(-1);
|
|
2063
|
+
const assistantRetainedForExplicitRecovery = assistantEntryId === undefined ||
|
|
2064
|
+
(!entriesAfterAssistant.some((entry) => entry.type === "compaction") &&
|
|
2065
|
+
latestAssistantEdit?.replacement !== null);
|
|
2066
|
+
const assistantUsageMatchesProjection = assistantIsProjected && !hasPostAssistantContextEdit;
|
|
2067
|
+
const explicitOverflow = assistantMessage.stopReason === "error" && isContextOverflow(assistantMessage);
|
|
2068
|
+
const contextOverflow = sameModel &&
|
|
2069
|
+
((explicitOverflow && assistantRetainedForExplicitRecovery) ||
|
|
2070
|
+
(assistantUsageMatchesProjection && isContextOverflow(assistantMessage, contextWindow)));
|
|
2071
|
+
const recoverableLength = sameModel && assistantIsProjected && isRecoverableLength(assistantMessage, this.model?.maxTokens ?? 0);
|
|
1778
2072
|
if (contextOverflow || recoverableLength) {
|
|
1779
2073
|
const willRetry = assistantMessage.stopReason !== "stop";
|
|
1780
2074
|
// Case 2: the response completed successfully. Compact, but do not retry because
|
|
@@ -1803,13 +2097,9 @@ export class AgentSession {
|
|
|
1803
2097
|
});
|
|
1804
2098
|
return false;
|
|
1805
2099
|
}
|
|
1806
|
-
//
|
|
1807
|
-
// retry once. The message remains in session history but is excluded from retry context.
|
|
2100
|
+
// Persistently omit the selected final attempt before post-run recovery compaction.
|
|
1808
2101
|
this._overflowRecoveryAttempted = true;
|
|
1809
|
-
|
|
1810
|
-
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
1811
|
-
this.agent.state.messages = messages.slice(0, -1);
|
|
1812
|
-
}
|
|
2102
|
+
this._omitRecoveryAttempt(assistantMessage, toolResults);
|
|
1813
2103
|
return await this._runAutoCompaction("overflow", willRetry);
|
|
1814
2104
|
}
|
|
1815
2105
|
// Case 3: threshold compaction without retry.
|
|
@@ -1817,8 +2107,13 @@ export class AgentSession {
|
|
|
1817
2107
|
// This ensures sessions that hit persistent API errors (e.g. 529) or malformed zero-usage
|
|
1818
2108
|
// responses can still compact and do not reset context accounting.
|
|
1819
2109
|
let contextTokens;
|
|
2110
|
+
const projection = currentProjection;
|
|
2111
|
+
const hasContextEdits = projection.entries.some((entry) => entry.sourceEntry.type === "context_edit");
|
|
1820
2112
|
const directContextTokens = assistantMessage.usage ? calculateContextTokens(assistantMessage.usage) : 0;
|
|
1821
|
-
if (
|
|
2113
|
+
if (hasContextEdits) {
|
|
2114
|
+
contextTokens = estimateProjectedContextTokens(projection, branch).tokens;
|
|
2115
|
+
}
|
|
2116
|
+
else if (assistantMessage.stopReason === "error" || directContextTokens === 0) {
|
|
1822
2117
|
const messages = this.agent.state.messages;
|
|
1823
2118
|
const estimate = estimateContextTokens(messages);
|
|
1824
2119
|
// Without provider usage, estimate.tokens is the pure message-size estimate.
|
|
@@ -1923,9 +2218,8 @@ export class AgentSession {
|
|
|
1923
2218
|
abortController.signal.throwIfAborted();
|
|
1924
2219
|
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension, usage);
|
|
1925
2220
|
const newEntries = this.sessionManager.getEntries();
|
|
1926
|
-
|
|
1927
|
-
this.
|
|
1928
|
-
const estimatedTokensAfter = estimateMessagesTokens(sessionContext.messages);
|
|
2221
|
+
this._refreshFinalizedContext();
|
|
2222
|
+
const estimatedTokensAfter = estimateMessagesTokens(this.sessionManager.buildSessionProjection().messages);
|
|
1929
2223
|
// Get the saved compaction entry for the extension event
|
|
1930
2224
|
const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
|
|
1931
2225
|
if (this._extensionRunner && savedCompactionEntry) {
|
|
@@ -1946,18 +2240,8 @@ export class AgentSession {
|
|
|
1946
2240
|
details,
|
|
1947
2241
|
};
|
|
1948
2242
|
this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry });
|
|
1949
|
-
if (willRetry)
|
|
1950
|
-
const messages = this.agent.state.messages;
|
|
1951
|
-
const lastMsg = messages[messages.length - 1];
|
|
1952
|
-
// The overflow response was persisted on message_end before _checkCompaction() removed it
|
|
1953
|
-
// from agent state. Rebuilding state from the new compaction can restore that kept entry,
|
|
1954
|
-
// leaving an assistant as the final message. agent.continue() rejects that state, so remove
|
|
1955
|
-
// the retriable error or truncated-length response again before continuing the interrupted turn.
|
|
1956
|
-
if (lastMsg?.role === "assistant" && (lastMsg.stopReason === "error" || lastMsg.stopReason === "length")) {
|
|
1957
|
-
this.agent.state.messages = messages.slice(0, -1);
|
|
1958
|
-
}
|
|
2243
|
+
if (willRetry)
|
|
1959
2244
|
return true;
|
|
1960
|
-
}
|
|
1961
2245
|
// Auto-compaction can complete while follow-up/steering/custom messages are waiting.
|
|
1962
2246
|
// Continue once so queued messages are delivered.
|
|
1963
2247
|
return this.agent.hasQueuedMessages();
|
|
@@ -2416,11 +2700,8 @@ export class AgentSession {
|
|
|
2416
2700
|
delayMs,
|
|
2417
2701
|
errorMessage: message.errorMessage || "Unknown error",
|
|
2418
2702
|
});
|
|
2419
|
-
//
|
|
2420
|
-
|
|
2421
|
-
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
2422
|
-
this.agent.state.messages = messages.slice(0, -1);
|
|
2423
|
-
}
|
|
2703
|
+
// Keep the failed attempt in raw history while durably omitting it from model projection.
|
|
2704
|
+
this._omitRecoveryAttempt(message);
|
|
2424
2705
|
// Wait with exponential backoff (abortable)
|
|
2425
2706
|
this._retryAbortController = new AbortController();
|
|
2426
2707
|
try {
|
|
@@ -2512,10 +2793,8 @@ export class AgentSession {
|
|
|
2512
2793
|
this._pendingBashMessages.push(bashMessage);
|
|
2513
2794
|
}
|
|
2514
2795
|
else {
|
|
2515
|
-
// Add to agent state immediately
|
|
2516
|
-
this.agent.state.messages.push(bashMessage);
|
|
2517
|
-
// Save to session
|
|
2518
2796
|
this.sessionManager.appendMessage(bashMessage);
|
|
2797
|
+
this._refreshFinalizedContext();
|
|
2519
2798
|
}
|
|
2520
2799
|
}
|
|
2521
2800
|
/**
|
|
@@ -2542,12 +2821,10 @@ export class AgentSession {
|
|
|
2542
2821
|
if (this._pendingBashMessages.length === 0)
|
|
2543
2822
|
return;
|
|
2544
2823
|
for (const bashMessage of this._pendingBashMessages) {
|
|
2545
|
-
// Add to agent state
|
|
2546
|
-
this.agent.state.messages.push(bashMessage);
|
|
2547
|
-
// Save to session
|
|
2548
2824
|
this.sessionManager.appendMessage(bashMessage);
|
|
2549
2825
|
}
|
|
2550
2826
|
this._pendingBashMessages = [];
|
|
2827
|
+
this._refreshFinalizedContext();
|
|
2551
2828
|
}
|
|
2552
2829
|
// =========================================================================
|
|
2553
2830
|
// Session Management
|
|
@@ -2721,9 +2998,8 @@ export class AgentSession {
|
|
|
2721
2998
|
if (label && !summaryText) {
|
|
2722
2999
|
this.sessionManager.appendLabelChange(targetId, label);
|
|
2723
3000
|
}
|
|
2724
|
-
// Update
|
|
2725
|
-
|
|
2726
|
-
this.agent.state.messages = sessionContext.messages;
|
|
3001
|
+
// Update finalized context from the canonical session projection.
|
|
3002
|
+
this._refreshFinalizedContext();
|
|
2727
3003
|
this._restoreToolsFromTranscript();
|
|
2728
3004
|
// Emit session_tree event
|
|
2729
3005
|
await this._extensionRunner.emit({
|
|
@@ -2829,30 +3105,24 @@ export class AgentSession {
|
|
|
2829
3105
|
// After compaction, the last assistant usage reflects pre-compaction context size.
|
|
2830
3106
|
// We can only trust usage from an assistant that responded after the latest compaction.
|
|
2831
3107
|
// If no such assistant exists, context token count is unknown until the next LLM response.
|
|
2832
|
-
const
|
|
2833
|
-
const
|
|
3108
|
+
const projection = this.sessionManager.buildSessionProjection();
|
|
3109
|
+
const branch = this.sessionManager.getBranch();
|
|
3110
|
+
const latestCompaction = getLatestCompactionEntry(branch);
|
|
2834
3111
|
if (latestCompaction) {
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
break;
|
|
2847
|
-
}
|
|
2848
|
-
}
|
|
2849
|
-
}
|
|
2850
|
-
}
|
|
2851
|
-
if (!hasPostCompactionUsage) {
|
|
3112
|
+
const projectedAssistants = new Set(projection.entries.flatMap((entry) => entry.messages.some((message) => message.role === "assistant" &&
|
|
3113
|
+
message.stopReason !== "aborted" &&
|
|
3114
|
+
message.stopReason !== "error" &&
|
|
3115
|
+
calculateContextTokens(message.usage) > 0)
|
|
3116
|
+
? [entry.sourceEntry.id]
|
|
3117
|
+
: []));
|
|
3118
|
+
const compactionIndex = branch.findIndex((entry) => entry.id === latestCompaction.id);
|
|
3119
|
+
const hasPostCompactionUsage = branch
|
|
3120
|
+
.slice(compactionIndex + 1)
|
|
3121
|
+
.some((entry) => projectedAssistants.has(entry.id));
|
|
3122
|
+
if (!hasPostCompactionUsage)
|
|
2852
3123
|
return { tokens: null, contextWindow, percent: null };
|
|
2853
|
-
}
|
|
2854
3124
|
}
|
|
2855
|
-
const estimate =
|
|
3125
|
+
const estimate = estimateProjectedContextTokens(projection, branch);
|
|
2856
3126
|
const percent = (estimate.tokens / contextWindow) * 100;
|
|
2857
3127
|
return {
|
|
2858
3128
|
tokens: estimate.tokens,
|