@bastani/atomic 0.8.26-alpha.1 → 0.8.26-alpha.11
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 +79 -0
- package/README.md +5 -5
- package/dist/builtin/intercom/CHANGELOG.md +60 -0
- package/dist/builtin/intercom/package.json +2 -2
- package/dist/builtin/mcp/CHANGELOG.md +60 -0
- package/dist/builtin/mcp/package.json +3 -3
- package/dist/builtin/subagents/CHANGELOG.md +61 -0
- package/dist/builtin/subagents/agents/codebase-analyzer.md +1 -1
- package/dist/builtin/subagents/agents/codebase-locator.md +1 -1
- package/dist/builtin/subagents/agents/codebase-online-researcher.md +9 -9
- package/dist/builtin/subagents/agents/codebase-pattern-finder.md +1 -1
- package/dist/builtin/subagents/agents/codebase-research-analyzer.md +1 -1
- package/dist/builtin/subagents/agents/codebase-research-locator.md +1 -1
- package/dist/builtin/subagents/agents/debugger.md +6 -6
- package/dist/builtin/subagents/package.json +4 -4
- package/dist/builtin/subagents/prompts/parallel-handoff-plan.md +1 -1
- package/dist/builtin/subagents/skills/browser/EXAMPLES.md +151 -0
- package/dist/builtin/subagents/skills/browser/LICENSE.txt +21 -0
- package/dist/builtin/subagents/skills/browser/REFERENCE.md +451 -0
- package/dist/builtin/subagents/skills/browser/SKILL.md +170 -0
- package/dist/builtin/subagents/skills/subagent/SKILL.md +4 -4
- package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +55 -12
- package/dist/builtin/subagents/src/runs/foreground/execution.ts +71 -12
- package/dist/builtin/subagents/src/runs/shared/acceptance.ts +2 -1
- package/dist/builtin/subagents/src/runs/shared/final-drain.ts +34 -0
- package/dist/builtin/subagents/src/runs/shared/model-fallback.ts +416 -7
- package/dist/builtin/subagents/src/runs/shared/worktree.ts +2 -2
- package/dist/builtin/web-access/CHANGELOG.md +60 -0
- package/dist/builtin/web-access/package.json +2 -2
- package/dist/builtin/workflows/CHANGELOG.md +72 -0
- package/dist/builtin/workflows/README.md +10 -8
- package/dist/builtin/workflows/builtin/deep-research-codebase.ts +11 -8
- package/dist/builtin/workflows/builtin/goal.ts +137 -109
- package/dist/builtin/workflows/builtin/index.d.ts +2 -0
- package/dist/builtin/workflows/builtin/open-claude-design.ts +228 -151
- package/dist/builtin/workflows/builtin/ralph.d.ts +2 -0
- package/dist/builtin/workflows/builtin/ralph.ts +452 -279
- package/dist/builtin/workflows/package.json +2 -2
- package/dist/builtin/workflows/skills/create-spec/SKILL.md +14 -0
- package/dist/builtin/workflows/skills/research-codebase/SKILL.md +29 -10
- package/dist/builtin/workflows/src/extension/index.ts +10 -2
- package/dist/builtin/workflows/src/extension/runtime.ts +35 -3
- package/dist/builtin/workflows/src/extension/wiring.ts +13 -1
- package/dist/builtin/workflows/src/runs/background/status.ts +52 -6
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +453 -21
- package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +77 -11
- package/dist/builtin/workflows/src/runs/shared/model-fallback.ts +402 -8
- package/dist/builtin/workflows/src/runs/shared/worktree.ts +2 -2
- package/dist/builtin/workflows/src/shared/authoring-contract.d.ts +2 -2
- package/dist/builtin/workflows/src/shared/persistence-restore.ts +182 -6
- package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +76 -6
- package/dist/builtin/workflows/src/shared/stage-prompt.ts +33 -2
- package/dist/builtin/workflows/src/shared/store-types.ts +31 -0
- package/dist/builtin/workflows/src/shared/store.ts +160 -18
- package/dist/builtin/workflows/src/shared/types.ts +3 -3
- package/dist/builtin/workflows/src/shared/workflow-failures.ts +758 -132
- package/dist/builtin/workflows/src/tui/stage-chat-view.ts +39 -3
- package/dist/builtin/workflows/src/tui/store-widget-installer.ts +74 -74
- package/dist/core/agent-session.d.ts +33 -6
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +157 -182
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/atomic-guide-command.d.ts.map +1 -1
- package/dist/core/atomic-guide-command.js +11 -9
- package/dist/core/atomic-guide-command.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts +1 -1
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +6 -3
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +23 -10
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/compaction/context-compaction.d.ts +175 -0
- package/dist/core/compaction/context-compaction.d.ts.map +1 -0
- package/dist/core/compaction/context-compaction.js +1636 -0
- package/dist/core/compaction/context-compaction.js.map +1 -0
- package/dist/core/compaction/index.d.ts +1 -0
- package/dist/core/compaction/index.d.ts.map +1 -1
- package/dist/core/compaction/index.js +1 -0
- package/dist/core/compaction/index.js.map +1 -1
- package/dist/core/extensions/types.d.ts +3 -2
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +3 -0
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +14 -7
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/session-manager.d.ts +41 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +146 -7
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/tools/ask-user-question/tool/format-answer.d.ts +5 -5
- package/dist/core/tools/ask-user-question/tool/format-answer.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/tool/format-answer.js +5 -5
- package/dist/core/tools/ask-user-question/tool/format-answer.js.map +1 -1
- package/dist/core/tools/ask-user-question/tool/response-envelope.d.ts +16 -3
- package/dist/core/tools/ask-user-question/tool/response-envelope.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/tool/response-envelope.js +21 -3
- package/dist/core/tools/ask-user-question/tool/response-envelope.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/modes/index.d.ts +1 -1
- package/dist/modes/index.d.ts.map +1 -1
- package/dist/modes/index.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host.js +17 -0
- package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
- package/dist/modes/interactive/components/context-compaction-summary-message.d.ts +17 -0
- package/dist/modes/interactive/components/context-compaction-summary-message.d.ts.map +1 -0
- package/dist/modes/interactive/components/context-compaction-summary-message.js +83 -0
- package/dist/modes/interactive/components/context-compaction-summary-message.js.map +1 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +4 -1
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +1 -0
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +1 -0
- package/dist/modes/interactive/components/index.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 +75 -10
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +13 -8
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +8 -1
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +4 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +14 -3
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/utils/git-env.d.ts +10 -0
- package/dist/utils/git-env.d.ts.map +1 -0
- package/dist/utils/git-env.js +33 -0
- package/dist/utils/git-env.js.map +1 -0
- package/docs/compaction.md +185 -50
- package/docs/custom-provider.md +11 -9
- package/docs/extensions.md +46 -42
- package/docs/index.md +13 -6
- package/docs/json.md +15 -12
- package/docs/packages.md +2 -0
- package/docs/providers.md +4 -1
- package/docs/quickstart.md +18 -11
- package/docs/rpc.md +38 -23
- package/docs/sdk.md +17 -8
- package/docs/session-format.md +26 -13
- package/docs/sessions.md +3 -3
- package/docs/settings.md +2 -2
- package/docs/skills.md +1 -15
- package/docs/termux.md +9 -10
- package/docs/themes.md +2 -2
- package/docs/tmux.md +3 -3
- package/docs/tui.md +19 -32
- package/docs/usage.md +2 -2
- package/docs/workflows.md +60 -16
- package/package.json +6 -12
- package/dist/builtin/subagents/skills/browser-use/SKILL.md +0 -234
- package/dist/builtin/subagents/skills/browser-use/references/cdp-python.md +0 -76
- package/dist/builtin/subagents/skills/browser-use/references/multi-session.md +0 -92
- package/node_modules/@earendil-works/pi-tui/README.md +0 -779
- package/node_modules/@earendil-works/pi-tui/dist/autocomplete.d.ts +0 -54
- package/node_modules/@earendil-works/pi-tui/dist/autocomplete.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/autocomplete.js +0 -632
- package/node_modules/@earendil-works/pi-tui/dist/autocomplete.js.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/components/box.d.ts +0 -22
- package/node_modules/@earendil-works/pi-tui/dist/components/box.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/components/box.js +0 -104
- package/node_modules/@earendil-works/pi-tui/dist/components/box.js.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/components/cancellable-loader.d.ts +0 -22
- package/node_modules/@earendil-works/pi-tui/dist/components/cancellable-loader.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/components/cancellable-loader.js +0 -35
- package/node_modules/@earendil-works/pi-tui/dist/components/cancellable-loader.js.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/components/editor.d.ts +0 -249
- package/node_modules/@earendil-works/pi-tui/dist/components/editor.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/components/editor.js +0 -1857
- package/node_modules/@earendil-works/pi-tui/dist/components/editor.js.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/components/image.d.ts +0 -28
- package/node_modules/@earendil-works/pi-tui/dist/components/image.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/components/image.js +0 -89
- package/node_modules/@earendil-works/pi-tui/dist/components/image.js.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/components/input.d.ts +0 -37
- package/node_modules/@earendil-works/pi-tui/dist/components/input.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/components/input.js +0 -378
- package/node_modules/@earendil-works/pi-tui/dist/components/input.js.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/components/loader.d.ts +0 -31
- package/node_modules/@earendil-works/pi-tui/dist/components/loader.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/components/loader.js +0 -69
- package/node_modules/@earendil-works/pi-tui/dist/components/loader.js.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/components/markdown.d.ts +0 -96
- package/node_modules/@earendil-works/pi-tui/dist/components/markdown.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/components/markdown.js +0 -644
- package/node_modules/@earendil-works/pi-tui/dist/components/markdown.js.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/components/select-list.d.ts +0 -50
- package/node_modules/@earendil-works/pi-tui/dist/components/select-list.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/components/select-list.js +0 -159
- package/node_modules/@earendil-works/pi-tui/dist/components/select-list.js.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/components/settings-list.d.ts +0 -50
- package/node_modules/@earendil-works/pi-tui/dist/components/settings-list.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/components/settings-list.js +0 -185
- package/node_modules/@earendil-works/pi-tui/dist/components/settings-list.js.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/components/spacer.d.ts +0 -12
- package/node_modules/@earendil-works/pi-tui/dist/components/spacer.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/components/spacer.js +0 -23
- package/node_modules/@earendil-works/pi-tui/dist/components/spacer.js.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/components/text.d.ts +0 -19
- package/node_modules/@earendil-works/pi-tui/dist/components/text.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/components/text.js +0 -89
- package/node_modules/@earendil-works/pi-tui/dist/components/text.js.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/components/truncated-text.d.ts +0 -13
- package/node_modules/@earendil-works/pi-tui/dist/components/truncated-text.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/components/truncated-text.js +0 -51
- package/node_modules/@earendil-works/pi-tui/dist/components/truncated-text.js.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/editor-component.d.ts +0 -39
- package/node_modules/@earendil-works/pi-tui/dist/editor-component.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/editor-component.js +0 -2
- package/node_modules/@earendil-works/pi-tui/dist/editor-component.js.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/fuzzy.d.ts +0 -16
- package/node_modules/@earendil-works/pi-tui/dist/fuzzy.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/fuzzy.js +0 -110
- package/node_modules/@earendil-works/pi-tui/dist/fuzzy.js.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/index.d.ts +0 -23
- package/node_modules/@earendil-works/pi-tui/dist/index.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/index.js +0 -32
- package/node_modules/@earendil-works/pi-tui/dist/index.js.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/keybindings.d.ts +0 -193
- package/node_modules/@earendil-works/pi-tui/dist/keybindings.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/keybindings.js +0 -174
- package/node_modules/@earendil-works/pi-tui/dist/keybindings.js.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/keys.d.ts +0 -184
- package/node_modules/@earendil-works/pi-tui/dist/keys.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/keys.js +0 -1173
- package/node_modules/@earendil-works/pi-tui/dist/keys.js.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/kill-ring.d.ts +0 -28
- package/node_modules/@earendil-works/pi-tui/dist/kill-ring.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/kill-ring.js +0 -44
- package/node_modules/@earendil-works/pi-tui/dist/kill-ring.js.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/native-modifiers.d.ts +0 -3
- package/node_modules/@earendil-works/pi-tui/dist/native-modifiers.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/native-modifiers.js +0 -53
- package/node_modules/@earendil-works/pi-tui/dist/native-modifiers.js.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/stdin-buffer.d.ts +0 -50
- package/node_modules/@earendil-works/pi-tui/dist/stdin-buffer.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/stdin-buffer.js +0 -361
- package/node_modules/@earendil-works/pi-tui/dist/stdin-buffer.js.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/terminal-image.d.ts +0 -90
- package/node_modules/@earendil-works/pi-tui/dist/terminal-image.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/terminal-image.js +0 -366
- package/node_modules/@earendil-works/pi-tui/dist/terminal-image.js.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts +0 -113
- package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/terminal.js +0 -472
- package/node_modules/@earendil-works/pi-tui/dist/terminal.js.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/tui.d.ts +0 -227
- package/node_modules/@earendil-works/pi-tui/dist/tui.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/tui.js +0 -1106
- package/node_modules/@earendil-works/pi-tui/dist/tui.js.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/undo-stack.d.ts +0 -17
- package/node_modules/@earendil-works/pi-tui/dist/undo-stack.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/undo-stack.js +0 -25
- package/node_modules/@earendil-works/pi-tui/dist/undo-stack.js.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/utils.d.ts +0 -84
- package/node_modules/@earendil-works/pi-tui/dist/utils.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/utils.js +0 -1029
- package/node_modules/@earendil-works/pi-tui/dist/utils.js.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/word-navigation.d.ts +0 -25
- package/node_modules/@earendil-works/pi-tui/dist/word-navigation.d.ts.map +0 -1
- package/node_modules/@earendil-works/pi-tui/dist/word-navigation.js +0 -96
- package/node_modules/@earendil-works/pi-tui/dist/word-navigation.js.map +0 -1
- package/node_modules/@earendil-works/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node +0 -0
- package/node_modules/@earendil-works/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
- package/node_modules/@earendil-works/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
- package/node_modules/@earendil-works/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
- package/node_modules/@earendil-works/pi-tui/package.json +0 -47
- package/node_modules/get-east-asian-width/index.d.ts +0 -60
- package/node_modules/get-east-asian-width/index.js +0 -30
- package/node_modules/get-east-asian-width/license +0 -9
- package/node_modules/get-east-asian-width/lookup-data.js +0 -21
- package/node_modules/get-east-asian-width/lookup.js +0 -138
- package/node_modules/get-east-asian-width/package.json +0 -71
- package/node_modules/get-east-asian-width/readme.md +0 -65
- package/node_modules/get-east-asian-width/utilities.js +0 -24
- package/node_modules/marked/LICENSE.md +0 -44
- package/node_modules/marked/README.md +0 -106
- package/node_modules/marked/bin/main.js +0 -282
- package/node_modules/marked/bin/marked.js +0 -15
- package/node_modules/marked/lib/marked.cjs +0 -2211
- package/node_modules/marked/lib/marked.cjs.map +0 -7
- package/node_modules/marked/lib/marked.d.cts +0 -728
- package/node_modules/marked/lib/marked.d.ts +0 -728
- package/node_modules/marked/lib/marked.esm.js +0 -2189
- package/node_modules/marked/lib/marked.esm.js.map +0 -7
- package/node_modules/marked/lib/marked.umd.js +0 -2213
- package/node_modules/marked/lib/marked.umd.js.map +0 -7
- package/node_modules/marked/man/marked.1 +0 -111
- package/node_modules/marked/man/marked.1.md +0 -92
- package/node_modules/marked/marked.min.js +0 -69
- package/node_modules/marked/package.json +0 -111
|
@@ -0,0 +1,1636 @@
|
|
|
1
|
+
import { Agent } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { createAssistantMessageEventStream, getSupportedThinkingLevels, isContextOverflow, streamSimple, StringEnum, } from "@earendil-works/pi-ai";
|
|
3
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { Type } from "typebox";
|
|
8
|
+
import { createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, } from "../messages.js";
|
|
9
|
+
import { buildContextDeletionFilteredPath, buildContextDeletionFilters, } from "../session-manager.js";
|
|
10
|
+
import { estimateTokens } from "./compaction.js";
|
|
11
|
+
export const CONTEXT_COMPACTION_PROMPT_VERSION = 1;
|
|
12
|
+
const CONTEXT_COMPACTION_THINKING_LEVEL_ORDER = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
13
|
+
const CONTEXT_DELETE_TOOL_NAME = "context_delete";
|
|
14
|
+
const CONTEXT_GREP_DELETE_TOOL_NAME = "context_grep_delete";
|
|
15
|
+
const CONTEXT_SEARCH_TRANSCRIPT_TOOL_NAME = "context_search_transcript";
|
|
16
|
+
const CONTEXT_READ_ENTRY_TOOL_NAME = "context_read_entry";
|
|
17
|
+
export const CONTEXT_COMPACTION_MAX_TURNS = 50;
|
|
18
|
+
const CONTEXT_GREP_DELETE_DEFAULT_MAX_MATCHES = 50;
|
|
19
|
+
const CONTEXT_GREP_DELETE_MAX_REGEX_PATTERN_CHARS = 512;
|
|
20
|
+
const CONTEXT_GREP_DELETE_MAX_REGEX_SCAN_CHARS = 250_000;
|
|
21
|
+
const CONTEXT_MANIFEST_MAX_ENTRIES = 80;
|
|
22
|
+
const CONTEXT_MANIFEST_PREVIEW_CHARS = 240;
|
|
23
|
+
const CONTEXT_CRITICAL_OVERFLOW_RECENT_ENTRY_COUNT = 5;
|
|
24
|
+
const CONTEXT_READ_ENTRY_DEFAULT_MAX_CHARS = 4000;
|
|
25
|
+
const CONTEXT_READ_ENTRY_MAX_CHARS = 12_000;
|
|
26
|
+
const CONTEXT_SEARCH_DEFAULT_MAX_MATCHES = 20;
|
|
27
|
+
const CONTEXT_SEARCH_MAX_MATCHES = 100;
|
|
28
|
+
const CONTEXT_SEARCH_DEFAULT_CONTEXT_CHARS = 160;
|
|
29
|
+
const CONTEXT_SEARCH_MAX_CONTEXT_CHARS = 500;
|
|
30
|
+
const ContextDeleteToolParameters = Type.Object({
|
|
31
|
+
deletions: Type.Array(Type.Object({
|
|
32
|
+
kind: StringEnum(["entry", "content_block"], {
|
|
33
|
+
description: "Delete an entire transcript entry or a single content block within one entry.",
|
|
34
|
+
}),
|
|
35
|
+
entryId: Type.String({ minLength: 1, description: "Stable transcript entry id to delete from." }),
|
|
36
|
+
blockIndex: Type.Optional(Type.Integer({
|
|
37
|
+
minimum: 0,
|
|
38
|
+
description: "Required when kind is content_block; omit when kind is entry.",
|
|
39
|
+
})),
|
|
40
|
+
}, { additionalProperties: false }), { description: "Deletion targets only. Protected entries and recent active context must not be included." }),
|
|
41
|
+
}, { additionalProperties: false });
|
|
42
|
+
const ContextGrepDeleteToolParameters = Type.Object({
|
|
43
|
+
pattern: Type.String({ minLength: 1, description: "Literal text or regular expression to match in transcript text." }),
|
|
44
|
+
regex: Type.Optional(Type.Boolean({ description: "Treat pattern as a JavaScript regular expression. Defaults to false." })),
|
|
45
|
+
caseSensitive: Type.Optional(Type.Boolean({ description: "Use case-sensitive matching. Defaults to false." })),
|
|
46
|
+
target: Type.Optional(StringEnum(["entry", "content_block"], {
|
|
47
|
+
description: "Delete whole matching entries or matching content blocks. Defaults to entry.",
|
|
48
|
+
})),
|
|
49
|
+
maxMatches: Type.Optional(Type.Integer({
|
|
50
|
+
minimum: 1,
|
|
51
|
+
maximum: 200,
|
|
52
|
+
description: "Safety cap. If more unprotected, not-yet-deleted candidate targets are found, no deletions are applied. Defaults to 50.",
|
|
53
|
+
})),
|
|
54
|
+
expectedMatchCount: Type.Optional(Type.Integer({
|
|
55
|
+
minimum: 0,
|
|
56
|
+
description: "Optional safety check. If the match count differs, no deletions are applied.",
|
|
57
|
+
})),
|
|
58
|
+
}, { additionalProperties: false });
|
|
59
|
+
const ContextSearchTranscriptToolParameters = Type.Object({
|
|
60
|
+
pattern: Type.String({ minLength: 1, description: "Literal text or regular expression to search for." }),
|
|
61
|
+
regex: Type.Optional(Type.Boolean({ description: "Treat pattern as a JavaScript regular expression. Defaults to false." })),
|
|
62
|
+
caseSensitive: Type.Optional(Type.Boolean({ description: "Use case-sensitive matching. Defaults to false." })),
|
|
63
|
+
target: Type.Optional(StringEnum(["entry", "content_block"], {
|
|
64
|
+
description: "Search whole entry text or individual content-block text. Defaults to entry.",
|
|
65
|
+
})),
|
|
66
|
+
maxMatches: Type.Optional(Type.Integer({ minimum: 1, maximum: CONTEXT_SEARCH_MAX_MATCHES, description: "Maximum matches to return. Defaults to 20." })),
|
|
67
|
+
contextChars: Type.Optional(Type.Integer({
|
|
68
|
+
minimum: 0,
|
|
69
|
+
maximum: CONTEXT_SEARCH_MAX_CONTEXT_CHARS,
|
|
70
|
+
description: "Characters of context to include before and after each match. Defaults to 160.",
|
|
71
|
+
})),
|
|
72
|
+
}, { additionalProperties: false });
|
|
73
|
+
const ContextReadEntryToolParameters = Type.Object({
|
|
74
|
+
entryId: Type.String({ minLength: 1, description: "Stable transcript entry id to read." }),
|
|
75
|
+
blockIndex: Type.Optional(Type.Integer({ minimum: 0, description: "Optional content block index to read instead of the whole entry text." })),
|
|
76
|
+
offset: Type.Optional(Type.Integer({ minimum: 0, description: "Character offset to begin reading. Defaults to 0." })),
|
|
77
|
+
maxChars: Type.Optional(Type.Integer({
|
|
78
|
+
minimum: 1,
|
|
79
|
+
maximum: CONTEXT_READ_ENTRY_MAX_CHARS,
|
|
80
|
+
description: "Maximum characters to return. Defaults to 4000; keep reads small to avoid overflowing context.",
|
|
81
|
+
})),
|
|
82
|
+
}, { additionalProperties: false });
|
|
83
|
+
const CONTEXT_DELETE_TOOL = {
|
|
84
|
+
name: CONTEXT_DELETE_TOOL_NAME,
|
|
85
|
+
description: "Record context compaction deletion targets directly against the transcript.",
|
|
86
|
+
parameters: ContextDeleteToolParameters,
|
|
87
|
+
};
|
|
88
|
+
const CONTEXT_GREP_DELETE_TOOL = {
|
|
89
|
+
name: CONTEXT_GREP_DELETE_TOOL_NAME,
|
|
90
|
+
description: "Bulk-delete transcript entries or content blocks matching a guarded grep/regex query.",
|
|
91
|
+
parameters: ContextGrepDeleteToolParameters,
|
|
92
|
+
};
|
|
93
|
+
const CONTEXT_SEARCH_TRANSCRIPT_TOOL = {
|
|
94
|
+
name: CONTEXT_SEARCH_TRANSCRIPT_TOOL_NAME,
|
|
95
|
+
description: "Search the full transcript working copy and return small snippets without mutating deletion state.",
|
|
96
|
+
parameters: ContextSearchTranscriptToolParameters,
|
|
97
|
+
};
|
|
98
|
+
const CONTEXT_READ_ENTRY_TOOL = {
|
|
99
|
+
name: CONTEXT_READ_ENTRY_TOOL_NAME,
|
|
100
|
+
description: "Read a small slice of one transcript entry or content block from the full transcript working copy.",
|
|
101
|
+
parameters: ContextReadEntryToolParameters,
|
|
102
|
+
};
|
|
103
|
+
const CONTEXT_COMPACTION_SYSTEM_PROMPT = `You are a context compaction assistant.
|
|
104
|
+
|
|
105
|
+
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.`;
|
|
106
|
+
const CONTEXT_COMPACTION_FIXED_PROMPT = `Reference the provided transcript file transcript and use your search/read tools for small inspections, then use context_delete or context_grep_delete for deletions.
|
|
107
|
+
|
|
108
|
+
You MUST NOT summarize.
|
|
109
|
+
You MUST NOT paraphrase.
|
|
110
|
+
You MUST NOT generate replacement context.
|
|
111
|
+
You MUST NOT mutate retained transcript objects or content.
|
|
112
|
+
Deletion tool calls are the compaction action; record only deletion targets by stable ID.
|
|
113
|
+
|
|
114
|
+
What Gets Deleted:
|
|
115
|
+
- Redundant tool outputs: file reads already acted on, grep/search results already processed, passing test output no longer needed.
|
|
116
|
+
- Exploratory dead ends: irrelevant files read, unhelpful or empty searches.
|
|
117
|
+
- Verbose boilerplate: license headers, import blocks the agent isn't modifying, configuration files read for reference.
|
|
118
|
+
- Superseded information: earlier versions of files that have since been edited, old error messages from bugs already fixed.
|
|
119
|
+
|
|
120
|
+
What Survives:
|
|
121
|
+
- Active file paths and line numbers: Any reference the agent might need to navigate.
|
|
122
|
+
- Current error messages: Unresolved bugss and their exact text.
|
|
123
|
+
- 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.
|
|
124
|
+
- Recent tool calls and their results: The last 3-5 operations.
|
|
125
|
+
- User instructions: The original task and any clarifications.
|
|
126
|
+
|
|
127
|
+
<output_format>
|
|
128
|
+
Call the context_delete tool one or more times with deletion targets in this shape:
|
|
129
|
+
{ "deletions": [{ "kind": "entry", "entryId": "..." }] }
|
|
130
|
+
|
|
131
|
+
For content-block deletions, use:
|
|
132
|
+
{ "kind": "content_block", "entryId": "...", "blockIndex": 0 }
|
|
133
|
+
|
|
134
|
+
The tool applies and validates deletion targets immediately. You can continue calling it for additional deletions if useful.
|
|
135
|
+
|
|
136
|
+
For guarded bulk deletion by text match, call context_grep_delete with a literal pattern or regex. It skips protected context, enforces maxMatches and expectedMatchCount, and validates through the same tool-call/tool-result safety rules.
|
|
137
|
+
|
|
138
|
+
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.
|
|
139
|
+
|
|
140
|
+
When you are done, reply with a brief plain-text completion message. Do not write deletion JSON or deletion target IDs outside tool calls.
|
|
141
|
+
</output_format>`;
|
|
142
|
+
function getMessageFromEntry(entry) {
|
|
143
|
+
if (entry.type === "message") {
|
|
144
|
+
return entry.message;
|
|
145
|
+
}
|
|
146
|
+
if (entry.type === "custom_message") {
|
|
147
|
+
return createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp, entry.excludeFromContext);
|
|
148
|
+
}
|
|
149
|
+
if (entry.type === "branch_summary") {
|
|
150
|
+
return createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp);
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
function isExcludedFromLlmContext(message) {
|
|
155
|
+
switch (message.role) {
|
|
156
|
+
case "bashExecution":
|
|
157
|
+
return Boolean(message.excludeFromContext);
|
|
158
|
+
case "custom":
|
|
159
|
+
return message.excludeFromContext === true;
|
|
160
|
+
default:
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function getContextEligibleMessageFromEntry(entry) {
|
|
165
|
+
const message = getMessageFromEntry(entry);
|
|
166
|
+
if (!message || isExcludedFromLlmContext(message))
|
|
167
|
+
return undefined;
|
|
168
|
+
return message;
|
|
169
|
+
}
|
|
170
|
+
function textFromUnknownContent(content) {
|
|
171
|
+
if (typeof content === "string")
|
|
172
|
+
return content;
|
|
173
|
+
if (!Array.isArray(content))
|
|
174
|
+
return JSON.stringify(content);
|
|
175
|
+
return content.map((block) => textFromContentBlock(block)).join("\n");
|
|
176
|
+
}
|
|
177
|
+
function textFromContentBlock(block) {
|
|
178
|
+
if (!block || typeof block !== "object")
|
|
179
|
+
return String(block);
|
|
180
|
+
const record = block;
|
|
181
|
+
if (record.type === "text" && typeof record.text === "string")
|
|
182
|
+
return record.text;
|
|
183
|
+
if (record.type === "thinking" && typeof record.thinking === "string")
|
|
184
|
+
return record.thinking;
|
|
185
|
+
if (record.type === "toolCall") {
|
|
186
|
+
const name = typeof record.name === "string" ? record.name : "tool";
|
|
187
|
+
const id = typeof record.id === "string" ? record.id : "unknown";
|
|
188
|
+
const args = "arguments" in record ? JSON.stringify(record.arguments) : "";
|
|
189
|
+
return `toolCall ${id} ${name} ${args}`.trim();
|
|
190
|
+
}
|
|
191
|
+
if (record.type === "image")
|
|
192
|
+
return "[image]";
|
|
193
|
+
return JSON.stringify(record);
|
|
194
|
+
}
|
|
195
|
+
const IMAGE_BLOCK_CHAR_ESTIMATE = 4800;
|
|
196
|
+
const IMAGE_BLOCK_TOKEN_ESTIMATE = Math.ceil(IMAGE_BLOCK_CHAR_ESTIMATE / 4);
|
|
197
|
+
function estimateTextTokens(text) {
|
|
198
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
199
|
+
}
|
|
200
|
+
function estimateContentBlockTokens(block, text) {
|
|
201
|
+
if (block && typeof block === "object" && block.type === "image") {
|
|
202
|
+
return IMAGE_BLOCK_TOKEN_ESTIMATE;
|
|
203
|
+
}
|
|
204
|
+
return estimateTextTokens(text);
|
|
205
|
+
}
|
|
206
|
+
function getToolCallIdFromBlock(block) {
|
|
207
|
+
if (!block || typeof block !== "object")
|
|
208
|
+
return undefined;
|
|
209
|
+
const record = block;
|
|
210
|
+
if (record.type !== "toolCall")
|
|
211
|
+
return undefined;
|
|
212
|
+
return typeof record.id === "string" ? record.id : undefined;
|
|
213
|
+
}
|
|
214
|
+
function getToolResultCallId(message) {
|
|
215
|
+
if (message.role !== "toolResult")
|
|
216
|
+
return undefined;
|
|
217
|
+
const callId = message.toolCallId;
|
|
218
|
+
return typeof callId === "string" ? callId : undefined;
|
|
219
|
+
}
|
|
220
|
+
function contentBlocksForEntry(entryId, message, protectedEntry, existingDeletedBlocks) {
|
|
221
|
+
if (message.role === "compactionSummary") {
|
|
222
|
+
const text = message.summary;
|
|
223
|
+
return [
|
|
224
|
+
{
|
|
225
|
+
entryId,
|
|
226
|
+
blockIndex: 0,
|
|
227
|
+
type: "summary",
|
|
228
|
+
text,
|
|
229
|
+
tokenEstimate: estimateTextTokens(text),
|
|
230
|
+
protected: protectedEntry,
|
|
231
|
+
},
|
|
232
|
+
];
|
|
233
|
+
}
|
|
234
|
+
const content = message.content;
|
|
235
|
+
if (!Array.isArray(content))
|
|
236
|
+
return [];
|
|
237
|
+
return content
|
|
238
|
+
.map((block, blockIndex) => {
|
|
239
|
+
if (existingDeletedBlocks?.has(blockIndex))
|
|
240
|
+
return undefined;
|
|
241
|
+
const text = textFromContentBlock(block);
|
|
242
|
+
return {
|
|
243
|
+
entryId,
|
|
244
|
+
blockIndex,
|
|
245
|
+
type: block && typeof block === "object" && typeof block.type === "string"
|
|
246
|
+
? (block.type)
|
|
247
|
+
: "unknown",
|
|
248
|
+
text,
|
|
249
|
+
tokenEstimate: estimateContentBlockTokens(block, text),
|
|
250
|
+
protected: protectedEntry,
|
|
251
|
+
toolCallId: getToolCallIdFromBlock(block),
|
|
252
|
+
};
|
|
253
|
+
})
|
|
254
|
+
.filter((block) => block !== undefined);
|
|
255
|
+
}
|
|
256
|
+
function messageText(message) {
|
|
257
|
+
switch (message.role) {
|
|
258
|
+
case "bashExecution":
|
|
259
|
+
return `Ran ${message.command}\n${message.output}`;
|
|
260
|
+
case "branchSummary":
|
|
261
|
+
case "compactionSummary":
|
|
262
|
+
return message.summary;
|
|
263
|
+
case "custom":
|
|
264
|
+
case "toolResult":
|
|
265
|
+
case "user":
|
|
266
|
+
return textFromUnknownContent(message.content);
|
|
267
|
+
case "assistant":
|
|
268
|
+
return textFromUnknownContent(message.content);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function hasAssistantError(message) {
|
|
272
|
+
return message.role === "assistant" && message.stopReason === "error";
|
|
273
|
+
}
|
|
274
|
+
function hasToolResultError(message) {
|
|
275
|
+
return message.role === "toolResult" && message.isError === true;
|
|
276
|
+
}
|
|
277
|
+
function hasFailedBashExecution(message) {
|
|
278
|
+
return message.role === "bashExecution" && typeof message.exitCode === "number" && message.exitCode !== 0;
|
|
279
|
+
}
|
|
280
|
+
function collectLatestSummaryCompactionIndex(pathEntries) {
|
|
281
|
+
for (let i = pathEntries.length - 1; i >= 0; i--) {
|
|
282
|
+
if (pathEntries[i].type === "compaction")
|
|
283
|
+
return i;
|
|
284
|
+
}
|
|
285
|
+
return -1;
|
|
286
|
+
}
|
|
287
|
+
function collectActiveEntryIndices(pathEntries, latestCompactionIndex) {
|
|
288
|
+
if (latestCompactionIndex < 0) {
|
|
289
|
+
return pathEntries.map((_, index) => index);
|
|
290
|
+
}
|
|
291
|
+
const latestCompaction = pathEntries[latestCompactionIndex];
|
|
292
|
+
if (latestCompaction.type !== "compaction")
|
|
293
|
+
return pathEntries.map((_, index) => index);
|
|
294
|
+
const indices = [];
|
|
295
|
+
let foundFirstKept = false;
|
|
296
|
+
for (let i = 0; i < latestCompactionIndex; i++) {
|
|
297
|
+
const entry = pathEntries[i];
|
|
298
|
+
if (entry.id === latestCompaction.firstKeptEntryId) {
|
|
299
|
+
foundFirstKept = true;
|
|
300
|
+
}
|
|
301
|
+
if (foundFirstKept)
|
|
302
|
+
indices.push(i);
|
|
303
|
+
}
|
|
304
|
+
for (let i = latestCompactionIndex + 1; i < pathEntries.length; i++) {
|
|
305
|
+
indices.push(i);
|
|
306
|
+
}
|
|
307
|
+
return indices;
|
|
308
|
+
}
|
|
309
|
+
function isProtectedEntry(entry, message, recentEntryIds) {
|
|
310
|
+
if (recentEntryIds.has(entry.id))
|
|
311
|
+
return true;
|
|
312
|
+
if (message.role === "user")
|
|
313
|
+
return true;
|
|
314
|
+
if (message.role === "custom")
|
|
315
|
+
return true;
|
|
316
|
+
if (message.role === "branchSummary" || message.role === "compactionSummary")
|
|
317
|
+
return true;
|
|
318
|
+
if (hasAssistantError(message) || hasToolResultError(message))
|
|
319
|
+
return true;
|
|
320
|
+
if (hasFailedBashExecution(message))
|
|
321
|
+
return true;
|
|
322
|
+
if (entry.type === "branch_summary")
|
|
323
|
+
return true;
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
export function prepareContextCompaction(pathEntries, settings, options = {}) {
|
|
327
|
+
if (pathEntries.length === 0)
|
|
328
|
+
return undefined;
|
|
329
|
+
const latestCompactionIndex = collectLatestSummaryCompactionIndex(pathEntries);
|
|
330
|
+
const deletionFilters = buildContextDeletionFilters(pathEntries);
|
|
331
|
+
const filteredPathEntries = buildContextDeletionFilteredPath(pathEntries, deletionFilters);
|
|
332
|
+
const filteredEntryById = new Map(filteredPathEntries.map((entry) => [entry.id, entry]));
|
|
333
|
+
const activeEntryIndices = collectActiveEntryIndices(pathEntries, latestCompactionIndex);
|
|
334
|
+
const messageEntryIds = activeEntryIndices
|
|
335
|
+
.map((index) => filteredEntryById.get(pathEntries[index].id))
|
|
336
|
+
.filter((entry) => entry !== undefined && getContextEligibleMessageFromEntry(entry) !== undefined)
|
|
337
|
+
.map((entry) => entry.id);
|
|
338
|
+
const recentEntryIds = new Set(messageEntryIds.slice(-CONTEXT_CRITICAL_OVERFLOW_RECENT_ENTRY_COUNT));
|
|
339
|
+
const protectedEntryIds = new Set();
|
|
340
|
+
const entries = [];
|
|
341
|
+
if (latestCompactionIndex >= 0) {
|
|
342
|
+
const latestCompaction = pathEntries[latestCompactionIndex];
|
|
343
|
+
if (latestCompaction.type === "compaction") {
|
|
344
|
+
const message = createCompactionSummaryMessage(latestCompaction.summary, latestCompaction.tokensBefore, latestCompaction.timestamp);
|
|
345
|
+
const contentBlocks = contentBlocksForEntry(latestCompaction.id, message, true, undefined);
|
|
346
|
+
protectedEntryIds.add(latestCompaction.id);
|
|
347
|
+
entries.push({
|
|
348
|
+
entryId: latestCompaction.id,
|
|
349
|
+
entryType: latestCompaction.type,
|
|
350
|
+
role: message.role,
|
|
351
|
+
text: messageText(message),
|
|
352
|
+
tokenEstimate: estimateTokens(message),
|
|
353
|
+
protected: true,
|
|
354
|
+
contentBlocks,
|
|
355
|
+
message,
|
|
356
|
+
toolCallIds: [],
|
|
357
|
+
toolResultFor: undefined,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
for (const index of activeEntryIndices) {
|
|
362
|
+
const rawEntry = pathEntries[index];
|
|
363
|
+
const entry = filteredEntryById.get(rawEntry.id);
|
|
364
|
+
if (!entry || entry.type === "context_compaction")
|
|
365
|
+
continue;
|
|
366
|
+
const message = getContextEligibleMessageFromEntry(entry);
|
|
367
|
+
if (!message)
|
|
368
|
+
continue;
|
|
369
|
+
const protectedEntry = isProtectedEntry(entry, message, recentEntryIds);
|
|
370
|
+
if (protectedEntry)
|
|
371
|
+
protectedEntryIds.add(entry.id);
|
|
372
|
+
const rawMessage = getContextEligibleMessageFromEntry(rawEntry) ?? message;
|
|
373
|
+
const contentBlocks = contentBlocksForEntry(entry.id, rawMessage, protectedEntry, deletionFilters.deletedContentBlocks.get(entry.id));
|
|
374
|
+
const toolCallIds = contentBlocks.map((block) => block.toolCallId).filter((id) => id !== undefined);
|
|
375
|
+
const text = contentBlocks.length > 0 ? contentBlocks.map((block) => block.text).join("\n") : messageText(message);
|
|
376
|
+
entries.push({
|
|
377
|
+
entryId: entry.id,
|
|
378
|
+
entryType: entry.type,
|
|
379
|
+
role: message.role,
|
|
380
|
+
text,
|
|
381
|
+
tokenEstimate: estimateTokens(message),
|
|
382
|
+
protected: protectedEntry,
|
|
383
|
+
contentBlocks,
|
|
384
|
+
message,
|
|
385
|
+
toolCallIds,
|
|
386
|
+
toolResultFor: getToolResultCallId(message),
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
if (entries.length < 2)
|
|
390
|
+
return undefined;
|
|
391
|
+
return {
|
|
392
|
+
branchEntries: pathEntries,
|
|
393
|
+
mode: options.mode ?? "standard",
|
|
394
|
+
transcript: {
|
|
395
|
+
entries,
|
|
396
|
+
protectedEntryIds: [...protectedEntryIds],
|
|
397
|
+
tokensBefore: entries.reduce((total, entry) => total + entry.tokenEstimate, 0),
|
|
398
|
+
settings,
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function targetKey(target) {
|
|
403
|
+
return target.kind === "entry" ? `entry:${target.entryId}` : `content_block:${target.entryId}:${target.blockIndex}`;
|
|
404
|
+
}
|
|
405
|
+
function rawTargetKey(target) {
|
|
406
|
+
return target.kind === "entry" ? `entry:${target.entryId}` : `content_block:${target.entryId}:${target.blockIndex}`;
|
|
407
|
+
}
|
|
408
|
+
function normalizeRawTarget(target) {
|
|
409
|
+
if (target.kind === "entry")
|
|
410
|
+
return { kind: "entry", entryId: target.entryId };
|
|
411
|
+
return { kind: "content_block", entryId: target.entryId, blockIndex: target.blockIndex };
|
|
412
|
+
}
|
|
413
|
+
function rawDeletionFromTarget(target) {
|
|
414
|
+
if (target.kind === "entry")
|
|
415
|
+
return { kind: "entry", entryId: target.entryId };
|
|
416
|
+
return { kind: "content_block", entryId: target.entryId, blockIndex: target.blockIndex };
|
|
417
|
+
}
|
|
418
|
+
function deletionRequestFromTargets(targets) {
|
|
419
|
+
return { deletions: targets.map(rawDeletionFromTarget) };
|
|
420
|
+
}
|
|
421
|
+
function getDeletedEntryIds(targets) {
|
|
422
|
+
return new Set(targets.filter((target) => target.kind === "entry").map((target) => target.entryId));
|
|
423
|
+
}
|
|
424
|
+
function getDeletedContentBlocks(targets) {
|
|
425
|
+
const blocksByEntry = new Map();
|
|
426
|
+
for (const target of targets) {
|
|
427
|
+
if (target.kind !== "content_block")
|
|
428
|
+
continue;
|
|
429
|
+
const blocks = blocksByEntry.get(target.entryId) ?? new Set();
|
|
430
|
+
blocks.add(target.blockIndex);
|
|
431
|
+
blocksByEntry.set(target.entryId, blocks);
|
|
432
|
+
}
|
|
433
|
+
return blocksByEntry;
|
|
434
|
+
}
|
|
435
|
+
function isToolCallBlockDeleted(entry, callId, deletedEntryIds, deletedContentBlocks) {
|
|
436
|
+
if (deletedEntryIds.has(entry.entryId))
|
|
437
|
+
return true;
|
|
438
|
+
const deletedBlocks = deletedContentBlocks.get(entry.entryId);
|
|
439
|
+
if (!deletedBlocks)
|
|
440
|
+
return false;
|
|
441
|
+
return entry.contentBlocks.some((block) => block.toolCallId === callId && deletedBlocks.has(block.blockIndex));
|
|
442
|
+
}
|
|
443
|
+
function toolCallBlockIndexes(entry, callId) {
|
|
444
|
+
return entry.contentBlocks
|
|
445
|
+
.filter((block) => block.toolCallId === callId)
|
|
446
|
+
.map((block) => block.blockIndex);
|
|
447
|
+
}
|
|
448
|
+
function addTarget(targets, target) {
|
|
449
|
+
if (targets.some((existing) => targetKey(existing) === targetKey(target)))
|
|
450
|
+
return false;
|
|
451
|
+
targets.push(target);
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
function deleteEntryTarget(targets, entryId) {
|
|
455
|
+
let changed = false;
|
|
456
|
+
for (let index = targets.length - 1; index >= 0; index--) {
|
|
457
|
+
const target = targets[index];
|
|
458
|
+
if (target.kind === "content_block" && target.entryId === entryId) {
|
|
459
|
+
targets.splice(index, 1);
|
|
460
|
+
changed = true;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return addTarget(targets, { kind: "entry", entryId }) || changed;
|
|
464
|
+
}
|
|
465
|
+
function removeEntryDeletion(targets, entryId) {
|
|
466
|
+
const originalLength = targets.length;
|
|
467
|
+
for (let index = targets.length - 1; index >= 0; index--) {
|
|
468
|
+
const target = targets[index];
|
|
469
|
+
if (target.kind === "entry" && target.entryId === entryId)
|
|
470
|
+
targets.splice(index, 1);
|
|
471
|
+
}
|
|
472
|
+
return targets.length !== originalLength;
|
|
473
|
+
}
|
|
474
|
+
function mergeContextDeletionTargets(baseTargets, additionalTargets) {
|
|
475
|
+
const targets = [...baseTargets];
|
|
476
|
+
for (const target of additionalTargets) {
|
|
477
|
+
if (target.kind === "entry") {
|
|
478
|
+
deleteEntryTarget(targets, target.entryId);
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
if (!getDeletedEntryIds(targets).has(target.entryId)) {
|
|
482
|
+
addTarget(targets, target);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return targets;
|
|
486
|
+
}
|
|
487
|
+
function canonicalizeEntryTargets(targets, entry) {
|
|
488
|
+
if (entry.protected || getDeletedEntryIds(targets).has(entry.entryId))
|
|
489
|
+
return false;
|
|
490
|
+
const deletedBlocks = getDeletedContentBlocks(targets).get(entry.entryId);
|
|
491
|
+
if (!deletedBlocks || !entry.contentBlocks.every((block) => deletedBlocks.has(block.blockIndex)))
|
|
492
|
+
return false;
|
|
493
|
+
// Only repair/promote when dependency reconciliation reaches this entry. Non-tool entries that
|
|
494
|
+
// request every block individually stay invalid so the assistant must choose explicit entry deletion.
|
|
495
|
+
return deleteEntryTarget(targets, entry.entryId);
|
|
496
|
+
}
|
|
497
|
+
function removeToolCallDeletion(targets, entry, callId) {
|
|
498
|
+
let changed = removeEntryDeletion(targets, entry.entryId);
|
|
499
|
+
const blockIndexes = new Set(toolCallBlockIndexes(entry, callId));
|
|
500
|
+
for (let index = targets.length - 1; index >= 0; index--) {
|
|
501
|
+
const target = targets[index];
|
|
502
|
+
if (target.kind === "content_block" && target.entryId === entry.entryId && blockIndexes.has(target.blockIndex)) {
|
|
503
|
+
targets.splice(index, 1);
|
|
504
|
+
changed = true;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return changed;
|
|
508
|
+
}
|
|
509
|
+
function addToolCallDeletion(targets, entry, callId) {
|
|
510
|
+
if (entry.protected)
|
|
511
|
+
return false;
|
|
512
|
+
let changed = false;
|
|
513
|
+
for (const blockIndex of toolCallBlockIndexes(entry, callId)) {
|
|
514
|
+
if (!getDeletedEntryIds(targets).has(entry.entryId)) {
|
|
515
|
+
changed = addTarget(targets, { kind: "content_block", entryId: entry.entryId, blockIndex }) || changed;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return canonicalizeEntryTargets(targets, entry) || changed;
|
|
519
|
+
}
|
|
520
|
+
let warnedReconciliationNonConvergence = false;
|
|
521
|
+
function reconcileToolDependencies(transcript, initialTargets) {
|
|
522
|
+
const targets = [...initialTargets];
|
|
523
|
+
const callEntries = new Map();
|
|
524
|
+
const entriesWithToolCalls = new Set();
|
|
525
|
+
const resultEntries = new Map();
|
|
526
|
+
for (const entry of transcript.entries) {
|
|
527
|
+
for (const callId of entry.toolCallIds) {
|
|
528
|
+
callEntries.set(callId, entry);
|
|
529
|
+
entriesWithToolCalls.add(entry);
|
|
530
|
+
}
|
|
531
|
+
if (entry.toolResultFor) {
|
|
532
|
+
const results = resultEntries.get(entry.toolResultFor) ?? [];
|
|
533
|
+
results.push(entry);
|
|
534
|
+
resultEntries.set(entry.toolResultFor, results);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// Bounded fixpoint repair: each pass can add/remove paired call/result targets. In practice this
|
|
538
|
+
// converges within one or two passes; the cap protects against accidental oscillation.
|
|
539
|
+
let changed = true;
|
|
540
|
+
let remainingPasses = Math.max(1, transcript.entries.length * 2);
|
|
541
|
+
while (changed && remainingPasses > 0) {
|
|
542
|
+
changed = false;
|
|
543
|
+
remainingPasses -= 1;
|
|
544
|
+
let deletedEntryIds = getDeletedEntryIds(targets);
|
|
545
|
+
let deletedContentBlocks = getDeletedContentBlocks(targets);
|
|
546
|
+
const recordChange = (nextChanged) => {
|
|
547
|
+
if (!nextChanged)
|
|
548
|
+
return;
|
|
549
|
+
changed = true;
|
|
550
|
+
deletedEntryIds = getDeletedEntryIds(targets);
|
|
551
|
+
deletedContentBlocks = getDeletedContentBlocks(targets);
|
|
552
|
+
};
|
|
553
|
+
for (const [callId, callEntry] of callEntries) {
|
|
554
|
+
const callDeleted = isToolCallBlockDeleted(callEntry, callId, deletedEntryIds, deletedContentBlocks);
|
|
555
|
+
const results = resultEntries.get(callId) ?? [];
|
|
556
|
+
if (callDeleted) {
|
|
557
|
+
const retainedProtectedResult = results.find((entry) => entry.protected && !deletedEntryIds.has(entry.entryId));
|
|
558
|
+
if (retainedProtectedResult) {
|
|
559
|
+
recordChange(removeToolCallDeletion(targets, callEntry, callId));
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
for (const result of results) {
|
|
563
|
+
recordChange(deleteEntryTarget(targets, result.entryId));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (isToolCallBlockDeleted(callEntry, callId, deletedEntryIds, deletedContentBlocks))
|
|
568
|
+
continue;
|
|
569
|
+
for (const result of results) {
|
|
570
|
+
if (!deletedEntryIds.has(result.entryId))
|
|
571
|
+
continue;
|
|
572
|
+
recordChange(deleteEntryTarget(targets, result.entryId));
|
|
573
|
+
if (callEntry.protected) {
|
|
574
|
+
recordChange(removeEntryDeletion(targets, result.entryId));
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
recordChange(addToolCallDeletion(targets, callEntry, callId));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
for (const entry of entriesWithToolCalls) {
|
|
581
|
+
recordChange(canonicalizeEntryTargets(targets, entry));
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (changed && !warnedReconciliationNonConvergence) {
|
|
585
|
+
warnedReconciliationNonConvergence = true;
|
|
586
|
+
console.warn(`Context compaction tool dependency reconciliation did not converge within the bounded pass limit; validation will continue with the last reconciled target set. entries=${transcript.entries.length} callEntries=${callEntries.size} targets=${targets.length}`);
|
|
587
|
+
}
|
|
588
|
+
return targets;
|
|
589
|
+
}
|
|
590
|
+
function validateToolDependencies(transcript, targets) {
|
|
591
|
+
const deletedEntryIds = getDeletedEntryIds(targets);
|
|
592
|
+
const deletedContentBlocks = getDeletedContentBlocks(targets);
|
|
593
|
+
const callEntries = new Map();
|
|
594
|
+
const resultEntries = new Map();
|
|
595
|
+
for (const entry of transcript.entries) {
|
|
596
|
+
for (const callId of entry.toolCallIds) {
|
|
597
|
+
callEntries.set(callId, entry);
|
|
598
|
+
}
|
|
599
|
+
if (entry.toolResultFor) {
|
|
600
|
+
const results = resultEntries.get(entry.toolResultFor) ?? [];
|
|
601
|
+
results.push(entry);
|
|
602
|
+
resultEntries.set(entry.toolResultFor, results);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
for (const [callId, callEntry] of callEntries) {
|
|
606
|
+
const callDeleted = isToolCallBlockDeleted(callEntry, callId, deletedEntryIds, deletedContentBlocks);
|
|
607
|
+
const results = resultEntries.get(callId) ?? [];
|
|
608
|
+
if (callDeleted) {
|
|
609
|
+
const danglingResult = results.find((entry) => !deletedEntryIds.has(entry.entryId));
|
|
610
|
+
if (danglingResult) {
|
|
611
|
+
throw new Error(`Deleting tool call ${callId} would leave tool result entry ${danglingResult.entryId} orphaned`);
|
|
612
|
+
}
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
const deletedResult = results.find((entry) => deletedEntryIds.has(entry.entryId));
|
|
616
|
+
if (deletedResult) {
|
|
617
|
+
throw new Error(`Deleting tool result entry ${deletedResult.entryId} would leave tool call ${callId} dangling`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
function computeContextCompactionStats(transcript, targets) {
|
|
622
|
+
const entryById = new Map(transcript.entries.map((entry) => [entry.entryId, entry]));
|
|
623
|
+
const deletedEntryIds = getDeletedEntryIds(targets);
|
|
624
|
+
let deletedTokens = 0;
|
|
625
|
+
let objectsDeleted = 0;
|
|
626
|
+
for (const entryId of deletedEntryIds) {
|
|
627
|
+
const entry = entryById.get(entryId);
|
|
628
|
+
if (!entry)
|
|
629
|
+
continue;
|
|
630
|
+
deletedTokens += entry.tokenEstimate;
|
|
631
|
+
objectsDeleted += 1 + entry.contentBlocks.length;
|
|
632
|
+
}
|
|
633
|
+
for (const target of targets) {
|
|
634
|
+
if (target.kind !== "content_block" || deletedEntryIds.has(target.entryId))
|
|
635
|
+
continue;
|
|
636
|
+
const entry = entryById.get(target.entryId);
|
|
637
|
+
if (!entry)
|
|
638
|
+
continue;
|
|
639
|
+
const block = entry.contentBlocks.find((item) => item.blockIndex === target.blockIndex);
|
|
640
|
+
if (!block)
|
|
641
|
+
continue;
|
|
642
|
+
deletedTokens += block.tokenEstimate;
|
|
643
|
+
objectsDeleted += 1;
|
|
644
|
+
}
|
|
645
|
+
const objectsBefore = transcript.entries.length + transcript.entries.reduce((total, entry) => total + entry.contentBlocks.length, 0);
|
|
646
|
+
const tokensBefore = transcript.tokensBefore;
|
|
647
|
+
const tokensAfter = Math.max(0, tokensBefore - deletedTokens);
|
|
648
|
+
const percentReduction = tokensBefore > 0 ? Math.round(((tokensBefore - tokensAfter) / tokensBefore) * 1000) / 10 : 0;
|
|
649
|
+
return {
|
|
650
|
+
objectsBefore,
|
|
651
|
+
objectsAfter: Math.max(0, objectsBefore - objectsDeleted),
|
|
652
|
+
objectsDeleted,
|
|
653
|
+
tokensBefore,
|
|
654
|
+
tokensAfter,
|
|
655
|
+
percentReduction,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
function isCriticalOverflowProtectedEntryDeletable(entry, transcript) {
|
|
659
|
+
if (!entry.protected)
|
|
660
|
+
return true;
|
|
661
|
+
const entryIndex = transcript.entries.findIndex((candidate) => candidate.entryId === entry.entryId);
|
|
662
|
+
if (entryIndex < 0)
|
|
663
|
+
return false;
|
|
664
|
+
const recentBoundary = Math.max(0, transcript.entries.length - CONTEXT_CRITICAL_OVERFLOW_RECENT_ENTRY_COUNT);
|
|
665
|
+
if (entryIndex >= recentBoundary)
|
|
666
|
+
return false;
|
|
667
|
+
if (hasAssistantError(entry.message) || hasToolResultError(entry.message) || hasFailedBashExecution(entry.message)) {
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
return (entry.role === "user" ||
|
|
671
|
+
entry.role === "custom" ||
|
|
672
|
+
entry.role === "branchSummary" ||
|
|
673
|
+
entry.role === "compactionSummary" ||
|
|
674
|
+
entry.entryType === "branch_summary");
|
|
675
|
+
}
|
|
676
|
+
function canDeleteProtectedTargetInMode(transcript, target, mode) {
|
|
677
|
+
if (mode !== "critical_overflow")
|
|
678
|
+
return false;
|
|
679
|
+
const entry = transcript.entries.find((candidate) => candidate.entryId === target.entryId);
|
|
680
|
+
if (!entry || !isCriticalOverflowProtectedEntryDeletable(entry, transcript))
|
|
681
|
+
return false;
|
|
682
|
+
if (target.kind === "entry")
|
|
683
|
+
return true;
|
|
684
|
+
const block = entry.contentBlocks.find((candidate) => candidate.blockIndex === target.blockIndex);
|
|
685
|
+
return block !== undefined;
|
|
686
|
+
}
|
|
687
|
+
export function validateContextDeletionRequest(request, transcript, options = {}) {
|
|
688
|
+
const mode = options.mode ?? "standard";
|
|
689
|
+
if (!request || typeof request !== "object" || !Array.isArray(request.deletions)) {
|
|
690
|
+
throw new Error("Context deletion request must be an object with a deletions array");
|
|
691
|
+
}
|
|
692
|
+
const entryById = new Map(transcript.entries.map((entry) => [entry.entryId, entry]));
|
|
693
|
+
const seen = new Set();
|
|
694
|
+
const deletedTargets = [];
|
|
695
|
+
for (const deletion of request.deletions) {
|
|
696
|
+
if (!deletion || typeof deletion !== "object") {
|
|
697
|
+
throw new Error("Deletion target must be an object");
|
|
698
|
+
}
|
|
699
|
+
if (deletion.kind !== "entry" && deletion.kind !== "content_block") {
|
|
700
|
+
throw new Error(`Unsupported deletion target kind: ${String(deletion.kind)}`);
|
|
701
|
+
}
|
|
702
|
+
if (typeof deletion.entryId !== "string" || deletion.entryId.length === 0) {
|
|
703
|
+
throw new Error("Deletion target entryId must be a non-empty string");
|
|
704
|
+
}
|
|
705
|
+
const entry = entryById.get(deletion.entryId);
|
|
706
|
+
if (!entry) {
|
|
707
|
+
throw new Error(`Unknown deletion target entryId: ${deletion.entryId}`);
|
|
708
|
+
}
|
|
709
|
+
if (entry.protected && !canDeleteProtectedTargetInMode(transcript, normalizeRawTarget(deletion), mode)) {
|
|
710
|
+
throw new Error(`Deletion target ${deletion.entryId} is protected`);
|
|
711
|
+
}
|
|
712
|
+
if (deletion.kind === "content_block") {
|
|
713
|
+
if (!Number.isInteger(deletion.blockIndex) || deletion.blockIndex === undefined || deletion.blockIndex < 0) {
|
|
714
|
+
throw new Error(`Invalid content block index for entry ${deletion.entryId}`);
|
|
715
|
+
}
|
|
716
|
+
const block = entry.contentBlocks.find((item) => item.blockIndex === deletion.blockIndex);
|
|
717
|
+
if (!block) {
|
|
718
|
+
throw new Error(`Unknown content block ${deletion.blockIndex} for entry ${deletion.entryId}`);
|
|
719
|
+
}
|
|
720
|
+
if (block.protected && !canDeleteProtectedTargetInMode(transcript, normalizeRawTarget(deletion), mode)) {
|
|
721
|
+
throw new Error(`Content block ${deletion.entryId}:${deletion.blockIndex} is protected`);
|
|
722
|
+
}
|
|
723
|
+
if (entry.contentBlocks.length <= 1) {
|
|
724
|
+
throw new Error(`Deleting the only content block of ${deletion.entryId} must be an entry deletion`);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
const key = rawTargetKey(deletion);
|
|
728
|
+
if (seen.has(key)) {
|
|
729
|
+
throw new Error(`Duplicate deletion target: ${key}`);
|
|
730
|
+
}
|
|
731
|
+
seen.add(key);
|
|
732
|
+
const normalized = normalizeRawTarget(deletion);
|
|
733
|
+
deletedTargets.push(normalized);
|
|
734
|
+
}
|
|
735
|
+
const reconciledTargets = reconcileToolDependencies(transcript, deletedTargets);
|
|
736
|
+
const reconciledDeletedEntryIds = getDeletedEntryIds(reconciledTargets);
|
|
737
|
+
for (const target of reconciledTargets) {
|
|
738
|
+
if (target.kind === "content_block" && reconciledDeletedEntryIds.has(target.entryId)) {
|
|
739
|
+
throw new Error(`Deletion target ${targetKey(target)} overlaps with entry deletion`);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
const deletedContentBlocks = getDeletedContentBlocks(reconciledTargets);
|
|
743
|
+
for (const [entryId, blockIndexes] of deletedContentBlocks) {
|
|
744
|
+
const entry = entryById.get(entryId);
|
|
745
|
+
if (entry?.contentBlocks.every((block) => blockIndexes.has(block.blockIndex))) {
|
|
746
|
+
throw new Error(`Content-block deletions for ${entryId} would remove every content block`);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
validateToolDependencies(transcript, reconciledTargets);
|
|
750
|
+
const remainingEntries = transcript.entries.filter((entry) => !reconciledDeletedEntryIds.has(entry.entryId));
|
|
751
|
+
if (remainingEntries.length === 0) {
|
|
752
|
+
throw new Error("Deletion request would remove all context entries");
|
|
753
|
+
}
|
|
754
|
+
const hasTaskBearingContext = remainingEntries.some((entry) => entry.role === "user" || (entry.role === "compactionSummary" && entry.protected));
|
|
755
|
+
if (!hasTaskBearingContext) {
|
|
756
|
+
throw new Error("Deletion request would leave no user task in context");
|
|
757
|
+
}
|
|
758
|
+
return {
|
|
759
|
+
deletedTargets: reconciledTargets,
|
|
760
|
+
protectedEntryIds: [...transcript.protectedEntryIds],
|
|
761
|
+
stats: computeContextCompactionStats(transcript, reconciledTargets),
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
function stripJsonFence(text) {
|
|
765
|
+
const trimmed = text.trim();
|
|
766
|
+
if (!trimmed.startsWith("```") || !trimmed.endsWith("```"))
|
|
767
|
+
return trimmed;
|
|
768
|
+
const firstLineEnd = trimmed.indexOf("\n");
|
|
769
|
+
if (firstLineEnd < 0)
|
|
770
|
+
return trimmed;
|
|
771
|
+
const fenceInfo = trimmed.slice(3, firstLineEnd).trim().toLowerCase();
|
|
772
|
+
if (fenceInfo !== "" && fenceInfo !== "json")
|
|
773
|
+
return trimmed;
|
|
774
|
+
return trimmed.slice(firstLineEnd + 1, -3).trim();
|
|
775
|
+
}
|
|
776
|
+
function contextDeletionRequestFromObject(value, source) {
|
|
777
|
+
if (!value || typeof value !== "object" || !Array.isArray(value.deletions)) {
|
|
778
|
+
throw new Error(`${source} must contain a deletions array`);
|
|
779
|
+
}
|
|
780
|
+
return value;
|
|
781
|
+
}
|
|
782
|
+
function escapeRegExpLiteral(text) {
|
|
783
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
784
|
+
}
|
|
785
|
+
function formatErrorMessage(error) {
|
|
786
|
+
return error instanceof Error ? error.message : String(error);
|
|
787
|
+
}
|
|
788
|
+
function createContextDeletionToolResult(text, details) {
|
|
789
|
+
return { content: [{ type: "text", text }], details, terminate: false };
|
|
790
|
+
}
|
|
791
|
+
function assertSafeRegexPattern(pattern) {
|
|
792
|
+
if (pattern.length > CONTEXT_GREP_DELETE_MAX_REGEX_PATTERN_CHARS) {
|
|
793
|
+
throw new Error(`Regex pattern is too long (${pattern.length} characters); maximum is ${CONTEXT_GREP_DELETE_MAX_REGEX_PATTERN_CHARS}`);
|
|
794
|
+
}
|
|
795
|
+
// Heuristic ReDoS guard for common catastrophic-backtracking shapes. JavaScript's RegExp engine
|
|
796
|
+
// does not expose a timeout, so reject nested quantified groups and backreferences instead of
|
|
797
|
+
// relying only on transcript scan-size caps.
|
|
798
|
+
const hasNestedQuantifiedGroup = /\((?:[^()\\]|\\.)*[+*](?:[^()\\]|\\.)*\)\s*(?:[+*]|\{\d)/u.test(pattern);
|
|
799
|
+
const hasQuantifiedAlternation = /\((?:[^()\\]|\\.)*\|(?:[^()\\]|\\.)*\)\s*(?:[+*]|\{\d)/u.test(pattern);
|
|
800
|
+
const hasBackreference = /\\[1-9]/u.test(pattern);
|
|
801
|
+
if (hasNestedQuantifiedGroup || hasQuantifiedAlternation || hasBackreference) {
|
|
802
|
+
throw new Error("Regex pattern is not allowed because it may cause excessive backtracking; use a literal pattern or exact deletion targets instead.");
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
function createGrepMatcher(pattern, regex, caseSensitive) {
|
|
806
|
+
if (regex) {
|
|
807
|
+
assertSafeRegexPattern(pattern);
|
|
808
|
+
}
|
|
809
|
+
try {
|
|
810
|
+
return new RegExp(regex ? pattern : escapeRegExpLiteral(pattern), caseSensitive ? "u" : "iu");
|
|
811
|
+
}
|
|
812
|
+
catch (error) {
|
|
813
|
+
throw new Error(`Invalid grep ${regex ? "regex" : "pattern"}: ${formatErrorMessage(error)}`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
function assertSafeRegexScan(scanChars) {
|
|
817
|
+
if (scanChars <= CONTEXT_GREP_DELETE_MAX_REGEX_SCAN_CHARS)
|
|
818
|
+
return;
|
|
819
|
+
throw new Error(`Regex grep would scan ${scanChars} characters; maximum is ${CONTEXT_GREP_DELETE_MAX_REGEX_SCAN_CHARS}. Use a literal pattern or exact deletion targets instead.`);
|
|
820
|
+
}
|
|
821
|
+
function clampInteger(value, defaultValue, minimum, maximum) {
|
|
822
|
+
if (value === undefined)
|
|
823
|
+
return defaultValue;
|
|
824
|
+
return Math.max(minimum, Math.min(maximum, value));
|
|
825
|
+
}
|
|
826
|
+
function textSlice(text, offset, maxChars) {
|
|
827
|
+
return text.slice(offset, Math.min(text.length, offset + maxChars));
|
|
828
|
+
}
|
|
829
|
+
function findMatchIndex(matcher, text) {
|
|
830
|
+
const match = matcher.exec(text);
|
|
831
|
+
matcher.lastIndex = 0;
|
|
832
|
+
return match?.index ?? -1;
|
|
833
|
+
}
|
|
834
|
+
function snippetForMatch(text, matchIndex, contextChars) {
|
|
835
|
+
const start = Math.max(0, matchIndex - contextChars);
|
|
836
|
+
const end = Math.min(text.length, matchIndex + contextChars);
|
|
837
|
+
const prefix = start > 0 ? "…" : "";
|
|
838
|
+
const suffix = end < text.length ? "…" : "";
|
|
839
|
+
return `${prefix}${text.slice(start, end)}${suffix}`;
|
|
840
|
+
}
|
|
841
|
+
function currentTargetDeleted(targets, target) {
|
|
842
|
+
const deletedEntryIds = getDeletedEntryIds(targets);
|
|
843
|
+
if (deletedEntryIds.has(target.entryId))
|
|
844
|
+
return true;
|
|
845
|
+
if (target.kind === "entry")
|
|
846
|
+
return false;
|
|
847
|
+
return getDeletedContentBlocks(targets).get(target.entryId)?.has(target.blockIndex) === true;
|
|
848
|
+
}
|
|
849
|
+
function addGrepCandidate(candidates, matches, seenTargets, candidate, match) {
|
|
850
|
+
const key = targetKey(candidate);
|
|
851
|
+
if (seenTargets.has(key))
|
|
852
|
+
return;
|
|
853
|
+
seenTargets.add(key);
|
|
854
|
+
candidates.push(candidate);
|
|
855
|
+
matches.push(match);
|
|
856
|
+
}
|
|
857
|
+
const moduleRequire = createRequire(import.meta.url);
|
|
858
|
+
function createTransientSqliteDatabase() {
|
|
859
|
+
// better-sqlite3 is the portable/package dependency for Node-based Atomic installs.
|
|
860
|
+
// Bun cannot dlopen better-sqlite3 yet, so Bun runtime/tests use the API-compatible builtin.
|
|
861
|
+
if (process.versions.bun) {
|
|
862
|
+
const sqlite = moduleRequire("bun:sqlite");
|
|
863
|
+
return new sqlite.Database(":memory:");
|
|
864
|
+
}
|
|
865
|
+
const BetterSqliteDatabase = moduleRequire("better-sqlite3");
|
|
866
|
+
return new BetterSqliteDatabase(":memory:");
|
|
867
|
+
}
|
|
868
|
+
class SqliteAdapter {
|
|
869
|
+
constructor(db) {
|
|
870
|
+
this.db = db;
|
|
871
|
+
}
|
|
872
|
+
static createTransient() {
|
|
873
|
+
return new SqliteAdapter(createTransientSqliteDatabase());
|
|
874
|
+
}
|
|
875
|
+
exec(sql) {
|
|
876
|
+
this.db.exec(sql);
|
|
877
|
+
}
|
|
878
|
+
run(sql, ...params) {
|
|
879
|
+
this.db.prepare(sql).run(...params);
|
|
880
|
+
}
|
|
881
|
+
all(sql, ...params) {
|
|
882
|
+
return this.db.prepare(sql).all(...params);
|
|
883
|
+
}
|
|
884
|
+
get(sql, ...params) {
|
|
885
|
+
return this.db.prepare(sql).get(...params);
|
|
886
|
+
}
|
|
887
|
+
transaction(operation) {
|
|
888
|
+
this.exec("BEGIN IMMEDIATE");
|
|
889
|
+
try {
|
|
890
|
+
const result = operation();
|
|
891
|
+
this.exec("COMMIT");
|
|
892
|
+
return result;
|
|
893
|
+
}
|
|
894
|
+
catch (error) {
|
|
895
|
+
try {
|
|
896
|
+
this.exec("ROLLBACK");
|
|
897
|
+
}
|
|
898
|
+
catch { }
|
|
899
|
+
throw error;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
close() {
|
|
903
|
+
this.db.close();
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
class ContextDeletionSqliteStore {
|
|
907
|
+
constructor(sqlite) {
|
|
908
|
+
this.sqlite = sqlite;
|
|
909
|
+
}
|
|
910
|
+
initialize(transcript) {
|
|
911
|
+
this.sqlite.transaction(() => {
|
|
912
|
+
this.sqlite.exec(`
|
|
913
|
+
PRAGMA foreign_keys = ON;
|
|
914
|
+
CREATE TABLE transcript_entries (
|
|
915
|
+
position INTEGER PRIMARY KEY,
|
|
916
|
+
entry_id TEXT NOT NULL UNIQUE,
|
|
917
|
+
role TEXT NOT NULL,
|
|
918
|
+
is_protected INTEGER NOT NULL,
|
|
919
|
+
token_estimate INTEGER NOT NULL,
|
|
920
|
+
text TEXT NOT NULL,
|
|
921
|
+
tool_result_for TEXT
|
|
922
|
+
);
|
|
923
|
+
CREATE TABLE transcript_content_blocks (
|
|
924
|
+
entry_id TEXT NOT NULL,
|
|
925
|
+
block_index INTEGER NOT NULL,
|
|
926
|
+
type TEXT NOT NULL,
|
|
927
|
+
is_protected INTEGER NOT NULL,
|
|
928
|
+
token_estimate INTEGER NOT NULL,
|
|
929
|
+
text TEXT NOT NULL,
|
|
930
|
+
tool_call_id TEXT,
|
|
931
|
+
PRIMARY KEY (entry_id, block_index),
|
|
932
|
+
FOREIGN KEY (entry_id) REFERENCES transcript_entries(entry_id) ON DELETE CASCADE
|
|
933
|
+
);
|
|
934
|
+
CREATE TABLE deletion_targets (
|
|
935
|
+
position INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
936
|
+
target_key TEXT NOT NULL UNIQUE,
|
|
937
|
+
kind TEXT NOT NULL CHECK (kind IN ('entry', 'content_block')),
|
|
938
|
+
entry_id TEXT NOT NULL,
|
|
939
|
+
block_index INTEGER
|
|
940
|
+
);
|
|
941
|
+
CREATE TABLE context_compaction_state (
|
|
942
|
+
key TEXT PRIMARY KEY,
|
|
943
|
+
value TEXT NOT NULL
|
|
944
|
+
);
|
|
945
|
+
INSERT INTO context_compaction_state (key, value) VALUES ('call_count', '0');
|
|
946
|
+
`);
|
|
947
|
+
for (const [position, entry] of transcript.entries.entries()) {
|
|
948
|
+
this.sqlite.run("INSERT INTO transcript_entries (position, entry_id, role, is_protected, token_estimate, text, tool_result_for) VALUES (?, ?, ?, ?, ?, ?, ?)", position, entry.entryId, entry.role, entry.protected ? 1 : 0, entry.tokenEstimate, entry.text, entry.toolResultFor ?? null);
|
|
949
|
+
for (const block of entry.contentBlocks) {
|
|
950
|
+
this.sqlite.run("INSERT INTO transcript_content_blocks (entry_id, block_index, type, is_protected, token_estimate, text, tool_call_id) VALUES (?, ?, ?, ?, ?, ?, ?)", block.entryId, block.blockIndex, block.type, block.protected ? 1 : 0, block.tokenEstimate, block.text, block.toolCallId ?? null);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
transaction(operation) {
|
|
956
|
+
return this.sqlite.transaction(operation);
|
|
957
|
+
}
|
|
958
|
+
readTargets() {
|
|
959
|
+
return this.sqlite
|
|
960
|
+
.all("SELECT kind, entry_id, block_index FROM deletion_targets ORDER BY position")
|
|
961
|
+
.map((row) => row.kind === "entry"
|
|
962
|
+
? { kind: "entry", entryId: row.entry_id }
|
|
963
|
+
: { kind: "content_block", entryId: row.entry_id, blockIndex: row.block_index });
|
|
964
|
+
}
|
|
965
|
+
replaceTargets(targets) {
|
|
966
|
+
this.sqlite.run("DELETE FROM deletion_targets");
|
|
967
|
+
for (const target of targets) {
|
|
968
|
+
this.sqlite.run("INSERT INTO deletion_targets (target_key, kind, entry_id, block_index) VALUES (?, ?, ?, ?)", targetKey(target), target.kind, target.entryId, target.kind === "content_block" ? target.blockIndex : null);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
listEntriesForGrep() {
|
|
972
|
+
return this.sqlite.all("SELECT entry_id, text, is_protected FROM transcript_entries ORDER BY position");
|
|
973
|
+
}
|
|
974
|
+
listContentBlocksForGrep() {
|
|
975
|
+
return this.sqlite.all(`
|
|
976
|
+
SELECT
|
|
977
|
+
blocks.entry_id,
|
|
978
|
+
blocks.block_index,
|
|
979
|
+
blocks.text,
|
|
980
|
+
entries.is_protected AS entry_protected,
|
|
981
|
+
blocks.is_protected AS block_protected,
|
|
982
|
+
(
|
|
983
|
+
SELECT COUNT(*)
|
|
984
|
+
FROM transcript_content_blocks sibling
|
|
985
|
+
WHERE sibling.entry_id = blocks.entry_id
|
|
986
|
+
) AS block_count
|
|
987
|
+
FROM transcript_content_blocks blocks
|
|
988
|
+
JOIN transcript_entries entries ON entries.entry_id = blocks.entry_id
|
|
989
|
+
ORDER BY entries.position, blocks.block_index
|
|
990
|
+
`);
|
|
991
|
+
}
|
|
992
|
+
getEntryForRead(entryId) {
|
|
993
|
+
return this.sqlite.get("SELECT entry_id, role, is_protected, token_estimate, text FROM transcript_entries WHERE entry_id = ?", entryId);
|
|
994
|
+
}
|
|
995
|
+
getContentBlockForRead(entryId, blockIndex) {
|
|
996
|
+
return this.sqlite.get(`
|
|
997
|
+
SELECT
|
|
998
|
+
blocks.entry_id,
|
|
999
|
+
blocks.block_index,
|
|
1000
|
+
blocks.type,
|
|
1001
|
+
blocks.token_estimate,
|
|
1002
|
+
blocks.text,
|
|
1003
|
+
entries.is_protected AS entry_protected,
|
|
1004
|
+
blocks.is_protected AS block_protected,
|
|
1005
|
+
(
|
|
1006
|
+
SELECT COUNT(*)
|
|
1007
|
+
FROM transcript_content_blocks sibling
|
|
1008
|
+
WHERE sibling.entry_id = blocks.entry_id
|
|
1009
|
+
) AS block_count
|
|
1010
|
+
FROM transcript_content_blocks blocks
|
|
1011
|
+
JOIN transcript_entries entries ON entries.entry_id = blocks.entry_id
|
|
1012
|
+
WHERE blocks.entry_id = ? AND blocks.block_index = ?
|
|
1013
|
+
`, entryId, blockIndex);
|
|
1014
|
+
}
|
|
1015
|
+
getGrepScanTextLength(target) {
|
|
1016
|
+
const table = target === "entry" ? "transcript_entries" : "transcript_content_blocks";
|
|
1017
|
+
const row = this.sqlite.get(`SELECT SUM(LENGTH(text)) AS scan_chars FROM ${table}`);
|
|
1018
|
+
return row?.scan_chars ?? 0;
|
|
1019
|
+
}
|
|
1020
|
+
incrementCallCount() {
|
|
1021
|
+
const next = this.getCallCount() + 1;
|
|
1022
|
+
this.setState("call_count", String(next));
|
|
1023
|
+
return next;
|
|
1024
|
+
}
|
|
1025
|
+
getCallCount() {
|
|
1026
|
+
return Number(this.getState("call_count") ?? "0");
|
|
1027
|
+
}
|
|
1028
|
+
setLastError(message) {
|
|
1029
|
+
this.setState("last_error", message);
|
|
1030
|
+
}
|
|
1031
|
+
clearLastError() {
|
|
1032
|
+
this.sqlite.run("DELETE FROM context_compaction_state WHERE key = ?", "last_error");
|
|
1033
|
+
}
|
|
1034
|
+
getLastError() {
|
|
1035
|
+
return this.getState("last_error");
|
|
1036
|
+
}
|
|
1037
|
+
getState(key) {
|
|
1038
|
+
return this.sqlite.get("SELECT value FROM context_compaction_state WHERE key = ?", key)?.value;
|
|
1039
|
+
}
|
|
1040
|
+
setState(key, value) {
|
|
1041
|
+
this.sqlite.run("INSERT INTO context_compaction_state (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value", key, value);
|
|
1042
|
+
}
|
|
1043
|
+
close() {
|
|
1044
|
+
this.sqlite.close();
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
function createContextDeletionSqliteStore(transcript) {
|
|
1048
|
+
const store = new ContextDeletionSqliteStore(SqliteAdapter.createTransient());
|
|
1049
|
+
store.initialize(transcript);
|
|
1050
|
+
return store;
|
|
1051
|
+
}
|
|
1052
|
+
export function createContextDeletionTool(transcript, options = {}) {
|
|
1053
|
+
const mode = options.mode ?? "standard";
|
|
1054
|
+
const store = createContextDeletionSqliteStore(transcript);
|
|
1055
|
+
let validatedResult;
|
|
1056
|
+
function readTargets() {
|
|
1057
|
+
return store.readTargets();
|
|
1058
|
+
}
|
|
1059
|
+
function applyValidatedTargets(additionalTargets) {
|
|
1060
|
+
const mergedTargets = mergeContextDeletionTargets(readTargets(), additionalTargets);
|
|
1061
|
+
validatedResult = validateContextDeletionRequest(deletionRequestFromTargets(mergedTargets), transcript, { mode });
|
|
1062
|
+
store.replaceTargets(validatedResult.deletedTargets);
|
|
1063
|
+
return validatedResult;
|
|
1064
|
+
}
|
|
1065
|
+
function currentStats() {
|
|
1066
|
+
return validatedResult?.stats ?? computeContextCompactionStats(transcript, readTargets());
|
|
1067
|
+
}
|
|
1068
|
+
function canDeleteProtectedTarget(target) {
|
|
1069
|
+
return canDeleteProtectedTargetInMode(transcript, target, mode);
|
|
1070
|
+
}
|
|
1071
|
+
const tool = {
|
|
1072
|
+
...CONTEXT_DELETE_TOOL,
|
|
1073
|
+
label: "context deletion request",
|
|
1074
|
+
executionMode: "parallel",
|
|
1075
|
+
async execute(_toolCallId, params) {
|
|
1076
|
+
return store.transaction(() => {
|
|
1077
|
+
const callCount = store.incrementCallCount();
|
|
1078
|
+
try {
|
|
1079
|
+
const incomingRequest = contextDeletionRequestFromObject(params, `${CONTEXT_DELETE_TOOL_NAME} arguments`);
|
|
1080
|
+
const incomingValidated = validateContextDeletionRequest(incomingRequest, transcript, { mode });
|
|
1081
|
+
const applied = applyValidatedTargets(incomingValidated.deletedTargets);
|
|
1082
|
+
store.clearLastError();
|
|
1083
|
+
const deletedTargets = readTargets();
|
|
1084
|
+
const details = {
|
|
1085
|
+
deletions: deletionRequestFromTargets(deletedTargets).deletions,
|
|
1086
|
+
deletedTargets,
|
|
1087
|
+
stats: applied.stats,
|
|
1088
|
+
callCount,
|
|
1089
|
+
};
|
|
1090
|
+
const text = `Recorded ${incomingValidated.deletedTargets.length} deletion target(s); ${deletedTargets.length} total validated deletion target(s) are selected. Continue calling ${CONTEXT_DELETE_TOOL_NAME} or ${CONTEXT_GREP_DELETE_TOOL_NAME} for additional deletions, or respond done when finished.`;
|
|
1091
|
+
return createContextDeletionToolResult(text, details);
|
|
1092
|
+
}
|
|
1093
|
+
catch (error) {
|
|
1094
|
+
const message = formatErrorMessage(error);
|
|
1095
|
+
store.setLastError(message);
|
|
1096
|
+
const deletedTargets = readTargets();
|
|
1097
|
+
const details = {
|
|
1098
|
+
deletions: deletionRequestFromTargets(deletedTargets).deletions,
|
|
1099
|
+
deletedTargets,
|
|
1100
|
+
stats: currentStats(),
|
|
1101
|
+
callCount,
|
|
1102
|
+
error: message,
|
|
1103
|
+
};
|
|
1104
|
+
return createContextDeletionToolResult(`Error recording context deletion targets: ${message}. No new deletion targets were applied; continue with a corrected tool call.`, details);
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
},
|
|
1108
|
+
};
|
|
1109
|
+
const grepTool = {
|
|
1110
|
+
...CONTEXT_GREP_DELETE_TOOL,
|
|
1111
|
+
label: "context grep delete",
|
|
1112
|
+
executionMode: "parallel",
|
|
1113
|
+
async execute(_toolCallId, params) {
|
|
1114
|
+
return store.transaction(() => {
|
|
1115
|
+
const callCount = store.incrementCallCount();
|
|
1116
|
+
const pattern = params.pattern;
|
|
1117
|
+
const regex = params.regex === true;
|
|
1118
|
+
const caseSensitive = params.caseSensitive === true;
|
|
1119
|
+
const target = params.target ?? "entry";
|
|
1120
|
+
const maxMatches = params.maxMatches ?? CONTEXT_GREP_DELETE_DEFAULT_MAX_MATCHES;
|
|
1121
|
+
const candidates = [];
|
|
1122
|
+
const matches = [];
|
|
1123
|
+
const skipped = [];
|
|
1124
|
+
const seenTargets = new Set();
|
|
1125
|
+
try {
|
|
1126
|
+
if (regex) {
|
|
1127
|
+
assertSafeRegexScan(store.getGrepScanTextLength(target));
|
|
1128
|
+
}
|
|
1129
|
+
const matcher = createGrepMatcher(pattern, regex, caseSensitive);
|
|
1130
|
+
const currentTargets = readTargets();
|
|
1131
|
+
if (target === "entry") {
|
|
1132
|
+
for (const entry of store.listEntriesForGrep()) {
|
|
1133
|
+
if (!matcher.test(entry.text))
|
|
1134
|
+
continue;
|
|
1135
|
+
const candidate = { kind: "entry", entryId: entry.entry_id };
|
|
1136
|
+
if (entry.is_protected === 1 && !canDeleteProtectedTarget(candidate)) {
|
|
1137
|
+
skipped.push({ entryId: entry.entry_id, target, reason: "protected_entry", text: entry.text });
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
if (currentTargetDeleted(currentTargets, candidate)) {
|
|
1141
|
+
skipped.push({ entryId: entry.entry_id, target, reason: "already_deleted", text: entry.text });
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
addGrepCandidate(candidates, matches, seenTargets, candidate, {
|
|
1145
|
+
entryId: entry.entry_id,
|
|
1146
|
+
target,
|
|
1147
|
+
text: entry.text,
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
else {
|
|
1152
|
+
for (const block of store.listContentBlocksForGrep()) {
|
|
1153
|
+
if (!matcher.test(block.text))
|
|
1154
|
+
continue;
|
|
1155
|
+
const candidate = block.block_count <= 1
|
|
1156
|
+
? { kind: "entry", entryId: block.entry_id }
|
|
1157
|
+
: { kind: "content_block", entryId: block.entry_id, blockIndex: block.block_index };
|
|
1158
|
+
if (block.entry_protected === 1 && !canDeleteProtectedTarget(candidate)) {
|
|
1159
|
+
skipped.push({
|
|
1160
|
+
entryId: block.entry_id,
|
|
1161
|
+
target,
|
|
1162
|
+
blockIndex: block.block_index,
|
|
1163
|
+
reason: "protected_entry",
|
|
1164
|
+
text: block.text,
|
|
1165
|
+
});
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
if (block.block_protected === 1 && !canDeleteProtectedTarget(candidate)) {
|
|
1169
|
+
skipped.push({
|
|
1170
|
+
entryId: block.entry_id,
|
|
1171
|
+
target,
|
|
1172
|
+
blockIndex: block.block_index,
|
|
1173
|
+
reason: "protected_block",
|
|
1174
|
+
text: block.text,
|
|
1175
|
+
});
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
if (currentTargetDeleted(currentTargets, candidate)) {
|
|
1179
|
+
skipped.push({
|
|
1180
|
+
entryId: block.entry_id,
|
|
1181
|
+
target: candidate.kind,
|
|
1182
|
+
...(candidate.kind === "content_block" ? { blockIndex: candidate.blockIndex } : {}),
|
|
1183
|
+
reason: "already_deleted",
|
|
1184
|
+
text: block.text,
|
|
1185
|
+
});
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
addGrepCandidate(candidates, matches, seenTargets, candidate, {
|
|
1189
|
+
entryId: block.entry_id,
|
|
1190
|
+
target: candidate.kind,
|
|
1191
|
+
...(candidate.kind === "content_block" ? { blockIndex: candidate.blockIndex } : {}),
|
|
1192
|
+
text: block.text,
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
let applied;
|
|
1197
|
+
if (params.expectedMatchCount !== undefined && candidates.length !== params.expectedMatchCount) {
|
|
1198
|
+
skipped.push({ reason: "expected_match_count_mismatch" });
|
|
1199
|
+
}
|
|
1200
|
+
else if (candidates.length > maxMatches) {
|
|
1201
|
+
skipped.push({ reason: "max_matches_exceeded" });
|
|
1202
|
+
}
|
|
1203
|
+
else if (candidates.length > 0) {
|
|
1204
|
+
applied = applyValidatedTargets(candidates);
|
|
1205
|
+
}
|
|
1206
|
+
store.clearLastError();
|
|
1207
|
+
const deletedTargets = readTargets();
|
|
1208
|
+
const details = {
|
|
1209
|
+
pattern,
|
|
1210
|
+
regex,
|
|
1211
|
+
caseSensitive,
|
|
1212
|
+
target,
|
|
1213
|
+
matches,
|
|
1214
|
+
skipped,
|
|
1215
|
+
deletedTargets,
|
|
1216
|
+
stats: applied?.stats ?? currentStats(),
|
|
1217
|
+
callCount,
|
|
1218
|
+
};
|
|
1219
|
+
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}.`;
|
|
1220
|
+
return createContextDeletionToolResult(text, details);
|
|
1221
|
+
}
|
|
1222
|
+
catch (error) {
|
|
1223
|
+
const message = formatErrorMessage(error);
|
|
1224
|
+
store.setLastError(message);
|
|
1225
|
+
const deletedTargets = readTargets();
|
|
1226
|
+
const details = {
|
|
1227
|
+
pattern,
|
|
1228
|
+
regex,
|
|
1229
|
+
caseSensitive,
|
|
1230
|
+
target,
|
|
1231
|
+
matches,
|
|
1232
|
+
skipped,
|
|
1233
|
+
deletedTargets,
|
|
1234
|
+
stats: currentStats(),
|
|
1235
|
+
callCount,
|
|
1236
|
+
error: message,
|
|
1237
|
+
};
|
|
1238
|
+
return createContextDeletionToolResult(`Error applying grep deletion for pattern ${JSON.stringify(pattern)}: ${message}. No new deletion targets were applied; continue with a corrected tool call.`, details);
|
|
1239
|
+
}
|
|
1240
|
+
});
|
|
1241
|
+
},
|
|
1242
|
+
};
|
|
1243
|
+
const searchTool = {
|
|
1244
|
+
...CONTEXT_SEARCH_TRANSCRIPT_TOOL,
|
|
1245
|
+
label: "context transcript search",
|
|
1246
|
+
executionMode: "parallel",
|
|
1247
|
+
async execute(_toolCallId, params) {
|
|
1248
|
+
return store.transaction(() => {
|
|
1249
|
+
const callCount = store.incrementCallCount();
|
|
1250
|
+
const pattern = params.pattern;
|
|
1251
|
+
const regex = params.regex === true;
|
|
1252
|
+
const caseSensitive = params.caseSensitive === true;
|
|
1253
|
+
const target = params.target ?? "entry";
|
|
1254
|
+
const maxMatches = clampInteger(params.maxMatches, CONTEXT_SEARCH_DEFAULT_MAX_MATCHES, 1, CONTEXT_SEARCH_MAX_MATCHES);
|
|
1255
|
+
const contextChars = clampInteger(params.contextChars, CONTEXT_SEARCH_DEFAULT_CONTEXT_CHARS, 0, CONTEXT_SEARCH_MAX_CONTEXT_CHARS);
|
|
1256
|
+
const matches = [];
|
|
1257
|
+
let truncated = false;
|
|
1258
|
+
try {
|
|
1259
|
+
if (regex) {
|
|
1260
|
+
assertSafeRegexScan(store.getGrepScanTextLength(target));
|
|
1261
|
+
}
|
|
1262
|
+
const matcher = createGrepMatcher(pattern, regex, caseSensitive);
|
|
1263
|
+
if (target === "entry") {
|
|
1264
|
+
for (const entry of store.listEntriesForGrep()) {
|
|
1265
|
+
const matchIndex = findMatchIndex(matcher, entry.text);
|
|
1266
|
+
if (matchIndex < 0)
|
|
1267
|
+
continue;
|
|
1268
|
+
if (matches.length >= maxMatches) {
|
|
1269
|
+
truncated = true;
|
|
1270
|
+
break;
|
|
1271
|
+
}
|
|
1272
|
+
matches.push({
|
|
1273
|
+
entryId: entry.entry_id,
|
|
1274
|
+
target,
|
|
1275
|
+
matchIndex,
|
|
1276
|
+
snippet: snippetForMatch(entry.text, matchIndex, contextChars),
|
|
1277
|
+
protected: entry.is_protected === 1,
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
else {
|
|
1282
|
+
for (const block of store.listContentBlocksForGrep()) {
|
|
1283
|
+
const matchIndex = findMatchIndex(matcher, block.text);
|
|
1284
|
+
if (matchIndex < 0)
|
|
1285
|
+
continue;
|
|
1286
|
+
if (matches.length >= maxMatches) {
|
|
1287
|
+
truncated = true;
|
|
1288
|
+
break;
|
|
1289
|
+
}
|
|
1290
|
+
matches.push({
|
|
1291
|
+
entryId: block.entry_id,
|
|
1292
|
+
target,
|
|
1293
|
+
blockIndex: block.block_index,
|
|
1294
|
+
matchIndex,
|
|
1295
|
+
snippet: snippetForMatch(block.text, matchIndex, contextChars),
|
|
1296
|
+
protected: block.entry_protected === 1 || block.block_protected === 1,
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
store.clearLastError();
|
|
1301
|
+
const details = {
|
|
1302
|
+
pattern,
|
|
1303
|
+
regex,
|
|
1304
|
+
caseSensitive,
|
|
1305
|
+
target,
|
|
1306
|
+
matches,
|
|
1307
|
+
truncated,
|
|
1308
|
+
callCount,
|
|
1309
|
+
};
|
|
1310
|
+
const text = `Found ${matches.length}${truncated ? "+" : ""} ${target} match(es) for ${JSON.stringify(pattern)}. Use ${CONTEXT_READ_ENTRY_TOOL_NAME} with small maxChars to inspect exact content before deleting.`;
|
|
1311
|
+
return createContextDeletionToolResult(text, details);
|
|
1312
|
+
}
|
|
1313
|
+
catch (error) {
|
|
1314
|
+
const message = formatErrorMessage(error);
|
|
1315
|
+
store.setLastError(message);
|
|
1316
|
+
const details = {
|
|
1317
|
+
pattern,
|
|
1318
|
+
regex,
|
|
1319
|
+
caseSensitive,
|
|
1320
|
+
target,
|
|
1321
|
+
matches,
|
|
1322
|
+
truncated,
|
|
1323
|
+
callCount,
|
|
1324
|
+
error: message,
|
|
1325
|
+
};
|
|
1326
|
+
return createContextDeletionToolResult(`Error searching transcript for ${JSON.stringify(pattern)}: ${message}. Try a literal pattern or narrower query.`, details);
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1329
|
+
},
|
|
1330
|
+
};
|
|
1331
|
+
const readEntryTool = {
|
|
1332
|
+
...CONTEXT_READ_ENTRY_TOOL,
|
|
1333
|
+
label: "context read entry",
|
|
1334
|
+
executionMode: "parallel",
|
|
1335
|
+
async execute(_toolCallId, params) {
|
|
1336
|
+
return store.transaction(() => {
|
|
1337
|
+
const callCount = store.incrementCallCount();
|
|
1338
|
+
const offset = clampInteger(params.offset, 0, 0, Number.MAX_SAFE_INTEGER);
|
|
1339
|
+
const maxChars = clampInteger(params.maxChars, CONTEXT_READ_ENTRY_DEFAULT_MAX_CHARS, 1, CONTEXT_READ_ENTRY_MAX_CHARS);
|
|
1340
|
+
try {
|
|
1341
|
+
const row = params.blockIndex === undefined
|
|
1342
|
+
? store.getEntryForRead(params.entryId)
|
|
1343
|
+
: store.getContentBlockForRead(params.entryId, params.blockIndex);
|
|
1344
|
+
if (!row) {
|
|
1345
|
+
throw new Error(params.blockIndex === undefined
|
|
1346
|
+
? `Unknown transcript entry: ${params.entryId}`
|
|
1347
|
+
: `Unknown transcript content block: ${params.entryId}:${params.blockIndex}`);
|
|
1348
|
+
}
|
|
1349
|
+
const text = row.text;
|
|
1350
|
+
const slice = textSlice(text, offset, maxChars);
|
|
1351
|
+
store.clearLastError();
|
|
1352
|
+
const details = {
|
|
1353
|
+
entryId: params.entryId,
|
|
1354
|
+
...(params.blockIndex === undefined ? {} : { blockIndex: params.blockIndex }),
|
|
1355
|
+
offset,
|
|
1356
|
+
maxChars,
|
|
1357
|
+
totalChars: text.length,
|
|
1358
|
+
text: slice,
|
|
1359
|
+
truncatedBefore: offset > 0,
|
|
1360
|
+
truncatedAfter: offset + maxChars < text.length,
|
|
1361
|
+
callCount,
|
|
1362
|
+
};
|
|
1363
|
+
const textResult = `Read ${slice.length} of ${text.length} characters from ${params.blockIndex === undefined ? params.entryId : `${params.entryId}:${params.blockIndex}`}. Keep reads small; increase offset for the next slice if needed.`;
|
|
1364
|
+
return createContextDeletionToolResult(textResult, details);
|
|
1365
|
+
}
|
|
1366
|
+
catch (error) {
|
|
1367
|
+
const message = formatErrorMessage(error);
|
|
1368
|
+
store.setLastError(message);
|
|
1369
|
+
const details = {
|
|
1370
|
+
entryId: params.entryId,
|
|
1371
|
+
...(params.blockIndex === undefined ? {} : { blockIndex: params.blockIndex }),
|
|
1372
|
+
offset,
|
|
1373
|
+
maxChars,
|
|
1374
|
+
totalChars: 0,
|
|
1375
|
+
text: "",
|
|
1376
|
+
truncatedBefore: false,
|
|
1377
|
+
truncatedAfter: false,
|
|
1378
|
+
callCount,
|
|
1379
|
+
error: message,
|
|
1380
|
+
};
|
|
1381
|
+
return createContextDeletionToolResult(`Error reading transcript entry: ${message}`, details);
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
},
|
|
1385
|
+
};
|
|
1386
|
+
return {
|
|
1387
|
+
tool,
|
|
1388
|
+
grepTool,
|
|
1389
|
+
searchTool,
|
|
1390
|
+
readEntryTool,
|
|
1391
|
+
tools: [tool, grepTool, searchTool, readEntryTool],
|
|
1392
|
+
getDeletionRequest: () => deletionRequestFromTargets(readTargets()),
|
|
1393
|
+
getValidatedResult: () => validatedResult,
|
|
1394
|
+
getLastError: () => store.getLastError(),
|
|
1395
|
+
getCallCount: () => store.getCallCount(),
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
export function parseContextDeletionRequest(text) {
|
|
1399
|
+
const stripped = stripJsonFence(text);
|
|
1400
|
+
let parsed;
|
|
1401
|
+
try {
|
|
1402
|
+
parsed = JSON.parse(stripped);
|
|
1403
|
+
}
|
|
1404
|
+
catch (error) {
|
|
1405
|
+
throw new Error(`Failed to parse context deletion request JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
1406
|
+
}
|
|
1407
|
+
return contextDeletionRequestFromObject(parsed, "Context deletion request JSON");
|
|
1408
|
+
}
|
|
1409
|
+
function isContextDeleteToolCall(content) {
|
|
1410
|
+
return content.type === "toolCall" && content.name === CONTEXT_DELETE_TOOL_NAME;
|
|
1411
|
+
}
|
|
1412
|
+
function textContentFromResponse(response) {
|
|
1413
|
+
return response.content
|
|
1414
|
+
.filter((content) => content.type === "text")
|
|
1415
|
+
.map((content) => content.text)
|
|
1416
|
+
.join("\n");
|
|
1417
|
+
}
|
|
1418
|
+
export function parseContextDeletionResponse(response) {
|
|
1419
|
+
const toolCalls = response.content.filter(isContextDeleteToolCall);
|
|
1420
|
+
if (toolCalls.length > 1) {
|
|
1421
|
+
throw new Error(`Context compaction assistant called ${CONTEXT_DELETE_TOOL_NAME} more than once`);
|
|
1422
|
+
}
|
|
1423
|
+
const toolCall = toolCalls[0];
|
|
1424
|
+
if (toolCall) {
|
|
1425
|
+
return contextDeletionRequestFromObject(toolCall.arguments, `${CONTEXT_DELETE_TOOL_NAME} arguments`);
|
|
1426
|
+
}
|
|
1427
|
+
const textContent = textContentFromResponse(response);
|
|
1428
|
+
if (textContent.trim().length === 0) {
|
|
1429
|
+
throw new Error(`Context compaction assistant did not call ${CONTEXT_DELETE_TOOL_NAME}`);
|
|
1430
|
+
}
|
|
1431
|
+
return parseContextDeletionRequest(textContent);
|
|
1432
|
+
}
|
|
1433
|
+
function truncateForPrompt(text, maxChars) {
|
|
1434
|
+
if (text.length <= maxChars)
|
|
1435
|
+
return text;
|
|
1436
|
+
return `${text.slice(0, maxChars)}\n[... ${text.length - maxChars} more characters omitted from context compaction prompt]`;
|
|
1437
|
+
}
|
|
1438
|
+
function transcriptEntryFilePayload(entry) {
|
|
1439
|
+
return {
|
|
1440
|
+
entryId: entry.entryId,
|
|
1441
|
+
entryType: entry.entryType,
|
|
1442
|
+
role: entry.role,
|
|
1443
|
+
protected: entry.protected,
|
|
1444
|
+
tokenEstimate: entry.tokenEstimate,
|
|
1445
|
+
toolCallIds: entry.toolCallIds,
|
|
1446
|
+
toolResultFor: entry.toolResultFor,
|
|
1447
|
+
text: entry.text,
|
|
1448
|
+
contentBlocks: entry.contentBlocks.map((block) => ({
|
|
1449
|
+
blockIndex: block.blockIndex,
|
|
1450
|
+
type: block.type,
|
|
1451
|
+
protected: block.protected,
|
|
1452
|
+
toolCallId: block.toolCallId,
|
|
1453
|
+
tokenEstimate: block.tokenEstimate,
|
|
1454
|
+
text: block.text,
|
|
1455
|
+
})),
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
function writeContextCompactionTranscriptFile(transcript) {
|
|
1459
|
+
const directory = mkdtempSync(join(tmpdir(), "atomic-context-transcript-"));
|
|
1460
|
+
const path = join(directory, "transcript.jsonl");
|
|
1461
|
+
const lines = transcript.entries
|
|
1462
|
+
.filter((entry) => !isExcludedFromLlmContext(entry.message))
|
|
1463
|
+
.map((entry) => JSON.stringify(transcriptEntryFilePayload(entry)));
|
|
1464
|
+
writeFileSync(path, `${lines.join("\n")}\n`, "utf8");
|
|
1465
|
+
return {
|
|
1466
|
+
path,
|
|
1467
|
+
cleanup: () => rmSync(directory, { recursive: true, force: true }),
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
function contextCompactionTranscriptManifest(transcript, transcriptFilePath) {
|
|
1471
|
+
const eligibleEntries = transcript.entries.filter((entry) => !isExcludedFromLlmContext(entry.message));
|
|
1472
|
+
const selectedEntryIds = new Set();
|
|
1473
|
+
const selectedEntries = [];
|
|
1474
|
+
const addEntry = (entry) => {
|
|
1475
|
+
if (selectedEntryIds.has(entry.entryId) || selectedEntries.length >= CONTEXT_MANIFEST_MAX_ENTRIES)
|
|
1476
|
+
return;
|
|
1477
|
+
selectedEntryIds.add(entry.entryId);
|
|
1478
|
+
selectedEntries.push(entry);
|
|
1479
|
+
};
|
|
1480
|
+
for (const entry of eligibleEntries.filter((entry) => entry.protected)) {
|
|
1481
|
+
addEntry(entry);
|
|
1482
|
+
}
|
|
1483
|
+
for (const entry of [...eligibleEntries]
|
|
1484
|
+
.filter((entry) => !entry.protected)
|
|
1485
|
+
.sort((left, right) => right.tokenEstimate - left.tokenEstimate)) {
|
|
1486
|
+
addEntry(entry);
|
|
1487
|
+
}
|
|
1488
|
+
selectedEntries.sort((left, right) => eligibleEntries.indexOf(left) - eligibleEntries.indexOf(right));
|
|
1489
|
+
return {
|
|
1490
|
+
transcriptFilePath,
|
|
1491
|
+
transcriptFileFormat: "jsonl: one compactable transcript entry per line with full text and contentBlocks text",
|
|
1492
|
+
totalEntries: eligibleEntries.length,
|
|
1493
|
+
manifestEntries: selectedEntries.length,
|
|
1494
|
+
omittedEntries: Math.max(0, eligibleEntries.length - selectedEntries.length),
|
|
1495
|
+
tokensBefore: transcript.tokensBefore,
|
|
1496
|
+
protectedEntryIds: transcript.protectedEntryIds,
|
|
1497
|
+
entries: selectedEntries.map((entry) => ({
|
|
1498
|
+
entryId: entry.entryId,
|
|
1499
|
+
role: entry.role,
|
|
1500
|
+
protected: entry.protected,
|
|
1501
|
+
tokenEstimate: entry.tokenEstimate,
|
|
1502
|
+
toolCallIds: entry.toolCallIds,
|
|
1503
|
+
toolResultFor: entry.toolResultFor,
|
|
1504
|
+
contentBlockCount: entry.contentBlocks.length,
|
|
1505
|
+
contentBlocks: entry.contentBlocks.map((block) => ({
|
|
1506
|
+
blockIndex: block.blockIndex,
|
|
1507
|
+
type: block.type,
|
|
1508
|
+
protected: block.protected,
|
|
1509
|
+
toolCallId: block.toolCallId,
|
|
1510
|
+
tokenEstimate: block.tokenEstimate,
|
|
1511
|
+
})),
|
|
1512
|
+
preview: truncateForPrompt(entry.text, CONTEXT_MANIFEST_PREVIEW_CHARS),
|
|
1513
|
+
})),
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
function contextCompactionModePrompt(mode) {
|
|
1517
|
+
if (mode === "critical_overflow") {
|
|
1518
|
+
return `\n<critical-overflow-mode>\nThe previous model request overflowed its context window. This is a critical LRU-style compaction pass. First delete stale unprotected context. If that is not enough, you may also delete the earliest protected entries or protected content shown in the manifest, especially old user/custom/summary context, while preserving recent entries, unresolved errors, failed commands, and enough task-bearing context for the assistant to continue.\n</critical-overflow-mode>`;
|
|
1519
|
+
}
|
|
1520
|
+
return `\n<standard-mode>\nDo not delete entries or content blocks marked protected. Protected context is only eligible during critical overflow recovery, not during standard compaction.\n</standard-mode>`;
|
|
1521
|
+
}
|
|
1522
|
+
export function buildContextCompactionPrompt(transcript, transcriptFilePath = "<transcript file will be written during context compaction>", mode = "standard") {
|
|
1523
|
+
return `${CONTEXT_COMPACTION_FIXED_PROMPT}${contextCompactionModePrompt(mode)}\n\n<transcript-file>\n${transcriptFilePath}\n</transcript-file>\n\n<context-manifest>\n${JSON.stringify(contextCompactionTranscriptManifest(transcript, transcriptFilePath), null, 2)}\n</context-manifest>`;
|
|
1524
|
+
}
|
|
1525
|
+
function createContextCompactionAssistantMessage(model, content, stopReason, errorMessage) {
|
|
1526
|
+
return {
|
|
1527
|
+
role: "assistant",
|
|
1528
|
+
content,
|
|
1529
|
+
api: model.api,
|
|
1530
|
+
provider: model.provider,
|
|
1531
|
+
model: model.id,
|
|
1532
|
+
usage: {
|
|
1533
|
+
input: 0,
|
|
1534
|
+
output: 0,
|
|
1535
|
+
cacheRead: 0,
|
|
1536
|
+
cacheWrite: 0,
|
|
1537
|
+
totalTokens: 0,
|
|
1538
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
1539
|
+
},
|
|
1540
|
+
stopReason,
|
|
1541
|
+
...(errorMessage !== undefined ? { errorMessage } : {}),
|
|
1542
|
+
timestamp: Date.now(),
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
function createContextCompactionStopStream(model, text) {
|
|
1546
|
+
const stream = createAssistantMessageEventStream();
|
|
1547
|
+
queueMicrotask(() => {
|
|
1548
|
+
const message = createContextCompactionAssistantMessage(model, [{ type: "text", text }], "stop");
|
|
1549
|
+
stream.push({ type: "done", reason: "stop", message });
|
|
1550
|
+
stream.end(message);
|
|
1551
|
+
});
|
|
1552
|
+
return stream;
|
|
1553
|
+
}
|
|
1554
|
+
function isContextCompactionOverflowError(model, errorMessage) {
|
|
1555
|
+
return isContextOverflow(createContextCompactionAssistantMessage(model, [], "error", errorMessage), model.contextWindow);
|
|
1556
|
+
}
|
|
1557
|
+
export function getLowestContextCompactionThinkingLevel(model) {
|
|
1558
|
+
const supportedLevels = getSupportedThinkingLevels(model);
|
|
1559
|
+
for (const level of CONTEXT_COMPACTION_THINKING_LEVEL_ORDER) {
|
|
1560
|
+
if (supportedLevels.includes(level))
|
|
1561
|
+
return level;
|
|
1562
|
+
}
|
|
1563
|
+
return "off";
|
|
1564
|
+
}
|
|
1565
|
+
async function runContextDeletionAssistant(transcript, model, apiKey, headers, signal, mode = "standard") {
|
|
1566
|
+
const maxTokens = Math.min(4096, model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY);
|
|
1567
|
+
if (signal?.aborted) {
|
|
1568
|
+
throw new Error("Context compaction failed: Request was aborted");
|
|
1569
|
+
}
|
|
1570
|
+
const transcriptFile = writeContextCompactionTranscriptFile(transcript);
|
|
1571
|
+
const promptMessage = {
|
|
1572
|
+
role: "user",
|
|
1573
|
+
content: [{ type: "text", text: buildContextCompactionPrompt(transcript, transcriptFile.path, mode) }],
|
|
1574
|
+
timestamp: Date.now(),
|
|
1575
|
+
};
|
|
1576
|
+
const deletionTool = createContextDeletionTool(transcript, { mode });
|
|
1577
|
+
const effectiveThinkingLevel = getLowestContextCompactionThinkingLevel(model);
|
|
1578
|
+
let compactionTurnCount = 0;
|
|
1579
|
+
const agent = new Agent({
|
|
1580
|
+
initialState: {
|
|
1581
|
+
systemPrompt: CONTEXT_COMPACTION_SYSTEM_PROMPT,
|
|
1582
|
+
model,
|
|
1583
|
+
thinkingLevel: effectiveThinkingLevel,
|
|
1584
|
+
tools: deletionTool.tools,
|
|
1585
|
+
},
|
|
1586
|
+
toolExecution: "parallel",
|
|
1587
|
+
streamFn: async (requestModel, context, streamOptions) => {
|
|
1588
|
+
compactionTurnCount += 1;
|
|
1589
|
+
if (compactionTurnCount > CONTEXT_COMPACTION_MAX_TURNS) {
|
|
1590
|
+
return createContextCompactionStopStream(requestModel, `Reached the context compaction turn cap (${CONTEXT_COMPACTION_MAX_TURNS}); using the deletions recorded so far.`);
|
|
1591
|
+
}
|
|
1592
|
+
return streamSimple(requestModel, context, {
|
|
1593
|
+
...streamOptions,
|
|
1594
|
+
maxTokens,
|
|
1595
|
+
apiKey,
|
|
1596
|
+
headers: headers ?? streamOptions?.headers,
|
|
1597
|
+
});
|
|
1598
|
+
},
|
|
1599
|
+
});
|
|
1600
|
+
const abortOnSignal = () => agent.abort();
|
|
1601
|
+
signal?.addEventListener("abort", abortOnSignal, { once: true });
|
|
1602
|
+
try {
|
|
1603
|
+
await agent.prompt(promptMessage);
|
|
1604
|
+
}
|
|
1605
|
+
finally {
|
|
1606
|
+
signal?.removeEventListener("abort", abortOnSignal);
|
|
1607
|
+
transcriptFile.cleanup();
|
|
1608
|
+
}
|
|
1609
|
+
if (signal?.aborted) {
|
|
1610
|
+
throw new Error("Context compaction failed: Request was aborted");
|
|
1611
|
+
}
|
|
1612
|
+
if (agent.state.errorMessage) {
|
|
1613
|
+
if (isContextCompactionOverflowError(model, agent.state.errorMessage)) {
|
|
1614
|
+
return {
|
|
1615
|
+
validatedResult: deletionTool.getValidatedResult(),
|
|
1616
|
+
lastToolError: deletionTool.getLastError(),
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
throw new Error(`Context compaction failed: ${agent.state.errorMessage}`);
|
|
1620
|
+
}
|
|
1621
|
+
if (deletionTool.getCallCount() === 0) {
|
|
1622
|
+
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})`);
|
|
1623
|
+
}
|
|
1624
|
+
return {
|
|
1625
|
+
validatedResult: deletionTool.getValidatedResult(),
|
|
1626
|
+
lastToolError: deletionTool.getLastError(),
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
export async function contextCompact(preparation, model, apiKey, headers, signal, _thinkingLevel, mode = preparation.mode ?? "standard") {
|
|
1630
|
+
const { validatedResult, lastToolError } = await runContextDeletionAssistant(preparation.transcript, model, apiKey, headers, signal, mode);
|
|
1631
|
+
if (!validatedResult || validatedResult.deletedTargets.length === 0) {
|
|
1632
|
+
throw new Error(lastToolError ? `No safe context deletions proposed; last deletion tool error: ${lastToolError}` : "No safe context deletions proposed");
|
|
1633
|
+
}
|
|
1634
|
+
return validatedResult;
|
|
1635
|
+
}
|
|
1636
|
+
//# sourceMappingURL=context-compaction.js.map
|