@bastani/atomic 0.8.29 → 0.8.30-alpha.2
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 +26 -0
- package/dist/builtin/cursor/CHANGELOG.md +4 -0
- package/dist/builtin/cursor/package.json +2 -2
- package/dist/builtin/intercom/CHANGELOG.md +4 -0
- package/dist/builtin/intercom/package.json +2 -2
- package/dist/builtin/mcp/CHANGELOG.md +4 -0
- package/dist/builtin/mcp/package.json +3 -3
- package/dist/builtin/subagents/CHANGELOG.md +9 -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 +4 -0
- package/dist/builtin/web-access/package.json +2 -2
- package/dist/builtin/workflows/CHANGELOG.md +11 -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 +561 -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 +470 -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 reasoning steps, EXCEPT do not delete any content block from the latest assistant message when that message contains thinking or redacted_thinking blocks.
|
|
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,113 @@ 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, deletedEntryIds = new Set()) {
|
|
539
|
+
for (let index = transcript.entries.length - 1; index >= 0; index--) {
|
|
540
|
+
const entry = transcript.entries[index];
|
|
541
|
+
if (entry.role === "assistant" && !deletedEntryIds.has(entry.entryId))
|
|
542
|
+
return entry;
|
|
543
|
+
}
|
|
544
|
+
return undefined;
|
|
545
|
+
}
|
|
546
|
+
function findLatestAssistantThinkingDeletionViolation(transcript, targets) {
|
|
547
|
+
const deletedEntryIds = getDeletedEntryIds(targets);
|
|
548
|
+
const latestRetainedAssistant = latestAssistantEntry(transcript, deletedEntryIds);
|
|
549
|
+
for (const target of targets) {
|
|
550
|
+
if (target.kind === "entry") {
|
|
551
|
+
const entry = findTranscriptEntry(transcript, target.entryId);
|
|
552
|
+
if (!entry || !assistantEntryHasThinkingContentBlock(entry))
|
|
553
|
+
continue;
|
|
554
|
+
const deletedEntryIdsIfTargetWereKept = new Set(deletedEntryIds);
|
|
555
|
+
deletedEntryIdsIfTargetWereKept.delete(target.entryId);
|
|
556
|
+
if (latestAssistantEntry(transcript, deletedEntryIdsIfTargetWereKept)?.entryId === target.entryId) {
|
|
557
|
+
return target;
|
|
558
|
+
}
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (latestRetainedAssistant?.entryId === target.entryId &&
|
|
562
|
+
assistantEntryHasThinkingContentBlock(latestRetainedAssistant)) {
|
|
563
|
+
return target;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return undefined;
|
|
567
|
+
}
|
|
568
|
+
function assertNoLatestAssistantThinkingDeletionTargets(transcript, targets) {
|
|
569
|
+
const violation = findLatestAssistantThinkingDeletionViolation(transcript, targets);
|
|
570
|
+
if (!violation)
|
|
571
|
+
return;
|
|
572
|
+
if (violation.kind === "entry") {
|
|
573
|
+
throw new Error(`Cannot delete assistant entry ${violation.entryId} because it is the latest assistant message retained after other deletions and contains thinking/redacted_thinking content blocks`);
|
|
574
|
+
}
|
|
575
|
+
throw new Error(`Cannot delete content block ${violation.entryId}:${violation.blockIndex} because a thinking/redacted_thinking block in the latest assistant message must remain unmodified; the latest retained assistant message contains thinking/redacted_thinking content blocks`);
|
|
576
|
+
}
|
|
377
577
|
function isToolCallBlockDeleted(entry, callId, deletedEntryIds, deletedContentBlocks) {
|
|
378
578
|
if (deletedEntryIds.has(entry.entryId))
|
|
379
579
|
return true;
|
|
@@ -404,15 +604,6 @@ function deleteEntryTarget(targets, entryId) {
|
|
|
404
604
|
}
|
|
405
605
|
return addTarget(targets, { kind: "entry", entryId }) || changed;
|
|
406
606
|
}
|
|
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
607
|
function mergeContextDeletionTargets(baseTargets, additionalTargets) {
|
|
417
608
|
const targets = [...baseTargets];
|
|
418
609
|
for (const target of additionalTargets) {
|
|
@@ -426,8 +617,10 @@ function mergeContextDeletionTargets(baseTargets, additionalTargets) {
|
|
|
426
617
|
}
|
|
427
618
|
return targets;
|
|
428
619
|
}
|
|
429
|
-
function canonicalizeEntryTargets(targets, entry) {
|
|
430
|
-
if (entry
|
|
620
|
+
function canonicalizeEntryTargets(transcript, targets, entry) {
|
|
621
|
+
if (!canDeleteTarget(transcript, { kind: "entry", entryId: entry.entryId }))
|
|
622
|
+
return false;
|
|
623
|
+
if (getDeletedEntryIds(targets).has(entry.entryId))
|
|
431
624
|
return false;
|
|
432
625
|
const deletedBlocks = getDeletedContentBlocks(targets).get(entry.entryId);
|
|
433
626
|
if (!deletedBlocks || !entry.contentBlocks.every((block) => deletedBlocks.has(block.blockIndex)))
|
|
@@ -436,28 +629,17 @@ function canonicalizeEntryTargets(targets, entry) {
|
|
|
436
629
|
// request every block individually stay invalid so the assistant must choose explicit entry deletion.
|
|
437
630
|
return deleteEntryTarget(targets, entry.entryId);
|
|
438
631
|
}
|
|
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;
|
|
632
|
+
function addToolCallDeletion(transcript, targets, entry, callId) {
|
|
454
633
|
let changed = false;
|
|
455
634
|
for (const blockIndex of toolCallBlockIndexes(entry, callId)) {
|
|
635
|
+
const target = { kind: "content_block", entryId: entry.entryId, blockIndex };
|
|
636
|
+
if (!canDeleteTarget(transcript, target))
|
|
637
|
+
continue;
|
|
456
638
|
if (!getDeletedEntryIds(targets).has(entry.entryId)) {
|
|
457
|
-
changed = addTarget(targets,
|
|
639
|
+
changed = addTarget(targets, target) || changed;
|
|
458
640
|
}
|
|
459
641
|
}
|
|
460
|
-
return canonicalizeEntryTargets(targets, entry) || changed;
|
|
642
|
+
return canonicalizeEntryTargets(transcript, targets, entry) || changed;
|
|
461
643
|
}
|
|
462
644
|
let warnedReconciliationNonConvergence = false;
|
|
463
645
|
function reconcileToolDependencies(transcript, initialTargets) {
|
|
@@ -496,9 +678,14 @@ function reconcileToolDependencies(transcript, initialTargets) {
|
|
|
496
678
|
const callDeleted = isToolCallBlockDeleted(callEntry, callId, deletedEntryIds, deletedContentBlocks);
|
|
497
679
|
const results = resultEntries.get(callId) ?? [];
|
|
498
680
|
if (callDeleted) {
|
|
499
|
-
const retainedProtectedResult = results.find((entry) =>
|
|
681
|
+
const retainedProtectedResult = results.find((entry) => !deletedEntryIds.has(entry.entryId) &&
|
|
682
|
+
!canDeleteTarget(transcript, { kind: "entry", entryId: entry.entryId }));
|
|
500
683
|
if (retainedProtectedResult) {
|
|
501
|
-
|
|
684
|
+
const retainedResultTarget = { kind: "entry", entryId: retainedProtectedResult.entryId };
|
|
685
|
+
if (isRecentTarget(transcript, retainedResultTarget)) {
|
|
686
|
+
throw new Error(formatRecentContextDeletionError(transcript, retainedResultTarget));
|
|
687
|
+
}
|
|
688
|
+
throw new Error(formatProtectedToolDependencyError(transcript, retainedResultTarget, `Cannot delete tool call ${callId} because its paired tool result entry ${retainedProtectedResult.entryId} is protected.`));
|
|
502
689
|
}
|
|
503
690
|
else {
|
|
504
691
|
for (const result of results) {
|
|
@@ -512,15 +699,19 @@ function reconcileToolDependencies(transcript, initialTargets) {
|
|
|
512
699
|
if (!deletedEntryIds.has(result.entryId))
|
|
513
700
|
continue;
|
|
514
701
|
recordChange(deleteEntryTarget(targets, result.entryId));
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
702
|
+
const callEntryTarget = { kind: "entry", entryId: callEntry.entryId };
|
|
703
|
+
const callBlockTarget = firstToolCallBlockTarget(callEntry, callId) ?? callEntryTarget;
|
|
704
|
+
if (!canDeleteTarget(transcript, callBlockTarget)) {
|
|
705
|
+
if (isRecentTarget(transcript, callBlockTarget)) {
|
|
706
|
+
throw new Error(formatRecentContextDeletionError(transcript, callBlockTarget));
|
|
707
|
+
}
|
|
708
|
+
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
709
|
}
|
|
519
|
-
recordChange(addToolCallDeletion(targets, callEntry, callId));
|
|
710
|
+
recordChange(addToolCallDeletion(transcript, targets, callEntry, callId));
|
|
520
711
|
}
|
|
521
712
|
}
|
|
522
713
|
for (const entry of entriesWithToolCalls) {
|
|
523
|
-
recordChange(canonicalizeEntryTargets(targets, entry));
|
|
714
|
+
recordChange(canonicalizeEntryTargets(transcript, targets, entry));
|
|
524
715
|
}
|
|
525
716
|
}
|
|
526
717
|
if (changed && !warnedReconciliationNonConvergence) {
|
|
@@ -602,11 +793,7 @@ function computeContextCompactionStats(transcript, targets) {
|
|
|
602
793
|
* message, an extension-injected `custom` message, or a branch summary (`branchSummary` role /
|
|
603
794
|
* `branch_summary` entry type) that recaps an earlier branch's task.
|
|
604
795
|
*
|
|
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.
|
|
796
|
+
* Verbatim compaction must always leave at least one task-bearing entry in context.
|
|
610
797
|
*/
|
|
611
798
|
function isTaskBearingEntry(entry) {
|
|
612
799
|
return (entry.role === "user" ||
|
|
@@ -614,37 +801,31 @@ function isTaskBearingEntry(entry) {
|
|
|
614
801
|
entry.role === "branchSummary" ||
|
|
615
802
|
entry.entryType === "branch_summary");
|
|
616
803
|
}
|
|
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);
|
|
804
|
+
function isRecentTarget(transcript, target) {
|
|
805
|
+
const entry = transcript.entries.find((candidate) => candidate.entryId === target.entryId);
|
|
806
|
+
return entry !== undefined && isRecentContextEntry(entry, transcript);
|
|
630
807
|
}
|
|
631
|
-
function
|
|
632
|
-
if (mode !== "critical_overflow")
|
|
633
|
-
return false;
|
|
808
|
+
function canDeleteTarget(transcript, target) {
|
|
634
809
|
const entry = transcript.entries.find((candidate) => candidate.entryId === target.entryId);
|
|
635
|
-
if (!entry
|
|
810
|
+
if (!entry)
|
|
811
|
+
return false;
|
|
812
|
+
if (isRecentTarget(transcript, target))
|
|
813
|
+
return false;
|
|
814
|
+
if (entry.protected)
|
|
636
815
|
return false;
|
|
637
816
|
if (target.kind === "entry")
|
|
638
817
|
return true;
|
|
639
818
|
const block = entry.contentBlocks.find((candidate) => candidate.blockIndex === target.blockIndex);
|
|
640
|
-
|
|
819
|
+
if (!block)
|
|
820
|
+
return false;
|
|
821
|
+
return !block.protected;
|
|
641
822
|
}
|
|
642
|
-
export function validateContextDeletionRequest(request, transcript
|
|
643
|
-
const mode = options.mode ?? "standard";
|
|
823
|
+
export function validateContextDeletionRequest(request, transcript) {
|
|
644
824
|
if (!request || typeof request !== "object" || !Array.isArray(request.deletions)) {
|
|
645
825
|
throw new Error("Context deletion request must be an object with a deletions array");
|
|
646
826
|
}
|
|
647
827
|
const entryById = new Map(transcript.entries.map((entry) => [entry.entryId, entry]));
|
|
828
|
+
const recentEntryIds = getRecentContextEntryIds(transcript);
|
|
648
829
|
const seen = new Set();
|
|
649
830
|
const deletedTargets = [];
|
|
650
831
|
for (const deletion of request.deletions) {
|
|
@@ -654,6 +835,7 @@ export function validateContextDeletionRequest(request, transcript, options = {}
|
|
|
654
835
|
if (deletion.kind !== "entry" && deletion.kind !== "content_block") {
|
|
655
836
|
throw new Error(`Unsupported deletion target kind: ${String(deletion.kind)}`);
|
|
656
837
|
}
|
|
838
|
+
assertIdOnlyDeletionTarget(deletion);
|
|
657
839
|
if (typeof deletion.entryId !== "string" || deletion.entryId.length === 0) {
|
|
658
840
|
throw new Error("Deletion target entryId must be a non-empty string");
|
|
659
841
|
}
|
|
@@ -661,19 +843,31 @@ export function validateContextDeletionRequest(request, transcript, options = {}
|
|
|
661
843
|
if (!entry) {
|
|
662
844
|
throw new Error(`Unknown deletion target entryId: ${deletion.entryId}`);
|
|
663
845
|
}
|
|
664
|
-
|
|
665
|
-
|
|
846
|
+
const normalized = normalizeRawTarget(deletion);
|
|
847
|
+
if (deletion.kind === "entry") {
|
|
848
|
+
if (recentEntryIds.has(deletion.entryId)) {
|
|
849
|
+
throw new Error(formatRecentContextDeletionError(transcript, normalized));
|
|
850
|
+
}
|
|
851
|
+
if (entry.protected) {
|
|
852
|
+
throw new Error(formatProtectedDeletionError(transcript, normalized));
|
|
853
|
+
}
|
|
666
854
|
}
|
|
667
855
|
if (deletion.kind === "content_block") {
|
|
668
|
-
if (!Number.isInteger(deletion.blockIndex) || deletion.blockIndex
|
|
856
|
+
if (typeof deletion.blockIndex !== "number" || !Number.isInteger(deletion.blockIndex) || deletion.blockIndex < 0) {
|
|
669
857
|
throw new Error(`Invalid content block index for entry ${deletion.entryId}`);
|
|
670
858
|
}
|
|
859
|
+
if (recentEntryIds.has(deletion.entryId)) {
|
|
860
|
+
throw new Error(formatRecentContextDeletionError(transcript, normalized));
|
|
861
|
+
}
|
|
862
|
+
if (entry.protected) {
|
|
863
|
+
throw new Error(formatProtectedDeletionError(transcript, normalized));
|
|
864
|
+
}
|
|
671
865
|
const block = entry.contentBlocks.find((item) => item.blockIndex === deletion.blockIndex);
|
|
672
866
|
if (!block) {
|
|
673
867
|
throw new Error(`Unknown content block ${deletion.blockIndex} for entry ${deletion.entryId}`);
|
|
674
868
|
}
|
|
675
|
-
if (block.protected
|
|
676
|
-
throw new Error(
|
|
869
|
+
if (block.protected) {
|
|
870
|
+
throw new Error(formatProtectedDeletionError(transcript, normalized));
|
|
677
871
|
}
|
|
678
872
|
if (entry.contentBlocks.length <= 1) {
|
|
679
873
|
throw new Error(`Deleting the only content block of ${deletion.entryId} must be an entry deletion`);
|
|
@@ -684,10 +878,13 @@ export function validateContextDeletionRequest(request, transcript, options = {}
|
|
|
684
878
|
throw new Error(`Duplicate deletion target: ${key}`);
|
|
685
879
|
}
|
|
686
880
|
seen.add(key);
|
|
687
|
-
const normalized = normalizeRawTarget(deletion);
|
|
688
881
|
deletedTargets.push(normalized);
|
|
689
882
|
}
|
|
690
883
|
const reconciledTargets = reconcileToolDependencies(transcript, deletedTargets);
|
|
884
|
+
// Tool reconciliation can add targets after the per-request checks above, so
|
|
885
|
+
// these post-reconcile assertions remain authoritative.
|
|
886
|
+
assertNoRecentContextDeletionTargets(transcript, reconciledTargets);
|
|
887
|
+
assertNoLatestAssistantThinkingDeletionTargets(transcript, reconciledTargets);
|
|
691
888
|
const reconciledDeletedEntryIds = getDeletedEntryIds(reconciledTargets);
|
|
692
889
|
for (const target of reconciledTargets) {
|
|
693
890
|
if (target.kind === "content_block" && reconciledDeletedEntryIds.has(target.entryId)) {
|
|
@@ -716,18 +913,6 @@ export function validateContextDeletionRequest(request, transcript, options = {}
|
|
|
716
913
|
stats: computeContextCompactionStats(transcript, reconciledTargets),
|
|
717
914
|
};
|
|
718
915
|
}
|
|
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
916
|
function contextDeletionRequestFromObject(value, source) {
|
|
732
917
|
if (!value || typeof value !== "object" || !Array.isArray(value.deletions)) {
|
|
733
918
|
throw new Error(`${source} must contain a deletions array`);
|
|
@@ -743,6 +928,69 @@ function formatErrorMessage(error) {
|
|
|
743
928
|
function createContextDeletionToolResult(text, details) {
|
|
744
929
|
return { content: [{ type: "text", text }], details, terminate: false };
|
|
745
930
|
}
|
|
931
|
+
function roundPercent(value) {
|
|
932
|
+
return Math.round(value * 10) / 10;
|
|
933
|
+
}
|
|
934
|
+
function percentOf(part, total) {
|
|
935
|
+
return total > 0 ? roundPercent((part / total) * 100) : 0;
|
|
936
|
+
}
|
|
937
|
+
function finitePositiveNumber(value) {
|
|
938
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
939
|
+
}
|
|
940
|
+
function createContextCompactionBudgetDetails(stats, callCount, contextWindow, parameters) {
|
|
941
|
+
const targetTokensAfter = Math.max(0, Math.floor(stats.tokensBefore * parameters.compression_ratio));
|
|
942
|
+
const targetReductionPercent = contextCompactionTargetReductionPercent(parameters);
|
|
943
|
+
const details = {
|
|
944
|
+
...(contextWindow !== undefined ? { contextWindow } : {}),
|
|
945
|
+
compression_ratio: parameters.compression_ratio,
|
|
946
|
+
tokensBefore: stats.tokensBefore,
|
|
947
|
+
currentTokensAfter: stats.tokensAfter,
|
|
948
|
+
deletedTokens: Math.max(0, stats.tokensBefore - stats.tokensAfter),
|
|
949
|
+
currentReductionPercent: stats.percentReduction,
|
|
950
|
+
targetReductionPercent,
|
|
951
|
+
targetTokensAfter,
|
|
952
|
+
tokensToDeleteForTarget: Math.max(0, stats.tokensAfter - targetTokensAfter),
|
|
953
|
+
...(contextWindow !== undefined
|
|
954
|
+
? {
|
|
955
|
+
contextWindowBeforePercent: percentOf(stats.tokensBefore, contextWindow),
|
|
956
|
+
contextWindowAfterPercent: percentOf(stats.tokensAfter, contextWindow),
|
|
957
|
+
}
|
|
958
|
+
: {}),
|
|
959
|
+
callCount,
|
|
960
|
+
};
|
|
961
|
+
return details;
|
|
962
|
+
}
|
|
963
|
+
function contextCompactionTargetMet(result, parameters) {
|
|
964
|
+
return (result !== undefined &&
|
|
965
|
+
result.deletedTargets.length > 0 &&
|
|
966
|
+
result.stats.percentReduction >= contextCompactionTargetReductionPercent(parameters));
|
|
967
|
+
}
|
|
968
|
+
function contextCompactionProgressKey(result) {
|
|
969
|
+
if (!result)
|
|
970
|
+
return "none:0";
|
|
971
|
+
return `${result.deletedTargets.length}:${result.stats.percentReduction}:${result.stats.tokensAfter}`;
|
|
972
|
+
}
|
|
973
|
+
function contextCompactionProgressPercent(result) {
|
|
974
|
+
return result?.stats.percentReduction ?? 0;
|
|
975
|
+
}
|
|
976
|
+
function createContextCompactionTargetNudgeMessage(result, parameters) {
|
|
977
|
+
const currentReductionPercent = contextCompactionProgressPercent(result);
|
|
978
|
+
const targetLabel = contextCompactionTargetLabel(parameters);
|
|
979
|
+
const tokensToDelete = result
|
|
980
|
+
? createContextCompactionBudgetDetails(result.stats, 0, undefined, parameters).tokensToDeleteForTarget
|
|
981
|
+
: undefined;
|
|
982
|
+
const remainingText = tokensToDelete !== undefined ? ` Delete about ${tokensToDelete} more token(s) if safe candidates exist.` : "";
|
|
983
|
+
return {
|
|
984
|
+
role: "user",
|
|
985
|
+
content: [
|
|
986
|
+
{
|
|
987
|
+
type: "text",
|
|
988
|
+
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}.`,
|
|
989
|
+
},
|
|
990
|
+
],
|
|
991
|
+
timestamp: Date.now(),
|
|
992
|
+
};
|
|
993
|
+
}
|
|
746
994
|
function assertSafeRegexPattern(pattern) {
|
|
747
995
|
if (pattern.length > CONTEXT_GREP_DELETE_MAX_REGEX_PATTERN_CHARS) {
|
|
748
996
|
throw new Error(`Regex pattern is too long (${pattern.length} characters); maximum is ${CONTEXT_GREP_DELETE_MAX_REGEX_PATTERN_CHARS}`);
|
|
@@ -809,6 +1057,69 @@ function addGrepCandidate(candidates, matches, seenTargets, candidate, match) {
|
|
|
809
1057
|
candidates.push(candidate);
|
|
810
1058
|
matches.push(match);
|
|
811
1059
|
}
|
|
1060
|
+
function pushProtectedGrepSkip(skipped, match) {
|
|
1061
|
+
skipped.push({
|
|
1062
|
+
entryId: match.entryId,
|
|
1063
|
+
target: match.target,
|
|
1064
|
+
...(match.blockIndex === undefined ? {} : { blockIndex: match.blockIndex }),
|
|
1065
|
+
reason: match.target === "content_block" ? "protected_block" : "protected_entry",
|
|
1066
|
+
text: match.text,
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
function filterProtectedGrepCandidates(candidates, matches, currentTargets, transcript, skipped) {
|
|
1070
|
+
const eligibleCandidates = [];
|
|
1071
|
+
const eligibleMatches = [];
|
|
1072
|
+
for (let index = 0; index < candidates.length; index++) {
|
|
1073
|
+
const candidate = candidates[index];
|
|
1074
|
+
const match = matches[index];
|
|
1075
|
+
if (!candidate || !match)
|
|
1076
|
+
continue;
|
|
1077
|
+
try {
|
|
1078
|
+
const mergedTargets = mergeContextDeletionTargets(currentTargets, [candidate]);
|
|
1079
|
+
validateContextDeletionRequest(deletionRequestFromTargets(mergedTargets), transcript);
|
|
1080
|
+
eligibleCandidates.push(candidate);
|
|
1081
|
+
eligibleMatches.push(match);
|
|
1082
|
+
}
|
|
1083
|
+
catch (error) {
|
|
1084
|
+
const message = formatErrorMessage(error);
|
|
1085
|
+
if (isProtectedContextDeletionErrorMessage(message)) {
|
|
1086
|
+
pushProtectedGrepSkip(skipped, match);
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
eligibleCandidates.push(candidate);
|
|
1090
|
+
eligibleMatches.push(match);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
// Some latest-assistant thinking violations only become visible after a grep batch also
|
|
1094
|
+
// deletes newer assistant entries. Classify the newly-unsafe grep candidates as
|
|
1095
|
+
// protected/skipped before maxMatches, expectedMatchCount, stats, or removals are computed.
|
|
1096
|
+
let changed = true;
|
|
1097
|
+
while (changed) {
|
|
1098
|
+
changed = false;
|
|
1099
|
+
const mergedTargets = mergeContextDeletionTargets(currentTargets, eligibleCandidates);
|
|
1100
|
+
const violation = findLatestAssistantThinkingDeletionViolation(transcript, mergedTargets);
|
|
1101
|
+
if (!violation)
|
|
1102
|
+
continue;
|
|
1103
|
+
const violationKey = targetKey(violation);
|
|
1104
|
+
let violationIndex = eligibleCandidates.findIndex((candidate) => targetKey(candidate) === violationKey);
|
|
1105
|
+
if (violationIndex < 0) {
|
|
1106
|
+
violationIndex = eligibleCandidates.findIndex((_candidate, candidateIndex) => {
|
|
1107
|
+
const remainingCandidates = eligibleCandidates.filter((_candidateToKeep, index) => index !== candidateIndex);
|
|
1108
|
+
const remainingTargets = mergeContextDeletionTargets(currentTargets, remainingCandidates);
|
|
1109
|
+
const remainingViolation = findLatestAssistantThinkingDeletionViolation(transcript, remainingTargets);
|
|
1110
|
+
return !remainingViolation || targetKey(remainingViolation) !== violationKey;
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
if (violationIndex < 0)
|
|
1114
|
+
continue;
|
|
1115
|
+
const [skippedMatch] = eligibleMatches.splice(violationIndex, 1);
|
|
1116
|
+
eligibleCandidates.splice(violationIndex, 1);
|
|
1117
|
+
if (skippedMatch)
|
|
1118
|
+
pushProtectedGrepSkip(skipped, skippedMatch);
|
|
1119
|
+
changed = true;
|
|
1120
|
+
}
|
|
1121
|
+
return { candidates: eligibleCandidates, matches: eligibleMatches };
|
|
1122
|
+
}
|
|
812
1123
|
function copyDeletionTarget(target) {
|
|
813
1124
|
return target.kind === "entry"
|
|
814
1125
|
? { kind: "entry", entryId: target.entryId }
|
|
@@ -829,30 +1140,36 @@ class ContextDeletionMemoryStore {
|
|
|
829
1140
|
entryId: entry.entryId,
|
|
830
1141
|
role: entry.role,
|
|
831
1142
|
protected: entry.protected,
|
|
1143
|
+
hasAssistantThinkingBlocks: assistantEntryHasThinkingContentBlock(entry),
|
|
832
1144
|
tokenEstimate: entry.tokenEstimate,
|
|
833
1145
|
text: entry.text,
|
|
834
1146
|
};
|
|
835
1147
|
});
|
|
836
1148
|
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
|
-
|
|
1149
|
+
this.contentBlocks = transcript.entries.flatMap((entry, entryPosition) => {
|
|
1150
|
+
const hasAssistantThinkingBlocks = assistantEntryHasThinkingContentBlock(entry);
|
|
1151
|
+
return entry.contentBlocks.map((block) => {
|
|
1152
|
+
if (block.entryId !== entry.entryId) {
|
|
1153
|
+
throw new Error(`Transcript content block ${block.entryId}:${block.blockIndex} does not belong to entry ${entry.entryId}`);
|
|
1154
|
+
}
|
|
1155
|
+
const blockKey = `${block.entryId}:${block.blockIndex}`;
|
|
1156
|
+
if (blockKeys.has(blockKey)) {
|
|
1157
|
+
throw new Error(`Duplicate transcript content block: ${blockKey}`);
|
|
1158
|
+
}
|
|
1159
|
+
blockKeys.add(blockKey);
|
|
1160
|
+
return {
|
|
1161
|
+
entryPosition,
|
|
1162
|
+
entryId: block.entryId,
|
|
1163
|
+
blockIndex: block.blockIndex,
|
|
1164
|
+
role: entry.role,
|
|
1165
|
+
type: block.type,
|
|
1166
|
+
protected: block.protected,
|
|
1167
|
+
hasAssistantThinkingBlocks,
|
|
1168
|
+
tokenEstimate: block.tokenEstimate,
|
|
1169
|
+
text: block.text,
|
|
1170
|
+
};
|
|
1171
|
+
});
|
|
1172
|
+
});
|
|
856
1173
|
this.contentBlockCountByEntryId = new Map();
|
|
857
1174
|
for (const block of this.contentBlocks) {
|
|
858
1175
|
this.contentBlockCountByEntryId.set(block.entryId, (this.contentBlockCountByEntryId.get(block.entryId) ?? 0) + 1);
|
|
@@ -879,6 +1196,7 @@ class ContextDeletionMemoryStore {
|
|
|
879
1196
|
entry_id: entry.entryId,
|
|
880
1197
|
text: entry.text,
|
|
881
1198
|
is_protected: entry.protected ? 1 : 0,
|
|
1199
|
+
has_assistant_thinking_blocks: entry.hasAssistantThinkingBlocks ? 1 : 0,
|
|
882
1200
|
}));
|
|
883
1201
|
}
|
|
884
1202
|
listContentBlocksForGrep() {
|
|
@@ -887,10 +1205,13 @@ class ContextDeletionMemoryStore {
|
|
|
887
1205
|
.map((block) => ({
|
|
888
1206
|
entry_id: block.entryId,
|
|
889
1207
|
block_index: block.blockIndex,
|
|
1208
|
+
role: block.role,
|
|
1209
|
+
type: block.type,
|
|
890
1210
|
text: block.text,
|
|
891
1211
|
entry_protected: this.entriesById.get(block.entryId)?.protected ? 1 : 0,
|
|
892
1212
|
block_protected: block.protected ? 1 : 0,
|
|
893
1213
|
block_count: this.contentBlockCountByEntryId.get(block.entryId) ?? 0,
|
|
1214
|
+
has_assistant_thinking_blocks: block.hasAssistantThinkingBlocks ? 1 : 0,
|
|
894
1215
|
}));
|
|
895
1216
|
}
|
|
896
1217
|
getEntryForRead(entryId) {
|
|
@@ -901,6 +1222,7 @@ class ContextDeletionMemoryStore {
|
|
|
901
1222
|
entry_id: entry.entryId,
|
|
902
1223
|
role: entry.role,
|
|
903
1224
|
is_protected: entry.protected ? 1 : 0,
|
|
1225
|
+
has_assistant_thinking_blocks: entry.hasAssistantThinkingBlocks ? 1 : 0,
|
|
904
1226
|
token_estimate: entry.tokenEstimate,
|
|
905
1227
|
text: entry.text,
|
|
906
1228
|
};
|
|
@@ -912,12 +1234,14 @@ class ContextDeletionMemoryStore {
|
|
|
912
1234
|
return {
|
|
913
1235
|
entry_id: block.entryId,
|
|
914
1236
|
block_index: block.blockIndex,
|
|
1237
|
+
role: block.role,
|
|
915
1238
|
type: block.type,
|
|
916
1239
|
token_estimate: block.tokenEstimate,
|
|
917
1240
|
text: block.text,
|
|
918
1241
|
entry_protected: this.entriesById.get(block.entryId)?.protected ? 1 : 0,
|
|
919
1242
|
block_protected: block.protected ? 1 : 0,
|
|
920
1243
|
block_count: this.contentBlockCountByEntryId.get(block.entryId) ?? 0,
|
|
1244
|
+
has_assistant_thinking_blocks: block.hasAssistantThinkingBlocks ? 1 : 0,
|
|
921
1245
|
};
|
|
922
1246
|
}
|
|
923
1247
|
getGrepScanTextLength(target) {
|
|
@@ -956,8 +1280,10 @@ class ContextDeletionMemoryStore {
|
|
|
956
1280
|
function createContextDeletionStore(transcript) {
|
|
957
1281
|
return new ContextDeletionMemoryStore(transcript);
|
|
958
1282
|
}
|
|
959
|
-
export function createContextDeletionTool(
|
|
960
|
-
const
|
|
1283
|
+
export function createContextDeletionTool(inputTranscript, options = {}) {
|
|
1284
|
+
const contextWindow = finitePositiveNumber(options.contextWindow);
|
|
1285
|
+
const parameters = normalizeContextCompactionParameters({ ...getTranscriptCompactionParameters(inputTranscript), ...options }, inputTranscript.parameters?.query ?? CONTEXT_COMPACTION_AUTO_QUERY);
|
|
1286
|
+
const transcript = { ...inputTranscript, parameters };
|
|
961
1287
|
const store = createContextDeletionStore(transcript);
|
|
962
1288
|
let validatedResult;
|
|
963
1289
|
function readTargets() {
|
|
@@ -965,7 +1291,7 @@ export function createContextDeletionTool(transcript, options = {}) {
|
|
|
965
1291
|
}
|
|
966
1292
|
function applyValidatedTargets(additionalTargets) {
|
|
967
1293
|
const mergedTargets = mergeContextDeletionTargets(readTargets(), additionalTargets);
|
|
968
|
-
validatedResult = validateContextDeletionRequest(deletionRequestFromTargets(mergedTargets), transcript
|
|
1294
|
+
validatedResult = validateContextDeletionRequest(deletionRequestFromTargets(mergedTargets), transcript);
|
|
969
1295
|
store.replaceTargets(validatedResult.deletedTargets);
|
|
970
1296
|
return validatedResult;
|
|
971
1297
|
}
|
|
@@ -973,7 +1299,7 @@ export function createContextDeletionTool(transcript, options = {}) {
|
|
|
973
1299
|
return validatedResult?.stats ?? computeContextCompactionStats(transcript, readTargets());
|
|
974
1300
|
}
|
|
975
1301
|
function canDeleteProtectedTarget(target) {
|
|
976
|
-
return
|
|
1302
|
+
return canDeleteTarget(transcript, target);
|
|
977
1303
|
}
|
|
978
1304
|
const tool = {
|
|
979
1305
|
...CONTEXT_DELETE_TOOL,
|
|
@@ -984,7 +1310,7 @@ export function createContextDeletionTool(transcript, options = {}) {
|
|
|
984
1310
|
const callCount = store.incrementCallCount();
|
|
985
1311
|
try {
|
|
986
1312
|
const incomingRequest = contextDeletionRequestFromObject(params, `${CONTEXT_DELETE_TOOL_NAME} arguments`);
|
|
987
|
-
const incomingValidated = validateContextDeletionRequest(incomingRequest, transcript
|
|
1313
|
+
const incomingValidated = validateContextDeletionRequest(incomingRequest, transcript);
|
|
988
1314
|
const applied = applyValidatedTargets(incomingValidated.deletedTargets);
|
|
989
1315
|
store.clearLastError();
|
|
990
1316
|
const deletedTargets = readTargets();
|
|
@@ -1027,6 +1353,7 @@ export function createContextDeletionTool(transcript, options = {}) {
|
|
|
1027
1353
|
const maxMatches = params.maxMatches ?? CONTEXT_GREP_DELETE_DEFAULT_MAX_MATCHES;
|
|
1028
1354
|
const candidates = [];
|
|
1029
1355
|
const matches = [];
|
|
1356
|
+
let reportedMatches = matches;
|
|
1030
1357
|
const skipped = [];
|
|
1031
1358
|
const seenTargets = new Set();
|
|
1032
1359
|
try {
|
|
@@ -1035,11 +1362,16 @@ export function createContextDeletionTool(transcript, options = {}) {
|
|
|
1035
1362
|
}
|
|
1036
1363
|
const matcher = createGrepMatcher(pattern, regex, caseSensitive);
|
|
1037
1364
|
const currentTargets = readTargets();
|
|
1365
|
+
const recentEntryIds = getRecentContextEntryIds(transcript);
|
|
1038
1366
|
if (target === "entry") {
|
|
1039
1367
|
for (const entry of store.listEntriesForGrep()) {
|
|
1040
1368
|
if (!matcher.test(entry.text))
|
|
1041
1369
|
continue;
|
|
1042
1370
|
const candidate = { kind: "entry", entryId: entry.entry_id };
|
|
1371
|
+
if (recentEntryIds.has(candidate.entryId)) {
|
|
1372
|
+
skipped.push({ entryId: entry.entry_id, target, reason: "protected_entry", text: entry.text });
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1043
1375
|
if (entry.is_protected === 1 && !canDeleteProtectedTarget(candidate)) {
|
|
1044
1376
|
skipped.push({ entryId: entry.entry_id, target, reason: "protected_entry", text: entry.text });
|
|
1045
1377
|
continue;
|
|
@@ -1062,6 +1394,16 @@ export function createContextDeletionTool(transcript, options = {}) {
|
|
|
1062
1394
|
const candidate = block.block_count <= 1
|
|
1063
1395
|
? { kind: "entry", entryId: block.entry_id }
|
|
1064
1396
|
: { kind: "content_block", entryId: block.entry_id, blockIndex: block.block_index };
|
|
1397
|
+
if (recentEntryIds.has(candidate.entryId)) {
|
|
1398
|
+
skipped.push({
|
|
1399
|
+
entryId: block.entry_id,
|
|
1400
|
+
target: candidate.kind,
|
|
1401
|
+
...(candidate.kind === "content_block" ? { blockIndex: candidate.blockIndex } : {}),
|
|
1402
|
+
reason: "protected_entry",
|
|
1403
|
+
text: block.text,
|
|
1404
|
+
});
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1065
1407
|
if (block.entry_protected === 1 && !canDeleteProtectedTarget(candidate)) {
|
|
1066
1408
|
skipped.push({
|
|
1067
1409
|
entryId: block.entry_id,
|
|
@@ -1100,15 +1442,17 @@ export function createContextDeletionTool(transcript, options = {}) {
|
|
|
1100
1442
|
});
|
|
1101
1443
|
}
|
|
1102
1444
|
}
|
|
1445
|
+
const eligible = filterProtectedGrepCandidates(candidates, matches, currentTargets, transcript, skipped);
|
|
1446
|
+
reportedMatches = eligible.matches;
|
|
1103
1447
|
let applied;
|
|
1104
|
-
if (params.expectedMatchCount !== undefined && candidates.length !== params.expectedMatchCount) {
|
|
1448
|
+
if (params.expectedMatchCount !== undefined && eligible.candidates.length !== params.expectedMatchCount) {
|
|
1105
1449
|
skipped.push({ reason: "expected_match_count_mismatch" });
|
|
1106
1450
|
}
|
|
1107
|
-
else if (candidates.length > maxMatches) {
|
|
1451
|
+
else if (eligible.candidates.length > maxMatches) {
|
|
1108
1452
|
skipped.push({ reason: "max_matches_exceeded" });
|
|
1109
1453
|
}
|
|
1110
|
-
else if (candidates.length > 0) {
|
|
1111
|
-
applied = applyValidatedTargets(candidates);
|
|
1454
|
+
else if (eligible.candidates.length > 0) {
|
|
1455
|
+
applied = applyValidatedTargets(eligible.candidates);
|
|
1112
1456
|
}
|
|
1113
1457
|
store.clearLastError();
|
|
1114
1458
|
const deletedTargets = readTargets();
|
|
@@ -1117,13 +1461,13 @@ export function createContextDeletionTool(transcript, options = {}) {
|
|
|
1117
1461
|
regex,
|
|
1118
1462
|
caseSensitive,
|
|
1119
1463
|
target,
|
|
1120
|
-
matches,
|
|
1464
|
+
matches: eligible.matches,
|
|
1121
1465
|
skipped,
|
|
1122
1466
|
deletedTargets,
|
|
1123
1467
|
stats: applied?.stats ?? currentStats(),
|
|
1124
1468
|
callCount,
|
|
1125
1469
|
};
|
|
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}.`;
|
|
1470
|
+
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
1471
|
return createContextDeletionToolResult(text, details);
|
|
1128
1472
|
}
|
|
1129
1473
|
catch (error) {
|
|
@@ -1135,7 +1479,7 @@ export function createContextDeletionTool(transcript, options = {}) {
|
|
|
1135
1479
|
regex,
|
|
1136
1480
|
caseSensitive,
|
|
1137
1481
|
target,
|
|
1138
|
-
matches,
|
|
1482
|
+
matches: reportedMatches,
|
|
1139
1483
|
skipped,
|
|
1140
1484
|
deletedTargets,
|
|
1141
1485
|
stats: currentStats(),
|
|
@@ -1290,53 +1634,38 @@ export function createContextDeletionTool(transcript, options = {}) {
|
|
|
1290
1634
|
});
|
|
1291
1635
|
},
|
|
1292
1636
|
};
|
|
1637
|
+
const budgetTool = {
|
|
1638
|
+
...CONTEXT_COMPACTION_BUDGET_TOOL,
|
|
1639
|
+
label: "context compaction budget",
|
|
1640
|
+
executionMode: "parallel",
|
|
1641
|
+
async execute(_toolCallId) {
|
|
1642
|
+
return store.transaction(() => {
|
|
1643
|
+
const callCount = store.incrementCallCount();
|
|
1644
|
+
store.clearLastError();
|
|
1645
|
+
const details = createContextCompactionBudgetDetails(currentStats(), callCount, contextWindow, parameters);
|
|
1646
|
+
const windowText = details.contextWindowBeforePercent !== undefined
|
|
1647
|
+
? ` Context window fullness: ${details.contextWindowBeforePercent}% before selected deletions, ${details.contextWindowAfterPercent}% after selected deletions.`
|
|
1648
|
+
: " Context window size is unknown for this model, so fullness percentages are unavailable.";
|
|
1649
|
+
const targetText = details.tokensToDeleteForTarget > 0
|
|
1650
|
+
? ` Delete about ${details.tokensToDeleteForTarget} more token(s) to reach the ${details.targetReductionPercent}% reduction target.`
|
|
1651
|
+
: ` The selected deletions meet or exceed the ${details.targetReductionPercent}% reduction target.`;
|
|
1652
|
+
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);
|
|
1653
|
+
});
|
|
1654
|
+
},
|
|
1655
|
+
};
|
|
1293
1656
|
return {
|
|
1294
1657
|
tool,
|
|
1295
1658
|
grepTool,
|
|
1296
1659
|
searchTool,
|
|
1297
1660
|
readEntryTool,
|
|
1298
|
-
|
|
1661
|
+
budgetTool,
|
|
1662
|
+
tools: [tool, grepTool, searchTool, readEntryTool, budgetTool],
|
|
1299
1663
|
getDeletionRequest: () => deletionRequestFromTargets(readTargets()),
|
|
1300
1664
|
getValidatedResult: () => validatedResult,
|
|
1301
1665
|
getLastError: () => store.getLastError(),
|
|
1302
1666
|
getCallCount: () => store.getCallCount(),
|
|
1303
1667
|
};
|
|
1304
1668
|
}
|
|
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
1669
|
function truncateForPrompt(text, maxChars) {
|
|
1341
1670
|
if (text.length <= maxChars)
|
|
1342
1671
|
return text;
|
|
@@ -1420,14 +1749,16 @@ function contextCompactionTranscriptManifest(transcript, transcriptFilePath) {
|
|
|
1420
1749
|
})),
|
|
1421
1750
|
};
|
|
1422
1751
|
}
|
|
1423
|
-
function
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1752
|
+
function contextCompactionParametersPrompt(parameters) {
|
|
1753
|
+
return `\n<compaction-parameters>\n${JSON.stringify({
|
|
1754
|
+
compression_ratio: parameters.compression_ratio,
|
|
1755
|
+
preserve_recent: parameters.preserve_recent,
|
|
1756
|
+
query: parameters.query,
|
|
1757
|
+
target_reduction_percent: contextCompactionTargetReductionPercent(parameters),
|
|
1758
|
+
}, null, 2)}\n</compaction-parameters>`;
|
|
1428
1759
|
}
|
|
1429
|
-
export function buildContextCompactionPrompt(transcript, transcriptFilePath = "<transcript file will be written during context compaction>",
|
|
1430
|
-
return `${
|
|
1760
|
+
export function buildContextCompactionPrompt(transcript, transcriptFilePath = "<transcript file will be written during context compaction>", parameters = getTranscriptCompactionParameters(transcript)) {
|
|
1761
|
+
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
1762
|
}
|
|
1432
1763
|
function createContextCompactionAssistantMessage(model, content, stopReason, errorMessage) {
|
|
1433
1764
|
return {
|
|
@@ -1461,7 +1792,8 @@ function createContextCompactionStopStream(model, text) {
|
|
|
1461
1792
|
function isContextCompactionOverflowError(model, errorMessage) {
|
|
1462
1793
|
return isContextOverflow(createContextCompactionAssistantMessage(model, [], "error", errorMessage), model.contextWindow);
|
|
1463
1794
|
}
|
|
1464
|
-
async function runContextDeletionAssistant(
|
|
1795
|
+
async function runContextDeletionAssistant(inputTranscript, model, apiKey, headers, signal, thinkingLevel = "off", parameters = getTranscriptCompactionParameters(inputTranscript)) {
|
|
1796
|
+
const transcript = { ...inputTranscript, parameters };
|
|
1465
1797
|
const maxTokens = model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY;
|
|
1466
1798
|
if (signal?.aborted) {
|
|
1467
1799
|
throw new Error("Context compaction failed: Request was aborted");
|
|
@@ -1469,11 +1801,10 @@ async function runContextDeletionAssistant(transcript, model, apiKey, headers, s
|
|
|
1469
1801
|
const transcriptFile = writeContextCompactionTranscriptFile(transcript);
|
|
1470
1802
|
const promptMessage = {
|
|
1471
1803
|
role: "user",
|
|
1472
|
-
content: [{ type: "text", text: buildContextCompactionPrompt(transcript, transcriptFile.path,
|
|
1804
|
+
content: [{ type: "text", text: buildContextCompactionPrompt(transcript, transcriptFile.path, parameters) }],
|
|
1473
1805
|
timestamp: Date.now(),
|
|
1474
1806
|
};
|
|
1475
|
-
const deletionTool = createContextDeletionTool(transcript, {
|
|
1476
|
-
let compactionTurnCount = 0;
|
|
1807
|
+
const deletionTool = createContextDeletionTool(transcript, { contextWindow: model.contextWindow, ...parameters });
|
|
1477
1808
|
const agent = new Agent({
|
|
1478
1809
|
initialState: {
|
|
1479
1810
|
systemPrompt: CONTEXT_COMPACTION_SYSTEM_PROMPT,
|
|
@@ -1483,9 +1814,9 @@ async function runContextDeletionAssistant(transcript, model, apiKey, headers, s
|
|
|
1483
1814
|
},
|
|
1484
1815
|
toolExecution: "parallel",
|
|
1485
1816
|
streamFn: async (requestModel, context, streamOptions) => {
|
|
1486
|
-
|
|
1487
|
-
if (
|
|
1488
|
-
return createContextCompactionStopStream(requestModel, `Reached the
|
|
1817
|
+
const currentResult = deletionTool.getValidatedResult();
|
|
1818
|
+
if (contextCompactionTargetMet(currentResult, parameters)) {
|
|
1819
|
+
return createContextCompactionStopStream(requestModel, `Reached the strict ${contextCompactionTargetLabel(parameters)} context-reduction requirement (${currentResult.stats.percentReduction}%); using the validated deletions recorded so far.`);
|
|
1489
1820
|
}
|
|
1490
1821
|
return streamSimple(requestModel, context, {
|
|
1491
1822
|
...streamOptions,
|
|
@@ -1495,6 +1826,25 @@ async function runContextDeletionAssistant(transcript, model, apiKey, headers, s
|
|
|
1495
1826
|
});
|
|
1496
1827
|
},
|
|
1497
1828
|
});
|
|
1829
|
+
let lastNudgedProgressKey;
|
|
1830
|
+
const unsubscribeNudge = agent.subscribe((event, eventSignal) => {
|
|
1831
|
+
if (event.type !== "turn_end" || signal?.aborted || eventSignal.aborted)
|
|
1832
|
+
return;
|
|
1833
|
+
if (event.message.role !== "assistant")
|
|
1834
|
+
return;
|
|
1835
|
+
if (event.message.stopReason === "error" || event.message.stopReason === "aborted")
|
|
1836
|
+
return;
|
|
1837
|
+
if (event.message.content.some((content) => content.type === "toolCall"))
|
|
1838
|
+
return;
|
|
1839
|
+
const currentResult = deletionTool.getValidatedResult();
|
|
1840
|
+
if (contextCompactionTargetMet(currentResult, parameters))
|
|
1841
|
+
return;
|
|
1842
|
+
const progressKey = contextCompactionProgressKey(currentResult);
|
|
1843
|
+
if (progressKey === lastNudgedProgressKey)
|
|
1844
|
+
return;
|
|
1845
|
+
lastNudgedProgressKey = progressKey;
|
|
1846
|
+
agent.followUp(createContextCompactionTargetNudgeMessage(currentResult, parameters));
|
|
1847
|
+
});
|
|
1498
1848
|
const abortOnSignal = () => agent.abort();
|
|
1499
1849
|
signal?.addEventListener("abort", abortOnSignal, { once: true });
|
|
1500
1850
|
try {
|
|
@@ -1502,6 +1852,7 @@ async function runContextDeletionAssistant(transcript, model, apiKey, headers, s
|
|
|
1502
1852
|
}
|
|
1503
1853
|
finally {
|
|
1504
1854
|
signal?.removeEventListener("abort", abortOnSignal);
|
|
1855
|
+
unsubscribeNudge();
|
|
1505
1856
|
transcriptFile.cleanup();
|
|
1506
1857
|
}
|
|
1507
1858
|
if (signal?.aborted) {
|
|
@@ -1517,18 +1868,39 @@ async function runContextDeletionAssistant(transcript, model, apiKey, headers, s
|
|
|
1517
1868
|
throw new Error(`Context compaction failed: ${agent.state.errorMessage}`);
|
|
1518
1869
|
}
|
|
1519
1870
|
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})`);
|
|
1871
|
+
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
1872
|
}
|
|
1522
1873
|
return {
|
|
1523
1874
|
validatedResult: deletionTool.getValidatedResult(),
|
|
1524
1875
|
lastToolError: deletionTool.getLastError(),
|
|
1525
1876
|
};
|
|
1526
1877
|
}
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1878
|
+
function hasMetContextCompactionTarget(run, parameters) {
|
|
1879
|
+
return contextCompactionTargetMet(run.validatedResult, parameters);
|
|
1880
|
+
}
|
|
1881
|
+
function formatContextCompactionTargetFailureMessage(attempts, parameters) {
|
|
1882
|
+
const targetLabel = contextCompactionTargetLabel(parameters);
|
|
1883
|
+
if (attempts.length === 0) {
|
|
1884
|
+
return `Context compaction did not meet the strict ${targetLabel} reduction requirement`;
|
|
1531
1885
|
}
|
|
1532
|
-
|
|
1886
|
+
const attemptDetails = attempts
|
|
1887
|
+
.map((attempt) => {
|
|
1888
|
+
const reduction = contextCompactionProgressPercent(attempt.validatedResult);
|
|
1889
|
+
const deletionCount = attempt.validatedResult?.deletedTargets.length ?? 0;
|
|
1890
|
+
const errorText = attempt.lastToolError ? `; last deletion tool error: ${attempt.lastToolError}` : "";
|
|
1891
|
+
return `attempt reached ${reduction}% with ${deletionCount} validated deletion target(s)${errorText}`;
|
|
1892
|
+
})
|
|
1893
|
+
.join("; ");
|
|
1894
|
+
return `Context compaction did not meet the strict ${targetLabel} reduction requirement; ${attemptDetails}`;
|
|
1895
|
+
}
|
|
1896
|
+
export async function contextCompact(preparation, model, apiKey, headers, signal, thinkingLevel = "off") {
|
|
1897
|
+
const parameters = normalizeContextCompactionParameters(preparation.parameters ?? preparation.transcript.parameters, preparation.parameters?.query ?? preparation.transcript.parameters?.query ?? CONTEXT_COMPACTION_AUTO_QUERY);
|
|
1898
|
+
const transcript = { ...preparation.transcript, parameters };
|
|
1899
|
+
const attempts = [];
|
|
1900
|
+
const standardRun = await runContextDeletionAssistant(transcript, model, apiKey, headers, signal, thinkingLevel, parameters);
|
|
1901
|
+
if (hasMetContextCompactionTarget(standardRun, parameters))
|
|
1902
|
+
return standardRun.validatedResult;
|
|
1903
|
+
attempts.push({ ...standardRun });
|
|
1904
|
+
throw new Error(formatContextCompactionTargetFailureMessage(attempts, parameters));
|
|
1533
1905
|
}
|
|
1534
1906
|
//# sourceMappingURL=context-compaction.js.map
|