@bastani/atomic 0.8.26-alpha.9 → 0.8.27-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/dist/builtin/intercom/CHANGELOG.md +18 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/CHANGELOG.md +22 -0
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +27 -0
- package/dist/builtin/subagents/agents/codebase-analyzer.md +1 -1
- package/dist/builtin/subagents/agents/codebase-locator.md +1 -1
- package/dist/builtin/subagents/agents/codebase-pattern-finder.md +1 -1
- package/dist/builtin/subagents/agents/codebase-research-analyzer.md +1 -1
- package/dist/builtin/subagents/agents/codebase-research-locator.md +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/CHANGELOG.md +18 -0
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +34 -0
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +4 -2
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/context-compaction.d.ts +124 -11
- package/dist/core/compaction/context-compaction.d.ts.map +1 -1
- package/dist/core/compaction/context-compaction.js +1056 -81
- package/dist/core/compaction/context-compaction.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/docs/compaction.md +120 -37
- package/docs/extensions.md +4 -4
- package/docs/json.md +1 -1
- package/docs/rpc.md +4 -4
- package/docs/sdk.md +2 -0
- package/docs/session-format.md +1 -1
- package/docs/sessions.md +1 -1
- package/docs/settings.md +1 -1
- package/docs/usage.md +2 -2
- package/package.json +1 -1
|
@@ -1,11 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Agent } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { createAssistantMessageEventStream, isContextOverflow, streamSimple, StringEnum, } from "@earendil-works/pi-ai";
|
|
3
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
2
6
|
import { Type } from "typebox";
|
|
3
7
|
import { createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, } from "../messages.js";
|
|
4
8
|
import { buildContextDeletionFilteredPath, buildContextDeletionFilters, } from "../session-manager.js";
|
|
5
9
|
import { estimateTokens } from "./compaction.js";
|
|
6
10
|
export const CONTEXT_COMPACTION_PROMPT_VERSION = 1;
|
|
7
|
-
const
|
|
8
|
-
const
|
|
11
|
+
const CONTEXT_DELETE_TOOL_NAME = "context_delete";
|
|
12
|
+
const CONTEXT_GREP_DELETE_TOOL_NAME = "context_grep_delete";
|
|
13
|
+
const CONTEXT_SEARCH_TRANSCRIPT_TOOL_NAME = "context_search_transcript";
|
|
14
|
+
const CONTEXT_READ_ENTRY_TOOL_NAME = "context_read_entry";
|
|
15
|
+
export const CONTEXT_COMPACTION_MAX_TURNS = 50;
|
|
16
|
+
const CONTEXT_GREP_DELETE_DEFAULT_MAX_MATCHES = 50;
|
|
17
|
+
const CONTEXT_GREP_DELETE_MAX_REGEX_PATTERN_CHARS = 512;
|
|
18
|
+
const CONTEXT_GREP_DELETE_MAX_REGEX_SCAN_CHARS = 250_000;
|
|
19
|
+
const CONTEXT_MANIFEST_MAX_ENTRIES = 80;
|
|
20
|
+
const CONTEXT_MANIFEST_PREVIEW_CHARS = 240;
|
|
21
|
+
const CONTEXT_CRITICAL_OVERFLOW_RECENT_ENTRY_COUNT = 5;
|
|
22
|
+
const CONTEXT_READ_ENTRY_DEFAULT_MAX_CHARS = 4000;
|
|
23
|
+
const CONTEXT_READ_ENTRY_MAX_CHARS = 12_000;
|
|
24
|
+
const CONTEXT_SEARCH_DEFAULT_MAX_MATCHES = 20;
|
|
25
|
+
const CONTEXT_SEARCH_MAX_MATCHES = 100;
|
|
26
|
+
const CONTEXT_SEARCH_DEFAULT_CONTEXT_CHARS = 160;
|
|
27
|
+
const CONTEXT_SEARCH_MAX_CONTEXT_CHARS = 500;
|
|
28
|
+
const ContextDeleteToolParameters = Type.Object({
|
|
9
29
|
deletions: Type.Array(Type.Object({
|
|
10
30
|
kind: StringEnum(["entry", "content_block"], {
|
|
11
31
|
description: "Delete an entire transcript entry or a single content block within one entry.",
|
|
@@ -17,21 +37,77 @@ const ContextDeletionPlanToolParameters = Type.Object({
|
|
|
17
37
|
})),
|
|
18
38
|
}, { additionalProperties: false }), { description: "Deletion targets only. Protected entries and recent active context must not be included." }),
|
|
19
39
|
}, { additionalProperties: false });
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
description: "
|
|
23
|
-
|
|
40
|
+
const ContextGrepDeleteToolParameters = Type.Object({
|
|
41
|
+
pattern: Type.String({ minLength: 1, description: "Literal text or regular expression to match in transcript text." }),
|
|
42
|
+
regex: Type.Optional(Type.Boolean({ description: "Treat pattern as a JavaScript regular expression. Defaults to false." })),
|
|
43
|
+
caseSensitive: Type.Optional(Type.Boolean({ description: "Use case-sensitive matching. Defaults to false." })),
|
|
44
|
+
target: Type.Optional(StringEnum(["entry", "content_block"], {
|
|
45
|
+
description: "Delete whole matching entries or matching content blocks. Defaults to entry.",
|
|
46
|
+
})),
|
|
47
|
+
maxMatches: Type.Optional(Type.Integer({
|
|
48
|
+
minimum: 1,
|
|
49
|
+
maximum: 200,
|
|
50
|
+
description: "Safety cap. If more unprotected, not-yet-deleted candidate targets are found, no deletions are applied. Defaults to 50.",
|
|
51
|
+
})),
|
|
52
|
+
expectedMatchCount: Type.Optional(Type.Integer({
|
|
53
|
+
minimum: 0,
|
|
54
|
+
description: "Optional safety check. If the match count differs, no deletions are applied.",
|
|
55
|
+
})),
|
|
56
|
+
}, { additionalProperties: false });
|
|
57
|
+
const ContextSearchTranscriptToolParameters = Type.Object({
|
|
58
|
+
pattern: Type.String({ minLength: 1, description: "Literal text or regular expression to search for." }),
|
|
59
|
+
regex: Type.Optional(Type.Boolean({ description: "Treat pattern as a JavaScript regular expression. Defaults to false." })),
|
|
60
|
+
caseSensitive: Type.Optional(Type.Boolean({ description: "Use case-sensitive matching. Defaults to false." })),
|
|
61
|
+
target: Type.Optional(StringEnum(["entry", "content_block"], {
|
|
62
|
+
description: "Search whole entry text or individual content-block text. Defaults to entry.",
|
|
63
|
+
})),
|
|
64
|
+
maxMatches: Type.Optional(Type.Integer({ minimum: 1, maximum: CONTEXT_SEARCH_MAX_MATCHES, description: "Maximum matches to return. Defaults to 20." })),
|
|
65
|
+
contextChars: Type.Optional(Type.Integer({
|
|
66
|
+
minimum: 0,
|
|
67
|
+
maximum: CONTEXT_SEARCH_MAX_CONTEXT_CHARS,
|
|
68
|
+
description: "Characters of context to include before and after each match. Defaults to 160.",
|
|
69
|
+
})),
|
|
70
|
+
}, { additionalProperties: false });
|
|
71
|
+
const ContextReadEntryToolParameters = Type.Object({
|
|
72
|
+
entryId: Type.String({ minLength: 1, description: "Stable transcript entry id to read." }),
|
|
73
|
+
blockIndex: Type.Optional(Type.Integer({ minimum: 0, description: "Optional content block index to read instead of the whole entry text." })),
|
|
74
|
+
offset: Type.Optional(Type.Integer({ minimum: 0, description: "Character offset to begin reading. Defaults to 0." })),
|
|
75
|
+
maxChars: Type.Optional(Type.Integer({
|
|
76
|
+
minimum: 1,
|
|
77
|
+
maximum: CONTEXT_READ_ENTRY_MAX_CHARS,
|
|
78
|
+
description: "Maximum characters to return. Defaults to 4000; keep reads small to avoid overflowing context.",
|
|
79
|
+
})),
|
|
80
|
+
}, { additionalProperties: false });
|
|
81
|
+
const CONTEXT_DELETE_TOOL = {
|
|
82
|
+
name: CONTEXT_DELETE_TOOL_NAME,
|
|
83
|
+
description: "Record context compaction deletion targets directly against the transcript.",
|
|
84
|
+
parameters: ContextDeleteToolParameters,
|
|
85
|
+
};
|
|
86
|
+
const CONTEXT_GREP_DELETE_TOOL = {
|
|
87
|
+
name: CONTEXT_GREP_DELETE_TOOL_NAME,
|
|
88
|
+
description: "Bulk-delete transcript entries or content blocks matching a guarded grep/regex query.",
|
|
89
|
+
parameters: ContextGrepDeleteToolParameters,
|
|
90
|
+
};
|
|
91
|
+
const CONTEXT_SEARCH_TRANSCRIPT_TOOL = {
|
|
92
|
+
name: CONTEXT_SEARCH_TRANSCRIPT_TOOL_NAME,
|
|
93
|
+
description: "Search the full transcript working copy and return small snippets without mutating deletion state.",
|
|
94
|
+
parameters: ContextSearchTranscriptToolParameters,
|
|
95
|
+
};
|
|
96
|
+
const CONTEXT_READ_ENTRY_TOOL = {
|
|
97
|
+
name: CONTEXT_READ_ENTRY_TOOL_NAME,
|
|
98
|
+
description: "Read a small slice of one transcript entry or content block from the full transcript working copy.",
|
|
99
|
+
parameters: ContextReadEntryToolParameters,
|
|
24
100
|
};
|
|
25
|
-
const CONTEXT_COMPACTION_SYSTEM_PROMPT =
|
|
26
|
-
const CONTEXT_COMPACTION_FIXED_PROMPT = `You are a context compaction planner for an AI coding assistant transcript.
|
|
101
|
+
const CONTEXT_COMPACTION_SYSTEM_PROMPT = `You are a context compaction assistant.
|
|
27
102
|
|
|
28
|
-
Your task is deletion-only verbatim compaction
|
|
103
|
+
Your task is to read relevant parts of a conversation between a user and an AI assistant provided via a transcript file, then run a series of tools to apply deletion-only verbatim compaction using the exact context_delete or context_grep_delete format specified.`;
|
|
104
|
+
const CONTEXT_COMPACTION_FIXED_PROMPT = `Reference the provided transcript file transcript and use your search/read tools for small inspections, then use context_delete or context_grep_delete for deletions.
|
|
29
105
|
|
|
30
106
|
You MUST NOT summarize.
|
|
31
107
|
You MUST NOT paraphrase.
|
|
32
108
|
You MUST NOT generate replacement context.
|
|
33
109
|
You MUST NOT mutate retained transcript objects or content.
|
|
34
|
-
|
|
110
|
+
Deletion tool calls are the compaction action; record only deletion targets by stable ID.
|
|
35
111
|
|
|
36
112
|
What Gets Deleted:
|
|
37
113
|
- Redundant tool outputs: file reads already acted on, grep/search results already processed, passing test output no longer needed.
|
|
@@ -47,13 +123,19 @@ What Survives:
|
|
|
47
123
|
- User instructions: The original task and any clarifications.
|
|
48
124
|
|
|
49
125
|
<output_format>
|
|
50
|
-
Call the
|
|
126
|
+
Call the context_delete tool one or more times with deletion targets in this shape:
|
|
51
127
|
{ "deletions": [{ "kind": "entry", "entryId": "..." }] }
|
|
52
128
|
|
|
53
129
|
For content-block deletions, use:
|
|
54
130
|
{ "kind": "content_block", "entryId": "...", "blockIndex": 0 }
|
|
55
131
|
|
|
56
|
-
|
|
132
|
+
The tool applies and validates deletion targets immediately. You can continue calling it for additional deletions if useful.
|
|
133
|
+
|
|
134
|
+
For guarded bulk deletion by text match, call context_grep_delete with a literal pattern or regex. It skips protected context, enforces maxMatches and expectedMatchCount, and validates through the same tool-call/tool-result safety rules.
|
|
135
|
+
|
|
136
|
+
The full transcript is available as a JSONL file path in the prompt, but do NOT try to load the whole file into context. Use context_search_transcript to find candidate entry IDs and context_read_entry to read only small slices (for example maxChars 1000-4000) before deleting.
|
|
137
|
+
|
|
138
|
+
When you are done, reply with a brief plain-text completion message. Do not write deletion JSON or deletion target IDs outside tool calls.
|
|
57
139
|
</output_format>`;
|
|
58
140
|
function getMessageFromEntry(entry) {
|
|
59
141
|
if (entry.type === "message") {
|
|
@@ -239,7 +321,7 @@ function isProtectedEntry(entry, message, recentEntryIds) {
|
|
|
239
321
|
return true;
|
|
240
322
|
return false;
|
|
241
323
|
}
|
|
242
|
-
export function prepareContextCompaction(pathEntries, settings) {
|
|
324
|
+
export function prepareContextCompaction(pathEntries, settings, options = {}) {
|
|
243
325
|
if (pathEntries.length === 0)
|
|
244
326
|
return undefined;
|
|
245
327
|
const latestCompactionIndex = collectLatestSummaryCompactionIndex(pathEntries);
|
|
@@ -251,7 +333,7 @@ export function prepareContextCompaction(pathEntries, settings) {
|
|
|
251
333
|
.map((index) => filteredEntryById.get(pathEntries[index].id))
|
|
252
334
|
.filter((entry) => entry !== undefined && getContextEligibleMessageFromEntry(entry) !== undefined)
|
|
253
335
|
.map((entry) => entry.id);
|
|
254
|
-
const recentEntryIds = new Set(messageEntryIds.slice(-
|
|
336
|
+
const recentEntryIds = new Set(messageEntryIds.slice(-CONTEXT_CRITICAL_OVERFLOW_RECENT_ENTRY_COUNT));
|
|
255
337
|
const protectedEntryIds = new Set();
|
|
256
338
|
const entries = [];
|
|
257
339
|
if (latestCompactionIndex >= 0) {
|
|
@@ -306,6 +388,7 @@ export function prepareContextCompaction(pathEntries, settings) {
|
|
|
306
388
|
return undefined;
|
|
307
389
|
return {
|
|
308
390
|
branchEntries: pathEntries,
|
|
391
|
+
mode: options.mode ?? "standard",
|
|
309
392
|
transcript: {
|
|
310
393
|
entries,
|
|
311
394
|
protectedEntryIds: [...protectedEntryIds],
|
|
@@ -325,6 +408,14 @@ function normalizeRawTarget(target) {
|
|
|
325
408
|
return { kind: "entry", entryId: target.entryId };
|
|
326
409
|
return { kind: "content_block", entryId: target.entryId, blockIndex: target.blockIndex };
|
|
327
410
|
}
|
|
411
|
+
function rawDeletionFromTarget(target) {
|
|
412
|
+
if (target.kind === "entry")
|
|
413
|
+
return { kind: "entry", entryId: target.entryId };
|
|
414
|
+
return { kind: "content_block", entryId: target.entryId, blockIndex: target.blockIndex };
|
|
415
|
+
}
|
|
416
|
+
function deletionRequestFromTargets(targets) {
|
|
417
|
+
return { deletions: targets.map(rawDeletionFromTarget) };
|
|
418
|
+
}
|
|
328
419
|
function getDeletedEntryIds(targets) {
|
|
329
420
|
return new Set(targets.filter((target) => target.kind === "entry").map((target) => target.entryId));
|
|
330
421
|
}
|
|
@@ -347,6 +438,153 @@ function isToolCallBlockDeleted(entry, callId, deletedEntryIds, deletedContentBl
|
|
|
347
438
|
return false;
|
|
348
439
|
return entry.contentBlocks.some((block) => block.toolCallId === callId && deletedBlocks.has(block.blockIndex));
|
|
349
440
|
}
|
|
441
|
+
function toolCallBlockIndexes(entry, callId) {
|
|
442
|
+
return entry.contentBlocks
|
|
443
|
+
.filter((block) => block.toolCallId === callId)
|
|
444
|
+
.map((block) => block.blockIndex);
|
|
445
|
+
}
|
|
446
|
+
function addTarget(targets, target) {
|
|
447
|
+
if (targets.some((existing) => targetKey(existing) === targetKey(target)))
|
|
448
|
+
return false;
|
|
449
|
+
targets.push(target);
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
function deleteEntryTarget(targets, entryId) {
|
|
453
|
+
let changed = false;
|
|
454
|
+
for (let index = targets.length - 1; index >= 0; index--) {
|
|
455
|
+
const target = targets[index];
|
|
456
|
+
if (target.kind === "content_block" && target.entryId === entryId) {
|
|
457
|
+
targets.splice(index, 1);
|
|
458
|
+
changed = true;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return addTarget(targets, { kind: "entry", entryId }) || changed;
|
|
462
|
+
}
|
|
463
|
+
function removeEntryDeletion(targets, entryId) {
|
|
464
|
+
const originalLength = targets.length;
|
|
465
|
+
for (let index = targets.length - 1; index >= 0; index--) {
|
|
466
|
+
const target = targets[index];
|
|
467
|
+
if (target.kind === "entry" && target.entryId === entryId)
|
|
468
|
+
targets.splice(index, 1);
|
|
469
|
+
}
|
|
470
|
+
return targets.length !== originalLength;
|
|
471
|
+
}
|
|
472
|
+
function mergeContextDeletionTargets(baseTargets, additionalTargets) {
|
|
473
|
+
const targets = [...baseTargets];
|
|
474
|
+
for (const target of additionalTargets) {
|
|
475
|
+
if (target.kind === "entry") {
|
|
476
|
+
deleteEntryTarget(targets, target.entryId);
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (!getDeletedEntryIds(targets).has(target.entryId)) {
|
|
480
|
+
addTarget(targets, target);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return targets;
|
|
484
|
+
}
|
|
485
|
+
function canonicalizeEntryTargets(targets, entry) {
|
|
486
|
+
if (entry.protected || getDeletedEntryIds(targets).has(entry.entryId))
|
|
487
|
+
return false;
|
|
488
|
+
const deletedBlocks = getDeletedContentBlocks(targets).get(entry.entryId);
|
|
489
|
+
if (!deletedBlocks || !entry.contentBlocks.every((block) => deletedBlocks.has(block.blockIndex)))
|
|
490
|
+
return false;
|
|
491
|
+
// Only repair/promote when dependency reconciliation reaches this entry. Non-tool entries that
|
|
492
|
+
// request every block individually stay invalid so the assistant must choose explicit entry deletion.
|
|
493
|
+
return deleteEntryTarget(targets, entry.entryId);
|
|
494
|
+
}
|
|
495
|
+
function removeToolCallDeletion(targets, entry, callId) {
|
|
496
|
+
let changed = removeEntryDeletion(targets, entry.entryId);
|
|
497
|
+
const blockIndexes = new Set(toolCallBlockIndexes(entry, callId));
|
|
498
|
+
for (let index = targets.length - 1; index >= 0; index--) {
|
|
499
|
+
const target = targets[index];
|
|
500
|
+
if (target.kind === "content_block" && target.entryId === entry.entryId && blockIndexes.has(target.blockIndex)) {
|
|
501
|
+
targets.splice(index, 1);
|
|
502
|
+
changed = true;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return changed;
|
|
506
|
+
}
|
|
507
|
+
function addToolCallDeletion(targets, entry, callId) {
|
|
508
|
+
if (entry.protected)
|
|
509
|
+
return false;
|
|
510
|
+
let changed = false;
|
|
511
|
+
for (const blockIndex of toolCallBlockIndexes(entry, callId)) {
|
|
512
|
+
if (!getDeletedEntryIds(targets).has(entry.entryId)) {
|
|
513
|
+
changed = addTarget(targets, { kind: "content_block", entryId: entry.entryId, blockIndex }) || changed;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return canonicalizeEntryTargets(targets, entry) || changed;
|
|
517
|
+
}
|
|
518
|
+
let warnedReconciliationNonConvergence = false;
|
|
519
|
+
function reconcileToolDependencies(transcript, initialTargets) {
|
|
520
|
+
const targets = [...initialTargets];
|
|
521
|
+
const callEntries = new Map();
|
|
522
|
+
const entriesWithToolCalls = new Set();
|
|
523
|
+
const resultEntries = new Map();
|
|
524
|
+
for (const entry of transcript.entries) {
|
|
525
|
+
for (const callId of entry.toolCallIds) {
|
|
526
|
+
callEntries.set(callId, entry);
|
|
527
|
+
entriesWithToolCalls.add(entry);
|
|
528
|
+
}
|
|
529
|
+
if (entry.toolResultFor) {
|
|
530
|
+
const results = resultEntries.get(entry.toolResultFor) ?? [];
|
|
531
|
+
results.push(entry);
|
|
532
|
+
resultEntries.set(entry.toolResultFor, results);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
// Bounded fixpoint repair: each pass can add/remove paired call/result targets. In practice this
|
|
536
|
+
// converges within one or two passes; the cap protects against accidental oscillation.
|
|
537
|
+
let changed = true;
|
|
538
|
+
let remainingPasses = Math.max(1, transcript.entries.length * 2);
|
|
539
|
+
while (changed && remainingPasses > 0) {
|
|
540
|
+
changed = false;
|
|
541
|
+
remainingPasses -= 1;
|
|
542
|
+
let deletedEntryIds = getDeletedEntryIds(targets);
|
|
543
|
+
let deletedContentBlocks = getDeletedContentBlocks(targets);
|
|
544
|
+
const recordChange = (nextChanged) => {
|
|
545
|
+
if (!nextChanged)
|
|
546
|
+
return;
|
|
547
|
+
changed = true;
|
|
548
|
+
deletedEntryIds = getDeletedEntryIds(targets);
|
|
549
|
+
deletedContentBlocks = getDeletedContentBlocks(targets);
|
|
550
|
+
};
|
|
551
|
+
for (const [callId, callEntry] of callEntries) {
|
|
552
|
+
const callDeleted = isToolCallBlockDeleted(callEntry, callId, deletedEntryIds, deletedContentBlocks);
|
|
553
|
+
const results = resultEntries.get(callId) ?? [];
|
|
554
|
+
if (callDeleted) {
|
|
555
|
+
const retainedProtectedResult = results.find((entry) => entry.protected && !deletedEntryIds.has(entry.entryId));
|
|
556
|
+
if (retainedProtectedResult) {
|
|
557
|
+
recordChange(removeToolCallDeletion(targets, callEntry, callId));
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
for (const result of results) {
|
|
561
|
+
recordChange(deleteEntryTarget(targets, result.entryId));
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (isToolCallBlockDeleted(callEntry, callId, deletedEntryIds, deletedContentBlocks))
|
|
566
|
+
continue;
|
|
567
|
+
for (const result of results) {
|
|
568
|
+
if (!deletedEntryIds.has(result.entryId))
|
|
569
|
+
continue;
|
|
570
|
+
recordChange(deleteEntryTarget(targets, result.entryId));
|
|
571
|
+
if (callEntry.protected) {
|
|
572
|
+
recordChange(removeEntryDeletion(targets, result.entryId));
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
recordChange(addToolCallDeletion(targets, callEntry, callId));
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
for (const entry of entriesWithToolCalls) {
|
|
579
|
+
recordChange(canonicalizeEntryTargets(targets, entry));
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (changed && !warnedReconciliationNonConvergence) {
|
|
583
|
+
warnedReconciliationNonConvergence = true;
|
|
584
|
+
console.warn(`Context compaction tool dependency reconciliation did not converge within the bounded pass limit; validation will continue with the last reconciled target set. entries=${transcript.entries.length} callEntries=${callEntries.size} targets=${targets.length}`);
|
|
585
|
+
}
|
|
586
|
+
return targets;
|
|
587
|
+
}
|
|
350
588
|
function validateToolDependencies(transcript, targets) {
|
|
351
589
|
const deletedEntryIds = getDeletedEntryIds(targets);
|
|
352
590
|
const deletedContentBlocks = getDeletedContentBlocks(targets);
|
|
@@ -415,15 +653,44 @@ function computeContextCompactionStats(transcript, targets) {
|
|
|
415
653
|
percentReduction,
|
|
416
654
|
};
|
|
417
655
|
}
|
|
418
|
-
|
|
419
|
-
if (!
|
|
420
|
-
|
|
656
|
+
function isCriticalOverflowProtectedEntryDeletable(entry, transcript) {
|
|
657
|
+
if (!entry.protected)
|
|
658
|
+
return true;
|
|
659
|
+
const entryIndex = transcript.entries.findIndex((candidate) => candidate.entryId === entry.entryId);
|
|
660
|
+
if (entryIndex < 0)
|
|
661
|
+
return false;
|
|
662
|
+
const recentBoundary = Math.max(0, transcript.entries.length - CONTEXT_CRITICAL_OVERFLOW_RECENT_ENTRY_COUNT);
|
|
663
|
+
if (entryIndex >= recentBoundary)
|
|
664
|
+
return false;
|
|
665
|
+
if (hasAssistantError(entry.message) || hasToolResultError(entry.message) || hasFailedBashExecution(entry.message)) {
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
return (entry.role === "user" ||
|
|
669
|
+
entry.role === "custom" ||
|
|
670
|
+
entry.role === "branchSummary" ||
|
|
671
|
+
entry.role === "compactionSummary" ||
|
|
672
|
+
entry.entryType === "branch_summary");
|
|
673
|
+
}
|
|
674
|
+
function canDeleteProtectedTargetInMode(transcript, target, mode) {
|
|
675
|
+
if (mode !== "critical_overflow")
|
|
676
|
+
return false;
|
|
677
|
+
const entry = transcript.entries.find((candidate) => candidate.entryId === target.entryId);
|
|
678
|
+
if (!entry || !isCriticalOverflowProtectedEntryDeletable(entry, transcript))
|
|
679
|
+
return false;
|
|
680
|
+
if (target.kind === "entry")
|
|
681
|
+
return true;
|
|
682
|
+
const block = entry.contentBlocks.find((candidate) => candidate.blockIndex === target.blockIndex);
|
|
683
|
+
return block !== undefined;
|
|
684
|
+
}
|
|
685
|
+
export function validateContextDeletionRequest(request, transcript, options = {}) {
|
|
686
|
+
const mode = options.mode ?? "standard";
|
|
687
|
+
if (!request || typeof request !== "object" || !Array.isArray(request.deletions)) {
|
|
688
|
+
throw new Error("Context deletion request must be an object with a deletions array");
|
|
421
689
|
}
|
|
422
690
|
const entryById = new Map(transcript.entries.map((entry) => [entry.entryId, entry]));
|
|
423
691
|
const seen = new Set();
|
|
424
|
-
const deletedEntryIds = new Set();
|
|
425
692
|
const deletedTargets = [];
|
|
426
|
-
for (const deletion of
|
|
693
|
+
for (const deletion of request.deletions) {
|
|
427
694
|
if (!deletion || typeof deletion !== "object") {
|
|
428
695
|
throw new Error("Deletion target must be an object");
|
|
429
696
|
}
|
|
@@ -437,7 +704,7 @@ export function validateContextDeletionPlan(plan, transcript) {
|
|
|
437
704
|
if (!entry) {
|
|
438
705
|
throw new Error(`Unknown deletion target entryId: ${deletion.entryId}`);
|
|
439
706
|
}
|
|
440
|
-
if (entry.protected) {
|
|
707
|
+
if (entry.protected && !canDeleteProtectedTargetInMode(transcript, normalizeRawTarget(deletion), mode)) {
|
|
441
708
|
throw new Error(`Deletion target ${deletion.entryId} is protected`);
|
|
442
709
|
}
|
|
443
710
|
if (deletion.kind === "content_block") {
|
|
@@ -448,7 +715,7 @@ export function validateContextDeletionPlan(plan, transcript) {
|
|
|
448
715
|
if (!block) {
|
|
449
716
|
throw new Error(`Unknown content block ${deletion.blockIndex} for entry ${deletion.entryId}`);
|
|
450
717
|
}
|
|
451
|
-
if (block.protected) {
|
|
718
|
+
if (block.protected && !canDeleteProtectedTargetInMode(transcript, normalizeRawTarget(deletion), mode)) {
|
|
452
719
|
throw new Error(`Content block ${deletion.entryId}:${deletion.blockIndex} is protected`);
|
|
453
720
|
}
|
|
454
721
|
if (entry.contentBlocks.length <= 1) {
|
|
@@ -462,34 +729,34 @@ export function validateContextDeletionPlan(plan, transcript) {
|
|
|
462
729
|
seen.add(key);
|
|
463
730
|
const normalized = normalizeRawTarget(deletion);
|
|
464
731
|
deletedTargets.push(normalized);
|
|
465
|
-
if (normalized.kind === "entry")
|
|
466
|
-
deletedEntryIds.add(normalized.entryId);
|
|
467
732
|
}
|
|
468
|
-
|
|
469
|
-
|
|
733
|
+
const reconciledTargets = reconcileToolDependencies(transcript, deletedTargets);
|
|
734
|
+
const reconciledDeletedEntryIds = getDeletedEntryIds(reconciledTargets);
|
|
735
|
+
for (const target of reconciledTargets) {
|
|
736
|
+
if (target.kind === "content_block" && reconciledDeletedEntryIds.has(target.entryId)) {
|
|
470
737
|
throw new Error(`Deletion target ${targetKey(target)} overlaps with entry deletion`);
|
|
471
738
|
}
|
|
472
739
|
}
|
|
473
|
-
const deletedContentBlocks = getDeletedContentBlocks(
|
|
740
|
+
const deletedContentBlocks = getDeletedContentBlocks(reconciledTargets);
|
|
474
741
|
for (const [entryId, blockIndexes] of deletedContentBlocks) {
|
|
475
742
|
const entry = entryById.get(entryId);
|
|
476
743
|
if (entry?.contentBlocks.every((block) => blockIndexes.has(block.blockIndex))) {
|
|
477
744
|
throw new Error(`Content-block deletions for ${entryId} would remove every content block`);
|
|
478
745
|
}
|
|
479
746
|
}
|
|
480
|
-
validateToolDependencies(transcript,
|
|
481
|
-
const remainingEntries = transcript.entries.filter((entry) => !
|
|
747
|
+
validateToolDependencies(transcript, reconciledTargets);
|
|
748
|
+
const remainingEntries = transcript.entries.filter((entry) => !reconciledDeletedEntryIds.has(entry.entryId));
|
|
482
749
|
if (remainingEntries.length === 0) {
|
|
483
|
-
throw new Error("Deletion
|
|
750
|
+
throw new Error("Deletion request would remove all context entries");
|
|
484
751
|
}
|
|
485
752
|
const hasTaskBearingContext = remainingEntries.some((entry) => entry.role === "user" || (entry.role === "compactionSummary" && entry.protected));
|
|
486
753
|
if (!hasTaskBearingContext) {
|
|
487
|
-
throw new Error("Deletion
|
|
754
|
+
throw new Error("Deletion request would leave no user task in context");
|
|
488
755
|
}
|
|
489
756
|
return {
|
|
490
|
-
deletedTargets,
|
|
757
|
+
deletedTargets: reconciledTargets,
|
|
491
758
|
protectedEntryIds: [...transcript.protectedEntryIds],
|
|
492
|
-
stats: computeContextCompactionStats(transcript,
|
|
759
|
+
stats: computeContextCompactionStats(transcript, reconciledTargets),
|
|
493
760
|
};
|
|
494
761
|
}
|
|
495
762
|
function stripJsonFence(text) {
|
|
@@ -504,25 +771,593 @@ function stripJsonFence(text) {
|
|
|
504
771
|
return trimmed;
|
|
505
772
|
return trimmed.slice(firstLineEnd + 1, -3).trim();
|
|
506
773
|
}
|
|
507
|
-
function
|
|
774
|
+
function contextDeletionRequestFromObject(value, source) {
|
|
508
775
|
if (!value || typeof value !== "object" || !Array.isArray(value.deletions)) {
|
|
509
776
|
throw new Error(`${source} must contain a deletions array`);
|
|
510
777
|
}
|
|
511
778
|
return value;
|
|
512
779
|
}
|
|
513
|
-
|
|
780
|
+
function escapeRegExpLiteral(text) {
|
|
781
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
782
|
+
}
|
|
783
|
+
function formatErrorMessage(error) {
|
|
784
|
+
return error instanceof Error ? error.message : String(error);
|
|
785
|
+
}
|
|
786
|
+
function createContextDeletionToolResult(text, details) {
|
|
787
|
+
return { content: [{ type: "text", text }], details, terminate: false };
|
|
788
|
+
}
|
|
789
|
+
function assertSafeRegexPattern(pattern) {
|
|
790
|
+
if (pattern.length > CONTEXT_GREP_DELETE_MAX_REGEX_PATTERN_CHARS) {
|
|
791
|
+
throw new Error(`Regex pattern is too long (${pattern.length} characters); maximum is ${CONTEXT_GREP_DELETE_MAX_REGEX_PATTERN_CHARS}`);
|
|
792
|
+
}
|
|
793
|
+
// Heuristic ReDoS guard for common catastrophic-backtracking shapes. JavaScript's RegExp engine
|
|
794
|
+
// does not expose a timeout, so reject nested quantified groups and backreferences instead of
|
|
795
|
+
// relying only on transcript scan-size caps.
|
|
796
|
+
const hasNestedQuantifiedGroup = /\((?:[^()\\]|\\.)*[+*](?:[^()\\]|\\.)*\)\s*(?:[+*]|\{\d)/u.test(pattern);
|
|
797
|
+
const hasQuantifiedAlternation = /\((?:[^()\\]|\\.)*\|(?:[^()\\]|\\.)*\)\s*(?:[+*]|\{\d)/u.test(pattern);
|
|
798
|
+
const hasBackreference = /\\[1-9]/u.test(pattern);
|
|
799
|
+
if (hasNestedQuantifiedGroup || hasQuantifiedAlternation || hasBackreference) {
|
|
800
|
+
throw new Error("Regex pattern is not allowed because it may cause excessive backtracking; use a literal pattern or exact deletion targets instead.");
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
function createGrepMatcher(pattern, regex, caseSensitive) {
|
|
804
|
+
if (regex) {
|
|
805
|
+
assertSafeRegexPattern(pattern);
|
|
806
|
+
}
|
|
807
|
+
try {
|
|
808
|
+
return new RegExp(regex ? pattern : escapeRegExpLiteral(pattern), caseSensitive ? "u" : "iu");
|
|
809
|
+
}
|
|
810
|
+
catch (error) {
|
|
811
|
+
throw new Error(`Invalid grep ${regex ? "regex" : "pattern"}: ${formatErrorMessage(error)}`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
function assertSafeRegexScan(scanChars) {
|
|
815
|
+
if (scanChars <= CONTEXT_GREP_DELETE_MAX_REGEX_SCAN_CHARS)
|
|
816
|
+
return;
|
|
817
|
+
throw new Error(`Regex grep would scan ${scanChars} characters; maximum is ${CONTEXT_GREP_DELETE_MAX_REGEX_SCAN_CHARS}. Use a literal pattern or exact deletion targets instead.`);
|
|
818
|
+
}
|
|
819
|
+
function clampInteger(value, defaultValue, minimum, maximum) {
|
|
820
|
+
if (value === undefined)
|
|
821
|
+
return defaultValue;
|
|
822
|
+
return Math.max(minimum, Math.min(maximum, value));
|
|
823
|
+
}
|
|
824
|
+
function textSlice(text, offset, maxChars) {
|
|
825
|
+
return text.slice(offset, Math.min(text.length, offset + maxChars));
|
|
826
|
+
}
|
|
827
|
+
function findMatchIndex(matcher, text) {
|
|
828
|
+
const match = matcher.exec(text);
|
|
829
|
+
matcher.lastIndex = 0;
|
|
830
|
+
return match?.index ?? -1;
|
|
831
|
+
}
|
|
832
|
+
function snippetForMatch(text, matchIndex, contextChars) {
|
|
833
|
+
const start = Math.max(0, matchIndex - contextChars);
|
|
834
|
+
const end = Math.min(text.length, matchIndex + contextChars);
|
|
835
|
+
const prefix = start > 0 ? "…" : "";
|
|
836
|
+
const suffix = end < text.length ? "…" : "";
|
|
837
|
+
return `${prefix}${text.slice(start, end)}${suffix}`;
|
|
838
|
+
}
|
|
839
|
+
function currentTargetDeleted(targets, target) {
|
|
840
|
+
const deletedEntryIds = getDeletedEntryIds(targets);
|
|
841
|
+
if (deletedEntryIds.has(target.entryId))
|
|
842
|
+
return true;
|
|
843
|
+
if (target.kind === "entry")
|
|
844
|
+
return false;
|
|
845
|
+
return getDeletedContentBlocks(targets).get(target.entryId)?.has(target.blockIndex) === true;
|
|
846
|
+
}
|
|
847
|
+
function addGrepCandidate(candidates, matches, seenTargets, candidate, match) {
|
|
848
|
+
const key = targetKey(candidate);
|
|
849
|
+
if (seenTargets.has(key))
|
|
850
|
+
return;
|
|
851
|
+
seenTargets.add(key);
|
|
852
|
+
candidates.push(candidate);
|
|
853
|
+
matches.push(match);
|
|
854
|
+
}
|
|
855
|
+
function copyDeletionTarget(target) {
|
|
856
|
+
return target.kind === "entry"
|
|
857
|
+
? { kind: "entry", entryId: target.entryId }
|
|
858
|
+
: { kind: "content_block", entryId: target.entryId, blockIndex: target.blockIndex };
|
|
859
|
+
}
|
|
860
|
+
class ContextDeletionMemoryStore {
|
|
861
|
+
constructor(transcript) {
|
|
862
|
+
this.deletionTargets = [];
|
|
863
|
+
this.callCount = 0;
|
|
864
|
+
const entryIds = new Set();
|
|
865
|
+
const blockKeys = new Set();
|
|
866
|
+
this.entries = transcript.entries.map((entry) => {
|
|
867
|
+
if (entryIds.has(entry.entryId)) {
|
|
868
|
+
throw new Error(`Duplicate transcript entry id: ${entry.entryId}`);
|
|
869
|
+
}
|
|
870
|
+
entryIds.add(entry.entryId);
|
|
871
|
+
return {
|
|
872
|
+
entryId: entry.entryId,
|
|
873
|
+
role: entry.role,
|
|
874
|
+
protected: entry.protected,
|
|
875
|
+
tokenEstimate: entry.tokenEstimate,
|
|
876
|
+
text: entry.text,
|
|
877
|
+
};
|
|
878
|
+
});
|
|
879
|
+
this.entriesById = new Map(this.entries.map((entry) => [entry.entryId, entry]));
|
|
880
|
+
this.contentBlocks = transcript.entries.flatMap((entry, entryPosition) => entry.contentBlocks.map((block) => {
|
|
881
|
+
if (block.entryId !== entry.entryId) {
|
|
882
|
+
throw new Error(`Transcript content block ${block.entryId}:${block.blockIndex} does not belong to entry ${entry.entryId}`);
|
|
883
|
+
}
|
|
884
|
+
const blockKey = `${block.entryId}:${block.blockIndex}`;
|
|
885
|
+
if (blockKeys.has(blockKey)) {
|
|
886
|
+
throw new Error(`Duplicate transcript content block: ${blockKey}`);
|
|
887
|
+
}
|
|
888
|
+
blockKeys.add(blockKey);
|
|
889
|
+
return {
|
|
890
|
+
entryPosition,
|
|
891
|
+
entryId: block.entryId,
|
|
892
|
+
blockIndex: block.blockIndex,
|
|
893
|
+
type: block.type,
|
|
894
|
+
protected: block.protected,
|
|
895
|
+
tokenEstimate: block.tokenEstimate,
|
|
896
|
+
text: block.text,
|
|
897
|
+
};
|
|
898
|
+
}));
|
|
899
|
+
this.contentBlockCountByEntryId = new Map();
|
|
900
|
+
for (const block of this.contentBlocks) {
|
|
901
|
+
this.contentBlockCountByEntryId.set(block.entryId, (this.contentBlockCountByEntryId.get(block.entryId) ?? 0) + 1);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
transaction(operation) {
|
|
905
|
+
const snapshot = this.snapshot();
|
|
906
|
+
try {
|
|
907
|
+
return operation();
|
|
908
|
+
}
|
|
909
|
+
catch (error) {
|
|
910
|
+
this.restore(snapshot);
|
|
911
|
+
throw error;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
readTargets() {
|
|
915
|
+
return this.deletionTargets.map(copyDeletionTarget);
|
|
916
|
+
}
|
|
917
|
+
replaceTargets(targets) {
|
|
918
|
+
this.deletionTargets = targets.map(copyDeletionTarget);
|
|
919
|
+
}
|
|
920
|
+
listEntriesForGrep() {
|
|
921
|
+
return this.entries.map((entry) => ({
|
|
922
|
+
entry_id: entry.entryId,
|
|
923
|
+
text: entry.text,
|
|
924
|
+
is_protected: entry.protected ? 1 : 0,
|
|
925
|
+
}));
|
|
926
|
+
}
|
|
927
|
+
listContentBlocksForGrep() {
|
|
928
|
+
return [...this.contentBlocks]
|
|
929
|
+
.sort((a, b) => a.entryPosition - b.entryPosition || a.blockIndex - b.blockIndex)
|
|
930
|
+
.map((block) => ({
|
|
931
|
+
entry_id: block.entryId,
|
|
932
|
+
block_index: block.blockIndex,
|
|
933
|
+
text: block.text,
|
|
934
|
+
entry_protected: this.entriesById.get(block.entryId)?.protected ? 1 : 0,
|
|
935
|
+
block_protected: block.protected ? 1 : 0,
|
|
936
|
+
block_count: this.contentBlockCountByEntryId.get(block.entryId) ?? 0,
|
|
937
|
+
}));
|
|
938
|
+
}
|
|
939
|
+
getEntryForRead(entryId) {
|
|
940
|
+
const entry = this.entriesById.get(entryId);
|
|
941
|
+
if (!entry)
|
|
942
|
+
return undefined;
|
|
943
|
+
return {
|
|
944
|
+
entry_id: entry.entryId,
|
|
945
|
+
role: entry.role,
|
|
946
|
+
is_protected: entry.protected ? 1 : 0,
|
|
947
|
+
token_estimate: entry.tokenEstimate,
|
|
948
|
+
text: entry.text,
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
getContentBlockForRead(entryId, blockIndex) {
|
|
952
|
+
const block = this.contentBlocks.find((candidate) => candidate.entryId === entryId && candidate.blockIndex === blockIndex);
|
|
953
|
+
if (!block)
|
|
954
|
+
return undefined;
|
|
955
|
+
return {
|
|
956
|
+
entry_id: block.entryId,
|
|
957
|
+
block_index: block.blockIndex,
|
|
958
|
+
type: block.type,
|
|
959
|
+
token_estimate: block.tokenEstimate,
|
|
960
|
+
text: block.text,
|
|
961
|
+
entry_protected: this.entriesById.get(block.entryId)?.protected ? 1 : 0,
|
|
962
|
+
block_protected: block.protected ? 1 : 0,
|
|
963
|
+
block_count: this.contentBlockCountByEntryId.get(block.entryId) ?? 0,
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
getGrepScanTextLength(target) {
|
|
967
|
+
const texts = target === "entry" ? this.entries : this.contentBlocks;
|
|
968
|
+
return texts.reduce((sum, item) => sum + item.text.length, 0);
|
|
969
|
+
}
|
|
970
|
+
incrementCallCount() {
|
|
971
|
+
this.callCount += 1;
|
|
972
|
+
return this.callCount;
|
|
973
|
+
}
|
|
974
|
+
getCallCount() {
|
|
975
|
+
return this.callCount;
|
|
976
|
+
}
|
|
977
|
+
setLastError(message) {
|
|
978
|
+
this.lastError = message;
|
|
979
|
+
}
|
|
980
|
+
clearLastError() {
|
|
981
|
+
this.lastError = undefined;
|
|
982
|
+
}
|
|
983
|
+
getLastError() {
|
|
984
|
+
return this.lastError;
|
|
985
|
+
}
|
|
986
|
+
snapshot() {
|
|
987
|
+
return {
|
|
988
|
+
deletionTargets: this.readTargets(),
|
|
989
|
+
callCount: this.callCount,
|
|
990
|
+
...(this.lastError === undefined ? {} : { lastError: this.lastError }),
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
restore(snapshot) {
|
|
994
|
+
this.deletionTargets = snapshot.deletionTargets.map(copyDeletionTarget);
|
|
995
|
+
this.callCount = snapshot.callCount;
|
|
996
|
+
this.lastError = snapshot.lastError;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
function createContextDeletionStore(transcript) {
|
|
1000
|
+
return new ContextDeletionMemoryStore(transcript);
|
|
1001
|
+
}
|
|
1002
|
+
export function createContextDeletionTool(transcript, options = {}) {
|
|
1003
|
+
const mode = options.mode ?? "standard";
|
|
1004
|
+
const store = createContextDeletionStore(transcript);
|
|
1005
|
+
let validatedResult;
|
|
1006
|
+
function readTargets() {
|
|
1007
|
+
return store.readTargets();
|
|
1008
|
+
}
|
|
1009
|
+
function applyValidatedTargets(additionalTargets) {
|
|
1010
|
+
const mergedTargets = mergeContextDeletionTargets(readTargets(), additionalTargets);
|
|
1011
|
+
validatedResult = validateContextDeletionRequest(deletionRequestFromTargets(mergedTargets), transcript, { mode });
|
|
1012
|
+
store.replaceTargets(validatedResult.deletedTargets);
|
|
1013
|
+
return validatedResult;
|
|
1014
|
+
}
|
|
1015
|
+
function currentStats() {
|
|
1016
|
+
return validatedResult?.stats ?? computeContextCompactionStats(transcript, readTargets());
|
|
1017
|
+
}
|
|
1018
|
+
function canDeleteProtectedTarget(target) {
|
|
1019
|
+
return canDeleteProtectedTargetInMode(transcript, target, mode);
|
|
1020
|
+
}
|
|
1021
|
+
const tool = {
|
|
1022
|
+
...CONTEXT_DELETE_TOOL,
|
|
1023
|
+
label: "context deletion request",
|
|
1024
|
+
executionMode: "parallel",
|
|
1025
|
+
async execute(_toolCallId, params) {
|
|
1026
|
+
return store.transaction(() => {
|
|
1027
|
+
const callCount = store.incrementCallCount();
|
|
1028
|
+
try {
|
|
1029
|
+
const incomingRequest = contextDeletionRequestFromObject(params, `${CONTEXT_DELETE_TOOL_NAME} arguments`);
|
|
1030
|
+
const incomingValidated = validateContextDeletionRequest(incomingRequest, transcript, { mode });
|
|
1031
|
+
const applied = applyValidatedTargets(incomingValidated.deletedTargets);
|
|
1032
|
+
store.clearLastError();
|
|
1033
|
+
const deletedTargets = readTargets();
|
|
1034
|
+
const details = {
|
|
1035
|
+
deletions: deletionRequestFromTargets(deletedTargets).deletions,
|
|
1036
|
+
deletedTargets,
|
|
1037
|
+
stats: applied.stats,
|
|
1038
|
+
callCount,
|
|
1039
|
+
};
|
|
1040
|
+
const text = `Recorded ${incomingValidated.deletedTargets.length} deletion target(s); ${deletedTargets.length} total validated deletion target(s) are selected. Continue calling ${CONTEXT_DELETE_TOOL_NAME} or ${CONTEXT_GREP_DELETE_TOOL_NAME} for additional deletions, or respond done when finished.`;
|
|
1041
|
+
return createContextDeletionToolResult(text, details);
|
|
1042
|
+
}
|
|
1043
|
+
catch (error) {
|
|
1044
|
+
const message = formatErrorMessage(error);
|
|
1045
|
+
store.setLastError(message);
|
|
1046
|
+
const deletedTargets = readTargets();
|
|
1047
|
+
const details = {
|
|
1048
|
+
deletions: deletionRequestFromTargets(deletedTargets).deletions,
|
|
1049
|
+
deletedTargets,
|
|
1050
|
+
stats: currentStats(),
|
|
1051
|
+
callCount,
|
|
1052
|
+
error: message,
|
|
1053
|
+
};
|
|
1054
|
+
return createContextDeletionToolResult(`Error recording context deletion targets: ${message}. No new deletion targets were applied; continue with a corrected tool call.`, details);
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
},
|
|
1058
|
+
};
|
|
1059
|
+
const grepTool = {
|
|
1060
|
+
...CONTEXT_GREP_DELETE_TOOL,
|
|
1061
|
+
label: "context grep delete",
|
|
1062
|
+
executionMode: "parallel",
|
|
1063
|
+
async execute(_toolCallId, params) {
|
|
1064
|
+
return store.transaction(() => {
|
|
1065
|
+
const callCount = store.incrementCallCount();
|
|
1066
|
+
const pattern = params.pattern;
|
|
1067
|
+
const regex = params.regex === true;
|
|
1068
|
+
const caseSensitive = params.caseSensitive === true;
|
|
1069
|
+
const target = params.target ?? "entry";
|
|
1070
|
+
const maxMatches = params.maxMatches ?? CONTEXT_GREP_DELETE_DEFAULT_MAX_MATCHES;
|
|
1071
|
+
const candidates = [];
|
|
1072
|
+
const matches = [];
|
|
1073
|
+
const skipped = [];
|
|
1074
|
+
const seenTargets = new Set();
|
|
1075
|
+
try {
|
|
1076
|
+
if (regex) {
|
|
1077
|
+
assertSafeRegexScan(store.getGrepScanTextLength(target));
|
|
1078
|
+
}
|
|
1079
|
+
const matcher = createGrepMatcher(pattern, regex, caseSensitive);
|
|
1080
|
+
const currentTargets = readTargets();
|
|
1081
|
+
if (target === "entry") {
|
|
1082
|
+
for (const entry of store.listEntriesForGrep()) {
|
|
1083
|
+
if (!matcher.test(entry.text))
|
|
1084
|
+
continue;
|
|
1085
|
+
const candidate = { kind: "entry", entryId: entry.entry_id };
|
|
1086
|
+
if (entry.is_protected === 1 && !canDeleteProtectedTarget(candidate)) {
|
|
1087
|
+
skipped.push({ entryId: entry.entry_id, target, reason: "protected_entry", text: entry.text });
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
if (currentTargetDeleted(currentTargets, candidate)) {
|
|
1091
|
+
skipped.push({ entryId: entry.entry_id, target, reason: "already_deleted", text: entry.text });
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
addGrepCandidate(candidates, matches, seenTargets, candidate, {
|
|
1095
|
+
entryId: entry.entry_id,
|
|
1096
|
+
target,
|
|
1097
|
+
text: entry.text,
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
else {
|
|
1102
|
+
for (const block of store.listContentBlocksForGrep()) {
|
|
1103
|
+
if (!matcher.test(block.text))
|
|
1104
|
+
continue;
|
|
1105
|
+
const candidate = block.block_count <= 1
|
|
1106
|
+
? { kind: "entry", entryId: block.entry_id }
|
|
1107
|
+
: { kind: "content_block", entryId: block.entry_id, blockIndex: block.block_index };
|
|
1108
|
+
if (block.entry_protected === 1 && !canDeleteProtectedTarget(candidate)) {
|
|
1109
|
+
skipped.push({
|
|
1110
|
+
entryId: block.entry_id,
|
|
1111
|
+
target,
|
|
1112
|
+
blockIndex: block.block_index,
|
|
1113
|
+
reason: "protected_entry",
|
|
1114
|
+
text: block.text,
|
|
1115
|
+
});
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
if (block.block_protected === 1 && !canDeleteProtectedTarget(candidate)) {
|
|
1119
|
+
skipped.push({
|
|
1120
|
+
entryId: block.entry_id,
|
|
1121
|
+
target,
|
|
1122
|
+
blockIndex: block.block_index,
|
|
1123
|
+
reason: "protected_block",
|
|
1124
|
+
text: block.text,
|
|
1125
|
+
});
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
if (currentTargetDeleted(currentTargets, candidate)) {
|
|
1129
|
+
skipped.push({
|
|
1130
|
+
entryId: block.entry_id,
|
|
1131
|
+
target: candidate.kind,
|
|
1132
|
+
...(candidate.kind === "content_block" ? { blockIndex: candidate.blockIndex } : {}),
|
|
1133
|
+
reason: "already_deleted",
|
|
1134
|
+
text: block.text,
|
|
1135
|
+
});
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
1138
|
+
addGrepCandidate(candidates, matches, seenTargets, candidate, {
|
|
1139
|
+
entryId: block.entry_id,
|
|
1140
|
+
target: candidate.kind,
|
|
1141
|
+
...(candidate.kind === "content_block" ? { blockIndex: candidate.blockIndex } : {}),
|
|
1142
|
+
text: block.text,
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
let applied;
|
|
1147
|
+
if (params.expectedMatchCount !== undefined && candidates.length !== params.expectedMatchCount) {
|
|
1148
|
+
skipped.push({ reason: "expected_match_count_mismatch" });
|
|
1149
|
+
}
|
|
1150
|
+
else if (candidates.length > maxMatches) {
|
|
1151
|
+
skipped.push({ reason: "max_matches_exceeded" });
|
|
1152
|
+
}
|
|
1153
|
+
else if (candidates.length > 0) {
|
|
1154
|
+
applied = applyValidatedTargets(candidates);
|
|
1155
|
+
}
|
|
1156
|
+
store.clearLastError();
|
|
1157
|
+
const deletedTargets = readTargets();
|
|
1158
|
+
const details = {
|
|
1159
|
+
pattern,
|
|
1160
|
+
regex,
|
|
1161
|
+
caseSensitive,
|
|
1162
|
+
target,
|
|
1163
|
+
matches,
|
|
1164
|
+
skipped,
|
|
1165
|
+
deletedTargets,
|
|
1166
|
+
stats: applied?.stats ?? currentStats(),
|
|
1167
|
+
callCount,
|
|
1168
|
+
};
|
|
1169
|
+
const text = `Matched ${matches.length} deletion target(s), skipped ${skipped.length}, and ${applied ? "applied" : "did not apply"} grep deletion for pattern ${JSON.stringify(pattern)}. Total validated deletion target(s): ${deletedTargets.length}.`;
|
|
1170
|
+
return createContextDeletionToolResult(text, details);
|
|
1171
|
+
}
|
|
1172
|
+
catch (error) {
|
|
1173
|
+
const message = formatErrorMessage(error);
|
|
1174
|
+
store.setLastError(message);
|
|
1175
|
+
const deletedTargets = readTargets();
|
|
1176
|
+
const details = {
|
|
1177
|
+
pattern,
|
|
1178
|
+
regex,
|
|
1179
|
+
caseSensitive,
|
|
1180
|
+
target,
|
|
1181
|
+
matches,
|
|
1182
|
+
skipped,
|
|
1183
|
+
deletedTargets,
|
|
1184
|
+
stats: currentStats(),
|
|
1185
|
+
callCount,
|
|
1186
|
+
error: message,
|
|
1187
|
+
};
|
|
1188
|
+
return createContextDeletionToolResult(`Error applying grep deletion for pattern ${JSON.stringify(pattern)}: ${message}. No new deletion targets were applied; continue with a corrected tool call.`, details);
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
},
|
|
1192
|
+
};
|
|
1193
|
+
const searchTool = {
|
|
1194
|
+
...CONTEXT_SEARCH_TRANSCRIPT_TOOL,
|
|
1195
|
+
label: "context transcript search",
|
|
1196
|
+
executionMode: "parallel",
|
|
1197
|
+
async execute(_toolCallId, params) {
|
|
1198
|
+
return store.transaction(() => {
|
|
1199
|
+
const callCount = store.incrementCallCount();
|
|
1200
|
+
const pattern = params.pattern;
|
|
1201
|
+
const regex = params.regex === true;
|
|
1202
|
+
const caseSensitive = params.caseSensitive === true;
|
|
1203
|
+
const target = params.target ?? "entry";
|
|
1204
|
+
const maxMatches = clampInteger(params.maxMatches, CONTEXT_SEARCH_DEFAULT_MAX_MATCHES, 1, CONTEXT_SEARCH_MAX_MATCHES);
|
|
1205
|
+
const contextChars = clampInteger(params.contextChars, CONTEXT_SEARCH_DEFAULT_CONTEXT_CHARS, 0, CONTEXT_SEARCH_MAX_CONTEXT_CHARS);
|
|
1206
|
+
const matches = [];
|
|
1207
|
+
let truncated = false;
|
|
1208
|
+
try {
|
|
1209
|
+
if (regex) {
|
|
1210
|
+
assertSafeRegexScan(store.getGrepScanTextLength(target));
|
|
1211
|
+
}
|
|
1212
|
+
const matcher = createGrepMatcher(pattern, regex, caseSensitive);
|
|
1213
|
+
if (target === "entry") {
|
|
1214
|
+
for (const entry of store.listEntriesForGrep()) {
|
|
1215
|
+
const matchIndex = findMatchIndex(matcher, entry.text);
|
|
1216
|
+
if (matchIndex < 0)
|
|
1217
|
+
continue;
|
|
1218
|
+
if (matches.length >= maxMatches) {
|
|
1219
|
+
truncated = true;
|
|
1220
|
+
break;
|
|
1221
|
+
}
|
|
1222
|
+
matches.push({
|
|
1223
|
+
entryId: entry.entry_id,
|
|
1224
|
+
target,
|
|
1225
|
+
matchIndex,
|
|
1226
|
+
snippet: snippetForMatch(entry.text, matchIndex, contextChars),
|
|
1227
|
+
protected: entry.is_protected === 1,
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
else {
|
|
1232
|
+
for (const block of store.listContentBlocksForGrep()) {
|
|
1233
|
+
const matchIndex = findMatchIndex(matcher, block.text);
|
|
1234
|
+
if (matchIndex < 0)
|
|
1235
|
+
continue;
|
|
1236
|
+
if (matches.length >= maxMatches) {
|
|
1237
|
+
truncated = true;
|
|
1238
|
+
break;
|
|
1239
|
+
}
|
|
1240
|
+
matches.push({
|
|
1241
|
+
entryId: block.entry_id,
|
|
1242
|
+
target,
|
|
1243
|
+
blockIndex: block.block_index,
|
|
1244
|
+
matchIndex,
|
|
1245
|
+
snippet: snippetForMatch(block.text, matchIndex, contextChars),
|
|
1246
|
+
protected: block.entry_protected === 1 || block.block_protected === 1,
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
store.clearLastError();
|
|
1251
|
+
const details = {
|
|
1252
|
+
pattern,
|
|
1253
|
+
regex,
|
|
1254
|
+
caseSensitive,
|
|
1255
|
+
target,
|
|
1256
|
+
matches,
|
|
1257
|
+
truncated,
|
|
1258
|
+
callCount,
|
|
1259
|
+
};
|
|
1260
|
+
const text = `Found ${matches.length}${truncated ? "+" : ""} ${target} match(es) for ${JSON.stringify(pattern)}. Use ${CONTEXT_READ_ENTRY_TOOL_NAME} with small maxChars to inspect exact content before deleting.`;
|
|
1261
|
+
return createContextDeletionToolResult(text, details);
|
|
1262
|
+
}
|
|
1263
|
+
catch (error) {
|
|
1264
|
+
const message = formatErrorMessage(error);
|
|
1265
|
+
store.setLastError(message);
|
|
1266
|
+
const details = {
|
|
1267
|
+
pattern,
|
|
1268
|
+
regex,
|
|
1269
|
+
caseSensitive,
|
|
1270
|
+
target,
|
|
1271
|
+
matches,
|
|
1272
|
+
truncated,
|
|
1273
|
+
callCount,
|
|
1274
|
+
error: message,
|
|
1275
|
+
};
|
|
1276
|
+
return createContextDeletionToolResult(`Error searching transcript for ${JSON.stringify(pattern)}: ${message}. Try a literal pattern or narrower query.`, details);
|
|
1277
|
+
}
|
|
1278
|
+
});
|
|
1279
|
+
},
|
|
1280
|
+
};
|
|
1281
|
+
const readEntryTool = {
|
|
1282
|
+
...CONTEXT_READ_ENTRY_TOOL,
|
|
1283
|
+
label: "context read entry",
|
|
1284
|
+
executionMode: "parallel",
|
|
1285
|
+
async execute(_toolCallId, params) {
|
|
1286
|
+
return store.transaction(() => {
|
|
1287
|
+
const callCount = store.incrementCallCount();
|
|
1288
|
+
const offset = clampInteger(params.offset, 0, 0, Number.MAX_SAFE_INTEGER);
|
|
1289
|
+
const maxChars = clampInteger(params.maxChars, CONTEXT_READ_ENTRY_DEFAULT_MAX_CHARS, 1, CONTEXT_READ_ENTRY_MAX_CHARS);
|
|
1290
|
+
try {
|
|
1291
|
+
const row = params.blockIndex === undefined
|
|
1292
|
+
? store.getEntryForRead(params.entryId)
|
|
1293
|
+
: store.getContentBlockForRead(params.entryId, params.blockIndex);
|
|
1294
|
+
if (!row) {
|
|
1295
|
+
throw new Error(params.blockIndex === undefined
|
|
1296
|
+
? `Unknown transcript entry: ${params.entryId}`
|
|
1297
|
+
: `Unknown transcript content block: ${params.entryId}:${params.blockIndex}`);
|
|
1298
|
+
}
|
|
1299
|
+
const text = row.text;
|
|
1300
|
+
const slice = textSlice(text, offset, maxChars);
|
|
1301
|
+
store.clearLastError();
|
|
1302
|
+
const details = {
|
|
1303
|
+
entryId: params.entryId,
|
|
1304
|
+
...(params.blockIndex === undefined ? {} : { blockIndex: params.blockIndex }),
|
|
1305
|
+
offset,
|
|
1306
|
+
maxChars,
|
|
1307
|
+
totalChars: text.length,
|
|
1308
|
+
text: slice,
|
|
1309
|
+
truncatedBefore: offset > 0,
|
|
1310
|
+
truncatedAfter: offset + maxChars < text.length,
|
|
1311
|
+
callCount,
|
|
1312
|
+
};
|
|
1313
|
+
const textResult = `Read ${slice.length} of ${text.length} characters from ${params.blockIndex === undefined ? params.entryId : `${params.entryId}:${params.blockIndex}`}. Keep reads small; increase offset for the next slice if needed.`;
|
|
1314
|
+
return createContextDeletionToolResult(textResult, details);
|
|
1315
|
+
}
|
|
1316
|
+
catch (error) {
|
|
1317
|
+
const message = formatErrorMessage(error);
|
|
1318
|
+
store.setLastError(message);
|
|
1319
|
+
const details = {
|
|
1320
|
+
entryId: params.entryId,
|
|
1321
|
+
...(params.blockIndex === undefined ? {} : { blockIndex: params.blockIndex }),
|
|
1322
|
+
offset,
|
|
1323
|
+
maxChars,
|
|
1324
|
+
totalChars: 0,
|
|
1325
|
+
text: "",
|
|
1326
|
+
truncatedBefore: false,
|
|
1327
|
+
truncatedAfter: false,
|
|
1328
|
+
callCount,
|
|
1329
|
+
error: message,
|
|
1330
|
+
};
|
|
1331
|
+
return createContextDeletionToolResult(`Error reading transcript entry: ${message}`, details);
|
|
1332
|
+
}
|
|
1333
|
+
});
|
|
1334
|
+
},
|
|
1335
|
+
};
|
|
1336
|
+
return {
|
|
1337
|
+
tool,
|
|
1338
|
+
grepTool,
|
|
1339
|
+
searchTool,
|
|
1340
|
+
readEntryTool,
|
|
1341
|
+
tools: [tool, grepTool, searchTool, readEntryTool],
|
|
1342
|
+
getDeletionRequest: () => deletionRequestFromTargets(readTargets()),
|
|
1343
|
+
getValidatedResult: () => validatedResult,
|
|
1344
|
+
getLastError: () => store.getLastError(),
|
|
1345
|
+
getCallCount: () => store.getCallCount(),
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
export function parseContextDeletionRequest(text) {
|
|
514
1349
|
const stripped = stripJsonFence(text);
|
|
515
1350
|
let parsed;
|
|
516
1351
|
try {
|
|
517
1352
|
parsed = JSON.parse(stripped);
|
|
518
1353
|
}
|
|
519
1354
|
catch (error) {
|
|
520
|
-
throw new Error(`Failed to parse context deletion
|
|
1355
|
+
throw new Error(`Failed to parse context deletion request JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
521
1356
|
}
|
|
522
|
-
return
|
|
1357
|
+
return contextDeletionRequestFromObject(parsed, "Context deletion request JSON");
|
|
523
1358
|
}
|
|
524
|
-
function
|
|
525
|
-
return content.type === "toolCall" && content.name ===
|
|
1359
|
+
function isContextDeleteToolCall(content) {
|
|
1360
|
+
return content.type === "toolCall" && content.name === CONTEXT_DELETE_TOOL_NAME;
|
|
526
1361
|
}
|
|
527
1362
|
function textContentFromResponse(response) {
|
|
528
1363
|
return response.content
|
|
@@ -530,73 +1365,213 @@ function textContentFromResponse(response) {
|
|
|
530
1365
|
.map((content) => content.text)
|
|
531
1366
|
.join("\n");
|
|
532
1367
|
}
|
|
533
|
-
export function
|
|
534
|
-
const toolCalls = response.content.filter(
|
|
1368
|
+
export function parseContextDeletionResponse(response) {
|
|
1369
|
+
const toolCalls = response.content.filter(isContextDeleteToolCall);
|
|
535
1370
|
if (toolCalls.length > 1) {
|
|
536
|
-
throw new Error(`Context compaction
|
|
1371
|
+
throw new Error(`Context compaction assistant called ${CONTEXT_DELETE_TOOL_NAME} more than once`);
|
|
537
1372
|
}
|
|
538
1373
|
const toolCall = toolCalls[0];
|
|
539
1374
|
if (toolCall) {
|
|
540
|
-
return
|
|
1375
|
+
return contextDeletionRequestFromObject(toolCall.arguments, `${CONTEXT_DELETE_TOOL_NAME} arguments`);
|
|
541
1376
|
}
|
|
542
1377
|
const textContent = textContentFromResponse(response);
|
|
543
1378
|
if (textContent.trim().length === 0) {
|
|
544
|
-
throw new Error(`Context compaction
|
|
1379
|
+
throw new Error(`Context compaction assistant did not call ${CONTEXT_DELETE_TOOL_NAME}`);
|
|
545
1380
|
}
|
|
546
|
-
return
|
|
1381
|
+
return parseContextDeletionRequest(textContent);
|
|
547
1382
|
}
|
|
548
1383
|
function truncateForPrompt(text, maxChars) {
|
|
549
1384
|
if (text.length <= maxChars)
|
|
550
1385
|
return text;
|
|
551
|
-
return `${text.slice(0, maxChars)}\n[... ${text.length - maxChars} more characters omitted from
|
|
1386
|
+
return `${text.slice(0, maxChars)}\n[... ${text.length - maxChars} more characters omitted from context compaction prompt]`;
|
|
552
1387
|
}
|
|
553
|
-
function
|
|
554
|
-
return
|
|
555
|
-
.filter((entry) => !isExcludedFromLlmContext(entry.message))
|
|
556
|
-
.map((entry) => ({
|
|
1388
|
+
function transcriptEntryFilePayload(entry) {
|
|
1389
|
+
return {
|
|
557
1390
|
entryId: entry.entryId,
|
|
1391
|
+
entryType: entry.entryType,
|
|
558
1392
|
role: entry.role,
|
|
559
1393
|
protected: entry.protected,
|
|
560
1394
|
tokenEstimate: entry.tokenEstimate,
|
|
561
1395
|
toolCallIds: entry.toolCallIds,
|
|
562
1396
|
toolResultFor: entry.toolResultFor,
|
|
1397
|
+
text: entry.text,
|
|
563
1398
|
contentBlocks: entry.contentBlocks.map((block) => ({
|
|
564
1399
|
blockIndex: block.blockIndex,
|
|
565
1400
|
type: block.type,
|
|
566
1401
|
protected: block.protected,
|
|
567
1402
|
toolCallId: block.toolCallId,
|
|
568
|
-
|
|
1403
|
+
tokenEstimate: block.tokenEstimate,
|
|
1404
|
+
text: block.text,
|
|
569
1405
|
})),
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
function writeContextCompactionTranscriptFile(transcript) {
|
|
1409
|
+
const directory = mkdtempSync(join(tmpdir(), "atomic-context-transcript-"));
|
|
1410
|
+
const path = join(directory, "transcript.jsonl");
|
|
1411
|
+
const lines = transcript.entries
|
|
1412
|
+
.filter((entry) => !isExcludedFromLlmContext(entry.message))
|
|
1413
|
+
.map((entry) => JSON.stringify(transcriptEntryFilePayload(entry)));
|
|
1414
|
+
writeFileSync(path, `${lines.join("\n")}\n`, "utf8");
|
|
1415
|
+
return {
|
|
1416
|
+
path,
|
|
1417
|
+
cleanup: () => rmSync(directory, { recursive: true, force: true }),
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
function contextCompactionTranscriptManifest(transcript, transcriptFilePath) {
|
|
1421
|
+
const eligibleEntries = transcript.entries.filter((entry) => !isExcludedFromLlmContext(entry.message));
|
|
1422
|
+
const selectedEntryIds = new Set();
|
|
1423
|
+
const selectedEntries = [];
|
|
1424
|
+
const addEntry = (entry) => {
|
|
1425
|
+
if (selectedEntryIds.has(entry.entryId) || selectedEntries.length >= CONTEXT_MANIFEST_MAX_ENTRIES)
|
|
1426
|
+
return;
|
|
1427
|
+
selectedEntryIds.add(entry.entryId);
|
|
1428
|
+
selectedEntries.push(entry);
|
|
1429
|
+
};
|
|
1430
|
+
for (const entry of eligibleEntries.filter((entry) => entry.protected)) {
|
|
1431
|
+
addEntry(entry);
|
|
1432
|
+
}
|
|
1433
|
+
for (const entry of [...eligibleEntries]
|
|
1434
|
+
.filter((entry) => !entry.protected)
|
|
1435
|
+
.sort((left, right) => right.tokenEstimate - left.tokenEstimate)) {
|
|
1436
|
+
addEntry(entry);
|
|
1437
|
+
}
|
|
1438
|
+
selectedEntries.sort((left, right) => eligibleEntries.indexOf(left) - eligibleEntries.indexOf(right));
|
|
1439
|
+
return {
|
|
1440
|
+
transcriptFilePath,
|
|
1441
|
+
transcriptFileFormat: "jsonl: one compactable transcript entry per line with full text and contentBlocks text",
|
|
1442
|
+
totalEntries: eligibleEntries.length,
|
|
1443
|
+
manifestEntries: selectedEntries.length,
|
|
1444
|
+
omittedEntries: Math.max(0, eligibleEntries.length - selectedEntries.length),
|
|
1445
|
+
tokensBefore: transcript.tokensBefore,
|
|
1446
|
+
protectedEntryIds: transcript.protectedEntryIds,
|
|
1447
|
+
entries: selectedEntries.map((entry) => ({
|
|
1448
|
+
entryId: entry.entryId,
|
|
1449
|
+
role: entry.role,
|
|
1450
|
+
protected: entry.protected,
|
|
1451
|
+
tokenEstimate: entry.tokenEstimate,
|
|
1452
|
+
toolCallIds: entry.toolCallIds,
|
|
1453
|
+
toolResultFor: entry.toolResultFor,
|
|
1454
|
+
contentBlockCount: entry.contentBlocks.length,
|
|
1455
|
+
contentBlocks: entry.contentBlocks.map((block) => ({
|
|
1456
|
+
blockIndex: block.blockIndex,
|
|
1457
|
+
type: block.type,
|
|
1458
|
+
protected: block.protected,
|
|
1459
|
+
toolCallId: block.toolCallId,
|
|
1460
|
+
tokenEstimate: block.tokenEstimate,
|
|
1461
|
+
})),
|
|
1462
|
+
preview: truncateForPrompt(entry.text, CONTEXT_MANIFEST_PREVIEW_CHARS),
|
|
1463
|
+
})),
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
function contextCompactionModePrompt(mode) {
|
|
1467
|
+
if (mode === "critical_overflow") {
|
|
1468
|
+
return `\n<critical-overflow-mode>\nThe previous model request overflowed its context window. This is a critical LRU-style compaction pass. First delete stale unprotected context. If that is not enough, you may also delete the earliest protected entries or protected content shown in the manifest, especially old user/custom/summary context, while preserving recent entries, unresolved errors, failed commands, and enough task-bearing context for the assistant to continue.\n</critical-overflow-mode>`;
|
|
1469
|
+
}
|
|
1470
|
+
return `\n<standard-mode>\nDo not delete entries or content blocks marked protected. Protected context is only eligible during critical overflow recovery, not during standard compaction.\n</standard-mode>`;
|
|
1471
|
+
}
|
|
1472
|
+
export function buildContextCompactionPrompt(transcript, transcriptFilePath = "<transcript file will be written during context compaction>", mode = "standard") {
|
|
1473
|
+
return `${CONTEXT_COMPACTION_FIXED_PROMPT}${contextCompactionModePrompt(mode)}\n\n<transcript-file>\n${transcriptFilePath}\n</transcript-file>\n\n<context-manifest>\n${JSON.stringify(contextCompactionTranscriptManifest(transcript, transcriptFilePath), null, 2)}\n</context-manifest>`;
|
|
1474
|
+
}
|
|
1475
|
+
function createContextCompactionAssistantMessage(model, content, stopReason, errorMessage) {
|
|
1476
|
+
return {
|
|
1477
|
+
role: "assistant",
|
|
1478
|
+
content,
|
|
1479
|
+
api: model.api,
|
|
1480
|
+
provider: model.provider,
|
|
1481
|
+
model: model.id,
|
|
1482
|
+
usage: {
|
|
1483
|
+
input: 0,
|
|
1484
|
+
output: 0,
|
|
1485
|
+
cacheRead: 0,
|
|
1486
|
+
cacheWrite: 0,
|
|
1487
|
+
totalTokens: 0,
|
|
1488
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
1489
|
+
},
|
|
1490
|
+
stopReason,
|
|
1491
|
+
...(errorMessage !== undefined ? { errorMessage } : {}),
|
|
1492
|
+
timestamp: Date.now(),
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
function createContextCompactionStopStream(model, text) {
|
|
1496
|
+
const stream = createAssistantMessageEventStream();
|
|
1497
|
+
queueMicrotask(() => {
|
|
1498
|
+
const message = createContextCompactionAssistantMessage(model, [{ type: "text", text }], "stop");
|
|
1499
|
+
stream.push({ type: "done", reason: "stop", message });
|
|
1500
|
+
stream.end(message);
|
|
1501
|
+
});
|
|
1502
|
+
return stream;
|
|
1503
|
+
}
|
|
1504
|
+
function isContextCompactionOverflowError(model, errorMessage) {
|
|
1505
|
+
return isContextOverflow(createContextCompactionAssistantMessage(model, [], "error", errorMessage), model.contextWindow);
|
|
1506
|
+
}
|
|
1507
|
+
async function runContextDeletionAssistant(transcript, model, apiKey, headers, signal, thinkingLevel = "off", mode = "standard") {
|
|
1508
|
+
const maxTokens = model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY;
|
|
1509
|
+
if (signal?.aborted) {
|
|
1510
|
+
throw new Error("Context compaction failed: Request was aborted");
|
|
1511
|
+
}
|
|
1512
|
+
const transcriptFile = writeContextCompactionTranscriptFile(transcript);
|
|
1513
|
+
const promptMessage = {
|
|
1514
|
+
role: "user",
|
|
1515
|
+
content: [{ type: "text", text: buildContextCompactionPrompt(transcript, transcriptFile.path, mode) }],
|
|
1516
|
+
timestamp: Date.now(),
|
|
1517
|
+
};
|
|
1518
|
+
const deletionTool = createContextDeletionTool(transcript, { mode });
|
|
1519
|
+
let compactionTurnCount = 0;
|
|
1520
|
+
const agent = new Agent({
|
|
1521
|
+
initialState: {
|
|
1522
|
+
systemPrompt: CONTEXT_COMPACTION_SYSTEM_PROMPT,
|
|
1523
|
+
model,
|
|
1524
|
+
thinkingLevel,
|
|
1525
|
+
tools: deletionTool.tools,
|
|
1526
|
+
},
|
|
1527
|
+
toolExecution: "parallel",
|
|
1528
|
+
streamFn: async (requestModel, context, streamOptions) => {
|
|
1529
|
+
compactionTurnCount += 1;
|
|
1530
|
+
if (compactionTurnCount > CONTEXT_COMPACTION_MAX_TURNS) {
|
|
1531
|
+
return createContextCompactionStopStream(requestModel, `Reached the context compaction turn cap (${CONTEXT_COMPACTION_MAX_TURNS}); using the deletions recorded so far.`);
|
|
1532
|
+
}
|
|
1533
|
+
return streamSimple(requestModel, context, {
|
|
1534
|
+
...streamOptions,
|
|
1535
|
+
maxTokens,
|
|
1536
|
+
apiKey,
|
|
1537
|
+
headers: headers ?? streamOptions?.headers,
|
|
1538
|
+
});
|
|
583
1539
|
},
|
|
584
|
-
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
if (
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
1540
|
+
});
|
|
1541
|
+
const abortOnSignal = () => agent.abort();
|
|
1542
|
+
signal?.addEventListener("abort", abortOnSignal, { once: true });
|
|
1543
|
+
try {
|
|
1544
|
+
await agent.prompt(promptMessage);
|
|
1545
|
+
}
|
|
1546
|
+
finally {
|
|
1547
|
+
signal?.removeEventListener("abort", abortOnSignal);
|
|
1548
|
+
transcriptFile.cleanup();
|
|
1549
|
+
}
|
|
1550
|
+
if (signal?.aborted) {
|
|
1551
|
+
throw new Error("Context compaction failed: Request was aborted");
|
|
1552
|
+
}
|
|
1553
|
+
if (agent.state.errorMessage) {
|
|
1554
|
+
if (isContextCompactionOverflowError(model, agent.state.errorMessage)) {
|
|
1555
|
+
return {
|
|
1556
|
+
validatedResult: deletionTool.getValidatedResult(),
|
|
1557
|
+
lastToolError: deletionTool.getLastError(),
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
throw new Error(`Context compaction failed: ${agent.state.errorMessage}`);
|
|
1561
|
+
}
|
|
1562
|
+
if (deletionTool.getCallCount() === 0) {
|
|
1563
|
+
throw new Error(`Context compaction did not call any transcript inspection or deletion tools (${CONTEXT_SEARCH_TRANSCRIPT_TOOL_NAME}, ${CONTEXT_READ_ENTRY_TOOL_NAME}, ${CONTEXT_DELETE_TOOL_NAME}, or ${CONTEXT_GREP_DELETE_TOOL_NAME})`);
|
|
1564
|
+
}
|
|
1565
|
+
return {
|
|
1566
|
+
validatedResult: deletionTool.getValidatedResult(),
|
|
1567
|
+
lastToolError: deletionTool.getLastError(),
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
export async function contextCompact(preparation, model, apiKey, headers, signal, thinkingLevel = "off", mode = preparation.mode ?? "standard") {
|
|
1571
|
+
const { validatedResult, lastToolError } = await runContextDeletionAssistant(preparation.transcript, model, apiKey, headers, signal, thinkingLevel, mode);
|
|
1572
|
+
if (!validatedResult || validatedResult.deletedTargets.length === 0) {
|
|
1573
|
+
throw new Error(lastToolError ? `No safe context deletions proposed; last deletion tool error: ${lastToolError}` : "No safe context deletions proposed");
|
|
1574
|
+
}
|
|
1575
|
+
return validatedResult;
|
|
601
1576
|
}
|
|
602
1577
|
//# sourceMappingURL=context-compaction.js.map
|