@bastani/atomic 0.8.29-alpha.4 → 0.8.30-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 +27 -0
- package/dist/builtin/cursor/CHANGELOG.md +6 -0
- package/dist/builtin/cursor/package.json +2 -2
- package/dist/builtin/intercom/CHANGELOG.md +7 -1
- package/dist/builtin/intercom/package.json +2 -2
- package/dist/builtin/mcp/CHANGELOG.md +7 -1
- package/dist/builtin/mcp/package.json +3 -3
- package/dist/builtin/subagents/CHANGELOG.md +11 -0
- package/dist/builtin/subagents/README.md +10 -30
- package/dist/builtin/subagents/package.json +4 -4
- package/dist/builtin/subagents/skills/subagent/SKILL.md +5 -11
- package/dist/builtin/subagents/src/agents/agent-management.ts +0 -5
- package/dist/builtin/subagents/src/agents/agent-serializer.ts +7 -3
- package/dist/builtin/subagents/src/agents/agents.ts +4 -29
- package/dist/builtin/subagents/src/agents/chain-serializer.ts +27 -25
- package/dist/builtin/subagents/src/extension/schemas.ts +0 -75
- package/dist/builtin/subagents/src/runs/background/async-execution.ts +0 -29
- package/dist/builtin/subagents/src/runs/background/run-status.ts +1 -2
- package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +134 -239
- package/dist/builtin/subagents/src/runs/foreground/chain-execution.ts +1 -52
- package/dist/builtin/subagents/src/runs/foreground/execution.ts +103 -94
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +0 -10
- package/dist/builtin/subagents/src/runs/shared/dynamic-fanout.ts +16 -8
- package/dist/builtin/subagents/src/runs/shared/model-fallback.ts +0 -1
- package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +0 -3
- package/dist/builtin/subagents/src/runs/shared/structured-output.ts +67 -2
- package/dist/builtin/subagents/src/runs/shared/subagent-control.ts +6 -20
- package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -1
- package/dist/builtin/subagents/src/runs/shared/workflow-graph.ts +2 -6
- package/dist/builtin/subagents/src/shared/settings.ts +1 -4
- package/dist/builtin/subagents/src/shared/types.ts +1 -156
- package/dist/builtin/subagents/src/tui/render.ts +0 -1
- package/dist/builtin/web-access/CHANGELOG.md +7 -1
- package/dist/builtin/web-access/package.json +2 -2
- package/dist/builtin/workflows/CHANGELOG.md +13 -0
- package/dist/builtin/workflows/README.md +2 -2
- package/dist/builtin/workflows/package.json +2 -2
- package/dist/builtin/workflows/src/extension/index.ts +8 -1
- package/dist/builtin/workflows/src/extension/wiring.ts +66 -10
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +70 -19
- package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +98 -14
- package/dist/builtin/workflows/src/runs/shared/model-fallback.ts +0 -1
- package/dist/builtin/workflows/src/shared/persistence-restore.ts +4 -0
- package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +4 -0
- package/dist/builtin/workflows/src/shared/store.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -4
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +2 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +21 -9
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +2 -2
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +6 -0
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +2 -0
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/compaction/context-compaction.d.ts +43 -16
- package/dist/core/compaction/context-compaction.d.ts.map +1 -1
- package/dist/core/compaction/context-compaction.js +518 -189
- package/dist/core/compaction/context-compaction.js.map +1 -1
- package/dist/core/extensions/loader.d.ts +4 -2
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +11 -7
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/types.d.ts +14 -3
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +2 -1
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/package-manager.d.ts +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +52 -18
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/resource-loader.d.ts +20 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +89 -24
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/session-manager.d.ts +14 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +145 -3
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +9 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +16 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/thinking-blocks.d.ts +7 -0
- package/dist/core/thinking-blocks.d.ts.map +1 -0
- package/dist/core/thinking-blocks.js +16 -0
- package/dist/core/thinking-blocks.js.map +1 -0
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +4 -0
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +30 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/tree-selector.js +87 -12
- package/dist/modes/interactive/components/tree-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +37 -18
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +24 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +58 -13
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/utils/child-process.d.ts +9 -4
- package/dist/utils/child-process.d.ts.map +1 -1
- package/dist/utils/child-process.js +42 -10
- package/dist/utils/child-process.js.map +1 -1
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +4 -27
- package/dist/utils/version-check.js.map +1 -1
- package/docs/compaction.md +469 -51
- package/docs/containerization.md +37 -37
- package/docs/extensions.md +23 -14
- package/docs/models.md +6 -4
- package/docs/packages.md +2 -0
- package/docs/providers.md +1 -1
- package/docs/subagents.md +11 -4
- package/docs/workflows.md +4 -2
- package/examples/extensions/README.md +2 -2
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/question.ts +39 -18
- package/examples/extensions/questionnaire.ts +49 -28
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +7 -5
- package/dist/builtin/subagents/src/runs/shared/acceptance.ts +0 -612
- package/dist/builtin/subagents/src/runs/shared/completion-guard.ts +0 -147
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
import { Agent } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import { createAssistantMessageEventStream, isContextOverflow, streamSimple, StringEnum
|
|
2
|
+
import { createAssistantMessageEventStream, isContextOverflow, streamSimple, StringEnum } from "@earendil-works/pi-ai";
|
|
3
3
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { Type } from "typebox";
|
|
7
7
|
import { createBranchSummaryMessage, createCustomMessage } from "../messages.js";
|
|
8
|
-
import {
|
|
8
|
+
import { isAssistantThinkingBlockType, messageHasAssistantThinkingContentBlock, } from "../thinking-blocks.js";
|
|
9
|
+
import { buildContextDeletionFilteredPath, buildEffectiveContextDeletionFilters, } from "../session-manager.js";
|
|
9
10
|
import { estimateTokens } from "./compaction.js";
|
|
10
11
|
export const CONTEXT_COMPACTION_PROMPT_VERSION = 1;
|
|
11
12
|
const CONTEXT_DELETE_TOOL_NAME = "context_delete";
|
|
12
13
|
const CONTEXT_GREP_DELETE_TOOL_NAME = "context_grep_delete";
|
|
13
14
|
const CONTEXT_SEARCH_TRANSCRIPT_TOOL_NAME = "context_search_transcript";
|
|
14
15
|
const CONTEXT_READ_ENTRY_TOOL_NAME = "context_read_entry";
|
|
15
|
-
|
|
16
|
+
const CONTEXT_COMPACTION_BUDGET_TOOL_NAME = "context_compaction_budget";
|
|
17
|
+
export const CONTEXT_COMPACTION_DEFAULT_COMPRESSION_RATIO = 0.5;
|
|
18
|
+
export const CONTEXT_COMPACTION_TARGET_REDUCTION_PERCENT = 50;
|
|
19
|
+
export const CONTEXT_COMPACTION_DEFAULT_PRESERVE_RECENT = 2;
|
|
20
|
+
export const CONTEXT_COMPACTION_AUTO_QUERY = "auto-detected";
|
|
16
21
|
const CONTEXT_GREP_DELETE_DEFAULT_MAX_MATCHES = 50;
|
|
17
22
|
const CONTEXT_GREP_DELETE_MAX_REGEX_PATTERN_CHARS = 512;
|
|
18
23
|
const CONTEXT_GREP_DELETE_MAX_REGEX_SCAN_CHARS = 250_000;
|
|
19
24
|
const CONTEXT_MANIFEST_MAX_ENTRIES = 80;
|
|
20
25
|
const CONTEXT_MANIFEST_PREVIEW_CHARS = 240;
|
|
21
|
-
const CONTEXT_CRITICAL_OVERFLOW_RECENT_ENTRY_COUNT = 5;
|
|
22
26
|
const CONTEXT_READ_ENTRY_DEFAULT_MAX_CHARS = 4000;
|
|
23
27
|
const CONTEXT_READ_ENTRY_MAX_CHARS = 12_000;
|
|
24
28
|
const CONTEXT_SEARCH_DEFAULT_MAX_MATCHES = 20;
|
|
@@ -35,7 +39,9 @@ const ContextDeleteToolParameters = Type.Object({
|
|
|
35
39
|
minimum: 0,
|
|
36
40
|
description: "Required when kind is content_block; omit when kind is entry.",
|
|
37
41
|
})),
|
|
38
|
-
}, { additionalProperties: false }), {
|
|
42
|
+
}, { additionalProperties: false }), {
|
|
43
|
+
description: "ID-only deletion targets. Include only kind, entryId, and blockIndex when needed; do not include transcript text, block contents, summaries, or replacement content. Invalid targets are rejected by the tool with correction guidance.",
|
|
44
|
+
}),
|
|
39
45
|
}, { additionalProperties: false });
|
|
40
46
|
const ContextGrepDeleteToolParameters = Type.Object({
|
|
41
47
|
pattern: Type.String({ minLength: 1, description: "Literal text or regular expression to match in transcript text." }),
|
|
@@ -47,7 +53,7 @@ const ContextGrepDeleteToolParameters = Type.Object({
|
|
|
47
53
|
maxMatches: Type.Optional(Type.Integer({
|
|
48
54
|
minimum: 1,
|
|
49
55
|
maximum: 200,
|
|
50
|
-
description: "
|
|
56
|
+
description: "Per-call safety cap. If more not-yet-deleted candidate targets are found in this tool call, no deletions are applied. Defaults to 50. This is not a cumulative compaction cap; call the tool again for additional batches.",
|
|
51
57
|
})),
|
|
52
58
|
expectedMatchCount: Type.Optional(Type.Integer({
|
|
53
59
|
minimum: 0,
|
|
@@ -78,6 +84,7 @@ const ContextReadEntryToolParameters = Type.Object({
|
|
|
78
84
|
description: "Maximum characters to return. Defaults to 4000; keep reads small to avoid overflowing context.",
|
|
79
85
|
})),
|
|
80
86
|
}, { additionalProperties: false });
|
|
87
|
+
const ContextCompactionBudgetToolParameters = Type.Object({}, { additionalProperties: false });
|
|
81
88
|
const CONTEXT_DELETE_TOOL = {
|
|
82
89
|
name: CONTEXT_DELETE_TOOL_NAME,
|
|
83
90
|
description: "Record context compaction deletion targets directly against the transcript.",
|
|
@@ -98,16 +105,36 @@ const CONTEXT_READ_ENTRY_TOOL = {
|
|
|
98
105
|
description: "Read a small slice of one transcript entry or content block from the full transcript working copy.",
|
|
99
106
|
parameters: ContextReadEntryToolParameters,
|
|
100
107
|
};
|
|
108
|
+
const CONTEXT_COMPACTION_BUDGET_TOOL = {
|
|
109
|
+
name: CONTEXT_COMPACTION_BUDGET_TOOL_NAME,
|
|
110
|
+
description: "Report current context-window fullness and reduction progress for the selected deletion targets without mutating deletion state.",
|
|
111
|
+
parameters: ContextCompactionBudgetToolParameters,
|
|
112
|
+
};
|
|
101
113
|
const CONTEXT_COMPACTION_SYSTEM_PROMPT = `You are a context compaction assistant.
|
|
102
114
|
|
|
103
115
|
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
|
-
|
|
116
|
+
function contextCompactionFixedPrompt(parameters) {
|
|
117
|
+
const targetLabel = contextCompactionTargetLabel(parameters);
|
|
118
|
+
return `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.
|
|
119
|
+
|
|
120
|
+
Compaction records deletion targets, not replacement content.
|
|
121
|
+
For context_delete, use id-only targets: stable entryId values and optional blockIndex values only.
|
|
122
|
+
For context_grep_delete, use a concise literal or regex pattern to select matching entries or blocks; do not paste full transcript entries or content-block bodies.
|
|
123
|
+
Do not summarize, paraphrase, or generate replacement context; those are not accepted compaction outputs.
|
|
124
|
+
Do not mutate retained transcript objects or content.
|
|
125
|
+
Deletion tool calls are the compaction action.
|
|
105
126
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
127
|
+
Strategy:
|
|
128
|
+
- Start by calling context_compaction_budget to see how much of the context window is full and how much reduction is needed.
|
|
129
|
+
- Spend a few turns exploring with search/read tools to gain high confidence of candidate blocks to remove.
|
|
130
|
+
- Prefer high-confidence exploit actions after that: delete obvious low-value entries via context_grep_delete or context_delete.
|
|
131
|
+
- Use grep deletion for repeated low-value patterns.
|
|
132
|
+
- Use exact id deletion for inspected one-off stale entries.
|
|
133
|
+
- Check context_compaction_budget after deletion batches to track progress.
|
|
134
|
+
- Strict requirement: reduce current context by at least ${targetLabel} before finishing. This is a hard completion gate, not a loose goal.
|
|
135
|
+
- Do not send a final plain-text completion message until context_compaction_budget reports at least ${targetLabel} currentReductionPercent.
|
|
136
|
+
- If the strict ${targetLabel} reduction is not met yet, continue removing low-value message entries or content blocks with context_delete/context_grep_delete.
|
|
137
|
+
- Use the focus query to preserve relevant context: ${JSON.stringify(parameters.query)}.
|
|
111
138
|
|
|
112
139
|
What Gets Deleted:
|
|
113
140
|
- Redundant tool outputs: file reads already acted on, grep/search results already processed, passing test output no longer needed.
|
|
@@ -117,11 +144,14 @@ What Gets Deleted:
|
|
|
117
144
|
|
|
118
145
|
What Survives:
|
|
119
146
|
- Active file paths and line numbers: Any reference the agent might need to navigate.
|
|
120
|
-
- Current error messages: Unresolved
|
|
147
|
+
- Current error messages: Unresolved bugs and their exact text.
|
|
121
148
|
- Reasoning decisions: Why the agent chose approach A over B. An agent's chain of thought (why it chose this file, what pattern it noticed, what fix it decided on) carries more information-per-token than the raw grep output or file content that informed those decisions.
|
|
122
149
|
- Recent tool calls and their results: The last 3-5 operations.
|
|
123
150
|
- User instructions: The original task and any clarifications.
|
|
124
151
|
|
|
152
|
+
Conditionally Deleted:
|
|
153
|
+
- Old Reasoning decisions: If there is nothing else to remove and the target reduction is not met, you can remove any reasoning steps, EXCEPT thinking or redacted_thinking blocks in the latest assistant message.
|
|
154
|
+
|
|
125
155
|
<output_format>
|
|
126
156
|
Call the context_delete tool one or more times with deletion targets in this shape:
|
|
127
157
|
{ "deletions": [{ "kind": "entry", "entryId": "..." }] }
|
|
@@ -131,12 +161,13 @@ For content-block deletions, use:
|
|
|
131
161
|
|
|
132
162
|
The tool applies and validates deletion targets immediately. You can continue calling it for additional deletions if useful.
|
|
133
163
|
|
|
134
|
-
For guarded bulk deletion by text match, call context_grep_delete with a literal pattern or regex. It
|
|
164
|
+
For guarded bulk deletion by text match, call context_grep_delete with a literal pattern or regex. It removes valid matching context, silently ignores candidates that validation does not allow so they are not counted as removals, enforces a per-call maxMatches safety cap and optional expectedMatchCount, and validates through the same tool-call/tool-result safety rules. maxMatches only limits one tool call; there is no cumulative cap across corrected or repeated deletion calls.
|
|
135
165
|
|
|
136
166
|
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
167
|
|
|
138
|
-
When
|
|
168
|
+
When the strict ${targetLabel} reduction requirement is met, reply with a brief plain-text completion message. Do not include deletion target IDs outside tool calls.
|
|
139
169
|
</output_format>`;
|
|
170
|
+
}
|
|
140
171
|
function getMessageFromEntry(entry) {
|
|
141
172
|
if (entry.type === "message") {
|
|
142
173
|
return entry.message;
|
|
@@ -190,6 +221,11 @@ function textFromContentBlock(block) {
|
|
|
190
221
|
return "[image]";
|
|
191
222
|
return JSON.stringify(record);
|
|
192
223
|
}
|
|
224
|
+
function assistantEntryHasThinkingContentBlock(entry) {
|
|
225
|
+
return (entry.role === "assistant" &&
|
|
226
|
+
(entry.contentBlocks.some((block) => isAssistantThinkingBlockType(block.type)) ||
|
|
227
|
+
messageHasAssistantThinkingContentBlock(entry.message)));
|
|
228
|
+
}
|
|
193
229
|
const IMAGE_BLOCK_CHAR_ESTIMATE = 4800;
|
|
194
230
|
const IMAGE_BLOCK_TOKEN_ESTIMATE = Math.ceil(IMAGE_BLOCK_CHAR_ESTIMATE / 4);
|
|
195
231
|
function estimateTextTokens(text) {
|
|
@@ -221,15 +257,17 @@ function contentBlocksForEntry(entryId, message, protectedEntry, existingDeleted
|
|
|
221
257
|
return [];
|
|
222
258
|
return content
|
|
223
259
|
.map((block, blockIndex) => {
|
|
224
|
-
if (existingDeletedBlocks?.has(blockIndex))
|
|
260
|
+
if (existingDeletedBlocks?.has(blockIndex)) {
|
|
225
261
|
return undefined;
|
|
262
|
+
}
|
|
263
|
+
const type = block && typeof block === "object" && typeof block.type === "string"
|
|
264
|
+
? (block.type)
|
|
265
|
+
: "unknown";
|
|
226
266
|
const text = textFromContentBlock(block);
|
|
227
267
|
return {
|
|
228
268
|
entryId,
|
|
229
269
|
blockIndex,
|
|
230
|
-
type
|
|
231
|
-
? (block.type)
|
|
232
|
-
: "unknown",
|
|
270
|
+
type,
|
|
233
271
|
text,
|
|
234
272
|
tokenEstimate: estimateContentBlockTokens(block, text),
|
|
235
273
|
protected: protectedEntry,
|
|
@@ -272,6 +310,51 @@ function hasToolResultError(message) {
|
|
|
272
310
|
function hasFailedBashExecution(message) {
|
|
273
311
|
return message.role === "bashExecution" && typeof message.exitCode === "number" && message.exitCode !== 0;
|
|
274
312
|
}
|
|
313
|
+
const CONTEXT_COMPACTION_QUERY_MAX_CHARS = 1000;
|
|
314
|
+
function normalizeCompressionRatio(value) {
|
|
315
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 && value < 1
|
|
316
|
+
? value
|
|
317
|
+
: CONTEXT_COMPACTION_DEFAULT_COMPRESSION_RATIO;
|
|
318
|
+
}
|
|
319
|
+
function normalizePreserveRecent(value) {
|
|
320
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : CONTEXT_COMPACTION_DEFAULT_PRESERVE_RECENT;
|
|
321
|
+
}
|
|
322
|
+
function normalizeQuery(value, fallbackQuery) {
|
|
323
|
+
const query = value?.trim() || fallbackQuery.trim() || CONTEXT_COMPACTION_AUTO_QUERY;
|
|
324
|
+
return query.length > CONTEXT_COMPACTION_QUERY_MAX_CHARS
|
|
325
|
+
? `${query.slice(0, CONTEXT_COMPACTION_QUERY_MAX_CHARS)}\n[... ${query.length - CONTEXT_COMPACTION_QUERY_MAX_CHARS} more characters omitted from compaction query]`
|
|
326
|
+
: query;
|
|
327
|
+
}
|
|
328
|
+
export function autoDetectContextCompactionQuery(pathEntries) {
|
|
329
|
+
for (let index = pathEntries.length - 1; index >= 0; index--) {
|
|
330
|
+
const entry = pathEntries[index];
|
|
331
|
+
if (entry.type === "context_compaction")
|
|
332
|
+
continue;
|
|
333
|
+
const message = getContextEligibleMessageFromEntry(entry);
|
|
334
|
+
if (!message || message.role !== "user")
|
|
335
|
+
continue;
|
|
336
|
+
const text = messageText(message).trim();
|
|
337
|
+
if (text.length > 0)
|
|
338
|
+
return normalizeQuery(text, CONTEXT_COMPACTION_AUTO_QUERY);
|
|
339
|
+
}
|
|
340
|
+
return CONTEXT_COMPACTION_AUTO_QUERY;
|
|
341
|
+
}
|
|
342
|
+
export function normalizeContextCompactionParameters(input = {}, fallbackQuery = CONTEXT_COMPACTION_AUTO_QUERY) {
|
|
343
|
+
return {
|
|
344
|
+
compression_ratio: normalizeCompressionRatio(input.compression_ratio),
|
|
345
|
+
preserve_recent: normalizePreserveRecent(input.preserve_recent),
|
|
346
|
+
query: normalizeQuery(input.query, fallbackQuery),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
function getTranscriptCompactionParameters(transcript) {
|
|
350
|
+
return normalizeContextCompactionParameters(transcript.parameters ?? transcript.settings, transcript.parameters?.query ?? transcript.settings.query ?? CONTEXT_COMPACTION_AUTO_QUERY);
|
|
351
|
+
}
|
|
352
|
+
function contextCompactionTargetReductionPercent(parameters) {
|
|
353
|
+
return roundPercent((1 - parameters.compression_ratio) * 100);
|
|
354
|
+
}
|
|
355
|
+
function contextCompactionTargetLabel(parameters) {
|
|
356
|
+
return `${contextCompactionTargetReductionPercent(parameters)}%`;
|
|
357
|
+
}
|
|
275
358
|
function isProtectedEntry(entry, message, recentEntryIds) {
|
|
276
359
|
if (recentEntryIds.has(entry.id))
|
|
277
360
|
return true;
|
|
@@ -292,13 +375,14 @@ function isProtectedEntry(entry, message, recentEntryIds) {
|
|
|
292
375
|
export function prepareContextCompaction(pathEntries, settings, options = {}) {
|
|
293
376
|
if (pathEntries.length === 0)
|
|
294
377
|
return undefined;
|
|
295
|
-
const
|
|
296
|
-
const filteredPathEntries = buildContextDeletionFilteredPath(pathEntries,
|
|
378
|
+
const effectiveDeletionFilters = buildEffectiveContextDeletionFilters(pathEntries);
|
|
379
|
+
const filteredPathEntries = buildContextDeletionFilteredPath(pathEntries, effectiveDeletionFilters);
|
|
380
|
+
const parameters = normalizeContextCompactionParameters({ ...settings, ...options }, autoDetectContextCompactionQuery(filteredPathEntries));
|
|
297
381
|
const rawEntryById = new Map(pathEntries.map((entry) => [entry.id, entry]));
|
|
298
382
|
const messageEntryIds = filteredPathEntries
|
|
299
383
|
.filter((entry) => entry.type !== "context_compaction" && getContextEligibleMessageFromEntry(entry) !== undefined)
|
|
300
384
|
.map((entry) => entry.id);
|
|
301
|
-
const recentEntryIds = new Set(messageEntryIds.slice(-
|
|
385
|
+
const recentEntryIds = new Set(parameters.preserve_recent > 0 ? messageEntryIds.slice(-parameters.preserve_recent) : []);
|
|
302
386
|
const protectedEntryIds = new Set();
|
|
303
387
|
const entries = [];
|
|
304
388
|
for (const entry of filteredPathEntries) {
|
|
@@ -312,7 +396,7 @@ export function prepareContextCompaction(pathEntries, settings, options = {}) {
|
|
|
312
396
|
if (protectedEntry)
|
|
313
397
|
protectedEntryIds.add(entry.id);
|
|
314
398
|
const rawMessage = getContextEligibleMessageFromEntry(rawEntry) ?? message;
|
|
315
|
-
const contentBlocks = contentBlocksForEntry(entry.id, rawMessage, protectedEntry,
|
|
399
|
+
const contentBlocks = contentBlocksForEntry(entry.id, rawMessage, protectedEntry, effectiveDeletionFilters.deletedContentBlocks.get(entry.id));
|
|
316
400
|
const toolCallIds = contentBlocks.map((block) => block.toolCallId).filter((id) => id !== undefined);
|
|
317
401
|
const text = contentBlocks.length > 0 ? contentBlocks.map((block) => block.text).join("\n") : messageText(message);
|
|
318
402
|
entries.push({
|
|
@@ -332,12 +416,13 @@ export function prepareContextCompaction(pathEntries, settings, options = {}) {
|
|
|
332
416
|
return undefined;
|
|
333
417
|
return {
|
|
334
418
|
branchEntries: pathEntries,
|
|
335
|
-
|
|
419
|
+
parameters,
|
|
336
420
|
transcript: {
|
|
337
421
|
entries,
|
|
338
422
|
protectedEntryIds: [...protectedEntryIds],
|
|
339
423
|
tokensBefore: entries.reduce((total, entry) => total + entry.tokenEstimate, 0),
|
|
340
424
|
settings,
|
|
425
|
+
parameters,
|
|
341
426
|
},
|
|
342
427
|
};
|
|
343
428
|
}
|
|
@@ -352,6 +437,14 @@ function normalizeRawTarget(target) {
|
|
|
352
437
|
return { kind: "entry", entryId: target.entryId };
|
|
353
438
|
return { kind: "content_block", entryId: target.entryId, blockIndex: target.blockIndex };
|
|
354
439
|
}
|
|
440
|
+
function assertIdOnlyDeletionTarget(target) {
|
|
441
|
+
const allowedKeys = target.kind === "content_block" ? new Set(["kind", "entryId", "blockIndex"]) : new Set(["kind", "entryId"]);
|
|
442
|
+
for (const key of Object.keys(target)) {
|
|
443
|
+
if (!allowedKeys.has(key)) {
|
|
444
|
+
throw new Error(`Deletion target includes unsupported property ${JSON.stringify(key)}; context deletion targets are id-only and must contain only kind, entryId${target.kind === "content_block" ? ", and blockIndex" : ""}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
355
448
|
function rawDeletionFromTarget(target) {
|
|
356
449
|
if (target.kind === "entry")
|
|
357
450
|
return { kind: "entry", entryId: target.entryId };
|
|
@@ -374,6 +467,98 @@ function getDeletedContentBlocks(targets) {
|
|
|
374
467
|
}
|
|
375
468
|
return blocksByEntry;
|
|
376
469
|
}
|
|
470
|
+
function recentContextEntryBoundary(transcript) {
|
|
471
|
+
const { preserve_recent } = getTranscriptCompactionParameters(transcript);
|
|
472
|
+
return preserve_recent > 0 ? Math.max(0, transcript.entries.length - preserve_recent) : transcript.entries.length;
|
|
473
|
+
}
|
|
474
|
+
function getRecentContextEntryIds(transcript) {
|
|
475
|
+
const { preserve_recent } = getTranscriptCompactionParameters(transcript);
|
|
476
|
+
if (preserve_recent <= 0)
|
|
477
|
+
return new Set();
|
|
478
|
+
return new Set(transcript.entries.slice(recentContextEntryBoundary(transcript)).map((entry) => entry.entryId));
|
|
479
|
+
}
|
|
480
|
+
function isRecentContextEntry(entry, transcript) {
|
|
481
|
+
const { preserve_recent } = getTranscriptCompactionParameters(transcript);
|
|
482
|
+
if (preserve_recent <= 0)
|
|
483
|
+
return false;
|
|
484
|
+
const entryIndex = transcript.entries.findIndex((candidate) => candidate.entryId === entry.entryId);
|
|
485
|
+
return entryIndex >= 0 && entryIndex >= recentContextEntryBoundary(transcript);
|
|
486
|
+
}
|
|
487
|
+
function formatRecentContextDeletionError(transcript, target) {
|
|
488
|
+
const { preserve_recent } = getTranscriptCompactionParameters(transcript);
|
|
489
|
+
const recentWindow = `last ${preserve_recent} context ${preserve_recent === 1 ? "entry" : "entries"}`;
|
|
490
|
+
if (target.kind === "entry") {
|
|
491
|
+
return `Cannot delete recent context entry ${target.entryId} because the ${recentWindow} must remain available for active continuity. Choose an older entry.`;
|
|
492
|
+
}
|
|
493
|
+
return `Cannot delete content block ${target.entryId}:${target.blockIndex} because entry ${target.entryId} is one of the ${recentWindow} that must remain available for active continuity. Choose an older entry or content block.`;
|
|
494
|
+
}
|
|
495
|
+
function deletionGuidance() {
|
|
496
|
+
return "Choose another deletion candidate.";
|
|
497
|
+
}
|
|
498
|
+
function findTranscriptEntry(transcript, entryId) {
|
|
499
|
+
return transcript.entries.find((entry) => entry.entryId === entryId);
|
|
500
|
+
}
|
|
501
|
+
function findTranscriptContentBlock(transcript, target) {
|
|
502
|
+
if (target.kind !== "content_block")
|
|
503
|
+
return undefined;
|
|
504
|
+
return findTranscriptEntry(transcript, target.entryId)?.contentBlocks.find((block) => block.blockIndex === target.blockIndex);
|
|
505
|
+
}
|
|
506
|
+
function firstToolCallBlockTarget(entry, callId) {
|
|
507
|
+
const blockIndex = toolCallBlockIndexes(entry, callId)[0];
|
|
508
|
+
return blockIndex === undefined ? undefined : { kind: "content_block", entryId: entry.entryId, blockIndex };
|
|
509
|
+
}
|
|
510
|
+
function formatProtectedDeletionError(transcript, target) {
|
|
511
|
+
const entry = findTranscriptEntry(transcript, target.entryId);
|
|
512
|
+
if (target.kind === "entry") {
|
|
513
|
+
const toolResultSuffix = entry?.toolResultFor ? ` for tool call ${entry.toolResultFor}` : "";
|
|
514
|
+
const toolCallSuffix = entry && entry.toolCallIds.length > 0 ? ` containing tool call ${entry.toolCallIds.join(", ")}` : "";
|
|
515
|
+
return `Deletion target ${target.entryId}${toolResultSuffix}${toolCallSuffix} is protected. ${deletionGuidance()}`;
|
|
516
|
+
}
|
|
517
|
+
const block = findTranscriptContentBlock(transcript, target);
|
|
518
|
+
const toolBlockSuffix = block?.toolCallId ? ` It is a protected tool block for tool call ${block.toolCallId}.` : "";
|
|
519
|
+
return `Content block ${target.entryId}:${target.blockIndex} is protected.${toolBlockSuffix} ${deletionGuidance()}`;
|
|
520
|
+
}
|
|
521
|
+
function formatProtectedToolDependencyError(transcript, blockedTarget, context) {
|
|
522
|
+
const protectedMessage = formatProtectedDeletionError(transcript, blockedTarget);
|
|
523
|
+
return `${context} ${protectedMessage}`;
|
|
524
|
+
}
|
|
525
|
+
function isProtectedContextDeletionErrorMessage(message) {
|
|
526
|
+
return (/\bprotected\b/i.test(message) ||
|
|
527
|
+
/Cannot delete (?:recent context entry|content block .* because entry .* is one of the last)/u.test(message) ||
|
|
528
|
+
/latest assistant message|thinking\/redacted_thinking block in the latest assistant message/u.test(message));
|
|
529
|
+
}
|
|
530
|
+
function assertNoRecentContextDeletionTargets(transcript, targets) {
|
|
531
|
+
const recentEntryIds = getRecentContextEntryIds(transcript);
|
|
532
|
+
for (const target of targets) {
|
|
533
|
+
if (recentEntryIds.has(target.entryId)) {
|
|
534
|
+
throw new Error(formatRecentContextDeletionError(transcript, target));
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
function latestAssistantEntry(transcript) {
|
|
539
|
+
for (let index = transcript.entries.length - 1; index >= 0; index--) {
|
|
540
|
+
const entry = transcript.entries[index];
|
|
541
|
+
if (entry.role === "assistant")
|
|
542
|
+
return entry;
|
|
543
|
+
}
|
|
544
|
+
return undefined;
|
|
545
|
+
}
|
|
546
|
+
function assertNoLatestAssistantThinkingDeletionTargets(transcript, targets) {
|
|
547
|
+
const latestAssistant = latestAssistantEntry(transcript);
|
|
548
|
+
if (!latestAssistant || !assistantEntryHasThinkingContentBlock(latestAssistant))
|
|
549
|
+
return;
|
|
550
|
+
for (const target of targets) {
|
|
551
|
+
if (target.entryId !== latestAssistant.entryId)
|
|
552
|
+
continue;
|
|
553
|
+
if (target.kind === "entry") {
|
|
554
|
+
throw new Error(`Cannot delete assistant entry ${target.entryId} because it is the latest assistant message and contains thinking/redacted_thinking content blocks`);
|
|
555
|
+
}
|
|
556
|
+
const block = latestAssistant.contentBlocks.find((candidate) => candidate.blockIndex === target.blockIndex);
|
|
557
|
+
if (block && isAssistantThinkingBlockType(block.type)) {
|
|
558
|
+
throw new Error(`Cannot delete content block ${target.entryId}:${target.blockIndex} because it is a thinking/redacted_thinking block in the latest assistant message`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
377
562
|
function isToolCallBlockDeleted(entry, callId, deletedEntryIds, deletedContentBlocks) {
|
|
378
563
|
if (deletedEntryIds.has(entry.entryId))
|
|
379
564
|
return true;
|
|
@@ -404,15 +589,6 @@ function deleteEntryTarget(targets, entryId) {
|
|
|
404
589
|
}
|
|
405
590
|
return addTarget(targets, { kind: "entry", entryId }) || changed;
|
|
406
591
|
}
|
|
407
|
-
function removeEntryDeletion(targets, entryId) {
|
|
408
|
-
const originalLength = targets.length;
|
|
409
|
-
for (let index = targets.length - 1; index >= 0; index--) {
|
|
410
|
-
const target = targets[index];
|
|
411
|
-
if (target.kind === "entry" && target.entryId === entryId)
|
|
412
|
-
targets.splice(index, 1);
|
|
413
|
-
}
|
|
414
|
-
return targets.length !== originalLength;
|
|
415
|
-
}
|
|
416
592
|
function mergeContextDeletionTargets(baseTargets, additionalTargets) {
|
|
417
593
|
const targets = [...baseTargets];
|
|
418
594
|
for (const target of additionalTargets) {
|
|
@@ -426,8 +602,10 @@ function mergeContextDeletionTargets(baseTargets, additionalTargets) {
|
|
|
426
602
|
}
|
|
427
603
|
return targets;
|
|
428
604
|
}
|
|
429
|
-
function canonicalizeEntryTargets(targets, entry) {
|
|
430
|
-
if (entry
|
|
605
|
+
function canonicalizeEntryTargets(transcript, targets, entry) {
|
|
606
|
+
if (!canDeleteTarget(transcript, { kind: "entry", entryId: entry.entryId }))
|
|
607
|
+
return false;
|
|
608
|
+
if (getDeletedEntryIds(targets).has(entry.entryId))
|
|
431
609
|
return false;
|
|
432
610
|
const deletedBlocks = getDeletedContentBlocks(targets).get(entry.entryId);
|
|
433
611
|
if (!deletedBlocks || !entry.contentBlocks.every((block) => deletedBlocks.has(block.blockIndex)))
|
|
@@ -436,28 +614,17 @@ function canonicalizeEntryTargets(targets, entry) {
|
|
|
436
614
|
// request every block individually stay invalid so the assistant must choose explicit entry deletion.
|
|
437
615
|
return deleteEntryTarget(targets, entry.entryId);
|
|
438
616
|
}
|
|
439
|
-
function
|
|
440
|
-
let changed = removeEntryDeletion(targets, entry.entryId);
|
|
441
|
-
const blockIndexes = new Set(toolCallBlockIndexes(entry, callId));
|
|
442
|
-
for (let index = targets.length - 1; index >= 0; index--) {
|
|
443
|
-
const target = targets[index];
|
|
444
|
-
if (target.kind === "content_block" && target.entryId === entry.entryId && blockIndexes.has(target.blockIndex)) {
|
|
445
|
-
targets.splice(index, 1);
|
|
446
|
-
changed = true;
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
return changed;
|
|
450
|
-
}
|
|
451
|
-
function addToolCallDeletion(targets, entry, callId) {
|
|
452
|
-
if (entry.protected)
|
|
453
|
-
return false;
|
|
617
|
+
function addToolCallDeletion(transcript, targets, entry, callId) {
|
|
454
618
|
let changed = false;
|
|
455
619
|
for (const blockIndex of toolCallBlockIndexes(entry, callId)) {
|
|
620
|
+
const target = { kind: "content_block", entryId: entry.entryId, blockIndex };
|
|
621
|
+
if (!canDeleteTarget(transcript, target))
|
|
622
|
+
continue;
|
|
456
623
|
if (!getDeletedEntryIds(targets).has(entry.entryId)) {
|
|
457
|
-
changed = addTarget(targets,
|
|
624
|
+
changed = addTarget(targets, target) || changed;
|
|
458
625
|
}
|
|
459
626
|
}
|
|
460
|
-
return canonicalizeEntryTargets(targets, entry) || changed;
|
|
627
|
+
return canonicalizeEntryTargets(transcript, targets, entry) || changed;
|
|
461
628
|
}
|
|
462
629
|
let warnedReconciliationNonConvergence = false;
|
|
463
630
|
function reconcileToolDependencies(transcript, initialTargets) {
|
|
@@ -496,9 +663,14 @@ function reconcileToolDependencies(transcript, initialTargets) {
|
|
|
496
663
|
const callDeleted = isToolCallBlockDeleted(callEntry, callId, deletedEntryIds, deletedContentBlocks);
|
|
497
664
|
const results = resultEntries.get(callId) ?? [];
|
|
498
665
|
if (callDeleted) {
|
|
499
|
-
const retainedProtectedResult = results.find((entry) =>
|
|
666
|
+
const retainedProtectedResult = results.find((entry) => !deletedEntryIds.has(entry.entryId) &&
|
|
667
|
+
!canDeleteTarget(transcript, { kind: "entry", entryId: entry.entryId }));
|
|
500
668
|
if (retainedProtectedResult) {
|
|
501
|
-
|
|
669
|
+
const retainedResultTarget = { kind: "entry", entryId: retainedProtectedResult.entryId };
|
|
670
|
+
if (isRecentTarget(transcript, retainedResultTarget)) {
|
|
671
|
+
throw new Error(formatRecentContextDeletionError(transcript, retainedResultTarget));
|
|
672
|
+
}
|
|
673
|
+
throw new Error(formatProtectedToolDependencyError(transcript, retainedResultTarget, `Cannot delete tool call ${callId} because its paired tool result entry ${retainedProtectedResult.entryId} is protected.`));
|
|
502
674
|
}
|
|
503
675
|
else {
|
|
504
676
|
for (const result of results) {
|
|
@@ -512,15 +684,19 @@ function reconcileToolDependencies(transcript, initialTargets) {
|
|
|
512
684
|
if (!deletedEntryIds.has(result.entryId))
|
|
513
685
|
continue;
|
|
514
686
|
recordChange(deleteEntryTarget(targets, result.entryId));
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
687
|
+
const callEntryTarget = { kind: "entry", entryId: callEntry.entryId };
|
|
688
|
+
const callBlockTarget = firstToolCallBlockTarget(callEntry, callId) ?? callEntryTarget;
|
|
689
|
+
if (!canDeleteTarget(transcript, callBlockTarget)) {
|
|
690
|
+
if (isRecentTarget(transcript, callBlockTarget)) {
|
|
691
|
+
throw new Error(formatRecentContextDeletionError(transcript, callBlockTarget));
|
|
692
|
+
}
|
|
693
|
+
throw new Error(formatProtectedToolDependencyError(transcript, callBlockTarget, `Cannot delete tool result entry ${result.entryId} because that would require deleting protected tool block for tool call ${callId}.`));
|
|
518
694
|
}
|
|
519
|
-
recordChange(addToolCallDeletion(targets, callEntry, callId));
|
|
695
|
+
recordChange(addToolCallDeletion(transcript, targets, callEntry, callId));
|
|
520
696
|
}
|
|
521
697
|
}
|
|
522
698
|
for (const entry of entriesWithToolCalls) {
|
|
523
|
-
recordChange(canonicalizeEntryTargets(targets, entry));
|
|
699
|
+
recordChange(canonicalizeEntryTargets(transcript, targets, entry));
|
|
524
700
|
}
|
|
525
701
|
}
|
|
526
702
|
if (changed && !warnedReconciliationNonConvergence) {
|
|
@@ -602,11 +778,7 @@ function computeContextCompactionStats(transcript, targets) {
|
|
|
602
778
|
* message, an extension-injected `custom` message, or a branch summary (`branchSummary` role /
|
|
603
779
|
* `branch_summary` entry type) that recaps an earlier branch's task.
|
|
604
780
|
*
|
|
605
|
-
* Verbatim compaction must always leave at least one task-bearing entry in context.
|
|
606
|
-
* also defines which protected entries `critical_overflow` may delete, because the intent each one
|
|
607
|
-
* carries is recoverable from any other surviving task-bearing entry. As a deliberate consequence,
|
|
608
|
-
* `critical_overflow` MAY delete every literal `user` message as long as a branch summary or custom
|
|
609
|
-
* entry survives — branch summaries intentionally carry the task forward.
|
|
781
|
+
* Verbatim compaction must always leave at least one task-bearing entry in context.
|
|
610
782
|
*/
|
|
611
783
|
function isTaskBearingEntry(entry) {
|
|
612
784
|
return (entry.role === "user" ||
|
|
@@ -614,37 +786,31 @@ function isTaskBearingEntry(entry) {
|
|
|
614
786
|
entry.role === "branchSummary" ||
|
|
615
787
|
entry.entryType === "branch_summary");
|
|
616
788
|
}
|
|
617
|
-
function
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
const entryIndex = transcript.entries.findIndex((candidate) => candidate.entryId === entry.entryId);
|
|
621
|
-
if (entryIndex < 0)
|
|
622
|
-
return false;
|
|
623
|
-
const recentBoundary = Math.max(0, transcript.entries.length - CONTEXT_CRITICAL_OVERFLOW_RECENT_ENTRY_COUNT);
|
|
624
|
-
if (entryIndex >= recentBoundary)
|
|
625
|
-
return false;
|
|
626
|
-
if (hasAssistantError(entry.message) || hasToolResultError(entry.message) || hasFailedBashExecution(entry.message)) {
|
|
627
|
-
return false;
|
|
628
|
-
}
|
|
629
|
-
return isTaskBearingEntry(entry);
|
|
789
|
+
function isRecentTarget(transcript, target) {
|
|
790
|
+
const entry = transcript.entries.find((candidate) => candidate.entryId === target.entryId);
|
|
791
|
+
return entry !== undefined && isRecentContextEntry(entry, transcript);
|
|
630
792
|
}
|
|
631
|
-
function
|
|
632
|
-
if (mode !== "critical_overflow")
|
|
633
|
-
return false;
|
|
793
|
+
function canDeleteTarget(transcript, target) {
|
|
634
794
|
const entry = transcript.entries.find((candidate) => candidate.entryId === target.entryId);
|
|
635
|
-
if (!entry
|
|
795
|
+
if (!entry)
|
|
796
|
+
return false;
|
|
797
|
+
if (isRecentTarget(transcript, target))
|
|
798
|
+
return false;
|
|
799
|
+
if (entry.protected)
|
|
636
800
|
return false;
|
|
637
801
|
if (target.kind === "entry")
|
|
638
802
|
return true;
|
|
639
803
|
const block = entry.contentBlocks.find((candidate) => candidate.blockIndex === target.blockIndex);
|
|
640
|
-
|
|
804
|
+
if (!block)
|
|
805
|
+
return false;
|
|
806
|
+
return !block.protected;
|
|
641
807
|
}
|
|
642
|
-
export function validateContextDeletionRequest(request, transcript
|
|
643
|
-
const mode = options.mode ?? "standard";
|
|
808
|
+
export function validateContextDeletionRequest(request, transcript) {
|
|
644
809
|
if (!request || typeof request !== "object" || !Array.isArray(request.deletions)) {
|
|
645
810
|
throw new Error("Context deletion request must be an object with a deletions array");
|
|
646
811
|
}
|
|
647
812
|
const entryById = new Map(transcript.entries.map((entry) => [entry.entryId, entry]));
|
|
813
|
+
const recentEntryIds = getRecentContextEntryIds(transcript);
|
|
648
814
|
const seen = new Set();
|
|
649
815
|
const deletedTargets = [];
|
|
650
816
|
for (const deletion of request.deletions) {
|
|
@@ -654,6 +820,7 @@ export function validateContextDeletionRequest(request, transcript, options = {}
|
|
|
654
820
|
if (deletion.kind !== "entry" && deletion.kind !== "content_block") {
|
|
655
821
|
throw new Error(`Unsupported deletion target kind: ${String(deletion.kind)}`);
|
|
656
822
|
}
|
|
823
|
+
assertIdOnlyDeletionTarget(deletion);
|
|
657
824
|
if (typeof deletion.entryId !== "string" || deletion.entryId.length === 0) {
|
|
658
825
|
throw new Error("Deletion target entryId must be a non-empty string");
|
|
659
826
|
}
|
|
@@ -661,19 +828,31 @@ export function validateContextDeletionRequest(request, transcript, options = {}
|
|
|
661
828
|
if (!entry) {
|
|
662
829
|
throw new Error(`Unknown deletion target entryId: ${deletion.entryId}`);
|
|
663
830
|
}
|
|
664
|
-
|
|
665
|
-
|
|
831
|
+
const normalized = normalizeRawTarget(deletion);
|
|
832
|
+
if (deletion.kind === "entry") {
|
|
833
|
+
if (recentEntryIds.has(deletion.entryId)) {
|
|
834
|
+
throw new Error(formatRecentContextDeletionError(transcript, normalized));
|
|
835
|
+
}
|
|
836
|
+
if (entry.protected) {
|
|
837
|
+
throw new Error(formatProtectedDeletionError(transcript, normalized));
|
|
838
|
+
}
|
|
666
839
|
}
|
|
667
840
|
if (deletion.kind === "content_block") {
|
|
668
|
-
if (!Number.isInteger(deletion.blockIndex) || deletion.blockIndex
|
|
841
|
+
if (typeof deletion.blockIndex !== "number" || !Number.isInteger(deletion.blockIndex) || deletion.blockIndex < 0) {
|
|
669
842
|
throw new Error(`Invalid content block index for entry ${deletion.entryId}`);
|
|
670
843
|
}
|
|
844
|
+
if (recentEntryIds.has(deletion.entryId)) {
|
|
845
|
+
throw new Error(formatRecentContextDeletionError(transcript, normalized));
|
|
846
|
+
}
|
|
847
|
+
if (entry.protected) {
|
|
848
|
+
throw new Error(formatProtectedDeletionError(transcript, normalized));
|
|
849
|
+
}
|
|
671
850
|
const block = entry.contentBlocks.find((item) => item.blockIndex === deletion.blockIndex);
|
|
672
851
|
if (!block) {
|
|
673
852
|
throw new Error(`Unknown content block ${deletion.blockIndex} for entry ${deletion.entryId}`);
|
|
674
853
|
}
|
|
675
|
-
if (block.protected
|
|
676
|
-
throw new Error(
|
|
854
|
+
if (block.protected) {
|
|
855
|
+
throw new Error(formatProtectedDeletionError(transcript, normalized));
|
|
677
856
|
}
|
|
678
857
|
if (entry.contentBlocks.length <= 1) {
|
|
679
858
|
throw new Error(`Deleting the only content block of ${deletion.entryId} must be an entry deletion`);
|
|
@@ -684,10 +863,13 @@ export function validateContextDeletionRequest(request, transcript, options = {}
|
|
|
684
863
|
throw new Error(`Duplicate deletion target: ${key}`);
|
|
685
864
|
}
|
|
686
865
|
seen.add(key);
|
|
687
|
-
const normalized = normalizeRawTarget(deletion);
|
|
688
866
|
deletedTargets.push(normalized);
|
|
689
867
|
}
|
|
690
868
|
const reconciledTargets = reconcileToolDependencies(transcript, deletedTargets);
|
|
869
|
+
// Tool reconciliation can add targets after the per-request checks above, so
|
|
870
|
+
// these post-reconcile assertions remain authoritative.
|
|
871
|
+
assertNoRecentContextDeletionTargets(transcript, reconciledTargets);
|
|
872
|
+
assertNoLatestAssistantThinkingDeletionTargets(transcript, reconciledTargets);
|
|
691
873
|
const reconciledDeletedEntryIds = getDeletedEntryIds(reconciledTargets);
|
|
692
874
|
for (const target of reconciledTargets) {
|
|
693
875
|
if (target.kind === "content_block" && reconciledDeletedEntryIds.has(target.entryId)) {
|
|
@@ -716,18 +898,6 @@ export function validateContextDeletionRequest(request, transcript, options = {}
|
|
|
716
898
|
stats: computeContextCompactionStats(transcript, reconciledTargets),
|
|
717
899
|
};
|
|
718
900
|
}
|
|
719
|
-
function stripJsonFence(text) {
|
|
720
|
-
const trimmed = text.trim();
|
|
721
|
-
if (!trimmed.startsWith("```") || !trimmed.endsWith("```"))
|
|
722
|
-
return trimmed;
|
|
723
|
-
const firstLineEnd = trimmed.indexOf("\n");
|
|
724
|
-
if (firstLineEnd < 0)
|
|
725
|
-
return trimmed;
|
|
726
|
-
const fenceInfo = trimmed.slice(3, firstLineEnd).trim().toLowerCase();
|
|
727
|
-
if (fenceInfo !== "" && fenceInfo !== "json")
|
|
728
|
-
return trimmed;
|
|
729
|
-
return trimmed.slice(firstLineEnd + 1, -3).trim();
|
|
730
|
-
}
|
|
731
901
|
function contextDeletionRequestFromObject(value, source) {
|
|
732
902
|
if (!value || typeof value !== "object" || !Array.isArray(value.deletions)) {
|
|
733
903
|
throw new Error(`${source} must contain a deletions array`);
|
|
@@ -743,6 +913,69 @@ function formatErrorMessage(error) {
|
|
|
743
913
|
function createContextDeletionToolResult(text, details) {
|
|
744
914
|
return { content: [{ type: "text", text }], details, terminate: false };
|
|
745
915
|
}
|
|
916
|
+
function roundPercent(value) {
|
|
917
|
+
return Math.round(value * 10) / 10;
|
|
918
|
+
}
|
|
919
|
+
function percentOf(part, total) {
|
|
920
|
+
return total > 0 ? roundPercent((part / total) * 100) : 0;
|
|
921
|
+
}
|
|
922
|
+
function finitePositiveNumber(value) {
|
|
923
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
924
|
+
}
|
|
925
|
+
function createContextCompactionBudgetDetails(stats, callCount, contextWindow, parameters) {
|
|
926
|
+
const targetTokensAfter = Math.max(0, Math.floor(stats.tokensBefore * parameters.compression_ratio));
|
|
927
|
+
const targetReductionPercent = contextCompactionTargetReductionPercent(parameters);
|
|
928
|
+
const details = {
|
|
929
|
+
...(contextWindow !== undefined ? { contextWindow } : {}),
|
|
930
|
+
compression_ratio: parameters.compression_ratio,
|
|
931
|
+
tokensBefore: stats.tokensBefore,
|
|
932
|
+
currentTokensAfter: stats.tokensAfter,
|
|
933
|
+
deletedTokens: Math.max(0, stats.tokensBefore - stats.tokensAfter),
|
|
934
|
+
currentReductionPercent: stats.percentReduction,
|
|
935
|
+
targetReductionPercent,
|
|
936
|
+
targetTokensAfter,
|
|
937
|
+
tokensToDeleteForTarget: Math.max(0, stats.tokensAfter - targetTokensAfter),
|
|
938
|
+
...(contextWindow !== undefined
|
|
939
|
+
? {
|
|
940
|
+
contextWindowBeforePercent: percentOf(stats.tokensBefore, contextWindow),
|
|
941
|
+
contextWindowAfterPercent: percentOf(stats.tokensAfter, contextWindow),
|
|
942
|
+
}
|
|
943
|
+
: {}),
|
|
944
|
+
callCount,
|
|
945
|
+
};
|
|
946
|
+
return details;
|
|
947
|
+
}
|
|
948
|
+
function contextCompactionTargetMet(result, parameters) {
|
|
949
|
+
return (result !== undefined &&
|
|
950
|
+
result.deletedTargets.length > 0 &&
|
|
951
|
+
result.stats.percentReduction >= contextCompactionTargetReductionPercent(parameters));
|
|
952
|
+
}
|
|
953
|
+
function contextCompactionProgressKey(result) {
|
|
954
|
+
if (!result)
|
|
955
|
+
return "none:0";
|
|
956
|
+
return `${result.deletedTargets.length}:${result.stats.percentReduction}:${result.stats.tokensAfter}`;
|
|
957
|
+
}
|
|
958
|
+
function contextCompactionProgressPercent(result) {
|
|
959
|
+
return result?.stats.percentReduction ?? 0;
|
|
960
|
+
}
|
|
961
|
+
function createContextCompactionTargetNudgeMessage(result, parameters) {
|
|
962
|
+
const currentReductionPercent = contextCompactionProgressPercent(result);
|
|
963
|
+
const targetLabel = contextCompactionTargetLabel(parameters);
|
|
964
|
+
const tokensToDelete = result
|
|
965
|
+
? createContextCompactionBudgetDetails(result.stats, 0, undefined, parameters).tokensToDeleteForTarget
|
|
966
|
+
: undefined;
|
|
967
|
+
const remainingText = tokensToDelete !== undefined ? ` Delete about ${tokensToDelete} more token(s) if safe candidates exist.` : "";
|
|
968
|
+
return {
|
|
969
|
+
role: "user",
|
|
970
|
+
content: [
|
|
971
|
+
{
|
|
972
|
+
type: "text",
|
|
973
|
+
text: `The strict ${targetLabel} context-reduction requirement is not met yet; current validated reduction is ${currentReductionPercent}%.${remainingText} Continue removing low-value message entries or message content blocks using ${CONTEXT_DELETE_TOOL_NAME} or ${CONTEXT_GREP_DELETE_TOOL_NAME}. Use the focus query ${JSON.stringify(parameters.query)} to preserve relevant context. Call ${CONTEXT_COMPACTION_BUDGET_TOOL_NAME} to verify progress, and do not provide a final answer until the validated reduction is at least ${targetLabel}.`,
|
|
974
|
+
},
|
|
975
|
+
],
|
|
976
|
+
timestamp: Date.now(),
|
|
977
|
+
};
|
|
978
|
+
}
|
|
746
979
|
function assertSafeRegexPattern(pattern) {
|
|
747
980
|
if (pattern.length > CONTEXT_GREP_DELETE_MAX_REGEX_PATTERN_CHARS) {
|
|
748
981
|
throw new Error(`Regex pattern is too long (${pattern.length} characters); maximum is ${CONTEXT_GREP_DELETE_MAX_REGEX_PATTERN_CHARS}`);
|
|
@@ -809,6 +1042,41 @@ function addGrepCandidate(candidates, matches, seenTargets, candidate, match) {
|
|
|
809
1042
|
candidates.push(candidate);
|
|
810
1043
|
matches.push(match);
|
|
811
1044
|
}
|
|
1045
|
+
function pushProtectedGrepSkip(skipped, match) {
|
|
1046
|
+
skipped.push({
|
|
1047
|
+
entryId: match.entryId,
|
|
1048
|
+
target: match.target,
|
|
1049
|
+
...(match.blockIndex === undefined ? {} : { blockIndex: match.blockIndex }),
|
|
1050
|
+
reason: match.target === "content_block" ? "protected_block" : "protected_entry",
|
|
1051
|
+
text: match.text,
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
function filterProtectedGrepCandidates(candidates, matches, currentTargets, transcript, skipped) {
|
|
1055
|
+
const eligibleCandidates = [];
|
|
1056
|
+
const eligibleMatches = [];
|
|
1057
|
+
for (let index = 0; index < candidates.length; index++) {
|
|
1058
|
+
const candidate = candidates[index];
|
|
1059
|
+
const match = matches[index];
|
|
1060
|
+
if (!candidate || !match)
|
|
1061
|
+
continue;
|
|
1062
|
+
try {
|
|
1063
|
+
const mergedTargets = mergeContextDeletionTargets(currentTargets, [candidate]);
|
|
1064
|
+
validateContextDeletionRequest(deletionRequestFromTargets(mergedTargets), transcript);
|
|
1065
|
+
eligibleCandidates.push(candidate);
|
|
1066
|
+
eligibleMatches.push(match);
|
|
1067
|
+
}
|
|
1068
|
+
catch (error) {
|
|
1069
|
+
const message = formatErrorMessage(error);
|
|
1070
|
+
if (isProtectedContextDeletionErrorMessage(message)) {
|
|
1071
|
+
pushProtectedGrepSkip(skipped, match);
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
eligibleCandidates.push(candidate);
|
|
1075
|
+
eligibleMatches.push(match);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
return { candidates: eligibleCandidates, matches: eligibleMatches };
|
|
1079
|
+
}
|
|
812
1080
|
function copyDeletionTarget(target) {
|
|
813
1081
|
return target.kind === "entry"
|
|
814
1082
|
? { kind: "entry", entryId: target.entryId }
|
|
@@ -829,30 +1097,36 @@ class ContextDeletionMemoryStore {
|
|
|
829
1097
|
entryId: entry.entryId,
|
|
830
1098
|
role: entry.role,
|
|
831
1099
|
protected: entry.protected,
|
|
1100
|
+
hasAssistantThinkingBlocks: assistantEntryHasThinkingContentBlock(entry),
|
|
832
1101
|
tokenEstimate: entry.tokenEstimate,
|
|
833
1102
|
text: entry.text,
|
|
834
1103
|
};
|
|
835
1104
|
});
|
|
836
1105
|
this.entriesById = new Map(this.entries.map((entry) => [entry.entryId, entry]));
|
|
837
|
-
this.contentBlocks = transcript.entries.flatMap((entry, entryPosition) =>
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1106
|
+
this.contentBlocks = transcript.entries.flatMap((entry, entryPosition) => {
|
|
1107
|
+
const hasAssistantThinkingBlocks = assistantEntryHasThinkingContentBlock(entry);
|
|
1108
|
+
return entry.contentBlocks.map((block) => {
|
|
1109
|
+
if (block.entryId !== entry.entryId) {
|
|
1110
|
+
throw new Error(`Transcript content block ${block.entryId}:${block.blockIndex} does not belong to entry ${entry.entryId}`);
|
|
1111
|
+
}
|
|
1112
|
+
const blockKey = `${block.entryId}:${block.blockIndex}`;
|
|
1113
|
+
if (blockKeys.has(blockKey)) {
|
|
1114
|
+
throw new Error(`Duplicate transcript content block: ${blockKey}`);
|
|
1115
|
+
}
|
|
1116
|
+
blockKeys.add(blockKey);
|
|
1117
|
+
return {
|
|
1118
|
+
entryPosition,
|
|
1119
|
+
entryId: block.entryId,
|
|
1120
|
+
blockIndex: block.blockIndex,
|
|
1121
|
+
role: entry.role,
|
|
1122
|
+
type: block.type,
|
|
1123
|
+
protected: block.protected,
|
|
1124
|
+
hasAssistantThinkingBlocks,
|
|
1125
|
+
tokenEstimate: block.tokenEstimate,
|
|
1126
|
+
text: block.text,
|
|
1127
|
+
};
|
|
1128
|
+
});
|
|
1129
|
+
});
|
|
856
1130
|
this.contentBlockCountByEntryId = new Map();
|
|
857
1131
|
for (const block of this.contentBlocks) {
|
|
858
1132
|
this.contentBlockCountByEntryId.set(block.entryId, (this.contentBlockCountByEntryId.get(block.entryId) ?? 0) + 1);
|
|
@@ -879,6 +1153,7 @@ class ContextDeletionMemoryStore {
|
|
|
879
1153
|
entry_id: entry.entryId,
|
|
880
1154
|
text: entry.text,
|
|
881
1155
|
is_protected: entry.protected ? 1 : 0,
|
|
1156
|
+
has_assistant_thinking_blocks: entry.hasAssistantThinkingBlocks ? 1 : 0,
|
|
882
1157
|
}));
|
|
883
1158
|
}
|
|
884
1159
|
listContentBlocksForGrep() {
|
|
@@ -887,10 +1162,13 @@ class ContextDeletionMemoryStore {
|
|
|
887
1162
|
.map((block) => ({
|
|
888
1163
|
entry_id: block.entryId,
|
|
889
1164
|
block_index: block.blockIndex,
|
|
1165
|
+
role: block.role,
|
|
1166
|
+
type: block.type,
|
|
890
1167
|
text: block.text,
|
|
891
1168
|
entry_protected: this.entriesById.get(block.entryId)?.protected ? 1 : 0,
|
|
892
1169
|
block_protected: block.protected ? 1 : 0,
|
|
893
1170
|
block_count: this.contentBlockCountByEntryId.get(block.entryId) ?? 0,
|
|
1171
|
+
has_assistant_thinking_blocks: block.hasAssistantThinkingBlocks ? 1 : 0,
|
|
894
1172
|
}));
|
|
895
1173
|
}
|
|
896
1174
|
getEntryForRead(entryId) {
|
|
@@ -901,6 +1179,7 @@ class ContextDeletionMemoryStore {
|
|
|
901
1179
|
entry_id: entry.entryId,
|
|
902
1180
|
role: entry.role,
|
|
903
1181
|
is_protected: entry.protected ? 1 : 0,
|
|
1182
|
+
has_assistant_thinking_blocks: entry.hasAssistantThinkingBlocks ? 1 : 0,
|
|
904
1183
|
token_estimate: entry.tokenEstimate,
|
|
905
1184
|
text: entry.text,
|
|
906
1185
|
};
|
|
@@ -912,12 +1191,14 @@ class ContextDeletionMemoryStore {
|
|
|
912
1191
|
return {
|
|
913
1192
|
entry_id: block.entryId,
|
|
914
1193
|
block_index: block.blockIndex,
|
|
1194
|
+
role: block.role,
|
|
915
1195
|
type: block.type,
|
|
916
1196
|
token_estimate: block.tokenEstimate,
|
|
917
1197
|
text: block.text,
|
|
918
1198
|
entry_protected: this.entriesById.get(block.entryId)?.protected ? 1 : 0,
|
|
919
1199
|
block_protected: block.protected ? 1 : 0,
|
|
920
1200
|
block_count: this.contentBlockCountByEntryId.get(block.entryId) ?? 0,
|
|
1201
|
+
has_assistant_thinking_blocks: block.hasAssistantThinkingBlocks ? 1 : 0,
|
|
921
1202
|
};
|
|
922
1203
|
}
|
|
923
1204
|
getGrepScanTextLength(target) {
|
|
@@ -956,8 +1237,10 @@ class ContextDeletionMemoryStore {
|
|
|
956
1237
|
function createContextDeletionStore(transcript) {
|
|
957
1238
|
return new ContextDeletionMemoryStore(transcript);
|
|
958
1239
|
}
|
|
959
|
-
export function createContextDeletionTool(
|
|
960
|
-
const
|
|
1240
|
+
export function createContextDeletionTool(inputTranscript, options = {}) {
|
|
1241
|
+
const contextWindow = finitePositiveNumber(options.contextWindow);
|
|
1242
|
+
const parameters = normalizeContextCompactionParameters({ ...getTranscriptCompactionParameters(inputTranscript), ...options }, inputTranscript.parameters?.query ?? CONTEXT_COMPACTION_AUTO_QUERY);
|
|
1243
|
+
const transcript = { ...inputTranscript, parameters };
|
|
961
1244
|
const store = createContextDeletionStore(transcript);
|
|
962
1245
|
let validatedResult;
|
|
963
1246
|
function readTargets() {
|
|
@@ -965,7 +1248,7 @@ export function createContextDeletionTool(transcript, options = {}) {
|
|
|
965
1248
|
}
|
|
966
1249
|
function applyValidatedTargets(additionalTargets) {
|
|
967
1250
|
const mergedTargets = mergeContextDeletionTargets(readTargets(), additionalTargets);
|
|
968
|
-
validatedResult = validateContextDeletionRequest(deletionRequestFromTargets(mergedTargets), transcript
|
|
1251
|
+
validatedResult = validateContextDeletionRequest(deletionRequestFromTargets(mergedTargets), transcript);
|
|
969
1252
|
store.replaceTargets(validatedResult.deletedTargets);
|
|
970
1253
|
return validatedResult;
|
|
971
1254
|
}
|
|
@@ -973,7 +1256,7 @@ export function createContextDeletionTool(transcript, options = {}) {
|
|
|
973
1256
|
return validatedResult?.stats ?? computeContextCompactionStats(transcript, readTargets());
|
|
974
1257
|
}
|
|
975
1258
|
function canDeleteProtectedTarget(target) {
|
|
976
|
-
return
|
|
1259
|
+
return canDeleteTarget(transcript, target);
|
|
977
1260
|
}
|
|
978
1261
|
const tool = {
|
|
979
1262
|
...CONTEXT_DELETE_TOOL,
|
|
@@ -984,7 +1267,7 @@ export function createContextDeletionTool(transcript, options = {}) {
|
|
|
984
1267
|
const callCount = store.incrementCallCount();
|
|
985
1268
|
try {
|
|
986
1269
|
const incomingRequest = contextDeletionRequestFromObject(params, `${CONTEXT_DELETE_TOOL_NAME} arguments`);
|
|
987
|
-
const incomingValidated = validateContextDeletionRequest(incomingRequest, transcript
|
|
1270
|
+
const incomingValidated = validateContextDeletionRequest(incomingRequest, transcript);
|
|
988
1271
|
const applied = applyValidatedTargets(incomingValidated.deletedTargets);
|
|
989
1272
|
store.clearLastError();
|
|
990
1273
|
const deletedTargets = readTargets();
|
|
@@ -1027,6 +1310,7 @@ export function createContextDeletionTool(transcript, options = {}) {
|
|
|
1027
1310
|
const maxMatches = params.maxMatches ?? CONTEXT_GREP_DELETE_DEFAULT_MAX_MATCHES;
|
|
1028
1311
|
const candidates = [];
|
|
1029
1312
|
const matches = [];
|
|
1313
|
+
let reportedMatches = matches;
|
|
1030
1314
|
const skipped = [];
|
|
1031
1315
|
const seenTargets = new Set();
|
|
1032
1316
|
try {
|
|
@@ -1035,11 +1319,16 @@ export function createContextDeletionTool(transcript, options = {}) {
|
|
|
1035
1319
|
}
|
|
1036
1320
|
const matcher = createGrepMatcher(pattern, regex, caseSensitive);
|
|
1037
1321
|
const currentTargets = readTargets();
|
|
1322
|
+
const recentEntryIds = getRecentContextEntryIds(transcript);
|
|
1038
1323
|
if (target === "entry") {
|
|
1039
1324
|
for (const entry of store.listEntriesForGrep()) {
|
|
1040
1325
|
if (!matcher.test(entry.text))
|
|
1041
1326
|
continue;
|
|
1042
1327
|
const candidate = { kind: "entry", entryId: entry.entry_id };
|
|
1328
|
+
if (recentEntryIds.has(candidate.entryId)) {
|
|
1329
|
+
skipped.push({ entryId: entry.entry_id, target, reason: "protected_entry", text: entry.text });
|
|
1330
|
+
continue;
|
|
1331
|
+
}
|
|
1043
1332
|
if (entry.is_protected === 1 && !canDeleteProtectedTarget(candidate)) {
|
|
1044
1333
|
skipped.push({ entryId: entry.entry_id, target, reason: "protected_entry", text: entry.text });
|
|
1045
1334
|
continue;
|
|
@@ -1062,6 +1351,16 @@ export function createContextDeletionTool(transcript, options = {}) {
|
|
|
1062
1351
|
const candidate = block.block_count <= 1
|
|
1063
1352
|
? { kind: "entry", entryId: block.entry_id }
|
|
1064
1353
|
: { kind: "content_block", entryId: block.entry_id, blockIndex: block.block_index };
|
|
1354
|
+
if (recentEntryIds.has(candidate.entryId)) {
|
|
1355
|
+
skipped.push({
|
|
1356
|
+
entryId: block.entry_id,
|
|
1357
|
+
target: candidate.kind,
|
|
1358
|
+
...(candidate.kind === "content_block" ? { blockIndex: candidate.blockIndex } : {}),
|
|
1359
|
+
reason: "protected_entry",
|
|
1360
|
+
text: block.text,
|
|
1361
|
+
});
|
|
1362
|
+
continue;
|
|
1363
|
+
}
|
|
1065
1364
|
if (block.entry_protected === 1 && !canDeleteProtectedTarget(candidate)) {
|
|
1066
1365
|
skipped.push({
|
|
1067
1366
|
entryId: block.entry_id,
|
|
@@ -1100,15 +1399,17 @@ export function createContextDeletionTool(transcript, options = {}) {
|
|
|
1100
1399
|
});
|
|
1101
1400
|
}
|
|
1102
1401
|
}
|
|
1402
|
+
const eligible = filterProtectedGrepCandidates(candidates, matches, currentTargets, transcript, skipped);
|
|
1403
|
+
reportedMatches = eligible.matches;
|
|
1103
1404
|
let applied;
|
|
1104
|
-
if (params.expectedMatchCount !== undefined && candidates.length !== params.expectedMatchCount) {
|
|
1405
|
+
if (params.expectedMatchCount !== undefined && eligible.candidates.length !== params.expectedMatchCount) {
|
|
1105
1406
|
skipped.push({ reason: "expected_match_count_mismatch" });
|
|
1106
1407
|
}
|
|
1107
|
-
else if (candidates.length > maxMatches) {
|
|
1408
|
+
else if (eligible.candidates.length > maxMatches) {
|
|
1108
1409
|
skipped.push({ reason: "max_matches_exceeded" });
|
|
1109
1410
|
}
|
|
1110
|
-
else if (candidates.length > 0) {
|
|
1111
|
-
applied = applyValidatedTargets(candidates);
|
|
1411
|
+
else if (eligible.candidates.length > 0) {
|
|
1412
|
+
applied = applyValidatedTargets(eligible.candidates);
|
|
1112
1413
|
}
|
|
1113
1414
|
store.clearLastError();
|
|
1114
1415
|
const deletedTargets = readTargets();
|
|
@@ -1117,13 +1418,13 @@ export function createContextDeletionTool(transcript, options = {}) {
|
|
|
1117
1418
|
regex,
|
|
1118
1419
|
caseSensitive,
|
|
1119
1420
|
target,
|
|
1120
|
-
matches,
|
|
1421
|
+
matches: eligible.matches,
|
|
1121
1422
|
skipped,
|
|
1122
1423
|
deletedTargets,
|
|
1123
1424
|
stats: applied?.stats ?? currentStats(),
|
|
1124
1425
|
callCount,
|
|
1125
1426
|
};
|
|
1126
|
-
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}.`;
|
|
1427
|
+
const text = `Matched ${eligible.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}.`;
|
|
1127
1428
|
return createContextDeletionToolResult(text, details);
|
|
1128
1429
|
}
|
|
1129
1430
|
catch (error) {
|
|
@@ -1135,7 +1436,7 @@ export function createContextDeletionTool(transcript, options = {}) {
|
|
|
1135
1436
|
regex,
|
|
1136
1437
|
caseSensitive,
|
|
1137
1438
|
target,
|
|
1138
|
-
matches,
|
|
1439
|
+
matches: reportedMatches,
|
|
1139
1440
|
skipped,
|
|
1140
1441
|
deletedTargets,
|
|
1141
1442
|
stats: currentStats(),
|
|
@@ -1290,53 +1591,38 @@ export function createContextDeletionTool(transcript, options = {}) {
|
|
|
1290
1591
|
});
|
|
1291
1592
|
},
|
|
1292
1593
|
};
|
|
1594
|
+
const budgetTool = {
|
|
1595
|
+
...CONTEXT_COMPACTION_BUDGET_TOOL,
|
|
1596
|
+
label: "context compaction budget",
|
|
1597
|
+
executionMode: "parallel",
|
|
1598
|
+
async execute(_toolCallId) {
|
|
1599
|
+
return store.transaction(() => {
|
|
1600
|
+
const callCount = store.incrementCallCount();
|
|
1601
|
+
store.clearLastError();
|
|
1602
|
+
const details = createContextCompactionBudgetDetails(currentStats(), callCount, contextWindow, parameters);
|
|
1603
|
+
const windowText = details.contextWindowBeforePercent !== undefined
|
|
1604
|
+
? ` Context window fullness: ${details.contextWindowBeforePercent}% before selected deletions, ${details.contextWindowAfterPercent}% after selected deletions.`
|
|
1605
|
+
: " Context window size is unknown for this model, so fullness percentages are unavailable.";
|
|
1606
|
+
const targetText = details.tokensToDeleteForTarget > 0
|
|
1607
|
+
? ` Delete about ${details.tokensToDeleteForTarget} more token(s) to reach the ${details.targetReductionPercent}% reduction target.`
|
|
1608
|
+
: ` The selected deletions meet or exceed the ${details.targetReductionPercent}% reduction target.`;
|
|
1609
|
+
return createContextDeletionToolResult(`Current selected deletions reduce context by ${details.currentReductionPercent}% (${details.deletedTokens} token(s)); tokens after selected deletions: ${details.currentTokensAfter}/${details.tokensBefore}.${windowText}${targetText} Keep maximizing useful retained context while aggressively removing low-value blocks.`, details);
|
|
1610
|
+
});
|
|
1611
|
+
},
|
|
1612
|
+
};
|
|
1293
1613
|
return {
|
|
1294
1614
|
tool,
|
|
1295
1615
|
grepTool,
|
|
1296
1616
|
searchTool,
|
|
1297
1617
|
readEntryTool,
|
|
1298
|
-
|
|
1618
|
+
budgetTool,
|
|
1619
|
+
tools: [tool, grepTool, searchTool, readEntryTool, budgetTool],
|
|
1299
1620
|
getDeletionRequest: () => deletionRequestFromTargets(readTargets()),
|
|
1300
1621
|
getValidatedResult: () => validatedResult,
|
|
1301
1622
|
getLastError: () => store.getLastError(),
|
|
1302
1623
|
getCallCount: () => store.getCallCount(),
|
|
1303
1624
|
};
|
|
1304
1625
|
}
|
|
1305
|
-
export function parseContextDeletionRequest(text) {
|
|
1306
|
-
const stripped = stripJsonFence(text);
|
|
1307
|
-
let parsed;
|
|
1308
|
-
try {
|
|
1309
|
-
parsed = JSON.parse(stripped);
|
|
1310
|
-
}
|
|
1311
|
-
catch (error) {
|
|
1312
|
-
throw new Error(`Failed to parse context deletion request JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
1313
|
-
}
|
|
1314
|
-
return contextDeletionRequestFromObject(parsed, "Context deletion request JSON");
|
|
1315
|
-
}
|
|
1316
|
-
function isContextDeleteToolCall(content) {
|
|
1317
|
-
return content.type === "toolCall" && content.name === CONTEXT_DELETE_TOOL_NAME;
|
|
1318
|
-
}
|
|
1319
|
-
function textContentFromResponse(response) {
|
|
1320
|
-
return response.content
|
|
1321
|
-
.filter((content) => content.type === "text")
|
|
1322
|
-
.map((content) => content.text)
|
|
1323
|
-
.join("\n");
|
|
1324
|
-
}
|
|
1325
|
-
export function parseContextDeletionResponse(response) {
|
|
1326
|
-
const toolCalls = response.content.filter(isContextDeleteToolCall);
|
|
1327
|
-
if (toolCalls.length > 1) {
|
|
1328
|
-
throw new Error(`Context compaction assistant called ${CONTEXT_DELETE_TOOL_NAME} more than once`);
|
|
1329
|
-
}
|
|
1330
|
-
const toolCall = toolCalls[0];
|
|
1331
|
-
if (toolCall) {
|
|
1332
|
-
return contextDeletionRequestFromObject(toolCall.arguments, `${CONTEXT_DELETE_TOOL_NAME} arguments`);
|
|
1333
|
-
}
|
|
1334
|
-
const textContent = textContentFromResponse(response);
|
|
1335
|
-
if (textContent.trim().length === 0) {
|
|
1336
|
-
throw new Error(`Context compaction assistant did not call ${CONTEXT_DELETE_TOOL_NAME}`);
|
|
1337
|
-
}
|
|
1338
|
-
return parseContextDeletionRequest(textContent);
|
|
1339
|
-
}
|
|
1340
1626
|
function truncateForPrompt(text, maxChars) {
|
|
1341
1627
|
if (text.length <= maxChars)
|
|
1342
1628
|
return text;
|
|
@@ -1420,14 +1706,16 @@ function contextCompactionTranscriptManifest(transcript, transcriptFilePath) {
|
|
|
1420
1706
|
})),
|
|
1421
1707
|
};
|
|
1422
1708
|
}
|
|
1423
|
-
function
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1709
|
+
function contextCompactionParametersPrompt(parameters) {
|
|
1710
|
+
return `\n<compaction-parameters>\n${JSON.stringify({
|
|
1711
|
+
compression_ratio: parameters.compression_ratio,
|
|
1712
|
+
preserve_recent: parameters.preserve_recent,
|
|
1713
|
+
query: parameters.query,
|
|
1714
|
+
target_reduction_percent: contextCompactionTargetReductionPercent(parameters),
|
|
1715
|
+
}, null, 2)}\n</compaction-parameters>`;
|
|
1428
1716
|
}
|
|
1429
|
-
export function buildContextCompactionPrompt(transcript, transcriptFilePath = "<transcript file will be written during context compaction>",
|
|
1430
|
-
return `${
|
|
1717
|
+
export function buildContextCompactionPrompt(transcript, transcriptFilePath = "<transcript file will be written during context compaction>", parameters = getTranscriptCompactionParameters(transcript)) {
|
|
1718
|
+
return `${contextCompactionFixedPrompt(parameters)}${contextCompactionParametersPrompt(parameters)}\n\n<transcript-file>\n${transcriptFilePath}\n</transcript-file>\n\n<context-manifest>\n${JSON.stringify(contextCompactionTranscriptManifest(transcript, transcriptFilePath), null, 2)}\n</context-manifest>`;
|
|
1431
1719
|
}
|
|
1432
1720
|
function createContextCompactionAssistantMessage(model, content, stopReason, errorMessage) {
|
|
1433
1721
|
return {
|
|
@@ -1461,7 +1749,8 @@ function createContextCompactionStopStream(model, text) {
|
|
|
1461
1749
|
function isContextCompactionOverflowError(model, errorMessage) {
|
|
1462
1750
|
return isContextOverflow(createContextCompactionAssistantMessage(model, [], "error", errorMessage), model.contextWindow);
|
|
1463
1751
|
}
|
|
1464
|
-
async function runContextDeletionAssistant(
|
|
1752
|
+
async function runContextDeletionAssistant(inputTranscript, model, apiKey, headers, signal, thinkingLevel = "off", parameters = getTranscriptCompactionParameters(inputTranscript)) {
|
|
1753
|
+
const transcript = { ...inputTranscript, parameters };
|
|
1465
1754
|
const maxTokens = model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY;
|
|
1466
1755
|
if (signal?.aborted) {
|
|
1467
1756
|
throw new Error("Context compaction failed: Request was aborted");
|
|
@@ -1469,11 +1758,10 @@ async function runContextDeletionAssistant(transcript, model, apiKey, headers, s
|
|
|
1469
1758
|
const transcriptFile = writeContextCompactionTranscriptFile(transcript);
|
|
1470
1759
|
const promptMessage = {
|
|
1471
1760
|
role: "user",
|
|
1472
|
-
content: [{ type: "text", text: buildContextCompactionPrompt(transcript, transcriptFile.path,
|
|
1761
|
+
content: [{ type: "text", text: buildContextCompactionPrompt(transcript, transcriptFile.path, parameters) }],
|
|
1473
1762
|
timestamp: Date.now(),
|
|
1474
1763
|
};
|
|
1475
|
-
const deletionTool = createContextDeletionTool(transcript, {
|
|
1476
|
-
let compactionTurnCount = 0;
|
|
1764
|
+
const deletionTool = createContextDeletionTool(transcript, { contextWindow: model.contextWindow, ...parameters });
|
|
1477
1765
|
const agent = new Agent({
|
|
1478
1766
|
initialState: {
|
|
1479
1767
|
systemPrompt: CONTEXT_COMPACTION_SYSTEM_PROMPT,
|
|
@@ -1483,9 +1771,9 @@ async function runContextDeletionAssistant(transcript, model, apiKey, headers, s
|
|
|
1483
1771
|
},
|
|
1484
1772
|
toolExecution: "parallel",
|
|
1485
1773
|
streamFn: async (requestModel, context, streamOptions) => {
|
|
1486
|
-
|
|
1487
|
-
if (
|
|
1488
|
-
return createContextCompactionStopStream(requestModel, `Reached the
|
|
1774
|
+
const currentResult = deletionTool.getValidatedResult();
|
|
1775
|
+
if (contextCompactionTargetMet(currentResult, parameters)) {
|
|
1776
|
+
return createContextCompactionStopStream(requestModel, `Reached the strict ${contextCompactionTargetLabel(parameters)} context-reduction requirement (${currentResult.stats.percentReduction}%); using the validated deletions recorded so far.`);
|
|
1489
1777
|
}
|
|
1490
1778
|
return streamSimple(requestModel, context, {
|
|
1491
1779
|
...streamOptions,
|
|
@@ -1495,6 +1783,25 @@ async function runContextDeletionAssistant(transcript, model, apiKey, headers, s
|
|
|
1495
1783
|
});
|
|
1496
1784
|
},
|
|
1497
1785
|
});
|
|
1786
|
+
let lastNudgedProgressKey;
|
|
1787
|
+
const unsubscribeNudge = agent.subscribe((event, eventSignal) => {
|
|
1788
|
+
if (event.type !== "turn_end" || signal?.aborted || eventSignal.aborted)
|
|
1789
|
+
return;
|
|
1790
|
+
if (event.message.role !== "assistant")
|
|
1791
|
+
return;
|
|
1792
|
+
if (event.message.stopReason === "error" || event.message.stopReason === "aborted")
|
|
1793
|
+
return;
|
|
1794
|
+
if (event.message.content.some((content) => content.type === "toolCall"))
|
|
1795
|
+
return;
|
|
1796
|
+
const currentResult = deletionTool.getValidatedResult();
|
|
1797
|
+
if (contextCompactionTargetMet(currentResult, parameters))
|
|
1798
|
+
return;
|
|
1799
|
+
const progressKey = contextCompactionProgressKey(currentResult);
|
|
1800
|
+
if (progressKey === lastNudgedProgressKey)
|
|
1801
|
+
return;
|
|
1802
|
+
lastNudgedProgressKey = progressKey;
|
|
1803
|
+
agent.followUp(createContextCompactionTargetNudgeMessage(currentResult, parameters));
|
|
1804
|
+
});
|
|
1498
1805
|
const abortOnSignal = () => agent.abort();
|
|
1499
1806
|
signal?.addEventListener("abort", abortOnSignal, { once: true });
|
|
1500
1807
|
try {
|
|
@@ -1502,6 +1809,7 @@ async function runContextDeletionAssistant(transcript, model, apiKey, headers, s
|
|
|
1502
1809
|
}
|
|
1503
1810
|
finally {
|
|
1504
1811
|
signal?.removeEventListener("abort", abortOnSignal);
|
|
1812
|
+
unsubscribeNudge();
|
|
1505
1813
|
transcriptFile.cleanup();
|
|
1506
1814
|
}
|
|
1507
1815
|
if (signal?.aborted) {
|
|
@@ -1517,18 +1825,39 @@ async function runContextDeletionAssistant(transcript, model, apiKey, headers, s
|
|
|
1517
1825
|
throw new Error(`Context compaction failed: ${agent.state.errorMessage}`);
|
|
1518
1826
|
}
|
|
1519
1827
|
if (deletionTool.getCallCount() === 0) {
|
|
1520
|
-
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})`);
|
|
1828
|
+
throw new Error(`Context compaction did not call any transcript inspection, budget, or deletion tools (${CONTEXT_SEARCH_TRANSCRIPT_TOOL_NAME}, ${CONTEXT_READ_ENTRY_TOOL_NAME}, ${CONTEXT_COMPACTION_BUDGET_TOOL_NAME}, ${CONTEXT_DELETE_TOOL_NAME}, or ${CONTEXT_GREP_DELETE_TOOL_NAME})`);
|
|
1521
1829
|
}
|
|
1522
1830
|
return {
|
|
1523
1831
|
validatedResult: deletionTool.getValidatedResult(),
|
|
1524
1832
|
lastToolError: deletionTool.getLastError(),
|
|
1525
1833
|
};
|
|
1526
1834
|
}
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1835
|
+
function hasMetContextCompactionTarget(run, parameters) {
|
|
1836
|
+
return contextCompactionTargetMet(run.validatedResult, parameters);
|
|
1837
|
+
}
|
|
1838
|
+
function formatContextCompactionTargetFailureMessage(attempts, parameters) {
|
|
1839
|
+
const targetLabel = contextCompactionTargetLabel(parameters);
|
|
1840
|
+
if (attempts.length === 0) {
|
|
1841
|
+
return `Context compaction did not meet the strict ${targetLabel} reduction requirement`;
|
|
1531
1842
|
}
|
|
1532
|
-
|
|
1843
|
+
const attemptDetails = attempts
|
|
1844
|
+
.map((attempt) => {
|
|
1845
|
+
const reduction = contextCompactionProgressPercent(attempt.validatedResult);
|
|
1846
|
+
const deletionCount = attempt.validatedResult?.deletedTargets.length ?? 0;
|
|
1847
|
+
const errorText = attempt.lastToolError ? `; last deletion tool error: ${attempt.lastToolError}` : "";
|
|
1848
|
+
return `attempt reached ${reduction}% with ${deletionCount} validated deletion target(s)${errorText}`;
|
|
1849
|
+
})
|
|
1850
|
+
.join("; ");
|
|
1851
|
+
return `Context compaction did not meet the strict ${targetLabel} reduction requirement; ${attemptDetails}`;
|
|
1852
|
+
}
|
|
1853
|
+
export async function contextCompact(preparation, model, apiKey, headers, signal, thinkingLevel = "off") {
|
|
1854
|
+
const parameters = normalizeContextCompactionParameters(preparation.parameters ?? preparation.transcript.parameters, preparation.parameters?.query ?? preparation.transcript.parameters?.query ?? CONTEXT_COMPACTION_AUTO_QUERY);
|
|
1855
|
+
const transcript = { ...preparation.transcript, parameters };
|
|
1856
|
+
const attempts = [];
|
|
1857
|
+
const standardRun = await runContextDeletionAssistant(transcript, model, apiKey, headers, signal, thinkingLevel, parameters);
|
|
1858
|
+
if (hasMetContextCompactionTarget(standardRun, parameters))
|
|
1859
|
+
return standardRun.validatedResult;
|
|
1860
|
+
attempts.push({ ...standardRun });
|
|
1861
|
+
throw new Error(formatContextCompactionTargetFailureMessage(attempts, parameters));
|
|
1533
1862
|
}
|
|
1534
1863
|
//# sourceMappingURL=context-compaction.js.map
|