newmark-agent 0.3.7 → 0.3.8
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/dist/conversation-utility-host.bundle.cjs +188 -4
- package/dist/core/agent.d.ts +16 -0
- package/dist/core/agent.js +192 -1
- package/dist/core/config.js +2 -0
- package/dist/tools/index.js +6 -3
- package/dist/ui/index.html +14 -3
- package/dist/wsl-agent-host.bundle.cjs +188 -4
- package/package.json +1 -1
|
@@ -327819,6 +327819,8 @@ function defaultConfig() {
|
|
|
327819
327819
|
auto_compress: { _description: "Auto-compress history", _type: "boolean", value: true },
|
|
327820
327820
|
compress_threshold_chars: { _description: "Compression threshold", _type: "integer", value: 8e4 },
|
|
327821
327821
|
keep_recent_messages: { _description: "Keep recent messages", _type: "integer", value: 10 },
|
|
327822
|
+
preserve_recent_messages: { _description: "dev-0.3.8 protected recent-message zone for context_history_manage (0 disables)", _type: "integer", value: 5 },
|
|
327823
|
+
compression_cache_max: { _description: "dev-0.3.8 max folded-segment cache entries retained for restore/search", _type: "integer", value: 8 },
|
|
327822
327824
|
structured_context_v2: { _description: "dev-0.3.0 structured context v2 (orchestrator + fixed order + snapshot)", _type: "boolean", value: true },
|
|
327823
327825
|
build_history_persistence: { _description: "dev-0.3.0 append-only Build History persistence", _type: "boolean", value: true },
|
|
327824
327826
|
branch_log_v2: { _description: "dev-0.3.0 branch long-log v2 (epoch summaries)", _type: "boolean", value: true },
|
|
@@ -335535,11 +335537,14 @@ var ToolExecutor = class {
|
|
|
335535
335537
|
t3("linked_plan", "Read or update the current conversation linked Markdown plan. Update requires the current expected_revision.", { action: { type: "string", enum: ["get", "update"] }, markdown: { type: "string" }, expected_revision: { type: "number" } }, ["action"]),
|
|
335536
335538
|
t3("build_history_query", "Read the concrete public work details of one historical Build Block. The prompt already exposes user input, final summary, and completion status; call this read-only tool only when the user asks what specifically happened. Select by newest-to-oldest history_index, or by run_id returned from an earlier query.", { history_index: { type: "number", minimum: 1, description: "1-based historical Build Block index from the request ledger; 1 is the newest previous task." }, run_id: { type: "string", description: "Exact run id returned by an earlier build_history_query result." }, max_events: { type: "number", minimum: 1, maximum: 200, description: "Maximum trailing public work events; defaults to 80." } }, []),
|
|
335537
335539
|
t3("context_compress", "Actively compress the LLM context history for this conversation. This collapses older history entries into a concise summary while preserving the recent tail, which reduces context tokens and cost. IMPORTANT: it affects only the LLM context (what the model sees); the displayed conversation history shown to the user is never altered. Call this when the conversation is long, token pressure is high, or you judge that older turns are no longer needed in full. Idempotent and safe: repeated calls produce incremental summaries.", { keep_recent: { type: "number", minimum: 2, maximum: 60, description: "Recent message count to keep uncompressed at the tail. Defaults to the configured keep_recent_messages." }, force: { type: "boolean", description: "Compress even if the context is not yet over the automatic threshold. Defaults to false." } }, []),
|
|
335538
|
-
t3("context_history_manage", "Manage the LLM context history for this conversation without affecting the displayed conversation history. This is the active context-management surface. Actions: list returns a bounded index of context entries (position, role, name, length, first-line preview); remove deletes the entry at a given position; summarize replaces a contiguous range of context entries with a concise local summary entry. The displayed conversation history (what the user sees) is never modified by any action.", {
|
|
335539
|
-
action: { type: "string", enum: ["list", "remove", "summarize"], description: "list: index context entries. remove: delete the entry at position. summarize: fold entries [from, to] into one summary entry." },
|
|
335540
|
+
t3("context_history_manage", "Manage the LLM context history for this conversation without affecting the displayed conversation history. This is the active context-management surface. Actions: list returns a bounded index of context entries (position, role, name, length, first-line preview); remove deletes the entry at a given position; summarize replaces a contiguous range of context entries with a concise local summary entry; restore reinserts the original messages of a folded segment from the compression cache using restore_id; search finds which cached folded segments contain a query (and the matching lines); status reports usage vs trigger/target budgets, the last compression, the compression cache, and the protected recent-message zone. The displayed conversation history (what the user sees) is never modified by any action. The recent context tail and the last user message are protected from remove/summarize unless dangerous is true.", {
|
|
335541
|
+
action: { type: "string", enum: ["list", "remove", "summarize", "restore", "search", "status"], description: "list: index context entries. remove: delete the entry at position. summarize: fold entries [from, to] into one summary entry. restore: reinsert cached original messages by restore_id. search: find folded cache entries containing query. status: report context usage, budgets, cache, and protected zone." },
|
|
335540
335542
|
position: { type: "number", minimum: 0, description: "0-based context entry index for remove, or the start of the range for summarize." },
|
|
335541
335543
|
to: { type: "number", minimum: 0, description: "0-based inclusive end of the range for summarize. Defaults to position." },
|
|
335542
|
-
limit: { type: "number", minimum: 5, maximum: 400, description: "Maximum context entries to list
|
|
335544
|
+
limit: { type: "number", minimum: 5, maximum: 400, description: "Maximum context entries to list (default 200), or search matches to return (default 20)." },
|
|
335545
|
+
restore_id: { type: "string", description: "Cache id of a folded segment (from search or status) to restore into context." },
|
|
335546
|
+
query: { type: "string", description: "Case-insensitive text to search for across cached folded segments and their summaries." },
|
|
335547
|
+
dangerous: { type: "boolean", description: "Set true to override the protected recent-message zone and allow removing/summarizing entries that include the recent context tail or the last user message." }
|
|
335543
335548
|
}, ["action"]),
|
|
335544
335549
|
t3("question", "Ask user a multiple-choice question", { questions: { type: "array" } }, ["questions"]),
|
|
335545
335550
|
t3("skill_download", "Download a skill", { name: { type: "string" }, source: { type: "string" } }, ["name", "source"]),
|
|
@@ -343418,6 +343423,8 @@ var Agent4 = class _Agent {
|
|
|
343418
343423
|
continuations = [];
|
|
343419
343424
|
activeConversationId = "default";
|
|
343420
343425
|
lastCompression = null;
|
|
343426
|
+
compressionCache = [];
|
|
343427
|
+
nextCompressionCacheId = 1;
|
|
343421
343428
|
workspaceConversations = /* @__PURE__ */ new Map();
|
|
343422
343429
|
isSubagentRuntime = false;
|
|
343423
343430
|
subagentName = "";
|
|
@@ -345879,6 +345886,7 @@ Review this persisted peer result and summarize or continue the parent task as n
|
|
|
345879
345886
|
this.workspaceConversations.set(key3, {
|
|
345880
345887
|
chatMessages: [...this.chatMessages],
|
|
345881
345888
|
history: [...this.history],
|
|
345889
|
+
compressionCache: [...this.compressionCache],
|
|
345882
345890
|
plan: this.normalizeConversationPlan(this.conversationPlan),
|
|
345883
345891
|
linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
|
|
345884
345892
|
subagentState: this.subagents.serialize(),
|
|
@@ -345905,6 +345913,7 @@ Review this persisted peer result and summarize or continue the parent task as n
|
|
|
345905
345913
|
title,
|
|
345906
345914
|
chatMessages: [...this.chatMessages],
|
|
345907
345915
|
history: [...this.history],
|
|
345916
|
+
compressionCache: [...this.compressionCache],
|
|
345908
345917
|
plan: this.normalizeConversationPlan(this.conversationPlan),
|
|
345909
345918
|
linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
|
|
345910
345919
|
subagentState: this.subagents.serialize(),
|
|
@@ -345932,6 +345941,8 @@ Review this persisted peer result and summarize or continue the parent task as n
|
|
|
345932
345941
|
if (!key3) {
|
|
345933
345942
|
this.chatMessages = [];
|
|
345934
345943
|
this.history = [];
|
|
345944
|
+
this.compressionCache = [];
|
|
345945
|
+
this.nextCompressionCacheId = 1;
|
|
345935
345946
|
this.conversationPlan = { items: [] };
|
|
345936
345947
|
this.linkedPlan = { markdown: "", revision: 0 };
|
|
345937
345948
|
this.workRuns = [];
|
|
@@ -345948,6 +345959,8 @@ Review this persisted peer result and summarize or continue the parent task as n
|
|
|
345948
345959
|
const saved = this.workspaceConversations.get(key3);
|
|
345949
345960
|
if (saved) {
|
|
345950
345961
|
this.history = [...saved.history];
|
|
345962
|
+
this.compressionCache = saved.compressionCache ? saved.compressionCache.map((entry) => ({ ...entry, messages: [...entry.messages] })) : [];
|
|
345963
|
+
this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map((entry) => Number(entry.id.replace(/^ctx-cache-/, "")) || 0)) + 1;
|
|
345951
345964
|
this.chatMessages = this.normalizeConversationChatMessages(saved.chatMessages, this.history);
|
|
345952
345965
|
this.conversationPlan = this.normalizeConversationPlan(saved.plan);
|
|
345953
345966
|
this.linkedPlan = this.normalizeLinkedPlan(saved.linkedPlan);
|
|
@@ -345967,6 +345980,8 @@ Review this persisted peer result and summarize or continue the parent task as n
|
|
|
345967
345980
|
const stateKey = this.workspaceConversationStateKey();
|
|
345968
345981
|
const persisted = stateKey && stored.conversations ? stored.conversations[stateKey] : null;
|
|
345969
345982
|
this.history = persisted?.history ? [...persisted.history] : [];
|
|
345983
|
+
this.compressionCache = persisted?.compressionCache ? persisted.compressionCache.map((entry) => ({ ...entry, messages: [...entry.messages] })) : [];
|
|
345984
|
+
this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map((entry) => Number(entry.id.replace(/^ctx-cache-/, "")) || 0)) + 1;
|
|
345970
345985
|
this.chatMessages = this.normalizeConversationChatMessages(persisted?.chatMessages || [], this.history);
|
|
345971
345986
|
this.conversationPlan = this.normalizeConversationPlan(persisted?.plan);
|
|
345972
345987
|
this.linkedPlan = this.normalizeLinkedPlan(persisted?.linkedPlan);
|
|
@@ -345984,6 +345999,7 @@ Review this persisted peer result and summarize or continue the parent task as n
|
|
|
345984
345999
|
this.workspaceConversations.set(key3, {
|
|
345985
346000
|
chatMessages: [...this.chatMessages],
|
|
345986
346001
|
history: [...this.history],
|
|
346002
|
+
compressionCache: [...this.compressionCache],
|
|
345987
346003
|
plan: this.normalizeConversationPlan(this.conversationPlan),
|
|
345988
346004
|
linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
|
|
345989
346005
|
subagentState: this.subagents.serialize(),
|
|
@@ -346210,7 +346226,7 @@ Review this persisted peer result and summarize or continue the parent task as n
|
|
|
346210
346226
|
} catch {
|
|
346211
346227
|
}
|
|
346212
346228
|
const action = String(input.action || "").trim();
|
|
346213
|
-
if (!action) return { ok: false, output: "[context_history_manage] action is required (list|remove|summarize).", error: "action is required." };
|
|
346229
|
+
if (!action) return { ok: false, output: "[context_history_manage] action is required (list|remove|summarize|restore|search|status).", error: "action is required." };
|
|
346214
346230
|
if (action === "list") {
|
|
346215
346231
|
const limit = Math.max(5, Math.min(400, Math.floor(Number(input.limit || 200))));
|
|
346216
346232
|
const entries = this.history.slice(0, limit).map((message, index) => ({
|
|
@@ -346233,11 +346249,19 @@ Review this persisted peer result and summarize or continue the parent task as n
|
|
|
346233
346249
|
metadata: { kind: "context-history-list" }
|
|
346234
346250
|
};
|
|
346235
346251
|
}
|
|
346252
|
+
const protectedZone = this.contextHistoryProtectedZone();
|
|
346236
346253
|
if (action === "remove") {
|
|
346237
346254
|
const position = Math.floor(Number(input.position));
|
|
346238
346255
|
if (!Number.isFinite(position) || position < 0 || position >= this.history.length) {
|
|
346239
346256
|
return { ok: false, output: `[context_history_manage] remove position ${position} out of range (0..${this.history.length - 1}).`, error: "remove position out of range." };
|
|
346240
346257
|
}
|
|
346258
|
+
if (protectedZone.has(position) && !Boolean(input.dangerous)) {
|
|
346259
|
+
return {
|
|
346260
|
+
ok: false,
|
|
346261
|
+
output: `[context_history_manage] remove position ${position} is protected (recent context tail or the last user message). Pass dangerous: true to override.`,
|
|
346262
|
+
error: "remove position is in the protected context zone."
|
|
346263
|
+
};
|
|
346264
|
+
}
|
|
346241
346265
|
const removed = this.history.splice(position, 1)[0];
|
|
346242
346266
|
this.saveWorkspaceConversationState(true);
|
|
346243
346267
|
return {
|
|
@@ -346259,6 +346283,14 @@ Review this persisted peer result and summarize or continue the parent task as n
|
|
|
346259
346283
|
const to = Number.isFinite(toRaw) ? Math.min(this.history.length - 1, Math.max(from, toRaw)) : from;
|
|
346260
346284
|
if (from >= this.history.length) return { ok: false, output: `[context_history_manage] summarize position ${from} out of range (0..${this.history.length - 1}).`, error: "summarize position out of range." };
|
|
346261
346285
|
if (to - from < 1) return { ok: false, output: "[context_history_manage] summarize requires at least two entries in range.", error: "summarize requires a range of at least two entries." };
|
|
346286
|
+
const protectedHit = this.history.slice(from, to + 1).some((_3, index) => protectedZone.has(from + index));
|
|
346287
|
+
if (protectedHit && !Boolean(input.dangerous)) {
|
|
346288
|
+
return {
|
|
346289
|
+
ok: false,
|
|
346290
|
+
output: "[context_history_manage] summarize range includes protected entries (recent context tail or the last user message). Pass dangerous: true to override.",
|
|
346291
|
+
error: "summarize range overlaps the protected context zone."
|
|
346292
|
+
};
|
|
346293
|
+
}
|
|
346262
346294
|
const segment = this.history.slice(from, to + 1);
|
|
346263
346295
|
const chars = segment.reduce((sum, message) => sum + (typeof message.content === "string" ? message.content.length : JSON.stringify(message.content || "").length), 0);
|
|
346264
346296
|
const summary = this.localCompressionSummary(
|
|
@@ -346272,6 +346304,8 @@ ${this.compressionHistoryContent(message.content || "")}`).join("\n\n").slice(0,
|
|
|
346272
346304
|
const replacement = { role: "system", content: `[Context History Summary]
|
|
346273
346305
|
${summary}` };
|
|
346274
346306
|
this.history.splice(from, to - from + 1, replacement);
|
|
346307
|
+
this.pushCompressionCacheEntry(`[Context History Summary]
|
|
346308
|
+
${summary}`, segment, "local-summarize", true);
|
|
346275
346309
|
this.saveWorkspaceConversationState(true);
|
|
346276
346310
|
return {
|
|
346277
346311
|
ok: true,
|
|
@@ -346289,6 +346323,111 @@ ${summary}` };
|
|
|
346289
346323
|
metadata: { kind: "context-history-summarize" }
|
|
346290
346324
|
};
|
|
346291
346325
|
}
|
|
346326
|
+
if (action === "restore") {
|
|
346327
|
+
const restoreId = String(input.restore_id || "").trim();
|
|
346328
|
+
const entry = this.compressionCache.find((item) => item.id === restoreId);
|
|
346329
|
+
if (!entry) return { ok: false, output: `[context_history_manage] restore unknown restore_id: ${restoreId}.`, error: "restore_id not found." };
|
|
346330
|
+
const summaryHeader = entry.summary.startsWith("[Context Compression") ? "[Context Compression" : "[Context History Summary]";
|
|
346331
|
+
const markerIndex = this.history.findIndex((message) => String(message.role || "") === "system" && String(message.content || "").includes(summaryHeader) && String(message.content || "").includes(entry.summary.slice(0, 200)));
|
|
346332
|
+
if (markerIndex < 0) {
|
|
346333
|
+
return { ok: false, output: "[context_history_manage] restore failed: the folded summary is no longer present in context history (already re-folded or removed).", error: "restore target summary not found in history." };
|
|
346334
|
+
}
|
|
346335
|
+
this.history.splice(markerIndex, 1, ...entry.messages.map((message) => ({ ...message })));
|
|
346336
|
+
this.compressionCache = this.compressionCache.filter((item) => item.id !== entry.id);
|
|
346337
|
+
this.saveWorkspaceConversationState(true);
|
|
346338
|
+
return {
|
|
346339
|
+
ok: true,
|
|
346340
|
+
output: JSON.stringify({
|
|
346341
|
+
ok: true,
|
|
346342
|
+
action: "restore",
|
|
346343
|
+
restoreId: entry.id,
|
|
346344
|
+
restoredEntries: entry.messages.length,
|
|
346345
|
+
restoredChars: entry.foldedChars,
|
|
346346
|
+
cacheRemaining: this.compressionCache.length,
|
|
346347
|
+
displayHistory: { untouched: true, messageCount: this.chatMessages.length }
|
|
346348
|
+
}, null, 2),
|
|
346349
|
+
metadata: { kind: "context-history-restore" }
|
|
346350
|
+
};
|
|
346351
|
+
}
|
|
346352
|
+
if (action === "search") {
|
|
346353
|
+
const query = String(input.query || "").trim().toLowerCase();
|
|
346354
|
+
const limit = Math.max(1, Math.min(200, Math.floor(Number(input.limit || 20))));
|
|
346355
|
+
if (!query) return { ok: false, output: "[context_history_manage] search requires query.", error: "search requires query." };
|
|
346356
|
+
const matches = [];
|
|
346357
|
+
for (const entry of this.compressionCache) {
|
|
346358
|
+
if (matches.length >= limit) break;
|
|
346359
|
+
const hit = { cacheId: entry.id, at: entry.at, summary: entry.summary.slice(0, 500), matches: [] };
|
|
346360
|
+
if (entry.summary.toLowerCase().includes(query)) {
|
|
346361
|
+
hit.matches.push({ index: -1, snippet: this.snippetAround(entry.summary, query) });
|
|
346362
|
+
}
|
|
346363
|
+
entry.messages.forEach((message, index) => {
|
|
346364
|
+
if (matches.length >= limit || hit.matches.length >= 40) return;
|
|
346365
|
+
const content = this.compressionHistoryContent(message.content || message.reasoning_content || "");
|
|
346366
|
+
if (content.toLowerCase().includes(query)) {
|
|
346367
|
+
hit.matches.push({ index, snippet: this.snippetAround(content, query) });
|
|
346368
|
+
}
|
|
346369
|
+
});
|
|
346370
|
+
if (hit.matches.length) matches.push(hit);
|
|
346371
|
+
}
|
|
346372
|
+
return {
|
|
346373
|
+
ok: true,
|
|
346374
|
+
output: JSON.stringify({
|
|
346375
|
+
ok: true,
|
|
346376
|
+
action: "search",
|
|
346377
|
+
query: String(input.query || ""),
|
|
346378
|
+
cacheEntries: this.compressionCache.length,
|
|
346379
|
+
matches,
|
|
346380
|
+
displayHistory: { untouched: true, messageCount: this.chatMessages.length }
|
|
346381
|
+
}, null, 2),
|
|
346382
|
+
metadata: { kind: "context-history-search" }
|
|
346383
|
+
};
|
|
346384
|
+
}
|
|
346385
|
+
if (action === "status") {
|
|
346386
|
+
const budget = this.compressionBudget(this.history);
|
|
346387
|
+
const estimatedTokens = this.estimateContextTokens(this.history);
|
|
346388
|
+
const maxTokens = this.contextMaxTokens();
|
|
346389
|
+
const protectedStartIndex = this.contextHistoryProtectedStartIndex();
|
|
346390
|
+
const lastUserIndex = this.history.map((message) => String(message.role || "")).lastIndexOf("user");
|
|
346391
|
+
return {
|
|
346392
|
+
ok: true,
|
|
346393
|
+
output: JSON.stringify({
|
|
346394
|
+
ok: true,
|
|
346395
|
+
action: "status",
|
|
346396
|
+
historyLength: this.history.length,
|
|
346397
|
+
chatMessages: this.chatMessages.length,
|
|
346398
|
+
estimatedTokens,
|
|
346399
|
+
maxTokens,
|
|
346400
|
+
triggerTokens: budget.triggerTokens,
|
|
346401
|
+
targetTokens: budget.targetTokens,
|
|
346402
|
+
summaryTokens: budget.summaryTokens,
|
|
346403
|
+
usagePercent: maxTokens > 0 ? Math.round(estimatedTokens / maxTokens * 1e3) / 10 : 0,
|
|
346404
|
+
thresholdReached: budget.triggerTokens > 0 && estimatedTokens >= budget.triggerTokens,
|
|
346405
|
+
keepRecentMessages: this.config.getNum("context", "keep_recent_messages") || 10,
|
|
346406
|
+
lastCompression: this.lastCompression ? {
|
|
346407
|
+
at: this.lastCompression.at,
|
|
346408
|
+
originalMessages: this.lastCompression.originalMessages,
|
|
346409
|
+
compressedMessages: this.lastCompression.compressedMessages,
|
|
346410
|
+
compressedTokens: this.lastCompression.compressedTokens,
|
|
346411
|
+
model: this.lastCompression.model,
|
|
346412
|
+
fallback: this.lastCompression.fallback
|
|
346413
|
+
} : null,
|
|
346414
|
+
cache: {
|
|
346415
|
+
entries: this.compressionCache.length,
|
|
346416
|
+
totalFoldedEntries: this.compressionCache.reduce((sum, item) => sum + item.foldedEntries, 0),
|
|
346417
|
+
totalFoldedChars: this.compressionCache.reduce((sum, item) => sum + item.foldedChars, 0),
|
|
346418
|
+
ids: this.compressionCache.map((item) => item.id)
|
|
346419
|
+
},
|
|
346420
|
+
protectedZone: {
|
|
346421
|
+
preserveRecentMessages: this.config.getNum("context", "preserve_recent_messages") || 5,
|
|
346422
|
+
protectedStartIndex,
|
|
346423
|
+
lastUserMessageIndex: lastUserIndex,
|
|
346424
|
+
protectedCount: protectedStartIndex >= 0 ? this.history.length - protectedStartIndex : 0
|
|
346425
|
+
},
|
|
346426
|
+
displayHistory: { untouched: true, messageCount: this.chatMessages.length }
|
|
346427
|
+
}, null, 2),
|
|
346428
|
+
metadata: { kind: "context-history-status" }
|
|
346429
|
+
};
|
|
346430
|
+
}
|
|
346292
346431
|
return { ok: false, output: `[context_history_manage] Unknown action: ${action}`, error: `Unknown action: ${action}` };
|
|
346293
346432
|
}
|
|
346294
346433
|
recordContextCompressionStep() {
|
|
@@ -346689,6 +346828,7 @@ ${summary}` };
|
|
|
346689
346828
|
model: fallbackUsed ? "model-switch-segmented-with-fallback" : modelName,
|
|
346690
346829
|
fallback: fallbackUsed
|
|
346691
346830
|
};
|
|
346831
|
+
this.pushCompressionCacheEntry(summary2, originalMessages.slice(0, recentStart), "model-switch", fallbackUsed);
|
|
346692
346832
|
this.persistCompressedHistory(summary2, recent.length, candidate2);
|
|
346693
346833
|
this.saveWorkspaceConversationState(true);
|
|
346694
346834
|
return { compressed: true, rounds, segments, droppedMessages: 0, estimatedTokens: this.estimateContextTokens(candidate2), maxTokens };
|
|
@@ -346722,6 +346862,7 @@ ${summary}` };
|
|
|
346722
346862
|
model: fallbackUsed ? "model-switch-segmented-with-fallback" : modelName,
|
|
346723
346863
|
fallback: fallbackUsed || droppedMessages > 0
|
|
346724
346864
|
};
|
|
346865
|
+
this.pushCompressionCacheEntry(summary, originalMessages.slice(0, recentStart), "model-switch", fallbackUsed || droppedMessages > 0);
|
|
346725
346866
|
this.persistCompressedHistory(summary, recent.length, candidate);
|
|
346726
346867
|
this.saveWorkspaceConversationState(true);
|
|
346727
346868
|
return { compressed: true, rounds, segments, droppedMessages, estimatedTokens: this.estimateContextTokens(candidate), maxTokens };
|
|
@@ -349002,6 +349143,7 @@ Falling back to built-in engine.` }];
|
|
|
349002
349143
|
model: compression.model,
|
|
349003
349144
|
fallback: compression.fallback
|
|
349004
349145
|
};
|
|
349146
|
+
this.pushCompressionCacheEntry(compression.summary, middle, compression.model, compression.fallback);
|
|
349005
349147
|
this.persistCompressedHistory(compression.summary, recent.length, msgs);
|
|
349006
349148
|
}
|
|
349007
349149
|
async buildCompressionSummary(middle, totalChars, budget, provider, signal, compressionModel, currentInstruction = "") {
|
|
@@ -349150,6 +349292,48 @@ ${text.slice(-tailChars).trimStart()}`;
|
|
|
349150
349292
|
}
|
|
349151
349293
|
if (this.isSubagentRuntime) this.subagentContextPersist?.(this.history.map((message) => ({ ...message })), this.lastCompression);
|
|
349152
349294
|
}
|
|
349295
|
+
pushCompressionCacheEntry(summary, messages, model, fallback) {
|
|
349296
|
+
if (!messages.length) return;
|
|
349297
|
+
const foldedChars = messages.reduce((sum, message) => sum + (typeof message.content === "string" ? message.content.length : JSON.stringify(message.content || "").length), 0);
|
|
349298
|
+
this.compressionCache.push({
|
|
349299
|
+
id: `ctx-cache-${this.nextCompressionCacheId}`,
|
|
349300
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
349301
|
+
summary,
|
|
349302
|
+
messages: messages.map((message) => ({ ...message })),
|
|
349303
|
+
foldedEntries: messages.length,
|
|
349304
|
+
foldedChars,
|
|
349305
|
+
model,
|
|
349306
|
+
fallback
|
|
349307
|
+
});
|
|
349308
|
+
this.nextCompressionCacheId += 1;
|
|
349309
|
+
const maxEntries = Math.max(0, Math.floor(this.config.getNum("context", "compression_cache_max") || 8));
|
|
349310
|
+
if (this.compressionCache.length > maxEntries) {
|
|
349311
|
+
this.compressionCache = this.compressionCache.slice(this.compressionCache.length - maxEntries);
|
|
349312
|
+
}
|
|
349313
|
+
this.saveWorkspaceConversationState(true);
|
|
349314
|
+
}
|
|
349315
|
+
contextHistoryProtectedStartIndex() {
|
|
349316
|
+
const preserve = Math.max(0, Math.floor(this.config.getNum("context", "preserve_recent_messages") || 5));
|
|
349317
|
+
const lastUserIndex = this.history.map((message) => String(message.role || "")).lastIndexOf("user");
|
|
349318
|
+
const candidates = [];
|
|
349319
|
+
if (preserve > 0 && this.history.length > 0) candidates.push(Math.max(0, this.history.length - preserve));
|
|
349320
|
+
if (lastUserIndex >= 0) candidates.push(lastUserIndex);
|
|
349321
|
+
return candidates.length ? Math.min(...candidates) : -1;
|
|
349322
|
+
}
|
|
349323
|
+
contextHistoryProtectedZone() {
|
|
349324
|
+
const start = this.contextHistoryProtectedStartIndex();
|
|
349325
|
+
const zone = /* @__PURE__ */ new Set();
|
|
349326
|
+
if (start >= 0) for (let i4 = start; i4 < this.history.length; i4 += 1) zone.add(i4);
|
|
349327
|
+
return zone;
|
|
349328
|
+
}
|
|
349329
|
+
snippetAround(content, query, radius = 150) {
|
|
349330
|
+
const text = String(content || "");
|
|
349331
|
+
const index = text.toLowerCase().indexOf(query.toLowerCase());
|
|
349332
|
+
if (index < 0) return text.slice(0, radius * 2);
|
|
349333
|
+
const from = Math.max(0, index - radius);
|
|
349334
|
+
const to = Math.min(text.length, index + query.length + radius);
|
|
349335
|
+
return `${from > 0 ? "\u2026" : ""}${text.slice(from, to).trim()}${to < text.length ? "\u2026" : ""}`;
|
|
349336
|
+
}
|
|
349153
349337
|
buildSystemPrompt() {
|
|
349154
349338
|
const cwd = this.workspace.current?.path || this.rootPath;
|
|
349155
349339
|
const enabledSkills = this.skills.active();
|
package/dist/core/agent.d.ts
CHANGED
|
@@ -58,6 +58,16 @@ export interface AutoRouteRatingResult {
|
|
|
58
58
|
}
|
|
59
59
|
export declare const ROOT_AGENT_ACTOR_ID = "00000000-0000-4000-8000-000000000001";
|
|
60
60
|
export declare function normalizeIntelligenceTier(value: unknown): 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'ultra';
|
|
61
|
+
export interface CompressionCacheEntry {
|
|
62
|
+
id: string;
|
|
63
|
+
at: string;
|
|
64
|
+
summary: string;
|
|
65
|
+
messages: Array<Record<string, unknown>>;
|
|
66
|
+
foldedEntries: number;
|
|
67
|
+
foldedChars: number;
|
|
68
|
+
model: string;
|
|
69
|
+
fallback: boolean;
|
|
70
|
+
}
|
|
61
71
|
type ConversationModelSelection = {
|
|
62
72
|
kind: 'auto';
|
|
63
73
|
} | {
|
|
@@ -286,6 +296,8 @@ export declare class Agent {
|
|
|
286
296
|
model: string;
|
|
287
297
|
fallback: boolean;
|
|
288
298
|
} | null;
|
|
299
|
+
private compressionCache;
|
|
300
|
+
private nextCompressionCacheId;
|
|
289
301
|
private workspaceConversations;
|
|
290
302
|
isSubagentRuntime: boolean;
|
|
291
303
|
private subagentName;
|
|
@@ -803,6 +815,10 @@ export declare class Agent {
|
|
|
803
815
|
private compactSummaryBody;
|
|
804
816
|
private formatCompressionSummary;
|
|
805
817
|
private persistCompressedHistory;
|
|
818
|
+
private pushCompressionCacheEntry;
|
|
819
|
+
private contextHistoryProtectedStartIndex;
|
|
820
|
+
private contextHistoryProtectedZone;
|
|
821
|
+
private snippetAround;
|
|
806
822
|
buildSystemPrompt(): string;
|
|
807
823
|
/**
|
|
808
824
|
* dev-0.3.0: assemble the model-request system prompt through the Context
|
package/dist/core/agent.js
CHANGED
|
@@ -191,6 +191,8 @@ class Agent {
|
|
|
191
191
|
continuations = [];
|
|
192
192
|
activeConversationId = 'default';
|
|
193
193
|
lastCompression = null;
|
|
194
|
+
compressionCache = [];
|
|
195
|
+
nextCompressionCacheId = 1;
|
|
194
196
|
workspaceConversations = new Map();
|
|
195
197
|
isSubagentRuntime = false;
|
|
196
198
|
subagentName = '';
|
|
@@ -3109,6 +3111,7 @@ class Agent {
|
|
|
3109
3111
|
this.workspaceConversations.set(key, {
|
|
3110
3112
|
chatMessages: [...this.chatMessages],
|
|
3111
3113
|
history: [...this.history],
|
|
3114
|
+
compressionCache: [...this.compressionCache],
|
|
3112
3115
|
plan: this.normalizeConversationPlan(this.conversationPlan),
|
|
3113
3116
|
linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
|
|
3114
3117
|
subagentState: this.subagents.serialize(),
|
|
@@ -3138,6 +3141,7 @@ class Agent {
|
|
|
3138
3141
|
title,
|
|
3139
3142
|
chatMessages: [...this.chatMessages],
|
|
3140
3143
|
history: [...this.history],
|
|
3144
|
+
compressionCache: [...this.compressionCache],
|
|
3141
3145
|
plan: this.normalizeConversationPlan(this.conversationPlan),
|
|
3142
3146
|
linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
|
|
3143
3147
|
subagentState: this.subagents.serialize(),
|
|
@@ -3167,6 +3171,8 @@ class Agent {
|
|
|
3167
3171
|
if (!key) {
|
|
3168
3172
|
this.chatMessages = [];
|
|
3169
3173
|
this.history = [];
|
|
3174
|
+
this.compressionCache = [];
|
|
3175
|
+
this.nextCompressionCacheId = 1;
|
|
3170
3176
|
this.conversationPlan = { items: [] };
|
|
3171
3177
|
this.linkedPlan = { markdown: '', revision: 0 };
|
|
3172
3178
|
this.workRuns = [];
|
|
@@ -3183,6 +3189,8 @@ class Agent {
|
|
|
3183
3189
|
const saved = this.workspaceConversations.get(key);
|
|
3184
3190
|
if (saved) {
|
|
3185
3191
|
this.history = [...saved.history];
|
|
3192
|
+
this.compressionCache = saved.compressionCache ? saved.compressionCache.map(entry => ({ ...entry, messages: [...entry.messages] })) : [];
|
|
3193
|
+
this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map(entry => Number(entry.id.replace(/^ctx-cache-/, '')) || 0)) + 1;
|
|
3186
3194
|
this.chatMessages = this.normalizeConversationChatMessages(saved.chatMessages, this.history);
|
|
3187
3195
|
this.conversationPlan = this.normalizeConversationPlan(saved.plan);
|
|
3188
3196
|
this.linkedPlan = this.normalizeLinkedPlan(saved.linkedPlan);
|
|
@@ -3202,6 +3210,8 @@ class Agent {
|
|
|
3202
3210
|
const stateKey = this.workspaceConversationStateKey();
|
|
3203
3211
|
const persisted = stateKey && stored.conversations ? stored.conversations[stateKey] : null;
|
|
3204
3212
|
this.history = persisted?.history ? [...persisted.history] : [];
|
|
3213
|
+
this.compressionCache = persisted?.compressionCache ? persisted.compressionCache.map(entry => ({ ...entry, messages: [...entry.messages] })) : [];
|
|
3214
|
+
this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map(entry => Number(entry.id.replace(/^ctx-cache-/, '')) || 0)) + 1;
|
|
3205
3215
|
this.chatMessages = this.normalizeConversationChatMessages(persisted?.chatMessages || [], this.history);
|
|
3206
3216
|
this.conversationPlan = this.normalizeConversationPlan(persisted?.plan);
|
|
3207
3217
|
this.linkedPlan = this.normalizeLinkedPlan(persisted?.linkedPlan);
|
|
@@ -3219,6 +3229,7 @@ class Agent {
|
|
|
3219
3229
|
this.workspaceConversations.set(key, {
|
|
3220
3230
|
chatMessages: [...this.chatMessages],
|
|
3221
3231
|
history: [...this.history],
|
|
3232
|
+
compressionCache: [...this.compressionCache],
|
|
3222
3233
|
plan: this.normalizeConversationPlan(this.conversationPlan),
|
|
3223
3234
|
linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
|
|
3224
3235
|
subagentState: this.subagents.serialize(),
|
|
@@ -3466,7 +3477,7 @@ class Agent {
|
|
|
3466
3477
|
catch { }
|
|
3467
3478
|
const action = String(input.action || '').trim();
|
|
3468
3479
|
if (!action)
|
|
3469
|
-
return { ok: false, output: '[context_history_manage] action is required (list|remove|summarize).', error: 'action is required.' };
|
|
3480
|
+
return { ok: false, output: '[context_history_manage] action is required (list|remove|summarize|restore|search|status).', error: 'action is required.' };
|
|
3470
3481
|
if (action === 'list') {
|
|
3471
3482
|
const limit = Math.max(5, Math.min(400, Math.floor(Number(input.limit || 200))));
|
|
3472
3483
|
const entries = this.history.slice(0, limit).map((message, index) => ({
|
|
@@ -3489,11 +3500,19 @@ class Agent {
|
|
|
3489
3500
|
metadata: { kind: 'context-history-list' },
|
|
3490
3501
|
};
|
|
3491
3502
|
}
|
|
3503
|
+
const protectedZone = this.contextHistoryProtectedZone();
|
|
3492
3504
|
if (action === 'remove') {
|
|
3493
3505
|
const position = Math.floor(Number(input.position));
|
|
3494
3506
|
if (!Number.isFinite(position) || position < 0 || position >= this.history.length) {
|
|
3495
3507
|
return { ok: false, output: `[context_history_manage] remove position ${position} out of range (0..${this.history.length - 1}).`, error: 'remove position out of range.' };
|
|
3496
3508
|
}
|
|
3509
|
+
if (protectedZone.has(position) && !Boolean(input.dangerous)) {
|
|
3510
|
+
return {
|
|
3511
|
+
ok: false,
|
|
3512
|
+
output: `[context_history_manage] remove position ${position} is protected (recent context tail or the last user message). Pass dangerous: true to override.`,
|
|
3513
|
+
error: 'remove position is in the protected context zone.',
|
|
3514
|
+
};
|
|
3515
|
+
}
|
|
3497
3516
|
const removed = this.history.splice(position, 1)[0];
|
|
3498
3517
|
this.saveWorkspaceConversationState(true);
|
|
3499
3518
|
return {
|
|
@@ -3517,11 +3536,20 @@ class Agent {
|
|
|
3517
3536
|
return { ok: false, output: `[context_history_manage] summarize position ${from} out of range (0..${this.history.length - 1}).`, error: 'summarize position out of range.' };
|
|
3518
3537
|
if (to - from < 1)
|
|
3519
3538
|
return { ok: false, output: '[context_history_manage] summarize requires at least two entries in range.', error: 'summarize requires a range of at least two entries.' };
|
|
3539
|
+
const protectedHit = this.history.slice(from, to + 1).some((_, index) => protectedZone.has(from + index));
|
|
3540
|
+
if (protectedHit && !Boolean(input.dangerous)) {
|
|
3541
|
+
return {
|
|
3542
|
+
ok: false,
|
|
3543
|
+
output: '[context_history_manage] summarize range includes protected entries (recent context tail or the last user message). Pass dangerous: true to override.',
|
|
3544
|
+
error: 'summarize range overlaps the protected context zone.',
|
|
3545
|
+
};
|
|
3546
|
+
}
|
|
3520
3547
|
const segment = this.history.slice(from, to + 1);
|
|
3521
3548
|
const chars = segment.reduce((sum, message) => sum + (typeof message.content === 'string' ? message.content.length : JSON.stringify(message.content || '').length), 0);
|
|
3522
3549
|
const summary = this.localCompressionSummary(`Workspace: ${this.workspace.current?.path || this.rootPath}\nMode: ${this.modeName()}`, segment.map((message, i) => `#${i + 1} [${String(message.role || 'unknown')}${message.name ? ` ${String(message.name)}` : ''}]\n${this.compressionHistoryContent(message.content || '')}`).join('\n\n').slice(0, 20000), segment.length, chars);
|
|
3523
3550
|
const replacement = { role: 'system', content: `[Context History Summary]\n${summary}` };
|
|
3524
3551
|
this.history.splice(from, to - from + 1, replacement);
|
|
3552
|
+
this.pushCompressionCacheEntry(`[Context History Summary]\n${summary}`, segment, 'local-summarize', true);
|
|
3525
3553
|
this.saveWorkspaceConversationState(true);
|
|
3526
3554
|
return {
|
|
3527
3555
|
ok: true,
|
|
@@ -3539,6 +3567,116 @@ class Agent {
|
|
|
3539
3567
|
metadata: { kind: 'context-history-summarize' },
|
|
3540
3568
|
};
|
|
3541
3569
|
}
|
|
3570
|
+
if (action === 'restore') {
|
|
3571
|
+
const restoreId = String(input.restore_id || '').trim();
|
|
3572
|
+
const entry = this.compressionCache.find(item => item.id === restoreId);
|
|
3573
|
+
if (!entry)
|
|
3574
|
+
return { ok: false, output: `[context_history_manage] restore unknown restore_id: ${restoreId}.`, error: 'restore_id not found.' };
|
|
3575
|
+
const summaryHeader = entry.summary.startsWith('[Context Compression') ? '[Context Compression' : '[Context History Summary]';
|
|
3576
|
+
const markerIndex = this.history.findIndex(message => String(message.role || '') === 'system' && String(message.content || '').includes(summaryHeader) && String(message.content || '').includes(entry.summary.slice(0, 200)));
|
|
3577
|
+
if (markerIndex < 0) {
|
|
3578
|
+
return { ok: false, output: '[context_history_manage] restore failed: the folded summary is no longer present in context history (already re-folded or removed).', error: 'restore target summary not found in history.' };
|
|
3579
|
+
}
|
|
3580
|
+
this.history.splice(markerIndex, 1, ...entry.messages.map(message => ({ ...message })));
|
|
3581
|
+
this.compressionCache = this.compressionCache.filter(item => item.id !== entry.id);
|
|
3582
|
+
this.saveWorkspaceConversationState(true);
|
|
3583
|
+
return {
|
|
3584
|
+
ok: true,
|
|
3585
|
+
output: JSON.stringify({
|
|
3586
|
+
ok: true,
|
|
3587
|
+
action: 'restore',
|
|
3588
|
+
restoreId: entry.id,
|
|
3589
|
+
restoredEntries: entry.messages.length,
|
|
3590
|
+
restoredChars: entry.foldedChars,
|
|
3591
|
+
cacheRemaining: this.compressionCache.length,
|
|
3592
|
+
displayHistory: { untouched: true, messageCount: this.chatMessages.length },
|
|
3593
|
+
}, null, 2),
|
|
3594
|
+
metadata: { kind: 'context-history-restore' },
|
|
3595
|
+
};
|
|
3596
|
+
}
|
|
3597
|
+
if (action === 'search') {
|
|
3598
|
+
const query = String(input.query || '').trim().toLowerCase();
|
|
3599
|
+
const limit = Math.max(1, Math.min(200, Math.floor(Number(input.limit || 20))));
|
|
3600
|
+
if (!query)
|
|
3601
|
+
return { ok: false, output: '[context_history_manage] search requires query.', error: 'search requires query.' };
|
|
3602
|
+
const matches = [];
|
|
3603
|
+
for (const entry of this.compressionCache) {
|
|
3604
|
+
if (matches.length >= limit)
|
|
3605
|
+
break;
|
|
3606
|
+
const hit = { cacheId: entry.id, at: entry.at, summary: entry.summary.slice(0, 500), matches: [] };
|
|
3607
|
+
if (entry.summary.toLowerCase().includes(query)) {
|
|
3608
|
+
hit.matches.push({ index: -1, snippet: this.snippetAround(entry.summary, query) });
|
|
3609
|
+
}
|
|
3610
|
+
entry.messages.forEach((message, index) => {
|
|
3611
|
+
if (matches.length >= limit || hit.matches.length >= 40)
|
|
3612
|
+
return;
|
|
3613
|
+
const content = this.compressionHistoryContent(message.content || message.reasoning_content || '');
|
|
3614
|
+
if (content.toLowerCase().includes(query)) {
|
|
3615
|
+
hit.matches.push({ index, snippet: this.snippetAround(content, query) });
|
|
3616
|
+
}
|
|
3617
|
+
});
|
|
3618
|
+
if (hit.matches.length)
|
|
3619
|
+
matches.push(hit);
|
|
3620
|
+
}
|
|
3621
|
+
return {
|
|
3622
|
+
ok: true,
|
|
3623
|
+
output: JSON.stringify({
|
|
3624
|
+
ok: true,
|
|
3625
|
+
action: 'search',
|
|
3626
|
+
query: String(input.query || ''),
|
|
3627
|
+
cacheEntries: this.compressionCache.length,
|
|
3628
|
+
matches,
|
|
3629
|
+
displayHistory: { untouched: true, messageCount: this.chatMessages.length },
|
|
3630
|
+
}, null, 2),
|
|
3631
|
+
metadata: { kind: 'context-history-search' },
|
|
3632
|
+
};
|
|
3633
|
+
}
|
|
3634
|
+
if (action === 'status') {
|
|
3635
|
+
const budget = this.compressionBudget(this.history);
|
|
3636
|
+
const estimatedTokens = this.estimateContextTokens(this.history);
|
|
3637
|
+
const maxTokens = this.contextMaxTokens();
|
|
3638
|
+
const protectedStartIndex = this.contextHistoryProtectedStartIndex();
|
|
3639
|
+
const lastUserIndex = this.history.map(message => String(message.role || '')).lastIndexOf('user');
|
|
3640
|
+
return {
|
|
3641
|
+
ok: true,
|
|
3642
|
+
output: JSON.stringify({
|
|
3643
|
+
ok: true,
|
|
3644
|
+
action: 'status',
|
|
3645
|
+
historyLength: this.history.length,
|
|
3646
|
+
chatMessages: this.chatMessages.length,
|
|
3647
|
+
estimatedTokens,
|
|
3648
|
+
maxTokens,
|
|
3649
|
+
triggerTokens: budget.triggerTokens,
|
|
3650
|
+
targetTokens: budget.targetTokens,
|
|
3651
|
+
summaryTokens: budget.summaryTokens,
|
|
3652
|
+
usagePercent: maxTokens > 0 ? Math.round((estimatedTokens / maxTokens) * 1000) / 10 : 0,
|
|
3653
|
+
thresholdReached: budget.triggerTokens > 0 && estimatedTokens >= budget.triggerTokens,
|
|
3654
|
+
keepRecentMessages: this.config.getNum('context', 'keep_recent_messages') || 10,
|
|
3655
|
+
lastCompression: this.lastCompression ? {
|
|
3656
|
+
at: this.lastCompression.at,
|
|
3657
|
+
originalMessages: this.lastCompression.originalMessages,
|
|
3658
|
+
compressedMessages: this.lastCompression.compressedMessages,
|
|
3659
|
+
compressedTokens: this.lastCompression.compressedTokens,
|
|
3660
|
+
model: this.lastCompression.model,
|
|
3661
|
+
fallback: this.lastCompression.fallback,
|
|
3662
|
+
} : null,
|
|
3663
|
+
cache: {
|
|
3664
|
+
entries: this.compressionCache.length,
|
|
3665
|
+
totalFoldedEntries: this.compressionCache.reduce((sum, item) => sum + item.foldedEntries, 0),
|
|
3666
|
+
totalFoldedChars: this.compressionCache.reduce((sum, item) => sum + item.foldedChars, 0),
|
|
3667
|
+
ids: this.compressionCache.map(item => item.id),
|
|
3668
|
+
},
|
|
3669
|
+
protectedZone: {
|
|
3670
|
+
preserveRecentMessages: this.config.getNum('context', 'preserve_recent_messages') || 5,
|
|
3671
|
+
protectedStartIndex,
|
|
3672
|
+
lastUserMessageIndex: lastUserIndex,
|
|
3673
|
+
protectedCount: protectedStartIndex >= 0 ? this.history.length - protectedStartIndex : 0,
|
|
3674
|
+
},
|
|
3675
|
+
displayHistory: { untouched: true, messageCount: this.chatMessages.length },
|
|
3676
|
+
}, null, 2),
|
|
3677
|
+
metadata: { kind: 'context-history-status' },
|
|
3678
|
+
};
|
|
3679
|
+
}
|
|
3542
3680
|
return { ok: false, output: `[context_history_manage] Unknown action: ${action}`, error: `Unknown action: ${action}` };
|
|
3543
3681
|
}
|
|
3544
3682
|
recordContextCompressionStep() {
|
|
@@ -3980,6 +4118,7 @@ class Agent {
|
|
|
3980
4118
|
model: fallbackUsed ? 'model-switch-segmented-with-fallback' : modelName,
|
|
3981
4119
|
fallback: fallbackUsed,
|
|
3982
4120
|
};
|
|
4121
|
+
this.pushCompressionCacheEntry(summary, originalMessages.slice(0, recentStart), 'model-switch', fallbackUsed);
|
|
3983
4122
|
this.persistCompressedHistory(summary, recent.length, candidate);
|
|
3984
4123
|
this.saveWorkspaceConversationState(true);
|
|
3985
4124
|
return { compressed: true, rounds, segments, droppedMessages: 0, estimatedTokens: this.estimateContextTokens(candidate), maxTokens };
|
|
@@ -4016,6 +4155,7 @@ class Agent {
|
|
|
4016
4155
|
model: fallbackUsed ? 'model-switch-segmented-with-fallback' : modelName,
|
|
4017
4156
|
fallback: fallbackUsed || droppedMessages > 0,
|
|
4018
4157
|
};
|
|
4158
|
+
this.pushCompressionCacheEntry(summary, originalMessages.slice(0, recentStart), 'model-switch', fallbackUsed || droppedMessages > 0);
|
|
4019
4159
|
this.persistCompressedHistory(summary, recent.length, candidate);
|
|
4020
4160
|
this.saveWorkspaceConversationState(true);
|
|
4021
4161
|
return { compressed: true, rounds, segments, droppedMessages, estimatedTokens: this.estimateContextTokens(candidate), maxTokens };
|
|
@@ -6574,6 +6714,7 @@ class Agent {
|
|
|
6574
6714
|
model: compression.model,
|
|
6575
6715
|
fallback: compression.fallback,
|
|
6576
6716
|
};
|
|
6717
|
+
this.pushCompressionCacheEntry(compression.summary, middle, compression.model, compression.fallback);
|
|
6577
6718
|
this.persistCompressedHistory(compression.summary, recent.length, msgs);
|
|
6578
6719
|
}
|
|
6579
6720
|
async buildCompressionSummary(middle, totalChars, budget, provider, signal, compressionModel, currentInstruction = '') {
|
|
@@ -6731,6 +6872,56 @@ class Agent {
|
|
|
6731
6872
|
if (this.isSubagentRuntime)
|
|
6732
6873
|
this.subagentContextPersist?.(this.history.map(message => ({ ...message })), this.lastCompression);
|
|
6733
6874
|
}
|
|
6875
|
+
pushCompressionCacheEntry(summary, messages, model, fallback) {
|
|
6876
|
+
if (!messages.length)
|
|
6877
|
+
return;
|
|
6878
|
+
const foldedChars = messages.reduce((sum, message) => sum + (typeof message.content === 'string'
|
|
6879
|
+
? message.content.length
|
|
6880
|
+
: JSON.stringify(message.content || '').length), 0);
|
|
6881
|
+
this.compressionCache.push({
|
|
6882
|
+
id: `ctx-cache-${this.nextCompressionCacheId}`,
|
|
6883
|
+
at: new Date().toISOString(),
|
|
6884
|
+
summary,
|
|
6885
|
+
messages: messages.map(message => ({ ...message })),
|
|
6886
|
+
foldedEntries: messages.length,
|
|
6887
|
+
foldedChars,
|
|
6888
|
+
model,
|
|
6889
|
+
fallback,
|
|
6890
|
+
});
|
|
6891
|
+
this.nextCompressionCacheId += 1;
|
|
6892
|
+
const maxEntries = Math.max(0, Math.floor(this.config.getNum('context', 'compression_cache_max') || 8));
|
|
6893
|
+
if (this.compressionCache.length > maxEntries) {
|
|
6894
|
+
this.compressionCache = this.compressionCache.slice(this.compressionCache.length - maxEntries);
|
|
6895
|
+
}
|
|
6896
|
+
this.saveWorkspaceConversationState(true);
|
|
6897
|
+
}
|
|
6898
|
+
contextHistoryProtectedStartIndex() {
|
|
6899
|
+
const preserve = Math.max(0, Math.floor(this.config.getNum('context', 'preserve_recent_messages') || 5));
|
|
6900
|
+
const lastUserIndex = this.history.map(message => String(message.role || '')).lastIndexOf('user');
|
|
6901
|
+
const candidates = [];
|
|
6902
|
+
if (preserve > 0 && this.history.length > 0)
|
|
6903
|
+
candidates.push(Math.max(0, this.history.length - preserve));
|
|
6904
|
+
if (lastUserIndex >= 0)
|
|
6905
|
+
candidates.push(lastUserIndex);
|
|
6906
|
+
return candidates.length ? Math.min(...candidates) : -1;
|
|
6907
|
+
}
|
|
6908
|
+
contextHistoryProtectedZone() {
|
|
6909
|
+
const start = this.contextHistoryProtectedStartIndex();
|
|
6910
|
+
const zone = new Set();
|
|
6911
|
+
if (start >= 0)
|
|
6912
|
+
for (let i = start; i < this.history.length; i += 1)
|
|
6913
|
+
zone.add(i);
|
|
6914
|
+
return zone;
|
|
6915
|
+
}
|
|
6916
|
+
snippetAround(content, query, radius = 150) {
|
|
6917
|
+
const text = String(content || '');
|
|
6918
|
+
const index = text.toLowerCase().indexOf(query.toLowerCase());
|
|
6919
|
+
if (index < 0)
|
|
6920
|
+
return text.slice(0, radius * 2);
|
|
6921
|
+
const from = Math.max(0, index - radius);
|
|
6922
|
+
const to = Math.min(text.length, index + query.length + radius);
|
|
6923
|
+
return `${from > 0 ? '…' : ''}${text.slice(from, to).trim()}${to < text.length ? '…' : ''}`;
|
|
6924
|
+
}
|
|
6734
6925
|
buildSystemPrompt() {
|
|
6735
6926
|
const cwd = this.workspace.current?.path || this.rootPath;
|
|
6736
6927
|
const enabledSkills = this.skills.active();
|
package/dist/core/config.js
CHANGED
|
@@ -911,6 +911,8 @@ function defaultConfig() {
|
|
|
911
911
|
auto_compress: { _description: "Auto-compress history", _type: "boolean", value: true },
|
|
912
912
|
compress_threshold_chars: { _description: "Compression threshold", _type: "integer", value: 80000 },
|
|
913
913
|
keep_recent_messages: { _description: "Keep recent messages", _type: "integer", value: 10 },
|
|
914
|
+
preserve_recent_messages: { _description: "dev-0.3.8 protected recent-message zone for context_history_manage (0 disables)", _type: "integer", value: 5 },
|
|
915
|
+
compression_cache_max: { _description: "dev-0.3.8 max folded-segment cache entries retained for restore/search", _type: "integer", value: 8 },
|
|
914
916
|
structured_context_v2: { _description: "dev-0.3.0 structured context v2 (orchestrator + fixed order + snapshot)", _type: "boolean", value: true },
|
|
915
917
|
build_history_persistence: { _description: "dev-0.3.0 append-only Build History persistence", _type: "boolean", value: true },
|
|
916
918
|
branch_log_v2: { _description: "dev-0.3.0 branch long-log v2 (epoch summaries)", _type: "boolean", value: true },
|
package/dist/tools/index.js
CHANGED
|
@@ -374,11 +374,14 @@ class ToolExecutor {
|
|
|
374
374
|
t('linked_plan', 'Read or update the current conversation linked Markdown plan. Update requires the current expected_revision.', { action: { type: 'string', enum: ['get', 'update'] }, markdown: { type: 'string' }, expected_revision: { type: 'number' } }, ['action']),
|
|
375
375
|
t('build_history_query', 'Read the concrete public work details of one historical Build Block. The prompt already exposes user input, final summary, and completion status; call this read-only tool only when the user asks what specifically happened. Select by newest-to-oldest history_index, or by run_id returned from an earlier query.', { history_index: { type: 'number', minimum: 1, description: '1-based historical Build Block index from the request ledger; 1 is the newest previous task.' }, run_id: { type: 'string', description: 'Exact run id returned by an earlier build_history_query result.' }, max_events: { type: 'number', minimum: 1, maximum: 200, description: 'Maximum trailing public work events; defaults to 80.' } }, []),
|
|
376
376
|
t('context_compress', 'Actively compress the LLM context history for this conversation. This collapses older history entries into a concise summary while preserving the recent tail, which reduces context tokens and cost. IMPORTANT: it affects only the LLM context (what the model sees); the displayed conversation history shown to the user is never altered. Call this when the conversation is long, token pressure is high, or you judge that older turns are no longer needed in full. Idempotent and safe: repeated calls produce incremental summaries.', { keep_recent: { type: 'number', minimum: 2, maximum: 60, description: 'Recent message count to keep uncompressed at the tail. Defaults to the configured keep_recent_messages.' }, force: { type: 'boolean', description: 'Compress even if the context is not yet over the automatic threshold. Defaults to false.' } }, []),
|
|
377
|
-
t('context_history_manage', 'Manage the LLM context history for this conversation without affecting the displayed conversation history. This is the active context-management surface. Actions: list returns a bounded index of context entries (position, role, name, length, first-line preview); remove deletes the entry at a given position; summarize replaces a contiguous range of context entries with a concise local summary entry. The displayed conversation history (what the user sees) is never modified by any action.', {
|
|
378
|
-
action: { type: 'string', enum: ['list', 'remove', 'summarize'], description: 'list: index context entries. remove: delete the entry at position. summarize: fold entries [from, to] into one summary entry.' },
|
|
377
|
+
t('context_history_manage', 'Manage the LLM context history for this conversation without affecting the displayed conversation history. This is the active context-management surface. Actions: list returns a bounded index of context entries (position, role, name, length, first-line preview); remove deletes the entry at a given position; summarize replaces a contiguous range of context entries with a concise local summary entry; restore reinserts the original messages of a folded segment from the compression cache using restore_id; search finds which cached folded segments contain a query (and the matching lines); status reports usage vs trigger/target budgets, the last compression, the compression cache, and the protected recent-message zone. The displayed conversation history (what the user sees) is never modified by any action. The recent context tail and the last user message are protected from remove/summarize unless dangerous is true.', {
|
|
378
|
+
action: { type: 'string', enum: ['list', 'remove', 'summarize', 'restore', 'search', 'status'], description: 'list: index context entries. remove: delete the entry at position. summarize: fold entries [from, to] into one summary entry. restore: reinsert cached original messages by restore_id. search: find folded cache entries containing query. status: report context usage, budgets, cache, and protected zone.' },
|
|
379
379
|
position: { type: 'number', minimum: 0, description: '0-based context entry index for remove, or the start of the range for summarize.' },
|
|
380
380
|
to: { type: 'number', minimum: 0, description: '0-based inclusive end of the range for summarize. Defaults to position.' },
|
|
381
|
-
limit: { type: 'number', minimum: 5, maximum: 400, description: 'Maximum context entries to list
|
|
381
|
+
limit: { type: 'number', minimum: 5, maximum: 400, description: 'Maximum context entries to list (default 200), or search matches to return (default 20).' },
|
|
382
|
+
restore_id: { type: 'string', description: 'Cache id of a folded segment (from search or status) to restore into context.' },
|
|
383
|
+
query: { type: 'string', description: 'Case-insensitive text to search for across cached folded segments and their summaries.' },
|
|
384
|
+
dangerous: { type: 'boolean', description: 'Set true to override the protected recent-message zone and allow removing/summarizing entries that include the recent context tail or the last user message.' },
|
|
382
385
|
}, ['action']),
|
|
383
386
|
t('question', 'Ask user a multiple-choice question', { questions: { type: 'array' } }, ['questions']),
|
|
384
387
|
t('skill_download', 'Download a skill', { name: { type: 'string' }, source: { type: 'string' } }, ['name', 'source']),
|
package/dist/ui/index.html
CHANGED
|
@@ -2959,6 +2959,10 @@ button:focus, select:focus, input:focus, textarea:focus, [contenteditable]:focus
|
|
|
2959
2959
|
.work-review-btn { height:28px; padding:0 10px; border:1px solid var(--glass-border-2); border-radius:6px; background:transparent; color:var(--text); cursor:pointer; font:600 11px var(--font); }
|
|
2960
2960
|
.work-review-btn:hover { background:var(--control-hover-bg); border-color:var(--border-hover); }
|
|
2961
2961
|
.work-review-list { border-top:1px solid var(--glass-border-1); }
|
|
2962
|
+
.work-review.collapsed .work-review-list { display: none; }
|
|
2963
|
+
.work-review-head { cursor: pointer; }
|
|
2964
|
+
.work-review-chevron { width:8px; height:8px; flex:0 0 auto; border-right:1px solid var(--text-dim); border-bottom:1px solid var(--text-dim); transform:rotate(-45deg); transition:transform 150ms ease; }
|
|
2965
|
+
.work-review.collapsed .work-review-chevron { transform:rotate(45deg); }
|
|
2962
2966
|
.work-review-file { min-height:34px; display:grid; grid-template-columns:minmax(0,1fr) auto; align-items:center; gap:12px; padding:0 12px; color:var(--text); }
|
|
2963
2967
|
.work-review-file:hover { background:var(--review-row-hover); }
|
|
2964
2968
|
.work-review-path { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font:11px var(--font-mono); }
|
|
@@ -8208,6 +8212,12 @@ function normalizeWorkReviewDiffs(diffs) {
|
|
|
8208
8212
|
return Object.keys(byPath).map(function(key) { return byPath[key]; });
|
|
8209
8213
|
}
|
|
8210
8214
|
|
|
8215
|
+
window.toggleWorkReview = function(head) {
|
|
8216
|
+
var review = head && head.closest ? head.closest('.work-review') : null;
|
|
8217
|
+
if (!review) return;
|
|
8218
|
+
review.classList.toggle('collapsed');
|
|
8219
|
+
};
|
|
8220
|
+
|
|
8211
8221
|
window.toggleWorkReviewFiles = function(button) {
|
|
8212
8222
|
var review = button && button.closest ? button.closest('.work-review') : null;
|
|
8213
8223
|
if (!review) return;
|
|
@@ -8301,15 +8311,16 @@ function addWorkReview(diffs) {
|
|
|
8301
8311
|
var added = files.reduce(function(total, file) { return total + file.added; }, 0);
|
|
8302
8312
|
var deleted = files.reduce(function(total, file) { return total + file.deleted; }, 0);
|
|
8303
8313
|
var review = document.createElement('div');
|
|
8304
|
-
review.className = 'work-review';
|
|
8314
|
+
review.className = 'work-review collapsed';
|
|
8305
8315
|
review.setAttribute('data-files', JSON.stringify(files));
|
|
8306
8316
|
var rows = files.map(function(file, index) {
|
|
8307
8317
|
return '<div class="work-review-file"' + (index >= 3 ? ' style="display:none"' : '') + '><span class="work-review-path">' + esc(file.path) + '</span><span><span class="work-review-add">+' + file.added + '</span><span class="work-review-del">-' + file.deleted + '</span></span></div>';
|
|
8308
8318
|
}).join('');
|
|
8309
8319
|
var editedLabel = files.length === 1 ? t('review.editedOne') : t('review.editedMany').replace('{count}', files.length);
|
|
8310
|
-
review.innerHTML = '<div class="work-review-head"><div class="work-review-mark">' + iconSvg('file-diff', t('review.fileChanges'), 'small') + '</div>' +
|
|
8320
|
+
review.innerHTML = '<div class="work-review-head" onclick="window.toggleWorkReview(this)"><div class="work-review-mark">' + iconSvg('file-diff', t('review.fileChanges'), 'small') + '</div>' +
|
|
8311
8321
|
'<div><div class="work-review-title">' + esc(editedLabel) + '</div><div class="work-review-stats"><span class="work-review-add">+' + added + '</span><span class="work-review-del">-' + deleted + '</span></div></div>' +
|
|
8312
|
-
'<div class="work-review-actions"><button class="work-review-btn" onclick="window.openWorkReview(this)">' + esc(t('review.open')) + '</button
|
|
8322
|
+
'<div class="work-review-actions"><button class="work-review-btn" onclick="window.openWorkReview(this);event.stopPropagation()">' + esc(t('review.open')) + '</button>' +
|
|
8323
|
+
'<span class="work-review-chevron" aria-hidden="true"></span></div></div>' +
|
|
8313
8324
|
'<div class="work-review-list">' + rows + (files.length > 3 ? '<button class="work-review-more" onclick="window.toggleWorkReviewFiles(this)">' + esc(t('review.showMore').replace('{count}', files.length - 3)) + '</button>' : '') + '</div>';
|
|
8314
8325
|
els['chat-area'].appendChild(review);
|
|
8315
8326
|
autoScrollIfAtBottom();
|
|
@@ -327823,6 +327823,8 @@ function defaultConfig() {
|
|
|
327823
327823
|
auto_compress: { _description: "Auto-compress history", _type: "boolean", value: true },
|
|
327824
327824
|
compress_threshold_chars: { _description: "Compression threshold", _type: "integer", value: 8e4 },
|
|
327825
327825
|
keep_recent_messages: { _description: "Keep recent messages", _type: "integer", value: 10 },
|
|
327826
|
+
preserve_recent_messages: { _description: "dev-0.3.8 protected recent-message zone for context_history_manage (0 disables)", _type: "integer", value: 5 },
|
|
327827
|
+
compression_cache_max: { _description: "dev-0.3.8 max folded-segment cache entries retained for restore/search", _type: "integer", value: 8 },
|
|
327826
327828
|
structured_context_v2: { _description: "dev-0.3.0 structured context v2 (orchestrator + fixed order + snapshot)", _type: "boolean", value: true },
|
|
327827
327829
|
build_history_persistence: { _description: "dev-0.3.0 append-only Build History persistence", _type: "boolean", value: true },
|
|
327828
327830
|
branch_log_v2: { _description: "dev-0.3.0 branch long-log v2 (epoch summaries)", _type: "boolean", value: true },
|
|
@@ -335539,11 +335541,14 @@ var ToolExecutor = class {
|
|
|
335539
335541
|
t3("linked_plan", "Read or update the current conversation linked Markdown plan. Update requires the current expected_revision.", { action: { type: "string", enum: ["get", "update"] }, markdown: { type: "string" }, expected_revision: { type: "number" } }, ["action"]),
|
|
335540
335542
|
t3("build_history_query", "Read the concrete public work details of one historical Build Block. The prompt already exposes user input, final summary, and completion status; call this read-only tool only when the user asks what specifically happened. Select by newest-to-oldest history_index, or by run_id returned from an earlier query.", { history_index: { type: "number", minimum: 1, description: "1-based historical Build Block index from the request ledger; 1 is the newest previous task." }, run_id: { type: "string", description: "Exact run id returned by an earlier build_history_query result." }, max_events: { type: "number", minimum: 1, maximum: 200, description: "Maximum trailing public work events; defaults to 80." } }, []),
|
|
335541
335543
|
t3("context_compress", "Actively compress the LLM context history for this conversation. This collapses older history entries into a concise summary while preserving the recent tail, which reduces context tokens and cost. IMPORTANT: it affects only the LLM context (what the model sees); the displayed conversation history shown to the user is never altered. Call this when the conversation is long, token pressure is high, or you judge that older turns are no longer needed in full. Idempotent and safe: repeated calls produce incremental summaries.", { keep_recent: { type: "number", minimum: 2, maximum: 60, description: "Recent message count to keep uncompressed at the tail. Defaults to the configured keep_recent_messages." }, force: { type: "boolean", description: "Compress even if the context is not yet over the automatic threshold. Defaults to false." } }, []),
|
|
335542
|
-
t3("context_history_manage", "Manage the LLM context history for this conversation without affecting the displayed conversation history. This is the active context-management surface. Actions: list returns a bounded index of context entries (position, role, name, length, first-line preview); remove deletes the entry at a given position; summarize replaces a contiguous range of context entries with a concise local summary entry. The displayed conversation history (what the user sees) is never modified by any action.", {
|
|
335543
|
-
action: { type: "string", enum: ["list", "remove", "summarize"], description: "list: index context entries. remove: delete the entry at position. summarize: fold entries [from, to] into one summary entry." },
|
|
335544
|
+
t3("context_history_manage", "Manage the LLM context history for this conversation without affecting the displayed conversation history. This is the active context-management surface. Actions: list returns a bounded index of context entries (position, role, name, length, first-line preview); remove deletes the entry at a given position; summarize replaces a contiguous range of context entries with a concise local summary entry; restore reinserts the original messages of a folded segment from the compression cache using restore_id; search finds which cached folded segments contain a query (and the matching lines); status reports usage vs trigger/target budgets, the last compression, the compression cache, and the protected recent-message zone. The displayed conversation history (what the user sees) is never modified by any action. The recent context tail and the last user message are protected from remove/summarize unless dangerous is true.", {
|
|
335545
|
+
action: { type: "string", enum: ["list", "remove", "summarize", "restore", "search", "status"], description: "list: index context entries. remove: delete the entry at position. summarize: fold entries [from, to] into one summary entry. restore: reinsert cached original messages by restore_id. search: find folded cache entries containing query. status: report context usage, budgets, cache, and protected zone." },
|
|
335544
335546
|
position: { type: "number", minimum: 0, description: "0-based context entry index for remove, or the start of the range for summarize." },
|
|
335545
335547
|
to: { type: "number", minimum: 0, description: "0-based inclusive end of the range for summarize. Defaults to position." },
|
|
335546
|
-
limit: { type: "number", minimum: 5, maximum: 400, description: "Maximum context entries to list
|
|
335548
|
+
limit: { type: "number", minimum: 5, maximum: 400, description: "Maximum context entries to list (default 200), or search matches to return (default 20)." },
|
|
335549
|
+
restore_id: { type: "string", description: "Cache id of a folded segment (from search or status) to restore into context." },
|
|
335550
|
+
query: { type: "string", description: "Case-insensitive text to search for across cached folded segments and their summaries." },
|
|
335551
|
+
dangerous: { type: "boolean", description: "Set true to override the protected recent-message zone and allow removing/summarizing entries that include the recent context tail or the last user message." }
|
|
335547
335552
|
}, ["action"]),
|
|
335548
335553
|
t3("question", "Ask user a multiple-choice question", { questions: { type: "array" } }, ["questions"]),
|
|
335549
335554
|
t3("skill_download", "Download a skill", { name: { type: "string" }, source: { type: "string" } }, ["name", "source"]),
|
|
@@ -343422,6 +343427,8 @@ var Agent4 = class _Agent {
|
|
|
343422
343427
|
continuations = [];
|
|
343423
343428
|
activeConversationId = "default";
|
|
343424
343429
|
lastCompression = null;
|
|
343430
|
+
compressionCache = [];
|
|
343431
|
+
nextCompressionCacheId = 1;
|
|
343425
343432
|
workspaceConversations = /* @__PURE__ */ new Map();
|
|
343426
343433
|
isSubagentRuntime = false;
|
|
343427
343434
|
subagentName = "";
|
|
@@ -345883,6 +345890,7 @@ Review this persisted peer result and summarize or continue the parent task as n
|
|
|
345883
345890
|
this.workspaceConversations.set(key3, {
|
|
345884
345891
|
chatMessages: [...this.chatMessages],
|
|
345885
345892
|
history: [...this.history],
|
|
345893
|
+
compressionCache: [...this.compressionCache],
|
|
345886
345894
|
plan: this.normalizeConversationPlan(this.conversationPlan),
|
|
345887
345895
|
linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
|
|
345888
345896
|
subagentState: this.subagents.serialize(),
|
|
@@ -345909,6 +345917,7 @@ Review this persisted peer result and summarize or continue the parent task as n
|
|
|
345909
345917
|
title,
|
|
345910
345918
|
chatMessages: [...this.chatMessages],
|
|
345911
345919
|
history: [...this.history],
|
|
345920
|
+
compressionCache: [...this.compressionCache],
|
|
345912
345921
|
plan: this.normalizeConversationPlan(this.conversationPlan),
|
|
345913
345922
|
linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
|
|
345914
345923
|
subagentState: this.subagents.serialize(),
|
|
@@ -345936,6 +345945,8 @@ Review this persisted peer result and summarize or continue the parent task as n
|
|
|
345936
345945
|
if (!key3) {
|
|
345937
345946
|
this.chatMessages = [];
|
|
345938
345947
|
this.history = [];
|
|
345948
|
+
this.compressionCache = [];
|
|
345949
|
+
this.nextCompressionCacheId = 1;
|
|
345939
345950
|
this.conversationPlan = { items: [] };
|
|
345940
345951
|
this.linkedPlan = { markdown: "", revision: 0 };
|
|
345941
345952
|
this.workRuns = [];
|
|
@@ -345952,6 +345963,8 @@ Review this persisted peer result and summarize or continue the parent task as n
|
|
|
345952
345963
|
const saved = this.workspaceConversations.get(key3);
|
|
345953
345964
|
if (saved) {
|
|
345954
345965
|
this.history = [...saved.history];
|
|
345966
|
+
this.compressionCache = saved.compressionCache ? saved.compressionCache.map((entry) => ({ ...entry, messages: [...entry.messages] })) : [];
|
|
345967
|
+
this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map((entry) => Number(entry.id.replace(/^ctx-cache-/, "")) || 0)) + 1;
|
|
345955
345968
|
this.chatMessages = this.normalizeConversationChatMessages(saved.chatMessages, this.history);
|
|
345956
345969
|
this.conversationPlan = this.normalizeConversationPlan(saved.plan);
|
|
345957
345970
|
this.linkedPlan = this.normalizeLinkedPlan(saved.linkedPlan);
|
|
@@ -345971,6 +345984,8 @@ Review this persisted peer result and summarize or continue the parent task as n
|
|
|
345971
345984
|
const stateKey = this.workspaceConversationStateKey();
|
|
345972
345985
|
const persisted = stateKey && stored.conversations ? stored.conversations[stateKey] : null;
|
|
345973
345986
|
this.history = persisted?.history ? [...persisted.history] : [];
|
|
345987
|
+
this.compressionCache = persisted?.compressionCache ? persisted.compressionCache.map((entry) => ({ ...entry, messages: [...entry.messages] })) : [];
|
|
345988
|
+
this.nextCompressionCacheId = Math.max(1, ...this.compressionCache.map((entry) => Number(entry.id.replace(/^ctx-cache-/, "")) || 0)) + 1;
|
|
345974
345989
|
this.chatMessages = this.normalizeConversationChatMessages(persisted?.chatMessages || [], this.history);
|
|
345975
345990
|
this.conversationPlan = this.normalizeConversationPlan(persisted?.plan);
|
|
345976
345991
|
this.linkedPlan = this.normalizeLinkedPlan(persisted?.linkedPlan);
|
|
@@ -345988,6 +346003,7 @@ Review this persisted peer result and summarize or continue the parent task as n
|
|
|
345988
346003
|
this.workspaceConversations.set(key3, {
|
|
345989
346004
|
chatMessages: [...this.chatMessages],
|
|
345990
346005
|
history: [...this.history],
|
|
346006
|
+
compressionCache: [...this.compressionCache],
|
|
345991
346007
|
plan: this.normalizeConversationPlan(this.conversationPlan),
|
|
345992
346008
|
linkedPlan: this.normalizeLinkedPlan(this.linkedPlan),
|
|
345993
346009
|
subagentState: this.subagents.serialize(),
|
|
@@ -346214,7 +346230,7 @@ Review this persisted peer result and summarize or continue the parent task as n
|
|
|
346214
346230
|
} catch {
|
|
346215
346231
|
}
|
|
346216
346232
|
const action = String(input2.action || "").trim();
|
|
346217
|
-
if (!action) return { ok: false, output: "[context_history_manage] action is required (list|remove|summarize).", error: "action is required." };
|
|
346233
|
+
if (!action) return { ok: false, output: "[context_history_manage] action is required (list|remove|summarize|restore|search|status).", error: "action is required." };
|
|
346218
346234
|
if (action === "list") {
|
|
346219
346235
|
const limit = Math.max(5, Math.min(400, Math.floor(Number(input2.limit || 200))));
|
|
346220
346236
|
const entries = this.history.slice(0, limit).map((message, index) => ({
|
|
@@ -346237,11 +346253,19 @@ Review this persisted peer result and summarize or continue the parent task as n
|
|
|
346237
346253
|
metadata: { kind: "context-history-list" }
|
|
346238
346254
|
};
|
|
346239
346255
|
}
|
|
346256
|
+
const protectedZone = this.contextHistoryProtectedZone();
|
|
346240
346257
|
if (action === "remove") {
|
|
346241
346258
|
const position = Math.floor(Number(input2.position));
|
|
346242
346259
|
if (!Number.isFinite(position) || position < 0 || position >= this.history.length) {
|
|
346243
346260
|
return { ok: false, output: `[context_history_manage] remove position ${position} out of range (0..${this.history.length - 1}).`, error: "remove position out of range." };
|
|
346244
346261
|
}
|
|
346262
|
+
if (protectedZone.has(position) && !Boolean(input2.dangerous)) {
|
|
346263
|
+
return {
|
|
346264
|
+
ok: false,
|
|
346265
|
+
output: `[context_history_manage] remove position ${position} is protected (recent context tail or the last user message). Pass dangerous: true to override.`,
|
|
346266
|
+
error: "remove position is in the protected context zone."
|
|
346267
|
+
};
|
|
346268
|
+
}
|
|
346245
346269
|
const removed = this.history.splice(position, 1)[0];
|
|
346246
346270
|
this.saveWorkspaceConversationState(true);
|
|
346247
346271
|
return {
|
|
@@ -346263,6 +346287,14 @@ Review this persisted peer result and summarize or continue the parent task as n
|
|
|
346263
346287
|
const to = Number.isFinite(toRaw) ? Math.min(this.history.length - 1, Math.max(from, toRaw)) : from;
|
|
346264
346288
|
if (from >= this.history.length) return { ok: false, output: `[context_history_manage] summarize position ${from} out of range (0..${this.history.length - 1}).`, error: "summarize position out of range." };
|
|
346265
346289
|
if (to - from < 1) return { ok: false, output: "[context_history_manage] summarize requires at least two entries in range.", error: "summarize requires a range of at least two entries." };
|
|
346290
|
+
const protectedHit = this.history.slice(from, to + 1).some((_3, index) => protectedZone.has(from + index));
|
|
346291
|
+
if (protectedHit && !Boolean(input2.dangerous)) {
|
|
346292
|
+
return {
|
|
346293
|
+
ok: false,
|
|
346294
|
+
output: "[context_history_manage] summarize range includes protected entries (recent context tail or the last user message). Pass dangerous: true to override.",
|
|
346295
|
+
error: "summarize range overlaps the protected context zone."
|
|
346296
|
+
};
|
|
346297
|
+
}
|
|
346266
346298
|
const segment = this.history.slice(from, to + 1);
|
|
346267
346299
|
const chars = segment.reduce((sum, message) => sum + (typeof message.content === "string" ? message.content.length : JSON.stringify(message.content || "").length), 0);
|
|
346268
346300
|
const summary = this.localCompressionSummary(
|
|
@@ -346276,6 +346308,8 @@ ${this.compressionHistoryContent(message.content || "")}`).join("\n\n").slice(0,
|
|
|
346276
346308
|
const replacement = { role: "system", content: `[Context History Summary]
|
|
346277
346309
|
${summary}` };
|
|
346278
346310
|
this.history.splice(from, to - from + 1, replacement);
|
|
346311
|
+
this.pushCompressionCacheEntry(`[Context History Summary]
|
|
346312
|
+
${summary}`, segment, "local-summarize", true);
|
|
346279
346313
|
this.saveWorkspaceConversationState(true);
|
|
346280
346314
|
return {
|
|
346281
346315
|
ok: true,
|
|
@@ -346293,6 +346327,111 @@ ${summary}` };
|
|
|
346293
346327
|
metadata: { kind: "context-history-summarize" }
|
|
346294
346328
|
};
|
|
346295
346329
|
}
|
|
346330
|
+
if (action === "restore") {
|
|
346331
|
+
const restoreId = String(input2.restore_id || "").trim();
|
|
346332
|
+
const entry = this.compressionCache.find((item) => item.id === restoreId);
|
|
346333
|
+
if (!entry) return { ok: false, output: `[context_history_manage] restore unknown restore_id: ${restoreId}.`, error: "restore_id not found." };
|
|
346334
|
+
const summaryHeader = entry.summary.startsWith("[Context Compression") ? "[Context Compression" : "[Context History Summary]";
|
|
346335
|
+
const markerIndex = this.history.findIndex((message) => String(message.role || "") === "system" && String(message.content || "").includes(summaryHeader) && String(message.content || "").includes(entry.summary.slice(0, 200)));
|
|
346336
|
+
if (markerIndex < 0) {
|
|
346337
|
+
return { ok: false, output: "[context_history_manage] restore failed: the folded summary is no longer present in context history (already re-folded or removed).", error: "restore target summary not found in history." };
|
|
346338
|
+
}
|
|
346339
|
+
this.history.splice(markerIndex, 1, ...entry.messages.map((message) => ({ ...message })));
|
|
346340
|
+
this.compressionCache = this.compressionCache.filter((item) => item.id !== entry.id);
|
|
346341
|
+
this.saveWorkspaceConversationState(true);
|
|
346342
|
+
return {
|
|
346343
|
+
ok: true,
|
|
346344
|
+
output: JSON.stringify({
|
|
346345
|
+
ok: true,
|
|
346346
|
+
action: "restore",
|
|
346347
|
+
restoreId: entry.id,
|
|
346348
|
+
restoredEntries: entry.messages.length,
|
|
346349
|
+
restoredChars: entry.foldedChars,
|
|
346350
|
+
cacheRemaining: this.compressionCache.length,
|
|
346351
|
+
displayHistory: { untouched: true, messageCount: this.chatMessages.length }
|
|
346352
|
+
}, null, 2),
|
|
346353
|
+
metadata: { kind: "context-history-restore" }
|
|
346354
|
+
};
|
|
346355
|
+
}
|
|
346356
|
+
if (action === "search") {
|
|
346357
|
+
const query = String(input2.query || "").trim().toLowerCase();
|
|
346358
|
+
const limit = Math.max(1, Math.min(200, Math.floor(Number(input2.limit || 20))));
|
|
346359
|
+
if (!query) return { ok: false, output: "[context_history_manage] search requires query.", error: "search requires query." };
|
|
346360
|
+
const matches = [];
|
|
346361
|
+
for (const entry of this.compressionCache) {
|
|
346362
|
+
if (matches.length >= limit) break;
|
|
346363
|
+
const hit = { cacheId: entry.id, at: entry.at, summary: entry.summary.slice(0, 500), matches: [] };
|
|
346364
|
+
if (entry.summary.toLowerCase().includes(query)) {
|
|
346365
|
+
hit.matches.push({ index: -1, snippet: this.snippetAround(entry.summary, query) });
|
|
346366
|
+
}
|
|
346367
|
+
entry.messages.forEach((message, index) => {
|
|
346368
|
+
if (matches.length >= limit || hit.matches.length >= 40) return;
|
|
346369
|
+
const content = this.compressionHistoryContent(message.content || message.reasoning_content || "");
|
|
346370
|
+
if (content.toLowerCase().includes(query)) {
|
|
346371
|
+
hit.matches.push({ index, snippet: this.snippetAround(content, query) });
|
|
346372
|
+
}
|
|
346373
|
+
});
|
|
346374
|
+
if (hit.matches.length) matches.push(hit);
|
|
346375
|
+
}
|
|
346376
|
+
return {
|
|
346377
|
+
ok: true,
|
|
346378
|
+
output: JSON.stringify({
|
|
346379
|
+
ok: true,
|
|
346380
|
+
action: "search",
|
|
346381
|
+
query: String(input2.query || ""),
|
|
346382
|
+
cacheEntries: this.compressionCache.length,
|
|
346383
|
+
matches,
|
|
346384
|
+
displayHistory: { untouched: true, messageCount: this.chatMessages.length }
|
|
346385
|
+
}, null, 2),
|
|
346386
|
+
metadata: { kind: "context-history-search" }
|
|
346387
|
+
};
|
|
346388
|
+
}
|
|
346389
|
+
if (action === "status") {
|
|
346390
|
+
const budget = this.compressionBudget(this.history);
|
|
346391
|
+
const estimatedTokens = this.estimateContextTokens(this.history);
|
|
346392
|
+
const maxTokens = this.contextMaxTokens();
|
|
346393
|
+
const protectedStartIndex = this.contextHistoryProtectedStartIndex();
|
|
346394
|
+
const lastUserIndex = this.history.map((message) => String(message.role || "")).lastIndexOf("user");
|
|
346395
|
+
return {
|
|
346396
|
+
ok: true,
|
|
346397
|
+
output: JSON.stringify({
|
|
346398
|
+
ok: true,
|
|
346399
|
+
action: "status",
|
|
346400
|
+
historyLength: this.history.length,
|
|
346401
|
+
chatMessages: this.chatMessages.length,
|
|
346402
|
+
estimatedTokens,
|
|
346403
|
+
maxTokens,
|
|
346404
|
+
triggerTokens: budget.triggerTokens,
|
|
346405
|
+
targetTokens: budget.targetTokens,
|
|
346406
|
+
summaryTokens: budget.summaryTokens,
|
|
346407
|
+
usagePercent: maxTokens > 0 ? Math.round(estimatedTokens / maxTokens * 1e3) / 10 : 0,
|
|
346408
|
+
thresholdReached: budget.triggerTokens > 0 && estimatedTokens >= budget.triggerTokens,
|
|
346409
|
+
keepRecentMessages: this.config.getNum("context", "keep_recent_messages") || 10,
|
|
346410
|
+
lastCompression: this.lastCompression ? {
|
|
346411
|
+
at: this.lastCompression.at,
|
|
346412
|
+
originalMessages: this.lastCompression.originalMessages,
|
|
346413
|
+
compressedMessages: this.lastCompression.compressedMessages,
|
|
346414
|
+
compressedTokens: this.lastCompression.compressedTokens,
|
|
346415
|
+
model: this.lastCompression.model,
|
|
346416
|
+
fallback: this.lastCompression.fallback
|
|
346417
|
+
} : null,
|
|
346418
|
+
cache: {
|
|
346419
|
+
entries: this.compressionCache.length,
|
|
346420
|
+
totalFoldedEntries: this.compressionCache.reduce((sum, item) => sum + item.foldedEntries, 0),
|
|
346421
|
+
totalFoldedChars: this.compressionCache.reduce((sum, item) => sum + item.foldedChars, 0),
|
|
346422
|
+
ids: this.compressionCache.map((item) => item.id)
|
|
346423
|
+
},
|
|
346424
|
+
protectedZone: {
|
|
346425
|
+
preserveRecentMessages: this.config.getNum("context", "preserve_recent_messages") || 5,
|
|
346426
|
+
protectedStartIndex,
|
|
346427
|
+
lastUserMessageIndex: lastUserIndex,
|
|
346428
|
+
protectedCount: protectedStartIndex >= 0 ? this.history.length - protectedStartIndex : 0
|
|
346429
|
+
},
|
|
346430
|
+
displayHistory: { untouched: true, messageCount: this.chatMessages.length }
|
|
346431
|
+
}, null, 2),
|
|
346432
|
+
metadata: { kind: "context-history-status" }
|
|
346433
|
+
};
|
|
346434
|
+
}
|
|
346296
346435
|
return { ok: false, output: `[context_history_manage] Unknown action: ${action}`, error: `Unknown action: ${action}` };
|
|
346297
346436
|
}
|
|
346298
346437
|
recordContextCompressionStep() {
|
|
@@ -346693,6 +346832,7 @@ ${summary}` };
|
|
|
346693
346832
|
model: fallbackUsed ? "model-switch-segmented-with-fallback" : modelName,
|
|
346694
346833
|
fallback: fallbackUsed
|
|
346695
346834
|
};
|
|
346835
|
+
this.pushCompressionCacheEntry(summary2, originalMessages.slice(0, recentStart), "model-switch", fallbackUsed);
|
|
346696
346836
|
this.persistCompressedHistory(summary2, recent.length, candidate2);
|
|
346697
346837
|
this.saveWorkspaceConversationState(true);
|
|
346698
346838
|
return { compressed: true, rounds, segments, droppedMessages: 0, estimatedTokens: this.estimateContextTokens(candidate2), maxTokens };
|
|
@@ -346726,6 +346866,7 @@ ${summary}` };
|
|
|
346726
346866
|
model: fallbackUsed ? "model-switch-segmented-with-fallback" : modelName,
|
|
346727
346867
|
fallback: fallbackUsed || droppedMessages > 0
|
|
346728
346868
|
};
|
|
346869
|
+
this.pushCompressionCacheEntry(summary, originalMessages.slice(0, recentStart), "model-switch", fallbackUsed || droppedMessages > 0);
|
|
346729
346870
|
this.persistCompressedHistory(summary, recent.length, candidate);
|
|
346730
346871
|
this.saveWorkspaceConversationState(true);
|
|
346731
346872
|
return { compressed: true, rounds, segments, droppedMessages, estimatedTokens: this.estimateContextTokens(candidate), maxTokens };
|
|
@@ -349006,6 +349147,7 @@ Falling back to built-in engine.` }];
|
|
|
349006
349147
|
model: compression.model,
|
|
349007
349148
|
fallback: compression.fallback
|
|
349008
349149
|
};
|
|
349150
|
+
this.pushCompressionCacheEntry(compression.summary, middle, compression.model, compression.fallback);
|
|
349009
349151
|
this.persistCompressedHistory(compression.summary, recent.length, msgs);
|
|
349010
349152
|
}
|
|
349011
349153
|
async buildCompressionSummary(middle, totalChars, budget, provider, signal, compressionModel, currentInstruction = "") {
|
|
@@ -349154,6 +349296,48 @@ ${text.slice(-tailChars).trimStart()}`;
|
|
|
349154
349296
|
}
|
|
349155
349297
|
if (this.isSubagentRuntime) this.subagentContextPersist?.(this.history.map((message) => ({ ...message })), this.lastCompression);
|
|
349156
349298
|
}
|
|
349299
|
+
pushCompressionCacheEntry(summary, messages, model, fallback) {
|
|
349300
|
+
if (!messages.length) return;
|
|
349301
|
+
const foldedChars = messages.reduce((sum, message) => sum + (typeof message.content === "string" ? message.content.length : JSON.stringify(message.content || "").length), 0);
|
|
349302
|
+
this.compressionCache.push({
|
|
349303
|
+
id: `ctx-cache-${this.nextCompressionCacheId}`,
|
|
349304
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
349305
|
+
summary,
|
|
349306
|
+
messages: messages.map((message) => ({ ...message })),
|
|
349307
|
+
foldedEntries: messages.length,
|
|
349308
|
+
foldedChars,
|
|
349309
|
+
model,
|
|
349310
|
+
fallback
|
|
349311
|
+
});
|
|
349312
|
+
this.nextCompressionCacheId += 1;
|
|
349313
|
+
const maxEntries = Math.max(0, Math.floor(this.config.getNum("context", "compression_cache_max") || 8));
|
|
349314
|
+
if (this.compressionCache.length > maxEntries) {
|
|
349315
|
+
this.compressionCache = this.compressionCache.slice(this.compressionCache.length - maxEntries);
|
|
349316
|
+
}
|
|
349317
|
+
this.saveWorkspaceConversationState(true);
|
|
349318
|
+
}
|
|
349319
|
+
contextHistoryProtectedStartIndex() {
|
|
349320
|
+
const preserve = Math.max(0, Math.floor(this.config.getNum("context", "preserve_recent_messages") || 5));
|
|
349321
|
+
const lastUserIndex = this.history.map((message) => String(message.role || "")).lastIndexOf("user");
|
|
349322
|
+
const candidates = [];
|
|
349323
|
+
if (preserve > 0 && this.history.length > 0) candidates.push(Math.max(0, this.history.length - preserve));
|
|
349324
|
+
if (lastUserIndex >= 0) candidates.push(lastUserIndex);
|
|
349325
|
+
return candidates.length ? Math.min(...candidates) : -1;
|
|
349326
|
+
}
|
|
349327
|
+
contextHistoryProtectedZone() {
|
|
349328
|
+
const start = this.contextHistoryProtectedStartIndex();
|
|
349329
|
+
const zone = /* @__PURE__ */ new Set();
|
|
349330
|
+
if (start >= 0) for (let i4 = start; i4 < this.history.length; i4 += 1) zone.add(i4);
|
|
349331
|
+
return zone;
|
|
349332
|
+
}
|
|
349333
|
+
snippetAround(content, query, radius = 150) {
|
|
349334
|
+
const text = String(content || "");
|
|
349335
|
+
const index = text.toLowerCase().indexOf(query.toLowerCase());
|
|
349336
|
+
if (index < 0) return text.slice(0, radius * 2);
|
|
349337
|
+
const from = Math.max(0, index - radius);
|
|
349338
|
+
const to = Math.min(text.length, index + query.length + radius);
|
|
349339
|
+
return `${from > 0 ? "\u2026" : ""}${text.slice(from, to).trim()}${to < text.length ? "\u2026" : ""}`;
|
|
349340
|
+
}
|
|
349157
349341
|
buildSystemPrompt() {
|
|
349158
349342
|
const cwd = this.workspace.current?.path || this.rootPath;
|
|
349159
349343
|
const enabledSkills = this.skills.active();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "newmark-agent",
|
|
3
3
|
"productName": "Newmark Agent",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.8",
|
|
5
5
|
"description": "Newmark Agent — Portable AI coding agent with rich GUI and CLI (TypeScript)",
|
|
6
6
|
"homepage": "https://github.com/positer/Newmark-Agent",
|
|
7
7
|
"repository": {
|