@aexol/spectral 0.9.58 → 0.9.59
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 +6 -0
- package/README.md +43 -0
- package/dist/agent/index.d.ts +6 -1
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +133 -14
- package/dist/extensions/web/index.d.ts +10 -0
- package/dist/extensions/web/index.d.ts.map +1 -0
- package/dist/extensions/web/index.js +11 -0
- package/dist/extensions/web/tools/fetch.d.ts +3 -0
- package/dist/extensions/web/tools/fetch.d.ts.map +1 -0
- package/dist/extensions/web/tools/fetch.js +80 -0
- package/dist/extensions/web/utils/fetcher.d.ts +34 -0
- package/dist/extensions/web/utils/fetcher.d.ts.map +1 -0
- package/dist/extensions/web/utils/fetcher.js +289 -0
- package/dist/extensions/web/utils/parser.d.ts +15 -0
- package/dist/extensions/web/utils/parser.d.ts.map +1 -0
- package/dist/extensions/web/utils/parser.js +205 -0
- package/dist/memory/branch.d.ts.map +1 -1
- package/dist/memory/branch.js +16 -8
- package/dist/memory/commands/status.d.ts.map +1 -1
- package/dist/memory/commands/status.js +3 -2
- package/dist/memory/config.d.ts +12 -0
- package/dist/memory/config.d.ts.map +1 -1
- package/dist/memory/config.js +21 -2
- package/dist/memory/hooks/compaction-trigger.d.ts.map +1 -1
- package/dist/memory/hooks/compaction-trigger.js +19 -3
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/memory/index.js +2 -0
- package/dist/memory/tool-output-compressor.d.ts +14 -20
- package/dist/memory/tool-output-compressor.d.ts.map +1 -1
- package/dist/memory/tool-output-compressor.js +391 -137
- package/dist/memory/tools/compact-context.d.ts +5 -0
- package/dist/memory/tools/compact-context.d.ts.map +1 -0
- package/dist/memory/tools/compact-context.js +772 -0
- package/dist/relay/dispatcher.d.ts +1 -1
- package/dist/relay/dispatcher.d.ts.map +1 -1
- package/dist/relay/dispatcher.js +61 -1
- package/dist/sdk/coding-agent/core/agent-session.d.ts +56 -3
- package/dist/sdk/coding-agent/core/agent-session.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/agent-session.js +408 -210
- package/dist/sdk/coding-agent/core/compaction/compaction.d.ts +57 -7
- package/dist/sdk/coding-agent/core/compaction/compaction.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/compaction/compaction.js +146 -27
- package/dist/sdk/coding-agent/core/compaction/dcp-lite.d.ts +24 -0
- package/dist/sdk/coding-agent/core/compaction/dcp-lite.d.ts.map +1 -0
- package/dist/sdk/coding-agent/core/compaction/dcp-lite.js +411 -0
- package/dist/sdk/coding-agent/core/compaction/index.d.ts +2 -0
- package/dist/sdk/coding-agent/core/compaction/index.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/compaction/index.js +2 -0
- package/dist/sdk/coding-agent/core/compaction/policy.d.ts +71 -0
- package/dist/sdk/coding-agent/core/compaction/policy.d.ts.map +1 -0
- package/dist/sdk/coding-agent/core/compaction/policy.js +147 -0
- package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/extensions/native-extensions.js +9 -0
- package/dist/sdk/coding-agent/core/extensions/types.d.ts +17 -0
- package/dist/sdk/coding-agent/core/extensions/types.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/settings-manager.d.ts +4 -10
- package/dist/sdk/coding-agent/core/settings-manager.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/settings-manager.js +17 -17
- package/dist/sdk/coding-agent/index.d.ts +1 -1
- package/dist/sdk/coding-agent/index.d.ts.map +1 -1
- package/dist/server/agent-bridge.d.ts +12 -5
- package/dist/server/agent-bridge.d.ts.map +1 -1
- package/dist/server/agent-bridge.js +65 -17
- package/dist/server/handlers/paths-home.d.ts +11 -0
- package/dist/server/handlers/paths-home.d.ts.map +1 -0
- package/dist/server/handlers/paths-home.js +10 -0
- package/dist/server/handlers/paths-list.d.ts +24 -0
- package/dist/server/handlers/paths-list.d.ts.map +1 -0
- package/dist/server/handlers/paths-list.js +56 -0
- package/dist/server/handlers/paths-pick-directory.d.ts.map +1 -1
- package/dist/server/handlers/paths-pick-directory.js +2 -1
- package/dist/server/handlers/sessions.d.ts +11 -1
- package/dist/server/handlers/sessions.d.ts.map +1 -1
- package/dist/server/handlers/sessions.js +22 -2
- package/dist/server/handlers/settings.d.ts +18 -1
- package/dist/server/handlers/settings.d.ts.map +1 -1
- package/dist/server/handlers/settings.js +63 -13
- package/dist/server/session-stream.d.ts +65 -37
- package/dist/server/session-stream.d.ts.map +1 -1
- package/dist/server/session-stream.js +525 -119
- package/dist/server/storage.d.ts +3 -0
- package/dist/server/storage.d.ts.map +1 -1
- package/dist/server/storage.js +25 -0
- package/dist/server/wire.d.ts +127 -2
- package/dist/server/wire.d.ts.map +1 -1
- package/package.json +2 -1
|
@@ -14,14 +14,13 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { basename, dirname } from "node:path";
|
|
17
|
-
import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, modelsAreEqual, resetApiProviders,
|
|
18
|
-
import { AssistantMessageEventStream } from "../../ai/utils/event-stream.js";
|
|
17
|
+
import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, modelsAreEqual, resetApiProviders, } from "../../ai/index.js";
|
|
19
18
|
import { stripFrontmatter } from "../utils/frontmatter.js";
|
|
20
19
|
import { resolvePath } from "../utils/paths.js";
|
|
21
20
|
import { sleep } from "../utils/sleep.js";
|
|
22
21
|
import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js";
|
|
23
22
|
import { executeBashWithOperations } from "./bash-executor.js";
|
|
24
|
-
import { calculateContextTokens, collectEntriesForBranchSummary,
|
|
23
|
+
import { calculateContextTokens, collectEntriesForBranchSummary, compactDcpLite, estimateContextTokens, estimateCompactionTokenMetrics, getManualCompactionSettings, normalizeCompactionPolicy, normalizeManualKeepRecentTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
|
|
25
24
|
import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
|
|
26
25
|
import { ExtensionRunner, wrapRegisteredTools, } from "./extensions/index.js";
|
|
27
26
|
import { emitSessionShutdownEvent } from "./extensions/runner.js";
|
|
@@ -48,11 +47,110 @@ export function parseSkillBlock(text) {
|
|
|
48
47
|
userMessage: match[4]?.trim() || undefined,
|
|
49
48
|
};
|
|
50
49
|
}
|
|
50
|
+
function normalizeManualCompactionOptions(input) {
|
|
51
|
+
if (typeof input === "string")
|
|
52
|
+
return { customInstructions: input };
|
|
53
|
+
return {
|
|
54
|
+
customInstructions: input?.customInstructions,
|
|
55
|
+
phaseBoundary: input?.phaseBoundary,
|
|
56
|
+
keepRecentTokens: input?.keepRecentTokens,
|
|
57
|
+
memoryHookMode: input?.memoryHookMode === "skip" ? "skip" : "inline",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const DCP_LITE_PHASE_BOUNDARY_CUSTOM_TYPE = "spectral.dcp-lite.phase-boundary";
|
|
61
|
+
const DCP_LITE_PHASE_BOUNDARY_MARKER = "DCP-lite phase boundary: prior raw context was compacted. Continue using the compaction summary above.";
|
|
62
|
+
function createVirtualPhaseBoundaryEntry(pathEntries) {
|
|
63
|
+
const parentId = pathEntries[pathEntries.length - 1]?.id ?? null;
|
|
64
|
+
return {
|
|
65
|
+
type: "custom_message",
|
|
66
|
+
customType: DCP_LITE_PHASE_BOUNDARY_CUSTOM_TYPE,
|
|
67
|
+
content: DCP_LITE_PHASE_BOUNDARY_MARKER,
|
|
68
|
+
display: false,
|
|
69
|
+
details: { virtual: true },
|
|
70
|
+
id: "__spectral_dcp_lite_phase_boundary_probe__",
|
|
71
|
+
parentId,
|
|
72
|
+
timestamp: new Date().toISOString(),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function isDcpLitePhaseBoundaryEntry(entry) {
|
|
76
|
+
return entry?.type === "custom_message" && entry.customType === DCP_LITE_PHASE_BOUNDARY_CUSTOM_TYPE;
|
|
77
|
+
}
|
|
78
|
+
function isRecord(value) {
|
|
79
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
80
|
+
}
|
|
81
|
+
function isObservationalMemoryDetails(details) {
|
|
82
|
+
if (!isRecord(details))
|
|
83
|
+
return false;
|
|
84
|
+
const record = details;
|
|
85
|
+
return (record.type === "observational-memory" &&
|
|
86
|
+
(record.version === 3 || record.version === 4) &&
|
|
87
|
+
Array.isArray(record.observations) &&
|
|
88
|
+
Array.isArray(record.reflections));
|
|
89
|
+
}
|
|
90
|
+
function buildCompactionTelemetryDetails(result) {
|
|
91
|
+
const telemetry = {};
|
|
92
|
+
if (typeof result.tokensCompacted === "number")
|
|
93
|
+
telemetry.tokensCompacted = result.tokensCompacted;
|
|
94
|
+
if (typeof result.tokensRemoved === "number")
|
|
95
|
+
telemetry.tokensRemoved = result.tokensRemoved;
|
|
96
|
+
if (typeof result.summaryTokens === "number")
|
|
97
|
+
telemetry.summaryTokens = result.summaryTokens;
|
|
98
|
+
if (typeof result.reductionPercent === "number")
|
|
99
|
+
telemetry.reductionPercent = result.reductionPercent;
|
|
100
|
+
if (typeof result.compactionNumber === "number")
|
|
101
|
+
telemetry.compactionNumber = result.compactionNumber;
|
|
102
|
+
if (result.largestTokenSources?.length)
|
|
103
|
+
telemetry.largestTokenSources = result.largestTokenSources;
|
|
104
|
+
return telemetry;
|
|
105
|
+
}
|
|
106
|
+
function preserveDcpHookMemoryDetails(dcpResult, extensionCompaction) {
|
|
107
|
+
if (!extensionCompaction ||
|
|
108
|
+
!isRecord(extensionCompaction.details) ||
|
|
109
|
+
!isObservationalMemoryDetails(extensionCompaction.details))
|
|
110
|
+
return dcpResult;
|
|
111
|
+
const extensionDetails = extensionCompaction.details;
|
|
112
|
+
const mergedDetails = {
|
|
113
|
+
...extensionDetails,
|
|
114
|
+
compactionTelemetry: buildCompactionTelemetryDetails(dcpResult),
|
|
115
|
+
};
|
|
116
|
+
if (isRecord(dcpResult.details)) {
|
|
117
|
+
mergedDetails.dcpLite = dcpResult.details;
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
...dcpResult,
|
|
121
|
+
details: mergedDetails,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function getNonShrinkingSkipMessage(result) {
|
|
125
|
+
const tokensCompacted = result.tokensCompacted;
|
|
126
|
+
const summaryTokens = result.summaryTokens;
|
|
127
|
+
const tokensRemoved = result.tokensRemoved;
|
|
128
|
+
if (typeof tokensCompacted !== "number" || typeof summaryTokens !== "number")
|
|
129
|
+
return undefined;
|
|
130
|
+
if (tokensCompacted <= 0) {
|
|
131
|
+
return "Compaction skipped: no eligible history to compact.";
|
|
132
|
+
}
|
|
133
|
+
if ((tokensRemoved ?? 0) <= 0 || summaryTokens >= tokensCompacted) {
|
|
134
|
+
return `Compaction skipped: generated summary would not shrink context (` +
|
|
135
|
+
`compacted ~${tokensCompacted.toLocaleString()} tokens, ` +
|
|
136
|
+
`summary +~${summaryTokens.toLocaleString()} tokens).`;
|
|
137
|
+
}
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
function markCompactionSkipped(result, skipReason, message) {
|
|
141
|
+
return {
|
|
142
|
+
...result,
|
|
143
|
+
skipped: true,
|
|
144
|
+
skipReason,
|
|
145
|
+
message,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
51
148
|
// ============================================================================
|
|
52
149
|
// Constants
|
|
53
150
|
// ============================================================================
|
|
54
151
|
/** Standard thinking levels */
|
|
55
152
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
|
|
153
|
+
const LENGTH_CONTINUATION_PROMPT = "Your previous assistant response ended because the provider hit its maximum output length. Continue exactly from where it stopped. Do not repeat, recap, or summarize earlier content. If the previous response ended mid-sentence, resume with the next missing word; otherwise continue with the next missing content.";
|
|
56
154
|
// ============================================================================
|
|
57
155
|
// AgentSession Class
|
|
58
156
|
// ============================================================================
|
|
@@ -80,6 +178,12 @@ export class AgentSession {
|
|
|
80
178
|
// Retry state
|
|
81
179
|
_retryAbortController = undefined;
|
|
82
180
|
_retryAttempt = 0;
|
|
181
|
+
_lengthContinuationAttempts = 0;
|
|
182
|
+
_lengthContinuationOutputTokens = 0;
|
|
183
|
+
_pendingInternalLengthContinuationMessages = 0;
|
|
184
|
+
_internalLengthContinuationUserMessages = new WeakSet();
|
|
185
|
+
_autoOlderHistoryCooldownTurnsRemaining = 0;
|
|
186
|
+
_lastAutoOlderHistorySkipSignature;
|
|
83
187
|
// Bash execution state
|
|
84
188
|
_bashAbortController = undefined;
|
|
85
189
|
_pendingBashMessages = [];
|
|
@@ -158,13 +262,6 @@ export class AgentSession {
|
|
|
158
262
|
}
|
|
159
263
|
throw new Error(formatNoApiKeyFoundMessage(model.provider));
|
|
160
264
|
}
|
|
161
|
-
async _getCompactionRequestAuth(model) {
|
|
162
|
-
if (this.agent.streamFn === streamSimple) {
|
|
163
|
-
return this._getRequiredRequestAuth(model);
|
|
164
|
-
}
|
|
165
|
-
const result = await this._modelRegistry.getApiKeyAndHeaders(model);
|
|
166
|
-
return result.ok ? { apiKey: result.apiKey, headers: result.headers } : {};
|
|
167
|
-
}
|
|
168
265
|
/**
|
|
169
266
|
* Install tool hooks once on the Agent instance.
|
|
170
267
|
*
|
|
@@ -243,6 +340,7 @@ export class AgentSession {
|
|
|
243
340
|
if (event.type === "message_start" && event.message.role === "user") {
|
|
244
341
|
this._overflowRecoveryAttempted = false;
|
|
245
342
|
const messageText = this._getUserMessageText(event.message);
|
|
343
|
+
this._handleUserMessageStart(messageText, event.message);
|
|
246
344
|
if (messageText) {
|
|
247
345
|
// Check steering queue first
|
|
248
346
|
const steeringIndex = this._steeringMessages.indexOf(messageText);
|
|
@@ -266,17 +364,7 @@ export class AgentSession {
|
|
|
266
364
|
this._emit(event.type === "agent_end" ? { ...event, willRetry: this._willRetryAfterAgentEnd(event) } : event);
|
|
267
365
|
// Handle session persistence
|
|
268
366
|
if (event.type === "message_end") {
|
|
269
|
-
|
|
270
|
-
if (event.message.role === "custom") {
|
|
271
|
-
// Persist as CustomMessageEntry
|
|
272
|
-
this.sessionManager.appendCustomMessageEntry(event.message.customType, event.message.content, event.message.display, event.message.details);
|
|
273
|
-
}
|
|
274
|
-
else if (event.message.role === "user" ||
|
|
275
|
-
event.message.role === "assistant" ||
|
|
276
|
-
event.message.role === "toolResult") {
|
|
277
|
-
// Regular LLM message - persist as SessionMessageEntry
|
|
278
|
-
this.sessionManager.appendMessage(event.message);
|
|
279
|
-
}
|
|
367
|
+
this._persistMessageEnd(event.message);
|
|
280
368
|
// Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere
|
|
281
369
|
// Track assistant message for auto-compaction (checked on agent_end)
|
|
282
370
|
if (event.message.role === "assistant") {
|
|
@@ -298,6 +386,43 @@ export class AgentSession {
|
|
|
298
386
|
}
|
|
299
387
|
}
|
|
300
388
|
};
|
|
389
|
+
_persistMessageEnd(message) {
|
|
390
|
+
// Check if this is a custom message from extensions
|
|
391
|
+
if (message.role === "custom") {
|
|
392
|
+
// Persist as CustomMessageEntry
|
|
393
|
+
this.sessionManager.appendCustomMessageEntry(message.customType, message.content, message.display, message.details);
|
|
394
|
+
}
|
|
395
|
+
else if ((message.role === "user" && !this._shouldSkipMessagePersistence(message)) ||
|
|
396
|
+
message.role === "assistant" ||
|
|
397
|
+
message.role === "toolResult") {
|
|
398
|
+
// Regular LLM message - persist as SessionMessageEntry
|
|
399
|
+
this.sessionManager.appendMessage(message);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
_handleUserMessageStart(messageText, message) {
|
|
403
|
+
const isInternalLengthContinuation = messageText === LENGTH_CONTINUATION_PROMPT && this._pendingInternalLengthContinuationMessages > 0;
|
|
404
|
+
if (isInternalLengthContinuation) {
|
|
405
|
+
this._pendingInternalLengthContinuationMessages--;
|
|
406
|
+
this._internalLengthContinuationUserMessages.add(message);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
this._resetLengthContinuationState();
|
|
410
|
+
if (this._autoOlderHistoryCooldownTurnsRemaining > 0) {
|
|
411
|
+
this._autoOlderHistoryCooldownTurnsRemaining--;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
_resetLengthContinuationState() {
|
|
415
|
+
this._lengthContinuationAttempts = 0;
|
|
416
|
+
this._lengthContinuationOutputTokens = 0;
|
|
417
|
+
}
|
|
418
|
+
_shouldSkipMessagePersistence(message) {
|
|
419
|
+
if (message.role !== "user")
|
|
420
|
+
return false;
|
|
421
|
+
if (!this._internalLengthContinuationUserMessages.has(message))
|
|
422
|
+
return false;
|
|
423
|
+
this._internalLengthContinuationUserMessages.delete(message);
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
301
426
|
_willRetryAfterAgentEnd(event) {
|
|
302
427
|
const settings = this.settingsManager.getRetrySettings();
|
|
303
428
|
if (!settings.enabled || this._retryAttempt >= settings.maxRetries) {
|
|
@@ -707,8 +832,81 @@ export class AgentSession {
|
|
|
707
832
|
});
|
|
708
833
|
this._retryAttempt = 0;
|
|
709
834
|
}
|
|
835
|
+
if (this._shouldAutoContinueLengthStop(msg)) {
|
|
836
|
+
this._queueInternalLengthContinuation(this._getAssistantOutputTokens(msg));
|
|
837
|
+
return true;
|
|
838
|
+
}
|
|
710
839
|
return await this._checkCompaction(msg);
|
|
711
840
|
}
|
|
841
|
+
_getCompactionPolicy() {
|
|
842
|
+
const manager = this.settingsManager;
|
|
843
|
+
return manager.getCompactionPolicy?.() ?? normalizeCompactionPolicy(manager.getCompactionSettings());
|
|
844
|
+
}
|
|
845
|
+
_shouldAutoContinueLengthStop(message) {
|
|
846
|
+
if (message.stopReason !== "length")
|
|
847
|
+
return false;
|
|
848
|
+
const policy = this._getCompactionPolicy().truncationContinuation;
|
|
849
|
+
const outputTokens = this._getAssistantOutputTokens(message);
|
|
850
|
+
if (this._lengthContinuationAttempts > 0) {
|
|
851
|
+
this._lengthContinuationOutputTokens += outputTokens;
|
|
852
|
+
}
|
|
853
|
+
const contextWindow = this.model?.contextWindow ?? 0;
|
|
854
|
+
if (isContextOverflow(message, contextWindow)) {
|
|
855
|
+
this._emitLengthContinuationEvent("skipped", "context_overflow", outputTokens, policy);
|
|
856
|
+
return false;
|
|
857
|
+
}
|
|
858
|
+
if (outputTokens <= 0 || !this._assistantMessageHasVisibleOutput(message)) {
|
|
859
|
+
this._emitLengthContinuationEvent("skipped", "empty_output", outputTokens, policy);
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
if (this._lengthContinuationAttempts >= policy.maxAttempts) {
|
|
863
|
+
this._emitLengthContinuationEvent("skipped", "max_attempts", outputTokens, policy);
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
if (this._lengthContinuationOutputTokens >= policy.totalOutputTokenBudget) {
|
|
867
|
+
this._emitLengthContinuationEvent("skipped", "budget_exhausted", outputTokens, policy);
|
|
868
|
+
return false;
|
|
869
|
+
}
|
|
870
|
+
return true;
|
|
871
|
+
}
|
|
872
|
+
_getAssistantOutputTokens(message) {
|
|
873
|
+
const outputTokens = message.usage.output;
|
|
874
|
+
if (!Number.isFinite(outputTokens) || outputTokens <= 0)
|
|
875
|
+
return 0;
|
|
876
|
+
return Math.trunc(outputTokens);
|
|
877
|
+
}
|
|
878
|
+
_emitLengthContinuationEvent(status, reason, outputTokens, policy) {
|
|
879
|
+
this._emit({
|
|
880
|
+
type: "length_continuation",
|
|
881
|
+
status,
|
|
882
|
+
reason,
|
|
883
|
+
attempt: this._lengthContinuationAttempts,
|
|
884
|
+
maxAttempts: policy.maxAttempts,
|
|
885
|
+
outputTokens,
|
|
886
|
+
continuationOutputTokens: this._lengthContinuationOutputTokens,
|
|
887
|
+
totalOutputTokenBudget: policy.totalOutputTokenBudget,
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
_assistantMessageHasVisibleOutput(message) {
|
|
891
|
+
return message.content.some((part) => {
|
|
892
|
+
if (part.type === "text")
|
|
893
|
+
return part.text.trim().length > 0;
|
|
894
|
+
if (part.type === "thinking")
|
|
895
|
+
return part.thinking.trim().length > 0;
|
|
896
|
+
return false;
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
_queueInternalLengthContinuation(outputTokens = 0) {
|
|
900
|
+
const policy = this._getCompactionPolicy().truncationContinuation;
|
|
901
|
+
this._lengthContinuationAttempts++;
|
|
902
|
+
this.agent.followUp({
|
|
903
|
+
role: "user",
|
|
904
|
+
content: [{ type: "text", text: LENGTH_CONTINUATION_PROMPT }],
|
|
905
|
+
timestamp: Date.now(),
|
|
906
|
+
});
|
|
907
|
+
this._pendingInternalLengthContinuationMessages++;
|
|
908
|
+
this._emitLengthContinuationEvent("queued", "length_stop", outputTokens, policy);
|
|
909
|
+
}
|
|
712
910
|
/**
|
|
713
911
|
* Send a prompt to the agent.
|
|
714
912
|
* - Handles extension commands (registered via spectral.registerCommand) immediately, even during streaming
|
|
@@ -1296,25 +1494,94 @@ export class AgentSession {
|
|
|
1296
1494
|
* Aborts current agent operation first.
|
|
1297
1495
|
* @param customInstructions Optional instructions for the compaction summary
|
|
1298
1496
|
*/
|
|
1299
|
-
async compact(
|
|
1300
|
-
return this._withCompactionLock(() => this._executeCompaction(
|
|
1497
|
+
async compact(options) {
|
|
1498
|
+
return this._withCompactionLock(() => this._executeCompaction(options));
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Build the shared deterministic DCP compaction result and stream the bounded
|
|
1502
|
+
* summary through the same progress event used by compaction previews.
|
|
1503
|
+
*/
|
|
1504
|
+
_buildDcpCompactionResult(preparation, extensionCompaction) {
|
|
1505
|
+
return preserveDcpHookMemoryDetails(compactDcpLite(preparation), extensionCompaction);
|
|
1506
|
+
}
|
|
1507
|
+
_emitCompactionResultSummary(result) {
|
|
1508
|
+
if (result.summary) {
|
|
1509
|
+
this._emit({
|
|
1510
|
+
type: "compaction_delta",
|
|
1511
|
+
delta: result.summary,
|
|
1512
|
+
content: result.summary,
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
_markIfNonShrinking(result) {
|
|
1517
|
+
const message = getNonShrinkingSkipMessage(result);
|
|
1518
|
+
return message ? markCompactionSkipped(result, "non_shrinking", message) : undefined;
|
|
1519
|
+
}
|
|
1520
|
+
async _emitSessionBeforeCompact(preparation, branchEntries, customInstructions, signal, emitProgress) {
|
|
1521
|
+
const runner = this._extensionRunner;
|
|
1522
|
+
if (!runner?.hasHandlers("session_before_compact"))
|
|
1523
|
+
return undefined;
|
|
1524
|
+
return (await runner.emit({
|
|
1525
|
+
type: "session_before_compact",
|
|
1526
|
+
preparation,
|
|
1527
|
+
branchEntries,
|
|
1528
|
+
customInstructions,
|
|
1529
|
+
signal,
|
|
1530
|
+
emitProgress,
|
|
1531
|
+
}));
|
|
1301
1532
|
}
|
|
1302
1533
|
/**
|
|
1303
1534
|
* Execute a manual compaction under the compaction lock.
|
|
1304
1535
|
*/
|
|
1305
|
-
async _executeCompaction(
|
|
1536
|
+
async _executeCompaction(options) {
|
|
1537
|
+
const { customInstructions, phaseBoundary, keepRecentTokens, memoryHookMode } = normalizeManualCompactionOptions(options);
|
|
1306
1538
|
this._disconnectFromAgent();
|
|
1307
1539
|
await this.abort();
|
|
1308
1540
|
this._compactionAbortController = new AbortController();
|
|
1309
1541
|
this._emit({ type: "compaction_start", reason: "manual" });
|
|
1310
1542
|
try {
|
|
1311
|
-
|
|
1312
|
-
|
|
1543
|
+
let pathEntries = this.sessionManager.getBranch();
|
|
1544
|
+
const policy = this._getCompactionPolicy();
|
|
1545
|
+
let settings = getManualCompactionSettings(policy.conversation, policy.manual);
|
|
1546
|
+
if (typeof keepRecentTokens === "number" && Number.isFinite(keepRecentTokens) && keepRecentTokens > 0) {
|
|
1547
|
+
const normalizedKeepRecentTokens = normalizeManualKeepRecentTokens(keepRecentTokens, policy.manual);
|
|
1548
|
+
if (normalizedKeepRecentTokens > 0) {
|
|
1549
|
+
settings = { ...settings, keepRecentTokens: normalizedKeepRecentTokens };
|
|
1550
|
+
}
|
|
1313
1551
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1552
|
+
if (phaseBoundary && !isDcpLitePhaseBoundaryEntry(pathEntries[pathEntries.length - 1])) {
|
|
1553
|
+
const preflightEntries = [...pathEntries, createVirtualPhaseBoundaryEntry(pathEntries)];
|
|
1554
|
+
const preflightPreparation = prepareCompaction(preflightEntries, settings, { phaseBoundary: true });
|
|
1555
|
+
if (!preflightPreparation) {
|
|
1556
|
+
const lastEntry = pathEntries[pathEntries.length - 1];
|
|
1557
|
+
if (lastEntry?.type === "compaction") {
|
|
1558
|
+
throw new Error("Already compacted");
|
|
1559
|
+
}
|
|
1560
|
+
throw new Error("Nothing to compact (session too small)");
|
|
1561
|
+
}
|
|
1562
|
+
const preflightResult = this._buildDcpCompactionResult(preflightPreparation);
|
|
1563
|
+
const preflightSkippedResult = this._markIfNonShrinking(preflightResult);
|
|
1564
|
+
if (preflightSkippedResult) {
|
|
1565
|
+
this._emit({
|
|
1566
|
+
type: "compaction_end",
|
|
1567
|
+
reason: "manual",
|
|
1568
|
+
result: preflightSkippedResult,
|
|
1569
|
+
aborted: false,
|
|
1570
|
+
willRetry: false,
|
|
1571
|
+
errorMessage: preflightSkippedResult.message,
|
|
1572
|
+
skipped: true,
|
|
1573
|
+
skipReason: preflightSkippedResult.skipReason,
|
|
1574
|
+
});
|
|
1575
|
+
return preflightSkippedResult;
|
|
1576
|
+
}
|
|
1577
|
+
this.sessionManager.appendCustomMessageEntry(DCP_LITE_PHASE_BOUNDARY_CUSTOM_TYPE, DCP_LITE_PHASE_BOUNDARY_MARKER, false, {
|
|
1578
|
+
customInstructions,
|
|
1579
|
+
createdAt: new Date().toISOString(),
|
|
1580
|
+
reason: "model-visible-compact-context",
|
|
1581
|
+
});
|
|
1582
|
+
pathEntries = this.sessionManager.getBranch();
|
|
1583
|
+
}
|
|
1584
|
+
const preparation = prepareCompaction(pathEntries, settings, { phaseBoundary });
|
|
1318
1585
|
if (!preparation) {
|
|
1319
1586
|
// Check why we can't compact
|
|
1320
1587
|
const lastEntry = pathEntries[pathEntries.length - 1];
|
|
@@ -1324,97 +1591,44 @@ export class AgentSession {
|
|
|
1324
1591
|
throw new Error("Nothing to compact (session too small)");
|
|
1325
1592
|
}
|
|
1326
1593
|
let extensionCompaction;
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
this._emit({
|
|
1334
|
-
type: "compaction_delta",
|
|
1335
|
-
delta,
|
|
1336
|
-
content: extensionProgressContent,
|
|
1337
|
-
});
|
|
1338
|
-
};
|
|
1339
|
-
if (this._extensionRunner.hasHandlers("session_before_compact")) {
|
|
1340
|
-
const result = (await this._extensionRunner.emit({
|
|
1341
|
-
type: "session_before_compact",
|
|
1342
|
-
preparation,
|
|
1343
|
-
branchEntries: pathEntries,
|
|
1344
|
-
customInstructions,
|
|
1345
|
-
signal: this._compactionAbortController.signal,
|
|
1346
|
-
emitProgress: emitExtensionProgress,
|
|
1347
|
-
}));
|
|
1348
|
-
if (result?.cancel) {
|
|
1349
|
-
throw new Error("Compaction cancelled");
|
|
1350
|
-
}
|
|
1351
|
-
if (result?.compaction) {
|
|
1352
|
-
extensionCompaction = result.compaction;
|
|
1353
|
-
fromExtension = true;
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
let summary;
|
|
1357
|
-
let firstKeptEntryId;
|
|
1358
|
-
let tokensBefore;
|
|
1359
|
-
let details;
|
|
1360
|
-
if (extensionCompaction) {
|
|
1361
|
-
// Extension provided compaction content
|
|
1362
|
-
summary = extensionCompaction.summary;
|
|
1363
|
-
firstKeptEntryId = extensionCompaction.firstKeptEntryId;
|
|
1364
|
-
tokensBefore = extensionCompaction.tokensBefore;
|
|
1365
|
-
details = extensionCompaction.details;
|
|
1366
|
-
// Emit the full summary as a compaction_delta so the frontend
|
|
1367
|
-
// displays it in the live preview panel during compaction.
|
|
1368
|
-
if (summary) {
|
|
1594
|
+
if (memoryHookMode !== "skip") {
|
|
1595
|
+
let extensionProgressContent = "";
|
|
1596
|
+
const emitExtensionProgress = (delta, content) => {
|
|
1597
|
+
if (!delta && !content)
|
|
1598
|
+
return;
|
|
1599
|
+
extensionProgressContent = content ?? `${extensionProgressContent}${delta}`;
|
|
1369
1600
|
this._emit({
|
|
1370
1601
|
type: "compaction_delta",
|
|
1371
|
-
delta
|
|
1372
|
-
content:
|
|
1602
|
+
delta,
|
|
1603
|
+
content: extensionProgressContent,
|
|
1373
1604
|
});
|
|
1605
|
+
};
|
|
1606
|
+
const beforeCompactResult = await this._emitSessionBeforeCompact(preparation, pathEntries, customInstructions, this._compactionAbortController.signal, emitExtensionProgress);
|
|
1607
|
+
if (!beforeCompactResult?.cancel && beforeCompactResult?.compaction) {
|
|
1608
|
+
extensionCompaction = beforeCompactResult.compaction;
|
|
1374
1609
|
}
|
|
1375
1610
|
}
|
|
1376
|
-
|
|
1377
|
-
// Wrap streamFn to emit compaction_delta events for live progress.
|
|
1378
|
-
const originalStreamFn = this.agent.streamFn;
|
|
1379
|
-
const wrappedStreamFn = originalStreamFn
|
|
1380
|
-
? async (model, context, options) => {
|
|
1381
|
-
const originalStream = await originalStreamFn(model, context, options);
|
|
1382
|
-
const wrapperStream = new AssistantMessageEventStream();
|
|
1383
|
-
let compactionContent = "";
|
|
1384
|
-
(async () => {
|
|
1385
|
-
try {
|
|
1386
|
-
for await (const event of originalStream) {
|
|
1387
|
-
wrapperStream.push(event);
|
|
1388
|
-
if (event.type === "text_delta") {
|
|
1389
|
-
compactionContent += event.delta;
|
|
1390
|
-
this._emit({
|
|
1391
|
-
type: "compaction_delta",
|
|
1392
|
-
delta: event.delta,
|
|
1393
|
-
content: compactionContent,
|
|
1394
|
-
});
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
wrapperStream.end();
|
|
1398
|
-
}
|
|
1399
|
-
catch (err) {
|
|
1400
|
-
// If the original stream errors, forward the error.
|
|
1401
|
-
wrapperStream.end();
|
|
1402
|
-
}
|
|
1403
|
-
})();
|
|
1404
|
-
return wrapperStream;
|
|
1405
|
-
}
|
|
1406
|
-
: undefined;
|
|
1407
|
-
// Generate compaction result
|
|
1408
|
-
const result = await compact(preparation, this.model, apiKey, headers, customInstructions, this._compactionAbortController.signal, this.thinkingLevel, wrappedStreamFn);
|
|
1409
|
-
summary = result.summary;
|
|
1410
|
-
firstKeptEntryId = result.firstKeptEntryId;
|
|
1411
|
-
tokensBefore = result.tokensBefore;
|
|
1412
|
-
details = result.details;
|
|
1413
|
-
}
|
|
1611
|
+
const compactResult = this._buildDcpCompactionResult(preparation, extensionCompaction);
|
|
1414
1612
|
if (this._compactionAbortController.signal.aborted) {
|
|
1415
1613
|
throw new Error("Compaction cancelled");
|
|
1416
1614
|
}
|
|
1417
|
-
this.
|
|
1615
|
+
const skippedResult = this._markIfNonShrinking(compactResult);
|
|
1616
|
+
if (skippedResult) {
|
|
1617
|
+
this._emit({
|
|
1618
|
+
type: "compaction_end",
|
|
1619
|
+
reason: "manual",
|
|
1620
|
+
result: skippedResult,
|
|
1621
|
+
aborted: false,
|
|
1622
|
+
willRetry: false,
|
|
1623
|
+
errorMessage: skippedResult.message,
|
|
1624
|
+
skipped: true,
|
|
1625
|
+
skipReason: skippedResult.skipReason,
|
|
1626
|
+
});
|
|
1627
|
+
return skippedResult;
|
|
1628
|
+
}
|
|
1629
|
+
this._emitCompactionResultSummary(compactResult);
|
|
1630
|
+
const { summary, firstKeptEntryId, tokensBefore, details } = compactResult;
|
|
1631
|
+
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, false);
|
|
1418
1632
|
const newEntries = this.sessionManager.getEntries();
|
|
1419
1633
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
1420
1634
|
this.agent.state.messages = sessionContext.messages;
|
|
@@ -1424,15 +1638,10 @@ export class AgentSession {
|
|
|
1424
1638
|
await this._extensionRunner.emit({
|
|
1425
1639
|
type: "session_compact",
|
|
1426
1640
|
compactionEntry: savedCompactionEntry,
|
|
1427
|
-
fromExtension,
|
|
1641
|
+
fromExtension: false,
|
|
1428
1642
|
});
|
|
1429
1643
|
}
|
|
1430
|
-
const compactionResult = {
|
|
1431
|
-
summary,
|
|
1432
|
-
firstKeptEntryId,
|
|
1433
|
-
tokensBefore,
|
|
1434
|
-
details,
|
|
1435
|
-
};
|
|
1644
|
+
const compactionResult = { ...compactResult, details };
|
|
1436
1645
|
this._emit({
|
|
1437
1646
|
type: "compaction_end",
|
|
1438
1647
|
reason: "manual",
|
|
@@ -1485,8 +1694,8 @@ export class AgentSession {
|
|
|
1485
1694
|
* @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
|
|
1486
1695
|
*/
|
|
1487
1696
|
async _checkCompaction(assistantMessage, skipAbortedCheck = true) {
|
|
1488
|
-
const
|
|
1489
|
-
if (!
|
|
1697
|
+
const policy = this._getCompactionPolicy();
|
|
1698
|
+
if (!this._isAutoOlderHistoryCompactionEnabled(policy))
|
|
1490
1699
|
return false;
|
|
1491
1700
|
// Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
|
|
1492
1701
|
if (skipAbortedCheck && assistantMessage.stopReason === "aborted")
|
|
@@ -1550,15 +1759,37 @@ export class AgentSession {
|
|
|
1550
1759
|
else {
|
|
1551
1760
|
contextTokens = calculateContextTokens(assistantMessage.usage);
|
|
1552
1761
|
}
|
|
1553
|
-
if (shouldCompact(contextTokens, contextWindow,
|
|
1762
|
+
if (shouldCompact(contextTokens, contextWindow, this._getAutoOlderHistoryCompactionSettings(policy))) {
|
|
1554
1763
|
return await this._runAutoCompaction("threshold", false);
|
|
1555
1764
|
}
|
|
1556
1765
|
return false;
|
|
1557
1766
|
}
|
|
1767
|
+
_getAutoOlderHistoryCompactionSettings(policy) {
|
|
1768
|
+
return {
|
|
1769
|
+
...policy.conversation,
|
|
1770
|
+
enabled: this._isAutoOlderHistoryCompactionEnabled(policy),
|
|
1771
|
+
keepRecentTokens: policy.autoOlderHistory.keepRecentTokens,
|
|
1772
|
+
contextLimitRatio: policy.autoOlderHistory.contextLimitRatio,
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
_isAutoOlderHistoryCompactionEnabled(policy) {
|
|
1776
|
+
return policy.conversation.enabled && policy.autoOlderHistory.enabled;
|
|
1777
|
+
}
|
|
1778
|
+
_autoOlderHistorySkipSignature(pathEntries, compactableTokens) {
|
|
1779
|
+
const lastEntryId = pathEntries[pathEntries.length - 1]?.id ?? "none";
|
|
1780
|
+
return `${lastEntryId}:${compactableTokens}`;
|
|
1781
|
+
}
|
|
1558
1782
|
/**
|
|
1559
1783
|
* Internal: Run auto-compaction with events.
|
|
1560
1784
|
*/
|
|
1561
1785
|
async _runAutoCompaction(reason, willRetry) {
|
|
1786
|
+
const policy = this._getCompactionPolicy();
|
|
1787
|
+
if (!this._isAutoOlderHistoryCompactionEnabled(policy)) {
|
|
1788
|
+
return false;
|
|
1789
|
+
}
|
|
1790
|
+
if (reason === "threshold" && this._autoOlderHistoryCooldownTurnsRemaining > 0) {
|
|
1791
|
+
return false;
|
|
1792
|
+
}
|
|
1562
1793
|
if (this._compactionInProgress) {
|
|
1563
1794
|
this._emit({
|
|
1564
1795
|
type: "compaction_end",
|
|
@@ -1570,11 +1801,13 @@ export class AgentSession {
|
|
|
1570
1801
|
return false;
|
|
1571
1802
|
}
|
|
1572
1803
|
this._compactionInProgress = true;
|
|
1573
|
-
const settings = this.
|
|
1804
|
+
const settings = this._getAutoOlderHistoryCompactionSettings(policy);
|
|
1574
1805
|
this._emit({ type: "compaction_start", reason });
|
|
1575
1806
|
this._autoCompactionAbortController = new AbortController();
|
|
1576
1807
|
try {
|
|
1577
|
-
|
|
1808
|
+
const pathEntries = this.sessionManager.getBranch();
|
|
1809
|
+
const preparation = prepareCompaction(pathEntries, settings, { phaseBoundary: false });
|
|
1810
|
+
if (!preparation) {
|
|
1578
1811
|
this._emit({
|
|
1579
1812
|
type: "compaction_end",
|
|
1580
1813
|
reason,
|
|
@@ -1584,40 +1817,34 @@ export class AgentSession {
|
|
|
1584
1817
|
});
|
|
1585
1818
|
return false;
|
|
1586
1819
|
}
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
apiKey = authResult.apiKey;
|
|
1602
|
-
headers = authResult.headers;
|
|
1603
|
-
}
|
|
1604
|
-
else {
|
|
1605
|
-
({ apiKey, headers } = await this._getCompactionRequestAuth(this.model));
|
|
1820
|
+
const compactableTokens = estimateCompactionTokenMetrics(preparation, "").tokensCompacted ?? 0;
|
|
1821
|
+
if (compactableTokens < policy.autoOlderHistory.minRawTokens) {
|
|
1822
|
+
this._lastAutoOlderHistorySkipSignature = this._autoOlderHistorySkipSignature(pathEntries, compactableTokens);
|
|
1823
|
+
this._autoOlderHistoryCooldownTurnsRemaining = policy.autoOlderHistory.cooldownTurns;
|
|
1824
|
+
this._emit({
|
|
1825
|
+
type: "compaction_end",
|
|
1826
|
+
reason,
|
|
1827
|
+
result: undefined,
|
|
1828
|
+
aborted: false,
|
|
1829
|
+
willRetry: false,
|
|
1830
|
+
errorMessage: `Auto-compaction skipped: insufficient compactable raw material (` +
|
|
1831
|
+
`~${compactableTokens.toLocaleString()} / ${policy.autoOlderHistory.minRawTokens.toLocaleString()} tokens).`,
|
|
1832
|
+
});
|
|
1833
|
+
return false;
|
|
1606
1834
|
}
|
|
1607
|
-
const
|
|
1608
|
-
|
|
1609
|
-
|
|
1835
|
+
const skipSignature = this._autoOlderHistorySkipSignature(pathEntries, compactableTokens);
|
|
1836
|
+
if (reason === "threshold" && this._lastAutoOlderHistorySkipSignature === skipSignature) {
|
|
1837
|
+
this._autoOlderHistoryCooldownTurnsRemaining = policy.autoOlderHistory.cooldownTurns;
|
|
1610
1838
|
this._emit({
|
|
1611
1839
|
type: "compaction_end",
|
|
1612
1840
|
reason,
|
|
1613
1841
|
result: undefined,
|
|
1614
1842
|
aborted: false,
|
|
1615
1843
|
willRetry: false,
|
|
1844
|
+
errorMessage: "Auto-compaction skipped: repeated non-shrinking older-history attempt without new raw material.",
|
|
1616
1845
|
});
|
|
1617
1846
|
return false;
|
|
1618
1847
|
}
|
|
1619
|
-
let extensionCompaction;
|
|
1620
|
-
let fromExtension = false;
|
|
1621
1848
|
let extensionProgressContent = "";
|
|
1622
1849
|
const emitExtensionProgress = (delta, content) => {
|
|
1623
1850
|
if (!delta && !content)
|
|
@@ -1629,58 +1856,9 @@ export class AgentSession {
|
|
|
1629
1856
|
content: extensionProgressContent,
|
|
1630
1857
|
});
|
|
1631
1858
|
};
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
preparation,
|
|
1636
|
-
branchEntries: pathEntries,
|
|
1637
|
-
customInstructions: undefined,
|
|
1638
|
-
signal: this._autoCompactionAbortController.signal,
|
|
1639
|
-
emitProgress: emitExtensionProgress,
|
|
1640
|
-
}));
|
|
1641
|
-
if (extensionResult?.cancel) {
|
|
1642
|
-
this._emit({
|
|
1643
|
-
type: "compaction_end",
|
|
1644
|
-
reason,
|
|
1645
|
-
result: undefined,
|
|
1646
|
-
aborted: true,
|
|
1647
|
-
willRetry: false,
|
|
1648
|
-
});
|
|
1649
|
-
return false;
|
|
1650
|
-
}
|
|
1651
|
-
if (extensionResult?.compaction) {
|
|
1652
|
-
extensionCompaction = extensionResult.compaction;
|
|
1653
|
-
fromExtension = true;
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
let summary;
|
|
1657
|
-
let firstKeptEntryId;
|
|
1658
|
-
let tokensBefore;
|
|
1659
|
-
let details;
|
|
1660
|
-
if (extensionCompaction) {
|
|
1661
|
-
// Extension provided compaction content
|
|
1662
|
-
summary = extensionCompaction.summary;
|
|
1663
|
-
firstKeptEntryId = extensionCompaction.firstKeptEntryId;
|
|
1664
|
-
tokensBefore = extensionCompaction.tokensBefore;
|
|
1665
|
-
details = extensionCompaction.details;
|
|
1666
|
-
// Emit the full summary as a compaction_delta so the frontend
|
|
1667
|
-
// displays it in the live preview panel during compaction.
|
|
1668
|
-
if (summary) {
|
|
1669
|
-
this._emit({
|
|
1670
|
-
type: "compaction_delta",
|
|
1671
|
-
delta: summary,
|
|
1672
|
-
content: summary,
|
|
1673
|
-
});
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
else {
|
|
1677
|
-
// Generate compaction result
|
|
1678
|
-
const compactResult = await compact(preparation, this.model, apiKey, headers, undefined, this._autoCompactionAbortController.signal, this.thinkingLevel, this.agent.streamFn);
|
|
1679
|
-
summary = compactResult.summary;
|
|
1680
|
-
firstKeptEntryId = compactResult.firstKeptEntryId;
|
|
1681
|
-
tokensBefore = compactResult.tokensBefore;
|
|
1682
|
-
details = compactResult.details;
|
|
1683
|
-
}
|
|
1859
|
+
const beforeCompactResult = await this._emitSessionBeforeCompact(preparation, pathEntries, undefined, this._autoCompactionAbortController.signal, emitExtensionProgress);
|
|
1860
|
+
const extensionCompaction = beforeCompactResult?.cancel ? undefined : beforeCompactResult?.compaction;
|
|
1861
|
+
const compactResult = this._buildDcpCompactionResult(preparation, extensionCompaction);
|
|
1684
1862
|
if (this._autoCompactionAbortController.signal.aborted) {
|
|
1685
1863
|
this._emit({
|
|
1686
1864
|
type: "compaction_end",
|
|
@@ -1691,7 +1869,27 @@ export class AgentSession {
|
|
|
1691
1869
|
});
|
|
1692
1870
|
return false;
|
|
1693
1871
|
}
|
|
1694
|
-
this.
|
|
1872
|
+
const skippedResult = this._markIfNonShrinking(compactResult);
|
|
1873
|
+
if (skippedResult) {
|
|
1874
|
+
this._lastAutoOlderHistorySkipSignature = skipSignature;
|
|
1875
|
+
this._autoOlderHistoryCooldownTurnsRemaining = policy.autoOlderHistory.cooldownTurns;
|
|
1876
|
+
this._emit({
|
|
1877
|
+
type: "compaction_end",
|
|
1878
|
+
reason,
|
|
1879
|
+
result: skippedResult,
|
|
1880
|
+
aborted: false,
|
|
1881
|
+
willRetry: false,
|
|
1882
|
+
errorMessage: skippedResult.message,
|
|
1883
|
+
skipped: true,
|
|
1884
|
+
skipReason: skippedResult.skipReason,
|
|
1885
|
+
});
|
|
1886
|
+
return false;
|
|
1887
|
+
}
|
|
1888
|
+
this._lastAutoOlderHistorySkipSignature = undefined;
|
|
1889
|
+
this._autoOlderHistoryCooldownTurnsRemaining = policy.autoOlderHistory.cooldownTurns;
|
|
1890
|
+
this._emitCompactionResultSummary(compactResult);
|
|
1891
|
+
const { summary, firstKeptEntryId, tokensBefore, details } = compactResult;
|
|
1892
|
+
this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, false);
|
|
1695
1893
|
const newEntries = this.sessionManager.getEntries();
|
|
1696
1894
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
1697
1895
|
this.agent.state.messages = sessionContext.messages;
|
|
@@ -1701,15 +1899,10 @@ export class AgentSession {
|
|
|
1701
1899
|
await this._extensionRunner.emit({
|
|
1702
1900
|
type: "session_compact",
|
|
1703
1901
|
compactionEntry: savedCompactionEntry,
|
|
1704
|
-
fromExtension,
|
|
1902
|
+
fromExtension: false,
|
|
1705
1903
|
});
|
|
1706
1904
|
}
|
|
1707
|
-
const result = {
|
|
1708
|
-
summary,
|
|
1709
|
-
firstKeptEntryId,
|
|
1710
|
-
tokensBefore,
|
|
1711
|
-
details,
|
|
1712
|
-
};
|
|
1905
|
+
const result = { ...compactResult, details };
|
|
1713
1906
|
this._emit({ type: "compaction_end", reason, result, aborted: false, willRetry });
|
|
1714
1907
|
if (willRetry) {
|
|
1715
1908
|
const messages = this.agent.state.messages;
|
|
@@ -1917,7 +2110,12 @@ export class AgentSession {
|
|
|
1917
2110
|
compact: (options) => {
|
|
1918
2111
|
void (async () => {
|
|
1919
2112
|
try {
|
|
1920
|
-
const result = await this.compact(
|
|
2113
|
+
const result = await this.compact({
|
|
2114
|
+
customInstructions: options?.customInstructions,
|
|
2115
|
+
phaseBoundary: options?.phaseBoundary,
|
|
2116
|
+
keepRecentTokens: options?.keepRecentTokens,
|
|
2117
|
+
memoryHookMode: options?.memoryHookMode,
|
|
2118
|
+
});
|
|
1921
2119
|
options?.onComplete?.(result);
|
|
1922
2120
|
}
|
|
1923
2121
|
catch (error) {
|