@averyyy/pi-coding-agent 0.86.1-piclient.1 → 0.87.0-piclient.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 +40 -0
- package/README.md +36 -0
- package/dist/bundle/chunks/{chunk-K53F3E6X.js → chunk-3OMQWEMP.js} +44 -38
- package/dist/bundle/chunks/chunk-CJ5EZ24X.js +2 -0
- package/dist/bundle/chunks/devin-runtime.js +7 -2
- package/dist/bundle/chunks/github-copilot.js +1 -1
- package/dist/bundle/chunks/{openai-completions-CYGM3XXP.js → openai-completions-XHML6MTL.js} +1 -1
- package/dist/bundle/chunks/{virtual-modules-TAAJ5574.js → virtual-modules-N7I2ZIFC.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/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 +28 -3
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +482 -145
- 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/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 +2 -2
- 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/compaction.md +23 -4
- package/docs/extensions.md +70 -6
- package/docs/models.md +29 -1
- package/docs/sdk.md +13 -2
- package/docs/session-format.md +15 -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/package.json +8 -8
- package/dist/bundle/chunks/chunk-WF5BMKD5.js +0 -2
|
@@ -18,22 +18,24 @@ import { contentText, getCurrentSystemMessage, normalizeContext, retryDelayMs }
|
|
|
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 { resolvePath } from "../utils/paths.js";
|
|
22
23
|
import { sleep } from "../utils/sleep.js";
|
|
23
24
|
import { normalizeToolResultImages } from "../utils/tool-result-images.js";
|
|
24
25
|
import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.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, 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 { cancelPiServerOperations, compactPiServer, } from "./pi-server-client.js";
|
|
35
37
|
import { expandPromptTemplate } from "./prompt-templates.js";
|
|
36
|
-
import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-manager.js";
|
|
38
|
+
import { CURRENT_SESSION_VERSION, getLatestCompactionEntry, SessionManager, } from "./session-manager.js";
|
|
37
39
|
import { createSyntheticSourceInfo } from "./source-info.js";
|
|
38
40
|
import { buildSystemPrompt, buildSystemPromptSections, diffSystemPromptSections, normalizeBuildSystemPromptOptions, } from "./system-prompt.js";
|
|
39
41
|
import { createLocalBashOperations } from "./tools/bash.js";
|
|
@@ -208,6 +210,15 @@ export class AgentSession {
|
|
|
208
210
|
// Extension system
|
|
209
211
|
_extensionRunner;
|
|
210
212
|
_turnIndex = 0;
|
|
213
|
+
_entryIdsByMessage = new WeakMap();
|
|
214
|
+
_boundaryDispatchedMessages = new WeakSet();
|
|
215
|
+
_lastAssistantMessage;
|
|
216
|
+
_lastAssistantToolResults = [];
|
|
217
|
+
_lastActivityOutcome = "completed";
|
|
218
|
+
_isBeforeSettle = false;
|
|
219
|
+
_abortDuringBeforeSettle = false;
|
|
220
|
+
_isEmittingAgentSettled = false;
|
|
221
|
+
_deferredSettledActions = [];
|
|
211
222
|
_resourceLoader;
|
|
212
223
|
_customTools;
|
|
213
224
|
_baseToolDefinitions = new Map();
|
|
@@ -265,6 +276,8 @@ export class AgentSession {
|
|
|
265
276
|
this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
|
|
266
277
|
this._installAgentToolHooks();
|
|
267
278
|
this._installAgentNextTurnRefresh();
|
|
279
|
+
this._installAgentRequestProjection();
|
|
280
|
+
this._installAgentBoundaryHooks();
|
|
268
281
|
this._installAgentForcedPromptProjection();
|
|
269
282
|
this._buildRuntime({
|
|
270
283
|
activeToolNames: this._initialActiveToolNames,
|
|
@@ -382,8 +395,10 @@ export class AgentSession {
|
|
|
382
395
|
: undefined;
|
|
383
396
|
const content = hookResult?.content ?? result.content ?? [];
|
|
384
397
|
// Runs after the extension hook so images injected or replaced by extensions are normalized too.
|
|
398
|
+
const resizeOptions = this.model?.inputLimits?.images?.resize;
|
|
385
399
|
const normalizedContent = await normalizeToolResultImages(content, {
|
|
386
400
|
autoResizeImages: this.settingsManager.getImageAutoResize(),
|
|
401
|
+
...(resizeOptions ? { resizeOptions } : {}),
|
|
387
402
|
});
|
|
388
403
|
if (!hookResult && normalizedContent === content) {
|
|
389
404
|
return undefined;
|
|
@@ -396,16 +411,112 @@ export class AgentSession {
|
|
|
396
411
|
};
|
|
397
412
|
};
|
|
398
413
|
}
|
|
414
|
+
async _compactBeforeNextAssistantResponse(context, signal) {
|
|
415
|
+
if (signal?.aborted)
|
|
416
|
+
return context;
|
|
417
|
+
const model = this.model;
|
|
418
|
+
const settings = this.settingsManager.getCompactionSettings(model);
|
|
419
|
+
const projection = this.sessionManager.buildSessionProjection();
|
|
420
|
+
if (!model ||
|
|
421
|
+
model.contextWindow <= 0 ||
|
|
422
|
+
!shouldCompact(estimateProjectedContextTokens(projection, this.sessionManager.getBranch()).tokens, model.contextWindow, settings)) {
|
|
423
|
+
return { ...context, messages: projection.messages };
|
|
424
|
+
}
|
|
425
|
+
await this._runAutoCompaction("threshold", false, undefined, undefined, signal);
|
|
426
|
+
return { ...context, messages: this.sessionManager.buildSessionProjection().messages };
|
|
427
|
+
}
|
|
428
|
+
_installAgentRequestProjection() {
|
|
429
|
+
const previousPrepareRequest = this.agent.prepareRequest;
|
|
430
|
+
this.agent.prepareRequest = async (request, signal) => {
|
|
431
|
+
const canonicalContext = {
|
|
432
|
+
...request.context,
|
|
433
|
+
messages: this.sessionManager.buildSessionProjection().messages,
|
|
434
|
+
// Messages declare the provider-visible loadout; context.tools keeps executable implementations.
|
|
435
|
+
tools: this.agent.state.tools.slice(),
|
|
436
|
+
};
|
|
437
|
+
const previous = await previousPrepareRequest?.({
|
|
438
|
+
...request,
|
|
439
|
+
context: canonicalContext,
|
|
440
|
+
model: this.agent.state.model,
|
|
441
|
+
thinkingLevel: this.agent.state.thinkingLevel,
|
|
442
|
+
}, signal);
|
|
443
|
+
return {
|
|
444
|
+
...previous,
|
|
445
|
+
context: previous?.context ?? canonicalContext,
|
|
446
|
+
model: previous?.model ?? this.agent.state.model,
|
|
447
|
+
thinkingLevel: previous?.thinkingLevel ?? this.agent.state.thinkingLevel,
|
|
448
|
+
};
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
async _dispatchTurnEndBoundary(message, toolResults) {
|
|
452
|
+
this._lastActivityOutcome =
|
|
453
|
+
message.stopReason === "aborted" ? "aborted" : message.stopReason === "error" ? "error" : "completed";
|
|
454
|
+
const messageEntryId = this._findPersistedMessageEntryId(message);
|
|
455
|
+
if (!this._extensionRunner.hasHandlers("turn_end"))
|
|
456
|
+
return false;
|
|
457
|
+
if (!messageEntryId) {
|
|
458
|
+
this._extensionRunner.emitError({
|
|
459
|
+
extensionPath: "<boundary>",
|
|
460
|
+
event: "turn_end",
|
|
461
|
+
error: "turn_end could not resolve the persisted assistant entry ID",
|
|
462
|
+
});
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
const toolResultEntryIds = toolResults.flatMap((result) => {
|
|
466
|
+
const entryId = this._findPersistedMessageEntryId(result);
|
|
467
|
+
return entryId ? [entryId] : [];
|
|
468
|
+
});
|
|
469
|
+
const boundary = await this._extensionRunner.emitBoundary({
|
|
470
|
+
type: "turn_end",
|
|
471
|
+
turnIndex: this._turnIndex,
|
|
472
|
+
message,
|
|
473
|
+
toolResults,
|
|
474
|
+
messageEntryId,
|
|
475
|
+
toolResultEntryIds,
|
|
476
|
+
outcome: this._lastActivityOutcome,
|
|
477
|
+
}, (entries) => this._buildBoundaryContext(entries, "turn_end"));
|
|
478
|
+
this._commitBoundaryDrafts(boundary.entries);
|
|
479
|
+
if (boundary.continue && !this._buildBoundaryContext([], "turn_end").canContinue) {
|
|
480
|
+
this._reportInvalidBoundaryContinuation("turn_end");
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
return boundary.continue;
|
|
484
|
+
}
|
|
485
|
+
_installAgentBoundaryHooks() {
|
|
486
|
+
const previousFinishTurn = this.agent.finishTurn;
|
|
487
|
+
this.agent.finishTurn = async (turn, signal) => {
|
|
488
|
+
this._boundaryDispatchedMessages.add(turn.message);
|
|
489
|
+
const extensionContinue = await this._dispatchTurnEndBoundary(turn.message, turn.toolResults);
|
|
490
|
+
const previousDecision = await previousFinishTurn?.(turn, signal);
|
|
491
|
+
if (previousDecision?.action === "end")
|
|
492
|
+
return previousDecision;
|
|
493
|
+
if (extensionContinue || previousDecision?.action === "continue")
|
|
494
|
+
return { action: "continue" };
|
|
495
|
+
return undefined;
|
|
496
|
+
};
|
|
497
|
+
}
|
|
399
498
|
_installAgentNextTurnRefresh() {
|
|
400
499
|
const previousPrepareNextTurnWithContext = this.agent.prepareNextTurnWithContext ??
|
|
401
500
|
(this.agent.prepareNextTurn
|
|
402
501
|
? async (_turn, signal) => await this.agent.prepareNextTurn?.(signal)
|
|
403
502
|
: undefined);
|
|
404
503
|
this.agent.prepareNextTurnWithContext = async (turn, signal) => {
|
|
405
|
-
const
|
|
406
|
-
|
|
504
|
+
const projectedTurn = {
|
|
505
|
+
...turn,
|
|
506
|
+
context: {
|
|
507
|
+
...turn.context,
|
|
508
|
+
messages: this.sessionManager.buildSessionProjection().messages,
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
const compactedContext = await this._compactBeforeNextProviderRequest(projectedTurn, signal);
|
|
512
|
+
const context = signal?.aborted
|
|
513
|
+
? projectedTurn.context
|
|
514
|
+
: compactedContext === undefined
|
|
515
|
+
? await this._compactBeforeNextAssistantResponse(projectedTurn.context, signal)
|
|
516
|
+
: (compactedContext ?? projectedTurn.context);
|
|
517
|
+
const nextTurn = { ...turn, context };
|
|
407
518
|
const previousSnapshot = await previousPrepareNextTurnWithContext?.(nextTurn, signal);
|
|
408
|
-
const nextContext = previousSnapshot?.context ??
|
|
519
|
+
const nextContext = previousSnapshot?.context ?? context;
|
|
409
520
|
const runOptions = this._runSystemPromptOptions ?? this._baseSystemPromptOptions;
|
|
410
521
|
const options = normalizeBuildSystemPromptOptions({
|
|
411
522
|
...runOptions,
|
|
@@ -433,6 +544,84 @@ export class AgentSession {
|
|
|
433
544
|
// =========================================================================
|
|
434
545
|
// Event Subscription
|
|
435
546
|
// =========================================================================
|
|
547
|
+
_refreshFinalizedContext() {
|
|
548
|
+
const projection = this.sessionManager.buildSessionProjection();
|
|
549
|
+
for (const entry of projection.entries) {
|
|
550
|
+
for (const message of entry.messages)
|
|
551
|
+
this._entryIdsByMessage.set(message, entry.sourceEntry.id);
|
|
552
|
+
}
|
|
553
|
+
this.agent.state.messages = projection.messages;
|
|
554
|
+
}
|
|
555
|
+
_applyBoundaryDrafts(manager, drafts) {
|
|
556
|
+
const appended = [];
|
|
557
|
+
for (const draft of drafts) {
|
|
558
|
+
let entryId;
|
|
559
|
+
switch (draft.type) {
|
|
560
|
+
case "custom":
|
|
561
|
+
entryId = manager.appendCustomEntry(draft.customType, draft.data);
|
|
562
|
+
break;
|
|
563
|
+
case "custom_message":
|
|
564
|
+
entryId = manager.appendCustomMessageEntry(draft.customType, draft.content, draft.display, draft.details);
|
|
565
|
+
break;
|
|
566
|
+
case "context_edit":
|
|
567
|
+
entryId = manager.appendContextEdit(draft.targetId, draft.replacement);
|
|
568
|
+
break;
|
|
569
|
+
case "compaction": {
|
|
570
|
+
const tokensBefore = estimateProjectedContextTokens(manager.buildSessionProjection(), manager.getBranch()).tokens;
|
|
571
|
+
entryId = manager.appendCompaction(draft.summary, draft.firstKeptEntryId, tokensBefore, draft.details, true, draft.usage);
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const entry = manager.getEntry(entryId);
|
|
576
|
+
if (entry)
|
|
577
|
+
appended.push(entry);
|
|
578
|
+
}
|
|
579
|
+
return appended;
|
|
580
|
+
}
|
|
581
|
+
_createBoundaryPreviewManager(drafts) {
|
|
582
|
+
const header = this.sessionManager.getHeader();
|
|
583
|
+
if (!header)
|
|
584
|
+
throw new Error("Session header is missing");
|
|
585
|
+
const manager = SessionManager.inMemory(this._cwd, undefined, [header, ...this.sessionManager.getBranch()]);
|
|
586
|
+
this._applyBoundaryDrafts(manager, drafts);
|
|
587
|
+
return manager;
|
|
588
|
+
}
|
|
589
|
+
_getPendingBoundaryMessages() {
|
|
590
|
+
return [...this.agent.peekQueuedMessages(), ...this._pendingCustomMessages];
|
|
591
|
+
}
|
|
592
|
+
_buildBoundaryContext(drafts, boundary) {
|
|
593
|
+
const projection = this._createBoundaryPreviewManager(drafts).buildSessionProjection();
|
|
594
|
+
const pendingMessages = this._getPendingBoundaryMessages();
|
|
595
|
+
const llmMessages = convertToLlm(projection.messages);
|
|
596
|
+
const finalRole = llmMessages[llmMessages.length - 1]?.role;
|
|
597
|
+
const hasNonSystemContext = llmMessages.some((message) => message.role !== "system");
|
|
598
|
+
const contextCanContinue = hasNonSystemContext && finalRole !== "assistant";
|
|
599
|
+
const pendingCustomContext = this._pendingCustomMessages.length > 0;
|
|
600
|
+
return {
|
|
601
|
+
contextEntries: projection.entries,
|
|
602
|
+
contextMessages: projection.messages,
|
|
603
|
+
llmMessages,
|
|
604
|
+
pendingMessages,
|
|
605
|
+
canContinue: contextCanContinue ||
|
|
606
|
+
pendingCustomContext ||
|
|
607
|
+
(boundary === "turn_end"
|
|
608
|
+
? this.agent.hasQueuedMessages()
|
|
609
|
+
: finalRole === "assistant" && this.agent.hasQueuedMessages()),
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
_commitBoundaryDrafts(drafts) {
|
|
613
|
+
const appended = this._applyBoundaryDrafts(this.sessionManager, drafts);
|
|
614
|
+
this._refreshFinalizedContext();
|
|
615
|
+
for (const entry of appended)
|
|
616
|
+
this._emit({ type: "entry_appended", entry });
|
|
617
|
+
}
|
|
618
|
+
_reportInvalidBoundaryContinuation(event) {
|
|
619
|
+
this._extensionRunner.emitError({
|
|
620
|
+
extensionPath: "<boundary>",
|
|
621
|
+
event,
|
|
622
|
+
error: `${event} requested continuation without runnable model context`,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
436
625
|
/** Emit an event to all listeners */
|
|
437
626
|
_emit(event) {
|
|
438
627
|
for (const l of this._eventListeners) {
|
|
@@ -471,16 +660,27 @@ export class AgentSession {
|
|
|
471
660
|
async _emitAgentSettled() {
|
|
472
661
|
this._cacheWarmer?.onAgentSettled();
|
|
473
662
|
this._isAgentRunActive = false;
|
|
663
|
+
this._isEmittingAgentSettled = true;
|
|
474
664
|
try {
|
|
475
665
|
await this._extensionRunner.emit({ type: "agent_settled" });
|
|
476
666
|
this._emit({ type: "agent_settled" });
|
|
477
667
|
}
|
|
478
668
|
finally {
|
|
479
|
-
this.
|
|
669
|
+
this._isEmittingAgentSettled = false;
|
|
670
|
+
}
|
|
671
|
+
const deferred = this._deferredSettledActions.splice(0);
|
|
672
|
+
if (deferred.length > 0) {
|
|
673
|
+
try {
|
|
674
|
+
for (const action of deferred)
|
|
675
|
+
await action();
|
|
676
|
+
}
|
|
677
|
+
finally {
|
|
678
|
+
this._resolveIdleWaitIfIdle();
|
|
679
|
+
}
|
|
680
|
+
return;
|
|
480
681
|
}
|
|
682
|
+
this._resolveIdleWaitIfIdle();
|
|
481
683
|
}
|
|
482
|
-
// Track last assistant message for auto-compaction check
|
|
483
|
-
_lastAssistantMessage = undefined;
|
|
484
684
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
485
685
|
_handleAgentEvent = async (event, _signal) => {
|
|
486
686
|
// When a user message starts, check if it's from either queue and remove it BEFORE emitting
|
|
@@ -505,29 +705,30 @@ export class AgentSession {
|
|
|
505
705
|
}
|
|
506
706
|
}
|
|
507
707
|
}
|
|
508
|
-
// Emit to extensions first
|
|
708
|
+
// Emit to extensions first, then notify public listeners.
|
|
509
709
|
await this._emitExtensionEvent(event);
|
|
510
|
-
// Notify all listeners
|
|
511
710
|
this._emit(event.type === "agent_end" ? { ...event, willRetry: this._willRetryAfterAgentEnd(event) } : event);
|
|
512
711
|
// Handle session persistence
|
|
513
712
|
if (event.type === "message_end") {
|
|
713
|
+
let entryId;
|
|
514
714
|
// Check if this is a custom message from extensions
|
|
515
715
|
if (event.message.role === "custom") {
|
|
516
716
|
// Persist as CustomMessageEntry
|
|
517
|
-
this.sessionManager.appendCustomMessageEntry(event.message.customType, event.message.content, event.message.display, event.message.details);
|
|
717
|
+
entryId = this.sessionManager.appendCustomMessageEntry(event.message.customType, event.message.content, event.message.display, event.message.details);
|
|
518
718
|
}
|
|
519
719
|
else if (event.message.role === "system" ||
|
|
520
720
|
event.message.role === "user" ||
|
|
521
721
|
event.message.role === "assistant" ||
|
|
522
722
|
event.message.role === "toolResult") {
|
|
523
723
|
// Regular LLM message - persist as SessionMessageEntry
|
|
524
|
-
this.sessionManager.appendMessage(event.message);
|
|
724
|
+
entryId = this.sessionManager.appendMessage(event.message);
|
|
525
725
|
}
|
|
726
|
+
if (entryId)
|
|
727
|
+
this._entryIdsByMessage.set(event.message, entryId);
|
|
526
728
|
// Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
|
|
527
|
-
// Track assistant message for auto-compaction (checked on agent_end)
|
|
528
729
|
if (event.message.role === "assistant") {
|
|
529
|
-
this._lastAssistantMessage = event.message;
|
|
530
730
|
const assistantMsg = event.message;
|
|
731
|
+
this._lastAssistantMessage = assistantMsg;
|
|
531
732
|
if (!this._isTerminalAssistantFailure(assistantMsg)) {
|
|
532
733
|
this._overflowRecoveryAttempted = false;
|
|
533
734
|
}
|
|
@@ -549,6 +750,7 @@ export class AgentSession {
|
|
|
549
750
|
// extension and listener dispatch above also picks up messages that turn_end
|
|
550
751
|
// handlers queued.
|
|
551
752
|
if (event.type === "turn_end") {
|
|
753
|
+
this._lastAssistantToolResults = event.toolResults;
|
|
552
754
|
this._flushPendingCustomMessages();
|
|
553
755
|
}
|
|
554
756
|
};
|
|
@@ -567,6 +769,47 @@ export class AgentSession {
|
|
|
567
769
|
}
|
|
568
770
|
return false;
|
|
569
771
|
}
|
|
772
|
+
_findPersistedMessageEntryId(message) {
|
|
773
|
+
const mapped = this._entryIdsByMessage.get(message);
|
|
774
|
+
if (mapped)
|
|
775
|
+
return mapped;
|
|
776
|
+
for (const entry of [...this.sessionManager.getBranch()].reverse()) {
|
|
777
|
+
if (entry.type === "message" && entry.message === message)
|
|
778
|
+
return entry.id;
|
|
779
|
+
}
|
|
780
|
+
const messageIndex = this.agent.state.messages.indexOf(message);
|
|
781
|
+
if (messageIndex < 0)
|
|
782
|
+
return undefined;
|
|
783
|
+
const projection = this.sessionManager.buildSessionProjection();
|
|
784
|
+
let projectedIndex = 0;
|
|
785
|
+
for (const entry of projection.entries) {
|
|
786
|
+
for (let i = 0; i < entry.messages.length; i++) {
|
|
787
|
+
if (projectedIndex === messageIndex) {
|
|
788
|
+
this._entryIdsByMessage.set(message, entry.sourceEntry.id);
|
|
789
|
+
return entry.sourceEntry.id;
|
|
790
|
+
}
|
|
791
|
+
projectedIndex++;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return undefined;
|
|
795
|
+
}
|
|
796
|
+
_omitRecoveryAttempt(message, toolResults = []) {
|
|
797
|
+
const targets = [message, ...toolResults];
|
|
798
|
+
const targetIds = targets.map((target) => this._findPersistedMessageEntryId(target));
|
|
799
|
+
const unresolvedProjectedTarget = targets.some((target, index) => targetIds[index] === undefined && this.agent.state.messages.includes(target));
|
|
800
|
+
if (unresolvedProjectedTarget) {
|
|
801
|
+
throw new Error("Cannot persist recovery omission because a projected message has no source entry");
|
|
802
|
+
}
|
|
803
|
+
for (const targetId of targetIds) {
|
|
804
|
+
if (!targetId)
|
|
805
|
+
continue;
|
|
806
|
+
const editId = this.sessionManager.appendContextEdit(targetId, null);
|
|
807
|
+
const entry = this.sessionManager.getEntry(editId);
|
|
808
|
+
if (entry)
|
|
809
|
+
this._emit({ type: "entry_appended", entry });
|
|
810
|
+
}
|
|
811
|
+
this._refreshFinalizedContext();
|
|
812
|
+
}
|
|
570
813
|
/** Find the last assistant message in agent state (including aborted ones) */
|
|
571
814
|
_findLastAssistantMessage() {
|
|
572
815
|
const messages = this.agent.state.messages;
|
|
@@ -612,14 +855,42 @@ export class AgentSession {
|
|
|
612
855
|
if (!this._isTerminalAssistantFailure(assistant)) {
|
|
613
856
|
return;
|
|
614
857
|
}
|
|
615
|
-
|
|
616
|
-
|
|
858
|
+
const terminalIndex = branch.findIndex((entry) => entry.id === terminalMessageEntry?.id);
|
|
859
|
+
const trailingEntries = terminalIndex >= 0 ? branch.slice(terminalIndex + 1) : [];
|
|
860
|
+
const trailingOmissionOnly = trailingEntries.every((entry) => entry.type === "context_edit" && entry.targetId === terminalMessageEntry?.id && entry.replacement === null);
|
|
861
|
+
if (trailingEntries.length === 0 || trailingOmissionOnly) {
|
|
862
|
+
// With no later durable metadata, moving the active leaf keeps the old
|
|
863
|
+
// failure out of the active branch while retaining it in full history.
|
|
864
|
+
if (terminalMessageEntry.parentId) {
|
|
865
|
+
this.sessionManager.branch(terminalMessageEntry.parentId);
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
this.sessionManager.resetLeaf();
|
|
869
|
+
}
|
|
870
|
+
this._refreshFinalizedContext();
|
|
871
|
+
return;
|
|
617
872
|
}
|
|
618
|
-
|
|
619
|
-
|
|
873
|
+
// Preserve metadata after the failed attempt. The canonical projection can
|
|
874
|
+
// omit the attempt through an append-only edit; rewinding the raw leaf would
|
|
875
|
+
// also discard usage, boundary, or replacement entries persisted after it.
|
|
876
|
+
const projection = this.sessionManager.buildSessionProjection();
|
|
877
|
+
const projectedEntry = projection.entries.find((entry) => entry.sourceEntry.id === terminalMessageEntry.id);
|
|
878
|
+
if (!projectedEntry ||
|
|
879
|
+
!projectedEntry.messages.some((message) => message.role === "assistant" && this._isTerminalAssistantFailure(message))) {
|
|
880
|
+
this._refreshFinalizedContext();
|
|
881
|
+
return;
|
|
620
882
|
}
|
|
621
|
-
|
|
622
|
-
|
|
883
|
+
let latestEdit;
|
|
884
|
+
for (let index = branch.length - 1; index >= 0; index--) {
|
|
885
|
+
const entry = branch[index];
|
|
886
|
+
if (entry.type === "context_edit" && entry.targetId === terminalMessageEntry.id) {
|
|
887
|
+
latestEdit = entry;
|
|
888
|
+
break;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
if (latestEdit?.replacement !== null)
|
|
892
|
+
this.sessionManager.appendContextEdit(terminalMessageEntry.id, null);
|
|
893
|
+
this._refreshFinalizedContext();
|
|
623
894
|
}
|
|
624
895
|
_detachPiServerTerminalAssistantFailure() {
|
|
625
896
|
this._detachTerminalAssistantFailure();
|
|
@@ -666,13 +937,9 @@ export class AgentSession {
|
|
|
666
937
|
await this._extensionRunner.emit(extensionEvent);
|
|
667
938
|
}
|
|
668
939
|
else if (event.type === "turn_end") {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
message: event.message,
|
|
673
|
-
toolResults: event.toolResults,
|
|
674
|
-
};
|
|
675
|
-
await this._extensionRunner.emit(extensionEvent);
|
|
940
|
+
if (event.message.role === "assistant" && !this._boundaryDispatchedMessages.delete(event.message)) {
|
|
941
|
+
await this._dispatchTurnEndBoundary(event.message, event.toolResults);
|
|
942
|
+
}
|
|
676
943
|
this._turnIndex++;
|
|
677
944
|
}
|
|
678
945
|
else if (event.type === "message_start") {
|
|
@@ -788,6 +1055,10 @@ export class AgentSession {
|
|
|
788
1055
|
// =========================================================================
|
|
789
1056
|
// Read-only State Access
|
|
790
1057
|
// =========================================================================
|
|
1058
|
+
/** Refresh the public finalized transcript from the canonical session projection. */
|
|
1059
|
+
refreshContext() {
|
|
1060
|
+
this._refreshFinalizedContext();
|
|
1061
|
+
}
|
|
791
1062
|
/** Full agent state */
|
|
792
1063
|
get state() {
|
|
793
1064
|
return this.agent.state;
|
|
@@ -1030,12 +1301,14 @@ export class AgentSession {
|
|
|
1030
1301
|
this._isAgentRunActive = true;
|
|
1031
1302
|
try {
|
|
1032
1303
|
await start();
|
|
1033
|
-
while (
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1304
|
+
while (!this._agentRunAbortRequested) {
|
|
1305
|
+
const shouldContinue = await this._handlePostAgentRun();
|
|
1306
|
+
if (!shouldContinue) {
|
|
1307
|
+
if (this._isAborting || this._agentRunAbortRequested || !(await this._runBeforeSettleBoundary()))
|
|
1308
|
+
break;
|
|
1038
1309
|
}
|
|
1310
|
+
if (this._isAborting || this._agentRunAbortRequested)
|
|
1311
|
+
break;
|
|
1039
1312
|
try {
|
|
1040
1313
|
await this.agent.continue();
|
|
1041
1314
|
}
|
|
@@ -1141,22 +1414,23 @@ export class AgentSession {
|
|
|
1141
1414
|
}
|
|
1142
1415
|
}
|
|
1143
1416
|
async _handlePostAgentRun() {
|
|
1144
|
-
const
|
|
1417
|
+
const message = this._lastAssistantMessage;
|
|
1418
|
+
const toolResults = this._lastAssistantToolResults;
|
|
1145
1419
|
this._lastAssistantMessage = undefined;
|
|
1420
|
+
this._lastAssistantToolResults = [];
|
|
1146
1421
|
if (this._agentRunAbortRequested) {
|
|
1147
1422
|
this._finishCancelledRetry();
|
|
1148
1423
|
return false;
|
|
1149
1424
|
}
|
|
1150
|
-
if (!
|
|
1151
|
-
return
|
|
1152
|
-
}
|
|
1425
|
+
if (!message)
|
|
1426
|
+
return this.agent.hasQueuedMessages();
|
|
1153
1427
|
if (this._isAborting) {
|
|
1154
|
-
if (isPiServerMode() && this._isTerminalAssistantFailure(
|
|
1428
|
+
if (isPiServerMode() && this._isTerminalAssistantFailure(message)) {
|
|
1155
1429
|
this._detachPiServerTerminalAssistantFailure();
|
|
1156
1430
|
}
|
|
1157
1431
|
return false;
|
|
1158
1432
|
}
|
|
1159
|
-
if (this._isRetryableError(
|
|
1433
|
+
if (this._isRetryableError(message) && (await this._prepareRetry(message))) {
|
|
1160
1434
|
if (this._agentRunAbortRequested)
|
|
1161
1435
|
this._finishCancelledRetry();
|
|
1162
1436
|
return !this._agentRunAbortRequested;
|
|
@@ -1168,25 +1442,49 @@ export class AgentSession {
|
|
|
1168
1442
|
if (this._isAborting) {
|
|
1169
1443
|
return false;
|
|
1170
1444
|
}
|
|
1171
|
-
if (
|
|
1445
|
+
if (message.stopReason === "error" && this._retryAttempt > 0) {
|
|
1172
1446
|
this._emit({
|
|
1173
1447
|
type: "auto_retry_end",
|
|
1174
1448
|
success: false,
|
|
1175
1449
|
attempt: this._retryAttempt,
|
|
1176
|
-
finalError:
|
|
1450
|
+
finalError: message.errorMessage,
|
|
1177
1451
|
});
|
|
1178
1452
|
this._retryAttempt = 0;
|
|
1179
1453
|
}
|
|
1180
|
-
if (await this._checkCompaction(
|
|
1454
|
+
if (await this._checkCompaction(message, true, toolResults)) {
|
|
1181
1455
|
return !this._agentRunAbortRequested;
|
|
1182
1456
|
}
|
|
1183
|
-
if (isPiServerMode() && this._isTerminalAssistantFailure(
|
|
1457
|
+
if (isPiServerMode() && this._isTerminalAssistantFailure(message)) {
|
|
1184
1458
|
this._detachPiServerTerminalAssistantFailure();
|
|
1185
1459
|
}
|
|
1186
1460
|
// The agent loop drains both queues before emitting agent_end. Any messages
|
|
1187
1461
|
// here were queued by agent_end extension handlers and need a continuation.
|
|
1188
1462
|
return !this._agentRunAbortRequested && this.agent.hasQueuedMessages();
|
|
1189
1463
|
}
|
|
1464
|
+
async _runBeforeSettleBoundary() {
|
|
1465
|
+
if (!this._extensionRunner.hasHandlers("agent_before_settle"))
|
|
1466
|
+
return this.agent.hasQueuedMessages();
|
|
1467
|
+
this._isBeforeSettle = true;
|
|
1468
|
+
this._abortDuringBeforeSettle = false;
|
|
1469
|
+
try {
|
|
1470
|
+
const result = await this._extensionRunner.emitBoundary({ type: "agent_before_settle", outcome: this._lastActivityOutcome }, (entries) => this._buildBoundaryContext(entries, "agent_before_settle"));
|
|
1471
|
+
this._commitBoundaryDrafts(result.entries);
|
|
1472
|
+
this._flushPendingCustomMessages();
|
|
1473
|
+
const finalContext = this._buildBoundaryContext([], "agent_before_settle");
|
|
1474
|
+
if (this._abortDuringBeforeSettle)
|
|
1475
|
+
return false;
|
|
1476
|
+
const shouldContinue = result.continue || this.agent.hasQueuedMessages();
|
|
1477
|
+
if (shouldContinue && !finalContext.canContinue) {
|
|
1478
|
+
if (result.continue)
|
|
1479
|
+
this._reportInvalidBoundaryContinuation("agent_before_settle");
|
|
1480
|
+
return false;
|
|
1481
|
+
}
|
|
1482
|
+
return shouldContinue;
|
|
1483
|
+
}
|
|
1484
|
+
finally {
|
|
1485
|
+
this._isBeforeSettle = false;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1190
1488
|
async _runInputHandlers(text, images, source, streamingBehavior) {
|
|
1191
1489
|
if (!this._extensionRunner.hasHandlers("input")) {
|
|
1192
1490
|
return { text, images };
|
|
@@ -1200,6 +1498,25 @@ export class AgentSession {
|
|
|
1200
1498
|
}
|
|
1201
1499
|
return { text, images };
|
|
1202
1500
|
}
|
|
1501
|
+
async _normalizePromptImages(images) {
|
|
1502
|
+
if (!images)
|
|
1503
|
+
return { images: [], hints: [] };
|
|
1504
|
+
const normalizedImages = [];
|
|
1505
|
+
const hints = [];
|
|
1506
|
+
for (const image of images) {
|
|
1507
|
+
const processed = await processImage(Buffer.from(image.data, "base64"), image.mimeType, {
|
|
1508
|
+
autoResizeImages: this.settingsManager.getImageAutoResize(),
|
|
1509
|
+
resizeOptions: this.model?.inputLimits?.images?.resize,
|
|
1510
|
+
});
|
|
1511
|
+
if (!processed.ok) {
|
|
1512
|
+
hints.push(processed.message);
|
|
1513
|
+
continue;
|
|
1514
|
+
}
|
|
1515
|
+
normalizedImages.push({ type: "image", data: processed.data, mimeType: processed.mimeType });
|
|
1516
|
+
hints.push(...processed.hints);
|
|
1517
|
+
}
|
|
1518
|
+
return { images: normalizedImages, hints };
|
|
1519
|
+
}
|
|
1203
1520
|
/**
|
|
1204
1521
|
* Send a prompt to the agent.
|
|
1205
1522
|
* - Handles extension commands (registered via pi.registerCommand) immediately, even during streaming
|
|
@@ -1210,6 +1527,10 @@ export class AgentSession {
|
|
|
1210
1527
|
* @throws Error if no model selected or no API key available (when not streaming)
|
|
1211
1528
|
*/
|
|
1212
1529
|
async prompt(text, options) {
|
|
1530
|
+
if (this._isEmittingAgentSettled) {
|
|
1531
|
+
this._deferredSettledActions.push(async () => await this.prompt(text, options));
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1213
1534
|
const expandPromptTemplates = options?.expandPromptTemplates ?? true;
|
|
1214
1535
|
const preflightResult = options?.preflightResult;
|
|
1215
1536
|
let messages;
|
|
@@ -1288,7 +1609,7 @@ export class AgentSession {
|
|
|
1288
1609
|
this._prePromptAbortController = prePromptAbortController;
|
|
1289
1610
|
this._prePromptCompletionPromise = prePromptCompletion;
|
|
1290
1611
|
try {
|
|
1291
|
-
await this._checkCompaction(lastAssistant, false, prePromptAbortController.signal);
|
|
1612
|
+
await this._checkCompaction(lastAssistant, false, [], prePromptAbortController.signal);
|
|
1292
1613
|
}
|
|
1293
1614
|
finally {
|
|
1294
1615
|
if (this._prePromptAbortController === prePromptAbortController) {
|
|
@@ -1307,13 +1628,23 @@ export class AgentSession {
|
|
|
1307
1628
|
if (isPiServerMode()) {
|
|
1308
1629
|
this._detachPiServerTerminalAssistantFailure();
|
|
1309
1630
|
}
|
|
1310
|
-
//
|
|
1631
|
+
// Emit before_agent_start before normalizing images so extension-driven model
|
|
1632
|
+
// selection determines the resize profile used for the request and history.
|
|
1633
|
+
const selectedToolsBefore = this._baseSystemPromptOptions.selectedTools;
|
|
1634
|
+
const result = await this._extensionRunner.emitBeforeAgentStart(expandedText, currentImages, this._baseSystemPromptOptions);
|
|
1635
|
+
// Handlers may edit event.systemPromptOptions.selectedTools or call setActiveTools(),
|
|
1636
|
+
// which updates the live loadout instead. An explicit edit wins; otherwise the live
|
|
1637
|
+
// loadout is authoritative, so a setActiveTools() call is not undone here.
|
|
1638
|
+
const handlerEditedTools = result.systemPromptOptions.selectedTools.length !== selectedToolsBefore.length ||
|
|
1639
|
+
result.systemPromptOptions.selectedTools.some((name, index) => name !== selectedToolsBefore[index]);
|
|
1640
|
+
if (!handlerEditedTools)
|
|
1641
|
+
result.systemPromptOptions.selectedTools = this.getActiveToolNames();
|
|
1642
|
+
const normalized = await this._normalizePromptImages(currentImages);
|
|
1643
|
+
const userText = normalized.hints.length > 0 ? `${expandedText}\n\n${normalized.hints.join("\n")}` : expandedText;
|
|
1644
|
+
// Build messages only after hooks and image normalization have completed.
|
|
1311
1645
|
messages = [];
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
if (currentImages) {
|
|
1315
|
-
userContent.push(...currentImages);
|
|
1316
|
-
}
|
|
1646
|
+
const userContent = [{ type: "text", text: userText }];
|
|
1647
|
+
userContent.push(...normalized.images);
|
|
1317
1648
|
messages.push({
|
|
1318
1649
|
role: "user",
|
|
1319
1650
|
content: userContent,
|
|
@@ -1324,16 +1655,6 @@ export class AgentSession {
|
|
|
1324
1655
|
messages.push(msg);
|
|
1325
1656
|
}
|
|
1326
1657
|
this._pendingNextTurnMessages = [];
|
|
1327
|
-
// Emit before_agent_start extension event
|
|
1328
|
-
const selectedToolsBefore = this._baseSystemPromptOptions.selectedTools;
|
|
1329
|
-
const result = await this._extensionRunner.emitBeforeAgentStart(expandedText, currentImages, this._baseSystemPromptOptions);
|
|
1330
|
-
// Handlers may edit event.systemPromptOptions.selectedTools or call setActiveTools(),
|
|
1331
|
-
// which updates the live loadout instead. An explicit edit wins; otherwise the live
|
|
1332
|
-
// loadout is authoritative, so a setActiveTools() call is not undone here.
|
|
1333
|
-
const handlerEditedTools = result.systemPromptOptions.selectedTools.length !== selectedToolsBefore.length ||
|
|
1334
|
-
result.systemPromptOptions.selectedTools.some((name, index) => name !== selectedToolsBefore[index]);
|
|
1335
|
-
if (!handlerEditedTools)
|
|
1336
|
-
result.systemPromptOptions.selectedTools = this.getActiveToolNames();
|
|
1337
1658
|
for (const msg of result.messages) {
|
|
1338
1659
|
messages.push({
|
|
1339
1660
|
role: "custom",
|
|
@@ -1546,6 +1867,10 @@ export class AgentSession {
|
|
|
1546
1867
|
}
|
|
1547
1868
|
}
|
|
1548
1869
|
else if (options?.triggerTurn) {
|
|
1870
|
+
if (this._isEmittingAgentSettled) {
|
|
1871
|
+
this._deferredSettledActions.push(async () => await this._runAgentPrompt(appMessage));
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1549
1874
|
await this._runAgentPrompt(appMessage);
|
|
1550
1875
|
}
|
|
1551
1876
|
else if (this.isStreaming) {
|
|
@@ -1560,8 +1885,8 @@ export class AgentSession {
|
|
|
1560
1885
|
}
|
|
1561
1886
|
}
|
|
1562
1887
|
_appendCustomMessage(appMessage) {
|
|
1563
|
-
this.agent.state.messages.push(appMessage);
|
|
1564
1888
|
this.sessionManager.appendCustomMessageEntry(appMessage.customType, appMessage.content, appMessage.display, appMessage.details);
|
|
1889
|
+
this._refreshFinalizedContext();
|
|
1565
1890
|
this._emit({ type: "message_start", message: appMessage });
|
|
1566
1891
|
this._emit({ type: "message_end", message: appMessage });
|
|
1567
1892
|
}
|
|
@@ -1666,6 +1991,8 @@ export class AgentSession {
|
|
|
1666
1991
|
this.abortBash();
|
|
1667
1992
|
this._prePromptAbortController?.abort();
|
|
1668
1993
|
this._sessionNameAbortController?.abort();
|
|
1994
|
+
if (this._isBeforeSettle)
|
|
1995
|
+
this._abortDuringBeforeSettle = true;
|
|
1669
1996
|
this.agent.abort();
|
|
1670
1997
|
// _runAgentOperation owns the settled event and the idle waiter. Do not
|
|
1671
1998
|
// start a queued continuation until that cleanup has completed.
|
|
@@ -2094,9 +2421,8 @@ export class AgentSession {
|
|
|
2094
2421
|
}
|
|
2095
2422
|
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension, usage);
|
|
2096
2423
|
const newEntries = this.sessionManager.getEntries();
|
|
2097
|
-
|
|
2098
|
-
this.
|
|
2099
|
-
const estimatedTokensAfter = estimateMessagesTokens(sessionContext.messages);
|
|
2424
|
+
this._refreshFinalizedContext();
|
|
2425
|
+
const estimatedTokensAfter = estimateMessagesTokens(this.sessionManager.buildSessionProjection().messages);
|
|
2100
2426
|
// Get the saved compaction entry for the extension event
|
|
2101
2427
|
const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
|
|
2102
2428
|
if (this._extensionRunner && savedCompactionEntry) {
|
|
@@ -2186,7 +2512,7 @@ export class AgentSession {
|
|
|
2186
2512
|
* @param assistantMessage The assistant message to check
|
|
2187
2513
|
* @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
|
|
2188
2514
|
*/
|
|
2189
|
-
async _checkCompaction(assistantMessage, skipAbortedCheck = true, externalSignal) {
|
|
2515
|
+
async _checkCompaction(assistantMessage, skipAbortedCheck = true, toolResults = [], externalSignal) {
|
|
2190
2516
|
if (externalSignal?.aborted)
|
|
2191
2517
|
return false;
|
|
2192
2518
|
const settings = this.settingsManager.getCompactionSettings(this.model);
|
|
@@ -2214,16 +2540,31 @@ export class AgentSession {
|
|
|
2214
2540
|
// Case 1: Recoverable failure. Explicit/silent context overflow still uses context metadata.
|
|
2215
2541
|
// A length stop is recoverable when output ended below the model's original desired limit,
|
|
2216
2542
|
// independent of the configured context size or any context-clamped provider request limit.
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
const
|
|
2220
|
-
|
|
2543
|
+
const currentProjection = this.sessionManager.buildSessionProjection();
|
|
2544
|
+
const assistantEntryId = this._findPersistedMessageEntryId(assistantMessage);
|
|
2545
|
+
const assistantIsProjected = assistantEntryId === undefined ||
|
|
2546
|
+
currentProjection.entries.some((entry) => entry.sourceEntry.id === assistantEntryId &&
|
|
2547
|
+
entry.messages.some((message) => message.role === "assistant"));
|
|
2548
|
+
const branch = this.sessionManager.getBranch();
|
|
2549
|
+
const assistantIndex = assistantEntryId ? branch.findIndex((entry) => entry.id === assistantEntryId) : -1;
|
|
2550
|
+
const entriesAfterAssistant = assistantIndex >= 0 ? branch.slice(assistantIndex + 1) : [];
|
|
2551
|
+
const hasPostAssistantContextEdit = entriesAfterAssistant.some((entry) => entry.type === "context_edit");
|
|
2552
|
+
const latestAssistantEdit = entriesAfterAssistant
|
|
2553
|
+
.filter((entry) => entry.type === "context_edit" && entry.targetId === assistantEntryId)
|
|
2554
|
+
.at(-1);
|
|
2555
|
+
const assistantRetainedForExplicitRecovery = assistantEntryId === undefined ||
|
|
2556
|
+
(!entriesAfterAssistant.some((entry) => entry.type === "compaction") &&
|
|
2557
|
+
latestAssistantEdit?.replacement !== null);
|
|
2558
|
+
const assistantUsageMatchesProjection = assistantIsProjected && !hasPostAssistantContextEdit;
|
|
2559
|
+
const explicitOverflow = assistantMessage.stopReason === "error" && isContextOverflow(assistantMessage);
|
|
2560
|
+
const contextOverflow = sameModel &&
|
|
2561
|
+
((explicitOverflow && assistantRetainedForExplicitRecovery) ||
|
|
2562
|
+
(assistantUsageMatchesProjection && isContextOverflow(assistantMessage, contextWindow)));
|
|
2563
|
+
const recoverableLength = sameModel && assistantIsProjected && isRecoverableLength(assistantMessage, this.model?.maxTokens ?? 0);
|
|
2221
2564
|
if (contextOverflow || recoverableLength) {
|
|
2222
2565
|
const willRetry = assistantMessage.stopReason !== "stop";
|
|
2223
2566
|
if (!willRetry) {
|
|
2224
|
-
return externalSignal
|
|
2225
|
-
? await this._runAutoCompaction("overflow", false, undefined, undefined, externalSignal)
|
|
2226
|
-
: await this._runAutoCompaction("overflow", false);
|
|
2567
|
+
return await this._runAutoCompaction("overflow", false, undefined, undefined, externalSignal);
|
|
2227
2568
|
}
|
|
2228
2569
|
if (this._overflowRecoveryAttempted) {
|
|
2229
2570
|
const errorMessage = contextOverflow
|
|
@@ -2239,30 +2580,25 @@ export class AgentSession {
|
|
|
2239
2580
|
});
|
|
2240
2581
|
return false;
|
|
2241
2582
|
}
|
|
2583
|
+
// Persistently omit the selected final attempt before post-run recovery compaction.
|
|
2242
2584
|
this._overflowRecoveryAttempted = true;
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
// but we don't want it in context for the retry)
|
|
2248
|
-
if (this._isTerminalAssistantFailure(assistantMessage)) {
|
|
2249
|
-
this._detachTerminalAssistantFailure();
|
|
2250
|
-
}
|
|
2251
|
-
else {
|
|
2252
|
-
const messages = this.agent.state.messages;
|
|
2253
|
-
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
2254
|
-
this.agent.state.messages = messages.slice(0, -1);
|
|
2255
|
-
}
|
|
2256
|
-
}
|
|
2257
|
-
return await this._runAutoCompaction("overflow", willRetry, terminalFailurePreparation, undefined, externalSignal);
|
|
2585
|
+
this._omitRecoveryAttempt(assistantMessage, toolResults);
|
|
2586
|
+
if (isPiServerMode())
|
|
2587
|
+
this._detachPiServerTerminalAssistantFailure();
|
|
2588
|
+
return await this._runAutoCompaction("overflow", willRetry, undefined, undefined, externalSignal);
|
|
2258
2589
|
}
|
|
2259
2590
|
// Case 2: Threshold - context is getting large
|
|
2260
2591
|
// For error messages or all-zero usage messages, estimate from the last valid response.
|
|
2261
2592
|
// This ensures sessions that hit persistent API errors (e.g. 529) or malformed zero-usage
|
|
2262
2593
|
// responses can still compact and do not reset context accounting.
|
|
2263
2594
|
let contextTokens;
|
|
2595
|
+
const projection = currentProjection;
|
|
2596
|
+
const hasContextEdits = projection.entries.some((entry) => entry.sourceEntry.type === "context_edit");
|
|
2264
2597
|
const directContextTokens = assistantMessage.usage ? calculateContextTokens(assistantMessage.usage) : 0;
|
|
2265
|
-
if (
|
|
2598
|
+
if (hasContextEdits) {
|
|
2599
|
+
contextTokens = estimateProjectedContextTokens(projection, branch).tokens;
|
|
2600
|
+
}
|
|
2601
|
+
else if (assistantMessage.stopReason === "error" || directContextTokens === 0) {
|
|
2266
2602
|
const messages = this.agent.state.messages;
|
|
2267
2603
|
const estimate = estimateContextTokens(messages);
|
|
2268
2604
|
// When a valid usage entry exists, verify it is post-compaction. Kept
|
|
@@ -2410,9 +2746,8 @@ export class AgentSession {
|
|
|
2410
2746
|
abortController.signal.throwIfAborted();
|
|
2411
2747
|
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension, usage);
|
|
2412
2748
|
const newEntries = this.sessionManager.getEntries();
|
|
2413
|
-
|
|
2414
|
-
this.
|
|
2415
|
-
const estimatedTokensAfter = estimateMessagesTokens(sessionContext.messages);
|
|
2749
|
+
this._refreshFinalizedContext();
|
|
2750
|
+
const estimatedTokensAfter = estimateMessagesTokens(this.sessionManager.buildSessionProjection().messages);
|
|
2416
2751
|
const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
|
|
2417
2752
|
if (this._extensionRunner && savedCompactionEntry) {
|
|
2418
2753
|
await this._extensionRunner.emit({
|
|
@@ -2494,7 +2829,38 @@ export class AgentSession {
|
|
|
2494
2829
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
2495
2830
|
if (contextWindow <= 0)
|
|
2496
2831
|
return false;
|
|
2497
|
-
const
|
|
2832
|
+
const branch = this.sessionManager.getBranch();
|
|
2833
|
+
const projection = this.sessionManager.buildSessionProjection();
|
|
2834
|
+
const toolResultEntryIndexes = turn.toolResults
|
|
2835
|
+
.map((message) => this._findPersistedMessageEntryId(message))
|
|
2836
|
+
.flatMap((id) => {
|
|
2837
|
+
const index = id ? branch.findIndex((entry) => entry.id === id) : -1;
|
|
2838
|
+
return index >= 0 ? [index] : [];
|
|
2839
|
+
});
|
|
2840
|
+
const lastToolResultIndex = Math.max(...toolResultEntryIndexes, -1);
|
|
2841
|
+
if (lastToolResultIndex >= 0) {
|
|
2842
|
+
for (const entry of branch.slice(lastToolResultIndex + 1)) {
|
|
2843
|
+
if (entry.type === "custom_message")
|
|
2844
|
+
return false;
|
|
2845
|
+
if (entry.type === "message" &&
|
|
2846
|
+
(entry.message.role === "user" ||
|
|
2847
|
+
entry.message.role === "custom" ||
|
|
2848
|
+
entry.message.role === "bashExecution")) {
|
|
2849
|
+
return false;
|
|
2850
|
+
}
|
|
2851
|
+
if (entry.type === "context_edit" && entry.replacement !== null) {
|
|
2852
|
+
const target = branch.find((candidate) => candidate.id === entry.targetId);
|
|
2853
|
+
if (target?.type === "custom_message" ||
|
|
2854
|
+
(target?.type === "message" &&
|
|
2855
|
+
(target.message.role === "user" ||
|
|
2856
|
+
target.message.role === "custom" ||
|
|
2857
|
+
target.message.role === "bashExecution"))) {
|
|
2858
|
+
return false;
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
const projectedTokens = estimateProjectedContextTokens(projection, branch).tokens;
|
|
2498
2864
|
return projectedTokens >= contextWindow - settings.reserveTokens;
|
|
2499
2865
|
}
|
|
2500
2866
|
_appendIntraTurnCompactionMarker() {
|
|
@@ -2530,13 +2896,13 @@ export class AgentSession {
|
|
|
2530
2896
|
const preparation = prepareCompaction(branchWithMarker, settings, preparationOptions);
|
|
2531
2897
|
if (!preparation) {
|
|
2532
2898
|
this._restoreSessionLeaf(markerParentId);
|
|
2533
|
-
return
|
|
2899
|
+
return null;
|
|
2534
2900
|
}
|
|
2535
|
-
await this._runAutoCompaction("threshold", false, preparation, preparationOptions);
|
|
2901
|
+
await this._runAutoCompaction("threshold", false, preparation, preparationOptions, signal);
|
|
2536
2902
|
const currentCompactionId = getLatestCompactionEntry(this.sessionManager.getBranch())?.id;
|
|
2537
2903
|
if (currentCompactionId === previousCompactionId) {
|
|
2538
2904
|
this._restoreSessionLeaf(markerParentId);
|
|
2539
|
-
return
|
|
2905
|
+
return null;
|
|
2540
2906
|
}
|
|
2541
2907
|
return this._currentAgentContext(turn.context);
|
|
2542
2908
|
}
|
|
@@ -2964,28 +3330,10 @@ export class AgentSession {
|
|
|
2964
3330
|
delayMs,
|
|
2965
3331
|
errorMessage: message.errorMessage || "Unknown error",
|
|
2966
3332
|
});
|
|
2967
|
-
|
|
3333
|
+
// Keep the failed attempt in raw history while durably omitting it from model projection.
|
|
3334
|
+
this._omitRecoveryAttempt(message);
|
|
3335
|
+
if (isPiServerMode())
|
|
2968
3336
|
this._detachPiServerTerminalAssistantFailure();
|
|
2969
|
-
}
|
|
2970
|
-
else {
|
|
2971
|
-
// Remove error message from agent state (keep in session for history)
|
|
2972
|
-
const messages = this.agent.state.messages;
|
|
2973
|
-
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
2974
|
-
this.agent.state.messages = messages.slice(0, -1);
|
|
2975
|
-
}
|
|
2976
|
-
}
|
|
2977
|
-
const lastMessage = this.agent.state.messages[this.agent.state.messages.length - 1];
|
|
2978
|
-
if (!lastMessage || lastMessage.role === "assistant") {
|
|
2979
|
-
const attempt = this._retryAttempt;
|
|
2980
|
-
this._retryAttempt = 0;
|
|
2981
|
-
this._emit({
|
|
2982
|
-
type: "auto_retry_end",
|
|
2983
|
-
success: false,
|
|
2984
|
-
attempt,
|
|
2985
|
-
finalError: lastMessage ? "Cannot continue from message role: assistant" : "No messages to continue from",
|
|
2986
|
-
});
|
|
2987
|
-
return false;
|
|
2988
|
-
}
|
|
2989
3337
|
// Wait with exponential backoff (abortable)
|
|
2990
3338
|
this._retryAbortController = new AbortController();
|
|
2991
3339
|
try {
|
|
@@ -3084,10 +3432,8 @@ export class AgentSession {
|
|
|
3084
3432
|
this._pendingBashMessages.push(bashMessage);
|
|
3085
3433
|
}
|
|
3086
3434
|
else {
|
|
3087
|
-
// Add to agent state immediately
|
|
3088
|
-
this.agent.state.messages.push(bashMessage);
|
|
3089
|
-
// Save to session
|
|
3090
3435
|
this.sessionManager.appendMessage(bashMessage);
|
|
3436
|
+
this._refreshFinalizedContext();
|
|
3091
3437
|
}
|
|
3092
3438
|
}
|
|
3093
3439
|
/**
|
|
@@ -3114,12 +3460,10 @@ export class AgentSession {
|
|
|
3114
3460
|
if (this._pendingBashMessages.length === 0)
|
|
3115
3461
|
return;
|
|
3116
3462
|
for (const bashMessage of this._pendingBashMessages) {
|
|
3117
|
-
// Add to agent state
|
|
3118
|
-
this.agent.state.messages.push(bashMessage);
|
|
3119
|
-
// Save to session
|
|
3120
3463
|
this.sessionManager.appendMessage(bashMessage);
|
|
3121
3464
|
}
|
|
3122
3465
|
this._pendingBashMessages = [];
|
|
3466
|
+
this._refreshFinalizedContext();
|
|
3123
3467
|
}
|
|
3124
3468
|
// =========================================================================
|
|
3125
3469
|
// Session Management
|
|
@@ -3298,9 +3642,8 @@ export class AgentSession {
|
|
|
3298
3642
|
if (label && !summaryText) {
|
|
3299
3643
|
this.sessionManager.appendLabelChange(targetId, label);
|
|
3300
3644
|
}
|
|
3301
|
-
// Update
|
|
3302
|
-
|
|
3303
|
-
this.agent.state.messages = sessionContext.messages;
|
|
3645
|
+
// Update finalized context from the canonical session projection.
|
|
3646
|
+
this._refreshFinalizedContext();
|
|
3304
3647
|
this._restoreToolsFromTranscript();
|
|
3305
3648
|
// Emit session_tree event
|
|
3306
3649
|
await this._extensionRunner.emit({
|
|
@@ -3410,30 +3753,24 @@ export class AgentSession {
|
|
|
3410
3753
|
// After compaction, the last assistant usage reflects pre-compaction context size.
|
|
3411
3754
|
// We can only trust usage from an assistant that responded after the latest compaction.
|
|
3412
3755
|
// If no such assistant exists, context token count is unknown until the next LLM response.
|
|
3413
|
-
const
|
|
3414
|
-
const
|
|
3756
|
+
const projection = this.sessionManager.buildSessionProjection();
|
|
3757
|
+
const branch = this.sessionManager.getBranch();
|
|
3758
|
+
const latestCompaction = getLatestCompactionEntry(branch);
|
|
3415
3759
|
if (latestCompaction) {
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
break;
|
|
3428
|
-
}
|
|
3429
|
-
}
|
|
3430
|
-
}
|
|
3431
|
-
}
|
|
3432
|
-
if (!hasPostCompactionUsage) {
|
|
3760
|
+
const projectedAssistants = new Set(projection.entries.flatMap((entry) => entry.messages.some((message) => message.role === "assistant" &&
|
|
3761
|
+
message.stopReason !== "aborted" &&
|
|
3762
|
+
message.stopReason !== "error" &&
|
|
3763
|
+
calculateContextTokens(message.usage) > 0)
|
|
3764
|
+
? [entry.sourceEntry.id]
|
|
3765
|
+
: []));
|
|
3766
|
+
const compactionIndex = branch.findIndex((entry) => entry.id === latestCompaction.id);
|
|
3767
|
+
const hasPostCompactionUsage = branch
|
|
3768
|
+
.slice(compactionIndex + 1)
|
|
3769
|
+
.some((entry) => projectedAssistants.has(entry.id));
|
|
3770
|
+
if (!hasPostCompactionUsage)
|
|
3433
3771
|
return { tokens: null, contextWindow, percent: null };
|
|
3434
|
-
}
|
|
3435
3772
|
}
|
|
3436
|
-
const estimate =
|
|
3773
|
+
const estimate = estimateProjectedContextTokens(projection, branch);
|
|
3437
3774
|
const percent = (estimate.tokens / contextWindow) * 100;
|
|
3438
3775
|
return {
|
|
3439
3776
|
tokens: estimate.tokens,
|